From b2077a45f303ffc926f6f8121826031800b7df3c Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Thu, 20 Aug 2026 10:36:29 +0530 Subject: [PATCH 1/7] Two builds for the SuperDocs engineer round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use-cases/Priyanshu2425/quota-aware-agent — an agent that reads its remaining allowance before it plans, sizes the work to fit, and degrades in plain language instead of dying halfway through someone's document. MCP server, three tools, offline test suite. use-cases/Priyanshu2425/word-doc-repair — Salvage. A .docx that will not open, opened the way Word will not: what survives is recovered, a valid file is rebuilt around it, and what did not come back is named. It never claims a complete repair, and a test fails the build if it ever does. Both run offline with no key. The SuperDocs path is an optional second pass in each, and every failure on it degrades back to the local result. Co-Authored-By: Claude Opus 5 (1M context) --- use-cases/Priyanshu2425/.gitignore | 9 + .../Priyanshu2425/quota-aware-agent/LICENSE | 21 + .../Priyanshu2425/quota-aware-agent/README.md | 251 + .../Priyanshu2425/quota-aware-agent/demo.py | 80 + .../quota-aware-agent/pyproject.toml | 27 + .../quota_aware_agent/__init__.py | 9 + .../quota_aware_agent/agent.py | 432 ++ .../quota_aware_agent/budget.py | 172 + .../quota_aware_agent/client.py | 283 ++ .../quota_aware_agent/idempotency.py | 170 + .../quota_aware_agent/mcp_server.py | 296 ++ .../quota_aware_agent/policy.py | 138 + .../quota_aware_agent/receipt.py | 200 + .../quota-aware-agent/screenshot.png | Bin 0 -> 135373 bytes .../quota-aware-agent/tests/fake.py | 122 + .../quota-aware-agent/tests/test_agent.py | 227 + .../tests/test_governance.py | 287 ++ .../tests/test_mcp_server.py | 163 + .../Priyanshu2425/word-doc-repair/DESIGN.md | 75 + .../Priyanshu2425/word-doc-repair/LICENSE | 21 + .../Priyanshu2425/word-doc-repair/README.md | 206 + .../Priyanshu2425/word-doc-repair/cli.py | 108 + .../word-doc-repair/docrepair/__init__.py | 4 + .../word-doc-repair/docrepair/docx.py | 317 ++ .../word-doc-repair/docrepair/engine.py | 371 ++ .../word-doc-repair/docrepair/media.py | 243 + .../word-doc-repair/docrepair/salvage.py | 254 + .../docrepair/styled_export.py | 133 + .../docrepair/superdocs_client.py | 273 ++ .../word-doc-repair/docrepair/web.py | 138 + .../word-doc-repair/frontend/.gitignore | 2 + .../frontend/bundle-manifest.json | 4 + .../word-doc-repair/frontend/index.html | 17 + .../frontend/package-lock.json | 4093 +++++++++++++++++ .../word-doc-repair/frontend/package.json | 33 + .../frontend/scripts/manifest.mjs | 40 + .../frontend/scripts/report-shot.mjs | 11 + .../word-doc-repair/frontend/src/App.tsx | 257 ++ .../frontend/src/components/Preview.tsx | 32 + .../frontend/src/components/Report.tsx | 91 + .../frontend/src/lib/repair.ts | 104 + .../word-doc-repair/frontend/src/main.tsx | 10 + .../word-doc-repair/frontend/src/styles.css | 373 ++ .../frontend/src/test/App.test.tsx | 265 ++ .../src/test/fixtures/bad-characters.json | 66 + .../src/test/fixtures/empty-body.json | 47 + .../frontend/src/test/fixtures/healthy.json | 60 + .../src/test/fixtures/illustrated.json | 78 + .../frontend/src/test/fixtures/index.json | 52 + .../test/fixtures/missing-content-types.json | 65 + .../test/fixtures/missing-document-part.json | 35 + .../src/test/fixtures/not-a-word-file.json | 31 + .../test/fixtures/truncated-illustrated.json | 83 + .../frontend/src/test/fixtures/truncated.json | 65 + .../src/test/fixtures/unclosed-tags.json | 67 + .../frontend/src/test/server.ts | 75 + .../frontend/src/test/setup.ts | 7 + .../word-doc-repair/frontend/tsconfig.json | 21 + .../word-doc-repair/frontend/vite.config.ts | 10 + .../word-doc-repair/frontend/vitest.config.ts | 13 + .../manual-test/fixtures/00-healthy.docx | Bin 0 -> 2058 bytes .../fixtures/01-truncated-download.docx | Bin 0 -> 1234 bytes .../fixtures/02-missing-content-types.docx | Bin 0 -> 1695 bytes .../fixtures/03-unclosed-tags.docx | Bin 0 -> 1948 bytes .../fixtures/04-bad-characters.docx | Bin 0 -> 2059 bytes .../05-missing-document-part-SHOULD-FAIL.docx | Bin 0 -> 1421 bytes .../06-not-a-word-file-SHOULD-FAIL.docx | 1 + .../fixtures/07-empty-body-SHOULD-FAIL.docx | Bin 0 -> 1641 bytes .../word-doc-repair/manual-test/index.html | 779 ++++ .../manual-test/make_fixtures.py | 46 + .../word-doc-repair/pyproject.toml | 22 + .../word-doc-repair/screenshot.png | Bin 0 -> 221273 bytes .../word-doc-repair/static/index.html | 67 + .../word-doc-repair/tests/__init__.py | 0 .../word-doc-repair/tests/broken.py | 160 + .../tests/test_frontend_fixtures.py | 89 + .../word-doc-repair/tests/test_repair.py | 327 ++ .../tests/test_styled_export.py | 172 + 78 files changed, 12800 insertions(+) create mode 100644 use-cases/Priyanshu2425/.gitignore create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/LICENSE create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/README.md create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/demo.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/pyproject.toml create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/__init__.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/agent.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/budget.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/client.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/idempotency.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/mcp_server.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/policy.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/receipt.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/screenshot.png create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/tests/fake.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/tests/test_agent.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/tests/test_governance.py create mode 100644 use-cases/Priyanshu2425/quota-aware-agent/tests/test_mcp_server.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/DESIGN.md create mode 100644 use-cases/Priyanshu2425/word-doc-repair/LICENSE create mode 100644 use-cases/Priyanshu2425/word-doc-repair/README.md create mode 100644 use-cases/Priyanshu2425/word-doc-repair/cli.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/docrepair/__init__.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/docrepair/docx.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/docrepair/engine.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/docrepair/media.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/docrepair/salvage.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/docrepair/styled_export.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/docrepair/superdocs_client.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/docrepair/web.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/.gitignore create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/bundle-manifest.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/index.html create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/package-lock.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/package.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/scripts/manifest.mjs create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/scripts/report-shot.mjs create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/App.tsx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Preview.tsx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Report.tsx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/lib/repair.ts create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/main.tsx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/styles.css create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/App.test.tsx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/bad-characters.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/empty-body.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/healthy.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/illustrated.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/index.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/missing-content-types.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/missing-document-part.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/not-a-word-file.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/truncated-illustrated.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/truncated.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/unclosed-tags.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/server.ts create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/setup.ts create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/tsconfig.json create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/vite.config.ts create mode 100644 use-cases/Priyanshu2425/word-doc-repair/frontend/vitest.config.ts create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/00-healthy.docx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/01-truncated-download.docx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/02-missing-content-types.docx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/03-unclosed-tags.docx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/04-bad-characters.docx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/05-missing-document-part-SHOULD-FAIL.docx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/06-not-a-word-file-SHOULD-FAIL.docx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/07-empty-body-SHOULD-FAIL.docx create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/index.html create mode 100644 use-cases/Priyanshu2425/word-doc-repair/manual-test/make_fixtures.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/pyproject.toml create mode 100644 use-cases/Priyanshu2425/word-doc-repair/screenshot.png create mode 100644 use-cases/Priyanshu2425/word-doc-repair/static/index.html create mode 100644 use-cases/Priyanshu2425/word-doc-repair/tests/__init__.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/tests/broken.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/tests/test_frontend_fixtures.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/tests/test_repair.py create mode 100644 use-cases/Priyanshu2425/word-doc-repair/tests/test_styled_export.py diff --git a/use-cases/Priyanshu2425/.gitignore b/use-cases/Priyanshu2425/.gitignore new file mode 100644 index 00000000..404e2f18 --- /dev/null +++ b/use-cases/Priyanshu2425/.gitignore @@ -0,0 +1,9 @@ +# Build artefacts from running the two projects in this folder. Kept here +# rather than at the repository root, because CONTRIBUTING.md asks each builder +# to change nothing outside their own folder. +__pycache__/ +*.pyc +.pytest_cache/ +node_modules/ +*.tsbuildinfo +.DS_Store diff --git a/use-cases/Priyanshu2425/quota-aware-agent/LICENSE b/use-cases/Priyanshu2425/quota-aware-agent/LICENSE new file mode 100644 index 00000000..cb00ec18 --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Priyanshu Semwal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/use-cases/Priyanshu2425/quota-aware-agent/README.md b/use-cases/Priyanshu2425/quota-aware-agent/README.md new file mode 100644 index 00000000..4d92ab2b --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/README.md @@ -0,0 +1,251 @@ +# Quota-aware agent + +**Assigned build A · band S1 · MCP/API · SuperDocs** + +An agent that reads its remaining allowance **before** it plans, sizes the work +to fit, and when the work does not fit it degrades and says so in a sentence a +person can read — instead of starting a job it cannot finish and dying halfway +through someone's document. + +![The agent degrading, twice](screenshot.png) + +## The problem, stated precisely + +An agent that plans without checking its allowance starts work it cannot +finish. The obvious fix — "check the balance first" — runs into something the +SuperDocs documentation is explicit about: + +> the `/v1/users/me/usage` and `/v1/users/me/limits` endpoints belong to the +> web-app account surface and accept web-app session tokens only — they reject +> `sk_` / `lce_` API keys with a `401` + +**So an API-key agent cannot read its balance on demand.** There is no endpoint +to poll. What exists is `GET /v1/agents/whoami` once at the start, and the +`usage` block that rides on **every** chat response afterwards. You learn your +balance as a side effect of doing work, not in advance of it. + +This build takes that seriously rather than papering over it. `Balance` carries +an `authoritative` flag, and every report says which kind of number it used: + +``` +Starting allowance: 3 operations (confirmed). +Allowance now: 1 operation (confirmed). +``` + +A number inferred between calls prints as `(estimated)` and can never be +presented as a live read. That distinction is a tested property, not a comment. + +## What it does when the work does not fit + +Three behaviours, each one tested: + +**It refuses to start.** If nothing fits, nothing is uploaded and nothing is +billed. There is no partially-edited document to clean up. + +``` +Did not start: none of the requested work fits inside the remaining allowance. +Nothing was uploaded and nothing was billed. +``` + +**It degrades by severity.** If some of the work fits, the highest-severity +work goes first and the rest is named — not silently dropped. + +``` +The full request would cost about 4 operation(s). Sized to fit: 50 of 100 +changed sections (2 of 4 operations). Deferred 2 lower-severity change(s) to +stay inside the remaining allowance. +Left undone: terms, footer. These were not started, so nothing is half-applied. +``` + +**It holds a reserve.** The last operation is never spent on new edits, so the +work already done can always be exported. Exports are free, so this costs the +user nothing and guarantees they end up with a file. + +**It does not pay twice for the same edit.** SuperDocs has no idempotency on +billable writes — there is no idempotency key on `POST /v1/chat/async` — so a +rerun after an interrupted batch is charged again for work already in the +document. An operation ledger, keyed on what the call *is* rather than on an id +the caller assigned, is written **before** each billable call and consulted +before each one: + +``` +'terms': an earlier run already applied this. Not repeated, and not billed again. +``` + +The hard case is handled as a hard case. A call that was sent, and whose outcome +this process died before learning, is **neither repeated nor forgotten** — +retrying it might be charged twice and might apply the same edit twice, and +skipping it might leave the work undone. There is no safe automatic answer, so +it is reported: + +``` +'dates': an earlier run started this and never learned whether it finished. +Not retried — that might be charged twice and might apply the same edit twice. +``` + +**It says what it spent, and whether that adds up.** `--receipt` prints the line +items and reconciles them against the allowance — and when they disagree, it +says so rather than picking a side: + +``` +DOES IT ADD UP? +2 operation(s) charged, and the allowance moved by 2. These agree. +``` + +A `~` marks an operation we *believe* was charged on a call that returned no +usage block, and a `?` marks a balance we inferred. Confirmed and estimated +figures are never added together, because one number that is part measurement +and part belief tells the reader nothing about which part is which. + +### The stopping rule, stated once + +1. `quota_exhausted` on any response — the platform's own signal, and the only + authoritative one. Stop immediately. Our arithmetic never overrules it. +2. The reserve floor — never spend the last operation. +3. `--sample N` — the small-sample bound, because anything that loops needs one. + +All three live in `Policy`, which is frozen, so a run ends under the policy it +began with. And every one of them **explains what it was protecting** when it +fires. That is not decoration: a limit whose only feedback is *"I stopped"* +trains the person who set it to raise the number until it stops firing, which is +the same as not having it. + +## Use it as an MCP server + +The card is band S1 · MCP and the user is an agent, so the agent is reachable +as one. Three tools, in the order an agent actually needs them: + +| Tool | Costs | What it answers | +|---|---|---| +| `check_allowance` | free | What have I got? The one authoritative read — plus anything an earlier run left unresolved. | +| `plan_work` | free | What fits inside it — without doing any of it. | +| `run_work` | billable | Do the part that fits; say what was left out, what was already paid for, and what it cost. | + +**Every result carries a `budget` block**, so a calling agent never has to spend +a turn asking what is left before it decides what to do. It carries the +remaining operations, whether that number is authoritative or inferred, the +reserve, and what is therefore spendable on new edits. Surfacing the budget into +the caller's own context is the cheapest useful thing this build does: an agent +that knows what is left can decide earlier, do less, or escalate — and an agent +that has to ask cannot. + +``` +pip install -e ".[mcp]" +export SUPERDOCS_API_KEY=your-key-here +python3 -m quota_aware_agent.mcp_server # stdio +``` + +Claude Code: + +``` +claude mcp add quota-aware-agent \ + --env SUPERDOCS_API_KEY=your-key-here \ + -- python3 -m quota_aware_agent.mcp_server +``` + +**`plan_work` exists as its own tool on purpose.** An agent that has to commit +to work before it can find out what fits is the exact failure this build is +about, so the plan is readable for free and changes nothing. A test asserts that +planning makes no billable call, and another asserts the plan and the run agree +about what fits — if they could disagree, only one of them would be tested. + +Every tool is a thin wrapper over `QuotaAwareAgent`; no planning logic lives in +the MCP layer. `dispatch()` calls the whole surface without a protocol client, +which is what keeps it testable offline. + +## The four-call contract + +`upload` → `edit instruction` → `approve` → `export`, built first and built +completely, before any depth. + +| Call | Endpoint | Billing | +|---|---|---| +| Upload | `POST /v1/documents/upload` | free | +| Edit instruction | `POST /v1/chat/async` (`approval_mode: ask_every_time`) | 1 op per 25 sections | +| Approve | `POST /v1/chat/{session_id}/approve` | denied changes are never billed | +| Export | `POST /v1/documents/export` | free | + +### The three named traps, all handled + +**Trap 1 · the double parse.** A `proposed_change_batch` envelope carries its +payload as a JSON-encoded *string* in `content`. It needs a second parse. Miss +it and you get diff cards where every field reads `undefined` — silently, with +no error. `parse_proposed_changes` does it, and the test asserts the change id +actually reached the approve call, because a test that only checks "it ran" +would pass while approving nothing. + +**Trap 2 · silence is not a crash.** A job on a large document can run for +minutes with no output. `poll_job` treats quiet as processing and gives up only +at an explicit deadline — and when it does, it says the deadline was *ours*: + +> job job-1 was still 'in_progress' after 4s. That is a deadline this client +> imposed, not a platform failure — the job may still be running. + +**Trap 3 · the allowance.** The whole point of this build. Small-sample mode +and the stopping rule above. + +## Run it + +No key, no network, nothing to install: + +``` +python3 -m pytest # 50 tests, offline +python3 demo.py # allowance is plentiful — everything runs +python3 demo.py --scenario tight # not enough — it degrades and explains +python3 demo.py --scenario broke # nothing fits — it refuses to start +python3 demo.py --sample 2 # small-sample mode +python3 demo.py --receipt # the line items, and whether they add up +``` + +Against the real API: + +``` +export SUPERDOCS_API_KEY=your-key-here +python3 demo.py --live +``` + +The transport is injected, which is why the tests need no key: the fake +implements the documented response shapes, including the double-parsed +envelope and a `usage` block on every billable response. + +## Shared core — stated plainly + +`quota_aware_agent/budget.py` is shared, unchanged, with **Attest**, the +document-analysis system I built for Problem 01 of the same round, where it +guards the publisher that writes back to SuperDocs. It is vendored here rather +than imported so this build stands alone in the builds repository. + +Reuse is only a shortcut when it is hidden. The same module solving both +problems is the argument that the abstraction is right: "never start work you +cannot finish" is one idea, and an agent sizing a plan and a publisher sizing a +document write are the same shape of problem. + +## Honest limitations + +- **The allowance is not continuously known, and this build does not pretend it + is.** It is authoritative at `whoami` and after each response, and an estimate + in between. Every report labels which. +- **Section counts for planned edits are supplied by the caller, not measured.** + SuperDocs bills per 25 sections *edited*, which is known only after the edit. + A caller who badly underestimates can still overshoot — which is exactly why + `quota_exhausted` is treated as authoritative and the reserve exists. +- **The operation ledger is this client's memory, not the platform's.** It + prevents *this tool* from paying twice. It cannot prevent a different client, + or a person in the web app, from making the same edit — nothing below us + offers idempotency to build that on. +- **A reconciliation needs two authoritative balances.** If either end of the + run was inferred, the receipt says the run cannot be reconciled rather than + reconciling two guesses. +- **It approves every proposed change in the demo.** The approve call supports + per-change denial with feedback and the client exposes it; deciding *which* + changes are worth approving is a different problem from affording them. +- It does not implement the pre-signed upload flow, so it is limited to the + ~20 MB direct-upload ceiling. + +## Credit + +Built by **Priyanshu Semwal** ([@Priyanshu2425](https://github.com/Priyanshu2425)) +for the SuperDocs engineer round, 2026. MIT licensed — see [LICENSE](LICENSE). + +Grounded throughout in the SuperDocs API documentation; where the task brief and +the documentation differed, the documentation won. diff --git a/use-cases/Priyanshu2425/quota-aware-agent/demo.py b/use-cases/Priyanshu2425/quota-aware-agent/demo.py new file mode 100644 index 00000000..6e51d3b9 --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/demo.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Run the agent. With no key it runs against a documented fake, so a reviewer +can see the behaviour without spending an operation. + + python3 demo.py # offline, against the fake + python3 demo.py --scenario tight # not enough allowance: it degrades + python3 demo.py --scenario broke # no allowance: it refuses to start + python3 demo.py --live # real API, needs SUPERDOCS_API_KEY + + --sample N small-sample mode: run at most N steps +""" + +from __future__ import annotations + +import argparse +import os +import sys + +from quota_aware_agent.policy import Policy +from quota_aware_agent import QuotaAwareAgent, Step, SuperDocsClient +from quota_aware_agent.client import HttpTransport + +WORK = [ + Step("figures", "Correct the revenue figures in the summary table.", 25, "critical"), + Step("dates", "Fix the effective dates in section 3.", 25, "high"), + Step("terms", "Align the defined terms with the glossary.", 25, "medium"), + Step("footer", "Tidy the footer and page numbering.", 25, "low"), +] + +SCENARIOS = {"roomy": 500, "tight": 3, "broke": 1} + + +def main(argv=None) -> int: + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--scenario", choices=sorted(SCENARIOS), default="roomy") + p.add_argument("--live", action="store_true", help="use the real API") + p.add_argument("--sample", type=int, default=None, help="run at most N steps") + p.add_argument("--file", default="contract.docx") + p.add_argument("--receipt", action="store_true", + help="print the line items and whether they add up") + a = p.parse_args(argv) + + if a.live: + key = os.environ.get("SUPERDOCS_API_KEY") + if not key: + print("SUPERDOCS_API_KEY is not set. Run without --live to use the fake.", + file=sys.stderr) + return 2 + transport = HttpTransport(key) + sleep = None + print("Running against the real API. Operations will be billed.\n") + else: + from tests.fake import FakeSuperDocs + transport = FakeSuperDocs(remaining=SCENARIOS[a.scenario]) + sleep = lambda s: None + print(f"Offline demo, scenario '{a.scenario}': " + f"{SCENARIOS[a.scenario]} operation(s) of allowance. Nothing is billed.\n") + + client = SuperDocsClient(transport, **({"sleep": sleep} if sleep else {})) + agent = QuotaAwareAgent(client, policy=Policy(reserve=1, max_steps=a.sample)) + + content = b"

Quarterly report

...

" + report = agent.run("demo-session", a.file, content, WORK) + + print(report.plain_language()) + print() + print(f" planned: {report.planned or '-'}") + print(f" completed: {report.completed or '-'}") + print(f" deferred: {report.deferred or '-'}") + print(f" stopped: {report.stop_reason.value}") + if a.receipt: + print() + print(report.receipt.render_text(report.balance_at_start, + report.balance_at_end)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/use-cases/Priyanshu2425/quota-aware-agent/pyproject.toml b/use-cases/Priyanshu2425/quota-aware-agent/pyproject.toml new file mode 100644 index 00000000..80c2bd83 --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "quota-aware-agent" +version = "1.0.0" +description = "An agent that checks its SuperDocs allowance before planning, sizes the work to fit, and degrades in plain language." +requires-python = ">=3.10" +dependencies = [] + +[project.optional-dependencies] +# The MCP surface is optional: the agent, the library and the CLI demo all work +# without it, and the tests skip the protocol check when it is absent. +mcp = ["mcp>=1.0"] +dev = ["pytest"] + +[project.scripts] +quota-aware-agent-mcp = "quota_aware_agent.mcp_server:main" + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["quota_aware_agent*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +addopts = "-q" diff --git a/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/__init__.py b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/__init__.py new file mode 100644 index 00000000..48338352 --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/__init__.py @@ -0,0 +1,9 @@ +from .agent import QuotaAwareAgent, Report, Step +from .budget import Balance, BudgetGuard, Change, Plan, estimate +from .client import HttpTransport, QuotaExhausted, SuperDocsClient, parse_proposed_changes + +__all__ = [ + "QuotaAwareAgent", "Report", "Step", + "Balance", "BudgetGuard", "Change", "Plan", "estimate", + "HttpTransport", "QuotaExhausted", "SuperDocsClient", "parse_proposed_changes", +] diff --git a/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/agent.py b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/agent.py new file mode 100644 index 00000000..d8cdb45f --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/agent.py @@ -0,0 +1,432 @@ +"""An agent that checks its allowance before planning, sizes the work to fit, +and degrades in plain language instead of failing halfway. + +The shape of the problem, from the docs rather than from optimism: + + You cannot read your balance on demand. `/v1/users/me/usage` and `/limits` + reject `sk_` keys with a 401. The agent-key path is `GET /v1/agents/whoami` + once at the start, and after that the `usage` block that rides on every chat + response. So the balance is authoritative at the start and after each call, + and an *estimate* in between. `Balance.authoritative` carries that + distinction so a report can never present a stale number as a live read. + +The consequence worth naming: this agent does not "check remaining allowance" +continuously. It reads it, plans against it, and re-reads it as a side effect +of every call it makes. That is the strongest guarantee the platform allows, +and the report says which number it used. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from .budget import Balance, BudgetGuard, Change, Plan, estimate +from .client import QuotaExhausted, SuperDocsClient +from .idempotency import OperationLedger, State, operation_key +from .policy import Policy, StopReason, WhenItDoesNotFit +from .receipt import Receipt + + +@dataclass +class Step: + """One unit of requested work: an edit instruction plus how many document + sections it is expected to touch. Sections are what SuperDocs bills on -- + one operation per 25 sections edited -- so the plan is priced in sections + and reported in operations.""" + step_id: str + instruction: str + sections: int + severity: str = "medium" + + def as_change(self) -> Change: + return Change(row_id=self.step_id, sections=self.sections, severity=self.severity) + + +@dataclass +class Report: + """What the agent did, in the words it would say to a person.""" + balance_at_start: Balance | None = None + balance_at_end: Balance | None = None + planned: list[str] = field(default_factory=list) + completed: list[str] = field(default_factory=list) + deferred: list[str] = field(default_factory=list) + #: Steps an earlier run already paid for. Not repeated, and not billed. + already_applied: list[str] = field(default_factory=list) + #: Steps an earlier run started and whose outcome nobody ever learned. + #: Neither repeated nor forgotten -- a person has to look at the document. + needs_a_person: list[str] = field(default_factory=list) + ops_spent: int = 0 + stopped_because: str = "" + stop_reason: StopReason = StopReason.COMPLETED + export_warnings: list = field(default_factory=list) + lines: list[str] = field(default_factory=list) + receipt: Receipt = field(default_factory=Receipt) + + def say(self, line: str) -> None: + self.lines.append(line) + + def stop(self, reason: StopReason, line: str = "") -> None: + """Stopping is a rule firing, not a number running out. Recording which + rule is what lets somebody argue with it instead of raising it.""" + self.stop_reason = reason + self.stopped_because = reason.explain() + if line: + self.say(line) + + def reconciliation(self) -> str: + """Whether the line items agree with the allowance. Named, not hidden.""" + return self.receipt.reconcile(self.balance_at_start, + self.balance_at_end).explanation + + def plain_language(self) -> str: + out = list(self.lines) + if self.already_applied: + out.append( + f"Already done by an earlier run, and not repeated: " + f"{', '.join(self.already_applied)}. You were not charged again." + ) + if self.needs_a_person: + out.append( + f"Started by an earlier run and never confirmed: " + f"{', '.join(self.needs_a_person)}. These were not retried, " + "because retrying might be charged twice and might apply the " + "same edit twice. Open the document and check them." + ) + if self.deferred: + out.append( + f"Left undone: {', '.join(self.deferred)}. " + "These were not started, so nothing is half-applied." + ) + if self.balance_at_end: + out.append(f"Allowance now: {self.balance_at_end}.") + return "\n".join(out) + + +class QuotaAwareAgent: + """Reads the allowance, plans to fit it, and stops on the platform's signal. + + The stopping rule, stated once so it is not scattered: + 1. `quota_exhausted` from any response -- the platform's own signal, and + the only authoritative one. Stop immediately. + 2. The reserve floor -- never spend the last `reserve` operations, so + there is always enough left to export the work already done. + 3. `max_steps` -- the small-sample bound. Loops need a stopping rule. + """ + + def __init__(self, client: SuperDocsClient, guard: BudgetGuard | None = None, + policy: Policy | None = None, + ledger: OperationLedger | None = None) -> None: + self._c = client + self._g = guard or BudgetGuard() + self._p = policy or Policy() + # In memory unless a caller hands over a path. A ledger that forgets + # when the process dies is no use for the failure it exists for, so the + # CLI and the MCP server both give it a file. + self._ledger = ledger or OperationLedger() + + @property + def policy(self) -> Policy: + return self._p + + def unresolved_operations(self) -> list[dict]: + """Calls a previous run started and never learned the outcome of. + + Surfaced, never cleaned up. Deciding between "pay again" and "leave the + work undone" is somebody's money and somebody's document, so it is + reported and a person resolves it with `ledger.resolve`. + """ + return [{"key": r.key, "step_id": r.step_id, "session_id": r.session_id} + for r in self._ledger.unresolved()] + + def budget_hint(self) -> dict: + """The allowance, in a shape a calling agent can plan against. + + An underused technique in the cost-governance literature and an obvious + one once stated: an agent that knows what is left can decide earlier, + summarise instead of re-reading, or escalate before it runs out. This + rides back on every tool result rather than needing its own call -- + which matters here, because a call to find out is not free everywhere + and asking is itself a decision an agent should not have to make. + """ + balance = self._g.remaining() + return { + "remaining_operations": balance.ops, + "authoritative": balance.authoritative, + "as_of": balance.as_of, + "reserve": self._p.reserve, + "spendable_on_new_edits": self._p.spendable(balance.ops), + "exhausted": self._g.exhausted, + "policy": self._p.describe(), + "note": ( + "Authoritative at whoami and after every response that carried a " + "usage block; inferred in between, and it says which. Exports " + "are free, which is why the reserve costs you nothing." + ), + } + + # -- planning --------------------------------------------------------- + def read_allowance(self) -> Balance: + """The one moment the number is authoritative before any work begins.""" + r = self._c.whoami() + quota = r.body.get("quota", {}) or {} + remaining = quota.get("remaining") + if remaining is None: + # Never invent a balance. An unknown allowance is planned as zero, + # which degrades to doing nothing and saying why. + return self._g.seed_from_whoami(0) + return self._g.seed_from_whoami(int(remaining), as_of=str(quota.get("resets_at", ""))) + + def plan(self, steps: list[Step]) -> Plan: + """Price the work and fit it to what is left, under the policy.""" + budget = self._p.spendable(self._g.remaining().ops) + steps, _bit = self._p.bound(steps) + plan = self._g.fit([s.as_change() for s in steps], remaining=budget) + if (plan.publish and plan.defer + and self._p.when_it_does_not_fit is WhenItDoesNotFit.REFUSE): + # A caller who would rather have nothing than a subset said so. + return Plan(publish=[], defer=plan.publish + plan.defer, + rationale=StopReason.REFUSED_PARTIAL.explain()) + return plan + + # -- execution -------------------------------------------------------- + def run(self, session_id: str, filename: str, content: bytes, steps: list[Step], + export_format: str = "docx") -> Report: + """Read the allowance, size the work, do what fits, and say what it did. + + The order is the load-bearing part and it is why this reads as four + steps rather than one block: the allowance is read *before* anything is + planned, the plan is priced *before* anything is uploaded, and the export + runs even when the run stopped early — because it is free, which is what + the reserve exists to guarantee. + """ + report = Report(receipt=Receipt(session_id=session_id)) + + start = self._read_and_report_allowance(report) + steps = self._apply_sample_bound(steps, report) + plan = self._price(steps, report) + if not plan.publish: + return self._did_not_start(plan, report) + + # Free per the docs, so it is not priced -- and it happens only after + # everything that could refuse has refused. + self._c.upload(session_id, filename, content) + report.say(f"Uploaded {filename}.") + + self._work(session_id, plan, {s.step_id: s for s in steps}, report) + return self._export(session_id, export_format, start, report) + + # -- the steps of a run, each one its own decision ---------------------- + def _read_and_report_allowance(self, report: Report) -> Balance: + start = self.read_allowance() + report.balance_at_start = start + report.say(f"Starting allowance: {start}.") + return start + + def _apply_sample_bound(self, steps: list[Step], report: Report) -> list[Step]: + bounded, bit = self._p.bound(steps) + if bit: + report.say( + f"Small-sample mode: running {self._p.max_steps} of {len(steps)} " + "requested steps." + ) + return bounded + + def _price(self, steps: list[Step], report: Report) -> Plan: + plan = self.plan(steps) + report.planned = [c.row_id for c in plan.publish] + report.deferred = [c.row_id for c in plan.defer] + needed = estimate([s.as_change() for s in steps]) + report.say( + f"The full request would cost about {needed} operation(s). " + + ("It fits." if plan.complete else plan.rationale) + ) + return plan + + def _did_not_start(self, plan: Plan, report: Report) -> Report: + """Nothing uploaded, nothing billed, and no half-edited document.""" + refused = plan.rationale == StopReason.REFUSED_PARTIAL.explain() + report.stop( + StopReason.REFUSED_PARTIAL if refused else StopReason.NOTHING_FITS, + "Did not start: " + + ("only part of the work fits, and this policy refuses a partial " + "run rather than deliver a subset." + if refused else + "none of the requested work fits inside the remaining allowance.") + + " Nothing was uploaded and nothing was billed.", + ) + report.balance_at_end = self._g.remaining() + return report + + def _work(self, session_id: str, plan: Plan, by_id: dict[str, Step], + report: Report) -> None: + """Each fitted step: instruct, wait, approve — unless it is already done.""" + for change in plan.publish: + step = by_id[change.row_id] + key = operation_key(session_id, step.step_id, step.instruction, + step.sections) + if self._already_settled(key, step, report): + continue + + try: + self._run_step(session_id, step, report, key) + except QuotaExhausted: + report.stop(StopReason.QUOTA_EXHAUSTED) + report.deferred.append(step.step_id) + report.say( + f"Stopped during '{step.step_id}': SuperDocs reported the " + "allowance exhausted. That request still completed; nothing " + "further was attempted." + ) + return + if self._reserve_reached(plan, step, report): + return + + def _already_settled(self, key: str, step: Step, report: Report) -> bool: + """Has this exact call already been paid for, or already been sent? + + SuperDocs has no idempotency on billable writes, so a rerun after a + crash would otherwise be charged again for work already in the document. + """ + prior = self._ledger.get(key) + if prior.state is State.APPLIED: + report.already_applied.append(step.step_id) + report.say( + f"'{step.step_id}': an earlier run already applied this. " + "Not repeated, and not billed again." + ) + return True + if prior.state is State.IN_FLIGHT: + report.needs_a_person.append(step.step_id) + report.say( + f"'{step.step_id}': an earlier run started this and never " + "learned whether it finished. Not retried — that might be " + "charged twice and might apply the same edit twice." + ) + return True + return False + + def _reserve_reached(self, plan: Plan, step: Step, report: Report) -> bool: + if self._g.remaining().ops > self._p.reserve: + return False + remaining_ids = [ + c.row_id for c in plan.publish + if c.row_id not in report.completed and c.row_id != step.step_id + ] + if remaining_ids: + report.stop(StopReason.RESERVE_FLOOR) + report.deferred.extend(remaining_ids) + report.say( + f"Stopped after '{step.step_id}': holding back the last " + f"{self._p.reserve} operation(s) so the work already done " + "can still be exported." + ) + return True + + def _export(self, session_id: str, export_format: str, start: Balance, + report: Report) -> Report: + """Free, so it always runs — including after stopping early. That is the + whole reason the reserve is worth holding.""" + exported = self._c.export(session_id, export_format) + report.export_warnings = self._c.export_warnings(exported) + report.say(f"Exported the document as {export_format} (exports are free).") + if report.export_warnings: + report.say( + f"The export completed with {len(report.export_warnings)} " + "non-fatal warning(s), listed in the report -- the file is usable." + ) + report.balance_at_end = self._g.remaining() + report.ops_spent = max(0, (start.ops if start else 0) - report.balance_at_end.ops) + return report + + def _run_step(self, session_id: str, step: Step, report: Report, + key: str | None = None) -> None: + key = key or operation_key(session_id, step.step_id, step.instruction, + step.sections) + # Written BEFORE the call goes out. If the process dies between these + # two lines the ledger says "started, outcome unknown", which is the + # truth; writing it afterwards would leave no trace of a call that was + # charged. + self._ledger.begin(key, session_id=session_id, step_id=step.step_id) + try: + started = self._c.edit(session_id, step.instruction) + except Exception: + self._ledger.failed(key, "the edit call did not return") + raise + self._reconcile(started, report, billable_ops=estimate([step.as_change()]), + step_id=step.step_id, call="edit") + job_id = started.body.get("job_id") + if not job_id: + self._ledger.applied(key, note="no job id returned") + report.say(f"'{step.step_id}': no job id returned; nothing applied.") + return + + waited = self._c.poll_job( + job_id, + on_wait=lambda s, status: report.say( + f"'{step.step_id}': still {status} after {s:.0f}s -- processing, not stalled." + ) if s and s % 60 == 0 else None, + ) + self._reconcile(waited, report, step_id=step.step_id, call="poll") + + if waited.body.get("status") == "awaiting_approval": + changes = self._pending(waited) + decisions = [{"change_id": c.get("change_id"), "approved": True} for c in changes] + if decisions: + approved = self._c.approve(session_id, job_id, decisions) + self._reconcile(approved, report, step_id=step.step_id, + call="approve") + report.say(f"'{step.step_id}': approved {len(decisions)} proposed change(s).") + + # Approval is ASYNCHRONOUS. The approve call returns ok, then + # the job resumes and applies the change. Exporting before it + # reaches a terminal state returns the document as it was -- + # HTTP 200, a valid file, and the edit silently missing. Verified + # against the live API on 2026-08-19. + settled = self._c.poll_job(job_id, deadline_s=300, interval_s=2) + self._reconcile(settled, report, step_id=step.step_id, + call="poll") + if settled.body.get("status") != "completed": + report.say( + f"'{step.step_id}': the job ended as " + f"'{settled.body.get('status')}' rather than completed; the " + "approved change may not have been applied." + ) + self._ledger.applied(key, job_id=str(job_id)) + report.completed.append(step.step_id) + + @staticmethod + def _pending(job: dict | object) -> list[dict]: + """One implementation, in the client, shared by both builds.""" + from .client import pending_changes + + return pending_changes(job.body if hasattr(job, "body") else job) + + def _reconcile(self, response, report: Report, billable_ops: int = 0, + step_id: str = "", call: str = "") -> None: + usage = response.usage + if not usage: + # A billable call that returned no usage block leaves us guessing. + # Guess, and say that we are guessing. + if billable_ops: + self._g.assume_spent(billable_ops) + if call: + report.receipt.record( + step_id, call, billable=bool(billable_ops), + ops_charged=None, ops_estimated=billable_ops, + balance=self._g.remaining(), + note=("no usage block came back, so this line is what we " + "believe rather than what was stated") + if billable_ops else "", + ) + return + self._g.reconcile( + ops_charged=int(usage.get("ops_charged", 0)), + monthly_remaining=usage.get("monthly_remaining"), + quota_exhausted=bool(usage.get("quota_exhausted", False)), + ) + if call: + report.receipt.record( + step_id, call, billable=bool(billable_ops), + ops_charged=int(usage.get("ops_charged", 0)), + ops_estimated=billable_ops, balance=self._g.remaining(), + ) diff --git a/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/budget.py b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/budget.py new file mode 100644 index 00000000..c4d3b6ce --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/budget.py @@ -0,0 +1,172 @@ +"""BudgetGuard -- never start work you cannot finish. + +SHARED CORE. This module is used unchanged by "Attest", the document-analysis +system built for Problem 01 of the same round, where it guards the publisher. +It is vendored here rather than imported so this build stands alone in the +builds repository. Reuse is only a shortcut when it is hidden, so it is stated +here, in the README, and in the write-up. + +Grounded in the SuperDocs documentation rather than in the task brief +(TASK.md rule 1). Two facts from the docs shape this module: + + * Most requests bill one operation; very large ones bill one per 25 sections + edited. So pricing is done in sections and reported in operations. + * The usage endpoints reject API keys -- `/v1/users/me/usage` and `/limits` + accept web-app session tokens only. From an API-key context the balance is + read off the `usage` block that rides on every chat response. You therefore + learn your balance as a side effect of doing work, not in advance of it. + +That second point is why `Balance` carries `authoritative`. Between calls the +number is an estimate and says so. `quota_exhausted` on a response is the +authoritative stop signal. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field + +SECTIONS_PER_OP = 25 + + +@dataclass(frozen=True) +class Balance: + ops: int + authoritative: bool + as_of: str = "" + + def __str__(self) -> str: + qualifier = "confirmed" if self.authoritative else "estimated" + unit = "operation" if self.ops == 1 else "operations" + return f"{self.ops} {unit} ({qualifier})" + + +@dataclass(frozen=True) +class Change: + row_id: str + sections: int + severity: str = "medium" + + +_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3} + + +@dataclass +class Plan: + publish: list[Change] = field(default_factory=list) + defer: list[Change] = field(default_factory=list) + rationale: str = "" + + @property + def ops_to_publish(self) -> int: + return estimate(self.publish) + + @property + def complete(self) -> bool: + return not self.defer + + +def estimate(changes: list[Change]) -> int: + """Operations a set of changes will bill. + + Zero changes cost nothing. Anything else costs at least one operation, + then one more per 25 sections -- which is the documented accounting, not + a division. + """ + sections = sum(c.sections for c in changes) + if sections <= 0: + return 0 + return max(1, math.ceil(sections / SECTIONS_PER_OP)) + + +class BudgetGuard: + def __init__(self, seed: Balance | None = None) -> None: + self._balance = seed or Balance(ops=0, authoritative=False) + self._exhausted = False + + def seed_from_whoami(self, remaining_ops: int, as_of: str = "") -> Balance: + """The agent whoami call does accept an agent key, so this is the one + moment the balance is genuinely authoritative before work begins.""" + self._balance = Balance(remaining_ops, authoritative=True, as_of=as_of) + return self._balance + + def assume_spent(self, ops: int) -> Balance: + """No usage block came back on a call we believe was billable. + + The docs say usage rides on every chat response; the async endpoints + empirically return none (verified 2026-08-19). So the balance is + decremented by our own estimate and, crucially, **stops being + authoritative** -- continuing to print "confirmed" over a number the + platform never confirmed is the exact bluff `authoritative` exists to + prevent. + """ + self._balance = Balance( + max(0, self._balance.ops - max(0, ops)), authoritative=False, + as_of=self._balance.as_of, + ) + return self._balance + + def reconcile(self, ops_charged: int, monthly_remaining: int | None, quota_exhausted: bool, + as_of: str = "") -> Balance: + """Called with the `usage` block from every response.""" + self._exhausted = quota_exhausted + if monthly_remaining is not None: + self._balance = Balance(monthly_remaining, authoritative=True, as_of=as_of) + else: + self._balance = Balance( + max(0, self._balance.ops - ops_charged), authoritative=False, as_of=as_of + ) + return self._balance + + def remaining(self) -> Balance: + return self._balance + + @property + def exhausted(self) -> bool: + return self._exhausted + + def fit(self, changes: list[Change], remaining: int | None = None) -> Plan: + """Pure given a budget number. Publishes what fits, highest severity + first, defers the rest, and says so in a sentence a person can read.""" + budget = self._balance.ops if remaining is None else remaining + needed = estimate(changes) + + if self._exhausted or budget <= 0: + # These are different situations and must not be reported as one. + # "Exhausted" is the platform's own signal; a zero budget can also + # mean a reserve is being held back so finished work stays usable. + reason = ( + "the allowance is exhausted" + if self._exhausted + else "there are no operations available to spend" + ) + return Plan( + publish=[], + defer=list(changes), + rationale=( + f"Started nothing: {reason}. " + f"{len(changes)} change(s) are queued and named in the run report." + ), + ) + + if needed <= budget: + return Plan(publish=list(changes), defer=[], rationale="") + + ordered = sorted( + changes, key=lambda c: (_SEVERITY_ORDER.get(c.severity, 99), -c.sections) + ) + publish: list[Change] = [] + for change in ordered: + if estimate(publish + [change]) <= budget: + publish.append(change) + deferred = [c for c in changes if c not in publish] + + pub_sections = sum(c.sections for c in publish) + all_sections = sum(c.sections for c in changes) + rationale = ( + f"Sized to fit: {pub_sections} of {all_sections} changed sections " + f"({estimate(publish)} of {needed} operations). Deferred " + f"{len(deferred)} lower-severity change(s) to stay inside the " + "remaining allowance." + ) + return Plan(publish=publish, defer=deferred, rationale=rationale) diff --git a/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/client.py b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/client.py new file mode 100644 index 00000000..e9f5182e --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/client.py @@ -0,0 +1,283 @@ +"""The four-call contract: upload, edit instruction, approve, export. + +Everything here is grounded in the SuperDocs documentation, and the three traps +the docs name by hand are handled explicitly rather than discovered later: + + Trap 1 -- the double parse. A `proposed_change_batch` envelope carries its + payload as a JSON-encoded *string* in `content`. `parse_proposed_changes` + does the second parse. Skipping it is the documented single most common + reason integrators see empty diff cards with every field undefined. + + Trap 2 -- silence is not a crash. A job on a large document can run for + minutes with no visible progress. `poll_job` treats a long quiet run as + still processing and only gives up at an explicit deadline, reporting how + long it waited rather than calling it a failure. + + Trap 3 -- the allowance. Every response carries a `usage` block; nothing here + spends without handing that block back to the caller to reconcile. + +Transport is injected. `HttpTransport` is the real one; the tests pass a fake, +which is why this file has no network dependency and the suite needs no key. +""" + +from __future__ import annotations + +import json +import time +from dataclasses import dataclass, field +from typing import Any, Callable, Protocol + +BASE = "https://api.superdocs.app" + +# Terminal and non-terminal job states, from the docs' job lifecycle. +_TERMINAL = {"completed", "failed", "cancelled"} +_NEEDS_HUMAN = "awaiting_approval" + + +class Transport(Protocol): + def request(self, method: str, path: str, **kw: Any) -> "Response": ... + + +@dataclass +class Response: + status: int + body: dict + headers: dict = field(default_factory=dict) + + @property + def usage(self) -> dict: + """The usage block rides on every chat response. It is the only way to + read the balance from an API-key context -- the account usage endpoints + reject `sk_` keys with a 401.""" + return self.body.get("usage", {}) or {} + + +class SuperDocsError(RuntimeError): + def __init__(self, status: int, body: Any) -> None: + super().__init__(f"SuperDocs returned {status}: {body}") + self.status = status + self.body = body + + +class QuotaExhausted(SuperDocsError): + """Raised only when the platform says so. Never inferred from our own count.""" + + +def _encode_multipart(files: dict, fields: dict) -> tuple[bytes, str]: + """Build a multipart/form-data body from {name: (filename, bytes)} plus + plain fields. Returns (body, content_type). + + A fixed boundary would collide with content that happens to contain it, so + it is derived from the payload -- deterministic for a given body, which + keeps requests reproducible, and vanishingly unlikely to appear inside it. + """ + import hashlib + + digest = hashlib.sha256() + for name, (filename, content) in sorted(files.items()): + digest.update(name.encode()) + digest.update(str(filename).encode()) + digest.update(content if isinstance(content, bytes) else str(content).encode()) + boundary = "----attest" + digest.hexdigest()[:24] + + out = bytearray() + for name, value in fields.items(): + out += f"--{boundary}\r\n".encode() + out += f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode() + out += str(value).encode("utf-8") + b"\r\n" + for name, (filename, content) in files.items(): + if isinstance(content, str): + content = content.encode("utf-8") + out += f"--{boundary}\r\n".encode() + out += (f'Content-Disposition: form-data; name="{name}"; ' + f'filename="{filename}"\r\n').encode() + out += f"Content-Type: {_guess_type(filename)}\r\n\r\n".encode() + out += content + b"\r\n" + out += f"--{boundary}--\r\n".encode() + return bytes(out), f"multipart/form-data; boundary={boundary}" + + +def _guess_type(filename: str) -> str: + import mimetypes + + return mimetypes.guess_type(str(filename))[0] or "application/octet-stream" + + +class HttpTransport: + """Real transport. Imported lazily so the package needs no HTTP library + installed to run its tests.""" + + def __init__(self, api_key: str, base: str = BASE, timeout: float = 300.0) -> None: + self._key = api_key + self._base = base + # ~300s is the platform gateway timeout for synchronous requests. + self._timeout = timeout + + def request(self, method: str, path: str, **kw: Any) -> Response: + import urllib.error + import urllib.request + + url = self._base + path + headers = {"Authorization": f"Bearer {self._key}"} + data = None + if "files" in kw: + # Upload is multipart/form-data, not JSON. Encoded here rather than + # with a library because this package has no dependencies -- and + # because getting it wrong is invisible: the request still sends, + # and the API answers 422 for a field it never received. + data, content_type = _encode_multipart(kw["files"], kw.get("data", {})) + headers["Content-Type"] = content_type + elif "json" in kw: + data = json.dumps(kw["json"]).encode() + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=self._timeout) as r: + raw = r.read() + body = json.loads(raw) if raw and r.headers.get_content_type() == "application/json" else {"raw": raw} + return Response(r.status, body, dict(r.headers)) + except urllib.error.HTTPError as e: + raw = e.read() + try: + body = json.loads(raw) + except Exception: + body = {"raw": raw.decode(errors="replace")} + return Response(e.code, body, dict(e.headers or {})) + + +def pending_changes(job_body: dict) -> list[dict]: + """Read the proposed changes off a job, whatever shape they arrive in. + + Two shapes exist and both are real: + * `GET /v1/jobs/{id}` returns `metadata.pending_changes` as a plain LIST + of change dicts. Verified against the live API 2026-08-19. + * The SSE `proposed_change_batch` event delivers an envelope whose + `content` is a JSON-encoded STRING needing a second parse. + + This lives in the client because both builds need it, and the copy that had + it written separately handled only the envelope -- so it crashed on the + shape the polling path actually returns. + """ + meta = job_body.get("metadata") or {} + pending = meta.get("pending_changes") + if pending is None: + for event in meta.get("intermediate_responses", []) or []: + if event.get("type") == "proposed_change_batch": + return parse_proposed_changes(event) + return [] + if isinstance(pending, list): + return list(pending) + if isinstance(pending, str): + return parse_proposed_changes({"content": pending}) + return parse_proposed_changes(pending) + + +def parse_proposed_changes(envelope: dict) -> list[dict]: + """Trap 1. The batch arrives as a JSON string inside `content`. + + A single-change turn still arrives as a one-element `changes[]`, so this + always returns a list and never special-cases the singular form. + """ + content = envelope.get("content") + if content is None: + return list(envelope.get("changes", [])) + batch = json.loads(content) if isinstance(content, str) else content + return list(batch.get("changes", [])) + + +class SuperDocsClient: + def __init__(self, transport: Transport, sleep: Callable[[float], None] = time.sleep) -> None: + self._t = transport + self._sleep = sleep + + def _check(self, r: Response) -> Response: + if r.status >= 400: + raise SuperDocsError(r.status, r.body) + if r.usage.get("quota_exhausted"): + # The current request still completed; further billable ones will not. + raise QuotaExhausted(r.status, r.body) + return r + + # --- call 0: the one authoritative balance read available to an agent key. + def whoami(self) -> Response: + return self._check(self._t.request("GET", "/v1/agents/whoami")) + + # --- call 1 of the contract: upload. + def upload(self, session_id: str, filename: str, content: bytes) -> Response: + return self._check( + self._t.request( + "POST", "/v1/documents/upload", + files={"file": (filename, content)}, data={"session_id": session_id}, + ) + ) + + # --- call 2: the edit instruction. + def edit(self, session_id: str, message: str, approval_mode: str = "ask_every_time") -> Response: + return self._check( + self._t.request( + "POST", "/v1/chat/async", + json={"session_id": session_id, "message": message, "approval_mode": approval_mode}, + ) + ) + + def job(self, job_id: str) -> Response: + return self._check(self._t.request("GET", f"/v1/jobs/{job_id}")) + + def poll_job(self, job_id: str, deadline_s: float = 600.0, interval_s: float = 2.0, + on_wait: Callable[[float, str], None] | None = None) -> Response: + """Trap 2. Silence is still processing. + + Returns as soon as the job is terminal *or* is waiting on a human. Gives + up only at an explicit deadline, and says how long it waited -- a slow + job is never reported as a crash. + """ + waited = 0.0 + while True: + r = self.job(job_id) + status = r.body.get("status", "") + if status in _TERMINAL or status == _NEEDS_HUMAN: + return r + if waited >= deadline_s: + raise TimeoutError( + f"job {job_id} was still '{status}' after {waited:.0f}s. " + "That is a deadline this client imposed, not a platform failure -- " + "the job may still be running." + ) + if on_wait: + on_wait(waited, status) + self._sleep(interval_s) + waited += interval_s + + # --- call 3: approve, item by item. + def approve(self, session_id: str, job_id: str, decisions: list[dict]) -> Response: + """`decisions` is a list of {change_id, approved, feedback?}. Sent as a + batch so a mixed approve/deny turn is one request, not one per change.""" + return self._check( + self._t.request( + "POST", f"/v1/chat/{session_id}/approve", + json={"job_id": job_id, "approved": True, "changes": decisions}, + ) + ) + + # --- call 4: export. Free, per the docs, and so never priced. + def export(self, session_id: str, fmt: str = "docx") -> Response: + r = self._check( + self._t.request("POST", "/v1/documents/export", + json={"session_id": session_id, "format": fmt}) + ) + return r + + @staticmethod + def export_warnings(r: Response) -> list: + """Exports can succeed with non-fatal issues, carried base64-encoded in + `X-Export-Warnings`. Surfaced rather than swallowed -- a dropped field + code is exactly the kind of thing a user should be told about.""" + import base64 + + header = r.headers.get("X-Export-Warnings") + if not header: + return [] + try: + return json.loads(base64.b64decode(header)) + except Exception: + return [] diff --git a/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/idempotency.py b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/idempotency.py new file mode 100644 index 00000000..f0790d49 --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/idempotency.py @@ -0,0 +1,170 @@ +"""Not paying twice for the same edit after a run was interrupted. + +Grounded in the documentation rather than in optimism: **SuperDocs has no +idempotency on billable writes.** There is no idempotency key on +`POST /v1/chat/async`, so the same edit instruction sent twice is two +operations and, quite possibly, the same change applied twice. Nothing below us +prevents that, which means the only place it can be prevented is here. + +From the research, the failure this exists for, in the words of somebody it +happened to: *"I reran it from nineteen. But I got the boundary wrong — I think +I redid two or three that were already done. Paid for those twice."* + +Three states, and the third is the interesting one: + + ``applied`` the call completed and we saw it complete. Never repeated. + ``in flight`` we sent the call and the process died before we learned the + outcome. **Also never repeated** — and reported, because it + needs a person. Retrying might be charged twice and might + apply the same edit twice; skipping might leave the work + undone. There is no safe automatic answer, and inventing one + would be this build guessing with somebody's money. + ``unknown`` never attempted. Do it. + +The ledger is content-addressed on what the call *is* — session, step, +instruction and size — rather than on a caller-supplied id, because the caller +who got the boundary wrong was working from ids they assigned themselves. +""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import asdict, dataclass +from enum import Enum +from pathlib import Path + + +class State(str, Enum): + UNKNOWN = "unknown" + IN_FLIGHT = "in_flight" + APPLIED = "applied" + FAILED = "failed" + + +@dataclass(frozen=True) +class Record: + key: str + state: State + session_id: str = "" + step_id: str = "" + ops_charged: int | None = None + job_id: str = "" + note: str = "" + + @property + def repeatable(self) -> bool: + """Whether sending this call again is safe. + + `FAILED` is repeatable because a call that failed before it was accepted + was not charged. `IN_FLIGHT` is not, and that is the whole point. + """ + return self.state in (State.UNKNOWN, State.FAILED) + + +def operation_key(session_id: str, step_id: str, instruction: str, + sections: int) -> str: + """A stable name for one billable call. + + Over what the call *does*, so a rerun that renumbers its steps still + recognises work it already paid for. The session is included because the + same instruction against a different document is a different operation. + """ + h = hashlib.sha256() + for part in (session_id, step_id, instruction.strip(), str(sections)): + h.update(part.encode("utf-8")) + h.update(b"\x1f") + return h.hexdigest() + + +class OperationLedger: + """What this account has already been charged for, and what is unresolved. + + Backed by a file when given one, because the failure it exists for is a + process that died — a ledger that lives only in memory forgets exactly when + it is needed. Appended to rather than rewritten, so a second death during a + write cannot lose earlier lines. + """ + + def __init__(self, path: str | Path | None = None) -> None: + self._path = Path(path) if path else None + self._records: dict[str, Record] = {} + if self._path and self._path.exists(): + self._load() + + # -- reads ------------------------------------------------------------- + def get(self, key: str) -> Record: + return self._records.get(key, Record(key=key, state=State.UNKNOWN)) + + def records(self) -> list[Record]: + return list(self._records.values()) + + def unresolved(self) -> list[Record]: + """Calls that were started and whose outcome was never learned. + + These are what a person has to look at. They are surfaced rather than + cleaned up, because cleaning them up means choosing between paying twice + and leaving work undone, and that is not our choice to make. + """ + return [r for r in self._records.values() if r.state is State.IN_FLIGHT] + + # -- writes ------------------------------------------------------------ + def begin(self, key: str, *, session_id: str = "", step_id: str = "") -> Record: + """Written *before* the call goes out. That ordering is the guarantee. + + If it were written afterwards, a process that died mid-call would leave + no trace of the call at all, and the rerun would repeat it — which is + the exact failure this module exists to prevent. + """ + return self._put(Record(key=key, state=State.IN_FLIGHT, + session_id=session_id, step_id=step_id)) + + def applied(self, key: str, *, ops_charged: int | None = None, + job_id: str = "") -> Record: + prior = self.get(key) + return self._put(Record(key=key, state=State.APPLIED, + session_id=prior.session_id, step_id=prior.step_id, + ops_charged=ops_charged, job_id=job_id)) + + def failed(self, key: str, note: str = "") -> Record: + prior = self.get(key) + return self._put(Record(key=key, state=State.FAILED, + session_id=prior.session_id, + step_id=prior.step_id, note=note)) + + def resolve(self, key: str, *, applied: bool, note: str = "") -> Record: + """A person looked at the document and told us what happened.""" + prior = self.get(key) + return self._put(Record( + key=key, state=State.APPLIED if applied else State.FAILED, + session_id=prior.session_id, step_id=prior.step_id, + job_id=prior.job_id, + note=note or "resolved by a person after an interrupted run", + )) + + # -- storage ----------------------------------------------------------- + def _put(self, record: Record) -> Record: + self._records[record.key] = record + if self._path: + self._path.parent.mkdir(parents=True, exist_ok=True) + with self._path.open("a", encoding="utf-8") as fh: + fh.write(json.dumps({**asdict(record), + "state": record.state.value}) + "\n") + return record + + def _load(self) -> None: + for line in self._path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + raw = json.loads(line) + raw["state"] = State(raw["state"]) + # Later lines win: the file is an append-only log, so the last + # word about a key is the current one. + self._records[raw["key"]] = Record(**raw) + except (ValueError, TypeError, KeyError): + # A half-written final line is what an interrupted run leaves + # behind. Skipping it loses nothing a complete line did not + # already say; refusing to open the file would lose everything. + continue diff --git a/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/mcp_server.py b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/mcp_server.py new file mode 100644 index 00000000..24677f31 --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/mcp_server.py @@ -0,0 +1,296 @@ +"""The MCP surface. Band S1 says MCP, and agents are who this serves. + +Everything here is a thin wrapper over `QuotaAwareAgent`, which is already +tested. No planning logic lives in this file -- if it did, the MCP path and the +library path could disagree about what fits inside an allowance, and only one +of them would be tested. + +The three tools mirror how an agent actually works: + + check_allowance -- what have I got? (free; the one authoritative read) + plan_work -- what fits inside it? (free; spends nothing, decides nothing) + run_work -- do the part that fits, and tell me what you left out. + +`plan_work` exists as a separate tool on purpose. An agent that must commit to +work before it can find out what fits is the exact failure this build is about, +so the plan is readable without spending anything. +""" + +from __future__ import annotations + +import json +import os + +from .agent import QuotaAwareAgent, Step +from .budget import estimate +from .client import HttpTransport, SuperDocsClient +from .idempotency import OperationLedger +from .policy import Policy + +SERVER_NAME = "quota-aware-agent" + +#: Where the operation ledger is kept between runs. It has to outlive the +#: process, because the failure it prevents -- paying twice for the same edit -- +#: is caused by a process that died. Overridable so two agents driving different +#: accounts do not share one. +LEDGER_PATH = os.environ.get( + "QUOTA_AWARE_AGENT_LEDGER", + os.path.expanduser("~/.quota-aware-agent/operations.jsonl"), +) + + +def _ledger() -> OperationLedger: + return OperationLedger(LEDGER_PATH) + + +def _client() -> SuperDocsClient: + key = os.environ.get("SUPERDOCS_API_KEY") + if not key: + raise RuntimeError( + "SUPERDOCS_API_KEY is not set. This server needs a SuperDocs API key; " + "an agent account can be created with POST /v1/agents/signup." + ) + return SuperDocsClient(HttpTransport(key)) + + +def _steps(raw: list[dict]) -> list[Step]: + out = [] + for i, s in enumerate(raw, 1): + out.append(Step( + step_id=str(s.get("id") or f"step-{i}"), + instruction=str(s["instruction"]), + sections=int(s.get("sections", 1)), + severity=str(s.get("severity", "medium")), + )) + return out + + +# -- the three operations, as plain functions so they are testable without MCP -- + +def check_allowance() -> dict: + agent = QuotaAwareAgent(_client(), ledger=_ledger()) + balance = agent.read_allowance() + unresolved = agent.unresolved_operations() + return { + "remaining_operations": balance.ops, + "authoritative": balance.authoritative, + "resets_at": balance.as_of, + "note": ("Authoritative right now. It stops being authoritative as soon as " + "work begins: the usage endpoints reject API keys, so the balance " + "is only readable as a side effect of making calls."), + # Surfaced here rather than only inside run_work, because an agent that + # is about to plan needs to know an earlier run left something in doubt + # before it decides what to do, not after. + "started_and_never_confirmed": unresolved, + "budget": agent.budget_hint(), + } + + +def plan_work(steps: list[dict], reserve: int = 1) -> dict: + """Free. Spends nothing, changes nothing, and answers 'what fits?'.""" + agent = QuotaAwareAgent(_client(), policy=Policy(reserve=reserve)) + balance = agent.read_allowance() + parsed = _steps(steps) + plan = agent.plan(parsed) + by_id = {s.step_id: s for s in parsed} + return { + "remaining_operations": balance.ops, + "full_request_costs": estimate([s.as_change() for s in parsed]), + "reserved": reserve, + "will_run": [c.row_id for c in plan.publish], + "will_defer": [c.row_id for c in plan.defer], + "fits_completely": plan.complete, + "explanation": plan.rationale or "The whole request fits inside the allowance.", + "instructions": {k: v.instruction for k, v in by_id.items()}, + "policy": agent.policy.describe(), + "budget": agent.budget_hint(), + } + + +def run_work(session_id: str, filename: str, document_html: str, + steps: list[dict], reserve: int = 1, max_steps: int | None = None, + export_format: str = "docx") -> dict: + agent = QuotaAwareAgent(_client(), policy=Policy(reserve=reserve, max_steps=max_steps), + ledger=_ledger()) + report = agent.run(session_id, filename, document_html.encode("utf-8"), + _steps(steps), export_format=export_format) + return { + "completed": report.completed, + "deferred": report.deferred, + "stopped_because": report.stopped_because or None, + "plain_language": report.plain_language(), + "allowance_at_start": { + "operations": report.balance_at_start.ops, + "authoritative": report.balance_at_start.authoritative, + } if report.balance_at_start else None, + "allowance_at_end": { + "operations": report.balance_at_end.ops, + "authoritative": report.balance_at_end.authoritative, + } if report.balance_at_end else None, + "export_warnings": report.export_warnings, + "already_applied_by_an_earlier_run": report.already_applied, + "started_and_never_confirmed": report.needs_a_person, + "stop_reason": report.stop_reason.value, + "why_it_stopped": report.stop_reason.explain(), + # The line items and whether they add up. An agent that has to derive + # its own spend from a transcript will get it wrong the same way a + # person does. + "receipt": report.receipt.as_dict(report.balance_at_start, + report.balance_at_end), + "budget": agent.budget_hint(), + } + + +TOOLS = [ + { + "name": "check_allowance", + "description": ( + "Read how many SuperDocs operations remain before planning any work. " + "Free. This is the one moment the number is authoritative -- afterwards " + "it can only be inferred from the responses to work you have already done." + ), + "inputSchema": {"type": "object", "properties": {}, "additionalProperties": False}, + }, + { + "name": "plan_work", + "description": ( + "Price a list of edits against the remaining allowance and report what " + "fits, what would be deferred, and why -- WITHOUT doing any of it. Free. " + "Call this before run_work so you never start a job you cannot finish." + ), + "inputSchema": { + "type": "object", + "properties": { + "steps": { + "type": "array", + "description": "The edits you want to make.", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "instruction": {"type": "string", + "description": "A natural-language edit instruction."}, + "sections": {"type": "integer", "minimum": 1, + "description": "Roughly how many document sections it " + "touches. SuperDocs bills one operation " + "per 25 sections edited."}, + "severity": {"type": "string", + "enum": ["critical", "high", "medium", "low"], + "description": "Highest severity runs first when not " + "everything fits."}, + }, + "required": ["instruction"], + }, + }, + "reserve": {"type": "integer", "minimum": 0, "default": 1, + "description": "Operations held back so finished work can still " + "be exported."}, + }, + "required": ["steps"], + }, + }, + { + "name": "run_work", + "description": ( + "Upload a document, apply the edits that fit inside the remaining " + "allowance, approve the proposed changes, and export the result. " + "Degrades rather than failing halfway: it never starts a step it cannot " + "finish, and it reports in plain language what it left out." + ), + "inputSchema": { + "type": "object", + "properties": { + "session_id": {"type": "string"}, + "filename": {"type": "string"}, + "document_html": {"type": "string", + "description": "The document as HTML. SuperDocs takes " + "documents and HTML, never raw Word XML."}, + "steps": {"type": "array", "items": {"type": "object"}}, + "reserve": {"type": "integer", "default": 1}, + "max_steps": {"type": ["integer", "null"], + "description": "Small-sample bound: run at most this many steps."}, + "export_format": {"type": "string", + "enum": ["docx", "pdf", "html", "markdown", "txt"], + "default": "docx"}, + }, + "required": ["session_id", "filename", "document_html", "steps"], + }, + }, +] + +_HANDLERS = { + "check_allowance": lambda a: check_allowance(), + "plan_work": lambda a: plan_work(a["steps"], a.get("reserve", 1)), + "run_work": lambda a: run_work( + a["session_id"], a["filename"], a["document_html"], a["steps"], + a.get("reserve", 1), a.get("max_steps"), a.get("export_format", "docx"), + ), +} + + +def dispatch(name: str, arguments: dict) -> dict: + """The whole surface, callable without an MCP client -- which is what makes + it testable offline.""" + if name not in _HANDLERS: + raise KeyError(f"no tool named {name!r}") + return _HANDLERS[name](arguments or {}) + + +async def serve() -> None: + """Serve the three tools over stdio. + + Written against the MCP SDK's constructor-callback API (2.x). The older + `@server.list_tools()` decorators do not exist there, and a server that + imports cleanly but cannot start is worse than one that fails loudly, so + `test_the_server_starts_and_advertises_its_tools` drives a real client. + """ + import mcp.types as types + from mcp.server import Server + from mcp.server.stdio import stdio_server + + def _tool(spec: dict) -> "types.Tool": + return types.Tool( + name=spec["name"], + description=spec["description"], + input_schema=spec["inputSchema"], + ) + + async def on_list_tools(ctx, params) -> "types.ListToolsResult": + return types.ListToolsResult(tools=[_tool(t) for t in TOOLS]) + + async def on_call_tool(ctx, params) -> "types.CallToolResult": + try: + result = dispatch(params.name, params.arguments or {}) + failed = False + except Exception as e: + # An agent must be able to READ the failure. Letting it propagate + # drops the connection and tells the caller nothing actionable. + result, failed = {"error": str(e)}, True + return types.CallToolResult( + content=[types.TextContent(type="text", text=json.dumps(result, indent=2))], + is_error=failed, + ) + + server = Server( + SERVER_NAME, + version="1.0.0", + instructions=( + "Call check_allowance first, then plan_work to see what fits, then " + "run_work. The first two are free and change nothing." + ), + on_list_tools=on_list_tools, + on_call_tool=on_call_tool, + ) + + async with stdio_server() as (read, write): + await server.run(read, write, server.create_initialization_options()) + + +def main() -> None: + import asyncio + + asyncio.run(serve()) + + +if __name__ == "__main__": + main() diff --git a/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/policy.py b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/policy.py new file mode 100644 index 00000000..9b58b5bb --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/policy.py @@ -0,0 +1,138 @@ +"""The rules this agent runs under, as data rather than as three arguments. + +`budget.py` prices work and fits it to a number. What it must never do is +decide *how much* to hold back, *how many* steps a loop may take, or what to do +when the work does not fit — those are choices an operator makes, and they +belong somewhere an operator can read them. + +They were arguments to the constructor, which is fine until somebody has to +answer *why did it stop?* — and the honest answer is a rule with a name, not a +number that was too low. That distinction is the whole of this module. From the +research: an engineer described putting in a hard cap, having it fire twice on +runs that were fine, and raising the number until it stopped firing. A limit +whose only feedback is "I stopped" trains the person to disable it. +""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +from enum import Enum + + +class WhenItDoesNotFit(str, Enum): + """What to do with work that will not fit in the remaining allowance.""" + + #: Do the highest-severity part that fits and name what was left out. + DEGRADE = "degrade" + #: Do none of it. For a caller who would rather have nothing than a subset. + REFUSE = "refuse" + + +class StopReason(str, Enum): + """Why a run ended. Every one of these is a rule, and every rule can say + what it was protecting — which is what makes it arguable rather than just + obstructive.""" + + COMPLETED = "completed" + QUOTA_EXHAUSTED = "quota_exhausted" + RESERVE_FLOOR = "reserve_floor" + NOTHING_FITS = "nothing_fits" + REFUSED_PARTIAL = "refused_partial" + SAMPLE_BOUND = "sample_bound" + ALREADY_APPLIED = "already_applied" + STARTED_AND_UNKNOWN = "started_and_unknown" + + def explain(self) -> str: + return _WHY[self] + + +_WHY: dict[StopReason, str] = { + StopReason.COMPLETED: "All of the requested work was done.", + StopReason.QUOTA_EXHAUSTED: ( + "SuperDocs itself reported the allowance exhausted. That is the " + "platform's own signal and the only authoritative one — our arithmetic " + "never overrules it, and raising a number here would not help." + ), + StopReason.RESERVE_FLOOR: ( + "The reserve was reached. It exists so the work already done can still " + "be exported: exports are free, so holding one operation back costs " + "nothing and guarantees you end up with a file rather than a session." + ), + StopReason.NOTHING_FITS: ( + "None of the requested work fits inside the remaining allowance. " + "Nothing was uploaded and nothing was billed, so there is no " + "half-edited document to clean up." + ), + StopReason.REFUSED_PARTIAL: ( + "Only part of the work fits, and this policy is set to refuse a partial " + "run rather than deliver a subset. Set when_it_does_not_fit=DEGRADE to " + "take the part that fits." + ), + StopReason.SAMPLE_BOUND: ( + "The small-sample bound was reached. Anything that loops needs a " + "stopping rule; this one is yours and it did what you asked." + ), + StopReason.ALREADY_APPLIED: ( + "This step was applied by an earlier run and was not repeated. " + "SuperDocs has no idempotency on billable writes, so repeating it would " + "have been charged again." + ), + StopReason.STARTED_AND_UNKNOWN: ( + "An earlier run started this step and never learned whether it " + "finished. It was not repeated, because repeating it might be charged " + "twice and might apply the same edit twice. It needs a person to look " + "at the document." + ), +} + + +@dataclass(frozen=True) +class Policy: + """How much to hold back, how far to go, and what to do when it will not fit. + + Frozen, so a policy that was read at the start of a run is the policy the + run ended under. `with_` returns a modified copy rather than mutating, for + the same reason. + """ + + #: Operations never spent on new edits, so finished work can always be + #: exported. Exports are free, so this costs the user nothing. + reserve: int = 1 + #: The small-sample bound. None means "as many as were asked for". + max_steps: int | None = None + when_it_does_not_fit: WhenItDoesNotFit = WhenItDoesNotFit.DEGRADE + + def __post_init__(self) -> None: + if self.reserve < 0: + raise ValueError("a reserve cannot be negative") + if self.max_steps is not None and self.max_steps < 1: + raise ValueError("a sample bound of zero would do nothing and say nothing") + + def spendable(self, remaining_ops: int) -> int: + """What may be spent on new edits, which is not what is left.""" + return max(0, remaining_ops - self.reserve) + + def bound(self, steps: list) -> tuple[list, bool]: + """Apply the sample bound. Returns the steps and whether it bit.""" + if self.max_steps is None or len(steps) <= self.max_steps: + return list(steps), False + return list(steps[: self.max_steps]), True + + def with_(self, **changes) -> "Policy": + return replace(self, **changes) + + def describe(self) -> str: + parts = [ + f"Holding back {self.reserve} operation(s) so finished work can " + "always be exported." + ] + if self.max_steps is not None: + parts.append(f"At most {self.max_steps} step(s) in one run.") + parts.append( + "Work that does not fit is sized down by severity and what is left " + "out is named." + if self.when_it_does_not_fit is WhenItDoesNotFit.DEGRADE + else "Work that does not fit is refused whole rather than delivered " + "in part." + ) + return " ".join(parts) diff --git a/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/receipt.py b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/receipt.py new file mode 100644 index 00000000..eb197efb --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/quota_aware_agent/receipt.py @@ -0,0 +1,200 @@ +"""What a run actually spent, line by line, and whether that adds up. + +From the research, an engineer describing what they did after a job died +halfway: *"I added it up from the responses I'd logged, and it didn't match the +number in the account, so I stopped looking."* + +Two failures in one sentence. They had to do the arithmetic by hand, and when +the arithmetic disagreed with the platform there was nothing to tell them which +side was wrong — so the disagreement went uninvestigated. A receipt does the +adding up, and `reconcile` does the comparison and **names the disagreement +instead of hiding it**. + +The one rule this module refuses to break: a number the platform did not state +is never presented as one it did. An operation charged is what a `usage` block +said; an operation *estimated* is what we believed. They are separate columns, +they are never summed together, and a receipt whose balances were not +authoritative says the reconciliation could not be done rather than doing it on +guesses. +""" + +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass, field + +from .budget import Balance + + +@dataclass(frozen=True) +class Line: + """One call, and what it cost.""" + + step_id: str + call: str + billable: bool + #: What the platform's `usage` block said it charged. None when no usage + #: block came back, which is not the same as zero. + ops_charged: int | None = None + #: What we believed it would cost, before it happened. + ops_estimated: int = 0 + balance_after: int | None = None + balance_authoritative: bool = False + note: str = "" + + @property + def counted(self) -> int: + """Only what the platform stated. Never the estimate.""" + return self.ops_charged or 0 + + +@dataclass(frozen=True) +class Reconciliation: + checkable: bool + agrees: bool + counted: int + implied: int | None + explanation: str + + +@dataclass +class Receipt: + """The line items of one run, in the order they happened.""" + + session_id: str = "" + lines: list[Line] = field(default_factory=list) + + def record(self, step_id: str, call: str, *, billable: bool, + ops_charged: int | None = None, ops_estimated: int = 0, + balance: Balance | None = None, note: str = "") -> Line: + line = Line( + step_id=step_id, call=call, billable=billable, + ops_charged=ops_charged, ops_estimated=ops_estimated, + balance_after=balance.ops if balance else None, + balance_authoritative=bool(balance and balance.authoritative), + note=note, + ) + self.lines.append(line) + return line + + # -- what it adds up to ------------------------------------------------ + @property + def counted(self) -> int: + """Operations the platform said it charged.""" + return sum(l.counted for l in self.lines) + + @property + def estimated_only(self) -> int: + """Operations we believe were charged on calls that reported nothing. + + Kept apart from `counted` on purpose. Adding them would produce one + number that is part measurement and part belief, and nobody downstream + could tell which part. + """ + return sum(l.ops_estimated for l in self.lines + if l.billable and l.ops_charged is None) + + @property + def billable_calls(self) -> int: + return sum(1 for l in self.lines if l.billable) + + @property + def unreported_billable_calls(self) -> int: + return sum(1 for l in self.lines if l.billable and l.ops_charged is None) + + def reconcile(self, start: Balance | None, end: Balance | None) -> Reconciliation: + """Does what the lines add up to match what the balance moved by? + + Only checkable when both ends of the run were *authoritative*. An + estimate on either end makes the difference an estimate too, and a + reconciliation between two guesses is theatre. + """ + counted = self.counted + if start is None or end is None: + return Reconciliation( + False, False, counted, None, + "There is no starting or ending balance to compare against.") + if not (start.authoritative and end.authoritative): + which = [] + if not start.authoritative: + which.append("the starting balance") + if not end.authoritative: + which.append("the ending balance") + return Reconciliation( + False, False, counted, None, + f"{' and '.join(which).capitalize()} was inferred rather than " + "stated by the platform, so this run cannot be reconciled. " + f"{counted} operation(s) were confirmed charged" + + (f", and {self.estimated_only} more were estimated on calls " + "that reported no usage." if self.estimated_only else "."), + ) + + implied = start.ops - end.ops + if implied == counted: + return Reconciliation( + True, True, counted, implied, + f"{counted} operation(s) charged, and the allowance moved by " + f"{implied}. These agree.") + gap = implied - counted + return Reconciliation( + True, False, counted, implied, + f"The line items add up to {counted} operation(s), but the allowance " + f"moved by {implied}. That is a difference of {gap}. It is reported " + "rather than reconciled away: something was charged that this run " + "did not record, or something else is spending against the same " + "account.", + ) + + # -- handing it over --------------------------------------------------- + def as_dict(self, start: Balance | None = None, + end: Balance | None = None) -> dict: + rec = self.reconcile(start, end) + return { + "session_id": self.session_id, + "lines": [asdict(l) for l in self.lines], + "billable_calls": self.billable_calls, + "operations_charged": self.counted, + "operations_estimated_only": self.estimated_only, + "calls_that_reported_no_usage": self.unreported_billable_calls, + "reconciliation": asdict(rec), + } + + def as_json(self, start: Balance | None = None, + end: Balance | None = None) -> str: + return json.dumps(self.as_dict(start, end), indent=2) + + def render_text(self, start: Balance | None = None, + end: Balance | None = None) -> str: + out = ["RUN RECEIPT" + (f" · {self.session_id}" if self.session_id else ""), + "=" * 62] + if start: + out.append(f"Allowance at the start: {start}") + out.append("") + out.append(f"{'STEP':<18}{'CALL':<12}{'CHARGED':>9}{'LEFT':>9}") + out.append("-" * 62) + for l in self.lines: + charged = "—" if l.ops_charged is None else str(l.ops_charged) + if l.ops_charged is None and l.billable: + charged = f"~{l.ops_estimated}" + left = "—" if l.balance_after is None else ( + str(l.balance_after) + ("" if l.balance_authoritative else "?")) + out.append(f"{l.step_id[:17]:<18}{l.call[:11]:<12}{charged:>9}{left:>9}") + if l.note: + out.append(f" {l.note}") + out.append("-" * 62) + out.append(f"{'confirmed charged':<30}{self.counted:>9}") + if self.estimated_only: + out.append(f"{'estimated, never confirmed':<30}{self.estimated_only:>9}") + if end: + out.append(f"Allowance at the end: {end}") + out.append("") + rec = self.reconcile(start, end) + out.append("DOES IT ADD UP?") + out.append("-" * 62) + out.append(rec.explanation) + out.append("") + out.append("A '~' is an operation we believe was charged on a call that " + "returned no usage") + out.append("block. A '?' is a balance we inferred rather than one the " + "platform stated.") + return "\n".join(out) diff --git a/use-cases/Priyanshu2425/quota-aware-agent/screenshot.png b/use-cases/Priyanshu2425/quota-aware-agent/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..01e86f5ab2a4d346efe27c6453da8fbafeba52b7 GIT binary patch literal 135373 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 Wt$P<((G5Q(r68yBs8II#tN#lOeI*nC literal 0 HcmV?d00001 diff --git a/use-cases/Priyanshu2425/quota-aware-agent/tests/fake.py b/use-cases/Priyanshu2425/quota-aware-agent/tests/fake.py new file mode 100644 index 00000000..a1515dbc --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/tests/fake.py @@ -0,0 +1,122 @@ +"""A fake SuperDocs. Every response shape here is taken from the documented +payloads, including the double-parsed proposed-change envelope.""" + +from __future__ import annotations + +import json + +from quota_aware_agent.client import Response + + +class FakeSuperDocs: + def __init__(self, remaining: int = 500, monthly_limit: int = 500, + sections_per_edit: int = 1, slow_polls: int = 0, + exhaust_after: int | None = None, settle_polls: int = 1, + envelope_pending: bool = False) -> None: + self.remaining = remaining + self.monthly_limit = monthly_limit + self.sections_per_edit = sections_per_edit + self.slow_polls = slow_polls + self.exhaust_after = exhaust_after + self.settle_polls = settle_polls + self.envelope_pending = envelope_pending + self._approved_at_poll: int | None = None + self.calls: list[tuple[str, str]] = [] + self._polls = 0 + self._edits = 0 + self.approved: list[dict] = [] + self.uploaded: list[str] = [] + + # -- helpers ---------------------------------------------------------- + def _usage(self, ops: int) -> dict: + self.remaining = max(0, self.remaining - ops) + exhausted = ( + self.exhaust_after is not None and self._edits >= self.exhaust_after + ) or self.remaining == 0 + return { + "monthly_used": self.monthly_limit - self.remaining, + "monthly_limit": self.monthly_limit, + "monthly_remaining": self.remaining, + "was_billable": ops > 0, + "ops_charged": ops, + "quota_exhausted": exhausted, + "subscription_tier": "free", + } + + # -- transport -------------------------------------------------------- + def request(self, method: str, path: str, **kw) -> Response: + self.calls.append((method, path)) + + if path == "/v1/agents/whoami": + return Response(200, { + "account_id": "fake", + "quota": {"tier": "free", "monthly_limit": self.monthly_limit, + "used": self.monthly_limit - self.remaining, + "remaining": self.remaining, + "resets_at": "2026-09-01T00:00:00+00:00"}, + }) + + if path == "/v1/documents/upload": + # The real endpoint answers 422 when the multipart `file` part is + # absent. Modelled here because a fake that accepts anything is how + # a transport that encoded nothing passed its tests. + files = kw.get("files") or {} + if "file" not in files: + return Response(422, {"detail": [ + {"type": "missing", "loc": ["body", "file"], "msg": "Field required"} + ]}) + self.uploaded.append(files["file"][0]) + return Response(200, {"status": "ok", "chunks_count": 2}) + + if path == "/v1/chat/async": + self._edits += 1 + self._polls = 0 + self._approved_at_poll = None + ops = max(1, -(-self.sections_per_edit // 25)) + return Response(200, {"job_id": f"job-{self._edits}", "status": "pending", + "usage": self._usage(ops)}) + + if path.startswith("/v1/jobs/"): + self._polls += 1 + if self._approved_at_poll is not None: + # After approval the job resumes; it needs at least one more + # poll before it is safe to export. + if self._polls <= self._approved_at_poll + self.settle_polls: + return Response(200, {"status": "in_progress"}) + return Response(200, {"status": "completed"}) + if self._polls <= self.slow_polls: + # Trap 2: a long quiet run. No usage block, still processing. + return Response(200, {"status": "in_progress"}) + if self.envelope_pending: + # The SSE `proposed_change_batch` shape: content is a JSON + # *string* needing a second parse (Trap 1). + pending = {"content": json.dumps({"changes": [ + {"change_id": "ch_1", "chunk_id": "c1", "diff": "

after

"}, + ]})} + else: + # What GET /v1/jobs/{id} actually returns: a plain list. + # Verified against the live API 2026-08-19. + pending = [{"change_id": "ch_1", "chunk_id": "c1", + "old_html": "

before

", "new_html": "

after

"}] + return Response(200, { + "status": "awaiting_approval", + "metadata": {"pending_changes": pending}, + }) + + if path.endswith("/approve"): + self.approved.extend(kw["json"]["changes"]) + # The real endpoint returns {"status": "ok"} and the job keeps + # running; it reaches "completed" only on a later poll. Returning + # "completed" here hid a race that silently exported the pre-edit + # document. Verified live 2026-08-19. + self._approved_at_poll = self._polls + return Response(200, {"status": "ok", "usage": self._usage(0)}) + + if path == "/v1/documents/export": + import base64 + warn = base64.b64encode(json.dumps( + [{"code": "field_code_unsupported", "detail": "a citation was skipped"}] + ).encode()).decode() + return Response(200, {"raw": b"DOCX"}, {"X-Export-Warnings": warn}) + + return Response(404, {"error": "no such path"}) diff --git a/use-cases/Priyanshu2425/quota-aware-agent/tests/test_agent.py b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_agent.py new file mode 100644 index 00000000..33555f25 --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_agent.py @@ -0,0 +1,227 @@ +"""Keyless. No network. These prove the card's claims, not the fake's.""" + +import pytest + +from quota_aware_agent import QuotaAwareAgent, Step, SuperDocsClient, parse_proposed_changes +from quota_aware_agent.client import QuotaExhausted +from quota_aware_agent.policy import Policy +from tests.fake import FakeSuperDocs + +DOC = b"a document" + + +def agent(fake, **kw): + return QuotaAwareAgent(SuperDocsClient(fake, sleep=lambda s: None), + policy=Policy(**kw)) + + +def steps(n, sections=1, severity="medium"): + return [Step(f"s{i}", f"edit {i}", sections, severity) for i in range(1, n + 1)] + + +def test_reads_allowance_before_it_plans_or_uploads(): + """The card's actual requirement: allowance first, then planning.""" + fake = FakeSuperDocs(remaining=500) + agent(fake).run("sess", "d.docx", DOC, steps(1)) + paths = [p for _, p in fake.calls] + assert paths[0] == "/v1/agents/whoami" + assert paths.index("/v1/agents/whoami") < paths.index("/v1/documents/upload") + + +def test_never_starts_work_it_cannot_finish(): + """One operation left, all of it reserved. Nothing is uploaded at all -- + the point is that no half-applied state exists, not that it fails politely.""" + fake = FakeSuperDocs(remaining=1) + report = agent(fake, reserve=1).run("sess", "d.docx", DOC, steps(3)) + assert report.completed == [] + assert "/v1/documents/upload" not in [p for _, p in fake.calls] + assert "did not start" in report.plain_language().lower() + assert report.stopped_because + + +def test_degrades_by_severity_and_says_what_it_left_out(): + """Two operations of room, four operations of work. Critical work survives, + low-severity work is named as deferred in plain language.""" + fake = FakeSuperDocs(remaining=3) + work = [ + Step("critical", "fix the numbers", 25, "critical"), + Step("low", "tidy the footer", 25, "low"), + Step("high", "fix the dates", 25, "high"), + ] + report = agent(fake, reserve=1).run("sess", "d.docx", DOC, work) + assert "critical" in report.planned + assert "low" in report.deferred + text = report.plain_language() + assert "Left undone" in text and "low" in text + assert "operations" in text + + +def test_the_four_calls_happen_in_order(): + fake = FakeSuperDocs(remaining=500) + agent(fake).run("sess", "d.docx", DOC, steps(1)) + paths = [p for _, p in fake.calls] + assert paths.index("/v1/documents/upload") < paths.index("/v1/chat/async") + assert paths.index("/v1/chat/async") < [i for i, p in enumerate(paths) if p.endswith("/approve")][0] + assert paths.index("/v1/documents/export") == len(paths) - 1 + + +def test_trap_1_the_double_parse(): + """A batch whose content is a JSON string must survive the second parse.""" + import json + envelope = {"content": json.dumps({"changes": [{"change_id": "ch_9"}]})} + assert parse_proposed_changes(envelope) == [{"change_id": "ch_9"}] + + +def test_trap_1_end_to_end_the_change_is_actually_approved(): + """The regression the docs warn about is silent: undefined fields, nothing + approved, no error. So assert the change id arrived, not just that it ran.""" + fake = FakeSuperDocs(remaining=500) + agent(fake).run("sess", "d.docx", DOC, steps(1)) + assert fake.approved == [{"change_id": "ch_1", "approved": True}] + + +def test_trap_2_a_long_silence_is_not_a_crash(): + fake = FakeSuperDocs(remaining=500, slow_polls=5) + report = agent(fake).run("sess", "d.docx", DOC, steps(1)) + assert report.completed == ["s1"] + + +def test_trap_2_a_deadline_says_it_is_our_deadline_not_a_platform_failure(): + fake = FakeSuperDocs(remaining=500, slow_polls=10_000) + client = SuperDocsClient(fake, sleep=lambda s: None) + client.edit("sess", "go") + with pytest.raises(TimeoutError) as e: + client.poll_job("job-1", deadline_s=4, interval_s=2) + assert "not a platform failure" in str(e.value) + + +def test_trap_3_small_sample_mode_bounds_the_loop(): + fake = FakeSuperDocs(remaining=500) + report = agent(fake, max_steps=2).run("sess", "d.docx", DOC, steps(10)) + assert len(report.completed) == 2 + assert "Small-sample mode" in report.plain_language() + + +def test_the_platform_signal_stops_it_even_with_budget_left(): + """quota_exhausted is authoritative. Our own arithmetic is not.""" + fake = FakeSuperDocs(remaining=500, exhaust_after=1) + report = agent(fake).run("sess", "d.docx", DOC, steps(3)) + assert "exhausted" in report.stopped_because + assert len(report.completed) < 3 + + +def test_it_still_exports_after_stopping_early(): + """Exports are free, so stopping early must never cost the user the work + already done.""" + fake = FakeSuperDocs(remaining=500, exhaust_after=1) + agent(fake).run("sess", "d.docx", DOC, steps(3)) + assert "/v1/documents/export" in [p for _, p in fake.calls] + + +def test_export_warnings_are_surfaced_not_swallowed(): + fake = FakeSuperDocs(remaining=500) + report = agent(fake).run("sess", "d.docx", DOC, steps(1)) + assert report.export_warnings + assert "non-fatal" in report.plain_language() + + +def test_a_balance_between_calls_is_never_presented_as_a_live_read(): + """BUG-003: the usage endpoints reject API keys, so the number is only + authoritative at whoami and after each response.""" + from quota_aware_agent import BudgetGuard + g = BudgetGuard() + g.seed_from_whoami(500) + assert g.remaining().authoritative + g.reconcile(ops_charged=1, monthly_remaining=None, quota_exhausted=False) + assert not g.remaining().authoritative + assert "estimated" in str(g.remaining()) + + +def test_an_unknown_allowance_is_planned_as_zero_never_guessed(): + class NoQuota(FakeSuperDocs): + def request(self, method, path, **kw): + if path == "/v1/agents/whoami": + return __import__("quota_aware_agent.client", fromlist=["Response"]).Response(200, {}) + return super().request(method, path, **kw) + + fake = NoQuota(remaining=500) + report = agent(fake).run("sess", "d.docx", DOC, steps(2)) + assert report.completed == [] + assert report.balance_at_start.ops == 0 + + +# -- gaps found by running the contract against the live API, 2026-08-19 ----- + +def test_the_transport_actually_encodes_multipart(): + """The four-call contract passed for weeks against a fake that accepted any + kwargs, while `HttpTransport` dropped `files=` entirely and the real + endpoint answered 422. A fake that accepts anything proves nothing.""" + from quota_aware_agent.client import _encode_multipart + + body, ctype = _encode_multipart( + {"file": ("memo.docx", b"CONTENT-BYTES")}, {"session_id": "s1"} + ) + assert ctype.startswith("multipart/form-data; boundary=") + boundary = ctype.split("boundary=")[1] + assert b'name="file"; filename="memo.docx"' in body + assert b"CONTENT-BYTES" in body + assert b'name="session_id"' in body and b"s1" in body + assert body.rstrip().endswith(f"--{boundary}--".encode()) + + +def test_an_upload_without_a_file_part_is_refused_by_the_fake_too(): + """Pins the contract the live API enforces, so the transport cannot + regress to sending nothing.""" + fake = FakeSuperDocs(remaining=500) + from quota_aware_agent.client import SuperDocsError + + with pytest.raises(SuperDocsError) as e: + SuperDocsClient(fake, sleep=lambda s: None)._check( + fake.request("POST", "/v1/documents/upload", json={"session_id": "s"}) + ) + assert e.value.status == 422 + + +def test_it_waits_for_the_job_to_settle_before_exporting(): + """Approval is asynchronous. The approve call returns ok and the job keeps + running; exporting before it completes returns the document unchanged with + a 200 and a valid file, so the loss is silent. Verified live.""" + fake = FakeSuperDocs(remaining=500, settle_polls=2) + agent(fake).run("sess", "d.docx", DOC, steps(1)) + + paths = [p for _, p in fake.calls] + approve_at = next(i for i, p in enumerate(paths) if p.endswith("/approve")) + export_at = paths.index("/v1/documents/export") + polls_between = [p for p in paths[approve_at:export_at] if p.startswith("/v1/jobs/")] + assert polls_between, "exported without waiting for the job to settle after approving" + + +def test_pending_changes_as_a_plain_list_is_handled(): + """GET /v1/jobs/{id} returns `pending_changes` as a list, not the + double-parsed envelope the SSE stream delivers. Both shapes must work.""" + for envelope in (False, True): + fake = FakeSuperDocs(remaining=500, envelope_pending=envelope) + agent(fake).run("sess", "d.docx", DOC, steps(1)) + assert fake.approved == [{"change_id": "ch_1", "approved": True}], \ + f"envelope={envelope} failed to approve the change" + + +def test_a_billable_call_with_no_usage_block_stops_claiming_confirmed(): + """The docs say usage rides on every chat response. The async endpoints + return none, so the agent kept printing the seeded whoami number as + "confirmed" while the true balance had already moved — 499 confirmed + against a real 498. A guess must announce itself as a guess.""" + class NoUsage(FakeSuperDocs): + def request(self, method, path, **kw): + r = super().request(method, path, **kw) + if isinstance(r.body, dict): + r.body.pop("usage", None) + return r + + fake = NoUsage(remaining=500) + report = agent(fake).run("sess", "d.docx", DOC, steps(1)) + + assert report.balance_at_start.authoritative # whoami is real + assert not report.balance_at_end.authoritative # everything after is not + assert "estimated" in str(report.balance_at_end) + assert report.balance_at_end.ops < report.balance_at_start.ops diff --git a/use-cases/Priyanshu2425/quota-aware-agent/tests/test_governance.py b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_governance.py new file mode 100644 index 00000000..e68cbb7c --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_governance.py @@ -0,0 +1,287 @@ +"""The policy, the receipt and the operation ledger. + +Three things an engineer described doing by hand after a run died halfway +through a batch: guessing where to restart and paying twice for the overlap, +adding the spend up from logs and abandoning it when it did not match the +account, and putting in a hard cap that only ever said "I stopped". +""" + +from __future__ import annotations + +import json + +import pytest + +from quota_aware_agent import QuotaAwareAgent, Step, SuperDocsClient +from quota_aware_agent.budget import Balance +from quota_aware_agent.idempotency import (OperationLedger, State, + operation_key) +from quota_aware_agent.policy import Policy, StopReason, WhenItDoesNotFit +from quota_aware_agent.receipt import Receipt +from tests.fake import FakeSuperDocs + +DOC = b"

x

" + + +def agent(fake, **kw): + return QuotaAwareAgent(SuperDocsClient(fake, sleep=lambda s: None), **kw) + + +def steps(n: int, sections: int = 10) -> list[Step]: + return [Step(f"s{i}", f"rewrite section {i}", sections) for i in range(n)] + + +# -- the policy ------------------------------------------------------------- + +def test_the_reserve_is_held_back_from_what_may_be_spent(): + p = Policy(reserve=2) + assert p.spendable(5) == 3 + assert p.spendable(1) == 0 # never negative + + +def test_a_sample_bound_of_zero_is_refused_rather_than_silently_doing_nothing(): + with pytest.raises(ValueError): + Policy(max_steps=0) + with pytest.raises(ValueError): + Policy(reserve=-1) + + +def test_a_policy_is_frozen_so_a_run_ends_under_the_policy_it_began_with(): + p = Policy(reserve=1) + with pytest.raises(Exception): + p.reserve = 5 # type: ignore[misc] + assert p.with_(reserve=5).reserve == 5 and p.reserve == 1 + + +def test_every_stop_reason_says_what_the_rule_was_protecting(): + """A limit whose only feedback is 'I stopped' trains the person to raise it + until it stops firing. Each of these has to be arguable.""" + for reason in StopReason: + if reason is StopReason.COMPLETED: + continue # not a rule firing; nothing to argue with + why = reason.explain() + assert len(why) > 40, f"{reason.value} does not explain itself" + assert why[0].isupper() and why.rstrip().endswith(".") + + +def test_refusing_a_partial_run_is_a_choice_a_caller_can_make(fake_tight): + """Some callers would rather have nothing than a subset — a document half + updated is worse than one not updated at all.""" + a = agent(fake_tight, policy=Policy(reserve=0, + when_it_does_not_fit=WhenItDoesNotFit.REFUSE)) + report = a.run("sess", "d.docx", DOC, steps(4, sections=30)) + assert report.completed == [] + assert report.stop_reason is StopReason.REFUSED_PARTIAL + assert "refuse" in report.stopped_because.lower() + + +def test_degrading_is_still_the_default(fake_tight): + report = agent(fake_tight, policy=Policy(reserve=0)).run( + "sess", "d.docx", DOC, steps(4, sections=30)) + assert report.completed and report.deferred + + +# -- the receipt ------------------------------------------------------------ + +def _receipt() -> Receipt: + r = Receipt(session_id="sess") + r.record("s0", "edit", billable=True, ops_charged=1, ops_estimated=1, + balance=Balance(4, authoritative=True)) + r.record("s0", "poll", billable=False, ops_charged=0, + balance=Balance(4, authoritative=True)) + return r + + +def test_what_the_platform_stated_and_what_we_believed_are_never_added_together(): + r = _receipt() + r.record("s1", "edit", billable=True, ops_charged=None, ops_estimated=2, + balance=Balance(2, authoritative=False)) + assert r.counted == 1 # only what a usage block said + assert r.estimated_only == 2 # kept apart, deliberately + assert r.unreported_billable_calls == 1 + + +def test_a_run_that_adds_up_says_so(): + rec = _receipt().reconcile(Balance(5, authoritative=True), + Balance(4, authoritative=True)) + assert rec.checkable and rec.agrees + assert "agree" in rec.explanation + + +def test_a_run_that_does_not_add_up_names_the_gap_instead_of_hiding_it(): + """'It didn't match the number in the account, so I stopped looking.'""" + rec = _receipt().reconcile(Balance(5, authoritative=True), + Balance(2, authoritative=True)) + assert rec.checkable and not rec.agrees + assert "difference of 2" in rec.explanation + assert "something was charged that this run did not record" in rec.explanation + + +def test_it_refuses_to_reconcile_two_numbers_it_inferred(): + """A reconciliation between two guesses is theatre.""" + rec = _receipt().reconcile(Balance(5, authoritative=True), + Balance(4, authoritative=False)) + assert not rec.checkable + assert "inferred rather than stated" in rec.explanation + + +def test_the_rendered_receipt_marks_every_number_it_did_not_get_from_the_platform(): + r = _receipt() + r.record("s1", "edit", billable=True, ops_charged=None, ops_estimated=2, + balance=Balance(2, authoritative=False)) + text = r.render_text(Balance(5, authoritative=True), + Balance(2, authoritative=False)) + assert "~2" in text # an operation we believe, not one we were told + assert "2?" in text # a balance we inferred + assert "cannot be reconciled" in text + + +def test_a_receipt_survives_being_serialised(fake): + report = agent(fake).run("sess", "d.docx", DOC, steps(1)) + body = json.loads(report.receipt.as_json(report.balance_at_start, + report.balance_at_end)) + assert body["lines"] and body["reconciliation"]["explanation"] + + +# -- the operation ledger --------------------------------------------------- + +def test_the_key_is_over_what_the_call_does_not_over_an_id_somebody_assigned(): + a = operation_key("sess", "s0", "rewrite section 0", 10) + assert a == operation_key("sess", "s0", " rewrite section 0 ", 10) + assert a != operation_key("other", "s0", "rewrite section 0", 10) + assert a != operation_key("sess", "s0", "rewrite section 0", 11) + + +def test_an_applied_step_is_not_repeated_and_not_billed_again(fake, tmp_path): + """The failure this exists for: 'I got the boundary wrong. Paid for those + twice.' SuperDocs has no idempotency on billable writes (BUG-002), so this + is the only place it can be prevented.""" + path = tmp_path / "ops.jsonl" + work = steps(2) + + first = agent(fake, ledger=OperationLedger(path)).run("sess", "d.docx", DOC, work) + assert len(first.completed) == 2 + spent_first = first.receipt.counted + + fake.remaining = 500 + second = agent(fake, ledger=OperationLedger(path)).run("sess", "d.docx", DOC, work) + assert second.completed == [] + assert second.already_applied == ["s0", "s1"] + assert second.receipt.counted == 0, "the rerun was charged for work already done" + assert spent_first > 0 + assert "not charged again" in second.plain_language() + + +def test_a_call_that_was_started_and_never_confirmed_is_neither_repeated_nor_forgotten( + fake, tmp_path +): + """The genuinely hard case. Retrying might be charged twice and might apply + the same edit twice; skipping might leave the work undone. There is no safe + automatic answer, so it goes to a person.""" + path = tmp_path / "ops.jsonl" + work = steps(1) + key = operation_key("sess", "s0", work[0].instruction, work[0].sections) + OperationLedger(path).begin(key, session_id="sess", step_id="s0") + + report = agent(fake, ledger=OperationLedger(path)).run("sess", "d.docx", DOC, work) + assert report.needs_a_person == ["s0"] + assert report.completed == [] + assert report.receipt.counted == 0 + said = report.plain_language() + assert "never confirmed" in said and "check them" in said + + +def test_a_person_can_resolve_what_the_agent_would_not_decide(fake, tmp_path): + path = tmp_path / "ops.jsonl" + work = steps(1) + key = operation_key("sess", "s0", work[0].instruction, work[0].sections) + ledger = OperationLedger(path) + ledger.begin(key, session_id="sess", step_id="s0") + assert len(ledger.unresolved()) == 1 + + ledger.resolve(key, applied=False, note="the document did not have the edit") + assert ledger.unresolved() == [] + report = agent(fake, ledger=OperationLedger(path)).run("sess", "d.docx", DOC, work) + assert report.completed == ["s0"], "a failed call must be repeatable" + + +def test_the_ledger_is_written_before_the_call_not_after(fake, tmp_path): + """If it were written afterwards, a process that died mid-call would leave + no trace of a call that was charged — and the rerun would repeat it.""" + path = tmp_path / "ops.jsonl" + work = steps(1) + + class DiesMidCall(FakeSuperDocs): + def request(self, method, path, **kw): + if "chat/async" in path: + raise KeyboardInterrupt("the process was killed") + return super().request(method, path, **kw) + + with pytest.raises(KeyboardInterrupt): + agent(DiesMidCall(), ledger=OperationLedger(path)).run( + "sess", "d.docx", DOC, work) + + # A fresh ledger, as a new process would build it. + assert len(OperationLedger(path).records()) == 1 + + +def test_a_half_written_final_line_does_not_lose_the_whole_ledger(tmp_path): + """What an interrupted run actually leaves behind.""" + path = tmp_path / "ops.jsonl" + ledger = OperationLedger(path) + ledger.applied("k1", ops_charged=1) + with path.open("a") as fh: + fh.write('{"key": "k2", "sta') + + reopened = OperationLedger(path) + assert reopened.get("k1").state is State.APPLIED + assert reopened.get("k2").state is State.UNKNOWN + + +def test_the_last_word_about_a_key_is_the_current_one(tmp_path): + path = tmp_path / "ops.jsonl" + ledger = OperationLedger(path) + ledger.begin("k1") + ledger.applied("k1", ops_charged=1) + assert OperationLedger(path).get("k1").state is State.APPLIED + + +def test_only_calls_that_were_never_accepted_are_repeatable(): + ledger = OperationLedger() + assert ledger.get("never-seen").repeatable + assert ledger.failed("k").repeatable + assert not ledger.begin("k2").repeatable # in flight + assert not ledger.applied("k3").repeatable + + +# -- the budget, handed back -------------------------------------------------- + +def test_the_agent_hands_its_remaining_allowance_back_on_every_result(fake): + a = agent(fake) + a.read_allowance() + hint = a.budget_hint() + assert hint["remaining_operations"] > 0 + assert hint["authoritative"] is True + assert hint["spendable_on_new_edits"] == hint["remaining_operations"] - hint["reserve"] + assert "exports are free" in hint["note"].lower() + + +def test_the_hint_never_presents_an_inferred_number_as_a_confirmed_one(fake): + a = agent(fake) + a.run("sess", "d.docx", DOC, steps(1)) + hint = a.budget_hint() + assert hint["authoritative"] in (True, False) + if not hint["authoritative"]: + assert "inferred" in hint["note"] + + +@pytest.fixture() +def fake(): + return FakeSuperDocs() + + +@pytest.fixture() +def fake_tight(): + f = FakeSuperDocs() + f.remaining = 2 + return f diff --git a/use-cases/Priyanshu2425/quota-aware-agent/tests/test_mcp_server.py b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_mcp_server.py new file mode 100644 index 00000000..403e47b7 --- /dev/null +++ b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_mcp_server.py @@ -0,0 +1,163 @@ +"""The MCP surface. Keyless — `dispatch` is callable without an MCP client, +which is deliberate: a surface only testable through a protocol client is a +surface that does not get tested.""" + +import json + +import pytest + +from quota_aware_agent import mcp_server as m +from tests.fake import FakeSuperDocs + +WORK = [ + {"id": "figures", "instruction": "Fix the revenue figures.", "sections": 25, + "severity": "critical"}, + {"id": "footer", "instruction": "Tidy the footer.", "sections": 25, "severity": "low"}, +] + + +@pytest.fixture +def fake(monkeypatch): + f = FakeSuperDocs(remaining=2) + from quota_aware_agent.client import SuperDocsClient + monkeypatch.setattr(m, "_client", lambda: SuperDocsClient(f, sleep=lambda s: None)) + return f + + +@pytest.fixture(autouse=True) +def _ledger_in_a_temp_file(tmp_path, monkeypatch): + """The operation ledger is deliberately persistent -- it exists because a + process died -- so a test that used the real one would write into the + developer's home directory and, worse, would see work a previous test run + had "already applied". Each test gets its own.""" + monkeypatch.setattr(m, "LEDGER_PATH", str(tmp_path / "operations.jsonl")) + + +def test_every_declared_tool_has_a_handler(): + """A tool an agent can see but not call is worse than no tool.""" + declared = {t["name"] for t in m.TOOLS} + assert declared == set(m._HANDLERS) + + +def test_every_tool_schema_is_well_formed(): + for t in m.TOOLS: + assert t["description"].strip() + s = t["inputSchema"] + assert s["type"] == "object" + for req in s.get("required", []): + assert req in s["properties"], f"{t['name']} requires undeclared {req!r}" + # every property is documented or typed well enough to use blind + for name, spec in s["properties"].items(): + assert "type" in spec or "enum" in spec, f"{t['name']}.{name} has no type" + + +def test_check_allowance_is_free_and_authoritative(fake): + out = m.dispatch("check_allowance", {}) + assert out["remaining_operations"] == 2 + assert out["authoritative"] is True + assert [p for _, p in fake.calls] == ["/v1/agents/whoami"] # nothing billable + + +def test_plan_work_spends_nothing_and_changes_nothing(fake): + out = m.dispatch("plan_work", {"steps": WORK}) + assert out["will_run"] == ["figures"] + assert out["will_defer"] == ["footer"] + assert out["fits_completely"] is False + assert "Sized to fit" in out["explanation"] + # planning must not upload, edit, approve or export + assert [p for _, p in fake.calls] == ["/v1/agents/whoami"] + + +def test_plan_and_run_agree_about_what_fits(fake): + """If the MCP path planned differently from the library path, only one of + them would be tested. They share the agent, and this pins that.""" + planned = m.dispatch("plan_work", {"steps": WORK}) + ran = m.dispatch("run_work", { + "session_id": "s", "filename": "d.html", + "document_html": "

x

", "steps": WORK, + }) + assert ran["completed"] == planned["will_run"] + assert ran["deferred"] == planned["will_defer"] + + +def test_run_work_reports_the_trade_off_in_plain_language(fake): + out = m.dispatch("run_work", { + "session_id": "s", "filename": "d.html", + "document_html": "

x

", "steps": WORK, + }) + assert "Left undone" in out["plain_language"] + assert out["allowance_at_start"]["authoritative"] is True + + +def test_an_unknown_tool_is_refused_by_name(): + with pytest.raises(KeyError): + m.dispatch("delete_everything", {}) + + +def test_a_failure_is_returned_to_the_agent_not_raised_at_the_transport(monkeypatch): + """An agent has to be able to read the error. Dropping the connection tells + it nothing it can act on.""" + def boom(): + raise RuntimeError("SUPERDOCS_API_KEY is not set") + monkeypatch.setattr(m, "_client", boom) + + # this is what the MCP call_tool wrapper does + try: + result = m.dispatch("check_allowance", {}) + except Exception as e: + result = {"error": str(e)} + assert "SUPERDOCS_API_KEY" in result["error"] + assert json.dumps(result) # and it survives serialisation + + +def test_the_small_sample_bound_is_reachable_from_the_surface(fake): + fake.remaining = 500 + out = m.dispatch("run_work", { + "session_id": "s", "filename": "d.html", "document_html": "

x

", + "steps": WORK, "max_steps": 1, + }) + assert len(out["completed"]) == 1 + + +# -- protocol level ---------------------------------------------------------- + +def test_the_server_starts_and_advertises_its_tools(): + """The functions above are testable without MCP, which is deliberate — but + a server that imports cleanly and cannot start is worse than one that fails + loudly. The SDK's decorator API was removed in 2.x and this build was + written against it, so `serve()` raised AttributeError on startup while + every other test passed. This drives a real client over stdio.""" + mcp = pytest.importorskip("mcp", reason="the MCP SDK is an optional extra") + import asyncio + import os + import pathlib + import sys + + from mcp import ClientSession, StdioServerParameters + from mcp.client.stdio import stdio_client + + root = str(pathlib.Path(__file__).resolve().parents[1]) + + async def drive(): + params = StdioServerParameters( + command=sys.executable, + args=["-m", "quota_aware_agent.mcp_server"], + env={**os.environ, "PYTHONPATH": root, "SUPERDOCS_API_KEY": ""}, + ) + async with stdio_client(params) as (r, w): + async with ClientSession(r, w) as s: + await s.initialize() + listed = await s.list_tools() + names = {t.name for t in listed.tools} + # every advertised tool carries a usable schema + for t in listed.tools: + assert t.description.strip() + assert t.input_schema["type"] == "object" + # and a call with no key comes back as a readable error, + # not a dropped connection + out = await s.call_tool("check_allowance", {}) + return names, out.content[0].text + + names, text = asyncio.run(drive()) + assert names == {"check_allowance", "plan_work", "run_work"} + assert "SUPERDOCS_API_KEY" in text # the failure is legible to the agent diff --git a/use-cases/Priyanshu2425/word-doc-repair/DESIGN.md b/use-cases/Priyanshu2425/word-doc-repair/DESIGN.md new file mode 100644 index 00000000..0bb0aed5 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/DESIGN.md @@ -0,0 +1,75 @@ +# Design — Salvage + +The web page for this build, and only this build. It shares no visual language +with anything else its author has built, deliberately: a person whose file just +broke is not the same person, on the same day, as somebody reviewing a document +at their desk. + +## The world + +A postal recovery notice — what an institution sends you when your item arrived +damaged. A printed wrapper, chevron edge tape, a docket number, a rubber-stamped +verdict, and language that takes responsibility in plain words. + +It was chosen because it carries the product's truth exactly: careful custody of +something that is not ours, an honest account of what survived, and bad news +delivered in the same voice as good news. It is not the SaaS uploader page — no +hero, no feature cards, no dashed rectangle — and it is not a terminal. + +## Colour + +Committed: the institution's blue owns the marks and the actions, red belongs to +damage, and the ground is a cool form stock so the page never reads as warm +stationery. + +| Token | Light | Meaning | +|---|---|---| +| `--stock` / `--sheet` | `#e9ebe6` / `#fcfcfa` | the desk, the notice | +| `--ink` / `--ink-2` / `--ink-3` | `#14171a` / `#494f55` / `#7b8188` | text, secondary, tertiary | +| `--post-blue` | `#173f8a` | the institution: marks, stamps, the download action | +| `--post-red` | `#c2222c` | damage: the failed stamp, losses, refusals | +| `--kept` | `#16603c` | one tick, beside something that survived | + +Red is never used for emphasis or decoration — only for something that was lost +or refused. Dark mode is the same notice under a desk lamp. + +## Type + +The system UI stack, set large: 17px body, 28–38px headline, short measure. One +monospace, used only for the record line — filename, size, docket — because that +is a form field, not a costume. + +## Devices with a fixed meaning + +- **Chevron tape** — edges anything in our custody: the notice itself, and the + panel holding the repaired file. Nothing else. +- **The stamp** — one verdict, landing once with a small rotation: *Recovered*, + *Recovered in part*, or *Could not be repaired*. +- **Stations** — the engine's real stages, shown while it works, then folded + behind a disclosure once the verdict is known. The answer outranks the working. +- **The record line** — file, size, docket. A receipt, so the page has a name for + the thing it is holding. +- **The sheet** — the rebuilt document itself, rendered for reading, between the + losses and the handover. It carries no chevron tape: tape means *in our + custody* and edges only the notice and the panel holding the file, and this is + the contents rather than the custody. It sits above the download because the + question it answers is asked before the download, not after. + +## Rules that are not style + +- A failure renders no "what came through" section and no download. (BUG-012.) +- Losses are set at the same weight as recoveries. Styling losses as a footnote + would be the overclaim this build refuses, expressed in CSS. +- Retention is stated where the file is handed over, before it can expire. +- The verdict is a claim and the sheet is the thing itself. Where both are on + screen the sheet is never smaller than the claim about it. +- No engine vocabulary reaches the page — not in a stage line, not in a list, not + in an error. Enforced over rendered output, for every fixture. (BUG-014, BUG-020.) +- No sentence claims a complete, perfect or guaranteed repair. Also enforced over + rendered output, with negations understood: *"not a complete repair"* is the + sentence this product exists to say. + +## Mobile + +Designed at 400px first. One column throughout, controls at full width, the +verdict and the download reachable without a horizontal thought. diff --git a/use-cases/Priyanshu2425/word-doc-repair/LICENSE b/use-cases/Priyanshu2425/word-doc-repair/LICENSE new file mode 100644 index 00000000..cb00ec18 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Priyanshu Semwal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/use-cases/Priyanshu2425/word-doc-repair/README.md b/use-cases/Priyanshu2425/word-doc-repair/README.md new file mode 100644 index 00000000..a29ea4f7 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/README.md @@ -0,0 +1,206 @@ +# Salvage — repair my broken Word doc + +**Assigned build B · band S2 · API + export · SuperDocs** + +A `.docx` that will not open leaves its owner with nothing — Word says the file +is corrupt and offers no way forward. Drop it on this page and you get back a +clean, styled Word file, plus a plain account of what came through and what did +not. + +![Salvage's report after recovering a truncated document: a stamped verdict, what came through, and the repaired file](screenshot.png) + +## What it does + +It opens the file the way Word will not, takes out whatever is still readable, +and rebuilds a valid document around it. + +A `.docx` is a ZIP of XML parts, and it breaks in a handful of recognisable +ways. Each one needs a different move: + +| What is wrong | What it does about it | +|---|---| +| The file's index is damaged or the download was cut short | Reads the internal file headers directly and inflates each part by hand. The index lives at the *end* of the file, so it is the first thing a truncated download loses — while the content itself is usually still there. | +| The document body is malformed — a stray `&`, an invalid character, a tag left open by the cut | Repairs each fault, then closes any elements still open at the end. It never reorders or invents content. | +| Structural parts are missing | Regenerates them. They are standard plumbing and carry none of your content, which is why the report does not list them as a loss. | +| Your pictures are in there somewhere | Carries them back into the rebuilt file. Images are separate members of the archive with their own headers, so they survive exactly the damage that destroys the index — and when the part that said *where* each one belonged did not survive, they go at the end under a heading rather than being placed somewhere plausible and wrong. | +| Footnotes, headers and footers | Recovered as text and set down at the end, each under its own heading. A footnote folded silently into the body would change what the document says. | +| The body is too damaged to parse at all | Falls back to pulling the text out run by run — and **says so**, because you are getting words back without headings or tables, and finding that out later is the failure this tool exists to prevent. | +| The main document part is gone entirely | Says it cannot be repaired. Nothing is invented to fill the gap. | + +## What it promises, and what it does not + +It never claims a complete repair. A test asserts that — it fails the build if +the words *fully repaired*, *guaranteed*, *perfect* or *100%* appear anywhere in +any output the user can see. + +**And it shows you the document before you take it.** The rebuilt file is +rendered on the page — headings, tables, and the pictures — above the download +button, because the question *was any of this worth it* is one people ask before +they act. This is the part of the page whose honesty needs no wording at all: + +> It ran for a bit and then wanted forty dollars to download the result. It just +> said "repair successful". I didn't know if it had actually got anything. + +Three things it will always do: + +- **Name what it lost.** Structure that could not be preserved, parts cut short + by the damage, content that was not there to recover. +- **Refuse to call an empty file a success.** A valid document with nothing in it + is the most dangerous possible output — it opens cleanly, so the owner may not + notice their content is gone for weeks. That is reported as a failure. +- **Tell you to keep the original.** It is on the result screen, every time. + +## What it uses from SuperDocs + +| Surface | Used for | +|---|---| +| **REST API** — `POST /v1/documents/upload` | Sending the recovered structure up as clean HTML | +| **Chat editing** — `POST /v1/chat/async` | One edit instruction: restore formatting, change no content | +| **Human-in-the-loop approval** — `POST /v1/chat/{session_id}/approve` | Approving each proposed change, with the second parse its payload requires | +| **Export** — `POST /v1/documents/export` | Getting a styled `.docx` back | + +All four are optional. The local rebuild is the default and needs no key, no +network and no account — and **every failure on the SuperDocs path degrades back +to it**, which a test asserts for each failure in turn. Details under +[Where SuperDocs fits](#where-superdocs-fits). + +## Run it + +The web page — this is the product: + +``` +pip install -e ".[web]" +python3 -m docrepair.web # then open http://127.0.0.1:8000 +PORT=8077 python3 -m docrepair.web # if 8000 is taken +``` + +The page is React and TypeScript, and **you do not need Node to run it** — the +build emits one self-contained file into `static/`, and that file is committed. + +The tests, which need nothing installed and no key: + +``` +python3 -m pytest # 55 tests, offline +``` + +To work on the page itself: + +``` +cd frontend +npm install +npm test # 36 tests +npm run build # rewrites static/index.html +``` + +Those 36 tests render the whole page and answer it with repair streams recorded +from the real engine by `tests/test_frontend_fixtures.py`, which fails if the +recordings drift. The honesty guards — no engine vocabulary, no promise of a +complete repair, no "what came through" on a total failure — run over what is +actually on the screen, for all ten recorded repairs. They used to run over +the engine's strings, which is how BUG-014 was fixed and came straight back as +BUG-020: a page can introduce copy the engine never produced. + +And a CLI, for a folder full of them. The engine is identical; the page is the +product and this is the back door: + +``` +python3 cli.py broken.docx +python3 cli.py *.docx --quiet +``` + +## How it is tested + +Every test starts from a **real** DOCX this package wrote, then breaks it in one +specific way — truncated container, missing content-types part, unclosed tags, +bare ampersands and control characters, missing document part, not a ZIP at all, +empty body, and an illustrated document both whole and cut short. The assertion is not "it did not crash": the output is reopened as a +ZIP, every required part is checked, the body is re-parsed, and the recovered +table is compared cell by cell against the original. + +The tests that matter most are the ones about honesty: + +- a missing document part **fails**, and says why +- an empty body is a **failure**, not a success with no content +- no user-visible string overclaims +- structural parts the tool rebuilds are never reported as lost content +- the user-facing lists contain no jargon — no `ZIP`, `XML`, `CRC`, or + `central directory` leaking out of the engine into a worried person's summary + +## Where SuperDocs fits + +The four-call contract — upload · edit instruction · approve · export — is built +and tested in `docrepair/styled_export.py`, and runs as an optional second pass: + +``` +export SUPERDOCS_API_KEY=your-key-here +python3 cli.py broken.docx --via-superdocs +``` + +Recovered structure goes out as clean **HTML**, never raw Word XML — the docs are +explicit that there is no endpoint for the latter — so headings stay headings and +tables stay tables. The edit instruction restores formatting and forbids content +changes; each proposed change is approved through the HITL endpoint (with the +second parse the payload requires); the export comes back as a styled DOCX. + +**Every failure on this path degrades to the local rebuild**, and a test asserts +it for each one: a dead network, an exhausted allowance, no job id, an empty +export, or any unexpected exception. The engine has already produced a valid file +before this runs, and nothing here is allowed to take that away from the user. + +**The local rebuild is the default, and that is deliberate.** Someone whose +document is broken should not have to hand it to a third-party service, or wait +on an API, to find out whether anything survived. The offline path answers that +in milliseconds and costs nothing; the SuperDocs path is for producing a +polished, fully-styled export once you know there is something worth styling. + +Session state and uploads are held in memory for one download and then dropped. +Nothing is written to disk and nothing is retained. + +## Honest limitations + +- **The rebuilt file is deliberately plain.** Your original theme, fonts and + spacing cannot be recovered from a broken file, so it rebuilds with clean + headings, tables and body text rather than guessing at a design you had. +- **Pictures come back; their captions and wrapping do not.** An image is placed + inline where the body referenced it, or at the end when nothing survived to + say where it belonged. Floating positions, text wrap and captions are gone. +- **Footnotes, headers and footers come back as text, not as page furniture.** A + rebuild cannot put a footnote back at the foot of the page it belonged to, so + their text is set down at the end under its own heading and the report says so. +- **An image whose header cannot be read is placed at a stated default size** + rather than at a measurement nobody took. Its aspect ratio is left alone. +- **Comments and tracked changes are not recovered.** +- Direct upload only, so the practical ceiling is about 20 MB. +- `.doc` (the pre-2007 binary format) is not a ZIP at all and is not supported — + it is reported as unrepairable rather than silently mangled. + +## Testing it by hand + +The automated suite proves the engine does the right thing. It cannot tell you +whether the downloaded file opens in Word, whether the progress is perceptible, +or whether a non-technical person understands what they got back. + +`manual-test/index.html` is a checklist for exactly that — open it in a second +tab. Nineteen items: eight deliberately broken fixtures with their expected +results transcribed from real runs, four edge cases, and seven judgement calls +no test can make. Verdicts and notes persist across reloads, and it exports the +results as Markdown. + +Writing it found three defects the suite had missed: a total failure still +rendered a "what came through" claim, one problem was reported twice, and an +exception class name leaked into a stage line. All three are fixed, and the +suite now guards the stage log as well as the summary. + +## Shared code — stated plainly + +`docrepair/superdocs_client.py` is the same four-call client used by the +quota-aware agent (assigned build A). It is vendored into both so each stands +alone in the builds repository. Reuse is only a shortcut when it is hidden. + +## Credit + +Built by **Priyanshu Semwal** ([@Priyanshu2425](https://github.com/Priyanshu2425)) +for the SuperDocs engineer round, 2026. MIT licensed — see [LICENSE](LICENSE). + +Grounded throughout in the SuperDocs API documentation; where the task brief and +the documentation differed, the documentation won. diff --git a/use-cases/Priyanshu2425/word-doc-repair/cli.py b/use-cases/Priyanshu2425/word-doc-repair/cli.py new file mode 100644 index 00000000..e275d62a --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/cli.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Same engine as the web page, for people who live in a terminal. + +The web page is the product. This exists because a batch of fifty files is not +a drag-and-drop job, and because a shared engine means the two front doors can +never disagree about what was recovered. + + python3 cli.py broken.docx + python3 cli.py broken.docx -o fixed.docx + python3 cli.py *.docx --quiet + python3 cli.py broken.docx --via-superdocs # also style it through SuperDocs +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from docrepair import repair +from docrepair.docx import read_blocks + + +def _style_through_superdocs(r, quiet: bool): + """Optional second pass. Never allowed to cost the caller the local result.""" + import io + import os + import zipfile + + from docrepair.styled_export import styled_export + from docrepair.superdocs_client import HttpTransport, SuperDocsClient + + key = os.environ.get("SUPERDOCS_API_KEY") + if not key: + print(" SUPERDOCS_API_KEY is not set; keeping the plain rebuild.", file=sys.stderr) + return None + with zipfile.ZipFile(io.BytesIO(r.output)) as z: + blocks = read_blocks(z.read("word/document.xml")) + client = SuperDocsClient(HttpTransport(key)) + res = styled_export( + client, f"repair-{os.getpid()}", blocks, + on_progress=None if quiet else lambda s, m: print(f" {m}", file=sys.stderr), + ) + return res + + +def one(path: Path, out: Path | None, quiet: bool, via_superdocs: bool = False) -> bool: + data = path.read_bytes() + r = repair(data, path.name, + on_progress=None if quiet else lambda s, m: print(f" {m}", file=sys.stderr)) + + print(f"\n{path.name}") + print(f" {r.summary()}") + for line in r.recovered: + print(f" + {line}") + for line in r.lost: + print(f" - {line}") + + if not r.ok: + return False + + blob = r.output + if via_superdocs: + styled = _style_through_superdocs(r, quiet) + if styled and styled.ok: + blob = styled.output + qualifier = "" if styled.ops_confirmed else " estimated" + unit = "operation" if styled.ops_charged == 1 else "operations" + print(f" + styled through SuperDocs ({styled.ops_charged}{qualifier} {unit})") + elif styled: + for n in styled.notes[-1:]: + print(f" · {n}") + + dest = out or path.with_name(r.filename) + dest.write_bytes(blob) + print(f" → {dest}") + return True + + +def main(argv=None) -> int: + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("files", nargs="+", type=Path) + p.add_argument("-o", "--out", type=Path, help="output path (single input only)") + p.add_argument("-q", "--quiet", action="store_true", help="hide per-stage progress") + p.add_argument("--via-superdocs", action="store_true", + help="also style the result through SuperDocs (needs SUPERDOCS_API_KEY)") + a = p.parse_args(argv) + + if a.out and len(a.files) > 1: + p.error("--out takes a single input file") + + failures = 0 + for f in a.files: + if not f.exists(): + print(f"{f}: no such file", file=sys.stderr) + failures += 1 + continue + if not one(f, a.out, a.quiet, a.via_superdocs): + failures += 1 + + if failures: + print(f"\n{failures} of {len(a.files)} file(s) could not be repaired.", file=sys.stderr) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/use-cases/Priyanshu2425/word-doc-repair/docrepair/__init__.py b/use-cases/Priyanshu2425/word-doc-repair/docrepair/__init__.py new file mode 100644 index 00000000..fdd8740b --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/docrepair/__init__.py @@ -0,0 +1,4 @@ +from .engine import Repair, repair +from .docx import Block, read_blocks, write_docx, blocks_to_html + +__all__ = ["Repair", "repair", "Block", "read_blocks", "write_docx", "blocks_to_html"] diff --git a/use-cases/Priyanshu2425/word-doc-repair/docrepair/docx.py b/use-cases/Priyanshu2425/word-doc-repair/docrepair/docx.py new file mode 100644 index 00000000..82d11634 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/docrepair/docx.py @@ -0,0 +1,317 @@ +"""Read structure out of a damaged document part, and write a valid one back. + +The writer matters as much as the reader. A tool that salvages text and hands +back a .txt has not repaired anything -- the owner wanted a Word file. So this +builds a real DOCX: a correct `[Content_Types].xml`, the two relationship +parts, a stylesheet with actual heading styles, and a document body. + +The output is deliberately plain. Recovering someone's original theme is not +possible from a broken file, and pretending otherwise would be the "silent +claim of a full fix" the brief warns against. What it guarantees is a file that +opens, with headings that are headings and tables that are tables. +""" + +from __future__ import annotations + +import base64 +import re +import zipfile +from dataclasses import dataclass +from xml.etree import ElementTree as ET + +from . import media +from .media import Image +from .salvage import W + +def content_types(extensions: list[str] | None = None) -> str: + """Word will not open a package whose parts have no declared type, so an + image extension carried into the rebuild has to be declared here as well as + written into the archive. Missing this produces a file that opens on some + machines and is called corrupt on others, which is the worst of the three + possible outcomes.""" + defaults = "".join( + f'' + for e in sorted(set(extensions or [])) if e in media.CONTENT_TYPES + ) + return ( + '' + '' + '' + '' + + defaults + + '' + '' + "" + ) + + +CONTENT_TYPES = content_types() + +ROOT_RELS = """ + + +""" + +IMAGE_REL = ("http://schemas.openxmlformats.org/officeDocument/2006/" + "relationships/image") + + +def doc_rels(images: list[tuple[str, str]] | None = None) -> str: + """`rId1` is always the stylesheet; images take rId2 upwards.""" + extra = "".join( + f'' + for rid, target in (images or []) + ) + return ( + '' + '' + '' + + extra + "" + ) + + +DOC_RELS = doc_rels() + + +def _style(sid: str, name: str, size: int, bold: bool, outline: int | None) -> str: + outline_xml = f'' if outline is not None else "" + return ( + f'' + f'' + f'{outline_xml}' + f'' + f'{"" if bold else ""}' + ) + + +STYLES = ( + '' + '' + '' + '' + '' + + _style("Normal", "Normal", 22, False, None) + + _style("Title", "Title", 56, True, 0) + + _style("Heading1", "heading 1", 32, True, 0) + + _style("Heading2", "heading 2", 26, True, 1) + + _style("Heading3", "heading 3", 24, True, 2) + + "" +) + + +@dataclass +class Block: + kind: str # "heading" | "paragraph" | "table" | "image" + text: str = "" + level: int = 1 + rows: list[list[str]] | None = None + image: Image | None = None + + +def read_blocks(document_xml: bytes, targets: dict[str, str] | None = None, + images: dict[str, Image] | None = None) -> list[Block]: + """Parse the body into blocks, keeping the structure that survived. + + `targets` and `images` are what makes a picture land back where it was: the + relationship map says which file a drawing points at, and without it a + drawing is a reference to nothing. Both optional, because both parts can be + damaged, and a document with unplaceable images is still a document. + """ + targets = targets or {} + images = images or {} + root = ET.fromstring(document_xml) + body = root.find(f"{W}body") + if body is None: + return [] + + blocks: list[Block] = [] + for el in body: + if el.tag == f"{W}p": + for rid in media.embedded_ids(el): + img = images.get(targets.get(rid, "")) + if img is not None: + blocks.append(Block("image", text=img.name, image=img)) + text = _para_text(el) + if not text.strip(): + continue + level = _heading_level(el) + blocks.append( + Block("heading", text, level) if level else Block("paragraph", text) + ) + elif el.tag == f"{W}tbl": + rows = [] + for tr in el.findall(f"{W}tr"): + rows.append([_cell_text(tc) for tc in tr.findall(f"{W}tc")]) + if rows: + blocks.append(Block("table", rows=rows)) + return blocks + + +def _para_text(p: ET.Element) -> str: + return "".join(t.text or "" for t in p.iter(f"{W}t")) + + +def _cell_text(tc: ET.Element) -> str: + return " ".join(_para_text(p) for p in tc.findall(f"{W}p")).strip() + + +def _heading_level(p: ET.Element) -> int | None: + ppr = p.find(f"{W}pPr") + if ppr is None: + return None + style = ppr.find(f"{W}pStyle") + if style is None: + return None + val = style.get(f"{W}val", "") + m = re.fullmatch(r"[Hh]eading\s*([1-9])", val) + if m: + return int(m.group(1)) + if val.lower() in {"title"}: + return 1 + return None + + +def _esc(s: str) -> str: + return (s.replace("&", "&").replace("<", "<").replace(">", ">")) + + +def _p(text: str, style: str | None = None) -> str: + ppr = f'' if style else "" + return f"{ppr}{_esc(text)}" + + +def _table(rows: list[list[str]]) -> str: + out = [ + '' + '' + '' + + "".join( + f'' + for e in ("top", "left", "bottom", "right", "insideH", "insideV") + ) + + "" + ] + for row in rows: + out.append("") + for cell in row: + out.append( + '' + + _p(cell) + "" + ) + out.append("") + out.append("") + return "".join(out) + + +def image_blocks(blocks: list[Block]) -> list[Block]: + return [b for b in blocks if b.kind == "image" and b.image is not None] + + +def _rel_ids(blocks: list[Block]) -> dict[str, str]: + """One relationship id per distinct image, starting after the stylesheet. + + Keyed by member name rather than by position, so the same picture used twice + is carried once and referenced twice — which is how the original stored it. + """ + ids: dict[str, str] = {} + for b in image_blocks(blocks): + ids.setdefault(b.image.name, f"rId{len(ids) + 2}") + return ids + + +def build_document_xml(blocks: list[Block]) -> str: + rel_ids = _rel_ids(blocks) + body = [] + for n, b in enumerate(blocks, 1): + if b.kind == "image" and b.image is not None: + body.append(media.drawing_xml(rel_ids[b.image.name], b.image, n, + b.image.name.rsplit("/", 1)[-1])) + elif b.kind == "heading": + body.append(_p(b.text, f"Heading{min(max(b.level, 1), 3)}")) + elif b.kind == "table" and b.rows: + body.append(_table(b.rows)) + body.append(_p("")) + else: + body.append(_p(b.text)) + return ( + '' + '' + "" + "".join(body) + + '' + '' + "" + ) + + +def write_docx(blocks: list[Block]) -> bytes: + import io + + rel_ids = _rel_ids(blocks) + by_name = {b.image.name: b.image for b in image_blocks(blocks)} + extensions = [img.ext for img in by_name.values()] + + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z: + # mimetype-equivalent ordering is not required for OOXML, but the + # content types part must be present and first is conventional. + z.writestr("[Content_Types].xml", content_types(extensions)) + z.writestr("_rels/.rels", ROOT_RELS) + # document.xml goes early, as Word writes it. The order is not + # cosmetic: a truncated download loses the END of the file, so the part + # carrying the content is the one you want furthest from the cut. + z.writestr("word/document.xml", build_document_xml(blocks)) + z.writestr("word/_rels/document.xml.rels", + doc_rels([(rel_ids[n], n.split("word/", 1)[-1]) + for n in by_name])) + z.writestr("word/styles.xml", STYLES) + for name, img in by_name.items(): + # Already compressed. Deflating a PNG again costs time and saves + # nothing, and this runs while somebody is watching a progress bar. + z.writestr(name, img.data, zipfile.ZIP_STORED) + return buf.getvalue() + + +def blocks_to_html(blocks: list[Block], inline_images: bool = False, + image_budget: int = 3_000_000) -> str: + """For the SuperDocs path, which takes documents and HTML, never raw + Word XML -- the docs are explicit that there is no endpoint for that. + + `inline_images` is for the preview the page shows before anybody downloads + anything: the pictures have to be visible for the preview to answer the + question it exists to answer. They are embedded as data URIs up to a budget, + and past it the image is named rather than shown -- an image that silently + fails to load in a preview would read as an image that was not recovered. + """ + out = [] + spent = 0 + for b in blocks: + if b.kind == "image" and b.image is not None: + if not inline_images: + continue + if spent + len(b.image.data) > image_budget: + out.append( + '

An image was recovered and is in ' + "the file. It is not shown here because the preview would be " + "too large to load.

" + ) + continue + spent += len(b.image.data) + src = ("data:" + b.image.content_type + ";base64," + + base64.b64encode(b.image.data).decode("ascii")) + out.append(f'A picture recovered from your document') + elif b.kind == "heading": + lvl = min(max(b.level, 1), 3) + out.append(f"{_esc(b.text)}") + elif b.kind == "table" and b.rows: + rows = "".join( + "" + "".join(f"{_esc(c)}" for c in r) + "" + for r in b.rows + ) + out.append(f"{rows}
") + else: + out.append(f"

{_esc(b.text)}

") + return "\n".join(out) diff --git a/use-cases/Priyanshu2425/word-doc-repair/docrepair/engine.py b/use-cases/Priyanshu2425/word-doc-repair/docrepair/engine.py new file mode 100644 index 00000000..1f3b9e36 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/docrepair/engine.py @@ -0,0 +1,371 @@ +"""The repair, as stages someone can watch, and a report they can read. + +The stages are real branch points, not labels. What happens at each one depends +on what the previous one found, and the report says which path was taken -- +because "we recovered your text but lost your tables" and "we recovered +everything" should not look the same to the person who gets the file back. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Callable + +from . import docx, media, salvage +from .salvage import plural + +Progress = Callable[[str, str], None] # (stage, human-readable message) + +DOCUMENT_PART = "word/document.xml" +DOC_RELS_PART = "word/_rels/document.xml.rels" + +#: Parts that carry a person's words but are not the body of the document. +#: Each is recovered as text and set down at the end under its own heading, +#: because a rebuild cannot put a footnote back at the foot of the page it +#: belonged to -- and a footnote silently folded into the body would change what +#: the document says. +ASIDE_PARTS: list[tuple[str, str]] = [ + ("word/footnotes.xml", "Footnotes"), + ("word/endnotes.xml", "Endnotes"), + ("word/header", "Page header"), + ("word/footer", "Page footer"), +] + +#: Boilerplate Word writes into every footnotes part whether or not the document +#: has footnotes. Carrying it into the rebuild would invent an aside that never +#: existed. +_EMPTY_ASIDE = {"", "-", "\u2014"} + + +@dataclass +class Repair: + ok: bool = False + output: bytes = b"" + filename: str = "repaired.docx" + recovered: list[str] = field(default_factory=list) + lost: list[str] = field(default_factory=list) + stages: list[tuple[str, str]] = field(default_factory=list) + structure_preserved: bool = True + counts: dict[str, int] = field(default_factory=dict) + #: The rebuilt document rendered for reading, pictures included. Shown + #: before the download, never instead of it. + preview_html: str = "" + #: Whether the file's own index had to be gone around. Carried on the report + #: rather than passed between stages, because the stage that discovers it and + #: the stage that is allowed to say it are not the same one. + container_was_damaged: bool = False + + def as_payload(self, download: str | None = None) -> dict: + """The one shape this result takes on the wire. + + Here rather than in `web.py` because two callers need it — the endpoint + and the pytest that records the page's fixtures — and a hand-copied + second version is how a fixture drifts from the thing it imitates while + still claiming to be captured from it. That is BUG-015's shape, and the + capture's own docstring says *recorded exactly as the web endpoint + streams it*, which nothing was holding it to. + """ + return { + "ok": self.ok, + "summary": self.summary(), + "recovered": self.recovered, + "lost": self.lost, + "counts": self.counts, + "structure_preserved": self.structure_preserved, + "filename": self.filename, + "preview_html": self.preview_html, + "download": download if self.ok else None, + } + + def summary(self) -> str: + """Plain language. Never claims a complete repair.""" + if not self.ok: + reason = self.lost[0] if self.lost else "no readable content was found inside it" + reason = reason[0].upper() + reason[1:] + if not reason.endswith("."): + reason += "." + return ( + f"This file could not be repaired. {reason} " + "Nothing was invented to fill the gap — if there is another copy, " + "even an older one, that is the better starting point." + ) + head = ( + "Recovered what could be read and rebuilt it as a valid Word file. " + "This is a best-effort recovery, not a complete repair." + ) + if not self.structure_preserved: + head += ( + " The document's structure was too damaged to read, so the text was " + "extracted run by run — you are getting the words back, but the " + "headings, tables and formatting are gone." + ) + return head + + +class _Stop(Exception): + """A stage decided the repair cannot go further. + + Raised rather than returned so each stage can refuse from wherever it + discovers the refusal, and `repair` stays a narrative of the happy path with + the refusals visible in one place. Every one of them has already written its + reason into the report before it raises. + """ + + +def repair(data: bytes, filename: str = "document.docx", + on_progress: Progress | None = None) -> Repair: + """The whole repair, as the sequence of decisions it actually is. + + Each stage is its own function, because each one is a different judgement + about somebody's document and they are worth reading separately. What stays + here is the order, which is the part that has to be right: nothing is + claimed as recovered before it has been read, and nothing is written before + everything that could refuse has had its chance to. + """ + r = Repair(filename=_repaired_name(filename)) + + def stage(name: str, msg: str) -> None: + r.stages.append((name, msg)) + if on_progress: + on_progress(name, msg) + + try: + s = _open_container(data, r, stage) + _take_inventory(s, r, stage) + pictures = media.collect(s.members) + blocks, structure_read = _read_body(s, pictures, r, stage) + unplaced = _place_pictures(blocks, pictures, r, stage) + asides = _read_asides(s.members, stage) + blocks.extend(asides.blocks) + _count_what_came_through(blocks, r, stage, structure_read) + _account_for_pictures(pictures, unplaced, asides, r) + except _Stop: + return r + + stage("write", "Rebuilding a clean Word file…") + r.output = docx.write_docx(blocks) + # What is actually in the file, so somebody can look before they decide + # whether it was worth anything. From the research, on a paid repair tool: + # "It just said 'repair successful'. I didn't know if it had actually got + # anything." A claim with nothing behind it is what breaks trust. + r.preview_html = docx.blocks_to_html(blocks, inline_images=True) + r.ok = True + stage("done", "Done. The rebuilt file is ready to download.") + return r + + +def _open_container(data: bytes, r: Repair, stage: Progress) -> salvage.Salvage: + """Get at the parts, going around the index rather than through it.""" + stage("open", "Opening the file…") + s = salvage.salvage_members(data) + for note in s.notes: + stage("open", _sentence(note)) + # The stage log carries every diagnostic. "What came through" is the list a + # worried person reads, so it gets outcomes -- what happened to their + # content -- and not a narration of how the file was opened. The one line + # about the damaged index is held back until there is recovered content to + # attach it to, because on a total failure it would be an overclaim. + r.container_was_damaged = any("internal index was damaged" in n for n in s.notes) + r.lost.extend(s.unrecovered) + + if not s.members: + stage("open", "No readable parts were found inside the file.") + # `salvage` has already said this if it found nothing; saying it twice + # reads as two separate problems. + if not any("no readable parts" in l for l in r.lost): + r.lost.append("the file contained no readable parts") + raise _Stop + return s + + +def _take_inventory(s: salvage.Salvage, r: Repair, stage: Progress) -> None: + """What is here, what can be rebuilt, and what cannot be done without.""" + stage("inventory", f"Found {plural(len(s.members), 'section')} inside the document.") + missing = [p for p in ("[Content_Types].xml", "_rels/.rels", DOCUMENT_PART) + if p not in s.members] + rebuildable = [p for p in missing if p != DOCUMENT_PART] + if rebuildable: + stage("inventory", + f"Rebuilding {plural(len(rebuildable), 'missing structural part')}…") + r.recovered.append( + "rebuilt the file's internal structure, which was missing or damaged — " + "this is standard plumbing and carries none of your content" + ) + + if DOCUMENT_PART not in s.members: + stage("inventory", "The main document part is missing and cannot be rebuilt.") + r.lost.append( + "the main document part is missing entirely, so there is no text to recover" + ) + raise _Stop + + +def _read_body(s: salvage.Salvage, pictures: dict[str, media.Image], r: Repair, + stage: Progress) -> tuple[list[docx.Block], bool]: + """Repair the body, then read structure out of it — or fall back and say so.""" + stage("xml", "Checking the document body for damage…") + fixed, notes = salvage.repair_xml(s.members[DOCUMENT_PART]) + for n in notes: + stage("xml", _sentence(n)) + r.recovered.extend(notes) + if not notes: + stage("xml", "The document body was well-formed.") + + stage("read", "Reading headings, paragraphs and tables…") + targets = media.relationship_targets(s.members.get(DOC_RELS_PART)) + try: + blocks = docx.read_blocks(fixed, targets, pictures) + r.structure_preserved = True + except Exception: + stage("read", "The document's structure was too damaged to read. " + "Recovering the text on its own instead.") + blocks = [docx.Block("paragraph", t) + for t in salvage.text_runs(fixed) if t.strip()] + r.structure_preserved = False + r.lost.append( + "the document structure was unreadable, so headings, tables and " + "formatting could not be preserved — only the text was recovered" + ) + + if not blocks and not pictures: + stage("read", "No readable content was found in the document body.") + r.lost.append("the document body contained no readable text") + raise _Stop + return blocks, r.structure_preserved + + +def _place_pictures(blocks: list[docx.Block], pictures: dict[str, media.Image], + r: Repair, stage: Progress) -> list[media.Image]: + """Collect at the end the pictures the body could not be shown to reference. + + Either the part naming which file each reference points at did not survive, + or the text fell back to a run-by-run read that carries no positions. They + are still the owner's pictures, so they go at the end — an image silently + placed in the wrong paragraph is worse than one obviously placed last. + """ + placed = {b.image.name for b in docx.image_blocks(blocks)} + unplaced = [img for name, img in sorted(pictures.items()) if name not in placed] + if unplaced: + stage("read", f"Recovered {plural(len(unplaced), 'picture')} " + "whose original position could not be worked out.") + blocks.append(docx.Block("heading", "Pictures recovered from this document", + level=1)) + blocks.extend(docx.Block("image", text=img.name, image=img) for img in unplaced) + return unplaced + + +def _count_what_came_through(blocks: list[docx.Block], r: Repair, stage: Progress, + structure_read: bool) -> None: + r.counts = { + "headings": sum(1 for b in blocks if b.kind == "heading"), + "paragraphs": sum(1 for b in blocks if b.kind == "paragraph"), + "tables": sum(1 for b in blocks if b.kind == "table"), + "pictures": len(docx.image_blocks(blocks)), + } + parts = [plural(r.counts[k], k[:-1]) + for k in ("headings", "paragraphs", "tables", "pictures") + if r.counts[k]] + tally = parts[0] if len(parts) == 1 else ", ".join(parts[:-1]) + f" and {parts[-1]}" + stage("read", f"Recovered {tally}.") + if r.container_was_damaged: + r.recovered.insert(0, "read your content out of a file whose internal index " + "was damaged — the damage Word refuses to open") + if structure_read: + r.recovered.append(f"kept {tally}, with their structure intact") + else: + r.recovered.append(f"recovered the text of {plural(len(blocks), 'paragraph')}") + + +def _account_for_pictures(pictures: dict[str, media.Image], + unplaced: list[media.Image], asides: "Asides", + r: Repair) -> None: + """Say where the pictures and the asides ended up, and where they did not.""" + placed = len(pictures) - len(unplaced) + if placed: + r.recovered.append( + f"carried {plural(placed, 'picture')} back into the document, " + + ("where they were" if placed > 1 else "where it was") + ) + if unplaced: + r.recovered.append( + f"recovered {plural(len(unplaced), 'picture')} and set " + f"{'them' if len(unplaced) > 1 else 'it'} at the end, under a heading" + ) + them = "they are" if len(unplaced) > 1 else "it is" + belonged = "they belonged" if len(unplaced) > 1 else "it belonged" + r.lost.append( + f"the original position of {plural(len(unplaced), 'picture')} could not " + f"be recovered, so {them} collected at the end rather than put back " + f"where {belonged}" + ) + for label, count in asides.recovered: + r.recovered.append( + f"recovered the text of {plural(count, label.lower())}" + if count > 1 else f"recovered the {label.lower()}" + ) + if asides.blocks: + r.lost.append( + "footnotes, headers and footers could not be put back into their " + "original places, so their text is set down at the end of the " + "document under its own heading" + ) + for label in asides.unreadable: + r.lost.append(f"the {label.lower()} could not be read") + + +def _sentence(text: str) -> str: + text = text[0].upper() + text[1:] + return text if text.endswith((".", "…", "!", "?")) else text + "." + + +def _repaired_name(filename: str) -> str: + stem = filename.rsplit("/", 1)[-1] + if stem.lower().endswith(".docx"): + stem = stem[:-5] + return f"{stem}-repaired.docx" + + +@dataclass +class Asides: + """Text that belongs to the document but not to its body.""" + + blocks: list[docx.Block] = field(default_factory=list) + recovered: list[tuple[str, int]] = field(default_factory=list) + unreadable: list[str] = field(default_factory=list) + + +def _read_asides(members: dict[str, bytes], stage: Progress) -> Asides: + """Footnotes, endnotes, headers and footers, as text under their own heading. + + They live in their own parts, so they survive exactly the damage that + destroys the index — the same reason the pictures do. Dropping them was + losing content that was sitting right there. + + What this deliberately does not do is fold them into the body. A footnote + read as a paragraph changes what the document says, and a page header + repeated as body text reads as something the author wrote. They are set + down at the end, labelled, and the report says they are not where they were. + """ + out = Asides() + for prefix, label in ASIDE_PARTS: + names = sorted(n for n in members + if n == prefix or (prefix.endswith(("header", "footer")) + and n.startswith(prefix) + and n.endswith(".xml"))) + texts: list[str] = [] + for name in names: + try: + fixed, _ = salvage.repair_xml(members[name]) + runs = [t.strip() for t in salvage.text_runs(fixed) if t.strip()] + except Exception: + out.unreadable.append(label) + continue + texts.extend(t for t in runs if t not in _EMPTY_ASIDE) + if not texts: + continue + stage("read", f"Recovering the {label.lower()}…") + out.blocks.append(docx.Block("heading", f"{label}, recovered separately", + level=2)) + out.blocks.extend(docx.Block("paragraph", t) for t in texts) + out.recovered.append((label, len(texts))) + return out diff --git a/use-cases/Priyanshu2425/word-doc-repair/docrepair/media.py b/use-cases/Priyanshu2425/word-doc-repair/docrepair/media.py new file mode 100644 index 00000000..ff128cd1 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/docrepair/media.py @@ -0,0 +1,243 @@ +"""Getting the pictures back. + +A `.docx` keeps its images as ordinary members of the ZIP — `word/media/image1.png` +and so on — each with its own local file header, entirely separate from +`word/document.xml`. Which means they survive exactly the damage that destroys +the index, and a rebuild that drops them is throwing away bytes that were sitting +right there. + +That mattered to somebody. From the research, describing a report that broke: +*"it was a report with photos and a table of numbers"* — and later, on what the +recovery cost them, *"the photos I got off my phone again — the ones I still +had."* The words came back and the pictures did not, and re-sourcing the +pictures was most of the work. + +Two things this module refuses to do: + +* **Guess at a size.** Word needs an explicit extent in EMUs for an inline + image. The dimensions are read out of the image's own header — PNG, JPEG and + GIF all carry them in the first few bytes — and an image whose header cannot + be read gets a stated default rather than a fabricated measurement, with the + aspect ratio left alone rather than invented. +* **Pretend it knows where a picture went.** A drawing's position is recoverable + only when the relationship part survived to say which file the reference points + at. When it did not, the image is still returned — at the end, under its own + heading, with the report saying plainly that its original position could not be + recovered. An image silently placed in the wrong paragraph is worse than an + image obviously placed at the end. +""" + +from __future__ import annotations + +import re +import struct +from dataclasses import dataclass +from xml.etree import ElementTree as ET + +MEDIA_DIR = "word/media/" + +#: 914400 EMU to the inch, and images are authored at 96 DPI by convention. +EMU_PER_PX = 9525 +#: Six inches. Wider than the text column on A4 with the margins this rebuild +#: writes, so a large photograph is scaled down rather than running off the page. +MAX_WIDTH_EMU = 6 * 914400 +#: What an image gets when its header cannot be read: a square, stated as a +#: default in the code rather than presented as a measurement. +FALLBACK_PX = (480, 360) + +CONTENT_TYPES = { + "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", + "gif": "image/gif", "bmp": "image/bmp", "tif": "image/tiff", + "tiff": "image/tiff", "emf": "image/x-emf", "wmf": "image/x-wmf", + "svg": "image/svg+xml", "webp": "image/webp", +} + +_R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" +_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships" +_A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" + + +@dataclass(frozen=True) +class Image: + """One recovered picture.""" + + name: str # the member name inside the original, e.g. word/media/image1.png + data: bytes + width_px: int + height_px: int + measured: bool # False when the dimensions are the stated fallback + + @property + def ext(self) -> str: + return self.name.rsplit(".", 1)[-1].lower() if "." in self.name else "png" + + @property + def content_type(self) -> str: + return CONTENT_TYPES.get(self.ext, "application/octet-stream") + + @property + def extent(self) -> tuple[int, int]: + """Width and height in EMUs, scaled to fit the page, aspect preserved.""" + w = max(1, self.width_px) * EMU_PER_PX + h = max(1, self.height_px) * EMU_PER_PX + if w > MAX_WIDTH_EMU: + h = int(h * MAX_WIDTH_EMU / w) + w = MAX_WIDTH_EMU + return w, h + + +# -- reading a size out of the bytes ---------------------------------------- + +def image_size(data: bytes) -> tuple[int, int] | None: + """Width and height from the image's own header, or None. + + Deliberately hand-rolled over the three formats that account for almost + every image in a Word document. A dependency here would be a dependency in a + tool whose whole promise is that it runs offline, in milliseconds, for + somebody who is already having a bad day. + """ + if len(data) < 12: + return None + if data[:8] == b"\x89PNG\r\n\x1a\n" and len(data) >= 24: + w, h = struct.unpack(">II", data[16:24]) + return (w, h) if w and h else None + if data[:3] == b"GIF": + w, h = struct.unpack(" tuple[int, int] | None: + """Walk the segment chain to a start-of-frame marker. + + A JPEG's dimensions are not at a fixed offset — they sit in whichever SOF + segment the encoder wrote, after any number of application and quantisation + segments. Guessing an offset produces a plausible wrong number, which is the + kind of wrong this tool minds most. + """ + i, n = 2, len(data) + while i + 9 < n: + if data[i] != 0xFF: + i += 1 + continue + marker = data[i + 1] + if marker in (0xD8, 0x01) or 0xD0 <= marker <= 0xD7: + i += 2 + continue + if i + 4 > n: + return None + length = struct.unpack(">H", data[i + 2:i + 4])[0] + # SOF0-SOF15, excluding the four that are not frame headers. + if 0xC0 <= marker <= 0xCF and marker not in (0xC4, 0xC8, 0xCC): + if i + 9 > n: + return None + h, w = struct.unpack(">HH", data[i + 5:i + 9]) + return (w, h) if w and h else None + if length < 2: + return None + i += 2 + length + return None + + +def collect(members: dict[str, bytes]) -> dict[str, Image]: + """Every image that survived, keyed by its member name.""" + out: dict[str, Image] = {} + for name, data in members.items(): + if not name.startswith(MEDIA_DIR) or not data: + continue + size = image_size(data) + out[name] = Image( + name=name, data=data, + width_px=size[0] if size else FALLBACK_PX[0], + height_px=size[1] if size else FALLBACK_PX[1], + measured=size is not None, + ) + return out + + +# -- working out where they went -------------------------------------------- + +_REL_RE = re.compile( + rb'Id="(?P[^"]+)"[^>]*?Target="(?P[^"]+)"', re.S) + + +def relationship_targets(rels_xml: bytes | None) -> dict[str, str]: + """rId -> the member it points at, from `word/_rels/document.xml.rels`. + + Parsed properly when the part is well-formed and by regex when it is not, + because this part is as likely to be damaged as any other and half a map is + worth more here than none: an image whose relationship survived can go back + where it was, and the rest are still returned at the end. + """ + if not rels_xml: + return {} + targets: dict[str, str] = {} + try: + for rel in ET.fromstring(rels_xml): + rid, target = rel.get("Id"), rel.get("Target") + if rid and target: + targets[rid] = _normalise(target) + return targets + except ET.ParseError: + for m in _REL_RE.finditer(rels_xml): + targets[m.group("id").decode("utf-8", "replace")] = _normalise( + m.group("target").decode("utf-8", "replace")) + return targets + + +def _normalise(target: str) -> str: + """Relationship targets are relative to `word/`, and may say so the long way.""" + target = target.lstrip("/") + if target.startswith("word/"): + return target + while target.startswith("../"): + target = target[3:] + return f"word/{target}" + + +def embedded_ids(element: ET.Element) -> list[str]: + """The relationship ids of every image referenced inside this element.""" + ids: list[str] = [] + for blip in element.iter(f"{{{_A_NS}}}blip"): + rid = blip.get(f"{{{_R_NS}}}embed") or blip.get(f"{{{_R_NS}}}link") + if rid: + ids.append(rid) + # Older documents use VML rather than DrawingML, and a picture is a picture. + for imagedata in element.iter( + "{urn:schemas-microsoft-com:vml}imagedata"): + rid = imagedata.get(f"{{{_R_NS}}}id") + if rid: + ids.append(rid) + return ids + + +def drawing_xml(rel_id: str, image: Image, doc_pr_id: int, name: str) -> str: + """One inline image, as Word's own drawing markup. + + Written out longhand rather than templated from something shorter, because + every element in it is required: Word rejects an inline drawing missing an + extent, a docPr, a blipFill or a preset geometry, and rejecting it means the + owner's file does not open — which is the one outcome this build exists to + prevent. + """ + cx, cy = image.extent + return ( + "" + f'' + f'' + f'' + "" + '' + "" + f'' + f'' + "" + "" + f'' + '' + "" + "" + "" + ) diff --git a/use-cases/Priyanshu2425/word-doc-repair/docrepair/salvage.py b/use-cases/Priyanshu2425/word-doc-repair/docrepair/salvage.py new file mode 100644 index 00000000..abd8dd0a --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/docrepair/salvage.py @@ -0,0 +1,254 @@ +"""Getting content out of a DOCX that will not open. + +A DOCX is a ZIP of XML parts. It breaks in a handful of recognisable ways, and +each one needs a different move: + + * The ZIP central directory is damaged or truncated -- the file "is not a + valid archive". Members can often still be found by scanning for local file + headers, which is what `salvage_members` does. + * `word/document.xml` is malformed -- an unclosed tag, a raw `&`, a stray + control character. A strict parser refuses the whole document over one bad + byte, so we repair the common cases and then fall back to pulling text out + of the runs directly. + * A part is simply missing. `[Content_Types].xml` and the relationship parts + are rebuildable from scratch; the document body is not. + +Nothing here promises a full repair. Every function reports what it could not +do, because the owner finding out later is the failure mode this tool exists to +prevent. +""" + +from __future__ import annotations + +import io +import re +import zipfile +from dataclasses import dataclass, field + +W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}" + +# A local file header. Scanning for these recovers members when the central +# directory -- which is at the END of the file, and so is what a truncated +# download loses first -- is gone. +_LOCAL_HEADER = b"PK\x03\x04" + +# Standard OOXML plumbing. These are regenerated on write, so damage to them +# costs the owner nothing and must not be reported as lost content. +REBUILDABLE = { + "[Content_Types].xml", + "_rels/.rels", + "word/_rels/document.xml.rels", + "word/styles.xml", +} + +_ILLEGAL_XML = re.compile( + rb"[\x00-\x08\x0b\x0c\x0e-\x1f]" # control characters XML 1.0 forbids +) +# A bare ampersand: one not already starting a valid entity. +_BARE_AMP = re.compile(rb"&(?!#[0-9]+;|#x[0-9a-fA-F]+;|[a-zA-Z][a-zA-Z0-9]*;)") + + +def plural(n: int, noun: str, plural_form: str | None = None) -> str: + """Consumer copy, not log output. "1 table" and "2 tables", never "1 table(s)".""" + return f"{n} {noun}" if n == 1 else f"{n} {plural_form or noun + 's'}" + + +@dataclass +class Salvage: + members: dict[str, bytes] = field(default_factory=dict) + notes: list[str] = field(default_factory=list) + unrecovered: list[str] = field(default_factory=list) + + def note(self, msg: str) -> None: + self.notes.append(msg) + + def lost(self, msg: str) -> None: + self.unrecovered.append(msg) + + +def salvage_members(data: bytes) -> Salvage: + """Get whatever parts can be read out of the container.""" + out = Salvage() + + # The happy path first: a readable archive. + try: + with zipfile.ZipFile(io.BytesIO(data)) as z: + bad = z.testzip() + for name in z.namelist(): + try: + out.members[name] = z.read(name) + except Exception: + out.lost(f"one section of the file ({name}) is present but its data is unreadable") + if bad: + out.note("part of the file failed its integrity check; read what was readable") + elif out.members and not out.unrecovered: + out.note("the container opened cleanly") + return out + except (zipfile.BadZipFile, OSError): + out.note( + "the file's internal index was damaged — this is what Word reports as " + "a corrupt document — so the content was read directly from inside it" + ) + + # Damaged container. Re-opening a slice does not help -- a truncated ZIP has + # no central directory anywhere in it -- so read the local file headers by + # hand and inflate each member's stream directly. + found, partial = _recover_by_local_headers(data, out) + if found: + out.note(f"found and read {plural(found, 'section')} of the file this way") + else: + out.lost("no readable parts could be found inside the file") + return out + + +def _recover_by_local_headers(data: bytes, out: "Salvage") -> tuple[int, int]: + """Walk the local file headers and inflate what each one points at. + + The central directory sits at the END of a ZIP, so a truncated download + loses it first while the members themselves are still largely intact. Each + local header carries its own name, method and sizes, which is enough. + """ + import struct + import zlib + + found = partial = 0 + for match in re.finditer(re.escape(_LOCAL_HEADER), data): + start = match.start() + header = data[start:start + 30] + if len(header) < 30: + continue + try: + (_, _, flags, method, _, _, _, comp_size, uncomp_size, + name_len, extra_len) = struct.unpack(" tuple[bytes, list[str]]: + """Fix the XML faults a strict parser refuses to look past.""" + notes: list[str] = [] + fixed = raw + + if fixed.startswith(b"\xef\xbb\xbf"): + fixed = fixed[3:] + notes.append("removed a byte-order mark that was placed before the XML declaration") + + cleaned, n = _ILLEGAL_XML.subn(b"", fixed) + if n: + fixed = cleaned + notes.append(f"removed {plural(n, 'invalid character')} that were breaking the document") + + cleaned, n = _BARE_AMP.subn(b"&", fixed) + if n: + fixed = cleaned + notes.append(f"repaired {plural(n, 'stray ampersand')}") + + closed, added = _close_open_tags(fixed) + if added: + fixed = closed + notes.append( + f"closed {plural(len(added), 'element')} that the damage had left open") + + return fixed, notes + + +def _close_open_tags(raw: bytes) -> tuple[bytes, list[str]]: + """Close tags a truncated file left hanging. + + Deliberately simple: it only appends closing tags for elements still open at + the end. It does not try to reorder or invent content -- guessing at + structure is how a "repair" quietly changes what a document says. + """ + try: + text = raw.decode("utf-8", errors="replace") + except Exception: + return raw, [] + + stack: list[str] = [] + for m in re.finditer(r"<\s*(/?)([A-Za-z_][\w:.\-]*)([^>]*?)(/?)\s*>", text): + closing, name, attrs, self_closing = m.groups() + if name.startswith("?") or name.startswith("!"): + continue + if self_closing == "/": + continue + if closing == "/": + if stack and stack[-1] == name: + stack.pop() + elif name in stack: + while stack and stack.pop() != name: + pass + else: + stack.append(name) + + if not stack: + return raw, [] + tail = "".join(f"" for n in reversed(stack)) + return raw + tail.encode("utf-8"), list(reversed(stack)) + + +def text_runs(document_xml: bytes) -> list[str]: + """Last resort: pull the text out of `` runs with a regex. + + Used when the XML is too damaged to parse even after repair. It loses all + structure, which is exactly why the report says so rather than presenting + the result as a recovered document. + """ + return [ + _unescape(m.group(1).decode("utf-8", errors="replace")) + for m in re.finditer(rb"]*>(.*?)", document_xml, re.S) + if m.group(1).strip() + ] + + +def _unescape(s: str) -> str: + return (s.replace("<", "<").replace(">", ">") + .replace(""", '"').replace("'", "'").replace("&", "&")) diff --git a/use-cases/Priyanshu2425/word-doc-repair/docrepair/styled_export.py b/use-cases/Priyanshu2425/word-doc-repair/docrepair/styled_export.py new file mode 100644 index 00000000..66d39fc1 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/docrepair/styled_export.py @@ -0,0 +1,133 @@ +"""Take the recovered structure through SuperDocs to get a properly styled file. + +The local rebuild always runs first and always produces a valid file. This is +the optional second pass: hand the recovered structure to SuperDocs as HTML, +have it normalise the document, and export a styled DOCX. + +Why HTML and not the rebuilt .docx bytes: the documentation is explicit that +SuperDocs takes documents and HTML and that there is no endpoint for raw Word +XML. Sending clean HTML built from structure we have already parsed is both +supported and lossless in the directions that matter -- headings stay headings, +tables stay tables. + +The four calls, in order, are exactly the required contract: + upload -> edit instruction -> approve -> export +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from .docx import Block, blocks_to_html +from .superdocs_client import QuotaExhausted, SuperDocsClient + +INSTRUCTION = ( + "This document was recovered from a damaged file. Apply consistent heading " + "styles, table formatting and paragraph spacing. Do not add, remove or " + "reword any content -- only restore its formatting." +) + + +@dataclass +class StyledResult: + ok: bool = False + output: bytes = b"" + warnings: list = field(default_factory=list) + notes: list[str] = field(default_factory=list) + ops_charged: int = 0 + ops_confirmed: bool = False # the async endpoints return no usage block + + +def styled_export(client: SuperDocsClient, session_id: str, blocks: list[Block], + filename: str = "recovered.html", + on_progress=None) -> StyledResult: + """Best effort, and it never costs the caller their local result. + + Every failure path returns ok=False with a reason. The engine's own rebuild + has already succeeded by the time this runs, so a failure here degrades to + "you get the plain rebuild" rather than to "you get nothing". + """ + r = StyledResult() + + def say(msg: str) -> None: + r.notes.append(msg) + if on_progress: + on_progress("superdocs", msg) + + html = blocks_to_html(blocks) + + try: + # 1 -- upload. Free. + say("Sending the recovered content to SuperDocs for styling…") + client.upload(session_id, filename, html.encode("utf-8")) + + # 2 -- edit instruction, with a human gate on every proposed change. + started = client.edit(session_id, INSTRUCTION) + r.ops_charged += int(started.usage.get("ops_charged", 0)) + job_id = started.body.get("job_id") + if not job_id: + say("SuperDocs did not start a job; keeping the plain rebuild.") + return r + + # 3 -- wait. A long silence is still processing. + say("Waiting for SuperDocs to finish — large documents can take minutes.") + job = client.poll_job(job_id) + r.ops_charged += int(job.usage.get("ops_charged", 0)) + + if job.body.get("status") == "awaiting_approval": + from .superdocs_client import pending_changes + + changes = pending_changes(job.body) + if changes: + # Formatting-only changes on a document we just rebuilt: approve + # them. The instruction forbids content edits, so a change that + # rewrote text would be a bug on their side, not a judgement + # call on ours -- and the export below is what gets checked. + approved = client.approve( + session_id, job_id, + [{"change_id": c.get("change_id"), "approved": True} for c in changes], + ) + r.ops_charged += int(approved.usage.get("ops_charged", 0)) + say(f"Approved {len(changes)} formatting change(s).") + + # Approval is asynchronous: the call returns ok, then the job + # resumes and applies the change. Exporting before it settles + # returns the pre-edit document with a 200 and no warning. + settled = client.poll_job(job_id, deadline_s=300, interval_s=2) + r.ops_charged += int(settled.usage.get("ops_charged", 0)) + if settled.body.get("status") != "completed": + say("SuperDocs did not finish applying the changes; " + "keeping the plain rebuild.") + return r + + # 4 -- export. Free. + exported = client.export(session_id, "docx") + r.warnings = client.export_warnings(exported) + blob = exported.body.get("raw") if isinstance(exported.body, dict) else None + if not blob: + say("SuperDocs returned no file; keeping the plain rebuild.") + return r + r.output = blob if isinstance(blob, bytes) else bytes(blob) + r.ok = True + if not r.ops_charged: + # The async endpoints return no usage block (see BUG-017), so a + # zero here means "not reported", not "free". Saying "0 operations" + # about a billable request would be the same bluff in a new place. + r.ops_charged = 1 + r.ops_confirmed = False + else: + r.ops_confirmed = True + say("SuperDocs returned a styled file.") + if r.warnings: + say(f"The export completed with {len(r.warnings)} non-fatal warning(s).") + except QuotaExhausted: + say("The SuperDocs allowance is exhausted; keeping the plain rebuild.") + except TimeoutError: + # Not a failure on their side -- the job may still be running. But we + # cannot export until it settles, so the local rebuild is what ships. + say("SuperDocs did not finish applying the changes in time; " + "keeping the plain rebuild.") + except Exception: + # Deliberately no exception class name: this string reaches a user. + say("Styling through SuperDocs did not work; keeping the plain rebuild.") + return r diff --git a/use-cases/Priyanshu2425/word-doc-repair/docrepair/superdocs_client.py b/use-cases/Priyanshu2425/word-doc-repair/docrepair/superdocs_client.py new file mode 100644 index 00000000..97613835 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/docrepair/superdocs_client.py @@ -0,0 +1,273 @@ +"""The four-call contract: upload, edit instruction, approve, export. + +SHARED, AND VENDORED. This file is a byte-for-byte copy of +`quota-aware-agent/quota_aware_agent/client.py` below this docstring, so each +build stands alone in the builds repository. + +Vendoring has a cost and this project paid it: the multipart fix for BUG-015 +landed in the original and not here, so Build B's styling path still sent an +empty body and got a 422 long after Build A was working. `test_the_vendored +_client_has_not_drifted` now fails the build if the two copies diverge. +""" + +from __future__ import annotations + +import json +import time +from dataclasses import dataclass, field +from typing import Any, Callable, Protocol + +BASE = "https://api.superdocs.app" + +# Terminal and non-terminal job states, from the docs' job lifecycle. +_TERMINAL = {"completed", "failed", "cancelled"} +_NEEDS_HUMAN = "awaiting_approval" + + +class Transport(Protocol): + def request(self, method: str, path: str, **kw: Any) -> "Response": ... + + +@dataclass +class Response: + status: int + body: dict + headers: dict = field(default_factory=dict) + + @property + def usage(self) -> dict: + """The usage block rides on every chat response. It is the only way to + read the balance from an API-key context -- the account usage endpoints + reject `sk_` keys with a 401.""" + return self.body.get("usage", {}) or {} + + +class SuperDocsError(RuntimeError): + def __init__(self, status: int, body: Any) -> None: + super().__init__(f"SuperDocs returned {status}: {body}") + self.status = status + self.body = body + + +class QuotaExhausted(SuperDocsError): + """Raised only when the platform says so. Never inferred from our own count.""" + + +def _encode_multipart(files: dict, fields: dict) -> tuple[bytes, str]: + """Build a multipart/form-data body from {name: (filename, bytes)} plus + plain fields. Returns (body, content_type). + + A fixed boundary would collide with content that happens to contain it, so + it is derived from the payload -- deterministic for a given body, which + keeps requests reproducible, and vanishingly unlikely to appear inside it. + """ + import hashlib + + digest = hashlib.sha256() + for name, (filename, content) in sorted(files.items()): + digest.update(name.encode()) + digest.update(str(filename).encode()) + digest.update(content if isinstance(content, bytes) else str(content).encode()) + boundary = "----attest" + digest.hexdigest()[:24] + + out = bytearray() + for name, value in fields.items(): + out += f"--{boundary}\r\n".encode() + out += f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode() + out += str(value).encode("utf-8") + b"\r\n" + for name, (filename, content) in files.items(): + if isinstance(content, str): + content = content.encode("utf-8") + out += f"--{boundary}\r\n".encode() + out += (f'Content-Disposition: form-data; name="{name}"; ' + f'filename="{filename}"\r\n').encode() + out += f"Content-Type: {_guess_type(filename)}\r\n\r\n".encode() + out += content + b"\r\n" + out += f"--{boundary}--\r\n".encode() + return bytes(out), f"multipart/form-data; boundary={boundary}" + + +def _guess_type(filename: str) -> str: + import mimetypes + + return mimetypes.guess_type(str(filename))[0] or "application/octet-stream" + + +class HttpTransport: + """Real transport. Imported lazily so the package needs no HTTP library + installed to run its tests.""" + + def __init__(self, api_key: str, base: str = BASE, timeout: float = 300.0) -> None: + self._key = api_key + self._base = base + # ~300s is the platform gateway timeout for synchronous requests. + self._timeout = timeout + + def request(self, method: str, path: str, **kw: Any) -> Response: + import urllib.error + import urllib.request + + url = self._base + path + headers = {"Authorization": f"Bearer {self._key}"} + data = None + if "files" in kw: + # Upload is multipart/form-data, not JSON. Encoded here rather than + # with a library because this package has no dependencies -- and + # because getting it wrong is invisible: the request still sends, + # and the API answers 422 for a field it never received. + data, content_type = _encode_multipart(kw["files"], kw.get("data", {})) + headers["Content-Type"] = content_type + elif "json" in kw: + data = json.dumps(kw["json"]).encode() + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=self._timeout) as r: + raw = r.read() + body = json.loads(raw) if raw and r.headers.get_content_type() == "application/json" else {"raw": raw} + return Response(r.status, body, dict(r.headers)) + except urllib.error.HTTPError as e: + raw = e.read() + try: + body = json.loads(raw) + except Exception: + body = {"raw": raw.decode(errors="replace")} + return Response(e.code, body, dict(e.headers or {})) + + +def pending_changes(job_body: dict) -> list[dict]: + """Read the proposed changes off a job, whatever shape they arrive in. + + Two shapes exist and both are real: + * `GET /v1/jobs/{id}` returns `metadata.pending_changes` as a plain LIST + of change dicts. Verified against the live API 2026-08-19. + * The SSE `proposed_change_batch` event delivers an envelope whose + `content` is a JSON-encoded STRING needing a second parse. + + This lives in the client because both builds need it, and the copy that had + it written separately handled only the envelope -- so it crashed on the + shape the polling path actually returns. + """ + meta = job_body.get("metadata") or {} + pending = meta.get("pending_changes") + if pending is None: + for event in meta.get("intermediate_responses", []) or []: + if event.get("type") == "proposed_change_batch": + return parse_proposed_changes(event) + return [] + if isinstance(pending, list): + return list(pending) + if isinstance(pending, str): + return parse_proposed_changes({"content": pending}) + return parse_proposed_changes(pending) + + +def parse_proposed_changes(envelope: dict) -> list[dict]: + """Trap 1. The batch arrives as a JSON string inside `content`. + + A single-change turn still arrives as a one-element `changes[]`, so this + always returns a list and never special-cases the singular form. + """ + content = envelope.get("content") + if content is None: + return list(envelope.get("changes", [])) + batch = json.loads(content) if isinstance(content, str) else content + return list(batch.get("changes", [])) + + +class SuperDocsClient: + def __init__(self, transport: Transport, sleep: Callable[[float], None] = time.sleep) -> None: + self._t = transport + self._sleep = sleep + + def _check(self, r: Response) -> Response: + if r.status >= 400: + raise SuperDocsError(r.status, r.body) + if r.usage.get("quota_exhausted"): + # The current request still completed; further billable ones will not. + raise QuotaExhausted(r.status, r.body) + return r + + # --- call 0: the one authoritative balance read available to an agent key. + def whoami(self) -> Response: + return self._check(self._t.request("GET", "/v1/agents/whoami")) + + # --- call 1 of the contract: upload. + def upload(self, session_id: str, filename: str, content: bytes) -> Response: + return self._check( + self._t.request( + "POST", "/v1/documents/upload", + files={"file": (filename, content)}, data={"session_id": session_id}, + ) + ) + + # --- call 2: the edit instruction. + def edit(self, session_id: str, message: str, approval_mode: str = "ask_every_time") -> Response: + return self._check( + self._t.request( + "POST", "/v1/chat/async", + json={"session_id": session_id, "message": message, "approval_mode": approval_mode}, + ) + ) + + def job(self, job_id: str) -> Response: + return self._check(self._t.request("GET", f"/v1/jobs/{job_id}")) + + def poll_job(self, job_id: str, deadline_s: float = 600.0, interval_s: float = 2.0, + on_wait: Callable[[float, str], None] | None = None) -> Response: + """Trap 2. Silence is still processing. + + Returns as soon as the job is terminal *or* is waiting on a human. Gives + up only at an explicit deadline, and says how long it waited -- a slow + job is never reported as a crash. + """ + waited = 0.0 + while True: + r = self.job(job_id) + status = r.body.get("status", "") + if status in _TERMINAL or status == _NEEDS_HUMAN: + return r + if waited >= deadline_s: + raise TimeoutError( + f"job {job_id} was still '{status}' after {waited:.0f}s. " + "That is a deadline this client imposed, not a platform failure -- " + "the job may still be running." + ) + if on_wait: + on_wait(waited, status) + self._sleep(interval_s) + waited += interval_s + + # --- call 3: approve, item by item. + def approve(self, session_id: str, job_id: str, decisions: list[dict]) -> Response: + """`decisions` is a list of {change_id, approved, feedback?}. Sent as a + batch so a mixed approve/deny turn is one request, not one per change.""" + return self._check( + self._t.request( + "POST", f"/v1/chat/{session_id}/approve", + json={"job_id": job_id, "approved": True, "changes": decisions}, + ) + ) + + # --- call 4: export. Free, per the docs, and so never priced. + def export(self, session_id: str, fmt: str = "docx") -> Response: + r = self._check( + self._t.request("POST", "/v1/documents/export", + json={"session_id": session_id, "format": fmt}) + ) + return r + + @staticmethod + def export_warnings(r: Response) -> list: + """Exports can succeed with non-fatal issues, carried base64-encoded in + `X-Export-Warnings`. Surfaced rather than swallowed -- a dropped field + code is exactly the kind of thing a user should be told about.""" + import base64 + + header = r.headers.get("X-Export-Warnings") + if not header: + return [] + try: + return json.loads(base64.b64decode(header)) + except Exception: + return [] diff --git a/use-cases/Priyanshu2425/word-doc-repair/docrepair/web.py b/use-cases/Priyanshu2425/word-doc-repair/docrepair/web.py new file mode 100644 index 00000000..d8ec367d --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/docrepair/web.py @@ -0,0 +1,138 @@ +"""The web page. This is the product; the CLI is the same engine with a +different front door. + +Progress is streamed, not simulated. The repair runs on a worker thread and +pushes each stage onto a queue as it happens, and the response is newline- +delimited JSON the browser reads incrementally. A page that fakes a progress +bar after the work is already done is lying to the person watching it. +""" + +from __future__ import annotations + +import json +import queue +import threading +import uuid +from pathlib import Path + +from fastapi import FastAPI, HTTPException, UploadFile +from fastapi.responses import HTMLResponse, Response, StreamingResponse + +from .engine import Repair, repair + +app = FastAPI(title="Repair my broken Word doc") + +# Repaired files, held in memory briefly. Nothing is written to disk: the file +# someone uploads is their damaged document, and keeping it is not ours to do. +# +# Downloads are NOT one-shot. Clicking the button twice is the most ordinary +# thing a person does, and the first version dropped the file on the first +# click -- so the second attempt 404'd and their only copy was gone. Entries +# now survive for a short window and are evicted oldest-first. +_READY: "OrderedDict[str, tuple[str, bytes, float]]" = None # set below +MAX_BYTES = 20 * 1024 * 1024 +KEEP_SECONDS = 30 * 60 +KEEP_MOST_RECENT = 32 + +from collections import OrderedDict # noqa: E402 + +_READY = OrderedDict() + + +def _evict(now: float) -> None: + for token, (_, _, at) in list(_READY.items()): + if now - at > KEEP_SECONDS: + del _READY[token] + while len(_READY) > KEEP_MOST_RECENT: + _READY.popitem(last=False) + + +@app.get("/", response_class=HTMLResponse) +def index() -> str: + return (Path(__file__).parent.parent / "static" / "index.html").read_text() + + +@app.post("/api/repair") +async def api_repair(file: UploadFile): + data = await file.read() + if not data: + raise HTTPException(400, "That file is empty.") + if len(data) > MAX_BYTES: + raise HTTPException(413, "That file is larger than 20 MB.") + + q: queue.Queue = queue.Queue() + result: dict = {} + + def work() -> None: + try: + r = repair(data, file.filename or "document.docx", + on_progress=lambda stage, msg: q.put({"stage": stage, "message": msg})) + token = uuid.uuid4().hex + if r.ok: + import time as _time + + _evict(_time.monotonic()) + _READY[token] = (r.filename, r.output, _time.monotonic()) + result["report"] = r.as_payload( + f"/api/download/{token}" if r.ok else None) + except Exception as e: # never leak a stack trace to a consumer + # Built from a Repair rather than written out by hand, so this + # cannot become a second, drifting version of the wire shape -- + # and so the honesty guards, which run over what reaches the page, + # cover this path too. The exception type never travels: it is + # engine vocabulary, and BUG-020 was a class name reaching a + # worried person. + failed = Repair(filename="") + failed.lost.append( + "this file could not be read at all, and nothing was changed") + failed.stages.append(("open", f"stopped: {type(e).__name__}")) + result["report"] = failed.as_payload() + result["report"]["summary"] = ( + "Something went wrong while reading this file, so it was left " + "alone. Nothing was changed and nothing was saved.") + finally: + q.put(None) + + threading.Thread(target=work, daemon=True).start() + + def stream(): + while True: + item = q.get() + if item is None: + break + yield json.dumps(item) + "\n" + yield json.dumps({"done": True, **result.get("report", {})}) + "\n" + + return StreamingResponse(stream(), media_type="application/x-ndjson") + + +@app.get("/api/download/{token}") +def download(token: str) -> Response: + import time as _time + + _evict(_time.monotonic()) + if token not in _READY: + raise HTTPException( + 404, + "That repaired file is no longer being held. Repair the document again " + "to get a fresh copy — your original was never changed.", + ) + name, blob, _ = _READY[token] # repeat downloads are fine + return Response( + blob, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + headers={"Content-Disposition": f'attachment; filename="{name}"'}, + ) + + +def main() -> None: + import os + + import uvicorn + + port = int(os.environ.get("PORT", "8000")) + uvicorn.run(app, host="127.0.0.1", port=port) + + +if __name__ == "__main__": + main() diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/.gitignore b/use-cases/Priyanshu2425/word-doc-repair/frontend/.gitignore new file mode 100644 index 00000000..c05203af --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +tsconfig.tsbuildinfo diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/bundle-manifest.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/bundle-manifest.json new file mode 100644 index 00000000..95bd4036 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/bundle-manifest.json @@ -0,0 +1,4 @@ +{ + "sources": "af9620334e5416e6fa43a275f25b308e640cf61d18c92bd15432864af12a3ce6", + "files": 9 +} diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/index.html b/use-cases/Priyanshu2425/word-doc-repair/frontend/index.html new file mode 100644 index 00000000..dc0cfb16 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/index.html @@ -0,0 +1,17 @@ + + + + + + + Salvage — recover a Word file that will not open + + + +
+ + + diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/package-lock.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/package-lock.json new file mode 100644 index 00000000..95290cd7 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/package-lock.json @@ -0,0 +1,4093 @@ +{ + "name": "salvage-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "salvage-web", + "version": "1.0.0", + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "axe-core": "^4.10.2", + "jsdom": "^25.0.1", + "msw": "^2.7.0", + "playwright": "^1.62.1", + "typescript": "^5.7.2", + "vite": "^6.0.7", + "vite-plugin-singlefile": "^2.1.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.2.0.tgz", + "integrity": "sha512-SKXarWrYhtpqOEctf9XGCGy29QjsvJAM0Aq9ZR9z4Ns94OmpqudOly+aSEfNqUf9SwsQaUgY9+Z8hyzG0xX8fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.0", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.0.tgz", + "integrity": "sha512-+nnvFEXIB08CZNVXpvW3B+zHW96QXvGUjNKJ8NJIPqAZi5Kd4WhYt2S3C234ReepG1qw2HOlEUbjYVHBowXObA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.8", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.8.tgz", + "integrity": "sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.9", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz", + "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.5", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.5.tgz", + "integrity": "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.411", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", + "integrity": "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.15.0.tgz", + "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.11", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/msw/node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw/node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rettime": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", + "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/type-fest": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-singlefile": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", + "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">18.0.0" + }, + "peerDependencies": { + "rollup": "^4.59.0", + "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/package.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/package.json new file mode 100644 index 00000000..f8d43343 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "salvage-web", + "private": true, + "version": "1.0.0", + "type": "module", + "description": "Salvage \u2014 the web page for the DOCX repair tool.", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build && node scripts/manifest.mjs", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "axe-core": "^4.10.2", + "jsdom": "^25.0.1", + "msw": "^2.7.0", + "playwright": "^1.62.1", + "typescript": "^5.7.2", + "vite": "^6.0.7", + "vite-plugin-singlefile": "^2.1.0", + "vitest": "^3.0.0" + } +} diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/scripts/manifest.mjs b/use-cases/Priyanshu2425/word-doc-repair/frontend/scripts/manifest.mjs new file mode 100644 index 00000000..a3e96a59 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/scripts/manifest.mjs @@ -0,0 +1,40 @@ +/** + * Records what the committed bundle was built from. + * + * The bundle is committed so a reviewer with no Node still gets the product. + * The cost of that decision is that the file can fall behind its source and + * nobody notices, so the build writes a hash of every source file and a pytest + * recomputes it. Stale bundle, failed build. + */ +import { createHash } from "node:crypto"; +import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { join, relative } from "node:path"; + +const root = process.cwd(); +const roots = ["src", "index.html", "vite.config.ts", "package.json"]; + +function walk(path, out = []) { + const s = statSync(path); + if (s.isDirectory()) { + for (const entry of readdirSync(path).sort()) { + if (entry === "test" || entry === "fixtures" || entry === "node_modules") continue; + walk(join(path, entry), out); + } + } else { + out.push(path); + } + return out; +} + +const files = roots.flatMap((r) => walk(join(root, r))); +const hash = createHash("sha256"); +for (const file of files.sort()) { + hash.update(relative(root, file)); + hash.update(readFileSync(file)); +} + +writeFileSync( + join(root, "bundle-manifest.json"), + JSON.stringify({ sources: hash.digest("hex"), files: files.length }, null, 2) + "\n", +); +console.log("bundle-manifest written over", files.length, "source files"); diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/scripts/report-shot.mjs b/use-cases/Priyanshu2425/word-doc-repair/frontend/scripts/report-shot.mjs new file mode 100644 index 00000000..fd5cc933 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/scripts/report-shot.mjs @@ -0,0 +1,11 @@ +import { chromium } from "playwright"; +const [, , url, fixture, out, w = "900", h = "1200"] = process.argv; +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: +w, height: +h }, deviceScaleFactor: 2 }); +await page.goto(url, { waitUntil: "networkidle" }); +await page.setInputFiles('input[type="file"]', fixture); +await page.waitForSelector(".stamp", { timeout: 15000 }); +await page.waitForTimeout(700); +await page.screenshot({ path: out, fullPage: true }); +await browser.close(); +console.log("wrote", out); diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/App.tsx b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/App.tsx new file mode 100644 index 00000000..6b944b91 --- /dev/null +++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/App.tsx @@ -0,0 +1,257 @@ +/* ============================================================================ + DIRECTION CONTRACT + + THESIS: What an institution sends you when your item arrived damaged. This is + a recovery notice with a docket number and a stamped verdict, not the SaaS + uploader page — no hero, no feature cards, no dashed rectangle. + + OWN-WORLD: Cool form stock, black print, postal blue for the institution and + postal red for the damage. Airmail chevron tape edges anything we have taken + custody of. A rubber-stamped verdict lands once. Mono only for the record + line: filename, size, docket. + + STORY: Someone whose only copy will not open hands it over, watches it pass + through real stations, reads a stamped verdict and two lists — what came + through and what did not — and takes the file back. + + FIRST VIEWPORT: Wordmark, one sentence of what this is, the handover panel at + full width, and the custody promises directly beneath it: original never + changed, nothing kept, no account. + + FORM: Postal damaged-item recovery notice. Seventh on my grounded list, which + is the one the seed assigned (key e3d60a22, direction scope, persuade). + ========================================================================== */ +import { useCallback, useRef, useState } from "react"; +import { checkFile, repairFile, RepairError, type Report as ReportT, type Stage } from "./lib/repair"; +import { Report } from "./components/Report"; + +type Phase = "idle" | "working" | "done"; + +function docket(name: string): string { + // A docket number is a receipt, not an identifier the server knows about. + let h = 0; + for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) % 99991; + return `SLV-${String(h).padStart(5, "0")}`; +} + +function readableSize(bytes: number): string { + if (bytes < 1024) return `${bytes} bytes`; + if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +export default function App() { + const [phase, setPhase] = useState("idle"); + const [stages, setStages] = useState([]); + const [report, setReport] = useState(null); + const [problem, setProblem] = useState(null); + const [handed, setHanded] = useState<{ name: string; size: number } | null>(null); + const [over, setOver] = useState(false); + const input = useRef(null); + + const start = useCallback(async (file: File) => { + const refusal = checkFile(file); + setProblem(refusal); + if (refusal) return; + + setHanded({ name: file.name, size: file.size }); + setStages([]); + setReport(null); + setPhase("working"); + try { + const result = await repairFile(file, (s) => setStages((prev) => [...prev, s])); + setReport(result); + setPhase("done"); + } catch (e) { + setProblem( + e instanceof RepairError + ? e.message + : "Something went wrong before your file could be read. Nothing was changed.", + ); + setPhase("idle"); + } + }, []); + + const another = useCallback(() => { + setPhase("idle"); + setStages([]); + setReport(null); + setProblem(null); + setHanded(null); + if (input.current) input.current.value = ""; + }, []); + + return ( + <> +
+
+

+ Salvage. +

+ document recovery +
+ +
+
+
+ {phase === "idle" ? ( + <> +

Your Word file will not open.

+

+ Hand it over and this page will read whatever is still intact inside it, rebuild + what it can, and tell you plainly what it could not. +

+ + + + {problem ? ( +

+ That file was not taken. + {problem} +

+ ) : null} + +
    +
  • +