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..e256fed5
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/README.md
@@ -0,0 +1,363 @@
+# 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.
+
+
+
+## Setup and test
+
+**Zero setup to see it work.** No key, no network, no install — the transport is
+injected and the tests answer with a documented fake, so nothing here costs an
+operation:
+
+```bash
+git clone https://github.com/superdocsapp/superdocs-builds.git
+cd superdocs-builds/use-cases/Priyanshu2425/quota-aware-agent
+
+python3 -m pytest # 71 passed, 1 skipped — the one skip is the MCP protocol check
+python3 backend/demo.py # watch it plan, size the work, run it, and export
+```
+
+Python 3.10 or newer. There are **no runtime dependencies** — that is why the
+two commands above work on a clone with nothing installed.
+
+
+The MCP surface — one extra install
+
+```bash
+pip install -e ".[mcp]"
+export SUPERDOCS_API_KEY=your-key-here # placeholder; never commit a real key
+python3 -m quota_aware_agent.mcp_server # stdio
+```
+
+Or register it with a client:
+
+```bash
+claude mcp add quota-aware-agent \
+ --env SUPERDOCS_API_KEY=your-key-here \
+ -- python3 -m quota_aware_agent.mcp_server
+```
+
+With the SDK installed the suite runs **72 passed, nothing skipped** — the extra
+test starts the real server over stdio and drives it with a real client, because
+a server that imports cleanly and cannot start is worse than one that fails
+loudly.
+
+
+
+Against the real API — this one spends operations
+
+```bash
+export SUPERDOCS_API_KEY=your-key-here
+python3 backend/demo.py --live --sample 1 # one step, one operation
+```
+
+`--sample N` is the small-sample bound. Use it. Exports and `whoami` are free;
+edits are not.
+
+
+### Environment
+
+| Variable | Required | What it is |
+|---|---|---|
+| `SUPERDOCS_API_KEY` | only for `--live` and the MCP server | Your SuperDocs API key. An agent can create its own account with `POST /v1/agents/signup`. |
+| `QUOTA_AWARE_AGENT_LEDGER` | no | Where the operation ledger is kept. Defaults to `~/.quota-aware-agent/operations.jsonl`. **Set it per account** — two agents driving different accounts must not share one. |
+
+### What to run to check each claim
+
+| Claim | Command |
+|---|---|
+| It sizes work to fit and names what it dropped | `python3 backend/demo.py --scenario tight` |
+| It refuses to start rather than half-finish | `python3 backend/demo.py --scenario broke` |
+| It can refuse a partial run outright | `python3 backend/demo.py --scenario tight --refuse` |
+| It bounds anything that loops | `python3 backend/demo.py --sample 2` |
+| It says what it spent, and whether that adds up | `python3 backend/demo.py --receipt` |
+| A rerun does not pay twice | `python3 backend/demo.py --scenario tight --ledger /tmp/ops.jsonl` — **twice** |
+
+The second run of that last one declines to repeat the first run's work and says
+so. That is graceful re-entry shown rather than asserted.
+
+## What it uses from SuperDocs
+
+| Surface | Used for |
+|---|---|
+| `GET /v1/agents/whoami` | The one authoritative allowance read available to an API key. Free. |
+| `POST /v1/documents/upload` | The document. Multipart; the filename extension decides the parser. |
+| `POST /v1/chat/async` | The edit instruction, with `approval_mode: ask_every_time`. **The only billable call.** |
+| `GET /v1/jobs/{id}` | Polling. Silence is treated as processing, never as a crash. |
+| `POST /v1/chat/{session_id}/approve` | Approving proposed changes, item by item. |
+| `POST /v1/documents/export` | The finished file. Free, so it runs even when the run stopped early. |
+| **MCP** | The delivery surface — four tools, of which one costs anything. |
+
+## 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.
+```
+
+A person opens the document, sees whether the edit is there, and says so through
+`resolve_operation`. That exit has to exist: a step that is never retried
+automatically and cannot be resolved through the surface is a step that can
+never run again.
+
+**It knows whether a failed call could have been billed.** A refused connection
+proves the request never reached SuperDocs, so a rerun may safely repeat it. A
+read timeout proves nothing — the request very possibly arrived, was charged and
+applied an edit, and only the answer was lost. Collapsing those two into "it
+failed" is how a retry pays twice, so they are recorded as different things and
+the default answer is *we do not know*.
+
+**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.
+
+## How the work is priced, and why that is the whole ballgame
+
+SuperDocs bills a **request**: most bill one operation, and very large ones bill
+one per 25 sections edited. The floor of one is therefore *per request*, not per
+plan — and this agent sends one request per step.
+
+That distinction is not a rounding error. Four five-section edits pooled to
+twenty sections price as **one** operation and bill as **four**. An agent that
+prices them pooled reports "it fits", starts, and runs out partway through
+somebody's document — which is the exact failure this build exists to prevent,
+arriving through its own arithmetic. `estimate(changes, batched=False)` is what
+the agent uses, `batched=True` is what a publisher sending one request uses, and
+`QuotaAwareAgent.BATCHED` names which one this is so the planner and the
+executor cannot drift apart about what a step costs.
+
+## 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. Four 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. Given a `session_id` it also prices against the ledger, so work an earlier run already paid for is named rather than quoted. |
+| `run_work` | billable | Do the part that fits; say what was left out, what was already paid for, and what it cost. Takes HTML, or the bytes of a real `.docx` as `document_base64`. |
+| `resolve_operation` | free | A person's answer about a call that was started and never confirmed. Without it that step could never run again — a state the surface could enter and not leave. |
+
+Both `plan_work` and `run_work` take `when_it_does_not_fit`: `degrade` (do the
+highest-severity part that fits, the default) or `refuse` (start nothing rather
+than deliver a subset).
+
+**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.
+
+## Why the tests need no key
+
+The transport is injected. `tests/fake.py` implements the documented response
+shapes — including the double-parsed envelope, a `usage` block on billable
+responses, and the `400` the live upload endpoint returns for a filename whose
+extension disagrees with its bytes. The demo answers with the **same** fake the
+suite uses, so what a reviewer watches is what the tests assert; a second fake
+written only for the demo would be free to flatter the code that calls it.
+
+See **Setup and test** at the top for every command.
+
+## Shared core — stated plainly
+
+`backend/quota_aware_agent/budget.py` is shared, unchanged, with another system by the
+same author, where it guards the publisher that writes documents back to
+SuperDocs. It is vendored here rather than imported so this build stands alone
+in this 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.
+- **The ledger is not locked against two processes running at once.** It is read
+ at start-up and appended to, so two runs launched simultaneously against the
+ same steps can both see "not yet attempted" and both pay. Sequential reruns —
+ the crash-and-resume case it was built for — are safe. A file lock would close
+ the concurrent case and is not built.
+- **A failure whose outcome is unknown needs a person, by design.** The agent
+ will not guess between paying twice and leaving work undone, so those steps
+ stay blocked until someone answers with `resolve_operation`.
+- **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/backend/demo.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/demo.py
new file mode 100644
index 00000000..90524061
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/demo.py
@@ -0,0 +1,106 @@
+#!/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 backend/demo.py # offline, against the fake
+ python3 backend/demo.py --scenario tight # not enough allowance, it degrades
+ python3 backend/demo.py --scenario broke # no allowance, it refuses to start
+ python3 backend/demo.py --live # real API, needs SUPERDOCS_API_KEY
+
+ --sample N small-sample mode: run at most N steps
+ --receipt print the line items and whether they add up
+ --refuse refuse a partial run rather than deliver a subset
+ --ledger F keep the operation ledger in F, so running twice shows what
+ a rerun after a crash does NOT pay for a second time
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import sys
+from pathlib import Path
+
+# The offline demo answers with `tests/fake.py`, the same fake the suite uses,
+# so what a reviewer watches here is what the tests assert. That lives beside
+# this package rather than inside it, so the project root goes on the path --
+# the alternative is a second fake, and a fake nobody wrote cannot flatter the
+# code that calls it.
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+from quota_aware_agent.policy import Policy, WhenItDoesNotFit
+from quota_aware_agent import QuotaAwareAgent, Step, SuperDocsClient
+from quota_aware_agent.client import HttpTransport
+from quota_aware_agent.idempotency import OperationLedger
+
+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")
+ # HTML content, so an HTML name. SuperDocs picks its parser from the
+ # extension, so "contract.docx" holding HTML is a 400 on a live run.
+ p.add_argument("--file", default="contract.html")
+ p.add_argument("--receipt", action="store_true",
+ help="print the line items and whether they add up")
+ p.add_argument("--refuse", action="store_true",
+ help="refuse a partial run rather than deliver a subset")
+ p.add_argument("--ledger", default=None,
+ help="keep the operation ledger in this file, so a second "
+ "run sees what the first one paid for")
+ 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 {}))
+ policy = Policy(
+ reserve=1, max_steps=a.sample,
+ when_it_does_not_fit=(WhenItDoesNotFit.REFUSE if a.refuse
+ else WhenItDoesNotFit.DEGRADE),
+ )
+ agent = QuotaAwareAgent(client, policy=policy,
+ ledger=OperationLedger(a.ledger) if a.ledger else None)
+
+ 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/backend/quota_aware_agent/__init__.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/__init__.py
new file mode 100644
index 00000000..48338352
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/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/backend/quota_aware_agent/agent.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/agent.py
new file mode 100644
index 00000000..7360057d
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/agent.py
@@ -0,0 +1,534 @@
+"""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, provably_never_sent
+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."
+ ),
+ }
+
+ def settled(self, session_id: str, steps: list[Step]) -> tuple[
+ list[Step], list[str], list[str]]:
+ """Split requested work by what an earlier run already did with it.
+
+ Returns (still to do, already applied, started and never confirmed).
+
+ This exists so a plan and a run cannot disagree. Pricing work that an
+ earlier run already paid for quotes a number nobody will be charged,
+ and an agent deciding what it can afford against that number is being
+ told the wrong thing by the tool whose entire job is telling it the
+ right thing.
+ """
+ to_do: list[Step] = []
+ applied: list[str] = []
+ unconfirmed: list[str] = []
+ for step in steps:
+ record = self._ledger.get(operation_key(
+ session_id, step.step_id, step.instruction, step.sections))
+ if record.state is State.APPLIED:
+ applied.append(step.step_id)
+ elif record.state is State.IN_FLIGHT:
+ unconfirmed.append(step.step_id)
+ else:
+ to_do.append(step)
+ return to_do, applied, unconfirmed
+
+ # -- planning ---------------------------------------------------------
+ def read_allowance(self) -> Balance:
+ """The one moment the number is authoritative before any work begins."""
+ r = self._c.whoami()
+ if r.usage.get("quota_exhausted") or getattr(self._c, "quota_exhausted", False):
+ # whoami is free and is not refused by exhaustion, but the signal it
+ # carries is the authoritative one and has to reach the planner.
+ self._g.mark_exhausted()
+ 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", "")))
+
+ #: Each step is its own `POST /v1/chat/async`, so each one bills at least
+ #: one operation. Pooling their sections and dividing by 25 -- which is
+ #: right for a publisher that sends one request -- under-prices an agent
+ #: that sends several, and an under-priced plan is an agent starting work
+ #: it cannot finish. Named here so the planner and `_run_step` cannot drift
+ #: apart about what a step costs.
+ BATCHED = False
+
+ 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,
+ batched=self.BATCHED)
+ 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(),
+ batched=self.BATCHED)
+ 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)
+ # Priced only over work nobody has paid for yet. What an earlier run
+ # settled is named in the report, not quoted as a cost.
+ steps = self._set_aside_what_earlier_runs_settled(session_id, steps, report)
+ if not steps and (report.already_applied or report.needs_a_person):
+ # Nothing left to do because an earlier run did it, not because it
+ # would not fit. Reporting that as "nothing fits" would send a
+ # caller off to buy allowance it does not need.
+ return self._nothing_left_to_do(session_id, export_format, start, 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 _set_aside_what_earlier_runs_settled(
+ self, session_id: str, steps: list[Step], report: Report) -> list[Step]:
+ to_do, applied, unconfirmed = self.settled(session_id, steps)
+ for step_id in applied:
+ report.already_applied.append(step_id)
+ report.say(f"'{step_id}': an earlier run already applied this. "
+ "Not repeated, and not billed again.")
+ for step_id in unconfirmed:
+ report.needs_a_person.append(step_id)
+ report.say(
+ f"'{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 to_do
+
+ 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], batched=self.BATCHED)
+ report.say(
+ f"The full request would cost about {needed} operation(s). "
+ + ("It fits." if plan.complete else plan.rationale)
+ )
+ return plan
+
+ def _nothing_left_to_do(self, session_id: str, export_format: str,
+ start: Balance, report: Report) -> Report:
+ """Every requested step was settled by an earlier run.
+
+ Nothing is billed, and the export still runs, because the point of a
+ rerun after a crash is to walk away with the file.
+ """
+ report.stop(
+ StopReason.STARTED_AND_UNKNOWN if report.needs_a_person
+ else StopReason.ALREADY_APPLIED,
+ "Nothing was sent: every requested step was settled by an earlier "
+ "run. You were not charged again.",
+ )
+ try:
+ return self._export(session_id, export_format, start, report)
+ except Exception as e:
+ # The session may not exist on this key any more. Say which,
+ # rather than turning a rerun into a crash.
+ report.say(
+ f"The export could not be made for session '{session_id}': {e}. "
+ "Nothing was billed. Open the session in SuperDocs and export "
+ "from there.")
+ report.balance_at_end = self._g.remaining()
+ return report
+
+ 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 as e:
+ # Which of these two it is decides whether a rerun repeats the call.
+ # "It failed" is not enough information to answer that, so it is
+ # never the answer recorded: only a failure that PROVES the request
+ # never reached SuperDocs is marked repeatable. Everything else --
+ # a read timeout, a 5xx from a gateway, a dropped connection -- may
+ # have been accepted and billed, and is recorded as started and
+ # unconfirmed so a person looks at it instead of a retry paying
+ # for it twice.
+ if provably_never_sent(e):
+ self._ledger.failed(
+ key, f"the edit call was rejected before it was billed: {e}")
+ else:
+ self._ledger.never_learned(
+ key, f"the edit call did not return, and may have been "
+ f"accepted and billed: {e}")
+ 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/backend/quota_aware_agent/budget.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/budget.py
new file mode 100644
index 00000000..773be111
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/budget.py
@@ -0,0 +1,213 @@
+"""BudgetGuard -- never start work you cannot finish.
+
+SHARED CORE. This module is used unchanged by another system by the same
+author, where it guards the publisher that writes documents back to SuperDocs.
+It is vendored here rather than imported so this build stands alone in its own
+repository. Reuse is only a shortcut when it is hidden, so it is stated here and
+in the README.
+
+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 = ""
+ #: How the published work will be sent -- one request, or one each. Carried
+ #: on the plan so a price quoted at planning time and the price charged at
+ #: run time cannot come from two different models of the same work.
+ batched: bool = True
+
+ @property
+ def ops_to_publish(self) -> int:
+ return estimate(self.publish, batched=self.batched)
+
+ @property
+ def complete(self) -> bool:
+ return not self.defer
+
+
+def estimate(changes: list[Change], *, batched: bool = True) -> 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.
+
+ `batched` is the question the docs force and that a section count alone
+ cannot answer: **do these changes ride in one request, or in several?**
+ The docs price a *request* -- "most requests bill one operation; very large
+ ones bill one per 25 sections edited" -- so the floor of one applies per
+ request, not per plan.
+
+ * ``batched=True`` -- all of it goes in a single `POST /v1/chat/async`.
+ That is the publisher's shape, where a document write is one call.
+ * ``batched=False`` -- each change is its own call. That is the agent's
+ shape: one edit instruction per step, one request each.
+
+ Getting this wrong is not a rounding error. Four steps of five sections
+ pooled to twenty sections price as ONE operation, and then bill as FOUR --
+ so the agent reports "it fits", starts, and runs out partway through
+ somebody's document. That is the exact failure this build exists to
+ prevent, arriving through its own arithmetic.
+ """
+ if not batched:
+ return sum(estimate([c]) for c in changes)
+ 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 mark_exhausted(self) -> None:
+ """The platform said so on a call that was allowed to complete anyway.
+
+ Free calls are not refused when the allowance is gone, but the signal
+ they carry is still the authoritative one and must reach the planner --
+ otherwise the agent reads "exhausted" and plans as though it had not.
+ """
+ self._exhausted = True
+
+ 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,
+ *, batched: bool = True) -> 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.
+
+ `batched` says whether the published set is one request or one each;
+ see `estimate`. It is threaded through rather than defaulted quietly,
+ because a plan priced under the wrong model is a plan that fits on
+ paper and overruns in practice.
+ """
+ budget = self._balance.ops if remaining is None else remaining
+ needed = estimate(changes, batched=batched)
+
+ 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."
+ ),
+ batched=batched,
+ )
+
+ if needed <= budget:
+ return Plan(publish=list(changes), defer=[], rationale="", batched=batched)
+
+ 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], batched=batched) <= 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, batched=batched)} 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,
+ batched=batched)
diff --git a/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/client.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/client.py
new file mode 100644
index 00000000..4138d6fe
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/client.py
@@ -0,0 +1,425 @@
+"""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."""
+
+
+class TransportFailure(RuntimeError):
+ """The call did not produce a response, and we have to say which kind.
+
+ The distinction is the whole point. A connection that was refused means the
+ request never reached SuperDocs and cannot have been billed, so a rerun may
+ safely repeat it. A read that timed out means the request very possibly did
+ reach SuperDocs, was charged, and applied an edit -- we simply never heard
+ the answer. Collapsing the two into "it failed" is how a retry pays twice.
+ """
+
+ def __init__(self, message: str, *, never_sent: bool) -> None:
+ super().__init__(message)
+ self.never_sent = never_sent
+
+
+def provably_never_sent(exc: BaseException) -> bool:
+ """True only when the request cannot have been billed.
+
+ Deliberately conservative: the default answer is "we do not know", because
+ the cost of wrongly believing a call was billed is one step reported to a
+ person, and the cost of wrongly believing it was not is the user paying
+ twice and possibly getting the same edit applied twice.
+ """
+ import socket
+
+ if isinstance(exc, TransportFailure):
+ return exc.never_sent
+ if isinstance(exc, QuotaExhausted):
+ # The request that carried this signal completed; it was answered.
+ return False
+ if isinstance(exc, SuperDocsError):
+ # A 4xx was rejected before any work happened, so it was not billed.
+ # A 5xx may have come from a gateway that had already passed the
+ # request on, so it proves nothing.
+ return exc.status < 500
+ reason = getattr(exc, "reason", exc)
+ return isinstance(reason, (ConnectionRefusedError, socket.gaierror))
+
+
+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 = "----formdata" + 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 {}))
+ except urllib.error.URLError as e:
+ # Name the cause and the fix, and -- more importantly -- say whether
+ # the request could have been billed, so the ledger can record the
+ # truth rather than the convenient answer.
+ never_sent = provably_never_sent(e)
+ raise TransportFailure(
+ f"could not reach {self._base} ({e.reason}). "
+ + ("The connection was refused or the host did not resolve, so "
+ "the request never reached SuperDocs and was not billed — "
+ "check network access to api.superdocs.app and retry."
+ if never_sent else
+ "It is not known whether the request arrived, so it must not "
+ "be assumed unbilled — rerun and read what the operation "
+ "ledger reports about it."),
+ never_sent=never_sent,
+ ) from e
+ except TimeoutError as e:
+ # A read timeout is the ambiguous case by definition: the request
+ # went out and the answer never came back.
+ raise TransportFailure(
+ f"no response from {self._base} within {self._timeout:.0f}s. "
+ "SuperDocs may still be processing this — large documents and "
+ "the deepest model settings take minutes — so the request may "
+ "well have been accepted and billed. It is recorded as started "
+ "and unconfirmed rather than retried.",
+ never_sent=False,
+ ) from e
+
+
+#: What the upload endpoint parses each extension as. Verified against the live
+#: API 2026-08-20: the **filename decides the parser**, not the bytes. HTML sent
+#: as `report.docx` is answered `400 Invalid DOCX file: File is not a zip file`,
+#: and the same bytes as `report.html` are accepted. `.txt` is accepted too and
+#: parses the markup as literal text, which is worse than an error because it
+#: succeeds.
+_ZIP_EXTENSIONS = {".docx", ".xlsx", ".pptx", ".odt"}
+_PDF_EXTENSIONS = {".pdf"}
+_TEXT_EXTENSIONS = {".html", ".htm", ".txt", ".md", ".markdown", ".rtf"}
+
+
+def check_upload_name(filename: str, content: bytes) -> None:
+ """Refuse a filename whose extension disagrees with the bytes.
+
+ This is a deliberate hardcoded defence sitting in front of the intelligent
+ path, not a guess about what the caller meant. The live API decides how to
+ parse an upload from the extension alone, so `agent.run(..., "contract.docx",
+ html_bytes)` reads perfectly and fails at the platform with a message about
+ zip files, which names neither the cause nor the fix. Worse, the mismatch
+ that does NOT error — HTML uploaded as `.txt` — succeeds and quietly parses
+ the markup as literal text, and nobody finds out until the export.
+
+ So it is checked here, before anything is sent, and the error says which
+ two things disagreed and both ways to make them agree.
+ """
+ import os
+
+ ext = os.path.splitext(str(filename))[1].lower()
+ if not ext:
+ raise ValueError(
+ f"'{filename}' has no file extension. SuperDocs chooses how to parse "
+ "an upload from the extension, so give one — '.html' for HTML, "
+ "'.docx' for a Word file, '.pdf' for a PDF.")
+
+ looks_like_zip = content[:4] == b"PK\x03\x04"
+ looks_like_pdf = content[:4] == b"%PDF"
+
+ if ext in _ZIP_EXTENSIONS and not looks_like_zip:
+ raise ValueError(
+ f"'{filename}' is named as a Word-family file but the bytes are not "
+ "a zip archive, and SuperDocs parses uploads by extension — it would "
+ "answer '400 Invalid DOCX file: File is not a zip file'. Either send "
+ "the real .docx bytes, or rename this to '.html' if it is HTML.")
+ if ext in _PDF_EXTENSIONS and not looks_like_pdf:
+ raise ValueError(
+ f"'{filename}' is named as a PDF but the bytes do not begin with "
+ "'%PDF'. Send the real PDF bytes, or rename it to match what it is.")
+ if ext in _TEXT_EXTENSIONS and (looks_like_zip or looks_like_pdf):
+ raise ValueError(
+ f"'{filename}' is named as text but the bytes are a "
+ f"{'zip archive (a .docx, most likely)' if looks_like_zip else 'PDF'}. "
+ "This would be accepted and parsed as literal text rather than as a "
+ "document — rename it to match its contents.")
+
+
+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
+ #: Set once the platform has said the allowance is exhausted, including
+ #: when it said so on a free call that was allowed to complete anyway.
+ self.quota_exhausted = False
+
+ def _check(self, r: Response, *, billable: bool = True) -> Response:
+ """Raise on an error, and on the platform's own exhaustion signal.
+
+ `billable=False` marks the free calls -- whoami and export. An exhausted
+ allowance must not stop those: exports and downloads never cost
+ operations, and the reserve exists precisely to promise that the work
+ already done can still be exported. Raising here would break that
+ promise at the exact moment it matters, turning "you always end up with
+ a file" into "you end up with a session and an exception". The signal is
+ still recorded, so the caller stops spending; it just does not block a
+ call that costs nothing.
+ """
+ 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.
+ self.quota_exhausted = True
+ if billable:
+ raise QuotaExhausted(r.status, r.body)
+ return r
+
+ # --- call 0: the one authoritative balance read available to an agent key.
+ def whoami(self) -> Response:
+ # Free, and it is the call that tells you the allowance is gone. Being
+ # refused by the exhaustion it exists to report would be absurd.
+ return self._check(self._t.request("GET", "/v1/agents/whoami"),
+ billable=False)
+
+ # --- call 1 of the contract: upload.
+ def upload(self, session_id: str, filename: str, content: bytes) -> Response:
+ check_upload_name(filename, content)
+ 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:
+ """Free per the docs, so an exhausted allowance never blocks it."""
+ return self._check(
+ self._t.request("POST", "/v1/documents/export",
+ json={"session_id": session_id, "format": fmt}),
+ billable=False,
+ )
+
+ @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/backend/quota_aware_agent/idempotency.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/idempotency.py
new file mode 100644
index 00000000..e65a9bec
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/idempotency.py
@@ -0,0 +1,193 @@
+"""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:
+ """The call was never accepted, so it was never charged.
+
+ Only for failures that *prove* the request never reached the platform.
+ A failure that merely means we never heard the answer is a different
+ thing and must stay `IN_FLIGHT` -- see `never_learned`.
+ """
+ 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 never_learned(self, key: str, note: str = "") -> Record:
+ """The call may have been accepted, and we never found out.
+
+ A read timeout, a dropped connection mid-response, a 5xx from a gateway
+ that had already passed the request on: in every one of those the edit
+ may be applied and the operation may be charged. Marking them `FAILED`
+ makes them repeatable, and repeating them is the double-billing this
+ module exists to prevent -- so they stay `IN_FLIGHT` and are reported
+ to a person, exactly like a run that died mid-call.
+ """
+ prior = self.get(key)
+ return self._put(Record(
+ key=key, state=State.IN_FLIGHT, session_id=prior.session_id,
+ step_id=prior.step_id, job_id=prior.job_id,
+ note=note or "the call was sent and its outcome was never learned",
+ ))
+
+ 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/backend/quota_aware_agent/mcp_server.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/mcp_server.py
new file mode 100644
index 00000000..fff31bba
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/mcp_server.py
@@ -0,0 +1,472 @@
+"""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 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.
+ resolve_operation -- a person's answer about a call that was started and never
+ confirmed. Free. Without it that step could never run again,
+ which is a state the surface could enter and not leave.
+
+`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, State, operation_key
+from .policy import Policy, WhenItDoesNotFit
+
+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
+
+
+def _when_it_does_not_fit(value: str) -> WhenItDoesNotFit:
+ try:
+ return WhenItDoesNotFit(str(value).lower())
+ except ValueError:
+ raise ValueError(
+ f"{value!r} is not a way to handle work that does not fit. Use "
+ "'degrade' to do the highest-severity part that fits, or 'refuse' "
+ "to do none of it."
+ ) from None
+
+
+# -- the 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, session_id: str = "",
+ when_it_does_not_fit: str = "degrade") -> dict:
+ """Free. Spends nothing, changes nothing, and answers 'what fits?'.
+
+ Given a `session_id` it also consults the operation ledger, so work an
+ earlier run already paid for is named rather than priced. Without one it
+ can only price, and it says so — a plan that quietly assumes a clean slate
+ is the plan that disagrees with the run.
+ """
+ policy = Policy(reserve=reserve,
+ when_it_does_not_fit=_when_it_does_not_fit(when_it_does_not_fit))
+ agent = QuotaAwareAgent(_client(), policy=policy, ledger=_ledger())
+ balance = agent.read_allowance()
+ parsed = _steps(steps)
+ if session_id:
+ to_price, already_applied, unconfirmed = agent.settled(session_id, parsed)
+ else:
+ to_price, already_applied, unconfirmed = parsed, [], []
+ plan = agent.plan(to_price)
+ 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 to_price],
+ batched=QuotaAwareAgent.BATCHED),
+ "reserved": reserve,
+ "will_run": [c.row_id for c in plan.publish],
+ "will_defer": [c.row_id for c in plan.defer],
+ "already_applied_by_an_earlier_run": already_applied,
+ "started_and_never_confirmed": unconfirmed,
+ "fits_completely": plan.complete,
+ "explanation": plan.rationale or "The whole request fits inside the allowance.",
+ "priced_against_the_ledger": bool(session_id),
+ "pricing_note": (
+ "Each step is its own billable request, so each one costs at least "
+ "one operation — the sections only add to that."
+ + ("" if session_id else
+ " No session_id was given, so this price assumes none of these "
+ "steps has been run before. Pass the session_id you will run "
+ "against to have work an earlier run already paid for excluded.")
+ ),
+ "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] | None = None, reserve: int = 1,
+ max_steps: int | None = None, export_format: str = "docx",
+ document_base64: str = "", when_it_does_not_fit: str = "degrade") -> dict:
+ agent = QuotaAwareAgent(
+ _client(),
+ policy=Policy(reserve=reserve, max_steps=max_steps,
+ when_it_does_not_fit=_when_it_does_not_fit(when_it_does_not_fit)),
+ ledger=_ledger())
+ report = agent.run(session_id, filename,
+ _document_bytes(document_html, document_base64),
+ _steps(steps or []), 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(),
+ }
+
+
+def resolve_operation(session_id: str, step_id: str, instruction: str,
+ sections: int, applied: bool, note: str = "") -> dict:
+ """Close out a step an earlier run started and never confirmed.
+
+ Without this the surface has a state it can enter and never leave: a call
+ recorded as started-and-unconfirmed is never retried, which is correct, and
+ was never resolvable through the surface, which is not — the step could
+ never run again. Somebody has to open the document, see whether the edit is
+ there, and say so. This is where they say it.
+
+ It is deliberately not automatic. Choosing between paying twice and leaving
+ work undone is somebody's money and somebody's document.
+ """
+ ledger = _ledger()
+ key = operation_key(session_id, step_id, instruction, int(sections))
+ before = ledger.get(key)
+ if before.state is not State.IN_FLIGHT:
+ return {
+ "resolved": False,
+ "state": before.state.value,
+ "explanation": (
+ f"'{step_id}' is recorded as '{before.state.value}', not as "
+ "started-and-unconfirmed, so there is nothing to resolve. Only "
+ "a call that was sent and whose outcome was never learned needs "
+ "a person. Check that the session_id, step id, instruction and "
+ "section count match the run exactly — the ledger is keyed on "
+ "what the call is, not on an id you assigned."
+ ),
+ }
+ after = ledger.resolve(key, applied=applied, note=note)
+ return {
+ "resolved": True,
+ "state": after.state.value,
+ "explanation": (
+ f"'{step_id}' is now recorded as already applied; a later run will "
+ "not repeat it and will not be billed for it again."
+ if applied else
+ f"'{step_id}' is now recorded as never applied; a later run will "
+ "send it. Only say this if you looked at the document and the edit "
+ "is not there."
+ ),
+ }
+
+
+def _document_bytes(document_html: str, document_base64: str) -> bytes:
+ """The document to upload, from whichever form the caller had it in.
+
+ HTML is the convenient case and base64 is the necessary one: an agent
+ holding a real .docx has no lossless way to put it through a JSON string
+ field, and SuperDocs takes documents, not raw Word XML. Offering only the
+ HTML field quietly restricted this build to callers who had already
+ converted their file — which is most of the work.
+ """
+ import base64 as _b64
+ import binascii
+
+ if document_base64:
+ if document_html:
+ raise ValueError(
+ "give document_html or document_base64, not both — one of them "
+ "would be silently ignored and you would not know which.")
+ try:
+ return _b64.b64decode(document_base64, validate=True)
+ except (binascii.Error, ValueError) as e:
+ raise ValueError(
+ f"document_base64 is not valid base64 ({e}). Send the file's "
+ "bytes base64-encoded, not its text.") from None
+ if not document_html:
+ raise ValueError(
+ "no document was given. Pass document_html for HTML, or "
+ "document_base64 for the bytes of a .docx or .pdf.")
+ return document_html.encode("utf-8")
+
+
+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."},
+ "session_id": {"type": "string",
+ "description": "The session you will run against. Given one, "
+ "steps an earlier run already paid for are "
+ "excluded from the price instead of quoted."},
+ "when_it_does_not_fit": {
+ "type": "string", "enum": ["degrade", "refuse"], "default": "degrade",
+ "description": "degrade: do the highest-severity part that fits. "
+ "refuse: do none of it rather than deliver a subset."},
+ },
+ "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. "
+ "Use document_base64 instead for a real file."},
+ "document_base64": {"type": "string",
+ "description": "The document's bytes, base64-encoded — for "
+ "a .docx or .pdf you already hold. Give this "
+ "or document_html, never both."},
+ "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"},
+ "when_it_does_not_fit": {
+ "type": "string", "enum": ["degrade", "refuse"], "default": "degrade",
+ "description": "degrade: do the highest-severity part that fits and name "
+ "the rest. refuse: start nothing rather than deliver a "
+ "subset."},
+ },
+ "required": ["session_id", "filename", "steps"],
+ },
+ },
+ {
+ "name": "resolve_operation",
+ "description": (
+ "Close out a step that check_allowance or run_work reported as "
+ "'started and never confirmed'. Such a step is never retried automatically, "
+ "because retrying might be charged twice and might apply the same edit twice — "
+ "so a person opens the document, sees whether the edit is there, and records "
+ "the answer here. Free. Until it is resolved, that step will not run again."
+ ),
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "session_id": {"type": "string"},
+ "step_id": {"type": "string"},
+ "instruction": {"type": "string",
+ "description": "The instruction exactly as it was sent. The "
+ "ledger is keyed on what the call is, not on "
+ "an id you assigned."},
+ "sections": {"type": "integer", "minimum": 1,
+ "description": "The section count exactly as it was sent."},
+ "applied": {"type": "boolean",
+ "description": "true if the edit IS in the document — it will not "
+ "be sent or billed again. false if it is NOT — it "
+ "will be sent on the next run."},
+ "note": {"type": "string", "description": "What you saw, for the record."},
+ },
+ "required": ["session_id", "step_id", "instruction", "sections", "applied"],
+ },
+ },
+]
+
+_HANDLERS = {
+ "check_allowance": lambda a: check_allowance(),
+ "plan_work": lambda a: plan_work(
+ a["steps"], a.get("reserve", 1), a.get("session_id", ""),
+ a.get("when_it_does_not_fit", "degrade"),
+ ),
+ "run_work": lambda a: run_work(
+ a["session_id"], a["filename"], a.get("document_html", ""), a["steps"],
+ a.get("reserve", 1), a.get("max_steps"), a.get("export_format", "docx"),
+ a.get("document_base64", ""), a.get("when_it_does_not_fit", "degrade"),
+ ),
+ "resolve_operation": lambda a: resolve_operation(
+ a["session_id"], a["step_id"], a["instruction"], a["sections"],
+ bool(a["applied"]), a.get("note", ""),
+ ),
+}
+
+
+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. Everything except run_work is free and changes nothing. "
+ "If check_allowance reports work started and never confirmed, a "
+ "person has to look at the document and answer with "
+ "resolve_operation before that step can run again."
+ ),
+ 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/backend/quota_aware_agent/policy.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/policy.py
new file mode 100644
index 00000000..9b58b5bb
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/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/backend/quota_aware_agent/receipt.py b/use-cases/Priyanshu2425/quota-aware-agent/backend/quota_aware_agent/receipt.py
new file mode 100644
index 00000000..eb197efb
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/backend/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/pyproject.toml b/use-cases/Priyanshu2425/quota-aware-agent/pyproject.toml
new file mode 100644
index 00000000..f9187838
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/pyproject.toml
@@ -0,0 +1,28 @@
+[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]
+where = ["backend"]
+include = ["quota_aware_agent*"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+pythonpath = ["backend"]
+addopts = "-q"
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 00000000..01e86f5a
Binary files /dev/null and b/use-cases/Priyanshu2425/quota-aware-agent/screenshot.png differ
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..d6ff2daa
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/tests/fake.py
@@ -0,0 +1,131 @@
+"""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"}
+ ]})
+ name, body = files["file"]
+ # The live API parses by EXTENSION, not by sniffing the bytes: HTML
+ # sent as report.docx is answered 400. Modelled here because a fake
+ # that accepts any filename is how the demo shipped uploading HTML
+ # under a .docx name and nobody found out until a live run.
+ # Verified against the live API 2026-08-20.
+ if (str(name).lower().endswith((".docx", ".xlsx", ".pptx", ".odt"))
+ and not bytes(body)[:4] == b"PK\x03\x04"):
+ return Response(400, {"detail": "Invalid DOCX file: File is not a zip file"})
+ self.uploaded.append(name)
+ 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..6dd30305
--- /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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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.html", 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..aa0fc7a8
--- /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.html", 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.html", 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.html", 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.html", DOC, work)
+ assert len(first.completed) == 2
+ spent_first = first.receipt.counted
+
+ fake.remaining = 500
+ second = agent(fake, ledger=OperationLedger(path)).run("sess", "d.html", 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.html", 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.html", 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.html", 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.html", 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..5e99dd51
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_mcp_server.py
@@ -0,0 +1,168 @@
+"""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": "
", "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
+
+ # The package lives under backend/, so that is what has to be on the path.
+ # Pointing PYTHONPATH at the project root instead made this test pass only
+ # on a machine where the package was already pip-installed -- which is every
+ # machine except a stranger's fresh clone, the one case it exists to cover.
+ root = str(pathlib.Path(__file__).resolve().parents[1] / "backend")
+
+ 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",
+ "resolve_operation"}
+ assert "SUPERDOCS_API_KEY" in text # the failure is legible to the agent
diff --git a/use-cases/Priyanshu2425/quota-aware-agent/tests/test_the_gaps_that_were_found.py b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_the_gaps_that_were_found.py
new file mode 100644
index 00000000..c1b0cd4d
--- /dev/null
+++ b/use-cases/Priyanshu2425/quota-aware-agent/tests/test_the_gaps_that_were_found.py
@@ -0,0 +1,360 @@
+"""The nine things this build claimed and did not do.
+
+Each test here is named for the claim, not for the function, because each one
+was a gap between what the README said and what the code did. They are kept in
+one file so the next reader can see the class of mistake rather than nine
+unrelated regressions: **every one of them is the agent believing a number, or
+a state, that it had not actually established.**
+"""
+
+from __future__ import annotations
+
+import base64
+import socket
+import urllib.error
+
+import pytest
+
+from quota_aware_agent import QuotaAwareAgent, Step, SuperDocsClient
+from quota_aware_agent import mcp_server as m
+from quota_aware_agent.budget import Change, estimate
+from quota_aware_agent.client import (SuperDocsError, TransportFailure,
+ provably_never_sent)
+from quota_aware_agent.idempotency import OperationLedger, State, operation_key
+from quota_aware_agent.policy import Policy, StopReason
+from tests.fake import FakeSuperDocs
+
+DOC = b"
x
"
+
+
+def agent(fake, **kw):
+ return QuotaAwareAgent(SuperDocsClient(fake, sleep=lambda s: None), **kw)
+
+
+@pytest.fixture
+def mcp_fake(monkeypatch):
+ def _make(**kw):
+ f = FakeSuperDocs(**kw)
+ monkeypatch.setattr(m, "_client",
+ lambda: SuperDocsClient(f, sleep=lambda s: None))
+ return f
+ return _make
+
+
+@pytest.fixture(autouse=True)
+def _ledger_in_a_temp_file(tmp_path, monkeypatch):
+ monkeypatch.setattr(m, "LEDGER_PATH", str(tmp_path / "operations.jsonl"))
+
+
+# -- 1. the price of several small edits ------------------------------------
+
+def test_each_step_is_its_own_request_so_each_one_costs_at_least_an_operation():
+ """Four five-section edits are four requests, not twenty pooled sections.
+
+ Pooled, they price as one operation and bill as four — so the agent reports
+ "it fits", starts, and runs out partway through somebody's document. That is
+ the failure this whole build exists to prevent, arriving through its own
+ arithmetic.
+ """
+ small = [Change(f"s{i}", sections=5) for i in range(4)]
+ assert estimate(small, batched=True) == 1 # one request, twenty sections
+ assert estimate(small, batched=False) == 4 # four requests, one each
+ assert QuotaAwareAgent.BATCHED is False
+
+
+def test_the_plan_it_quotes_is_the_plan_it_can_afford_to_finish():
+ """It must never finish a run in a state its own opening sentence ruled out."""
+ fake = FakeSuperDocs(remaining=3, sections_per_edit=5)
+ report = agent(fake, policy=Policy(reserve=1)).run(
+ "sess", "f.html", DOC, [Step(f"s{i}", f"do {i}", 5) for i in range(4)])
+
+ assert "would cost about 4 operation(s)" in report.lines[1]
+ assert report.completed == ["s0", "s1"]
+ assert report.deferred == ["s2", "s3"]
+ # Named up front as deferred, not discovered at the reserve floor mid-run.
+ assert report.stop_reason is StopReason.COMPLETED
+
+
+# -- 2 & 3. a failure that may still have been billed -----------------------
+
+def test_a_call_that_may_have_been_billed_is_never_marked_repeatable(tmp_path):
+ """A read timeout means the request went out and the answer never came back.
+
+ Marking it FAILED makes it repeatable, and repeating it is the double
+ billing the ledger exists to prevent.
+ """
+ class TimesOut(FakeSuperDocs):
+ def request(self, method, path, **kw):
+ if path == "/v1/chat/async":
+ raise TimeoutError("the read timed out")
+ return super().request(method, path, **kw)
+
+ path = tmp_path / "ops.jsonl"
+ a = agent(TimesOut(remaining=50), policy=Policy(reserve=1),
+ ledger=OperationLedger(path))
+ with pytest.raises(TimeoutError):
+ a.run("sess", "f.html", DOC, [Step("s0", "do thing", 5)])
+
+ record = OperationLedger(path).get(operation_key("sess", "s0", "do thing", 5))
+ assert record.state is State.IN_FLIGHT
+ assert not record.repeatable
+
+
+def test_a_call_that_provably_never_arrived_is_repeatable(tmp_path):
+ """The other half of the same distinction. A refused connection was not
+ billed, and refusing to retry it would strand work for no reason."""
+ class Refused(FakeSuperDocs):
+ def request(self, method, path, **kw):
+ if path == "/v1/chat/async":
+ raise urllib.error.URLError(ConnectionRefusedError(61, "refused"))
+ return super().request(method, path, **kw)
+
+ path = tmp_path / "ops.jsonl"
+ a = agent(Refused(remaining=50), policy=Policy(reserve=1),
+ ledger=OperationLedger(path))
+ with pytest.raises(urllib.error.URLError):
+ a.run("sess", "f.html", DOC, [Step("s0", "do thing", 5)])
+
+ record = OperationLedger(path).get(operation_key("sess", "s0", "do thing", 5))
+ assert record.state is State.FAILED
+ assert record.repeatable
+
+
+def test_the_default_answer_about_a_failure_is_that_we_do_not_know():
+ """Conservative on purpose: the cost of being wrong is not symmetric."""
+ assert provably_never_sent(urllib.error.URLError(ConnectionRefusedError()))
+ assert provably_never_sent(urllib.error.URLError(socket.gaierror()))
+ assert provably_never_sent(SuperDocsError(422, {})) # rejected, unbilled
+ assert not provably_never_sent(SuperDocsError(502, {})) # gateway; unknown
+ assert not provably_never_sent(TimeoutError())
+ assert not provably_never_sent(RuntimeError("something else"))
+ assert provably_never_sent(TransportFailure("x", never_sent=True))
+
+
+# -- 4. the free call the reserve is a promise about ------------------------
+
+class _SaysExhaustedOnEveryResponse(FakeSuperDocs):
+ """The allowance is gone before the run starts, and the platform says so on
+ everything — including the calls that do not cost anything."""
+
+ def request(self, method, path, **kw):
+ r = super().request(method, path, **kw)
+ r.body.setdefault("usage", {})["quota_exhausted"] = True
+ return r
+
+
+class _RunsOutMidRunAndSaysSoOnTheExportToo(FakeSuperDocs):
+ """The sequence that actually happens: room at whoami, exhausted after the
+ first edit, and the exhaustion still being reported on the free export."""
+
+ def request(self, method, path, **kw):
+ r = super().request(method, path, **kw)
+ if path == "/v1/documents/export" and self._edits:
+ r.body.setdefault("usage", {})["quota_exhausted"] = True
+ return r
+
+
+def test_an_exhausted_allowance_never_blocks_the_export_it_promised():
+ """The reserve's whole argument is 'you always end up with a file'.
+
+ Exports are free, so an exhausted allowance has no business refusing one —
+ and a guarantee that breaks at the moment it is needed is not a guarantee
+ but a sentence in a README.
+ """
+ fake = _RunsOutMidRunAndSaysSoOnTheExportToo(
+ remaining=2, exhaust_after=1, sections_per_edit=25)
+ report = agent(fake, policy=Policy(reserve=0)).run(
+ "sess", "f.html", DOC, [Step("a", "first", 25), Step("b", "second", 25)])
+
+ assert report.stop_reason is StopReason.QUOTA_EXHAUSTED
+ assert ("POST", "/v1/documents/export") in fake.calls
+ assert any("Exported the document" in l for l in report.lines)
+
+
+def test_the_exhaustion_signal_reaches_the_planner_even_off_a_free_call():
+ """whoami is free and is not refused by exhaustion — but what it reports
+ still has to change what the agent does next, or the signal was cosmetic.
+ Nothing is uploaded, so there is nothing to export and nothing to clean up."""
+ fake = _SaysExhaustedOnEveryResponse(remaining=5, sections_per_edit=25)
+ a = agent(fake, policy=Policy(reserve=0))
+ a.read_allowance()
+ plan = a.plan([Step("a", "first", 25)])
+
+ assert plan.publish == []
+ assert "the allowance is exhausted" in plan.rationale
+
+
+# -- 5. the plan and the ledger --------------------------------------------
+
+def test_a_plan_does_not_quote_a_price_for_work_already_paid_for(mcp_fake):
+ fake = mcp_fake(remaining=5, sections_per_edit=25)
+ work = [{"id": "figures", "instruction": "Fix the figures.", "sections": 25},
+ {"id": "footer", "instruction": "Tidy the footer.", "sections": 25}]
+
+ m.dispatch("run_work", {"session_id": "s1", "filename": "f.html",
+ "document_html": "
x
", "steps": work})
+
+ plan = m.dispatch("plan_work", {"steps": work, "session_id": "s1"})
+ assert plan["already_applied_by_an_earlier_run"] == ["figures", "footer"]
+ assert plan["will_run"] == []
+ assert plan["full_request_costs"] == 0
+ assert plan["priced_against_the_ledger"] is True
+
+
+def test_a_plan_without_a_session_says_it_assumed_a_clean_slate(mcp_fake):
+ mcp_fake(remaining=5)
+ plan = m.dispatch("plan_work", {"steps": [
+ {"id": "a", "instruction": "Fix the figures.", "sections": 25}]})
+ assert plan["priced_against_the_ledger"] is False
+ assert "assumes none of these steps has been run before" in plan["pricing_note"]
+
+
+def test_a_rerun_of_finished_work_is_not_reported_as_not_fitting(mcp_fake):
+ """'Nothing fits' would send a caller off to buy allowance it does not need."""
+ fake = mcp_fake(remaining=5, sections_per_edit=25)
+ work = [{"id": "figures", "instruction": "Fix the figures.", "sections": 25}]
+ m.dispatch("run_work", {"session_id": "s1", "filename": "f.html",
+ "document_html": "
", "steps": work})
+ assert again["stop_reason"] == StopReason.ALREADY_APPLIED.value
+ assert again["already_applied_by_an_earlier_run"] == ["figures"]
+ assert again["receipt"]["operations_charged"] == 0
+ # It still walks away with the file.
+ assert ("POST", "/v1/documents/export") in fake.calls
+
+
+# -- 6. the state the surface could enter and not leave ---------------------
+
+def test_a_started_and_unconfirmed_step_can_be_closed_out_by_a_person(mcp_fake):
+ fake = mcp_fake(remaining=5, sections_per_edit=25)
+ led = OperationLedger(m.LEDGER_PATH)
+ key = operation_key("s1", "figures", "Fix the figures.", 25)
+ led.begin(key, session_id="s1", step_id="figures")
+
+ work = [{"id": "figures", "instruction": "Fix the figures.", "sections": 25}]
+ stuck = m.dispatch("run_work", {"session_id": "s1", "filename": "f.html",
+ "document_html": "
x
", "steps": work})
+ assert stuck["started_and_never_confirmed"] == ["figures"]
+ assert stuck["completed"] == []
+
+ told = m.dispatch("resolve_operation", {
+ "session_id": "s1", "step_id": "figures",
+ "instruction": "Fix the figures.", "sections": 25, "applied": False,
+ "note": "opened the document; the edit is not there"})
+ assert told["resolved"] is True
+
+ after = m.dispatch("run_work", {"session_id": "s1", "filename": "f.html",
+ "document_html": "
x
", "steps": work})
+ assert after["completed"] == ["figures"]
+
+
+def test_resolving_something_that_needs_no_resolution_says_so_rather_than_lying(mcp_fake):
+ mcp_fake(remaining=5)
+ out = m.dispatch("resolve_operation", {
+ "session_id": "s1", "step_id": "figures",
+ "instruction": "Fix the figures.", "sections": 25, "applied": True})
+ assert out["resolved"] is False
+ assert "nothing to resolve" in out["explanation"]
+
+
+# -- 7. refusing a partial run, from the surface ----------------------------
+
+def test_refusing_a_partial_run_is_reachable_from_the_surface(mcp_fake):
+ fake = mcp_fake(remaining=2, sections_per_edit=25)
+ work = [{"id": "figures", "instruction": "Fix the figures.", "sections": 25,
+ "severity": "critical"},
+ {"id": "footer", "instruction": "Tidy the footer.", "sections": 25,
+ "severity": "low"}]
+
+ out = m.dispatch("run_work", {"session_id": "s1", "filename": "f.html",
+ "document_html": "
x
", "steps": work,
+ "when_it_does_not_fit": "refuse"})
+ assert out["stop_reason"] == StopReason.REFUSED_PARTIAL.value
+ assert out["completed"] == []
+ assert ("POST", "/v1/documents/upload") not in fake.calls
+
+ plan = m.dispatch("plan_work", {"steps": work, "when_it_does_not_fit": "refuse"})
+ assert plan["will_run"] == []
+
+
+def test_an_unknown_way_of_not_fitting_names_the_two_that_exist(mcp_fake):
+ mcp_fake(remaining=5)
+ with pytest.raises(ValueError, match="'degrade'"):
+ m.dispatch("plan_work", {"steps": [], "when_it_does_not_fit": "explode"})
+
+
+# -- 8. a caller who holds a real file --------------------------------------
+
+def test_a_real_file_can_be_sent_as_bytes_not_only_as_html(mcp_fake):
+ fake = mcp_fake(remaining=5, sections_per_edit=25)
+ docx = b"PK\x03\x04 pretend this is a real docx"
+ m.dispatch("run_work", {
+ "session_id": "s1", "filename": "contract.docx",
+ "document_base64": base64.b64encode(docx).decode(),
+ "steps": [{"id": "a", "instruction": "Fix it.", "sections": 25}]})
+ assert fake.uploaded == ["contract.docx"]
+
+
+def test_two_documents_in_one_call_is_refused_rather_than_one_being_ignored(mcp_fake):
+ mcp_fake(remaining=5)
+ with pytest.raises(ValueError, match="not both"):
+ m._document_bytes("
x
", base64.b64encode(b"x").decode())
+
+
+def test_no_document_at_all_names_both_ways_to_give_one(mcp_fake):
+ mcp_fake(remaining=5)
+ with pytest.raises(ValueError, match="document_base64"):
+ m._document_bytes("", "")
+
+
+def test_bad_base64_is_named_as_bad_base64_not_as_a_failed_upload(mcp_fake):
+ mcp_fake(remaining=5)
+ with pytest.raises(ValueError, match="not valid base64"):
+ m._document_bytes("", "this is not base64!!!")
+
+
+# -- 9. the extension is what SuperDocs parses by ---------------------------
+
+def test_a_filename_that_disagrees_with_its_bytes_is_refused_before_it_is_sent():
+ """The live API chooses its parser from the extension alone.
+
+ HTML uploaded as `report.docx` comes back `400 Invalid DOCX file: File is
+ not a zip file` — a message that names neither the cause nor the fix, and
+ arrives only after a round trip. Verified live 2026-08-20, which is also how
+ this was found: the demo's own default filename was `contract.docx` holding
+ HTML, so `--live` could never have worked.
+ """
+ from quota_aware_agent.client import check_upload_name
+
+ with pytest.raises(ValueError, match="not a zip archive"):
+ check_upload_name("report.docx", b"
x
")
+ with pytest.raises(ValueError, match="do not begin with '%PDF'"):
+ check_upload_name("report.pdf", b"
x
")
+ with pytest.raises(ValueError, match="no file extension"):
+ check_upload_name("report", b"
x
")
+
+ # And the agreeing cases are not obstructed.
+ check_upload_name("report.html", b"
x
")
+ check_upload_name("report.docx", b"PK\x03\x04zip")
+ check_upload_name("report.pdf", b"%PDF-1.7")
+
+
+def test_the_mismatch_that_would_have_SUCCEEDED_is_refused_too():
+ """`.txt` holding a .docx is accepted by the platform and parsed as literal
+ text. A silent wrong answer is worse than a 400, so it is refused here."""
+ from quota_aware_agent.client import check_upload_name
+
+ with pytest.raises(ValueError, match="parsed as literal text"):
+ check_upload_name("report.txt", b"PK\x03\x04zip")
+
+
+def test_the_fake_refuses_the_same_upload_the_live_api_refuses():
+ """A fake that accepts any filename is how this shipped unnoticed."""
+ fake = FakeSuperDocs(remaining=50)
+ r = fake.request("POST", "/v1/documents/upload",
+ files={"file": ("report.docx", b"
x
")},
+ data={"session_id": "s1"})
+ assert r.status == 400
+ assert "not a zip file" in r.body["detail"]
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..031ac3f5
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/DESIGN.md
@@ -0,0 +1,85 @@
+# 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 second offer** — the styled copy, ruled off below the handover rather
+ than boxed in its own tape. Tape means *in our custody*, and there is only one
+ custody; a second taped panel would read as a second document. It is always
+ below the download and never above it, because the plain file is the promise
+ and this is the extra.
+- **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.
+- A step this copy of the page cannot take is never rendered as a button. It is
+ either genuinely available or it is one quiet sentence saying it is off and
+ that nothing else is affected.
+- The styled copy is never automatic and never replaces the plain one. Both
+ downloads stay on the page together.
+- 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..884929dd
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/README.md
@@ -0,0 +1,307 @@
+# 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.
+
+
+
+## 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.
+
+**And it checks the styled copy before offering it.** A styling pass has no
+reason to change a single word, so the file that comes back is compared against
+what went out, word for word, and thrown away if the wording moved at all. This
+is not theoretical: on the first live run, a four-line recovered report came
+back with three invented paragraphs, a subtotal row, a disclaimer and a
+signature block. It opened cleanly and read better than the plain rebuild, and
+it was partly fiction — which for a recovery tool is the worst output there is,
+because its owner would not notice.
+
+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 reachable from the page: once the plain file is downloadable, a
+**Send it for styling** button offers a second, styled copy. It is never
+automatic — someone whose document just broke should not have it sent to a
+third-party service because a page decided that for them — and the plain rebuild
+is already in their hands before the button exists.
+
+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).
+
+## Set it up
+
+**Requirements.** Python 3.10 or newer, and nothing else. Node is needed only to
+change the page — the built bundle is committed, so running it needs no Node and
+no network.
+
+```
+git clone && cd use-cases/Priyanshu2425/word-doc-repair
+
+python3 -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
+pip install -e ".[web,dev]"
+
+python3 -m docrepair.web # http://127.0.0.1:8000
+```
+
+Open the address it prints and drop a damaged `.docx` on the page. That is the
+whole product; everything below is optional.
+
+| | |
+|---|---|
+| `PORT=8077 python3 -m docrepair.web` | if 8000 is taken |
+| `SUPERDOCS_API_KEY=sk_… python3 -m docrepair.web` | also offers the styled copy (see [Where SuperDocs fits](#where-superdocs-fits)) |
+| no key set | the page says styling is off, in a line, and the rest works exactly as before |
+
+Nothing is written to disk and nothing is retained: uploads and repaired files
+are held in memory for one download window and then dropped.
+
+Have a folder of them rather than one? The CLI is the same engine:
+
+```
+python3 backend/cli.py broken.docx
+python3 backend/cli.py broken.docx -o fixed.docx
+python3 backend/cli.py *.docx --quiet
+python3 backend/cli.py broken.docx --via-superdocs # needs SUPERDOCS_API_KEY
+```
+
+## Test it
+
+Three commands, five files. **None of them needs an API key, a network or a
+bucket** — every SuperDocs failure and success is driven through an injected
+fake, and every DOCX the automated tests use is written by the tests themselves
+at run time and then broken in one specific way. (The eight files under
+`manual-test/fixtures/` are committed, because those are for testing by hand.)
+
+```
+python3 -m pytest # 63 pass, 1 skip — needs nothing installed
+pip install -e ".[web,dev]"
+python3 -m pytest # 76 pass — adds the endpoint tests
+cd frontend && npm install && npm test # 46 pass — the page, rendered and driven
+```
+
+| Suite | Count | What it holds |
+|---|---|---|
+| `tests/test_repair.py` | 31 | The engine, over a real DOCX broken eight specific ways. The assertion is never "it did not crash": the output is reopened, every required part checked, the body re-parsed, the table compared cell by cell against the original. |
+| `tests/test_styled_export.py` | 21 | The four SuperDocs calls, in order, and every way the path can fail — dead network, exhausted allowance, no job id, empty export, a job that never settles, an unreadable balance. Each one must degrade to the plain rebuild. |
+| `tests/test_web.py` | 13 | The two endpoints, called the way the page calls them. Skips rather than fails when the web extra is absent. |
+| `tests/test_frontend_fixtures.py` | 11 | Records the page's fixtures from real repairs and fails if they drift. |
+| `frontend/…/App.test.tsx` | 46 | The whole page rendered, answered with those recorded streams, driven through every path a person can take. |
+
+**The tests that matter most are the ones about honesty**, and they run over
+what is on the screen rather than over the engine's strings:
+
+- a missing document part **fails**, and says why
+- an empty body is a **failure**, not a success with no content
+- no user-visible string claims a complete, perfect or guaranteed repair —
+ with negations understood, because *"not a complete repair"* is the sentence
+ this build exists to say
+- structural parts the tool rebuilds are never reported as lost content
+- no engine vocabulary reaches a worried person — no `ZIP`, `XML`, `CRC`,
+ `central directory`, no exception class name
+- a styled copy whose wording changed is **thrown away**, not handed over
+
+That distinction — rendered output, not engine output — is not pedantry:
+BUG-014 was fixed in the engine and came straight back as BUG-020 in the page.
+
+**Working on the page itself:**
+
+```
+cd frontend
+npm install
+npm run dev # vite on 5173, /api proxied to the engine on 8000
+ # SALVAGE_API=http://127.0.0.1:8077 npm run dev if you moved it
+npm test # 46
+npm run build # rewrites backend/static/index.html AND bundle-manifest.json
+```
+
+The bundle is committed so a reviewer with no Node still gets the product. The
+cost of that decision is that it can fall behind its source and nobody notices,
+so the build writes a hash over every source file and a pytest recomputes it.
+**Stale bundle, failed build** — if you change anything under `frontend/src`,
+run `npm run build` before committing.
+
+**Checking it by hand.** Three pages, all openable straight from disk:
+
+| File | What it is |
+|---|---|
+| `manual-test/index.html` | The interactive checklist — 19 items, verdicts persist across reloads, exports as Markdown |
+| `manual-test/MANUAL_QA_PLAN.html` | The record of an actual run against a live server, with what could not be run marked *not run* rather than passed |
+| `manual-test/UI_FLOWS.html` | Every path through the page, screen by screen |
+| `SYSTEM_DESIGN.html` | How it is built and why — the architecture, the two paths, the failure matrix |
+
+`manual-test/make_fixtures.py` regenerates the eight broken files in
+`manual-test/fixtures/` if you want fresh ones.
+
+## 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 `backend/docrepair/styled_export.py`, and runs as an optional
+second pass. Set a key and the page offers it; the CLI takes a flag:
+
+```
+export SUPERDOCS_API_KEY=your-key-here
+python3 -m docrepair.web # the button appears on the result screen
+python3 backend/cli.py broken.docx --via-superdocs
+```
+
+Without a key the page says so in a line rather than showing a button that
+fails when somebody presses it: `GET /api/capabilities` is asked before anything
+is offered.
+
+**The allowance is read before anything is spent.** `GET /v1/agents/whoami`
+carries the remaining balance, and a styling pass that cannot finish is refused
+before the first billable call rather than discovered halfway through — trap 3,
+asked in advance. A balance that cannot be read is *not* treated as a balance of
+zero: a personal key is not an agent key, and refusing on a number nobody
+managed to read would be its own kind of bluff.
+
+**And the result is checked, not trusted.** `content_drift` compares the words
+that came back against the words that went out. Any addition or removal and the
+styled file is discarded with the reason said plainly. The instruction was
+tightened at the same time and the same document then came back word for word
+identical — but the instruction is the request and the guard is the promise.
+
+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.**
+- **The styled copy carries the recovered text and nothing else.** Pictures are
+ not sent for styling: the styling pass takes clean HTML, and an image with no
+ surviving position is not something a formatting pass can place. The plain
+ rebuild is the one that has your pictures in it.
+- **A styled copy is not always available.** It needs a key, an allowance, and a
+ result that comes back saying exactly what it was given. Any of the three
+ missing and you get the plain rebuild and a sentence saying why.
+- 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
+
+`backend/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/backend/cli.py b/use-cases/Priyanshu2425/word-doc-repair/backend/cli.py
new file mode 100644
index 00000000..e275d62a
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/backend/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/backend/docrepair/__init__.py b/use-cases/Priyanshu2425/word-doc-repair/backend/docrepair/__init__.py
new file mode 100644
index 00000000..fdd8740b
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/backend/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/backend/docrepair/docx.py b/use-cases/Priyanshu2425/word-doc-repair/backend/docrepair/docx.py
new file mode 100644
index 00000000..82d11634
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/backend/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.
+ {phase === "working"
+ ? "Reading your document"
+ : report?.ok
+ ? "Your document, as far as it could be read"
+ : "This file could not be repaired"}
+
+ ) : null}
+
+ {phase === "done" && report ? (
+ <>
+
+ {/* The verdict is what someone came for; the steps are what
+ they read only if they want to check the working. */}
+
+ What it did, step by step ({stages.length})
+
+ {stages.map((s, i) => (
+
+
+ {s.message}
+
+ ))}
+
+
+ >
+ ) : null}
+ >
+ ) : null}
+
+
+
+
+
+ There is a command-line version of the same engine for a folder of damaged files:{" "}
+ python3 backend/cli.py broken.docx
+
+
+
+
+ >
+ );
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Preview.tsx b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Preview.tsx
new file mode 100644
index 00000000..addfe64e
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Preview.tsx
@@ -0,0 +1,32 @@
+/**
+ * What is actually in the file, before anybody decides whether it was worth it.
+ *
+ * From the research, on a paid repair site: *"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."* They did not pay, and they
+ * rewrote the document by hand over a Sunday.
+ *
+ * A verdict is a claim. This is the thing itself — and it is the one part of
+ * this page whose honesty needs no wording at all.
+ *
+ * The markup comes from the engine, which escapes every piece of text it is
+ * given and emits no attribute it did not construct, so what lands here is the
+ * reader's own document and nothing else.
+ */
+export function Preview({ html }: { html: string }) {
+ if (!html.trim()) return null;
+ return (
+
+
+ What is in the file
+
+
+ Read it here before you download it. This is the rebuilt document itself, not a
+ description of it.
+
+
+
+
+
+ );
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Report.tsx b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Report.tsx
new file mode 100644
index 00000000..86f60280
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Report.tsx
@@ -0,0 +1,99 @@
+import type { Report as ReportT } from "../lib/repair";
+import { Preview } from "./Preview";
+import { Styling } from "./Styling";
+
+/**
+ * What happened, in the order a frightened person needs it: the verdict first,
+ * then what came through, then what did not, then the file.
+ *
+ * A failed repair renders no "what came through" claim at all. That was BUG-012:
+ * a total failure that still showed the heading is the precise overclaim this
+ * build's README says it does not make.
+ */
+export function Report({
+ report,
+ onAnother,
+ styling,
+}: {
+ report: ReportT;
+ onAnother: () => void;
+ /** Whether this copy of the page can style, and what to say if it cannot. */
+ styling: { available: boolean; note: string };
+}) {
+ const partial = report.ok && report.lost.length > 0;
+ const stamp = !report.ok ? "Could not be repaired" : partial ? "Recovered in part" : "Recovered";
+
+ return (
+
+
+ Saved as {report.filename}. You can download it more than once. This page holds
+ it for about thirty minutes and then lets it go — if the link stops working, repair the
+ document again. Your original was never changed.
+
+
+ );
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Styling.tsx b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Styling.tsx
new file mode 100644
index 00000000..562a1d06
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/components/Styling.tsx
@@ -0,0 +1,141 @@
+import { useState } from "react";
+import { RepairError, styleDocument, type Stage, type Styled } from "../lib/repair";
+
+/**
+ * The optional second file.
+ *
+ * It sits below the download, never above it, and it is never automatic. The
+ * plain rebuild is already in the person's hands by the time this appears —
+ * everything here is an offer of something more, and every failure it can have
+ * ends with them still holding what they already had.
+ *
+ * The waiting line is not decoration. The docs are explicit that a large
+ * document can take minutes with nothing visible happening, and a page that
+ * does not say so turns a slow success into a suspected crash.
+ */
+export function Styling({ url, available, note }: { url: string; available: boolean; note: string }) {
+ const [phase, setPhase] = useState<"idle" | "working" | "done">("idle");
+ const [stages, setStages] = useState([]);
+ const [styled, setStyled] = useState(null);
+ const [problem, setProblem] = useState(null);
+
+ if (!available) {
+ return note ?
+ The file above is plain on purpose. The fonts and spacing of your original could not be read
+ out of a damaged file, and this page will not guess at a design you had. SuperDocs can lay
+ the recovered headings, tables and paragraphs out with consistent styling instead. It is
+ told to change no words, only how they are set. What comes back is compared against what
+ went out, word for word, and thrown away if the wording moved at all — a recovered document
+ that reads well but says something you did not write is worse than a plain one. The plain
+ file above is untouched whatever happens.
+
+
+ {phase === "idle" ? (
+ <>
+
+
+ This sends the recovered text to SuperDocs. If you would rather it stayed on this
+ machine, the file above is already yours and nothing more needs to happen.
+
+ >
+ ) : null}
+
+ {phase !== "idle" ? (
+
+ {stages.map((s, i) => (
+
+
+ {s.message}
+
+ ))}
+
+ ) : null}
+
+ {phase === "working" ? (
+
+ Still working. A long document can take a few minutes, and a quiet wait is
+ normal.
+
+
+ Saved as {styled.filename}. It carries the same recovered content as the file
+ above, laid out with consistent styling — it recovers nothing extra. That cost{" "}
+ {cost}. The plain file above is untouched and still yours.
+ {styled.warnings > 0 ? (
+ <>
+ {" "}
+ The export reported {styled.warnings} non-fatal{" "}
+ {styled.warnings === 1 ? "warning" : "warnings"}, so compare it against the plain
+ file before you rely on it.
+ >
+ ) : null}
+
+ ) : null}
+
+ );
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/lib/repair.ts b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/lib/repair.ts
new file mode 100644
index 00000000..44768a6c
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/lib/repair.ts
@@ -0,0 +1,194 @@
+/**
+ * The one conversation this page has with the server.
+ *
+ * `POST /api/repair` answers with newline-delimited JSON: a stage line each
+ * time the engine reaches one, then a final line carrying the report. Reading
+ * it as a stream is the point -- someone watching their document being read
+ * needs to see it happening, not a spinner that could mean anything.
+ */
+
+export interface Stage {
+ stage: string;
+ message: string;
+}
+
+export interface Report {
+ ok: boolean;
+ summary: string;
+ recovered: string[];
+ lost: string[];
+ counts: Record;
+ structure_preserved: boolean;
+ filename: string;
+ /** The rebuilt document, rendered for reading. Empty on a failure, because
+ * there is nothing to preview and a preview of nothing is a claim. */
+ preview_html: string;
+ download: string | null;
+ /** Where the optional styling pass can be started. Null on a failure. */
+ style: string | null;
+}
+
+/** What this copy of the page can do. Asked before anything is offered, so the
+ * styling step is either genuinely available or plainly explained. */
+export interface Capabilities {
+ styling: boolean;
+ note: string;
+}
+
+export interface Styled {
+ ok: boolean;
+ notes: string[];
+ /** A styled file came back and was thrown away because its wording had
+ * changed. Not a failure of the service so much as a refusal by this page. */
+ rejected_for_content: boolean;
+ filename: string;
+ download: string | null;
+ ops_charged: number;
+ ops_confirmed: boolean;
+ allowance_known: boolean;
+ allowance_remaining: number;
+ warnings: number;
+}
+
+export const MAX_BYTES = 20 * 1024 * 1024;
+
+export class RepairError extends Error {}
+
+/** What we can tell before sending anything, said in the reader's words. */
+export function checkFile(file: File): string | null {
+ if (file.size === 0) {
+ return "That file is empty — there is nothing inside it to recover. If you have another copy, even an older one, try that instead.";
+ }
+ if (file.size > MAX_BYTES) {
+ return "That file is larger than 20 MB, which is more than this page can take.";
+ }
+ if (!/\.docx$/i.test(file.name)) {
+ // The advice that used to stand here was "open it in Word and save it as
+ // .docx" -- addressed to somebody whose Word will not open the file, which
+ // is why they are here. It is still the right first move when the file is
+ // merely old, so it stays; what was missing is the other half.
+ return "This page can only work on Word .docx files. If yours is a .doc, open it in Word once and save it as .docx — and if Word will not open it either, this page cannot recover that older format.";
+ }
+ return null;
+}
+
+export async function capabilities(): Promise {
+ try {
+ const res = await fetch("/api/capabilities");
+ if (!res.ok) throw new Error("unavailable");
+ return (await res.json()) as Capabilities;
+ } catch {
+ // A page that cannot ask does not offer. Silence here is not "yes".
+ return { styling: false, note: "" };
+ }
+}
+
+/**
+ * The second pass, and only when somebody asks for it.
+ *
+ * Answers in the same newline-delimited stream as the repair, because it is the
+ * same situation: something slow is happening to a person's document and they
+ * are entitled to watch it. A long silence here is still processing, which is
+ * what the waiting line on the page says.
+ */
+export async function styleDocument(
+ url: string,
+ onStage: (stage: Stage) => void,
+ signal?: AbortSignal,
+): Promise {
+ let res: Response;
+ try {
+ res = await fetch(url, { method: "POST", signal });
+ } catch {
+ throw new RepairError(
+ "The connection dropped. The rebuilt file above is unchanged and still yours.",
+ );
+ }
+ if (!res.ok || !res.body) {
+ let detail = "";
+ try {
+ const body = await res.json();
+ if (typeof body?.detail === "string") detail = body.detail;
+ } catch {
+ /* nothing more to learn */
+ }
+ throw new RepairError(
+ detail || "Styling could not be started. The rebuilt file above is unchanged and still yours.",
+ );
+ }
+ const final = await readStream(res, onStage);
+ if (!final) {
+ throw new RepairError(
+ "The connection dropped part-way through. The rebuilt file above is unchanged and still yours.",
+ );
+ }
+ return final as unknown as Styled;
+}
+
+/** One reader for both streams: stage lines, then a final line marked done. */
+async function readStream(
+ res: Response,
+ onStage: (stage: Stage) => void,
+): Promise | null> {
+ const reader = res.body!.getReader();
+ const decoder = new TextDecoder();
+ let buffer = "";
+ let final: Record | null = null;
+
+ for (;;) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+
+ let cut: number;
+ while ((cut = buffer.indexOf("\n")) >= 0) {
+ const line = buffer.slice(0, cut).trim();
+ buffer = buffer.slice(cut + 1);
+ if (!line) continue;
+ const parsed = JSON.parse(line) as Record;
+ if (parsed.done) final = parsed;
+ else onStage(parsed as unknown as Stage);
+ }
+ }
+ return final;
+}
+
+export async function repairFile(
+ file: File,
+ onStage: (stage: Stage) => void,
+ signal?: AbortSignal,
+): Promise {
+ const form = new FormData();
+ form.append("file", file, file.name);
+
+ let res: Response;
+ try {
+ res = await fetch("/api/repair", { method: "POST", body: form, signal });
+ } catch {
+ throw new RepairError(
+ "The connection dropped before your file could be read. Nothing was changed — your original is exactly as it was.",
+ );
+ }
+
+ if (!res.ok || !res.body) {
+ let detail = "";
+ try {
+ const body = await res.json();
+ if (typeof body?.detail === "string") detail = body.detail;
+ } catch {
+ /* nothing more to learn */
+ }
+ throw new RepairError(
+ detail || "This page could not take that file just now. Nothing was changed.",
+ );
+ }
+
+ const report = (await readStream(res, onStage)) as unknown as Report | null;
+
+ if (!report) {
+ throw new RepairError(
+ "The connection dropped part-way through. Nothing was changed — your original is exactly as it was.",
+ );
+ }
+ return report;
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/main.tsx b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/main.tsx
new file mode 100644
index 00000000..693ac2e7
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App";
+import "./styles.css";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/styles.css b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/styles.css
new file mode 100644
index 00000000..84224fcf
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/styles.css
@@ -0,0 +1,397 @@
+/* ============================================================================
+ Salvage — a postal recovery notice.
+
+ The world: what an institution sends you when your item arrives damaged. A
+ printed wrapper, chevron edge tape, a docket number, a rubber-stamped verdict,
+ and language that takes responsibility in plain words. It is the opposite of
+ a SaaS uploader, and it is deliberately nothing like the author's other work —
+ different product, different person, different day.
+
+ The person reading this has already lost something once. Every decision below
+ is made for them: large type, short lines, one thing at a time, bad news in
+ the same voice as good news.
+ ========================================================================== */
+
+:root {
+ --stock: #e9ebe6; /* cool form stock, not warm paper */
+ --sheet: #fcfcfa;
+ --ink: #14171a;
+ --ink-2: #494f55;
+ --ink-3: #7b8188;
+ --rule: #ccd1cd;
+ --rule-firm: #a9b0aa;
+ --post-blue: #173f8a; /* the institution */
+ --post-red: #c2222c; /* the damage */
+ --kept: #16603c;
+ --focus: #173f8a;
+
+ --t1: 12px;
+ --t2: 13px;
+ --t3: 15px;
+ --t4: 17px;
+ --t5: 21px;
+ --t6: 28px;
+ --t7: 38px;
+
+ --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ Helvetica, Arial, sans-serif;
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
+ --ease: cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --stock: #101314;
+ --sheet: #181b1d;
+ --ink: #eef1f0;
+ --ink-2: #b3b9bc;
+ --ink-3: #838a8e;
+ --rule: #2a2f31;
+ --rule-firm: #3d4346;
+ --post-blue: #7aa0e8;
+ --post-red: #ef7a72;
+ --kept: #6cc094;
+ --focus: #7aa0e8;
+ }
+}
+
+* { box-sizing: border-box; }
+html { -webkit-text-size-adjust: 100%; }
+body {
+ margin: 0;
+ background: var(--stock);
+ color: var(--ink);
+ font: var(--t3) / 1.6 var(--sans);
+}
+:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 2px; }
+
+/* The signature device: airmail chevron tape. It edges the notice and nothing
+ else, so it always reads as the boundary of the thing in our custody. */
+.tape {
+ height: 9px;
+ background: repeating-linear-gradient(
+ -45deg,
+ var(--post-red) 0 12px,
+ var(--sheet) 12px 24px,
+ var(--post-blue) 24px 36px,
+ var(--sheet) 36px 48px
+ );
+}
+
+.page { max-width: 660px; margin: 0 auto; padding: 0 18px 72px; }
+
+.brand {
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ padding: 20px 0 0;
+}
+.brand__mark {
+ font-size: var(--t5);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ margin: 0;
+}
+.brand__mark span { color: var(--post-blue); }
+.brand__line { font-size: var(--t1); color: var(--ink-3); letter-spacing: 0.04em; }
+
+.notice {
+ background: var(--sheet);
+ border: 1px solid var(--rule-firm);
+ margin-top: 16px;
+}
+.notice__body { padding: 22px 20px 26px; }
+@media (min-width: 620px) { .notice__body { padding: 30px 34px 34px; } }
+
+h1 {
+ font-size: var(--t6);
+ line-height: 1.18;
+ letter-spacing: -0.025em;
+ margin: 0 0 10px;
+ font-weight: 680;
+ text-wrap: balance;
+}
+@media (min-width: 620px) { h1 { font-size: var(--t7); } }
+.sub { font-size: var(--t4); color: var(--ink-2); margin: 0 0 24px; max-width: 46ch; }
+
+/* ------------------------------------------------------------- the handover */
+.dropzone {
+ display: block;
+ width: 100%;
+ border: 2px solid var(--ink);
+ background: var(--sheet);
+ padding: 30px 20px;
+ text-align: center;
+ cursor: pointer;
+ transition: background 140ms var(--ease), border-color 140ms var(--ease);
+}
+.dropzone:hover { background: var(--stock); }
+.dropzone.is-over { background: var(--stock); border-color: var(--post-blue); }
+.dropzone__title {
+ display: block;
+ font-size: var(--t5);
+ font-weight: 640;
+ letter-spacing: -0.015em;
+}
+.dropzone__hint { display: block; font-size: var(--t2); color: var(--ink-3); margin-top: 8px; }
+.dropzone input { position: absolute; width: 1px; height: 1px; opacity: 0; }
+
+.custody {
+ list-style: none;
+ margin: 20px 0 0;
+ padding: 0;
+ display: grid;
+ gap: 9px;
+ font-size: var(--t2);
+ color: var(--ink-2);
+}
+.custody li { display: grid; grid-template-columns: 16px 1fr; gap: 9px; align-items: start; }
+.custody b { color: var(--ink); font-weight: 620; }
+.custody i {
+ width: 9px; height: 9px; margin-top: 6px;
+ background: var(--post-blue);
+ font-style: normal;
+}
+
+/* --------------------------------------------------------------- the record */
+.record {
+ margin: 22px 0 0;
+ border-top: 1px solid var(--rule);
+ padding-top: 12px;
+ font-family: var(--mono);
+ font-size: var(--t1);
+ color: var(--ink-3);
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 20px;
+}
+.record b { color: var(--ink-2); font-weight: 500; }
+
+/* --------------------------------------------------------------- the stages */
+.stations { list-style: none; margin: 22px 0 0; padding: 0; }
+.steps { margin-top: 30px; border-top: 1px solid var(--rule); padding-top: 12px; }
+.steps summary {
+ font-size: var(--t2);
+ color: var(--ink-2);
+ cursor: pointer;
+ list-style-position: outside;
+}
+.steps summary:hover { color: var(--ink); }
+.steps .stations { margin-top: 10px; }
+.station {
+ display: grid;
+ grid-template-columns: 22px 1fr;
+ gap: 12px;
+ padding: 9px 0;
+ border-bottom: 1px solid var(--rule);
+ font-size: var(--t3);
+ animation: arrive 320ms var(--ease);
+}
+@keyframes arrive { from { opacity: 0; transform: translateY(-3px); } }
+@media (prefers-reduced-motion: reduce) { .station { animation: none; } }
+.station__mark {
+ width: 15px; height: 15px; margin-top: 5px;
+ border: 1.5px solid var(--post-blue);
+ border-radius: 50%;
+}
+.station--done .station__mark { background: var(--post-blue); }
+.station__text { color: var(--ink-2); }
+.station:last-child .station__text { color: var(--ink); }
+
+.working {
+ font-size: var(--t2);
+ color: var(--ink-3);
+ margin: 16px 0 0;
+ display: flex;
+ gap: 9px;
+ align-items: center;
+}
+.working__bar {
+ flex: 1;
+ height: 3px;
+ background: var(--rule);
+ overflow: hidden;
+ position: relative;
+}
+.working__bar::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ width: 38%;
+ background: var(--post-blue);
+ animation: sweep 1400ms var(--ease) infinite;
+}
+@keyframes sweep { from { transform: translateX(-100%); } to { transform: translateX(320%); } }
+@media (prefers-reduced-motion: reduce) { .working__bar::after { animation: none; width: 100%; opacity: 0.5; } }
+
+/* --------------------------------------------------------------- the verdict */
+.stamp {
+ display: inline-block;
+ border: 2.5px solid var(--post-blue);
+ color: var(--post-blue);
+ padding: 7px 14px 6px;
+ font-size: var(--t3);
+ font-weight: 760;
+ letter-spacing: 0.13em;
+ text-transform: uppercase;
+ transform: rotate(-2.2deg);
+ animation: land 420ms var(--ease);
+}
+.stamp--part { border-color: var(--post-blue); }
+.stamp--failed { border-color: var(--post-red); color: var(--post-red); transform: rotate(1.6deg); }
+@keyframes land {
+ from { opacity: 0; transform: rotate(-9deg) scale(1.14); }
+}
+@media (prefers-reduced-motion: reduce) { .stamp { animation: none; } }
+
+.verdict { font-size: var(--t4); line-height: 1.55; margin: 18px 0 0; max-width: 48ch; }
+
+.outcome { margin: 26px 0 0; }
+.outcome__head {
+ font-size: var(--t1);
+ font-weight: 720;
+ letter-spacing: 0.11em;
+ text-transform: uppercase;
+ color: var(--ink-2);
+ margin: 0 0 8px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid var(--rule-firm);
+}
+.outcome__list { list-style: none; margin: 0; padding: 0; }
+.outcome__list li {
+ display: grid;
+ grid-template-columns: 20px 1fr;
+ gap: 10px;
+ padding: 8px 0;
+ border-bottom: 1px solid var(--rule);
+ font-size: var(--t3);
+}
+.outcome__list i { font-style: normal; margin-top: 2px; font-weight: 700; }
+.outcome--kept i { color: var(--kept); }
+.outcome--lost i { color: var(--post-red); }
+
+.handoff {
+ margin: 28px 0 0;
+ border: 1px solid var(--rule-firm);
+ background: var(--stock);
+}
+.handoff__body { padding: 18px 18px 20px; }
+.btn {
+ font: inherit;
+ font-size: var(--t4);
+ font-weight: 640;
+ background: var(--post-blue);
+ color: #fff;
+ border: 0;
+ padding: 13px 22px;
+ cursor: pointer;
+ display: inline-block;
+ text-decoration: none;
+ transition: filter 120ms var(--ease);
+}
+.btn:hover { filter: brightness(1.12); }
+.btn--quiet {
+ background: none;
+ color: var(--ink);
+ border: 1px solid var(--rule-firm);
+ font-size: var(--t3);
+ padding: 10px 16px;
+}
+.btn--quiet:hover { border-color: var(--ink-3); filter: none; }
+.keep { font-size: var(--t2); color: var(--ink-3); margin: 12px 0 0; }
+
+.problem {
+ margin: 18px 0 0;
+ border-left: 1px solid var(--post-red);
+ padding-left: 14px;
+ font-size: var(--t3);
+ color: var(--ink);
+}
+.problem b { color: var(--post-red); }
+
+.after { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 26px; }
+
+/* The styled copy. A second offer, set below the handover and quieter than it:
+ the plain file is the promise this page keeps, and this is the extra. Ruled
+ off rather than boxed, because a second panel in chevron tape would read as a
+ second custody and there is only one. */
+.styling {
+ margin: 30px 0 0;
+ border-top: 1px solid var(--rule);
+ padding-top: 20px;
+}
+.styling__lead {
+ font-size: var(--t3);
+ color: var(--ink-2);
+ margin: 10px 0 16px;
+ max-width: 60ch;
+}
+.styling .handoff { margin-top: 20px; }
+.styling__off {
+ margin: 30px 0 0;
+ border-top: 1px solid var(--rule);
+ padding-top: 16px;
+ font-size: var(--t2);
+ color: var(--ink-3);
+}
+
+.cli {
+ margin: 34px 0 0;
+ font-size: var(--t2);
+ color: var(--ink-3);
+ border-top: 1px solid var(--rule);
+ padding-top: 14px;
+}
+.cli code {
+ font-family: var(--mono);
+ font-size: var(--t1);
+ color: var(--ink-2);
+ background: var(--stock);
+ padding: 2px 5px;
+}
+.foot {
+ max-width: 660px;
+ margin: 0 auto;
+ padding: 18px;
+ font-size: var(--t1);
+ color: var(--ink-3);
+ display: flex;
+ gap: 14px;
+ flex-wrap: wrap;
+}
+.foot a { color: inherit; }
+
+.sr-only {
+ position: absolute; width: 1px; height: 1px;
+ padding: 0; margin: -1px; overflow: hidden;
+ clip: rect(0 0 0 0); white-space: nowrap; border: 0;
+}
+
+/* ── The preview ──────────────────────────────────────────────────────────
+ The rebuilt document itself, shown before the handover. No chevron tape:
+ tape means "in our custody" and edges only the notice and the panel holding
+ the file. This is the contents, so it is set as a sheet on the desk. */
+.preview { margin: 26px 0 0; }
+.preview__note { margin: 4px 0 12px; color: var(--ink-2); font-size: 15px; }
+.preview__sheet {
+ background: var(--sheet);
+ border: 1px solid var(--rule-firm);
+ max-height: 380px;
+ overflow-y: auto;
+ padding: 22px 24px;
+}
+.preview__sheet:focus-visible { outline: 2px solid var(--post-blue); outline-offset: 2px; }
+.preview__doc { font-size: 15px; line-height: 1.6; color: var(--ink); }
+.preview__doc > :first-child { margin-top: 0; }
+.preview__doc h1 { font-size: 22px; margin: 20px 0 8px; }
+.preview__doc h2 { font-size: 18px; margin: 18px 0 6px; }
+.preview__doc h3 { font-size: 16px; margin: 16px 0 6px; }
+.preview__doc p { margin: 0 0 10px; }
+.preview__doc img {
+ display: block; max-width: 100%; height: auto; margin: 14px 0;
+ border: 1px solid var(--rule);
+}
+.preview__doc table { border-collapse: collapse; margin: 12px 0; font-size: 14px; }
+.preview__doc td { border: 1px solid var(--rule-firm); padding: 5px 9px; }
+.preview__doc .image-omitted { color: var(--ink-3); font-style: italic; }
+@media (max-width: 520px) { .preview__sheet { padding: 16px; max-height: 300px; } }
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/App.test.tsx b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/App.test.tsx
new file mode 100644
index 00000000..861f742b
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/App.test.tsx
@@ -0,0 +1,427 @@
+/**
+ * Salvage, tested at its one seam: the HTTP boundary.
+ *
+ * The guards below run over what is on the screen rather than over the engine's
+ * strings. That distinction is the whole point -- BUG-014 was fixed in the
+ * engine and came straight back as BUG-020, and a page can introduce copy the
+ * engine never produced.
+ */
+import { render, screen, waitFor, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import axe from "axe-core";
+import { describe, expect, it, beforeEach } from "vitest";
+
+import App from "../App";
+import { CAPTURES, dropConnectionAfter, serve, serveCapabilities, serveStyling } from "./server";
+
+/** Vocabulary that belongs to the engine and never to a person's screen. */
+const JARGON = [
+ "ZIP", "XML", "CRC", "zlib", "central directory", "ParseError", "TimeoutError",
+ "Traceback", "stack trace", "NoneType", "utf-8", "b'", "0x",
+];
+
+/** Promises this build is forbidden to make. A denial is not a promise --
+ * "not a complete repair" is the sentence this build exists to say -- so a
+ * match only counts when nothing negates it just before. */
+const OVERCLAIM = [
+ "fully repaired", "fully restored", "completely repaired", "complete repair",
+ "guaranteed", "guarantee", "perfect", "perfectly", "100%", "flawless",
+ "as good as new", "everything was recovered", "nothing was lost",
+];
+
+function claimsWithoutNegation(text: string, phrase: string): boolean {
+ const lower = text.toLowerCase();
+ const needle = phrase.toLowerCase();
+ for (let at = lower.indexOf(needle); at >= 0; at = lower.indexOf(needle, at + 1)) {
+ const before = lower.slice(Math.max(0, at - 28), at);
+ if (!/\b(not|never|no|cannot|can't|isn't|doesn't|without)\b[^.]*$/.test(before)) return true;
+ }
+ return false;
+}
+
+function file(name = "broken.docx", size = 4096) {
+ const f = new File([new Uint8Array(size)], name, {
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ });
+ Object.defineProperty(f, "size", { value: size });
+ return f;
+}
+
+async function hand(user: ReturnType, f = file()) {
+ const input = document.querySelector('input[type="file"]') as HTMLInputElement;
+ await user.upload(input, f);
+}
+
+/** Render the page and hand it one of the recorded repairs. */
+async function repairing(name: string) {
+ serve(name);
+ const user = userEvent.setup();
+ render();
+ await hand(user);
+ return user;
+}
+
+beforeEach(() => {
+ dropConnectionAfter(null);
+ serve("truncated");
+ serveCapabilities(false, "");
+ serveStyling({
+ status: 200,
+ stages: ["Sending the recovered content to SuperDocs for styling…"],
+ final: {
+ ok: true,
+ rejected_for_content: false,
+ notes: ["SuperDocs returned a styled file."],
+ filename: "broken-repaired-styled.docx",
+ download: "/api/download/styled-token",
+ ops_charged: 1,
+ ops_confirmed: false,
+ allowance_known: true,
+ allowance_remaining: 42,
+ warnings: 0,
+ },
+ });
+});
+
+describe("the styled copy", () => {
+ /** Set up a finished repair on a page that can style. */
+ async function repaired() {
+ serveCapabilities(true, "Styling is available on this page.");
+ const user = await repairing("truncated");
+ await screen.findByText(/what came through/i);
+ return user;
+ }
+
+ it("offers nothing when this copy of the page cannot style", async () => {
+ serveCapabilities(false, "Styling is switched off on this copy of the page.");
+ await repairing("truncated");
+ await screen.findByText(/what came through/i);
+ expect(screen.queryByRole("button", { name: /send it for styling/i })).toBeNull();
+ // and it says so, rather than leaving a person wondering what they missed
+ expect(screen.getByText(/switched off on this copy/i)).toBeInTheDocument();
+ });
+
+ it("never offers a styled copy of a document it could not repair", async () => {
+ serveCapabilities(true, "Styling is available on this page.");
+ await repairing("missing-document-part");
+ await screen.findAllByText(/could not be repaired/i);
+ expect(screen.queryByRole("button", { name: /send it for styling/i })).toBeNull();
+ });
+
+ it("waits to be asked, and says the plain file is already theirs", async () => {
+ await repaired();
+ expect(screen.getByRole("button", { name: /send it for styling/i })).toBeInTheDocument();
+ expect(screen.getByText(/already yours/i)).toBeInTheDocument();
+ // nothing has been sent: the second download does not exist yet
+ expect(screen.queryByRole("link", { name: /download the styled file/i })).toBeNull();
+ });
+
+ it("hands back a second file without taking away the first", async () => {
+ const user = await repaired();
+ await user.click(screen.getByRole("button", { name: /send it for styling/i }));
+
+ const styled = await screen.findByRole("link", { name: /download the styled file/i });
+ expect(styled).toHaveAttribute("href", "/api/download/styled-token");
+ // the plain rebuild is still on the page, still downloadable
+ expect(screen.getByRole("link", { name: /download the repaired file/i })).toBeInTheDocument();
+ expect(screen.getByText(/untouched and still yours/i)).toBeInTheDocument();
+ });
+
+ it("says what it cost, and marks an unconfirmed number as an estimate", async () => {
+ const user = await repaired();
+ await user.click(screen.getByRole("button", { name: /send it for styling/i }));
+ await screen.findByRole("link", { name: /download the styled file/i });
+ expect(screen.getByText(/1 estimated operation/i)).toBeInTheDocument();
+ });
+
+ it("keeps the plain file when styling fails, and says so without blaming them", async () => {
+ serveStyling({
+ final: {
+ ok: false,
+ rejected_for_content: false,
+ notes: ["Styling did not work. The rebuilt file above is unchanged and still yours."],
+ filename: "broken-repaired-styled.docx",
+ download: null,
+ ops_charged: 0,
+ ops_confirmed: false,
+ allowance_known: false,
+ allowance_remaining: 0,
+ warnings: 0,
+ },
+ });
+ const user = await repaired();
+ await user.click(screen.getByRole("button", { name: /send it for styling/i }));
+
+ expect(await screen.findByText(/no styled copy/i)).toBeInTheDocument();
+ expect(screen.queryByRole("link", { name: /download the styled file/i })).toBeNull();
+ expect(screen.getByRole("link", { name: /download the repaired file/i })).toBeInTheDocument();
+ });
+
+ it("says plainly when a styled copy came back rewritten and was thrown away", async () => {
+ serveStyling({
+ final: {
+ ok: false,
+ rejected_for_content: true,
+ notes: [
+ "The styled version came back with the wording changed, so it was thrown away rather than handed over. Your document should say what you wrote. The rebuilt file is unchanged and still yours.",
+ ],
+ filename: "broken-repaired-styled.docx",
+ download: null,
+ ops_charged: 1,
+ ops_confirmed: false,
+ allowance_known: true,
+ allowance_remaining: 41,
+ warnings: 0,
+ },
+ });
+ const user = await repaired();
+ await user.click(screen.getByRole("button", { name: /send it for styling/i }));
+
+ expect(await screen.findByRole("alert")).toHaveTextContent(/thrown away/i);
+ expect((await screen.findAllByText(/wording changed/i)).length).toBe(2);
+ // and nothing is offered for download but the file they already had
+ expect(screen.queryByRole("link", { name: /download the styled file/i })).toBeNull();
+ expect(screen.getByRole("link", { name: /download the repaired file/i })).toBeInTheDocument();
+ });
+
+ it("reports an exhausted allowance as a refusal to spend, not as a failure of theirs",
+ async () => {
+ serveStyling({
+ final: {
+ ok: false,
+ rejected_for_content: false,
+ notes: [
+ "There is no styling allowance left this month, so nothing was sent and nothing was spent. The rebuilt file is unchanged and still yours.",
+ ],
+ filename: "broken-repaired-styled.docx",
+ download: null,
+ ops_charged: 0,
+ ops_confirmed: false,
+ allowance_known: true,
+ allowance_remaining: 0,
+ warnings: 0,
+ },
+ });
+ const user = await repaired();
+ await user.click(screen.getByRole("button", { name: /send it for styling/i }));
+ // Twice: once on the page, once in the live region a screen reader hears.
+ expect((await screen.findAllByText(/nothing was spent/i)).length).toBe(2);
+ });
+
+ it("surfaces a refusal from the server in the reader's words", async () => {
+ serveStyling({
+ status: 409,
+ detail: "Styling is switched off on this copy of the page. The rebuilt file above is unchanged and still yours.",
+ });
+ const user = await repaired();
+ await user.click(screen.getByRole("button", { name: /send it for styling/i }));
+ expect((await screen.findAllByText(/switched off on this copy/i)).length).toBe(2);
+ });
+
+ it("says a quiet wait is normal while it works", async () => {
+ serveStyling({ stages: ["Waiting for SuperDocs to finish — large documents can take minutes."] });
+ const user = await repaired();
+ await user.click(screen.getByRole("button", { name: /send it for styling/i }));
+ await waitFor(() =>
+ expect(screen.getByText(/take minutes|quiet wait is normal/i)).toBeInTheDocument(),
+ );
+ });
+});
+
+describe("arriving", () => {
+ it("says what this is and what it will not do", async () => {
+ render();
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(/will not open/i);
+ expect(screen.getByText(/best-effort recovery/i)).toBeInTheDocument();
+ expect(screen.getByText(/your original is never changed/i)).toBeInTheDocument();
+ expect(screen.getByText(/no account/i)).toBeInTheDocument();
+ });
+
+ it("takes a file by choosing one, with no account and no install", async () => {
+ const user = userEvent.setup();
+ render();
+ await hand(user);
+ expect(await screen.findByText(/recovered in part|^recovered$/i)).toBeInTheDocument();
+ });
+});
+
+describe("what it refuses before sending anything", () => {
+ it("says plainly when the file is empty", async () => {
+ const user = userEvent.setup();
+ render();
+ await hand(user, file("broken.docx", 0));
+ expect(await screen.findByRole("alert")).toHaveTextContent(/empty/i);
+ expect(screen.queryByText(/reading your document/i)).not.toBeInTheDocument();
+ });
+
+ it("says plainly when the file is too large", async () => {
+ const user = userEvent.setup();
+ render();
+ await hand(user, file("huge.docx", 21 * 1024 * 1024));
+ expect(await screen.findByRole("alert")).toHaveTextContent(/larger than 20 MB/i);
+ });
+
+ it("tells someone with a .doc what to do about it", async () => {
+ const user = userEvent.setup();
+ render();
+ await hand(user, file("old.doc"));
+ expect(await screen.findByRole("alert")).toHaveTextContent(/save it as \.docx/i);
+ });
+});
+
+describe("watching it work", () => {
+ it("keeps every real step it took, and puts the answer above them", async () => {
+ const user = userEvent.setup();
+ render();
+ await hand(user);
+
+ // The verdict is what someone came for, so the steps fold away behind it.
+ const steps = await screen.findByText(/what it did, step by step/i);
+ await user.click(steps);
+ const stations = screen.getByRole("list", { name: /what this page did/i });
+ expect(within(stations).getAllByRole("listitem").length).toBeGreaterThan(3);
+ expect(stations.textContent).toMatch(/Opening the file/i);
+ // ...and they are the engine's own stages, not a decorative sequence.
+ const captured = CAPTURES.find((c) => c.name === "truncated")!;
+ expect(within(stations).getAllByRole("listitem").length).toBe(captured.events.length);
+ });
+
+ it("says the connection dropped rather than freezing, and that nothing changed", async () => {
+ dropConnectionAfter(2);
+ const user = userEvent.setup();
+ render();
+ await hand(user);
+ expect(await screen.findByRole("alert")).toHaveTextContent(/connection dropped/i);
+ expect(screen.getByRole("alert")).toHaveTextContent(/original is exactly as it was/i);
+ });
+});
+
+describe("the report", () => {
+ it("names what came through and what did not, and hands the file back", async () => {
+ serve("truncated");
+ const user = userEvent.setup();
+ render();
+ await hand(user);
+
+ expect(await screen.findByText(/what came through/i)).toBeInTheDocument();
+ const link = screen.getByRole("link", { name: /download the repaired file/i });
+ expect(link).toHaveAttribute("href", expect.stringContaining("/api/download/"));
+ expect(screen.getByText(/more than once/i)).toBeInTheDocument();
+ expect(screen.getByText(/thirty minutes/i)).toBeInTheDocument();
+ });
+
+ it("makes no claim about what came through when nothing did", async () => {
+ // BUG-012. A total failure that still shows the heading is the exact
+ // overclaim this build says it does not make.
+ serve("not-a-word-file");
+ const user = userEvent.setup();
+ render();
+ await hand(user);
+
+ expect((await screen.findAllByText(/could not be repaired/i)).length).toBeGreaterThan(0);
+ expect(screen.queryByText(/what came through/i)).not.toBeInTheDocument();
+ expect(screen.queryByRole("link", { name: /download/i })).not.toBeInTheDocument();
+ expect(screen.getByText(/what went wrong/i)).toBeInTheDocument();
+ // and the page does not head a total failure with a line about what was read
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
+ /could not be repaired/i,
+ );
+ });
+
+ it("treats a valid file with nothing in it as a failure, not a quiet success", async () => {
+ serve("empty-body");
+ const user = userEvent.setup();
+ render();
+ await hand(user);
+ expect((await screen.findAllByText(/could not be repaired/i)).length).toBeGreaterThan(0);
+ expect(screen.queryByRole("link", { name: /download/i })).not.toBeInTheDocument();
+ });
+
+ it("lets someone repair another document without reloading", async () => {
+ const user = userEvent.setup();
+ render();
+ await hand(user);
+ await screen.findByText(/what came through/i);
+ await user.click(screen.getByRole("button", { name: /repair another document/i }));
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(/will not open/i);
+ });
+});
+
+describe("seeing it before taking it", () => {
+ it("shows the rebuilt document itself, above the download", async () => {
+ await repairing("illustrated");
+ const preview = await screen.findByRole("group", { name: /the rebuilt document/i });
+ expect(within(preview).getByText("Site inspection")).toBeInTheDocument();
+ expect(within(preview).getByText(/No further defects were observed/)).toBeInTheDocument();
+
+ /* Above the download, because the question it answers — was any of this
+ worth it — is one somebody asks before they act, not after. */
+ const download = screen.getByRole("link", { name: /download the repaired file/i });
+ expect(preview.compareDocumentPosition(download))
+ .toBe(Node.DOCUMENT_POSITION_FOLLOWING);
+ });
+
+ it("shows the pictures, because the pictures are the part people lose", async () => {
+ await repairing("illustrated");
+ const preview = await screen.findByRole("group", { name: /the rebuilt document/i });
+ const img = within(preview).getByRole("img");
+ expect(img.getAttribute("src")).toMatch(/^data:image\/png;base64,/);
+ });
+
+ it("says the preview is the document rather than a description of it", async () => {
+ await repairing("illustrated");
+ expect(await screen.findByText(/not a description of it/i)).toBeInTheDocument();
+ });
+
+ it("shows no preview when nothing could be recovered", async () => {
+ await repairing("missing-document-part");
+ expect((await screen.findAllByText(/could not be repaired/i)).length)
+ .toBeGreaterThan(0);
+ expect(screen.queryByRole("group", { name: /the rebuilt document/i })).toBeNull();
+ });
+});
+
+describe("the language, over every document this build is tested on", () => {
+ it.each(CAPTURES.map((c) => c.name))("says nothing in the engine's words: %s", async (name) => {
+ serve(name);
+ const user = userEvent.setup();
+ const { container } = render();
+ await hand(user);
+ await waitFor(() =>
+ expect(container.textContent).toMatch(/recovered|could not be repaired/i),
+ );
+
+ const text = container.textContent ?? "";
+ const found = JARGON.filter((j) => text.includes(j));
+ expect(found, `the page showed engine vocabulary: ${found.join(", ")}`).toEqual([]);
+ });
+
+ it.each(CAPTURES.map((c) => c.name))("promises nothing it cannot do: %s", async (name) => {
+ serve(name);
+ const user = userEvent.setup();
+ const { container } = render();
+ await hand(user);
+ await waitFor(() =>
+ expect(container.textContent).toMatch(/recovered|could not be repaired/i),
+ );
+
+ const text = (container.textContent ?? "").toLowerCase();
+ const found = OVERCLAIM.filter((w) => claimsWithoutNegation(text, w));
+ expect(found, `the page overclaimed: ${found.join(", ")}`).toEqual([]);
+ });
+});
+
+describe("everyone can use it", () => {
+ it("has no automatically detectable accessibility violations, idle or reporting", async () => {
+ const user = userEvent.setup();
+ const { container } = render();
+
+ let results = await axe.run(container, { rules: { region: { enabled: false } } });
+ expect(results.violations.filter((v) => ["serious", "critical"].includes(v.impact ?? ""))).toEqual([]);
+
+ await hand(user);
+ await screen.findByText(/what came through/i);
+ results = await axe.run(container, { rules: { region: { enabled: false } } });
+ expect(results.violations.filter((v) => ["serious", "critical"].includes(v.impact ?? ""))).toEqual([]);
+ });
+});
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/bad-characters.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/bad-characters.json
new file mode 100644
index 00000000..dc3be77c
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/bad-characters.json
@@ -0,0 +1,67 @@
+{
+ "name": "bad-characters",
+ "filename": "04-bad-characters.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "The container opened cleanly."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 5 sections inside the document."
+ },
+ {
+ "stage": "xml",
+ "message": "Checking the document body for damage…"
+ },
+ {
+ "stage": "xml",
+ "message": "Removed 2 invalid characters that were breaking the document."
+ },
+ {
+ "stage": "xml",
+ "message": "Repaired 1 stray ampersand."
+ },
+ {
+ "stage": "read",
+ "message": "Reading headings, paragraphs and tables…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovered 2 headings, 2 paragraphs and 1 table."
+ },
+ {
+ "stage": "write",
+ "message": "Rebuilding a clean Word file…"
+ },
+ {
+ "stage": "done",
+ "message": "Done. The rebuilt file is ready to download."
+ }
+ ],
+ "report": {
+ "ok": true,
+ "summary": "Recovered what could be read and rebuilt it as a valid Word file. This is a best-effort recovery, not a complete repair.",
+ "recovered": [
+ "removed 2 invalid characters that were breaking the document",
+ "repaired 1 stray ampersand",
+ "kept 2 headings, 2 paragraphs and 1 table, with their structure intact"
+ ],
+ "lost": [],
+ "counts": {
+ "headings": 2,
+ "paragraphs": 2,
+ "tables": 1,
+ "pictures": 0
+ },
+ "structure_preserved": true,
+ "filename": "04-bad-characters-repaired.docx",
+ "preview_html": "
Quarterly Report
\n
Revenue rose in Q3 — driven by renewals & upsell.
\n
Regional breakdown
\n
Region
Revenue
EMEA
1.2M
APAC
0.8M
\n
Prepared by the finance team.
",
+ "download": "/api/download/bad-characters-token",
+ "style": "/api/style/bad-characters-token"
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/empty-body.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/empty-body.json
new file mode 100644
index 00000000..65022e7f
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/empty-body.json
@@ -0,0 +1,48 @@
+{
+ "name": "empty-body",
+ "filename": "07-empty-body.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "The container opened cleanly."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 5 sections inside the document."
+ },
+ {
+ "stage": "xml",
+ "message": "Checking the document body for damage…"
+ },
+ {
+ "stage": "xml",
+ "message": "The document body was well-formed."
+ },
+ {
+ "stage": "read",
+ "message": "Reading headings, paragraphs and tables…"
+ },
+ {
+ "stage": "read",
+ "message": "No readable content was found in the document body."
+ }
+ ],
+ "report": {
+ "ok": false,
+ "summary": "This file could not be repaired. The document body contained no readable text. Nothing was invented to fill the gap — if there is another copy, even an older one, that is the better starting point.",
+ "recovered": [],
+ "lost": [
+ "the document body contained no readable text"
+ ],
+ "counts": {},
+ "structure_preserved": true,
+ "filename": "07-empty-body-repaired.docx",
+ "preview_html": "",
+ "download": null,
+ "style": null
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/healthy.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/healthy.json
new file mode 100644
index 00000000..5262eb14
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/healthy.json
@@ -0,0 +1,61 @@
+{
+ "name": "healthy",
+ "filename": "00-healthy.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "The container opened cleanly."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 5 sections inside the document."
+ },
+ {
+ "stage": "xml",
+ "message": "Checking the document body for damage…"
+ },
+ {
+ "stage": "xml",
+ "message": "The document body was well-formed."
+ },
+ {
+ "stage": "read",
+ "message": "Reading headings, paragraphs and tables…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovered 2 headings, 2 paragraphs and 1 table."
+ },
+ {
+ "stage": "write",
+ "message": "Rebuilding a clean Word file…"
+ },
+ {
+ "stage": "done",
+ "message": "Done. The rebuilt file is ready to download."
+ }
+ ],
+ "report": {
+ "ok": true,
+ "summary": "Recovered what could be read and rebuilt it as a valid Word file. This is a best-effort recovery, not a complete repair.",
+ "recovered": [
+ "kept 2 headings, 2 paragraphs and 1 table, with their structure intact"
+ ],
+ "lost": [],
+ "counts": {
+ "headings": 2,
+ "paragraphs": 2,
+ "tables": 1,
+ "pictures": 0
+ },
+ "structure_preserved": true,
+ "filename": "00-healthy-repaired.docx",
+ "preview_html": "
Quarterly Report
\n
Revenue rose in Q3 — driven by renewals & upsell.
\n
Regional breakdown
\n
Region
Revenue
EMEA
1.2M
APAC
0.8M
\n
Prepared by the finance team.
",
+ "download": "/api/download/healthy-token",
+ "style": "/api/style/healthy-token"
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/illustrated.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/illustrated.json
new file mode 100644
index 00000000..1dc1984b
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/illustrated.json
@@ -0,0 +1,79 @@
+{
+ "name": "illustrated",
+ "filename": "08-illustrated.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "The container opened cleanly."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 9 sections inside the document."
+ },
+ {
+ "stage": "xml",
+ "message": "Checking the document body for damage…"
+ },
+ {
+ "stage": "xml",
+ "message": "The document body was well-formed."
+ },
+ {
+ "stage": "read",
+ "message": "Reading headings, paragraphs and tables…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovering the footnotes…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovering the page header…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovering the page footer…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovered 4 headings, 5 paragraphs and 1 picture."
+ },
+ {
+ "stage": "write",
+ "message": "Rebuilding a clean Word file…"
+ },
+ {
+ "stage": "done",
+ "message": "Done. The rebuilt file is ready to download."
+ }
+ ],
+ "report": {
+ "ok": true,
+ "summary": "Recovered what could be read and rebuilt it as a valid Word file. This is a best-effort recovery, not a complete repair.",
+ "recovered": [
+ "kept 4 headings, 5 paragraphs and 1 picture, with their structure intact",
+ "carried 1 picture back into the document, where it was",
+ "recovered the footnotes",
+ "recovered the page header",
+ "recovered the page footer"
+ ],
+ "lost": [
+ "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"
+ ],
+ "counts": {
+ "headings": 4,
+ "paragraphs": 5,
+ "tables": 0,
+ "pictures": 1
+ },
+ "structure_preserved": true,
+ "filename": "08-illustrated-repaired.docx",
+ "preview_html": "
Site inspection
\n
The east elevation, photographed on arrival:
\n\n
No further defects were observed.
\n
Footnotes, recovered separately
\n
Measurements taken with a laser rangefinder.
\n
Page header, recovered separately
\n
Inspection report — draft
\n
Page footer, recovered separately
\n
Page 1 of 1
",
+ "download": "/api/download/illustrated-token",
+ "style": "/api/style/illustrated-token"
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/index.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/index.json
new file mode 100644
index 00000000..c3357209
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/index.json
@@ -0,0 +1,52 @@
+[
+ {
+ "name": "healthy",
+ "filename": "00-healthy.docx",
+ "ok": true
+ },
+ {
+ "name": "truncated",
+ "filename": "01-truncated-download.docx",
+ "ok": true
+ },
+ {
+ "name": "missing-content-types",
+ "filename": "02-missing-content-types.docx",
+ "ok": true
+ },
+ {
+ "name": "unclosed-tags",
+ "filename": "03-unclosed-tags.docx",
+ "ok": true
+ },
+ {
+ "name": "bad-characters",
+ "filename": "04-bad-characters.docx",
+ "ok": true
+ },
+ {
+ "name": "missing-document-part",
+ "filename": "05-missing-document-part.docx",
+ "ok": false
+ },
+ {
+ "name": "not-a-word-file",
+ "filename": "06-not-a-word-file.docx",
+ "ok": false
+ },
+ {
+ "name": "empty-body",
+ "filename": "07-empty-body.docx",
+ "ok": false
+ },
+ {
+ "name": "illustrated",
+ "filename": "08-illustrated.docx",
+ "ok": true
+ },
+ {
+ "name": "truncated-illustrated",
+ "filename": "09-truncated-illustrated.docx",
+ "ok": true
+ }
+]
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/missing-content-types.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/missing-content-types.json
new file mode 100644
index 00000000..bf625fc9
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/missing-content-types.json
@@ -0,0 +1,66 @@
+{
+ "name": "missing-content-types",
+ "filename": "02-missing-content-types.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "The container opened cleanly."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 4 sections inside the document."
+ },
+ {
+ "stage": "inventory",
+ "message": "Rebuilding 1 missing structural part…"
+ },
+ {
+ "stage": "xml",
+ "message": "Checking the document body for damage…"
+ },
+ {
+ "stage": "xml",
+ "message": "The document body was well-formed."
+ },
+ {
+ "stage": "read",
+ "message": "Reading headings, paragraphs and tables…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovered 2 headings, 2 paragraphs and 1 table."
+ },
+ {
+ "stage": "write",
+ "message": "Rebuilding a clean Word file…"
+ },
+ {
+ "stage": "done",
+ "message": "Done. The rebuilt file is ready to download."
+ }
+ ],
+ "report": {
+ "ok": true,
+ "summary": "Recovered what could be read and rebuilt it as a valid Word file. This is a best-effort recovery, not a complete repair.",
+ "recovered": [
+ "rebuilt the file's internal structure, which was missing or damaged — this is standard plumbing and carries none of your content",
+ "kept 2 headings, 2 paragraphs and 1 table, with their structure intact"
+ ],
+ "lost": [],
+ "counts": {
+ "headings": 2,
+ "paragraphs": 2,
+ "tables": 1,
+ "pictures": 0
+ },
+ "structure_preserved": true,
+ "filename": "02-missing-content-types-repaired.docx",
+ "preview_html": "
Quarterly Report
\n
Revenue rose in Q3 — driven by renewals & upsell.
\n
Regional breakdown
\n
Region
Revenue
EMEA
1.2M
APAC
0.8M
\n
Prepared by the finance team.
",
+ "download": "/api/download/missing-content-types-token",
+ "style": "/api/style/missing-content-types-token"
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/missing-document-part.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/missing-document-part.json
new file mode 100644
index 00000000..43bd823f
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/missing-document-part.json
@@ -0,0 +1,36 @@
+{
+ "name": "missing-document-part",
+ "filename": "05-missing-document-part.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "The container opened cleanly."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 4 sections inside the document."
+ },
+ {
+ "stage": "inventory",
+ "message": "The main document part is missing and cannot be rebuilt."
+ }
+ ],
+ "report": {
+ "ok": false,
+ "summary": "This file could not be repaired. The main document part is missing entirely, so there is no text to recover. Nothing was invented to fill the gap — if there is another copy, even an older one, that is the better starting point.",
+ "recovered": [],
+ "lost": [
+ "the main document part is missing entirely, so there is no text to recover"
+ ],
+ "counts": {},
+ "structure_preserved": true,
+ "filename": "05-missing-document-part-repaired.docx",
+ "preview_html": "",
+ "download": null,
+ "style": null
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/not-a-word-file.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/not-a-word-file.json
new file mode 100644
index 00000000..6269c72b
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/not-a-word-file.json
@@ -0,0 +1,32 @@
+{
+ "name": "not-a-word-file",
+ "filename": "06-not-a-word-file.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "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."
+ },
+ {
+ "stage": "open",
+ "message": "No readable parts were found inside the file."
+ }
+ ],
+ "report": {
+ "ok": false,
+ "summary": "This file could not be repaired. No readable parts could be found inside the file. Nothing was invented to fill the gap — if there is another copy, even an older one, that is the better starting point.",
+ "recovered": [],
+ "lost": [
+ "no readable parts could be found inside the file"
+ ],
+ "counts": {},
+ "structure_preserved": true,
+ "filename": "06-not-a-word-file-repaired.docx",
+ "preview_html": "",
+ "download": null,
+ "style": null
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/truncated-illustrated.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/truncated-illustrated.json
new file mode 100644
index 00000000..29023275
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/truncated-illustrated.json
@@ -0,0 +1,84 @@
+{
+ "name": "truncated-illustrated",
+ "filename": "09-truncated-illustrated.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "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."
+ },
+ {
+ "stage": "open",
+ "message": "Found and read 9 sections of the file this way."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 9 sections inside the document."
+ },
+ {
+ "stage": "xml",
+ "message": "Checking the document body for damage…"
+ },
+ {
+ "stage": "xml",
+ "message": "The document body was well-formed."
+ },
+ {
+ "stage": "read",
+ "message": "Reading headings, paragraphs and tables…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovering the footnotes…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovering the page header…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovering the page footer…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovered 4 headings, 5 paragraphs and 1 picture."
+ },
+ {
+ "stage": "write",
+ "message": "Rebuilding a clean Word file…"
+ },
+ {
+ "stage": "done",
+ "message": "Done. The rebuilt file is ready to download."
+ }
+ ],
+ "report": {
+ "ok": true,
+ "summary": "Recovered what could be read and rebuilt it as a valid Word file. This is a best-effort recovery, not a complete repair.",
+ "recovered": [
+ "read your content out of a file whose internal index was damaged — the damage Word refuses to open",
+ "kept 4 headings, 5 paragraphs and 1 picture, with their structure intact",
+ "carried 1 picture back into the document, where it was",
+ "recovered the footnotes",
+ "recovered the page header",
+ "recovered the page footer"
+ ],
+ "lost": [
+ "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"
+ ],
+ "counts": {
+ "headings": 4,
+ "paragraphs": 5,
+ "tables": 0,
+ "pictures": 1
+ },
+ "structure_preserved": true,
+ "filename": "09-truncated-illustrated-repaired.docx",
+ "preview_html": "
Site inspection
\n
The east elevation, photographed on arrival:
\n\n
No further defects were observed.
\n
Footnotes, recovered separately
\n
Measurements taken with a laser rangefinder.
\n
Page header, recovered separately
\n
Inspection report — draft
\n
Page footer, recovered separately
\n
Page 1 of 1
",
+ "download": "/api/download/truncated-illustrated-token",
+ "style": "/api/style/truncated-illustrated-token"
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/truncated.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/truncated.json
new file mode 100644
index 00000000..0f4597e1
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/truncated.json
@@ -0,0 +1,66 @@
+{
+ "name": "truncated",
+ "filename": "01-truncated-download.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "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."
+ },
+ {
+ "stage": "open",
+ "message": "Found and read 4 sections of the file this way."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 4 sections inside the document."
+ },
+ {
+ "stage": "xml",
+ "message": "Checking the document body for damage…"
+ },
+ {
+ "stage": "xml",
+ "message": "The document body was well-formed."
+ },
+ {
+ "stage": "read",
+ "message": "Reading headings, paragraphs and tables…"
+ },
+ {
+ "stage": "read",
+ "message": "Recovered 2 headings, 2 paragraphs and 1 table."
+ },
+ {
+ "stage": "write",
+ "message": "Rebuilding a clean Word file…"
+ },
+ {
+ "stage": "done",
+ "message": "Done. The rebuilt file is ready to download."
+ }
+ ],
+ "report": {
+ "ok": true,
+ "summary": "Recovered what could be read and rebuilt it as a valid Word file. This is a best-effort recovery, not a complete repair.",
+ "recovered": [
+ "read your content out of a file whose internal index was damaged — the damage Word refuses to open",
+ "kept 2 headings, 2 paragraphs and 1 table, with their structure intact"
+ ],
+ "lost": [],
+ "counts": {
+ "headings": 2,
+ "paragraphs": 2,
+ "tables": 1,
+ "pictures": 0
+ },
+ "structure_preserved": true,
+ "filename": "01-truncated-download-repaired.docx",
+ "preview_html": "
Quarterly Report
\n
Revenue rose in Q3 — driven by renewals & upsell.
\n
Regional breakdown
\n
Region
Revenue
EMEA
1.2M
APAC
0.8M
\n
Prepared by the finance team.
",
+ "download": "/api/download/truncated-token",
+ "style": "/api/style/truncated-token"
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/unclosed-tags.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/unclosed-tags.json
new file mode 100644
index 00000000..c569ee90
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/fixtures/unclosed-tags.json
@@ -0,0 +1,68 @@
+{
+ "name": "unclosed-tags",
+ "filename": "03-unclosed-tags.docx",
+ "events": [
+ {
+ "stage": "open",
+ "message": "Opening the file…"
+ },
+ {
+ "stage": "open",
+ "message": "The container opened cleanly."
+ },
+ {
+ "stage": "inventory",
+ "message": "Found 5 sections inside the document."
+ },
+ {
+ "stage": "xml",
+ "message": "Checking the document body for damage…"
+ },
+ {
+ "stage": "xml",
+ "message": "Closed 6 elements that the damage had left open."
+ },
+ {
+ "stage": "read",
+ "message": "Reading headings, paragraphs and tables…"
+ },
+ {
+ "stage": "read",
+ "message": "The document's structure was too damaged to read. Recovering the text on its own instead."
+ },
+ {
+ "stage": "read",
+ "message": "Recovered 6 paragraphs."
+ },
+ {
+ "stage": "write",
+ "message": "Rebuilding a clean Word file…"
+ },
+ {
+ "stage": "done",
+ "message": "Done. The rebuilt file is ready to download."
+ }
+ ],
+ "report": {
+ "ok": true,
+ "summary": "Recovered what could be read and rebuilt it as a valid Word file. This is a best-effort recovery, not a complete repair. 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.",
+ "recovered": [
+ "closed 6 elements that the damage had left open",
+ "recovered the text of 6 paragraphs"
+ ],
+ "lost": [
+ "the document structure was unreadable, so headings, tables and formatting could not be preserved — only the text was recovered"
+ ],
+ "counts": {
+ "headings": 0,
+ "paragraphs": 6,
+ "tables": 0,
+ "pictures": 0
+ },
+ "structure_preserved": false,
+ "filename": "03-unclosed-tags-repaired.docx",
+ "preview_html": "
",
+ "download": "/api/download/unclosed-tags-token",
+ "style": "/api/style/unclosed-tags-token"
+ }
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/server.ts b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/server.ts
new file mode 100644
index 00000000..65cb3b66
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/server.ts
@@ -0,0 +1,125 @@
+/**
+ * The fake this page is tested against streams real repairs.
+ *
+ * Every line it sends was produced by the engine and recorded by
+ * tests/test_frontend_fixtures.py, which fails if the recording drifts. A page
+ * that tells somebody what happened to their document must be tested against
+ * what actually happens to documents.
+ */
+import { http, HttpResponse } from "msw";
+import { setupServer } from "msw/node";
+
+import healthy from "./fixtures/healthy.json";
+import truncated from "./fixtures/truncated.json";
+import missingContentTypes from "./fixtures/missing-content-types.json";
+import unclosedTags from "./fixtures/unclosed-tags.json";
+import badCharacters from "./fixtures/bad-characters.json";
+import missingDocumentPart from "./fixtures/missing-document-part.json";
+import notAWordFile from "./fixtures/not-a-word-file.json";
+import emptyBody from "./fixtures/empty-body.json";
+import illustrated from "./fixtures/illustrated.json";
+import truncatedIllustrated from "./fixtures/truncated-illustrated.json";
+
+export interface Capture {
+ name: string;
+ filename: string;
+ events: { stage: string; message: string }[];
+ report: Record;
+}
+
+export const CAPTURES: Capture[] = [
+ healthy,
+ truncated,
+ missingContentTypes,
+ unclosedTags,
+ badCharacters,
+ missingDocumentPart,
+ notAWordFile,
+ emptyBody,
+ illustrated,
+ truncatedIllustrated,
+] as Capture[];
+
+let serving: Capture = truncated as Capture;
+export function serve(name: string) {
+ const found = CAPTURES.find((c) => c.name === name);
+ if (!found) throw new Error(`no captured repair called ${name}`);
+ serving = found;
+}
+
+/** Cut the stream off after `n` lines, the way a dropped connection does. */
+let cutAfter: number | null = null;
+export function dropConnectionAfter(n: number | null) {
+ cutAfter = n;
+}
+
+/** What this copy of the page can do. The default is off, because the offline
+ * rebuild is the product and styling is the extra. */
+let styling: { on: boolean; note: string } = { on: false, note: "" };
+export function serveCapabilities(on: boolean, note = "") {
+ styling = { on, note };
+}
+
+/** How the styling endpoint answers next. Stages first, then the final line —
+ * the same wire shape as a repair, because it is the same situation. */
+let stylingAnswer: {
+ status: number;
+ detail?: string;
+ stages: string[];
+ final: Record;
+} = {
+ status: 200,
+ stages: ["Sending the recovered content to SuperDocs for styling…"],
+ final: {
+ ok: true,
+ notes: ["SuperDocs returned a styled file."],
+ filename: "d-repaired-styled.docx",
+ download: "/api/download/styled-token",
+ ops_charged: 1,
+ ops_confirmed: false,
+ allowance_known: true,
+ allowance_remaining: 42,
+ warnings: 0,
+ },
+};
+export function serveStyling(next: Partial) {
+ stylingAnswer = { ...stylingAnswer, ...next };
+}
+
+function ndjson(lines: string[]) {
+ const body = new ReadableStream({
+ start(controller) {
+ const encoder = new TextEncoder();
+ const limit = cutAfter ?? lines.length;
+ lines.slice(0, limit).forEach((l) => controller.enqueue(encoder.encode(l + "\n")));
+ controller.close();
+ },
+ });
+ return new HttpResponse(body, { headers: { "content-type": "application/x-ndjson" } });
+}
+
+export const handlers = [
+ http.get("/api/capabilities", () =>
+ HttpResponse.json({ styling: styling.on, note: styling.note }),
+ ),
+
+ http.post("/api/style/:token", () => {
+ if (stylingAnswer.status !== 200) {
+ return HttpResponse.json({ detail: stylingAnswer.detail }, { status: stylingAnswer.status });
+ }
+ return ndjson([
+ ...stylingAnswer.stages.map((m) => JSON.stringify({ stage: "superdocs", message: m })),
+ JSON.stringify({ done: true, ...stylingAnswer.final }),
+ ]);
+ }),
+
+ http.post("/api/repair", () => {
+ const capture = serving;
+ return ndjson([
+ ...capture.events.map((e) => JSON.stringify(e)),
+ JSON.stringify({ done: true, ...capture.report }),
+ ]);
+ }),
+];
+
+export const server = setupServer(...handlers);
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/setup.ts b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/setup.ts
new file mode 100644
index 00000000..c52dfa98
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/src/test/setup.ts
@@ -0,0 +1,7 @@
+import "@testing-library/jest-dom/vitest";
+import { afterAll, afterEach, beforeAll } from "vitest";
+import { server } from "./server";
+
+beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/tsconfig.json b/use-cases/Priyanshu2425/word-doc-repair/frontend/tsconfig.json
new file mode 100644
index 00000000..2996f5b1
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "types": ["vitest/globals", "@testing-library/jest-dom"]
+ },
+ "include": ["src", "vite.config.ts", "vitest.config.ts"]
+}
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/vite.config.ts b/use-cases/Priyanshu2425/word-doc-repair/frontend/vite.config.ts
new file mode 100644
index 00000000..3ab49f4d
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { viteSingleFile } from "vite-plugin-singlefile";
+
+/** One file into the directory the FastAPI app already reads. No install, no
+ * build step, no Node on the machine that serves it. */
+export default defineConfig({
+ plugins: [react(), viteSingleFile()],
+ build: { outDir: "../backend/static", emptyOutDir: true, target: "es2020", cssMinify: true },
+ // `npm run dev` serves the page on 5173 and the engine answers on 8000, so
+ // without this the page's only conversation with the server 404s and the
+ // dev server is useful for nothing but looking at the idle screen.
+ // PORT=8077 python3 -m docrepair.web -> set SALVAGE_API to match.
+ server: {
+ proxy: { "/api": process.env.SALVAGE_API ?? "http://127.0.0.1:8000" },
+ },
+});
diff --git a/use-cases/Priyanshu2425/word-doc-repair/frontend/vitest.config.ts b/use-cases/Priyanshu2425/word-doc-repair/frontend/vitest.config.ts
new file mode 100644
index 00000000..ab0ec389
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/frontend/vitest.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: "jsdom",
+ globals: true,
+ setupFiles: ["./src/test/setup.ts"],
+ css: false,
+ restoreMocks: true,
+ },
+});
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/MANUAL_QA_PLAN.html b/use-cases/Priyanshu2425/word-doc-repair/manual-test/MANUAL_QA_PLAN.html
new file mode 100644
index 00000000..39a1b536
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/manual-test/MANUAL_QA_PLAN.html
@@ -0,0 +1,292 @@
+
+
+
+
+
+Salvage — Manual QA Plan
+
+
+
+
+
+
+
Salvage — Manual QA Plan
+
Every check below was run on the date stamped here, against a live
+ server, not against the modules. Where a check could not be run, it says not run rather
+ than passed. Verdicts are transcribed from the actual output, which is why some of them are worded
+ awkwardly — that is what the machine said.
+
+ build · word-doc-repair (assigned build B)
+ run · 2026-08-20
+ server · 127.0.0.1:8123
+ key · live SuperDocs personal key
+
+
+
+
Automated coverage at the time of this run: 63 offline Python tests,
+76 with the web extra installed, 46 frontend tests. This page
+covers what those cannot: whether the downloaded file opens, whether the wait is bearable, and
+whether a person who is not an engineer understands what they were handed.
+
+
A The engine, through the HTTP surface
+
Each fixture in manual-test/fixtures/ posted to POST /api/repair
+and the streamed report read line by line.
+
+
+
#
What was done
Expected
Actual
Verdict
+
+
A1
Post 00-healthy.docx
+
Repairs, offers a download and a styling link, preview rendered
Covered by test_it_does_not_spend_an_allowance_it_has_already_been_told_is_gone; not reproducible live without burning 500 operations
PASS (automated)
+
D10
Dead network, no job id, empty export, unexpected error
+
Each degrades to the plain rebuild
+
Covered by tests/test_styled_export.py, one test per failure
PASS (automated)
+
+
+
+
+
+
The defect this run found — and what was done about it
+
The first live styling run came back with the wording rewritten: a four-line recovered
+ report gained three invented paragraphs, a subtotal row, a “Remarks & Disclaimers” section, a
+ signature block and a footer — none of it in the document that was sent. It opens cleanly and it
+ reads better than the plain rebuild, which is exactly why it is the worst output this product could
+ produce: its owner would not notice.
+
Two changes, in this order. The instruction was tightened to forbid additions by name. Then a
+ guard was added that does not depend on the instruction being obeyed: the words that come back are
+ compared against the words that went out, and any difference at all throws the styled file away
+ with the reason said plainly. On the re-run the same document came back word for word identical —
+ but the instruction is the request and the guard is the promise, and only one of
+ them is a test.
+
+
+
+
+
#
What was done
Expected
Actual
Verdict
+
+
D11
Feed the guard the rewritten file from the first live run
+
Rejected
drift = (199 added, 0 removed) → thrown away
PASS
+
D12
Feed the guard an unchanged rebuild
+
Accepted — a guard that rejects everything is an off switch
drift = (0, 0)
PASS
+
D13
A rewritten export, end to end through the page
+
No styled download offered; the alert says Thrown away
+
Covered at both layers: test_a_styled_file_that_was_rewritten_never_reaches_the_download and the page test
PASS (automated)
+
+
+
+
+
E Judgement calls no test can make
+
These need a person. They are listed so that “we did not check” is on the record
+rather than implied.
+
+
+
#
The question
Why no test can answer it
Verdict
+
+
E1
Does the repaired file open in Word without a repair prompt?
+
Needs Word
NOT RUN
+
E2
Does the styled file look styled to a person, not just to a parser?
+
Needs eyes on a rendered page
NOT RUN
+
E3
Is a 27-second wait bearable with the stage lines showing?
+
Perception, not timing
NOT RUN
+
E4
Does a non-technical person understand “Recovered in part”?
+
Comprehension
NOT RUN
+
E5
Does anyone read what did not come through, or only the stamp?
+
Attention
NOT RUN
+
E6
Is “Thrown away” frightening in the wrong way?
+
Tone. It is meant to be reassuring — their file is safe — and it may not read that way
NOT RUN
+
E0
Does the page load and behave in a real browser?
+
The 46 frontend tests render the whole page and drive it, and every endpoint was exercised
+ over real HTTP — but no browser was driven end to end in this session. Listed rather than
+ implied.
NOT RUN
+
E7
Would somebody send a confidential document to a third party after reading that button’s copy?
+
Consent. The copy says what is sent; whether it is understood is the question
NOT RUN
+
+
+
+
+
F How to run this plan again
+
cd use-cases/Priyanshu2425/word-doc-repair
+python3 -m venv .venv && .venv/bin/pip install -e ".[web,dev]"
+
+.venv/bin/python -m pytest # 76 with the web extra, 63 without
+cd frontend && npm install && npm test # 46
+
+# section A–C, with no key set:
+PORT=8123 .venv/bin/python -m docrepair.web
+curl -s -X POST -F "file=@manual-test/fixtures/01-truncated-download.docx" \
+ http://127.0.0.1:8123/api/repair
+
+# section D, with a key:
+SUPERDOCS_API_KEY=sk_... PORT=8123 .venv/bin/python -m docrepair.web
+curl -s http://127.0.0.1:8123/api/capabilities
+curl -s -N -X POST http://127.0.0.1:8123/api/style/<token>
+
+
manual-test/index.html is the interactive nineteen-item checklist for the
+ page itself, with verdicts that persist across reloads and export as Markdown. This plan is the
+ record of a run; that one is the instrument.
+
UI_FLOWS.html, beside this file, is the same territory walked screen by screen.
Every path a person can take through the page, in the order they take it, with the
+ thing to check at each step. The screen sketches are transcribed from the component source and from
+ the engine's real output — every stage line, count, docket and message below was taken from an
+ actual run, not written to look plausible. What they are not is a screenshot: nobody
+ loaded this page in a browser as part of writing them, which is exactly why this document exists
+ as a walkthrough for a person to do. The API-level evidence is in
+ MANUAL_QA_PLAN.html beside this file.
+
+ build · word-doc-repair (assigned build B)
+ written · 2026-08-20
+ page · React, one committed bundle, no Node needed to run it
+
+
+
+
1 Arrive
+
Somebody whose only copy will not open lands here, probably frightened, probably from
+a search. The first screen has to answer can you help me and will you make it worse
+before it asks for anything.
+
+
+
Step 1 · open the page
+
In the first viewport, without scrolling: the wordmark, one sentence saying what
+ this is, the panel that takes the file, and the four custody promises beneath it.
+
Salvage. document recovery
+ ┌─ ▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨ ─┐
+ 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.
+
+ ┌──────────────────────────────────┐
+ │ Choose the damaged file │
+ │ or drag it onto this panel │
+ │ Word .docx · up to 20 MB │
+ └──────────────────────────────────┘
+
+ ✓ Your original is never changed.
+ ✓ Your file is not kept.
+ ✓ No account, and nothing to install.
+ ✓ It is a best-effort recovery.
+ └─ ▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨ ─┘
+
Check. No hero, no feature cards, no pricing. The fourth promise says
+ best-effort before anything is uploaded, not after.
+
+
+
+
Step 1a · hand it something it will not take
+
A .doc, a 25 MB file, or an empty one. Refused before anything is
+ sent, under the heading That file was not taken.
+
Check. The .doc message tells you to re-save it in Word
+ and says what to do if Word will not open it either — which is the situation you are
+ actually in. Advice that assumes your Word works is advice for somebody else.
+
+
+
2 Watch it work
+
The stages are streamed from the engine as they happen. The one thing this screen may
+never do is animate a bar over work that already finished.
+
+
+
Step 2 · the file is taken
+
The upload panel is replaced by a record line and a list of stations that grows one
+ line at a time.
+
Reading your document
+ file 01-truncated-download.docx size 4 KB docket SLV-71981
+
+ ● Opening the file…
+ ● The file's internal index was damaged…
+ ● Found 4 sections inside the document.
+ ● Checking the document body for damage…
+ ○ Reading headings, paragraphs and tables…
+
+ Working ▓▓▓▓▓▓▓░░░░░░░░
+
Check. Lines appear one after another, not all at once at the end. The
+ record line carries a docket — a receipt, so the page has a name for what it is holding.
+
Check. A screen reader hears the latest stage: there is a polite live region
+ carrying it.
+
+
+
3 Read the verdict
+
Three outcomes, and they must not look alike. The stamp lands once; the working folds
+away behind a disclosure, because the answer outranks the working.
+
+
+
+
Outcome
Stamp
What is on screen
Try it with
+
+
Everything read
Recovered
+
What came through · the sheet · download · the styling offer
+
00-healthy.docx, and also 01-truncated-download.docx — a badly
+ damaged container whose content came back whole, which is the point of the build
+
Read, with losses
Recovered in part
+
Both lists, set at the same weight · the sheet · download · the styling offer
+
03-unclosed-tags.docx — the run-by-run fallback, which loses the structure and says so
+
Could not be read
Could not be repaired
+
What went wrong. No “what came through”, no sheet, no download,
+ no styling offer
+
05-missing-document-part.docx
+
+
+
+
+
+
Step 3 · the report
+
Your document, as far as it could be read
+ file 01-truncated-download.docx size 4 KB docket SLV-71981
+
+ ╭──────────────────────╮
+ │ RECOVERED │ ← lands once, small rotation
+ ╰──────────────────────╯
+
+ Recovered what could be read and rebuilt it as a valid
+ Word file. This is a best-effort recovery, not a complete
+ repair.
+
+ What came through
+ ✓ read your content out of a file whose internal index
+ was damaged — the damage Word refuses to open
+ ✓ kept 2 headings, 2 paragraphs and 1 table, with their
+ structure intact
+
+ (no "what did not come through" section: on this file
+ there were no losses, and a heading with nothing under
+ it is a claim)
+
+ ┌ the rebuilt document, rendered for reading ──────────┐
+ │ Quarterly Report │
+ │ Revenue rose in Q3 — driven by renewals & upsell. │
+ │ Regional breakdown │
+ │ ┌────────┬──────────┐ │
+ │ │ Region │ Revenue │ … │
+ └──────────────────────────────────────────────────────┘
+
+ ┌─ ▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨ ─┐
+ [ Download the repaired file ]
+ Saved as …-repaired.docx. You can download it more than
+ once. This page holds it for about thirty minutes…
+ └─ ▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨ ─┘
+
+ ▸ What it did, step by step (10)
+
Check. On a file that did lose something — try
+ 03-unclosed-tags.docx — losses are set at the same weight as recoveries. Styling
+ losses as a footnote would be the overclaim this build refuses, expressed in CSS.
+
Check. The rebuilt document is on the page, above the download.
+ “Was any of this worth it” is a question people ask before they act, not after.
+
Check. The stage log is behind a disclosure, still complete, still readable.
+
+
+
+
Step 3a · the total failure
+
Stamp reads Could not be repaired. There is no “what came through” heading at
+ all, no preview, and no download button.
+
Check. This was a real defect once (BUG-012): a failure that still rendered
+ the heading. A heading with nothing under it is a claim.
+
+
+
4 The styled copy — the SuperDocs path
+
The card this build answers is banded API + export. This is where those four
+calls live, and the whole design question is how to offer a second, better-looking file without ever
+putting the first one at risk.
+
+
+
Step 4 · the offer, below the download and never above it
+
─────────────────────────────────────────────────────
+ A styled copy, if you want one
+
+ The file above is plain on purpose. The fonts and spacing
+ of your original could not be read out of a damaged file,
+ and this page will not guess at a design you had.
+ SuperDocs can lay the recovered headings, tables and
+ paragraphs out with consistent styling instead. It is told
+ to change no words, only how they are set. What comes back
+ is compared against what went out, word for word, and
+ thrown away if the wording moved at all…
+
+ [ Send it for styling ]
+
+ This sends the recovered text to SuperDocs. If you would
+ rather it stayed on this machine, the file above is
+ already yours and nothing more needs to happen.
+
Check. Nothing has been sent yet. Somebody whose document just broke should
+ not have it handed to a third party because a page decided that for them.
+
Check. The panel carries no chevron tape. Tape means in our custody,
+ and there is only one custody.
+
+
+
+
Step 4a · with no key configured
+
No button at all — one quiet line: “Styling is switched off on this copy of the
+ page, so the rebuilt file is the plain one. Nothing else is affected.”
+
Check. The page asks GET /api/capabilities before it offers
+ anything. A step this copy cannot take is never rendered as a button that fails when pressed — and
+ the sentence says what is not affected, because a person reading a missing feature needs to
+ know their file is not the thing that went wrong.
+
+
+
+
Step 4b · press it and wait
+
● Sending the recovered content to SuperDocs for styling…
+ ○ Waiting for SuperDocs to finish — large documents can
+ take minutes.
+
+ Still working. A long document can take a few minutes,
+ and a quiet wait is normal. ▓▓▓▓▓▓░░░░░░░░
+
Check. Trap 2 from the docs, answered in the interface: a long silence is
+ still processing. Measured live at 27 seconds on a four-paragraph document. A page that does not
+ say this turns a slow success into a suspected crash.
+
+
+
+
Step 4c · it worked
+
● Sending the recovered content to SuperDocs for styling…
+ ● Waiting for SuperDocs to finish…
+ ● Approved 5 formatting change(s).
+ ● SuperDocs returned a styled file.
+
+ ┌─ ▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨ ─┐
+ [ Download the styled file ]
+ Saved as …-repaired-styled.docx. It carries the same
+ recovered content as the file above, laid out with
+ consistent styling — it recovers nothing extra. That
+ cost 1 estimated operation. The plain file above is
+ untouched and still yours.
+ └─ ▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨ ─┘
+
Check.Two download buttons are now on the page. The styled copy is
+ an addition; it never replaces the file the person already had.
+
Check. “it recovers nothing extra” — a styled file looks better, and looking
+ better is easy to mistake for having recovered more.
+
Check. The cost is stated, and an unconfirmed number says estimated.
+ The async endpoints return no usage block, so a zero would mean “not reported”, not “free”.
+
+
+
+
Step 4d · it came back rewritten
+
Thrown away. The styled version came back with the wording
+ changed, so it was thrown away rather than handed over.
+ Your document should say what you wrote. The rebuilt file
+ is unchanged and still yours.
+
Check. No styled download is offered. The plain one is still there.
+
Why this exists. On the first live run the styled file came back with three
+ invented paragraphs, a subtotal row, a disclaimer and a signature block. It opened cleanly and read
+ better than the plain rebuild — which is exactly why it is the worst thing this product could hand
+ somebody. The wording is Thrown away, not Failed, because nothing failed: a claim was
+ refused.
+
+
+
+
Step 4e · the other ways it can end
+
Each ends with the plain file still on the page and one sentence saying why.
+
No styled copy. There is no styling allowance left this
+ month, so nothing was sent and nothing was spent. The
+ rebuilt file is unchanged and still yours.
+
+ No styled copy. Styling did not work. The rebuilt file
+ above is unchanged and still yours.
+
+ No styled copy. The connection dropped. The rebuilt file
+ above is unchanged and still yours.
+
Check. The exhausted-allowance case says nothing was spent. The
+ balance is read before the first billable call, so this is a refusal to start rather than a failure
+ halfway through.
+
Check. No exception class name, no ZIP, no XML
+ anywhere. Guarded over rendered output for every recorded repair — BUG-014 was fixed in the engine
+ and came straight back as BUG-020 in the page.
+
+
+
5 Leave, and come back
+
+
Step 5 · repair another document
+
The button at the bottom returns the page to its first screen, cleared.
+
Check. The file input is genuinely reset — choosing the same file again
+ starts a new run rather than doing nothing.
+
+
+
Step 5a · come back to a stale link
+
Downloads are held about thirty minutes. After that:
+ “That repaired file is no longer being held. Repair the document again to get a fresh copy —
+ your original was never changed.”
+
Check. The reassurance is in the error, where it is needed, not only on the
+ first screen where it has been forgotten.
+
+
+
6 Cross-cutting
+
+
+
What
How to check it
Standing
+
+
Keyboard only
Tab through: choose file → download → send for styling → styled download → repair another
Every control is reachable and shows focus
+
Screen reader
The live region announces the current stage while working and the verdict when done
Automated axe pass, idle and reporting, no serious or critical violations
+
400 px wide
One column throughout; the verdict and the download reachable without a horizontal thought
Designed mobile-first
+
Dark mode
The same notice under a desk lamp
Palette defined for both
+
Reduced motion
The working bar stops animating
prefers-reduced-motion honoured
+
No Node installed
The bundle is committed; a hash guard fails the build if it falls behind its source
bundle-manifest.json
+
+
+
+
+
+
+
+
+
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/00-healthy.docx b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/00-healthy.docx
new file mode 100644
index 00000000..7adb1afa
Binary files /dev/null and b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/00-healthy.docx differ
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/01-truncated-download.docx b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/01-truncated-download.docx
new file mode 100644
index 00000000..e87fb4ca
Binary files /dev/null and b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/01-truncated-download.docx differ
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/02-missing-content-types.docx b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/02-missing-content-types.docx
new file mode 100644
index 00000000..835f7639
Binary files /dev/null and b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/02-missing-content-types.docx differ
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/03-unclosed-tags.docx b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/03-unclosed-tags.docx
new file mode 100644
index 00000000..458c3c60
Binary files /dev/null and b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/03-unclosed-tags.docx differ
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/04-bad-characters.docx b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/04-bad-characters.docx
new file mode 100644
index 00000000..6105af71
Binary files /dev/null and b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/04-bad-characters.docx differ
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/05-missing-document-part-SHOULD-FAIL.docx b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/05-missing-document-part-SHOULD-FAIL.docx
new file mode 100644
index 00000000..b78798be
Binary files /dev/null and b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/05-missing-document-part-SHOULD-FAIL.docx differ
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/06-not-a-word-file-SHOULD-FAIL.docx b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/06-not-a-word-file-SHOULD-FAIL.docx
new file mode 100644
index 00000000..ec8f3afe
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/06-not-a-word-file-SHOULD-FAIL.docx
@@ -0,0 +1 @@
+This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.This was never a Word document.
\ No newline at end of file
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/07-empty-body-SHOULD-FAIL.docx b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/07-empty-body-SHOULD-FAIL.docx
new file mode 100644
index 00000000..5e2c8557
Binary files /dev/null and b/use-cases/Priyanshu2425/word-doc-repair/manual-test/fixtures/07-empty-body-SHOULD-FAIL.docx differ
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/index.html b/use-cases/Priyanshu2425/word-doc-repair/manual-test/index.html
new file mode 100644
index 00000000..1c28f561
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/manual-test/index.html
@@ -0,0 +1,779 @@
+
+
+
+
+
+Manual test · Repair my broken Word doc
+
+
+
+
+
+
Manual test · Repair my broken Word doc
+
Fifteen minutes with a browser, a Word-ish app, and eight deliberately broken
+ files. The automated suite already proves the engine does the right thing — this sheet is for
+ the things only a person can judge: whether the page communicates, whether the file it hands
+ back actually opens, and whether the copy is honest.
+
+
+
Your browser is blocking local storage, so your
+ answers will not survive a reload. Copy the Markdown before you close the tab.
+
+
+
+
0 of 19 checked
+ no verdicts yet
+
+
+ Copied
+
+
+
+
+
+
Before you start
+
From the build root
+ (builds/use-cases/word-doc-repair/), in a terminal:
Why 8077 and not 8000: the default is
+ python3 -m docrepair.web on port 8000, but port 8000 is already taken on this
+ machine — so pass PORT. If 8077 is busy too, pick anything free and adjust the
+ URL. If the page does not load, check the terminal: uvicorn prints the address
+ it actually bound to.
+
You will also need Microsoft Word, Apple Pages, LibreOffice or
+ Google Docs to open the repaired files — checking that the download opens is half of this
+ test, and no automated test can do it.
+
The eight test files are in
+ manual-test/fixtures/, generated by
+ python3 manual-test/make_fixtures.py from tests/broken.py — the
+ same generators the pytest suite uses, so what you test by hand is byte-for-byte what CI
+ tests. If a download link below 404s, run that script. If your browser opens a fixture
+ instead of saving it, right-click the link and choose Save Link As.
+
Your answers save automatically to this browser and survive a
+ reload. When you are done, hit Copy results as Markdown and paste the result back.
+
+
+
+
+
The eight files
+
Work top to bottom. Each expected result below was read out of
+ backend/docrepair/engine.py and confirmed against a real run, so the wording quoted is
+ the wording the page should show — near enough, not to the letter.
Nothing — this is an intact Word file, and it is here so you can tell a real failure
+ apart from a fixture that was broken on purpose.
+
What to do
+
+
Download it, then drop it on the page.
+
Download the repaired file and open it in Word / Pages / Google Docs.
+
+
What should happen
+
+
Stages, in order: Opening the file… → The container opened cleanly. →
+ Found 5 sections inside the document. → Checking the document body for
+ damage… → The document body was well-formed. → Reading headings,
+ paragraphs and tables… → Recovered 2 headings, 2 paragraphs and 1 table. →
+ Rebuilding a clean Word file… → Done.
Summary still says “best-effort recovery, not a complete repair” — it does not
+ upgrade its language just because the file was fine. That is deliberate; judge whether
+ it reads as over-cautious.
+
“What came through”: one line, “Kept 2 headings, 2 paragraphs and 1 table, with
+ their structure intact.”“What did not” must not appear at all.
+
The downloaded file is named 00-healthy-repaired.docx, opens without a
+ repair prompt, and contains: Quarterly Report as a heading, Regional
+ breakdown as a heading, and a real 3-row × 2-column table
+ (Region/Revenue, EMEA/1.2M, APAC/0.8M) — a table you can click into, not text with
+ spaces.
The download was cut off at 60%, so the index Word needs to find anything is gone — this
+ is the classic “Word says the file is corrupt” case.
+
What to do
+
+
Try opening it in Word first, so you have seen the failure this tool exists to
+ undo. Word should refuse it.
+
Drop it on the page. Download and open the result.
+
+
What should happen
+
+
Second stage line reads “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.”
+ then “Found and read 4 sections of the file this way.”
+
Succeeds. Counts: 2 headings, 2 paragraphs, 1 table — the same as the healthy
+ control, because the cut took the index, not the content.
+
“What came through” has two lines: the one about reading content out of a file
+ whose internal index was damaged, and the one about keeping the headings, paragraphs and
+ table with their structure intact.
+
“What did not” must not appear. The cut did destroy one of the file's internal
+ parts (the style sheet), but that part is regenerated and carries none of your writing —
+ listing it would tell you that you lost something you did not.
+
Nowhere in anything you can see should the words ZIP, XML,
+ central directory or a filename like document.xml.rels appear.
+
The downloaded file opens with headings as headings and the table as a table.
The internal manifest that tells Word what the pieces are has been deleted — the content
+ is all still there, but nothing can read it.
+
What to do
+
Drop it on the page. Download and open the result.
+
What should happen
+
+
Opens cleanly (no “internal index was damaged” line here — the container itself is
+ fine). Inventory says “Found 4 sections inside the document.” then
+ “Rebuilding 1 missing structural part…” — singular, not “1 part(s)”.
“What came through” leads with “Rebuilt the file's internal structure, which was
+ missing or damaged — this is standard plumbing and carries none of your content.”
+ The second half of that sentence is the point: judge whether it actually reassures you
+ rather than alarming you.
+
“What did not” must not appear.
+
Downloaded file opens with the heading and table structure intact.
The document body itself is chopped off mid-sentence, leaving tags hanging open.
+
What to do
+
+
Drop it on the page. Read the summary paragraph slowly — this is the case where
+ the tool has to admit a real loss.
+
Download and open the result.
+
+
What should happen
+
This is the most important card on the sheet. It succeeds, but it succeeds
+ worse than the ones above, and the whole product rests on whether you can tell.
+
+
Stage line “Closed 7 elements that the damage had left open.” then
+ “The document's structure was too damaged to read. Recovering the text on its own
+ instead.” then “Recovered 6 paragraphs.” No exception name should appear —
+ it used to read “(ParseError)” here, which this sheet caught and is now
+ fixed and tested.
+
Verdict heading is still “Your file is ready”, but the summary must carry a
+ second sentence:
+
“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.”
+
Counts row shows 0 headings, 6 paragraphs, 0 tables. Two of those numbers are
+ zero — check they do not read as a bug or a broken layout.
+
“What came through”: closed 7 elements… and recovered the text of 6
+ paragraphs. “What did not” must appear here, saying headings, tables and
+ formatting could not be preserved.
+
The downloaded file opens and contains the words Quarterly Report,
+ Regional breakdown, EMEA, APAC — but as plain paragraphs.
+ No heading styles, no table. That is correct behaviour, not a failure: the test is
+ whether the page warned you before you opened it.
+
Judgement: would a non-technical person, seeing this screen, understand they
+ must not treat the result as their document back? Or does “Your file is ready” plus a
+ green tick swamp the warning?
“What came through” has three lines (invalid characters, ampersand, kept structure).
+ “What did not” must not appear.
+
In the downloaded file, that sentence reads exactly
+ “Revenue rose in Q3 — driven by renewals & upsell.” — the ampersand is
+ still there as an ampersand. Losing it, or seeing & printed
+ literally, is a fail.
+
The control characters are gone with no visible box or question-mark glyph left
+ behind.
The part that holds all the actual writing has been deleted — the wrapper survived, the
+ document did not.
+
What to do
+
Drop it on the page and read the whole result screen.
+
What should happen
+
+
It must fail. Heading “This one could not be repaired”, in red.
+
Only four stage lines, stopping at
+ “The main document part is missing and cannot be rebuilt.” — the run halts there
+ rather than pretending to carry on.
+
Summary reads: “This file could not be repaired. The main document part is missing
+ entirely, so there is no text to recover. Nothing was invented to fill the gap — if
+ there is another copy, even an older one, that is the better starting point.”
+
No download button. If you can download anything here, that is a serious fail —
+ an empty file that opens fine is the worst possible outcome.
+
“What came through” should be absent. “What did not” has one line about the main
+ document part being missing.
+
No counts row, no zeros, no stack trace, no error code.
+
Judgement: the advice to go find an older copy is the only useful thing left to
+ say. Does it land as help, or as a brush-off?
It is plain text with a .docx name stuck on it — the realistic case of
+ someone who saved, exported or renamed the wrong thing.
+
What to do
+
Drop it on the page. Read “What came through” especially closely.
+
What should happen
+
+
It must fail.“This one could not be repaired”, no download button.
+
Summary: “This file could not be repaired. No readable parts could be found inside
+ the file. Nothing was invented to fill the gap — …”
+
There must be no “What came through” section at all. Nothing came through —
+ the repair failed. Writing this sheet surfaced a real defect here: the failure screen
+ used to show “Read your content out of a file whose internal index was damaged”
+ on a screen that recovered nothing, which is exactly the overclaim this build says it
+ does not make. It is fixed, and a test now pins it. If that line is back, mark
+ this fail.
+
“What did not” should name the problem once. It previously said both
+ “No readable parts could be found inside the file” and “The file contained no
+ readable parts” — one problem stated twice reads as two. Also fixed and tested.
+
Nothing anywhere should mention the file not being a valid archive, or use the word
+ ZIP.
A structurally perfect Word file with nothing written in it — every part valid, the body
+ empty.
+
What to do
+
Drop it on the page. This one is the trap: everything about it looks healthy.
+
What should happen
+
+
It must fail, even though nothing about the file is technically broken. An
+ empty valid document is the most dangerous output there is: it opens cleanly, so its
+ owner may not notice their content is gone for weeks.
+
The stages run further than the other two failures — through “The document body was
+ well-formed.” and “Reading headings, paragraphs and tables…” — before
+ “No readable content was found in the document body.”
+
Summary: “This file could not be repaired. The document body contained no readable
+ text. Nothing was invented to fill the gap — …”
+
No download button. A downloadable, valid, empty .docx here is the
+ single worst failure on this whole sheet.
+
Judgement: the earlier stages all pass with ticks before it fails at the end.
+ Does that read as confusing — a run that looks like it is going well and then is not?
+
+
+
+
+
+
Edge cases
+
Things a real person does that the fixtures do not cover.
+
+
+
+
Download the same repaired file twice
+
Repair 00-healthy.docx, click the download button, then click
+ it again (or press Back and re-download).
+
Expected: the file is held for exactly one download and then dropped,
+ so the second attempt fails. It should fail as a readable sentence —
+ “That file is no longer available. Please repair it again.” — not as a raw JSON blob,
+ a FastAPI error page or a browser 404. Note exactly what you saw; this is a plausible thing
+ for a worried person to do, and a raw error here undoes the whole tone of the product.
+
+
+
+
Drag and drop, not just the file picker
+
Drag a fixture onto the drop zone from Finder / Explorer.
+
Expected: the zone visibly highlights while the file is over it, and
+ the repair starts on drop. Then check the click path works too, and that
+ “Repair another file” returns you to a clean drop zone that accepts a second file
+ without a page reload.
+
+
+
+
A completely empty file
+
Create one — touch empty.docx — and upload it.
+
Expected: the server rejects it with “That file is empty.” and
+ the page shows that sentence on the failure screen rather than a generic
+ “That file could not be read.” or a console error.
+
+
+
+
Refresh mid-repair, and repair something large-ish
+
Reload the page while a repair is running; then try any real
+ .docx you have lying around (a CV, a report) to see the tool meet a document it
+ was not built against.
+
Expected: reloading leaves no stuck spinner and no error. A real
+ document either repairs with sensible counts, or fails with a sentence you can understand —
+ never a stack trace, and never a summary that mentions something you did not upload.
+
+
+
+
+
Judgement checks
+
No test can make these calls. Answer them after you have run all eight files.
+
+
+
+
Does the downloaded file actually open?
+
Open every repaired file you got (cases 00–04) in at least two of
+ Word, Pages, Google Docs, LibreOffice.
+
Pass means: no “Word found unreadable content” prompt, no recovery
+ dialog. Headings are real heading styles (they show up in the navigation pane / outline, not
+ just bigger text), and the table is a real table you can click into a cell of — not
+ tab-separated text. Cases 00, 01, 02 and 04 must all clear this bar. Case 03 must not — it
+ told you the structure was gone.
+
+
+
+
Is the progress genuinely visible, or does it flash past?
+
These files are tiny, so the whole repair takes milliseconds. Watch the
+ stage list closely on case 01.
+
The question: can you actually read the stages as they land, or does
+ the result screen replace them before your eyes get there? Progress you cannot perceive is
+ the same as no progress — and worse, it makes the stage log look decorative. Does anything
+ remain visible afterwards to tell you what was done?
+
+
+
+
Does any wording overclaim?
+
Read every screen you saw for anything that promises more than was
+ delivered: “fixed”, “restored”, “recovered your document”, a green tick next to a partial
+ result, a success heading over a real loss.
+
Specifically: case 03 succeeded while losing every heading and the
+ table, and case 06 failed while showing a “what came through” line. Does the language hold
+ up in both, or does the happy-path framing leak into the sad paths?
+
+
+
+
Would a non-technical person understand what they got back?
+
Read the case 03 result screen as if you had just lost the only copy of
+ your dissertation.
+
The question: do you know, without asking anyone, (a) whether you
+ got your document back, (b) what specifically is missing, (c) what to do next? Flag any
+ phrase you had to read twice, and any word you would not say out loud to a worried
+ friend.
+
+
+
+
Does it read as a consumer product, not an engineering tool?
+
Look for: jargon leaking through (ZIP, XML,
+ ParseError, part filenames, byte counts), log-style phrasing where a sentence should
+ be, counts labelled in lowercase plural like a debug dump. Three such leaks were found and
+ fixed while this sheet was written, and the automated suite now guards the stage log as
+ well as the summary — so anything you still find here is new.
+
Also: does the page look finished — spacing, alignment, the drop
+ zone, the download button — or does it look like a test harness for the engine?
+
+
+
+
Legible in both dark mode and light mode?
+
Switch your OS appearance and reload the product page (not just this
+ sheet). Check every state: drop zone, drop-zone hover, the stage list mid-run, a success
+ screen, a failure screen.
+
Look for: the red failure heading against the dark panel, the green
+ ticks, the muted caveat text at the bottom, the blue download button — anything that goes
+ low-contrast or muddy in one theme. Check this sheet too.
+
+
+
+
Does it work at phone width?
+
Narrow the window to about 360px, or open it on a phone on the same
+ network.
+
Look for: no horizontal scrolling anywhere, the counts row wrapping
+ rather than squashing, long stage sentences not overflowing their row, the download button
+ and “Repair another file” not colliding, and the drop zone still tappable.
+
+
+
+
+
+
+
+
diff --git a/use-cases/Priyanshu2425/word-doc-repair/manual-test/make_fixtures.py b/use-cases/Priyanshu2425/word-doc-repair/manual-test/make_fixtures.py
new file mode 100644
index 00000000..a95076be
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/manual-test/make_fixtures.py
@@ -0,0 +1,46 @@
+"""Dump the test-suite's broken documents to disk for hands-on testing.
+
+The fixtures are the *same* ones the automated suite uses — `tests/broken.py`
+is the single source of truth, so a file a human tests by hand is byte-for-byte
+the file pytest tests. Run from the build root:
+
+ python3 manual-test/make_fixtures.py
+"""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(ROOT))
+
+from tests import broken # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "fixtures"
+
+# (filename, generator). The names are what a tester reads in a download
+# folder, so they say what the file is for, not what function made it.
+FIXTURES = [
+ ("00-healthy.docx", broken.healthy),
+ ("01-truncated-download.docx", broken.truncated_container),
+ ("02-missing-content-types.docx", broken.missing_content_types),
+ ("03-unclosed-tags.docx", broken.unclosed_tags),
+ ("04-bad-characters.docx", broken.bare_ampersand_and_control_chars),
+ ("05-missing-document-part-SHOULD-FAIL.docx", broken.missing_document_part),
+ ("06-not-a-word-file-SHOULD-FAIL.docx", broken.not_a_zip_at_all),
+ ("07-empty-body-SHOULD-FAIL.docx", broken.empty_body),
+]
+
+
+def main() -> int:
+ OUT.mkdir(parents=True, exist_ok=True)
+ for name, make in FIXTURES:
+ data = make()
+ (OUT / name).write_bytes(data)
+ print(f"{name:46} {len(data):>7,} bytes")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/use-cases/Priyanshu2425/word-doc-repair/pyproject.toml b/use-cases/Priyanshu2425/word-doc-repair/pyproject.toml
new file mode 100644
index 00000000..9bdbafbe
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/pyproject.toml
@@ -0,0 +1,27 @@
+[project]
+name = "word-doc-repair"
+version = "1.0.0"
+description = "Best-effort recovery for a Word document that will not open."
+requires-python = ">=3.10"
+dependencies = []
+
+[project.optional-dependencies]
+web = ["fastapi", "uvicorn", "python-multipart"]
+# `httpx2` is what `starlette.testclient` drives the app with. It is listed
+# because leaving it out did not skip the endpoint tests -- it interrupted
+# collection, so `pip install -e ".[web,dev]" && pytest` ran *no* tests at all
+# for anybody following the README. See BUG-057.
+dev = ["pytest", "httpx2"]
+
+[build-system]
+requires = ["setuptools>=68"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.packages.find]
+where = ["backend"]
+include = ["docrepair*"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+pythonpath = ["backend"]
+addopts = "-q"
diff --git a/use-cases/Priyanshu2425/word-doc-repair/screenshot.png b/use-cases/Priyanshu2425/word-doc-repair/screenshot.png
new file mode 100644
index 00000000..748ad8ca
Binary files /dev/null and b/use-cases/Priyanshu2425/word-doc-repair/screenshot.png differ
diff --git a/use-cases/Priyanshu2425/word-doc-repair/tests/__init__.py b/use-cases/Priyanshu2425/word-doc-repair/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/use-cases/Priyanshu2425/word-doc-repair/tests/broken.py b/use-cases/Priyanshu2425/word-doc-repair/tests/broken.py
new file mode 100644
index 00000000..9ce81742
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/tests/broken.py
@@ -0,0 +1,160 @@
+"""Deliberately messy DOCX files — the reviewer's stated test.
+
+Each function breaks a *healthy* document in one recognisable way, so a test
+failure names the fault it was meant to survive.
+"""
+
+from __future__ import annotations
+
+import io
+import zipfile
+
+from docrepair import docx
+
+
+def healthy() -> bytes:
+ return docx.write_docx([
+ docx.Block("heading", "Quarterly Report", 1),
+ docx.Block("paragraph", "Revenue rose in Q3 — driven by renewals & upsell."),
+ docx.Block("heading", "Regional breakdown", 2),
+ docx.Block("table", rows=[["Region", "Revenue"], ["EMEA", "1.2M"], ["APAC", "0.8M"]]),
+ docx.Block("paragraph", "Prepared by the finance team."),
+ ])
+
+
+def _rewrite(data: bytes, changes: dict[str, bytes | None]) -> bytes:
+ """Rebuild the archive with parts replaced (or dropped when value is None)."""
+ buf = io.BytesIO()
+ with zipfile.ZipFile(io.BytesIO(data)) as src, \
+ zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as dst:
+ for name in src.namelist():
+ if name in changes:
+ if changes[name] is None:
+ continue
+ dst.writestr(name, changes[name])
+ else:
+ dst.writestr(name, src.read(name))
+ for name, val in changes.items():
+ if val is not None and name not in src.namelist():
+ dst.writestr(name, val)
+ return buf.getvalue()
+
+
+def truncated_container() -> bytes:
+ """A cut-off download. The central directory lives at the end of a ZIP, so
+ this is the classic "Word says the file is corrupt" case."""
+ data = healthy()
+ return data[: int(len(data) * 0.6)]
+
+
+def missing_content_types() -> bytes:
+ return _rewrite(healthy(), {"[Content_Types].xml": None})
+
+
+def unclosed_tags() -> bytes:
+ """document.xml cut mid-element."""
+ with zipfile.ZipFile(io.BytesIO(healthy())) as z:
+ body = z.read("word/document.xml")
+ return _rewrite(healthy(), {"word/document.xml": body[: int(len(body) * 0.7)]})
+
+
+def bare_ampersand_and_control_chars() -> bytes:
+ with zipfile.ZipFile(io.BytesIO(healthy())) as z:
+ body = z.read("word/document.xml")
+ broken = body.replace(b"renewals & upsell", b"renewals & upsell\x07\x00")
+ return _rewrite(healthy(), {"word/document.xml": broken})
+
+
+def missing_document_part() -> bytes:
+ return _rewrite(healthy(), {"word/document.xml": None})
+
+
+def not_a_zip_at_all() -> bytes:
+ return b"This was never a Word document." * 40
+
+
+def empty_body() -> bytes:
+ body = (b''
+ b"")
+ return _rewrite(healthy(), {"word/document.xml": body})
+
+
+# -- a document with pictures and page furniture ----------------------------
+#
+# The fixtures above are text, headings and a table, because that was what the
+# rebuild carried. A document that loses its photographs loses something its
+# owner will have to go and find again, so the fixtures now include some.
+
+def _png(width: int = 8, height: int = 6) -> bytes:
+ """A real PNG, built here rather than checked in as base64.
+
+ Small enough to be free and genuine enough that the size read out of its
+ header is a fact rather than a fixture constant.
+ """
+ import struct
+ import zlib
+
+ def chunk(kind: bytes, payload: bytes) -> bytes:
+ return (struct.pack(">I", len(payload)) + kind + payload
+ + struct.pack(">I", zlib.crc32(kind + payload) & 0xFFFFFFFF))
+
+ raw = b"".join(b"\x00" + bytes([200, 40, 40] * width) for _ in range(height))
+ return (b"\x89PNG\r\n\x1a\n"
+ + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
+ + chunk(b"IDAT", zlib.compress(raw))
+ + chunk(b"IEND", b""))
+
+
+PICTURE = _png()
+
+_ASIDE = (
+ ''
+ ''
+ "{text}"
+)
+
+
+def illustrated() -> bytes:
+ """A healthy document with a picture, a header, a footer and a footnote."""
+ base = docx.write_docx([
+ docx.Block("heading", "Site inspection", 1),
+ docx.Block("paragraph", "The east elevation, photographed on arrival:"),
+ docx.Block("image", text="word/media/image1.png",
+ image=docx.Image(name="word/media/image1.png", data=PICTURE,
+ width_px=8, height_px=6, measured=True)),
+ docx.Block("paragraph", "No further defects were observed."),
+ ])
+ with zipfile.ZipFile(io.BytesIO(base)) as z:
+ types = z.read("[Content_Types].xml").decode()
+ overrides = "".join(
+ f''
+ for part, ct in (
+ ("word/header1.xml", "application/vnd.openxmlformats-officedocument."
+ "wordprocessingml.header+xml"),
+ ("word/footer1.xml", "application/vnd.openxmlformats-officedocument."
+ "wordprocessingml.footer+xml"),
+ ("word/footnotes.xml", "application/vnd.openxmlformats-officedocument."
+ "wordprocessingml.footnotes+xml"),
+ )
+ )
+ return _rewrite(base, {
+ "[Content_Types].xml": types.replace("", overrides + "").encode(),
+ "word/header1.xml": _ASIDE.format(root="hdr", text="Inspection report — draft")
+ .encode(),
+ "word/footer1.xml": _ASIDE.format(root="ftr", text="Page 1 of 1").encode(),
+ "word/footnotes.xml": _ASIDE.format(
+ root="footnotes",
+ text="Measurements taken with a laser rangefinder.").encode(),
+ })
+
+
+def truncated_illustrated() -> bytes:
+ """The common case, on a document that has something to lose."""
+ data = illustrated()
+ return data[: int(len(data) * 0.85)]
+
+
+def illustrated_without_its_map() -> bytes:
+ """The picture survives; the part saying where it belonged does not."""
+ return _rewrite(illustrated(), {"word/_rels/document.xml.rels": None})
diff --git a/use-cases/Priyanshu2425/word-doc-repair/tests/test_frontend_fixtures.py b/use-cases/Priyanshu2425/word-doc-repair/tests/test_frontend_fixtures.py
new file mode 100644
index 00000000..be420f56
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/tests/test_frontend_fixtures.py
@@ -0,0 +1,89 @@
+"""The fixtures Salvage's interface is tested against are captured from real repairs.
+
+Not written by hand. A hand-written fake of this stream would let the interface
+pass its tests while showing a person something the engine never says -- which is
+the shape of BUG-015, and of BUG-012 and BUG-014 before it.
+
+Regenerate deliberately with:
+
+ SALVAGE_UPDATE_FIXTURES=1 pytest tests/test_frontend_fixtures.py
+"""
+
+from __future__ import annotations
+
+import json
+import os
+from pathlib import Path
+
+import pytest
+
+from docrepair.engine import repair
+from tests import broken
+
+FIXTURES = Path(__file__).resolve().parents[1] / "frontend" / "src" / "test" / "fixtures"
+
+CASES = [
+ ("healthy", "00-healthy.docx", broken.healthy),
+ ("truncated", "01-truncated-download.docx", broken.truncated_container),
+ ("missing-content-types", "02-missing-content-types.docx", broken.missing_content_types),
+ ("unclosed-tags", "03-unclosed-tags.docx", broken.unclosed_tags),
+ ("bad-characters", "04-bad-characters.docx", broken.bare_ampersand_and_control_chars),
+ ("missing-document-part", "05-missing-document-part.docx", broken.missing_document_part),
+ ("not-a-word-file", "06-not-a-word-file.docx", broken.not_a_zip_at_all),
+ ("empty-body", "07-empty-body.docx", broken.empty_body),
+ ("illustrated", "08-illustrated.docx", broken.illustrated),
+ ("truncated-illustrated", "09-truncated-illustrated.docx",
+ broken.truncated_illustrated),
+]
+
+
+def _capture(name: str, filename: str, make) -> dict:
+ """One repair, recorded exactly as the web endpoint streams it."""
+ events: list[dict] = []
+ r = repair(make(), filename, on_progress=lambda stage, message: events.append(
+ {"stage": stage, "message": message}
+ ))
+ report = r.as_payload(f"/api/download/{name}-token", f"/api/style/{name}-token")
+ return {"name": name, "filename": filename, "events": events, "report": report}
+
+
+def _dump(value: object) -> str:
+ return json.dumps(value, indent=2, ensure_ascii=False) + "\n"
+
+
+def test_the_interface_fixtures_are_captured_from_real_repairs_and_have_not_drifted():
+ FIXTURES.mkdir(parents=True, exist_ok=True)
+ update = os.environ.get("SALVAGE_UPDATE_FIXTURES") == "1"
+ stale: list[str] = []
+
+ index = []
+ for name, filename, make in CASES:
+ captured = _capture(name, filename, make)
+ index.append({"name": name, "filename": filename, "ok": captured["report"]["ok"]})
+ path = FIXTURES / f"{name}.json"
+ text = _dump(captured)
+ if update or not path.exists():
+ path.write_text(text)
+ continue
+ if path.read_text() != text:
+ stale.append(name)
+
+ index_path = FIXTURES / "index.json"
+ index_text = _dump(index)
+ if update or not index_path.exists():
+ index_path.write_text(index_text)
+ elif index_path.read_text() != index_text:
+ stale.append("index")
+
+ assert not stale, (
+ "these interface fixtures no longer match what a real repair produces: "
+ + ", ".join(sorted(stale))
+ + ". The interface is tested against them, so a drifted fixture is a page "
+ "that passes its tests and lies to somebody about their document. "
+ "Regenerate with SALVAGE_UPDATE_FIXTURES=1 and read the diff."
+ )
+
+
+@pytest.mark.parametrize("name,_f,_m", CASES)
+def test_every_case_was_captured(name: str, _f: str, _m):
+ assert (FIXTURES / f"{name}.json").exists(), f"{name} was never captured"
diff --git a/use-cases/Priyanshu2425/word-doc-repair/tests/test_repair.py b/use-cases/Priyanshu2425/word-doc-repair/tests/test_repair.py
new file mode 100644
index 00000000..01f3800b
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/tests/test_repair.py
@@ -0,0 +1,327 @@
+"""Keyless, offline. Every test starts from a real DOCX and breaks it."""
+
+import io
+import zipfile
+
+import pytest
+
+from docrepair import repair
+from docrepair.docx import read_blocks
+from tests import broken
+
+
+def opens_cleanly(data: bytes) -> list:
+ """The reviewer's bar: the output is a valid, readable Word file."""
+ with zipfile.ZipFile(io.BytesIO(data)) as z:
+ assert z.testzip() is None
+ for required in ("[Content_Types].xml", "_rels/.rels",
+ "word/document.xml", "word/styles.xml"):
+ assert required in z.namelist(), f"output is missing {required}"
+ return read_blocks(z.read("word/document.xml"))
+
+
+def test_a_healthy_document_round_trips_unchanged():
+ r = repair(broken.healthy(), "fine.docx")
+ assert r.ok
+ blocks = opens_cleanly(r.output)
+ # The spacer paragraph the writer puts after a table is empty, and empty
+ # paragraphs are skipped on read -- so five blocks, not six.
+ assert [b.kind for b in blocks] == ["heading", "paragraph", "heading",
+ "table", "paragraph"]
+
+
+def test_a_truncated_container_still_yields_a_valid_file():
+ r = repair(broken.truncated_container(), "cut.docx")
+ assert r.ok
+ opens_cleanly(r.output)
+ assert any("internal index was damaged" in n for n in r.recovered)
+
+
+def test_a_missing_content_types_part_is_rebuilt_and_said_so():
+ r = repair(broken.missing_content_types(), "ct.docx")
+ assert r.ok
+ opens_cleanly(r.output)
+ assert any("rebuilt the file's internal structure" in n
+ and "carries none of your content" in n for n in r.recovered)
+
+
+def test_unclosed_tags_are_closed_and_the_text_survives():
+ r = repair(broken.unclosed_tags(), "cut.docx")
+ assert r.ok
+ blocks = opens_cleanly(r.output)
+ assert any("Quarterly Report" in b.text for b in blocks)
+ assert any("closed" in n for n in r.recovered)
+
+
+def test_bare_ampersands_and_control_characters_are_repaired():
+ r = repair(broken.bare_ampersand_and_control_chars(), "amp.docx")
+ assert r.ok
+ blocks = opens_cleanly(r.output)
+ assert any("renewals & upsell" in b.text for b in blocks)
+ assert any("ampersand" in n for n in r.recovered)
+ assert any("invalid character" in n for n in r.recovered)
+
+
+def test_the_table_survives_as_a_table_not_as_text():
+ """Structure is the thing people lose. Assert it is actually structure."""
+ r = repair(broken.healthy(), "t.docx")
+ blocks = opens_cleanly(r.output)
+ tables = [b for b in blocks if b.kind == "table"]
+ assert len(tables) == 1
+ assert tables[0].rows[0] == ["Region", "Revenue"]
+ assert len(tables[0].rows) == 3
+
+
+def test_headings_survive_as_headings():
+ r = repair(broken.healthy(), "h.docx")
+ blocks = opens_cleanly(r.output)
+ headings = [b for b in blocks if b.kind == "heading"]
+ assert [h.text for h in headings] == ["Quarterly Report", "Regional breakdown"]
+ assert [h.level for h in headings] == [1, 2]
+
+
+# -- the honest-failure half -------------------------------------------------
+
+def test_a_missing_document_part_fails_and_says_why():
+ r = repair(broken.missing_document_part(), "gone.docx")
+ assert not r.ok
+ assert r.output == b""
+ assert "could not be repaired" in r.summary()
+ assert any("missing entirely" in l for l in r.lost)
+
+
+def test_a_file_that_was_never_a_docx_fails_cleanly():
+ r = repair(broken.not_a_zip_at_all(), "notes.txt")
+ assert not r.ok
+ assert "could not be repaired" in r.summary()
+ assert "Nothing was invented" in r.summary()
+
+
+def test_an_empty_body_is_reported_rather_than_returned_as_success():
+ """An empty valid file is the most dangerous output: it opens fine, so the
+ owner may not notice their content is gone until much later."""
+ r = repair(broken.empty_body(), "empty.docx")
+ assert not r.ok
+ assert any("no readable text" in l for l in r.lost)
+
+
+def test_no_wording_anywhere_claims_a_complete_or_guaranteed_repair():
+ for maker in (broken.healthy, broken.truncated_container, broken.unclosed_tags):
+ r = repair(maker(), "x.docx")
+ text = " ".join([r.summary(), *r.recovered, *(m for _, m in r.stages)]).lower()
+ for forbidden in ("fully repaired", "completely repaired", "fully restored",
+ "guaranteed", "perfect", "as good as new", "100%"):
+ assert forbidden not in text, f"output claims too much: {forbidden!r}"
+ assert "best-effort" in r.summary().lower()
+
+
+def test_a_structure_loss_is_stated_not_hidden():
+ """When only text could be recovered, the user must be told the headings and
+ tables are gone — that is exactly the loss they would otherwise find later."""
+ from docrepair import engine
+
+ r = engine.Repair()
+ r.ok = True
+ r.structure_preserved = False
+ assert "headings, tables and formatting are gone" in r.summary()
+
+
+def test_progress_is_reported_stage_by_stage():
+ seen = []
+ repair(broken.truncated_container(), "p.docx", on_progress=lambda s, m: seen.append(s))
+ assert seen[0] == "open"
+ assert seen[-1] == "done"
+ assert {"inventory", "read", "write"} <= set(seen)
+
+
+def test_a_rebuilt_structural_part_is_never_reported_as_a_loss():
+ """Telling someone they lost `document.xml.rels` is telling them they lost
+ something they did not — it is regenerated, and it carries none of their
+ content. Nothing in the losses list may name a rebuildable part."""
+ from docrepair.salvage import REBUILDABLE
+
+ r = repair(broken.truncated_container(), "cut.docx")
+ assert r.ok
+ for part in REBUILDABLE:
+ assert not any(part in l for l in r.lost), f"reported {part} as lost content"
+
+
+def test_the_failure_sentence_reads_as_a_sentence():
+ r = repair(broken.missing_document_part(), "gone.docx")
+ body = r.summary().split("This file could not be repaired. ", 1)[1]
+ assert body[0].isupper()
+ assert ". Nothing was invented" in r.summary()
+
+
+def test_the_user_facing_lists_are_outcomes_not_diagnostics():
+ """"What came through" is read by someone worried about their document, not
+ by an engineer reading a log. Narration of how the file was opened belongs
+ in the stage log; this list is about what happened to their content."""
+ jargon = ("ZIP", "XML", "CRC", "local header", "zlib", "central directory",
+ "ParseError", "Exception", "traceback")
+ # Every fixture, and the stage log too -- the stage log is what a user
+ # watches while it works, so a leak there is just as visible.
+ for maker in (broken.truncated_container, broken.unclosed_tags,
+ broken.missing_content_types, broken.not_a_zip_at_all,
+ broken.bare_ampersand_and_control_chars, broken.empty_body):
+ r = repair(maker(), "x.docx")
+ joined = " ".join(r.recovered + r.lost + [m for _, m in r.stages])
+ for word in jargon:
+ assert word not in joined, f"{maker.__name__} leaks {word!r}: {joined}"
+
+
+def test_counts_are_pluralised_like_english():
+ from docrepair.salvage import plural
+
+ assert plural(1, "table") == "1 table"
+ assert plural(2, "table") == "2 tables"
+ r = repair(broken.healthy(), "h.docx")
+ assert "1 table," in " ".join(r.recovered) or "1 table " in " ".join(r.recovered)
+ assert "(s)" not in " ".join(r.recovered + r.lost + [m for _, m in r.stages])
+
+
+def test_a_total_failure_claims_nothing_came_through():
+ """The overclaim the manual harness caught: a file that yielded nothing
+ still rendered a "what came through" bullet, because the container note was
+ appended before anything had been read. A recovery claim on a screen where
+ nothing was recovered is exactly what this build promises not to make."""
+ for maker in (broken.not_a_zip_at_all, broken.missing_document_part,
+ broken.empty_body):
+ r = repair(maker(), "x.docx")
+ assert not r.ok
+ assert r.recovered == [], f"{maker.__name__} claims {r.recovered} on a failure"
+
+
+def test_a_loss_is_never_reported_twice():
+ """Two phrasings of one problem read to a user as two problems."""
+ for maker in (broken.not_a_zip_at_all, broken.missing_document_part,
+ broken.empty_body, broken.truncated_container):
+ r = repair(maker(), "x.docx")
+ assert len(r.lost) == len(set(r.lost))
+ no_parts = [l for l in r.lost if "no readable parts" in l]
+ assert len(no_parts) <= 1, r.lost
+
+
+# -- the pictures, and the page furniture -----------------------------------
+#
+# "It was a report with photos and a table of numbers... 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.
+
+def _reopen(data: bytes):
+ return zipfile.ZipFile(io.BytesIO(data))
+
+
+def test_a_picture_comes_back_in_the_rebuilt_file():
+ r = repair(broken.illustrated(), "site.docx")
+ assert r.ok
+ with _reopen(r.output) as z:
+ names = z.namelist()
+ assert "word/media/image1.png" in names
+ assert z.read("word/media/image1.png") == broken.PICTURE
+ assert b"" in z.read("word/document.xml")
+
+
+def test_the_rebuilt_file_declares_the_picture_so_word_will_open_it():
+ """A part with no declared type produces a file that opens on some machines
+ and is called corrupt on others — the worst of the three outcomes."""
+ r = repair(broken.illustrated(), "site.docx")
+ with _reopen(r.output) as z:
+ types = z.read("[Content_Types].xml").decode()
+ rels = z.read("word/_rels/document.xml.rels").decode()
+ body = z.read("word/document.xml").decode()
+ assert 'Extension="png"' in types and "image/png" in types
+ assert "media/image1.png" in rels
+ rel_id = rels.split('Id="', 2)[2].split('"')[0]
+ assert f'r:embed="{rel_id}"' in body, "the drawing points at a relationship that exists"
+
+
+def test_a_picture_survives_the_damage_that_destroys_the_index():
+ """Media are separate members with their own local headers, which is why
+ they survive exactly the truncation Word refuses to open."""
+ r = repair(broken.truncated_illustrated(), "site.docx")
+ assert r.ok and r.counts["pictures"] == 1
+ with _reopen(r.output) as z:
+ assert z.read("word/media/image1.png") == broken.PICTURE
+
+
+def test_a_picture_whose_position_is_lost_is_kept_and_the_report_says_where():
+ """Silently placing it in the wrong paragraph would be worse than obviously
+ placing it at the end."""
+ r = repair(broken.illustrated_without_its_map(), "site.docx")
+ assert r.ok and r.counts["pictures"] == 1
+ assert any("set it at the end" in n for n in r.recovered)
+ assert any("original position" in n for n in r.lost)
+ with _reopen(r.output) as z:
+ assert b"Pictures recovered from this document" in z.read("word/document.xml")
+
+
+def test_footnotes_headers_and_footers_come_back_as_text_and_say_they_moved():
+ r = repair(broken.illustrated(), "site.docx")
+ with _reopen(r.output) as z:
+ body = z.read("word/document.xml").decode()
+ assert "Measurements taken with a laser rangefinder." in body
+ assert "Inspection report" in body and "Page 1 of 1" in body
+ assert "Footnotes, recovered separately" in body
+ # And the report does not let that pass as a full recovery.
+ assert any("could not be put back into their original places" in l for l in r.lost)
+
+
+def test_an_aside_is_never_folded_into_the_body_where_it_would_change_the_meaning():
+ r = repair(broken.illustrated(), "site.docx")
+ with _reopen(r.output) as z:
+ body = z.read("word/document.xml").decode()
+ assert body.index("No further defects") < body.index("Measurements taken")
+
+
+def test_a_document_with_no_pictures_says_nothing_about_pictures():
+ """Absence is stated where it is true, and not where it is not."""
+ r = repair(broken.healthy(), "q.docx")
+ assert r.counts["pictures"] == 0
+ assert not any("picture" in n for n in r.recovered + r.lost)
+
+
+def test_the_size_is_read_out_of_the_image_rather_than_assumed():
+ from docrepair import media
+
+ assert media.image_size(broken.PICTURE) == (8, 6)
+ assert media.image_size(b"not an image at all") is None
+ img = media.collect({"word/media/x.bin": b"not an image at all"})["word/media/x.bin"]
+ assert not img.measured, "an unmeasurable image must not claim a measurement"
+ assert (img.width_px, img.height_px) == media.FALLBACK_PX
+
+
+def test_a_very_wide_picture_is_scaled_to_the_page_and_keeps_its_shape():
+ from docrepair.media import MAX_WIDTH_EMU, Image
+
+ wide = Image("word/media/w.png", b"", width_px=4000, height_px=1000, measured=True)
+ cx, cy = wide.extent
+ assert cx == MAX_WIDTH_EMU
+ assert abs(cx / cy - 4.0) < 0.01
+
+
+# -- seeing it before taking it ---------------------------------------------
+
+def test_the_report_carries_a_preview_of_what_is_in_the_file():
+ """'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 = repair(broken.illustrated(), "site.docx")
+ assert "
Site inspection
" in r.preview_html
+ assert "No further defects were observed." in r.preview_html
+ assert 'src="data:image/png;base64,' in r.preview_html
+
+
+def test_a_failure_carries_no_preview_because_there_is_nothing_to_preview():
+ r = repair(broken.missing_document_part(), "x.docx")
+ assert not r.ok and r.preview_html == ""
+
+
+def test_a_preview_too_large_to_load_names_the_picture_instead_of_dropping_it():
+ from docrepair import docx as _docx
+ from docrepair.media import Image
+
+ blocks = [_docx.Block("image", text="word/media/big.png",
+ image=Image("word/media/big.png", b"x" * 100,
+ width_px=10, height_px=10, measured=True))]
+ html = _docx.blocks_to_html(blocks, inline_images=True, image_budget=10)
+ assert "image-omitted" in html and "is in the file" in html
diff --git a/use-cases/Priyanshu2425/word-doc-repair/tests/test_styled_export.py b/use-cases/Priyanshu2425/word-doc-repair/tests/test_styled_export.py
new file mode 100644
index 00000000..20b20096
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/tests/test_styled_export.py
@@ -0,0 +1,316 @@
+"""The SuperDocs path, keyless. The rule under test is that it can fail in any
+way at all and the user still ends up with the file the local rebuild made."""
+
+import json
+
+import pytest
+
+from docrepair import repair
+from docrepair.styled_export import styled_export
+from docrepair.superdocs_client import Response, SuperDocsClient
+from tests import broken
+
+
+class FakeSuperDocs:
+ def __init__(self, fail_at=None, quota_exhausted=False, no_job=False, no_file=False,
+ never_settles=False, quota_remaining=None):
+ self.fail_at, self.no_job, self.no_file = fail_at, no_job, no_file
+ self.quota_exhausted = quota_exhausted
+ self.never_settles = never_settles
+ # None means the balance cannot be read -- which is the ordinary case
+ # for a personal key, and is not the same as a balance of zero.
+ self.quota_remaining = quota_remaining
+ # What the export hands back. A real .docx by default, built from the
+ # HTML this fake was given, so the content guard has something honest
+ # to check. Tests that want a rewrite pass their own.
+ self.exported = None
+ self.calls, self.approved, self.uploaded_html = [], [], None
+ self._approved = False
+
+ def request(self, method, path, **kw):
+ self.calls.append(path)
+ if self.fail_at and self.fail_at in path:
+ raise ConnectionError("network went away")
+
+ usage = {"ops_charged": 1, "monthly_remaining": 10,
+ "quota_exhausted": self.quota_exhausted}
+
+ if path == "/v1/agents/whoami":
+ if self.quota_remaining is None:
+ return Response(401, {"detail": "not an agent key"})
+ return Response(200, {"quota": {"tier": "free", "monthly_limit": 500,
+ "used": 500 - self.quota_remaining,
+ "remaining": self.quota_remaining}})
+ if path == "/v1/documents/upload":
+ self.uploaded_html = kw["files"]["file"][1]
+ if self.exported is None:
+ self.exported = docx_of(self.uploaded_html)
+ return Response(200, {"ok": True})
+ if path == "/v1/chat/async":
+ if self.no_job:
+ return Response(200, {"usage": usage})
+ return Response(200, {"job_id": "j1", "status": "pending", "usage": usage})
+ if path.startswith("/v1/jobs/"):
+ if self._approved:
+ # Approval is asynchronous on the real API: the job resumes and
+ # only then reaches completed. Exporting before that returns the
+ # pre-edit document with a 200. Verified live 2026-08-19.
+ if self.never_settles:
+ return Response(200, {"status": "in_progress", "usage": usage})
+ return Response(200, {"status": "completed", "usage": usage})
+ batch = json.dumps({"changes": [{"change_id": "ch_1"}]})
+ return Response(200, {"status": "awaiting_approval",
+ "metadata": {"pending_changes": {"content": batch}},
+ "usage": usage})
+ if path.endswith("/approve"):
+ self.approved.extend(kw["json"]["changes"])
+ self._approved = True
+ return Response(200, {"status": "ok", "usage": usage})
+ if path == "/v1/documents/export":
+ if self.no_file:
+ return Response(200, {"raw": b""}, {})
+ return Response(200, {"raw": self.exported}, {})
+ return Response(404, {})
+
+
+def docx_of(html) -> bytes:
+ """A .docx saying exactly what the HTML says -- a styling pass that changed
+ nothing but the styling. The honest answer, which the guard must accept."""
+ import re
+
+ from docrepair.docx import Block, write_docx
+
+ text = html.decode("utf-8") if isinstance(html, bytes) else html
+ words = re.sub(r"<[^>]+>", " ", text)
+ import html as _html
+
+ return write_docx([Block("paragraph", _html.unescape(words))])
+
+
+def blocks_of(data=None):
+ r = repair(data or broken.healthy(), "d.docx")
+ from docrepair.docx import read_blocks
+ import io, zipfile
+ with zipfile.ZipFile(io.BytesIO(r.output)) as z:
+ return read_blocks(z.read("word/document.xml"))
+
+
+def run(fake, blocks=None):
+ return styled_export(SuperDocsClient(fake, sleep=lambda s: None),
+ "sess", blocks or blocks_of())
+
+
+def test_the_four_calls_happen_in_the_required_order():
+ fake = FakeSuperDocs()
+ r = run(fake)
+ assert r.ok and r.output
+ order = [c for c in fake.calls if not c.startswith("/v1/jobs/")]
+ # The balance read comes first and is not part of the contract -- it is
+ # trap 3, asked before the work rather than discovered inside it.
+ assert order == ["/v1/agents/whoami",
+ "/v1/documents/upload", "/v1/chat/async",
+ "/v1/chat/sess/approve", "/v1/documents/export"]
+ # and it waited for the job to settle before exporting
+ approve_at = fake.calls.index("/v1/chat/sess/approve")
+ export_at = fake.calls.index("/v1/documents/export")
+ assert any(c.startswith("/v1/jobs/") for c in fake.calls[approve_at:export_at]), \
+ "exported without waiting for the approved change to be applied"
+
+
+def test_it_sends_html_never_raw_word_xml():
+ """The docs are explicit that there is no endpoint for raw Word XML."""
+ fake = FakeSuperDocs()
+ run(fake)
+ sent = fake.uploaded_html.decode()
+ assert "
" in sent and "
" in sent
+ assert "w:document" not in sent and "PK" not in sent[:4]
+
+
+def test_the_structure_survives_into_the_html():
+ fake = FakeSuperDocs()
+ run(fake)
+ sent = fake.uploaded_html.decode()
+ assert "Quarterly Report" in sent
+ assert "
Regional breakdown
" in sent
+ assert "
EMEA
" in sent
+
+
+def test_the_proposed_change_is_double_parsed_and_actually_approved():
+ fake = FakeSuperDocs()
+ run(fake)
+ assert fake.approved == [{"change_id": "ch_1", "approved": True}]
+
+
+@pytest.mark.parametrize("kwargs,expect", [
+ ({"fail_at": "/v1/chat/async"}, "did not work"),
+ ({"fail_at": "/v1/documents/export"}, "did not work"),
+ ({"quota_exhausted": True}, "allowance is exhausted"),
+ ({"no_job": True}, "did not start a job"),
+ ({"no_file": True}, "returned no file"),
+])
+def test_every_failure_degrades_to_the_local_rebuild_and_says_why(kwargs, expect):
+ """The engine has already produced a valid file before this runs. Nothing
+ here may take that away from the user."""
+ r = run(FakeSuperDocs(**kwargs))
+ assert not r.ok
+ assert r.output == b""
+ assert any(expect in n for n in r.notes), r.notes
+
+
+def test_it_never_raises_whatever_happens():
+ class Exploding:
+ def request(self, *a, **k):
+ raise RuntimeError("boom")
+
+ r = styled_export(SuperDocsClient(Exploding(), sleep=lambda s: None), "s", blocks_of())
+ assert not r.ok and r.notes
+
+
+def test_it_keeps_the_plain_rebuild_when_the_job_never_finishes_applying():
+ """If the job never reaches completed, the styled export would be the
+ document as it was before the edit — so the local rebuild is kept instead."""
+ r = run(FakeSuperDocs(never_settles=True))
+ assert not r.ok
+ assert any("did not finish applying" in n for n in r.notes)
+
+
+def test_the_vendored_client_has_not_drifted():
+ """Build B vendors Build A's client so each stands alone in the builds
+ repository. The multipart fix for BUG-015 landed in the original and not
+ here, so this build kept sending an empty body and getting a 422 long after
+ the other one worked. Everything below the docstring must stay identical."""
+ import pathlib
+
+ here = (pathlib.Path(__file__).resolve().parents[1]
+ / "backend/docrepair/superdocs_client.py")
+ there = (pathlib.Path(__file__).resolve().parents[2]
+ / "quota-aware-agent/backend/quota_aware_agent/client.py")
+ origin_build = there.parents[2]
+ if not origin_build.exists(): # published alone, without its sibling
+ import pytest
+ pytest.skip(f"{origin_build.name} is not in this checkout")
+ assert there.exists(), (
+ f"{origin_build.name} is here but {there.name} is not where this guard "
+ "looks. A guard that skips when its target moves is a guard that has "
+ "stopped guarding, and nothing fails."
+ )
+
+ def body(p):
+ return p.read_text().split('"""', 2)[2]
+
+ assert body(here) == body(there), (
+ "the vendored client has drifted from its origin -- re-copy it, "
+ "keeping only the docstring different"
+ )
+
+
+def test_it_never_reports_zero_operations_for_a_billable_call():
+ """The async endpoints return no usage block, so a zero means "not
+ reported", not "free". Printing "0 operations" about a request that was
+ billed is the same bluff BUG-017 was about, in a new place."""
+ r = run(FakeSuperDocs())
+ assert r.ok
+ assert r.ops_charged >= 1
+
+
+def test_it_does_not_spend_an_allowance_it_has_already_been_told_is_gone():
+ """Trap 3. A styling pass that cannot finish should never be started.
+
+ The failure this prevents is not a wasted call -- it is a person watching a
+ progress line for work that was refused at the far end, which is exactly the
+ shape of "it ran for a bit and then wanted money" the README quotes.
+ """
+ fake = FakeSuperDocs(quota_remaining=0)
+ r = run(fake)
+ assert not r.ok
+ assert fake.calls == ["/v1/agents/whoami"], "it sent something anyway"
+ assert r.ops_charged == 0
+ assert r.allowance_known and r.allowance_remaining == 0
+ assert any("nothing was spent" in n for n in r.notes)
+ assert any("still yours" in n for n in r.notes)
+
+
+def test_a_balance_it_cannot_read_is_not_treated_as_a_balance_of_zero():
+ """A personal key is not an agent key, and `whoami` answers only the latter.
+
+ Refusing on a number nobody managed to read would be its own bluff, so the
+ work proceeds and the report says the balance was unknown.
+ """
+ fake = FakeSuperDocs() # whoami answers 401
+ r = run(fake)
+ assert r.ok, "an unreadable balance stopped work it had no business stopping"
+ assert not r.allowance_known
+
+
+def test_a_balance_that_is_there_is_read_and_reported():
+ fake = FakeSuperDocs(quota_remaining=7)
+ r = run(fake)
+ assert r.ok
+ assert r.allowance_known and r.allowance_remaining == 7
+
+
+def test_a_balance_read_that_blows_up_never_costs_the_caller_the_styling():
+ """The preflight is a courtesy, not a gate. If it cannot answer, it gets out
+ of the way."""
+ class Exploding(FakeSuperDocs):
+ def request(self, method, path, **kw):
+ if path == "/v1/agents/whoami":
+ raise ConnectionError("no network for this one call")
+ return super().request(method, path, **kw)
+
+ r = run(Exploding())
+ assert r.ok and not r.allowance_known
+
+
+def test_a_styled_file_that_says_something_else_is_thrown_away():
+ """The defect this guard exists for, seen live on 2026-08-20: a four-line
+ recovered report came back with three invented paragraphs, a subtotal row,
+ a disclaimer and a signature block. It opens cleanly and it reads better
+ than the plain rebuild, and it is partly fiction. Handing that to somebody
+ who came here to get their own words back is the worst thing this product
+ could do -- worse than returning nothing, because they would not notice.
+ """
+ fake = FakeSuperDocs()
+ fake.exported = _rewritten()
+ r = run(fake)
+ assert not r.ok, "a rewritten document was handed over as a repair"
+ assert r.rejected_for_content
+ assert r.output == b""
+ assert any("wording changed" in n for n in r.notes)
+ assert any("still yours" in n for n in r.notes)
+
+
+def _rewritten() -> bytes:
+ from docrepair.docx import Block, write_docx
+
+ return write_docx([
+ Block("heading", "Quarterly Report", level=1),
+ Block("paragraph", "Revenue rose in Q3."),
+ Block("paragraph", "This growth reflects a sustained commitment to "
+ "client retention and successful expansion."),
+ ])
+
+
+def test_a_styled_file_that_says_the_same_thing_is_accepted():
+ """The guard has to let the good case through, or it is just an off switch."""
+ r = run(FakeSuperDocs())
+ assert r.ok and not r.rejected_for_content
+
+
+def test_the_drift_check_reads_re_wrapping_and_re_escaping_as_no_change():
+ from docrepair.docx import Block, write_docx
+ from docrepair.styled_export import content_drift
+
+ html = "
Q3 & Q4
Revenue rose\n \u2014 driven by renewals.
"
+ same = write_docx([Block("heading", "Q3 & Q4", level=1),
+ Block("paragraph", "Revenue rose \u2014 driven by renewals.")])
+ assert content_drift(html, same) == (0, 0)
+
+
+def test_a_styled_file_that_drops_content_is_thrown_away_too():
+ from docrepair.docx import Block, write_docx
+
+ fake = FakeSuperDocs()
+ fake.exported = write_docx([Block("paragraph", "Quarterly Report")])
+ r = run(fake)
+ assert not r.ok and r.rejected_for_content
diff --git a/use-cases/Priyanshu2425/word-doc-repair/tests/test_web.py b/use-cases/Priyanshu2425/word-doc-repair/tests/test_web.py
new file mode 100644
index 00000000..17ee7b8d
--- /dev/null
+++ b/use-cases/Priyanshu2425/word-doc-repair/tests/test_web.py
@@ -0,0 +1,199 @@
+"""The two endpoints the page actually talks to, exercised as the page does.
+
+`fastapi` is an optional extra — the engine and its suite need nothing
+installed — so these skip rather than fail on a bare checkout. They are the
+tests that catch the class of defect the module tests cannot: a capability that
+exists in the code and is unreachable from the surface a person uses. Styling
+was in `styled_export.py` with tests around it for a day before anything on the
+web page could start it.
+"""
+
+from __future__ import annotations
+
+import json
+
+import pytest
+
+fastapi = pytest.importorskip("fastapi", reason="the web extra is not installed")
+pytest.importorskip("multipart", reason="the web extra is not installed")
+try:
+ from fastapi.testclient import TestClient
+except RuntimeError as missing: # starlette raises this, it does not ImportError
+ # Skip, do not blow up collection. `importorskip` cannot help here: the
+ # import succeeds far enough to raise from inside starlette, and an
+ # uncaught raise at module scope interrupts the *whole* run rather than
+ # this file. That is BUG-057, and it is why this is a try rather than one
+ # more importorskip line.
+ pytest.skip(f"the web extra needs an HTTP client: {missing}",
+ allow_module_level=True)
+
+from docrepair import web # noqa: E402
+from tests import broken # noqa: E402
+
+
+@pytest.fixture
+def client(monkeypatch):
+ monkeypatch.delenv("SUPERDOCS_API_KEY", raising=False)
+ web._READY.clear()
+ return TestClient(web.app)
+
+
+def stream(response) -> tuple[list[dict], dict]:
+ """The page's own read of the wire: stage lines, then the final line."""
+ lines = [json.loads(l) for l in response.text.splitlines() if l.strip()]
+ final = [l for l in lines if l.get("done")]
+ assert len(final) == 1, "a stream must end with exactly one final line"
+ return [l for l in lines if not l.get("done")], final[0]
+
+
+def repair_one(client, data=None, name="broken.docx"):
+ r = client.post("/api/repair", files={"file": (name, data or broken.truncated_container())})
+ assert r.status_code == 200
+ return stream(r)
+
+
+def test_a_repair_streams_its_real_stages_and_then_the_report(client):
+ stages, report = repair_one(client)
+ assert stages, "the page was given no stages to show"
+ assert report["ok"] and report["download"] and report["style"]
+ assert report["preview_html"]
+
+
+def test_the_repaired_file_can_be_downloaded_more_than_once(client):
+ _, report = repair_one(client)
+ first = client.get(report["download"])
+ second = client.get(report["download"])
+ assert first.status_code == second.status_code == 200
+ assert first.content == second.content
+ assert first.content[:2] == b"PK"
+ assert "attachment" in first.headers["content-disposition"]
+
+
+def test_a_file_that_cannot_be_repaired_is_offered_neither_download_nor_styling(client):
+ _, report = repair_one(client, broken.missing_document_part(), "gone.docx")
+ assert not report["ok"]
+ assert report["download"] is None and report["style"] is None
+
+
+def test_an_empty_upload_is_refused_in_the_reader_s_words(client):
+ r = client.post("/api/repair", files={"file": ("empty.docx", b"")})
+ assert r.status_code == 400
+ assert "empty" in r.json()["detail"].lower()
+
+
+def test_a_file_over_the_ceiling_is_refused_before_it_is_read(client):
+ r = client.post("/api/repair",
+ files={"file": ("big.docx", b"x" * (web.MAX_BYTES + 1))})
+ assert r.status_code == 413
+
+
+def test_the_page_is_told_styling_is_off_when_no_key_is_configured(client):
+ body = client.get("/api/capabilities").json()
+ assert body["styling"] is False
+ assert "switched off" in body["note"]
+ # And it says what is *not* affected, because a person reading a greyed-out
+ # step needs to know their file is not the thing that went wrong.
+ assert "Nothing else is affected" in body["note"]
+
+
+def test_styling_is_refused_plainly_rather_than_failing_when_it_is_off(client):
+ _, report = repair_one(client)
+ r = client.post(report["style"])
+ assert r.status_code == 409
+ detail = r.json()["detail"]
+ assert "switched off" in detail and "still yours" in detail
+
+
+def test_a_stale_styling_token_is_refused_the_same_way_a_download_is(client):
+ assert client.post("/api/style/nosuchtoken").status_code == 404
+ assert client.get("/api/download/nosuchtoken").status_code == 404
+
+
+def test_the_page_is_told_styling_is_on_when_a_key_is_configured(client, monkeypatch):
+ monkeypatch.setenv("SUPERDOCS_API_KEY", "sk_test")
+ assert client.get("/api/capabilities").json()["styling"] is True
+
+
+def test_styling_streams_its_own_stages_and_hands_back_a_second_file(client, monkeypatch):
+ """The whole point of the endpoint: a *second* file, and the first one still
+ there afterwards. A styling pass that replaced the plain rebuild would take
+ away the only thing this product promised."""
+ monkeypatch.setenv("SUPERDOCS_API_KEY", "sk_test")
+ from tests.test_styled_export import FakeSuperDocs
+
+ monkeypatch.setattr(web, "_styling_key", lambda: "sk_test")
+ import docrepair.superdocs_client as sc
+
+ # The fake answers every call at once, so nothing here waits on a clock.
+ monkeypatch.setattr(sc, "HttpTransport", lambda key: FakeSuperDocs())
+
+ _, report = repair_one(client)
+ stages, styled = stream(client.post(report["style"]))
+ assert styled["ok"], styled
+ assert stages, "nothing was shown while it worked"
+ assert styled["download"] and styled["download"] != report["download"]
+ assert styled["filename"].endswith("-styled.docx")
+ assert client.get(styled["download"]).content[:2] == b"PK"
+ # the plain rebuild is untouched and still collectable
+ assert client.get(report["download"]).status_code == 200
+
+
+def test_a_styling_failure_leaves_the_plain_rebuild_exactly_where_it_was(client,
+ monkeypatch):
+ monkeypatch.setattr(web, "_styling_key", lambda: "sk_test")
+ import docrepair.superdocs_client as sc
+
+ def explode(key):
+ raise RuntimeError("no transport today")
+
+ monkeypatch.setattr(sc, "HttpTransport", explode)
+
+ _, report = repair_one(client)
+ _, styled = stream(client.post(report["style"]))
+ assert styled["ok"] is False
+ assert styled["download"] is None
+ assert styled["notes"], "it failed without saying anything"
+ assert client.get(report["download"]).status_code == 200
+
+
+def test_no_engine_vocabulary_or_class_name_reaches_the_page_from_either_endpoint(
+ client, monkeypatch):
+ """BUG-020, guarded on the second endpoint before it can happen there."""
+ monkeypatch.setattr(web, "_styling_key", lambda: "sk_test")
+ import docrepair.superdocs_client as sc
+
+ def explode(key):
+ raise ValueError("ValueError: something internal")
+
+ monkeypatch.setattr(sc, "HttpTransport", explode)
+
+ _, report = repair_one(client)
+ stages, styled = stream(client.post(report["style"]))
+ text = " ".join([s["message"] for s in stages] + styled["notes"])
+ for word in ("ValueError", "RuntimeError", "Traceback", "ZIP", "XML"):
+ assert word not in text, f"{word!r} reached a worried person"
+
+
+def test_a_styled_file_that_was_rewritten_never_reaches_the_download(client, monkeypatch):
+ """The guard, at the surface. A rewritten document that got as far as the
+ page would be handed over with a download button and a cheerful line about
+ styling -- which is how somebody ends up circulating three paragraphs they
+ never wrote."""
+ monkeypatch.setattr(web, "_styling_key", lambda: "sk_test")
+ from tests.test_styled_export import FakeSuperDocs, _rewritten
+ import docrepair.superdocs_client as sc
+
+ def transport(key):
+ fake = FakeSuperDocs()
+ fake.exported = _rewritten()
+ return fake
+
+ monkeypatch.setattr(sc, "HttpTransport", transport)
+
+ _, report = repair_one(client)
+ _, styled = stream(client.post(report["style"]))
+ assert styled["ok"] is False
+ assert styled["rejected_for_content"] is True
+ assert styled["download"] is None
+ assert any("wording changed" in n for n in styled["notes"])
+ assert client.get(report["download"]).status_code == 200