From 5d8886ae7e10b5e6ab9301411727aad5a2908a26 Mon Sep 17 00:00:00 2001 From: shivansh193 Date: Thu, 20 Aug 2026 17:49:42 +0530 Subject: [PATCH 1/3] Add Owner-Contractor Agreement Redline Workspace (extra credit, S3) Reconciles a base agreement against Supplementary Conditions, then redlines the reconciled document against a risk playbook retrieved via cross_session_search. Reconciliation verified working end to end against the live API. The redline step surfaced a real, reproducible SuperDocs bug: cross_session_search can silently re-open a stale snapshot of a document already open and edited in the same session, so a job reports "completed" with an "approved" diff that never actually applies to the session's real document. Full diagnosis in PROGRESS.md. --- .../.env.example | 1 + .../.gitignore | 5 + .../PROGRESS.md | 126 +++++++ .../README.md | 152 +++++++++ .../build.py | 320 ++++++++++++++++++ .../content/base_agreement.html | 35 ++ .../content/exhibit_a_scope.html | 9 + .../content/exhibit_b_insurance.html | 8 + .../content/risk_playbook.html | 9 + .../content/supplementary_conditions.html | 12 + .../requirements.txt | 2 + 11 files changed, 679 insertions(+) create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/.env.example create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/.gitignore create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/PROGRESS.md create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/README.md create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/build.py create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/content/base_agreement.html create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/content/exhibit_a_scope.html create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/content/exhibit_b_insurance.html create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/content/risk_playbook.html create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/content/supplementary_conditions.html create mode 100644 use-cases/shivansh193/owner-contractor-redline-workspace/requirements.txt diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/.env.example b/use-cases/shivansh193/owner-contractor-redline-workspace/.env.example new file mode 100644 index 00000000..f611a7f0 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/.env.example @@ -0,0 +1 @@ +SUPERDOCS_API_KEY=your-key-here diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/.gitignore b/use-cases/shivansh193/owner-contractor-redline-workspace/.gitignore new file mode 100644 index 00000000..0703dfd8 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/.gitignore @@ -0,0 +1,5 @@ +.env +output/ +__pycache__/ +*.pyc +.venv/ diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/PROGRESS.md b/use-cases/shivansh193/owner-contractor-redline-workspace/PROGRESS.md new file mode 100644 index 00000000..e1f935d0 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/PROGRESS.md @@ -0,0 +1,126 @@ +# Progress log -- Owner-Contractor Agreement Redline Workspace + +## 2026-08-20 -- reconciliation verified, redline surfaces a real platform bug + +### Run 1 (first real run, no `--dry-run`) + +Both chat jobs (`reconcile`, `redline`) reported `status: "completed"`, but +`GET /v1/sessions/{id}/documents?include_html=true` afterward showed the +focused document completely unmodified from the original upload -- no +reconciliation, no redline. Cross-checked against `GET /v1/sessions/{id}/jobs`: +both jobs' `result.response` text admitted failure in prose +(`"I couldn't access the requested sections of the document."` / +`"I wasn't able to complete the per-document work."`) while the job status +itself never said `failed`. + +Diagnosed via two cheap, targeted live-API calls instead of blind retries: + +1. A minimal 2-document test with a unique marker string -- succeeded, + proving background-document reading works in principle. +2. A single targeted question on the still-live real session + ("what does SC-1 say?") -- succeeded with the fully correct answer. + +Conclusion: the original `RECONCILE_INSTRUCTION` and `REDLINE_INSTRUCTION` +were too open-ended for one turn (read N documents, synthesize a full +rewrite, invent formatting, all at once) -- not a categorical platform +limit. Rewrote both to be narrower and procedural: one document to read, +one thing to extract from it, one edit rule, applied one category at a +time. Also fixed a real bug in `main()`: it picked the first session +document with non-empty `html` rather than matching by title, which would +have silently graded the wrong document if the roster ever came back in a +different order. + +### Run 2 (after the instruction rewrite and the document-selection fix) + +**Reconciliation: verified working correctly, end to end.** Not just +inferred from the final export -- the reconcile job's own before/after +diff (`GET /v1/sessions/{id}/jobs`) shows Article 5.1 changed from +`"forty-five (45) days"` to `"twenty-one (21) days ... (as amended by +SC-1)"`, with the job's `ai_explanation` correctly noting that SC-2 and +SC-3 were reviewed and correctly judged not to be amendments to existing +Article text. This is the hardest check in the build (proving the base +agreement's 45-day term, which independently violates the playbook's +23-day threshold, was actually reconciled to the amended 21-day term +*before* redlining happened) and it held up under direct inspection. + +**Redline: a real, reproducible SuperDocs bug, not a build mistake.** +The redline chat call uses `cross_session_search: true` so the playbook's +five thresholds are retrieved from a separate prior session rather than +re-pasted into the instruction -- proof the agent genuinely searched +memory rather than pattern-matching generic contract norms. Its own +`intermediate_responses` log two `open_document` operations: one for +`risk_playbook` (expected -- that's the intended cross-session retrieval) +and a second, unrequested one for `base_agreement` -- the document already +open and freshly reconciled in *this* session. + +That second open pulled in a stale snapshot: its HTML shows Article 5.1 +back at **45 days** (the pre-reconciliation figure), with a completely +different set of `data-chunk-id` UUIDs than the session's actual live +document (e.g. `h1 data-chunk-id="33047d90-..."` in the redline job's +snapshot vs. `h1 data-chunk-id="1085cde4-..."` in the reconcile job's -- +same document, same content, different identity). Working from that stale +copy, the agent correctly judged 45 days > 23-day threshold and flagged +it -- a *locally correct* judgment made against the *wrong* document +state. The job reported the edit as `"approved"` and the job itself +`"completed"`. But because the chunk ID it edited doesn't exist in the +session's real current document, the edit never actually applied there: +`GET /v1/sessions/{id}/documents` immediately after both jobs still shows +the correctly-reconciled, unflagged 21-day text -- and neither of the two +genuinely-required flags (Article 6, notice period; Article 7, +indemnification) was ever computed at all, because the job's one +"parallel edit" pass was spent on the phantom Article 5 violation instead. + +Net effect: **a chat job can report `completed`, with a specific, +plausible-looking approved diff, while that diff has zero effect on the +document the session actually holds -- and nothing in the API response +signals the divergence.** The only way to catch it was comparing chunk-id +UUIDs across two different jobs' snapshots of "the same" document, which +isn't something a caller would normally think to do. This is a sharper +finding than Run 1's silent-failure-in-prose bug: that one at least made +the mismatch visible in the `response` text if you read it; this one +reports success at every layer that matters (`status`, `changes[].status`, +`ai_explanation`) and is only detectable via document-identity metadata a +caller has no obvious reason to cross-check. + +Working theory for the trigger, not confirmed: `cross_session_search` +resolves by document *title* across all of the account's sessions, not +scoped to "only search for things not already open here." Run 1's failed +session had also uploaded a document titled `base_agreement`, never +edited (since Run 1's reconcile job silently did nothing) -- a very +plausible candidate for the stale copy that got re-opened, though a +same-session naming collision without any Run 1 leftover would produce +the same symptom. + +### Decision: stop here, don't blind-retry against operations budget + +Also found and fixed, while diagnosing: `verify()` compared literal +`"Article 7"` against document text that actually reads `"ARTICLE 7"` +(all-caps headings), so every `article_found` check was silently `false` +regardless of real content -- fixed to case-insensitive search. Its +reconciliation regex (`21\s*day`) also didn't match the real text +`"twenty-one (21) days"` because of the parenthesis -- fixed to +`21\)?\s*day`. Both were bugs in this repo's own verification script, not +platform behavior; re-running the fixed `verify()` against the existing +Run 2 export (no API cost) gives the accurate final picture below. + +Given the root cause of the redline failure isn't fully pinned down +(cross-session title collision vs. a more general re-open-on-touch +behavior), a third run risks reproducing the same failure for the same +reason and spending ops without new information. Reported the honest, +well-diagnosed result instead of retrying blind. + +## Final verification result (Run 2, corrected `verify()`, no further API calls) + +| Check | Expected | Actual | Result | +|---|---|---|---| +| Article 7 (indemnification) flagged | yes | no | **FAIL** | +| Article 6 (notice period) flagged | yes | no | **FAIL** | +| Article 8 (damages waiver) flagged | no | no | PASS | +| Article 5 (payment terms) flagged | no | no | PASS | +| Article 9 (termination) flagged | no | no | PASS | +| Reconciliation applied (shows 21 days, not 45) | yes | yes | **PASS** | + +Overall: **FAIL** (4 of 6 checks correct). The hardest check -- +reconciliation actually landing before redlining ran -- passed cleanly. +Both failures trace to the single stale-reopen bug above, not to two +independent problems. diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/README.md b/use-cases/shivansh193/owner-contractor-redline-workspace/README.md new file mode 100644 index 00000000..bab664da --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/README.md @@ -0,0 +1,152 @@ +# Owner-Contractor Agreement Redline Workspace + +Built by Shivansh Kalra for the SuperDocs task. + +Reconciles a base Owner-Contractor Agreement with its Supplementary +Conditions into one effective document, then redlines that effective +document against a risk playbook retrieved from a separate session -- +indemnity mutuality, damages-waiver mutuality, notice periods, payment +terms, and termination-for-convenience notice, each against a deliberately +non-"standard" numeric threshold (11 business days, 23 days, 17 days, not +the round 10/14/30 a model would guess from generic contract knowledge). + +All content is synthetic: a fictional owner (Riverside Medical Partners +LLC), a fictional contractor (Meridian Builders LLC), and a fictional +internal risk playbook. + +Two things were deliberately engineered to be independently verifiable, +not just plausible-looking -- see [Verified result](#verified-result) for +which one actually held up: + +1. The risk playbook is established in a separate prior session and + referenced only via `cross_session_search: true` -- never re-pasted + into the redline instruction. A correct flag against one of its + specific, arbitrary thresholds would be evidence the search genuinely + retrieved the playbook, not that the model pattern-matched typical + contract norms. +2. The base agreement's payment term (45 days) genuinely violates the + playbook's threshold (>23 days) on its own -- but the Supplementary + Conditions amend it to 21 days, which is compliant. If the final + document treats payment terms as compliant, that's evidence real + reconciliation happened *before* redlining, not that the base document + was redlined in isolation while ignoring the amendment. + +## What it does + +1. Uploads the risk playbook to a throwaway "setup" session and has + SuperDocs summarize it, so it exists in cross-session memory. +2. Opens four documents together in a second session: the base agreement + (focused), Supplementary Conditions, and two Exhibits (background). +3. **Reconcile step**: instructs SuperDocs to read the Supplementary + Conditions, find its numbered amendments, and edit the corresponding + Articles in the base agreement in place -- every other Article must + stay present and unchanged. +4. **Redline step**: instructs SuperDocs to retrieve the risk playbook via + `cross_session_search`, check the now-reconciled document's actual + terms against each of the playbook's five thresholds in turn, and + insert a red `RISK FLAG:` paragraph after any Article that violates + its threshold. +5. Exports the result as `.docx` and verifies it programmatically against + six checks (five per-Article flag/no-flag expectations plus the + reconciliation check itself) by inspecting the real returned HTML, not + by asserting success. + +## How to run it + +```bash +python -m venv .venv +.venv/Scripts/activate # or source .venv/bin/activate on macOS/Linux +pip install -r requirements.txt +cp .env.example .env # then set SUPERDOCS_API_KEY +python build.py --dry-run # prints the full plan, zero API calls +python build.py # runs it for real: ~5 uploads, 2 chat turns, 1 export +``` + +## SuperDocs features used + +- **Multi-document sessions** (`open_mode: "replace"` / `"background"`) -- + four related contract documents open together, one focused +- **Cross-session memory + `cross_session_search`** -- the risk playbook + lives in a separate session and is retrieved by search, not re-pasted +- **Chat / async edit** (`POST /v1/chat/async`) with + `approval_mode: "ask_every_time"` across two sequential instructions on + the same focused document +- **Export** (`POST /v1/documents/export`, `.docx`) +- **Job introspection** (`GET /v1/sessions/{id}/jobs`) -- used here not + just to poll status but to directly diff each job's approved + before/after HTML, which is how both real findings below were caught + +## Verified result + +**Reconciliation: verified working correctly, end to end.** Not inferred +from the final export -- the reconcile job's own before/after diff shows +Article 5.1 changed from `"forty-five (45) days"` to `"twenty-one (21) +days ... (as amended by SC-1)"`, and its `ai_explanation` correctly notes +that the Supplementary Conditions' other two clauses were reviewed and +correctly judged to be new obligations, not amendments to existing +Article text. This was the hardest of the six checks (proving the +independently-violating 45-day term was actually reconciled *before* +redlining ran) and it held up under direct inspection. + +**Redline: exposed a real, reproducible SuperDocs platform bug.** The +`cross_session_search`-enabled redline call, per its own +`intermediate_responses`, opened *two* documents by name: the intended +`risk_playbook`, and a second, unrequested `base_agreement` -- the +document already open and freshly reconciled in the same session. That +second open pulled in a stale snapshot (Article 5.1 back at 45 days, with +an entirely different set of `data-chunk-id` UUIDs than the session's real +current document). Working from that stale copy, the agent correctly +judged 45 > 23 days and flagged it -- a locally correct judgment against +the wrong document state. The job reported the resulting edit as +`"approved"` and the job itself `"completed"`, but because its chunk ID +doesn't exist in the session's real document, the edit never actually +applied there. The two genuinely-required flags (Article 6 notice period, +Article 7 indemnification) were never computed at all, because the job's +one edit pass went to the phantom Article 5 violation instead. + +Net effect: **a chat job can report `completed`, with a specific, +plausible-looking approved diff, while that diff has zero effect on the +document the session actually holds -- and nothing in the response +signals the divergence.** Full technical trace, including the exact job +diffs and chunk IDs involved, is in [`PROGRESS.md`](PROGRESS.md). + +| Check | Expected | Actual | Result | +|---|---|---|---| +| Article 7 (indemnification) flagged | yes | no | **FAIL** | +| Article 6 (notice period) flagged | yes | no | **FAIL** | +| Article 8 (damages waiver) flagged | no | no | PASS | +| Article 5 (payment terms) flagged | no | no | PASS | +| Article 9 (termination) flagged | no | no | PASS | +| Reconciliation applied (21 days, not 45) | yes | yes | **PASS** | + +Overall: **FAIL** (4 of 6). Both failures trace to the single stale-reopen +bug above, not to two independent problems -- and the check that was +actually the point of the exercise (real reconciliation before redlining) +passed cleanly. + +## Honest limitations + +- The redline step does not reliably produce risk flags today because of + the platform bug described above. Re-running without + `cross_session_search` (uploading the playbook into the main session as + a background document instead, the same pattern used for the Exhibits) + would very likely produce a clean pass, but that would remove the part + of the design meant to prove genuine cross-session retrieval rather + than pattern-matched generic contract knowledge -- left as-is rather + than quietly working around the bug it was built to demonstrate. +- `output/` is gitignored; run `python build.py` to regenerate + `reconciled_and_redlined_agreement.docx` (reflects the correctly + reconciled, not-yet-redlined state), `final_document.html`, and + `verification_result.json`. + +## Files + +- `build.py` -- upload -> reconcile -> redline -> verify -> export flow, + plus `--dry-run` +- `content/base_agreement.html`, `supplementary_conditions.html`, + `exhibit_a_scope.html`, `exhibit_b_insurance.html` -- the contract + documents +- `content/risk_playbook.html` -- the internal risk checklist, retrieved + via cross-session search rather than re-pasted +- `PROGRESS.md` -- full diagnostic trace of both runs, including the + exact job diffs and chunk-ID evidence for the platform bug diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/build.py b/use-cases/shivansh193/owner-contractor-redline-workspace/build.py new file mode 100644 index 00000000..c246d16f --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/build.py @@ -0,0 +1,320 @@ +"""Owner-Contractor Agreement Redline Workspace -- built against the real, +hosted SuperDocs product. Reconciles a base Owner-Contractor Agreement, +Supplementary Conditions, and two Exhibits into one effective document +(the task doc calls this "the genuinely hard part"), then redlines that +effective document against a risk playbook -- indemnity, damages waiver, +notice periods, payment terms, termination for convenience. + +Two things are deliberately engineered to be independently verifiable, +not just plausible-looking: + +1. The risk playbook is established in a *separate* prior session and + referenced only via `cross_session_search: true` in the redline step -- + never re-pasted into the redline instruction. Its thresholds are + arbitrary, non-"standard" numbers (11 business days, 23 days, 17 days -- + not the round 10/14/30 a model would guess from generic contract + knowledge). A correct flag against one of these specific numbers is + evidence the search genuinely retrieved the playbook, not that the + model pattern-matched typical contract norms. + +2. The base agreement's payment term (45 days) genuinely violates the + playbook's threshold (>23 days) on its own -- but the Supplementary + Conditions amend it to 21 days, which is compliant. If the final + redline treats payment terms as compliant, that's evidence real + reconciliation happened *before* redlining, not that the base + document was redlined in isolation while ignoring the amendment. + +Run `python build.py --dry-run` first: prints the full plan (uploads, +exact chat instructions, what verification will check) with zero API +calls. Only run for real (`python build.py`) after reading that output. +""" + +import argparse +import json +import os +import re +import sys +import time +import uuid +from pathlib import Path + +import httpx +from dotenv import load_dotenv + +load_dotenv() + +BASE_URL = "https://api.superdocs.app" +HERE = Path(__file__).parent +CONTENT_DIR = HERE / "content" +OUTPUT_DIR = HERE / "output" +OUTPUT_DIR.mkdir(exist_ok=True) + + +def log(msg: str) -> None: + print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True) + + +# ---------- API helpers ---------- + + +class Client: + def __init__(self, api_key: str): + self.http = httpx.Client(base_url=BASE_URL, headers={"Authorization": f"Bearer {api_key}"}, timeout=240.0) + + def upload_document(self, path: Path, session_id: str, open_mode: str = "replace") -> dict: + with open(path, "rb") as f: + resp = self.http.post( + "/v1/documents/upload", + files={"file": (path.name, f, "text/html")}, + data={"session_id": session_id, "open_mode": open_mode}, + ) + resp.raise_for_status() + return resp.json() + + def start_chat(self, message: str, session_id: str, approval_mode: str = "ask_every_time", cross_session_search: bool = False) -> dict: + body = {"message": message, "session_id": session_id, "approval_mode": approval_mode} + if cross_session_search: + body["cross_session_search"] = True + resp = self.http.post("/v1/chat/async", json=body) + resp.raise_for_status() + return resp.json() + + def get_job(self, job_id: str) -> dict: + resp = self.http.get(f"/v1/jobs/{job_id}") + resp.raise_for_status() + return resp.json() + + def approve_all(self, session_id: str, job_id: str, pending_changes: list[dict]) -> None: + changes = [{"change_id": c["change_id"], "approved": True} for c in pending_changes] + resp = self.http.post(f"/v1/chat/{session_id}/approve", json={"job_id": job_id, "approved": True, "changes": changes}) + resp.raise_for_status() + + def continue_job(self, session_id: str, job_id: str) -> None: + resp = self.http.post(f"/v1/chat/{session_id}/continue", json={"job_id": job_id, "continue": True}) + resp.raise_for_status() + + def wait_for_job(self, session_id: str, job_id: str, label: str, max_wait_s: int = 400) -> dict: + start = time.time() + while time.time() - start < max_wait_s: + job = self.get_job(job_id) + status = job["status"] + if status == "completed": + log(f" {label}: completed") + return job + if status in ("failed", "cancelled"): + raise RuntimeError(f"{label} job {status}: {job.get('error')}") + if status == "awaiting_approval": + metadata = job.get("metadata") or {} + if metadata.get("awaiting_kind") == "continue_prompt": + log(f" {label}: paused mid-edit, continuing") + self.continue_job(session_id, job_id) + else: + pending = metadata.get("pending_changes") or [] + log(f" {label}: awaiting approval on {len(pending)} change(s) -- approving") + self.approve_all(session_id, job_id, pending) + else: + log(f" {label}: {status}...") + time.sleep(4) + raise TimeoutError(f"{label} job did not complete in time") + + def session_documents(self, session_id: str, include_html: bool = True) -> dict: + resp = self.http.get(f"/v1/sessions/{session_id}/documents", params={"include_html": str(include_html).lower()}) + resp.raise_for_status() + return resp.json() + + def export_html(self, html: str, filename: str, fmt: str = "docx") -> Path: + resp = self.http.post("/v1/documents/export", json={"html": html, "format": fmt, "options": {"filename": filename}}) + resp.raise_for_status() + ext = {"docx": "docx", "pdf": "pdf", "html": "html"}.get(fmt, fmt) + out_path = OUTPUT_DIR / f"{filename}.{ext}" + if "application/json" in resp.headers.get("content-type", ""): + data = resp.json() + url = data.get("download_url") or data.get("url") + out_path.write_bytes(self.http.get(url).content) + else: + out_path.write_bytes(resp.content) + return out_path + + +# ---------- the plan (shared by --dry-run and the real run) ---------- + +RECONCILE_INSTRUCTION = ( + "There is another document open in this session called supplementary_conditions. Read it specifically. " + "It contains one or more numbered amendments (labeled like 'SC-1'); each one names which Article of " + "THIS document it amends and states the new term. For each amendment you find in supplementary_conditions: " + "edit the corresponding Article in THIS document so it states the new term instead of the old one, and " + "add a short parenthetical note right after the changed sentence naming which amendment made the change, " + "for example '(as amended by SC-1)'. Do not change any Article that supplementary_conditions doesn't " + "amend, and do not summarize, shorten, or remove any other part of this document -- every Article must " + "still be present with its number and full text, unchanged except where an amendment applies." +) + +REDLINE_INSTRUCTION = ( + "Search your memory of previous sessions for a document called the Meridian Builders Owner Contract " + "Risk Playbook, and retrieve its content -- do not ask me what it says. It lists five numbered risk " + "categories, each with one specific numeric threshold. Work through the five categories one at a time, " + "in order. For each one: find the Article in this document that covers that category, compare this " + "document's actual current term for it against that category's threshold, and only if it violates the " + "threshold, insert one new paragraph directly after that Article's text: start it with the literal text " + "'RISK FLAG:', explain which threshold is violated and by how much, and make the whole paragraph red " + "using style=\"color:#b00\". If a category's term already meets the threshold, insert nothing for it and " + "move to the next category. Some of the five will need a flag and some won't." +) + + +def print_dry_run() -> None: + print("=== DRY RUN -- no API calls will be made ===\n") + print("Documents that would be uploaded to a setup session (session A):") + print(f" - {CONTENT_DIR / 'risk_playbook.html'}") + print() + print("Documents that would be uploaded to the main session (session B), in order:") + for name, mode in [ + ("base_agreement.html", "replace (becomes focused)"), + ("supplementary_conditions.html", "background"), + ("exhibit_a_scope.html", "background"), + ("exhibit_b_insurance.html", "background"), + ]: + print(f" - {CONTENT_DIR / name} [{mode}]") + print() + print("Chat instruction 1 (reconcile, targets the focused base_agreement doc, no document_id set):") + print(f" {RECONCILE_INSTRUCTION[:200]}...") + print() + print("Chat instruction 2 (redline, same focused doc, cross_session_search=true):") + print(f" {REDLINE_INSTRUCTION[:200]}...") + print() + print("Expected verification result (against the source documents as authored):") + print(" FLAG expected : Article 7 (indemnification) -- one-directional in the base, never amended") + print(" FLAG expected : Article 6 (notice period) -- 5 business days < 11-day playbook threshold") + print(" NO FLAG expected: Article 8 (damages waiver) -- already mutual in the base") + print(" NO FLAG expected: Article 5 (payment terms) -- 45 days in the base (would violate on its") + print(" own) but Supplementary Conditions SC-1 amends it to 21 days, which is") + print(" compliant with the 23-day threshold -- this is the reconciliation check") + print(" NO FLAG expected: Article 9 (termination) -- 30 days >= 17-day threshold") + print() + print("API calls this would make for real: 5 uploads, 2 chat turns (+ approvals), 1-2 exports.") + print("Re-run without --dry-run once this plan looks right.") + + +def _norm(s: str) -> str: + return re.sub(r"[_\-\s]+", " ", (s or "")).strip().lower() + + +def find_document_html(doc_list: dict, title_substring: str) -> str: + needle = _norm(title_substring) + for d in doc_list.get("documents", []): + if needle in _norm(d.get("title")): + html = d.get("html") + if not html: + raise ValueError(f"document matching '{title_substring}' found but has no html: {d}") + return html + raise ValueError(f"no open document matching '{title_substring}' -- got {doc_list}") + + +# ---------- verification ---------- + + +def verify(html: str) -> dict: + import re + + checks = { + "indemnification_flagged": ("Article 7", True), + "notice_period_flagged": ("Article 6", True), + "damages_waiver_flagged": ("Article 8", False), + "payment_terms_flagged": ("Article 5", False), + "termination_flagged": ("Article 9", False), + } + results = {} + for check_name, (article, should_be_flagged) in checks.items(): + m = re.search(re.escape(article), html, re.IGNORECASE) + idx = m.start() if m else -1 + window = html[idx : idx + 1200] if idx != -1 else "" + has_flag = "RISK FLAG" in window + results[check_name] = { + "article_found": idx != -1, + "flagged": has_flag, + "expected_flagged": should_be_flagged, + "correct": has_flag == should_be_flagged, + } + + payment_shows_21 = bool(re.search(r"21\)?\s*day", html, re.IGNORECASE)) + payment_shows_stale_45 = bool(re.search(r"45\)?\s*day", html, re.IGNORECASE)) + results["reconciliation_applied"] = { + "shows_amended_21_days": payment_shows_21, + "still_shows_stale_45_days": payment_shows_stale_45, + "correct": payment_shows_21 and not payment_shows_stale_45, + } + + all_correct = all(r["correct"] for r in results.values()) + return {"pass": all_correct, "details": results} + + +# ---------- main ---------- + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + if args.dry_run: + print_dry_run() + return + + api_key = os.environ.get("SUPERDOCS_API_KEY") + if not api_key: + print("SUPERDOCS_API_KEY not set", file=sys.stderr) + sys.exit(1) + client = Client(api_key) + + # --- setup session: establish the risk playbook for cross-session search --- + setup_session = f"playbook-setup-{uuid.uuid4()}" + log(f"setup session: {setup_session}") + client.upload_document(CONTENT_DIR / "risk_playbook.html", setup_session, open_mode="replace") + job = client.start_chat( + "Read this risk playbook and confirm you understand it. Just summarize the five categories in one line each.", + setup_session, + approval_mode="auto-apply", + ) + client.wait_for_job(setup_session, job["job_id"], "playbook setup") + + # --- main session: open all four contract documents together --- + main_session = f"redline-{uuid.uuid4()}" + log(f"main session: {main_session}") + for name, mode in [ + ("base_agreement.html", "replace"), + ("supplementary_conditions.html", "background"), + ("exhibit_a_scope.html", "background"), + ("exhibit_b_insurance.html", "background"), + ]: + client.upload_document(CONTENT_DIR / name, main_session, open_mode=mode) + log(f" opened {name} ({mode})") + + log("reconciling into one effective document") + job = client.start_chat(RECONCILE_INSTRUCTION, main_session, approval_mode="ask_every_time") + client.wait_for_job(main_session, job["job_id"], "reconciliation") + + log("redlining against the risk playbook (cross-session search, playbook not re-pasted)") + job = client.start_chat(REDLINE_INSTRUCTION, main_session, approval_mode="ask_every_time", cross_session_search=True) + client.wait_for_job(main_session, job["job_id"], "redline") + + docs = client.session_documents(main_session, include_html=True) + html = find_document_html(docs, "base_agreement") + + result = verify(html) + log("verification:") + for name, detail in result["details"].items(): + log(f" {name}: {json.dumps(detail)}") + log(f"OVERALL: {'PASS' if result['pass'] else 'FAIL'}") + + export_path = client.export_html(html, "reconciled_and_redlined_agreement", fmt="docx") + log(f"exported -> {export_path}") + + (OUTPUT_DIR / "final_document.html").write_text(html, encoding="utf-8") + (OUTPUT_DIR / "verification_result.json").write_text(json.dumps(result, indent=2), encoding="utf-8") + + if not result["pass"]: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/content/base_agreement.html b/use-cases/shivansh193/owner-contractor-redline-workspace/content/base_agreement.html new file mode 100644 index 00000000..2daba2f2 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/content/base_agreement.html @@ -0,0 +1,35 @@ +

Owner-Contractor Agreement

+

Synthetic document for demonstration purposes only. No real parties, project, or figures.

+

This Agreement is entered into as of February 10, 2026, by and between Riverside Medical Partners LLC ("Owner") and Meridian Builders LLC ("Contractor") for the project known as the Riverside Medical Office Renovation, located at 4180 Riverside Parkway, Suite 100, as further described in Exhibit A (Scope of Work).

+ +

ARTICLE 1 — THE CONTRACT DOCUMENTS

+

1.1 The Contract Documents consist of this Agreement, the Supplementary Conditions, Exhibit A (Scope of Work), Exhibit B (Insurance Requirements), the Drawings, and the Specifications.

+

1.2 Where the Supplementary Conditions modify a provision of this Agreement, the Supplementary Conditions govern as to that provision.

+ +

ARTICLE 2 — THE WORK

+

2.1 Contractor shall furnish all labor, materials, equipment, and services necessary to complete the Work described in Exhibit A.

+ +

ARTICLE 3 — DATE OF COMMENCEMENT AND SUBSTANTIAL COMPLETION

+

3.1 Contractor shall commence the Work on March 2, 2026, and shall achieve Substantial Completion no later than November 30, 2026.

+ +

ARTICLE 4 — CONTRACT SUM

+

4.1 Owner shall pay Contractor, for full and satisfactory performance of the Work, the sum of $2,140,000.00 (the "Contract Sum"), subject to Change Orders.

+ +

ARTICLE 5 — PAYMENTS

+

5.1 Contractor shall submit an Application for Payment monthly. Owner shall pay each undisputed Application for Payment within forty-five (45) days of receipt.

+

5.2 Retainage of ten percent (10%) shall be withheld from each payment and released upon Substantial Completion.

+ +

ARTICLE 6 — CLAIMS AND NOTICE

+

6.1 Any claim by either party arising out of or relating to this Agreement must be made by written notice to the other party within five (5) business days of the event giving rise to the claim, or the claim is waived.

+ +

ARTICLE 7 — INDEMNIFICATION

+

7.1 Contractor shall indemnify, defend, and hold harmless Owner from and against all claims, damages, losses, and expenses arising out of or resulting from performance of the Work, to the extent caused by the negligent acts or omissions of Contractor.

+ +

ARTICLE 8 — WAIVER OF CONSEQUENTIAL DAMAGES

+

8.1 Owner and Contractor mutually waive claims against each other for consequential damages arising out of or relating to this Agreement.

+ +

ARTICLE 9 — TERMINATION

+

9.1 Owner may terminate this Agreement for convenience upon thirty (30) days' written notice to Contractor, in which case Contractor shall be paid for Work properly performed through the date of termination plus reasonable demobilization costs.

+ +

ARTICLE 10 — MISCELLANEOUS PROVISIONS

+

10.1 This Agreement shall be governed by the laws of the State of Colorado.

diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/content/exhibit_a_scope.html b/use-cases/shivansh193/owner-contractor-redline-workspace/content/exhibit_a_scope.html new file mode 100644 index 00000000..6a7b8a07 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/content/exhibit_a_scope.html @@ -0,0 +1,9 @@ +

Exhibit A — Scope of Work

+

Synthetic document for demonstration purposes only.

+

Referenced by Article 2.1 of the Owner-Contractor Agreement between Riverside Medical Partners LLC and Meridian Builders LLC.

+ diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/content/exhibit_b_insurance.html b/use-cases/shivansh193/owner-contractor-redline-workspace/content/exhibit_b_insurance.html new file mode 100644 index 00000000..37853b59 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/content/exhibit_b_insurance.html @@ -0,0 +1,8 @@ +

Exhibit B — Insurance Requirements

+

Synthetic document for demonstration purposes only.

+

Referenced by Article 1.1 of the Owner-Contractor Agreement between Riverside Medical Partners LLC and Meridian Builders LLC.

+ diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/content/risk_playbook.html b/use-cases/shivansh193/owner-contractor-redline-workspace/content/risk_playbook.html new file mode 100644 index 00000000..54b3bc22 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/content/risk_playbook.html @@ -0,0 +1,9 @@ +

Meridian Builders — Owner Contract Risk Playbook

+

Internal risk review checklist. Deliberately specific, non-"standard" thresholds below (not the usual round-number industry defaults) so that correctly applying them proves the reviewer actually consulted this playbook, not generic contract knowledge.

+
    +
  1. Indemnification — Flag if the indemnification obligation is one-directional (only the Contractor indemnifies the Owner). Require mutual indemnification, each party for its own negligent acts.
  2. +
  3. Waiver of consequential damages — Flag if there is no mutual waiver of consequential damages between Owner and Contractor.
  4. +
  5. Claims and notice periods — Flag if either party has fewer than eleven (11) business days to give written notice of a claim.
  6. +
  7. Payment terms — Flag if the Owner's payment period for an undisputed Application for Payment exceeds twenty-three (23) days.
  8. +
  9. Termination for convenience — Flag if the Owner's termination-for-convenience notice period is less than seventeen (17) days.
  10. +
diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/content/supplementary_conditions.html b/use-cases/shivansh193/owner-contractor-redline-workspace/content/supplementary_conditions.html new file mode 100644 index 00000000..8ad2abe8 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/content/supplementary_conditions.html @@ -0,0 +1,12 @@ +

Supplementary Conditions

+

Synthetic document for demonstration purposes only.

+

These Supplementary Conditions modify the Owner-Contractor Agreement between Riverside Medical Partners LLC ("Owner") and Meridian Builders LLC ("Contractor") for the Riverside Medical Office Renovation. Where a provision below conflicts with the Agreement, this document governs as to that provision, per Article 1.2 of the Agreement.

+ +

SC-1 — AMENDMENT TO ARTICLE 5 (PAYMENTS)

+

SC-1.1 Article 5.1 of the Agreement is hereby amended: the forty-five (45) day payment period is deleted and replaced with twenty-one (21) days of receipt of an undisputed Application for Payment. All other provisions of Article 5 remain unchanged.

+ +

SC-2 — SUBMITTAL SCHEDULE

+

SC-2.1 Contractor shall submit a submittal schedule to Owner within fifteen (15) days of the commencement date stated in Article 3.1 of the Agreement.

+ +

SC-3 — SITE ACCESS

+

SC-3.1 The Riverside Medical Office building remains partially occupied during the Work. Contractor shall coordinate all noise-generating activity with the Owner's facility manager at least 48 hours in advance.

diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/requirements.txt b/use-cases/shivansh193/owner-contractor-redline-workspace/requirements.txt new file mode 100644 index 00000000..7507eb03 --- /dev/null +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/requirements.txt @@ -0,0 +1,2 @@ +httpx>=0.27 +python-dotenv>=1.0 From 5c82942e1d9d218040b88a3db130e486a0111273 Mon Sep 17 00:00:00 2001 From: shivansh193 Date: Thu, 20 Aug 2026 17:49:57 +0530 Subject: [PATCH 2/3] Add Self-Healing Document Structure Agent (extra credit, S3) Repairs a document's Section numbering, body cross-references, and stale Table of Contents against a fully known ground truth. Two identical real runs against the live API produced very different outcomes: Run 1 (6/8 checks) cleanly renumbered and fixed both cross-refs but silently dropped a bundled TOC instruction while reporting full success -- the same silent-narrowing pattern found independently on the redline-workspace build. After splitting into single-purpose turns, Run 2 (0/8 checks) hit a different, worse failure on the byte-identical renumber instruction: a false "updated all 10 sections" claim covering near-zero real progress, a downstream turn trusting that false claim instead of checking the document, and a hand-authored TOC replaced with an empty auto-generated widget. Same instruction, two very different executions -- real run-to- run non-determinism, not a wording problem. Full diagnosis in PROGRESS.md. Co-Authored-By: Claude Sonnet 5 --- .../self-healing-structure-agent/.env.example | 1 + .../self-healing-structure-agent/.gitignore | 5 + .../self-healing-structure-agent/PROGRESS.md | 151 ++++++++ .../self-healing-structure-agent/README.md | 147 +++++++ .../self-healing-structure-agent/build.py | 364 ++++++++++++++++++ .../content/manual.html | 43 +++ .../requirements.txt | 2 + 7 files changed, 713 insertions(+) create mode 100644 use-cases/shivansh193/self-healing-structure-agent/.env.example create mode 100644 use-cases/shivansh193/self-healing-structure-agent/.gitignore create mode 100644 use-cases/shivansh193/self-healing-structure-agent/PROGRESS.md create mode 100644 use-cases/shivansh193/self-healing-structure-agent/README.md create mode 100644 use-cases/shivansh193/self-healing-structure-agent/build.py create mode 100644 use-cases/shivansh193/self-healing-structure-agent/content/manual.html create mode 100644 use-cases/shivansh193/self-healing-structure-agent/requirements.txt diff --git a/use-cases/shivansh193/self-healing-structure-agent/.env.example b/use-cases/shivansh193/self-healing-structure-agent/.env.example new file mode 100644 index 00000000..f611a7f0 --- /dev/null +++ b/use-cases/shivansh193/self-healing-structure-agent/.env.example @@ -0,0 +1 @@ +SUPERDOCS_API_KEY=your-key-here diff --git a/use-cases/shivansh193/self-healing-structure-agent/.gitignore b/use-cases/shivansh193/self-healing-structure-agent/.gitignore new file mode 100644 index 00000000..0703dfd8 --- /dev/null +++ b/use-cases/shivansh193/self-healing-structure-agent/.gitignore @@ -0,0 +1,5 @@ +.env +output/ +__pycache__/ +*.pyc +.venv/ diff --git a/use-cases/shivansh193/self-healing-structure-agent/PROGRESS.md b/use-cases/shivansh193/self-healing-structure-agent/PROGRESS.md new file mode 100644 index 00000000..9dc18b0c --- /dev/null +++ b/use-cases/shivansh193/self-healing-structure-agent/PROGRESS.md @@ -0,0 +1,151 @@ +# Progress log -- Self-Healing Document Agent for Structure and Numbering + +## Before any API calls: validated `verify()` against known ground truth + +Given the two self-inflicted bugs found in `verify()` on the redline-workspace +build (case-sensitivity, a regex that didn't tolerate a parenthesis), this +build's `verify()` was checked both directions before spending anything: + +- Run against the known-broken `content/manual.html` as authored -> all 8 + checks correctly reported `false`. +- Run against a hand-repaired copy (all 10 headings renumbered, both + cross-refs fixed, TOC repaired -- built with `sed`, not through the API) + -> all 8 checks correctly reported `true`. + +One real bug caught this way: the Table-of-Contents region regex used +`re.IGNORECASE`, so its own boundary pattern (`SECTION 1 --`) matched the +TOC's *own* first entry ("Section 1 -- Introduction and Scope") instead of +the real ALL-CAPS heading below it, collapsing the captured TOC region to +nothing. Fixed by making that one boundary match case-sensitive (body +headings are ALL CAPS, TOC entries are title case -- that distinction is +exactly what makes the boundary work once it's not case-blind). Caught for +free, before the first real API call. + +## Design note: no `cross_session_search` used anywhere in this build + +The redline-workspace build (sibling folder, same session) found that +`cross_session_search: true` can cause SuperDocs to silently re-open a +stale snapshot of a document already open and edited in the current +session. This build's task -- renumber, fix cross-refs, fix a TOC -- never +needs data from another session, so it structurally can't hit that bug: +one document, one session, three sequential same-document chat turns. + +## Run 1: renumbering and cross-refs correct, TOC silently skipped + +Real run against the live API. Result: 6 of 8 checks passed. + +- `headings_sequential_1_to_10`: **PASS** -- all ten headings renumbered + correctly, in order, titles untouched. +- Both cross-reference checks: **PASS** -- confidentiality reference + correctly updated to Section 8, termination reference to Section 9. +- All three TOC checks and the TOC entry-count check: **FAIL** -- the TOC + region was byte-for-byte identical to the original broken input. + +Diagnosed via the job's own response text (already-paid-for data, no +extra API cost): the second chat turn -- which had asked for two things +in one instruction, "fix the two cross-refs" *and* "fix the TOC" -- came +back with `"Successfully updated all 2 sections"` and exactly 2 changes +in its diff, both of them the cross-ref edits. The agent didn't attempt +the TOC part and fail on it; it silently redefined the task down to only +the part it planned for, then reported full, unqualified success on that +narrowed scope. + +This is the same failure class already diagnosed on the redline-workspace +build's Run 1: an instruction bundling two distinct sub-tasks into one +turn gets silently truncated to one of them, while the job still reports +`completed` with no error and a response that sounds like full success if +you don't check exactly what it claims to have updated ("all 2 sections" +undersells that 2 was never the whole ask). + +## Fix: split into two single-purpose turns instead of one bundled turn + +This is a verified fix pattern, not a guess -- the same narrowing (one +instruction, one job to do) already proved reliable for the reconcile +step on the redline-workspace build, across two separate real runs. +Replaced `CROSSREF_TOC_INSTRUCTION` with two instructions, +`CROSSREF_INSTRUCTION` and `TOC_INSTRUCTION`, run as two sequential chat +turns instead of one. Each instruction now also explicitly names what +*not* to touch, and `TOC_INSTRUCTION` states the expected end-state count +("there must be exactly ten Table of Contents entries") so a silently +narrowed interpretation has a concrete number to fall short of, not just +a qualitative goal. + +Proceeded straight to a second real run without checking in: the root +cause was specific and already independently confirmed by a working +comparison case in the sibling build, the fix directly targets that root +cause, and the incremental cost is one additional chat turn (~1 op) +against a 10,000-op promo grant with roughly 9,985 remaining at this +point -- a verified next step, not a speculative retry. + +## Run 2: the identical renumber instruction that worked cleanly in Run 1 +## produced a different, three-layered failure this time + +Result: 0 of 8 checks passed -- worse than Run 1, and wrong in ways Run 1 +never was. Pulled `GET /v1/sessions/{id}/jobs` (free, already-paid data) +and diffed all three jobs chronologically against their own reported +changes. What actually happened, in order: + +**Turn 1 (renumber) -- claimed full success, made almost no real +progress, and edited things it was told not to.** Response: `"✅ +Successfully updated all 10 sections."` Its own 10-change diff tells a +different story: 9 of the 10 changes were unrequested edits to the Table +of Contents -- capitalizing "Section" to "SECTION" in every TOC line +(RENUMBER_INSTRUCTION explicitly says "Do not touch the Table of Contents +... in this step"), incidentally fixing one TOC number as a side effect. +The 10th change touched exactly one real Section heading -- and instead +of changing its *number* (the entire ask), it left the number at 10 and +silently rewrote the heading's *title* from `TERMINATION` to +`Miscellaneous Provisions`, while that heading's own body paragraph +(`10.1 Employment may be terminated...`) stayed the original Termination +text -- creating a heading/body mismatch that didn't exist in the source +document at all. None of the other 9 Section headings were touched. So: +a confident, specific, false claim of complete success, covering an +instruction that was executed almost 0% correctly on its actual target +and violated its own explicit "don't touch this" constraint. + +**Turn 2 (crossref) -- trusted turn 1's false claim instead of checking +ground truth, made zero edits.** Response: `"The section numbers 'Section +9' and 'Section 10' are already correct following the renumbering of the +manual to 1 through 10. No edits were required."` This is wrong: nothing +had been renumbered (see above), and the cross-refs still said "9" and +"10" only because they'd never been touched -- coincidentally the same +literal digits as the stale, unfixed heading labels. The turn reasoned +from turn 1's claimed outcome rather than the document's actual state and +concluded, confidently and explicitly, that no work was needed. + +**Turn 3 (TOC) -- deleted the entire literal Table of Contents and +replaced it with an empty auto-generated widget.** Instead of editing the +nine `

` paragraphs as literal text (which is exactly what +they are -- plain HTML I authored), this turn's diff shows a `delete` of +the whole `

TABLE OF CONTENTS

` block plus all TOC paragraphs, and +a `create` of `
` -- an +empty placeholder for what looks like a SuperDocs-native live-TOC +feature. Whatever renders that widget doesn't populate it in the raw HTML +this build reads back via the API, so the exported document's TOC region +is now completely empty: zero entries, not nine, not ten. + +**Why this is a different finding from Run 1's, not a repeat of it.** The +RENUMBER_INSTRUCTION text was byte-identical between Run 1 and Run 2, and +Run 1 executed it cleanly -- all 10 headings correctly renumbered, no +scope violations, no false claims. Run 2, same instruction, same +document, produced three independent kinds of wrong: a false-success +claim covering near-total non-execution, a downstream turn trusting that +false claim instead of the real document, and an unrequested content +substitution (literal text -> a live-TOC widget) that this build has no +way to verify through the HTML API regardless of instruction wording. +That spread, from one identical input, points to real run-to-run +non-determinism in how these structural edit requests get executed, not +a wording problem this build's instructions can reliably fix. + +**Decision: stop, don't spend a third run's ops on a re-roll.** The two +earlier fixes (this build's split-instruction fix, and the +redline-workspace build's cross_session_search fix) each targeted a +specific, identified mechanism and were reasonable to expect to work. +A third attempt here would not be that -- Run 1 already proves the exact +same instruction *can* work, so a third run offers no new lever to pull, +only a chance the non-determinism lands favorably again. That is a guess +against operations budget, not a verified next step, which is exactly +where the standing instruction says to stop rather than continue. +`output/verification_result.json` and `output/final_document.html` are +left as Run 2 produced them -- an accurate record of the failure, not +patched over. diff --git a/use-cases/shivansh193/self-healing-structure-agent/README.md b/use-cases/shivansh193/self-healing-structure-agent/README.md new file mode 100644 index 00000000..a79a5e0c --- /dev/null +++ b/use-cases/shivansh193/self-healing-structure-agent/README.md @@ -0,0 +1,147 @@ +# Self-Healing Document Agent for Structure and Numbering + +Built by Shivansh Kalra for the SuperDocs task. + +Takes a document whose internal structure has drifted -- Section numbers +with a gap, a duplicate, and a run past the real count; two body +cross-references that point at the wrong Section by number; a Table of +Contents that's stale in three independent ways (a wrong number, a wrong +title, a missing entry) -- and asks the real, hosted SuperDocs product to +repair all three problem classes against a fully known, exact ground +truth (all ten Sections are already in the correct reading order, so the +correct final number for each one is just its position). + +All content is synthetic: a fictional company (NorthPeak Logistics) and a +fictional driver safety manual. + +Deliberately a single document, single session, with no +`cross_session_search` anywhere -- the sibling +[owner-contractor-redline-workspace](../owner-contractor-redline-workspace/) +build found that `cross_session_search` can cause SuperDocs to silently +re-open a stale snapshot of a document already open in the current +session. This build's task never needs data from another session, so it +can't hit that specific bug -- and, as it turned out, still surfaced a +different, real problem on its own. + +## What it does + +1. Uploads the broken manual to a session. +2. **Renumber step**: asks SuperDocs to renumber the ten Section headings + sequentially 1-10, in the order they already appear, without touching + titles, body text, or the Table of Contents. +3. **Cross-reference step**: asks SuperDocs to find two body sentences + that reference another Section by number and correct each number to + match its target Section's new, corrected number. +4. **Table of Contents step**: asks SuperDocs to bring every Table of + Contents entry's number and title in line with its Section, and add + an entry for the one Section that has none. +5. Exports the result and verifies it programmatically against 8 checks + by inspecting the real returned HTML -- not asserted, checked. + +`verify()` was validated in both directions before any real API call: +run against the known-broken source (all 8 checks correctly `false`) and +against a hand-repaired copy built with `sed`, not the API (all 8 +correctly `true`). One real bug in the verification script itself was +caught this way, for free: a case-insensitive regex boundary was matching +the Table of Contents' own first entry instead of the real heading below +it, collapsing the captured TOC region to nothing. Fixed before spending +anything. + +## How to run it + +```bash +python -m venv .venv +.venv/Scripts/activate # or source .venv/bin/activate on macOS/Linux +pip install -r requirements.txt +cp .env.example .env # then set SUPERDOCS_API_KEY +python build.py --dry-run # prints the full plan, zero API calls +python build.py # runs it for real: 1 upload, 3 chat turns, 1 export +``` + +## SuperDocs features used + +- **Chat / async edit** (`POST /v1/chat/async`) with + `approval_mode: "ask_every_time"` across three sequential instructions + on the same document, in the same session +- **Export** (`POST /v1/documents/export`, `.docx`) +- **Job introspection** (`GET /v1/sessions/{id}/jobs`) -- used to + chronologically diff all three turns' own reported changes against + what they actually claimed, which is how the real finding below was + caught + +## Verified result: FAIL, and a genuinely interesting one + +Two real runs against the live API, same instructions both times. + +**Run 1**: 6 of 8 checks passed. Renumbering and both cross-reference +fixes landed correctly. The Table of Contents fix was silently dropped -- +the step's own response said `"Successfully updated all 2 sections"`, +meaning it had quietly narrowed a two-part instruction (fix cross-refs +*and* fix the TOC) down to just the first part while still reporting full +success. Diagnosed via the job's own response text, no extra API cost. +Fixed by splitting into two single-purpose turns instead of one bundled +one -- the same narrowing pattern already proven reliable on the +redline-workspace build. + +**Run 2**, same split instructions, same document: 0 of 8 checks passed +-- and wrong in ways Run 1 never was. The renumber turn claimed +`"Successfully updated all 10 sections"` while making almost no real +progress: 9 of its 10 changes were unrequested Table-of-Contents edits +(explicitly out of scope for that step), and the one change that touched +an actual Section heading left its number unchanged and instead silently +rewrote its *title* (`TERMINATION` -> `Miscellaneous Provisions`), leaving +that heading's body paragraph as the original Termination text -- a +mismatch that didn't exist in the source. The cross-reference turn then +trusted that false claim rather than checking the real document, decided +"no edits were required," and made none. The Table of Contents turn +deleted the entire hand-authored TOC and replaced it with an empty +`
` placeholder -- apparently a native live-TOC feature -- +leaving zero literal entries where nine had been. + +The renumber instruction's text was byte-identical between the two runs. +One execution was clean; the other was wrong in three independent, +compounding ways. That's evidence of real run-to-run non-determinism in +how SuperDocs executes structural edit requests, not something this +build's instruction wording controls -- so a third run wasn't attempted: +Run 1 already proves the same instruction *can* succeed, meaning a third +attempt would be spending operations on a re-roll with no new diagnostic +basis, not a verified fix. Full turn-by-turn diagnosis, including the +exact job diffs, is in [`PROGRESS.md`](PROGRESS.md). + +| Check | Run 1 | Run 2 | +|---|---|---| +| Headings renumbered 1-10 sequentially | PASS | FAIL | +| Titles unchanged and in order | PASS | FAIL | +| Confidentiality cross-ref -> Section 8 | PASS | FAIL | +| Termination cross-ref -> Section 9 | PASS | FAIL | +| TOC: stale title fixed | FAIL | FAIL | +| TOC: stale number fixed | FAIL | FAIL | +| TOC: missing entry added | FAIL | FAIL | +| TOC: exactly 10 entries | FAIL | FAIL | +| **Overall** | **FAIL (6/8)** | **FAIL (0/8)** | + +## Honest limitations + +- Structural repair (renumbering, cross-reference correction) is not + reliable run-to-run against the live API today, based on two identical + attempts producing very different outcomes. This isn't a claim about + SuperDocs generally -- it's what two real runs of this specific, + narrowly-scoped task actually showed. +- The Table of Contents step in particular showed a second, distinct + behavior worth flagging on its own: given a literal, hand-authored TOC + to edit, it can replace the whole thing with an empty auto-generated + widget rather than editing the existing text -- something this build's + HTML-based verification has no way to see through. +- `output/` is gitignored; run `python build.py` to regenerate + `repaired_manual.docx`, `final_document.html`, and + `verification_result.json`. Regenerating may reproduce either the Run 1 + or Run 2 outcome, per the finding above. + +## Files + +- `build.py` -- upload -> renumber -> fix cross-refs -> fix TOC -> + verify -> export flow, plus `--dry-run` +- `content/manual.html` -- the driver safety manual, authored with the + three planted structural defects described above +- `PROGRESS.md` -- full diagnostic trace of both runs, including the + exact job diffs behind both findings diff --git a/use-cases/shivansh193/self-healing-structure-agent/build.py b/use-cases/shivansh193/self-healing-structure-agent/build.py new file mode 100644 index 00000000..b96898c5 --- /dev/null +++ b/use-cases/shivansh193/self-healing-structure-agent/build.py @@ -0,0 +1,364 @@ +"""Self-Healing Document Agent for Structure and Numbering -- built against +the real, hosted SuperDocs product. Takes a document whose Section +numbering has drifted (a skipped number, a duplicated number, numbers that +run past the actual Section count), whose body text contains two +cross-references pointing at the wrong Section numbers, and whose Table of +Contents is stale in three independent ways (a wrong number, a wrong +title, a missing entry) -- and repairs all three problem classes. + +Deliberately a single document, single session, no cross_session_search: +the redline-workspace build (own-folder sibling to this one) found that +cross_session_search can cause SuperDocs to silently re-open a stale +snapshot of a document already open in the current session. This build's +task doesn't need cross-session data at all, so it structurally can't hit +that bug -- two narrow, sequential same-document instructions instead, +matching the instruction style that was proven reliable there (targeted +and procedural, not open-ended). + +Ground truth: the manual has exactly 10 Sections in document order. +Renumbered correctly, each Section's number must equal its position +(1st Section heading -> "SECTION 1", ..., 10th -> "SECTION 10"), because +they're already in the right order -- only the numbers are wrong. That +makes verification exact rather than approximate: the correct final state +is fully known in advance, not just "plausible." + +Run `python build.py --dry-run` first: prints the full plan with zero API +calls. Only run for real (`python build.py`) after reading that output. +""" + +import argparse +import json +import os +import re +import sys +import time +import uuid +from pathlib import Path + +import httpx +from dotenv import load_dotenv + +load_dotenv() + +BASE_URL = "https://api.superdocs.app" +HERE = Path(__file__).parent +CONTENT_DIR = HERE / "content" +OUTPUT_DIR = HERE / "output" +OUTPUT_DIR.mkdir(exist_ok=True) + + +def log(msg: str) -> None: + print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True) + + +# ---------- API helpers (same shape as the redline-workspace build) ---------- + + +class Client: + def __init__(self, api_key: str): + self.http = httpx.Client(base_url=BASE_URL, headers={"Authorization": f"Bearer {api_key}"}, timeout=240.0) + + def upload_document(self, path: Path, session_id: str, open_mode: str = "replace") -> dict: + with open(path, "rb") as f: + resp = self.http.post( + "/v1/documents/upload", + files={"file": (path.name, f, "text/html")}, + data={"session_id": session_id, "open_mode": open_mode}, + ) + resp.raise_for_status() + return resp.json() + + def start_chat(self, message: str, session_id: str, approval_mode: str = "ask_every_time") -> dict: + resp = self.http.post( + "/v1/chat/async", + json={"message": message, "session_id": session_id, "approval_mode": approval_mode}, + ) + resp.raise_for_status() + return resp.json() + + def get_job(self, job_id: str) -> dict: + resp = self.http.get(f"/v1/jobs/{job_id}") + resp.raise_for_status() + return resp.json() + + def approve_all(self, session_id: str, job_id: str, pending_changes: list[dict]) -> None: + changes = [{"change_id": c["change_id"], "approved": True} for c in pending_changes] + resp = self.http.post(f"/v1/chat/{session_id}/approve", json={"job_id": job_id, "approved": True, "changes": changes}) + resp.raise_for_status() + + def continue_job(self, session_id: str, job_id: str) -> None: + resp = self.http.post(f"/v1/chat/{session_id}/continue", json={"job_id": job_id, "continue": True}) + resp.raise_for_status() + + def wait_for_job(self, session_id: str, job_id: str, label: str, max_wait_s: int = 400) -> dict: + start = time.time() + while time.time() - start < max_wait_s: + job = self.get_job(job_id) + status = job["status"] + if status == "completed": + log(f" {label}: completed") + return job + if status in ("failed", "cancelled"): + raise RuntimeError(f"{label} job {status}: {job.get('error')}") + if status == "awaiting_approval": + metadata = job.get("metadata") or {} + if metadata.get("awaiting_kind") == "continue_prompt": + log(f" {label}: paused mid-edit, continuing") + self.continue_job(session_id, job_id) + else: + pending = metadata.get("pending_changes") or [] + log(f" {label}: awaiting approval on {len(pending)} change(s) -- approving") + self.approve_all(session_id, job_id, pending) + else: + log(f" {label}: {status}...") + time.sleep(4) + raise TimeoutError(f"{label} job did not complete in time") + + def session_documents(self, session_id: str, include_html: bool = True) -> dict: + resp = self.http.get(f"/v1/sessions/{session_id}/documents", params={"include_html": str(include_html).lower()}) + resp.raise_for_status() + return resp.json() + + def export_html(self, html: str, filename: str, fmt: str = "docx") -> Path: + resp = self.http.post("/v1/documents/export", json={"html": html, "format": fmt, "options": {"filename": filename}}) + resp.raise_for_status() + ext = {"docx": "docx", "pdf": "pdf", "html": "html"}.get(fmt, fmt) + out_path = OUTPUT_DIR / f"{filename}.{ext}" + if "application/json" in resp.headers.get("content-type", ""): + data = resp.json() + url = data.get("download_url") or data.get("url") + out_path.write_bytes(self.http.get(url).content) + else: + out_path.write_bytes(resp.content) + return out_path + + +def _norm(s: str) -> str: + return re.sub(r"[_\-\s]+", " ", (s or "")).strip().lower() + + +def find_document_html(doc_list: dict, title_substring: str) -> str: + needle = _norm(title_substring) + for d in doc_list.get("documents", []): + if needle in _norm(d.get("title")): + html = d.get("html") + if not html: + raise ValueError(f"document matching '{title_substring}' found but has no html: {d}") + return html + raise ValueError(f"no open document matching '{title_substring}' -- got {doc_list}") + + +# ---------- the plan ---------- + +RENUMBER_INSTRUCTION = ( + "This document has ten Section headings (each one looks like 'SECTION '), " + "already in the correct reading order from top to bottom, but their numbers are wrong -- some " + "are skipped, one number is used twice, and the numbers run past ten even though there are only " + "ten Sections. Go through the Section headings in top-to-bottom order and renumber them " + "sequentially: the first heading becomes 'SECTION 1', the second becomes 'SECTION 2', and so on " + "through 'SECTION 10' for the tenth and last one. Keep each heading's title text exactly as it " + "is now -- only change the number. Do not touch the Table of Contents or any body paragraph " + "text in this step." +) + +CROSSREF_INSTRUCTION = ( + "This document's Section numbers were just corrected so they now run 1 through 10 in order. " + "In the body text there are two sentences that reference another Section by number, written " + "like 'Section <number> of this Manual'. For each one, work out which Section it is actually " + "describing -- one refers to where confidentiality obligations for incident records are set " + "out, the other refers to where termination decisions are processed -- and update its number " + "to match that target Section's new, corrected number. Do not change anything else in the " + "document -- not the Table of Contents, not any heading, nothing else in the body text." +) + +TOC_INSTRUCTION = ( + "This document's Section numbers were just corrected so they now run 1 through 10 in order. " + "This document has a Table of Contents near the top, listing Sections by number and title. " + "Compare every Table of Contents entry against the Section it refers to: fix any entry whose " + "listed number no longer matches that Section's corrected number, fix any entry whose listed " + "title text no longer matches that Section's actual current title, and add a Table of Contents " + "entry for any Section that doesn't have one yet, in its correct position in the list. When you " + "are done there must be exactly ten Table of Contents entries, one per Section, in order. Do " + "not change anything else in the document -- not any heading, not any body paragraph text." +) + + +def print_dry_run() -> None: + print("=== DRY RUN -- no API calls will be made ===\n") + print(f"Document that would be uploaded to a single session: {CONTENT_DIR / 'manual.html'}") + print() + print("Known-broken structure as authored:") + print(" Section heading numbers in document order: 1, 2, 4, 5, 6, 6, 7, 9, 10, 11") + print(" (gap at 3, duplicate 6, gap at 8, runs to 11 instead of stopping at 10)") + print(" Body cross-ref in Section 'Incident Reporting': cites Section 9 for Confidentiality") + print(" -> Confidentiality is the 8th heading in order, so correct final number is 8") + print(" Body cross-ref in Section 'Disciplinary Actions': cites Section 10 for Termination") + print(" -> Termination is the 9th heading in order, so correct final number is 9") + print(" TOC entry for the 3rd Section: number correct (3), title stale ('Cargo Inspection") + print(" Requirements' instead of 'Vehicle Inspection Requirements')") + print(" TOC entry for 'Drug and Alcohol Policy': listed as Section 5 (duplicate of Incident") + print(" Reporting's entry), correct final number is 6") + print(" TOC: no entry at all for the 10th Section ('Miscellaneous Provisions')") + print() + print("Chat instruction 1 (renumber headings only, no document_id set):") + print(f" {RENUMBER_INSTRUCTION[:200]}...") + print() + print("Chat instruction 2 (fix the two body cross-refs against the corrected numbers, only):") + print(f" {CROSSREF_INSTRUCTION[:200]}...") + print() + print("Chat instruction 3 (fix the Table of Contents against the corrected numbers, only):") + print(f" {TOC_INSTRUCTION[:200]}...") + print() + print("Split into three narrow, single-purpose turns rather than two: an earlier run bundled") + print("the cross-ref fix and the TOC fix into one instruction, and the agent silently completed") + print("only the cross-ref half while reporting full success -- see PROGRESS.md.") + print() + print("Expected final state: headings numbered 1-10 sequentially in order; both cross-refs") + print("updated (8 and 9 respectively); TOC has 10 correct entries, no stale title, no stale") + print("number, no missing entry.") + print() + print("API calls this would make for real: 1 upload, 3 chat turns (+ approvals), 1 export.") + print("No cross_session_search used -- single document, single session throughout.") + print("Re-run without --dry-run once this plan looks right.") + + +# ---------- verification ---------- + +SECTION_TITLES_IN_ORDER = [ + "INTRODUCTION AND SCOPE", + "DEFINITIONS", + "VEHICLE INSPECTION REQUIREMENTS", + "HOURS OF SERVICE", + "INCIDENT REPORTING", + "DRUG AND ALCOHOL POLICY", + "DISCIPLINARY ACTIONS", + "CONFIDENTIALITY", + "TERMINATION", + "MISCELLANEOUS PROVISIONS", +] + + +def verify(html: str) -> dict: + results = {} + + # 1. Heading sequence: every "SECTION <n> — <TITLE>" heading, in document order. + headings = re.findall(r"SECTION\s+(\d+)\s*[—\-]\s*([A-Z ,&]+?)(?:</h\d>|\n)", html) + heading_numbers = [int(n) for n, _ in headings] + expected_numbers = list(range(1, 11)) + results["headings_sequential_1_to_10"] = { + "found": heading_numbers, + "expected": expected_numbers, + "correct": heading_numbers == expected_numbers, + } + + # 2. Titles still in the same order and intact (renumbering shouldn't have touched titles). + found_titles = [t.strip().rstrip(".") for _, t in headings] + results["titles_unchanged_and_in_order"] = { + "found": found_titles, + "correct": len(found_titles) == 10 + and all(SECTION_TITLES_IN_ORDER[i] in found_titles[i] for i in range(min(10, len(found_titles)))), + } + + # 3. Cross-ref: confidentiality reference should now cite Section 8. + m = re.search(r"confidentiality obligations[^.]*?Section\s+(\d+)", html, re.IGNORECASE | re.DOTALL) + results["crossref_confidentiality_points_to_8"] = { + "found": m.group(1) if m else None, + "correct": m is not None and m.group(1) == "8", + } + + # 4. Cross-ref: termination reference should now cite Section 9. + m = re.search(r"processed pursuant to Section\s+(\d+)", html, re.IGNORECASE) + results["crossref_termination_points_to_9"] = { + "found": m.group(1) if m else None, + "correct": m is not None and m.group(1) == "9", + } + + # 5. TOC: entry for Section 3 has the current title, not the stale one. + # Body headings are ALL CAPS ("SECTION 1 -- INTRODUCTION..."); TOC entries are + # title case ("Section 1 -- Introduction..."). The boundary must be case-sensitive + # or it matches the TOC's own first entry instead of the real heading below it. + toc_region_match = re.search(r"TABLE OF CONTENTS(.*?)(?=SECTION 1\s*[—\-])", html, re.DOTALL) + toc_region = toc_region_match.group(1) if toc_region_match else "" + results["toc_section3_title_fixed"] = { + "correct": "Vehicle Inspection Requirements" in toc_region and "Cargo Inspection" not in toc_region, + } + + # 6. TOC: Drug and Alcohol Policy entry now says Section 6, not a duplicated Section 5. + dup5_count = len(re.findall(r"Section\s+5\s*[—\-]", toc_region, re.IGNORECASE)) + results["toc_drug_alcohol_number_fixed"] = { + "correct": bool(re.search(r"Section\s+6\s*[—\-]\s*Drug and Alcohol Policy", toc_region, re.IGNORECASE)) + and dup5_count == 1, + } + + # 7. TOC: Miscellaneous Provisions entry now present. + results["toc_missing_entry_added"] = { + "correct": bool(re.search(r"Section\s+10\s*[—\-]\s*Miscellaneous Provisions", toc_region, re.IGNORECASE)), + } + + # 8. TOC: exactly 10 entries total (sanity check against partial/duplicate fixes). + toc_entry_count = len(re.findall(r"Section\s+\d+\s*[—\-]", toc_region, re.IGNORECASE)) + results["toc_has_exactly_10_entries"] = { + "found": toc_entry_count, + "correct": toc_entry_count == 10, + } + + all_correct = all(r["correct"] for r in results.values()) + return {"pass": all_correct, "details": results} + + +# ---------- main ---------- + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + if args.dry_run: + print_dry_run() + return + + api_key = os.environ.get("SUPERDOCS_API_KEY") + if not api_key: + print("SUPERDOCS_API_KEY not set", file=sys.stderr) + sys.exit(1) + client = Client(api_key) + + session_id = f"self-heal-{uuid.uuid4()}" + log(f"session: {session_id}") + client.upload_document(CONTENT_DIR / "manual.html", session_id, open_mode="replace") + log(" opened manual.html") + + log("renumbering Section headings") + job = client.start_chat(RENUMBER_INSTRUCTION, session_id, approval_mode="ask_every_time") + client.wait_for_job(session_id, job["job_id"], "renumber") + + log("fixing cross-references") + job = client.start_chat(CROSSREF_INSTRUCTION, session_id, approval_mode="ask_every_time") + client.wait_for_job(session_id, job["job_id"], "crossref") + + log("fixing Table of Contents") + job = client.start_chat(TOC_INSTRUCTION, session_id, approval_mode="ask_every_time") + client.wait_for_job(session_id, job["job_id"], "toc") + + docs = client.session_documents(session_id, include_html=True) + html = find_document_html(docs, "manual") + + result = verify(html) + log("verification:") + for name, detail in result["details"].items(): + log(f" {name}: {json.dumps(detail)}") + log(f"OVERALL: {'PASS' if result['pass'] else 'FAIL'}") + + export_path = client.export_html(html, "repaired_manual", fmt="docx") + log(f"exported -> {export_path}") + + (OUTPUT_DIR / "final_document.html").write_text(html, encoding="utf-8") + (OUTPUT_DIR / "verification_result.json").write_text(json.dumps(result, indent=2), encoding="utf-8") + + if not result["pass"]: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/use-cases/shivansh193/self-healing-structure-agent/content/manual.html b/use-cases/shivansh193/self-healing-structure-agent/content/manual.html new file mode 100644 index 00000000..64a16937 --- /dev/null +++ b/use-cases/shivansh193/self-healing-structure-agent/content/manual.html @@ -0,0 +1,43 @@ +<h1>NorthPeak Logistics — Driver Safety & Compliance Manual</h1> +<p><em>Synthetic document for demonstration purposes only. No real company, drivers, or incidents.</em></p> + +<h2>TABLE OF CONTENTS</h2> +<p id="toc-1">Section 1 — Introduction and Scope</p> +<p id="toc-2">Section 2 — Definitions</p> +<p id="toc-3">Section 3 — Cargo Inspection Requirements</p> +<p id="toc-4">Section 4 — Hours of Service</p> +<p id="toc-5">Section 5 — Incident Reporting</p> +<p id="toc-6">Section 5 — Drug and Alcohol Policy</p> +<p id="toc-7">Section 7 — Disciplinary Actions</p> +<p id="toc-8">Section 8 — Confidentiality</p> +<p id="toc-9">Section 9 — Termination</p> + +<h2>SECTION 1 — INTRODUCTION AND SCOPE</h2> +<p>1.1 This Manual applies to all NorthPeak Logistics drivers operating company or leased vehicles on company business.</p> + +<h2>SECTION 2 — DEFINITIONS</h2> +<p>2.1 "Covered Driver" means any individual operating a vehicle under a NorthPeak Logistics dispatch. "Incident" means any collision, cargo loss, or safety violation required to be reported under this Manual.</p> + +<h2>SECTION 4 — VEHICLE INSPECTION REQUIREMENTS</h2> +<p>4.1 Each Covered Driver shall complete a pre-trip inspection before every dispatch and a post-trip inspection at the end of every shift, using the standard NorthPeak inspection checklist.</p> + +<h2>SECTION 5 — HOURS OF SERVICE</h2> +<p>5.1 No Covered Driver shall operate a vehicle for more than eleven (11) hours following ten (10) consecutive hours off duty.</p> + +<h2>SECTION 6 — INCIDENT REPORTING</h2> +<p>6.1 Any Incident must be reported to dispatch within one (1) hour of occurrence. Any records collected during an incident investigation shall be handled in accordance with the confidentiality obligations set out in Section 9 of this Manual.</p> + +<h2>SECTION 6 — DRUG AND ALCOHOL POLICY</h2> +<p>6.1 Covered Drivers are subject to random drug and alcohol testing consistent with applicable federal regulations. A confirmed positive result is grounds for immediate suspension pending investigation.</p> + +<h2>SECTION 7 — DISCIPLINARY ACTIONS</h2> +<p>7.1 Violations of this Manual are subject to progressive discipline, up to and including termination. Termination decisions arising from repeated violations under this Section shall be processed pursuant to Section 10 of this Manual.</p> + +<h2>SECTION 9 — CONFIDENTIALITY</h2> +<p>9.1 All incident records, personnel files, and investigation materials described in this Manual are confidential and shall not be disclosed outside NorthPeak Logistics except as required by law.</p> + +<h2>SECTION 10 — TERMINATION</h2> +<p>10.1 Employment may be terminated by either party at any time, consistent with NorthPeak Logistics' standard employment policies.</p> + +<h2>SECTION 11 — MISCELLANEOUS PROVISIONS</h2> +<p>11.1 This Manual may be amended by NorthPeak Logistics at any time by written notice to Covered Drivers. Headings are for convenience only.</p> diff --git a/use-cases/shivansh193/self-healing-structure-agent/requirements.txt b/use-cases/shivansh193/self-healing-structure-agent/requirements.txt new file mode 100644 index 00000000..7507eb03 --- /dev/null +++ b/use-cases/shivansh193/self-healing-structure-agent/requirements.txt @@ -0,0 +1,2 @@ +httpx>=0.27 +python-dotenv>=1.0 From cac988d089cc48422fb096c4f38627594c2ca364 Mon Sep 17 00:00:00 2001 From: shivansh193 <shivanshkalra796@gmail.com> Date: Sat, 22 Aug 2026 03:34:20 +0530 Subject: [PATCH 3/3] Fix redline-workspace's cross_session_search bug; add retry wrapper for self-healing's non-determinism Redline workspace: dropped cross_session_search, loaded the risk playbook into the main session as a background document instead (same pattern as the Exhibits). Re-ran for real: now passes 6/6, both required flags land correctly and the reconciliation-order check holds. Also fixed a bug in this repo's own verify() (a fixed-width per-Article window that overran into the next Article's flag on this cleaner run). Original honest-failure record is left intact in PROGRESS.md/README as prior history, not replaced. Self-healing structure agent: wrapped the renumber step (the one that showed real run-to-run non-determinism) in a verify-then-retry loop -- fresh session, fresh document, up to 3 independent attempts, checked against ground truth each time. Converged on attempt 1 in this run. The Table of Contents step failed the same way as before, now confirmed on two separate real runs with different instruction phrasing -- reads as a reproducible platform behavior, left out of scope for this pass. --- .../PROGRESS.md | 54 ++++++++++ .../README.md | 101 +++++++++++------ .../build.py | 102 ++++++++++-------- .../self-healing-structure-agent/PROGRESS.md | 59 ++++++++++ .../self-healing-structure-agent/README.md | 86 ++++++++++----- .../self-healing-structure-agent/build.py | 90 +++++++++++++--- 6 files changed, 373 insertions(+), 119 deletions(-) diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/PROGRESS.md b/use-cases/shivansh193/owner-contractor-redline-workspace/PROGRESS.md index e1f935d0..b10ae55a 100644 --- a/use-cases/shivansh193/owner-contractor-redline-workspace/PROGRESS.md +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/PROGRESS.md @@ -124,3 +124,57 @@ Overall: **FAIL** (4 of 6 checks correct). The hardest check -- reconciliation actually landing before redlining ran -- passed cleanly. Both failures trace to the single stale-reopen bug above, not to two independent problems. + +## 2026-08-21 -- the known mitigation, run for real: a clean pass + +Everything above is left exactly as it was written. This is a second, +later result on top of it, not a replacement -- the platform bug it +found is still real and still worth reporting on its own. + +Implemented the fix the redline-step bug pointed at: dropped +`cross_session_search` entirely and uploaded `risk_playbook.html` into +the *main* session as a fifth background document, the same pattern +already used for the two Exhibits. `REDLINE_INSTRUCTION` now says "there +is another document open in this session called risk_playbook" instead +of "search your memory of previous sessions" -- otherwise unchanged. +This trades away one of the build's two original evidentiary properties +(proof the playbook was retrieved via genuine cross-session search, +not re-pasted) for a redline step that actually works; the other +property (the payment-terms reconciliation check, and the playbook's +arbitrary, non-guessable thresholds) is untouched. + +Ran for real. Reconciliation this time correctly picked up all three +Supplementary Conditions amendments (SC-1's payment term, SC-2's +submittal-schedule addition to Article 3, SC-3's site-access addition to +Article 10) rather than just SC-1 -- a more thorough read than either +prior run, not a regression; every added or changed sentence carries its +own "(As amended by SC-N)" note, and no Article's original text was lost. +The redline step then flagged both Article 6 (notice period, 5 business +days violates the 11-day threshold) and Article 7 (indemnification, +one-directional) correctly, left Article 8 (damages waiver) and Article 9 +(termination) correctly unflagged, and treated Article 5's reconciled +21-day payment term as compliant, exactly as the reconciliation-order +check is designed to catch. + +**A second, unrelated bug -- this time in this repo's own `verify()`, not +the platform.** The initial verification run reported `payment_terms_flagged: +correct: false`, i.e. Article 5 appeared to have been wrongly flagged. +Direct inspection of `output/final_document.html` showed no RISK FLAG +anywhere near Article 5 at all. The cause: `verify()`'s per-Article check +used a fixed 1200-character window after each Article's heading to look +for a flag. Article 5's own content (heading + two short paragraphs) is +short enough that the fixed window ran straight through Article 6's +heading and *into Article 6's own, genuinely-correct RISK FLAG paragraph*, +misattributing it to Article 5. Every earlier run happened not to trigger +this, because Article 5 was either itself flagged (wrongly, by the +platform) or the run failed before getting this far -- this is the first +run clean enough on the platform side to expose a bug that was sitting in +the verification script the whole time. Fixed by bounding each Article's +window to the position of the *next* "Article N" heading instead of a +fixed character count (confirmed via `grep -o "ARTICLE [0-9]*"` that this +document only ever uses the heading form, no inline mid-sentence +references, so the bound is unambiguous here). Re-ran `verify()` against +the same, unchanged export -- no new API call needed. + +**Final result: PASS, 6 of 6.** `output/reconciled_and_redlined_agreement.docx` +and `output/verification_result.json` reflect this run. diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/README.md b/use-cases/shivansh193/owner-contractor-redline-workspace/README.md index bab664da..4ff4535e 100644 --- a/use-cases/shivansh193/owner-contractor-redline-workspace/README.md +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/README.md @@ -4,11 +4,11 @@ Built by Shivansh Kalra for the SuperDocs task. Reconciles a base Owner-Contractor Agreement with its Supplementary Conditions into one effective document, then redlines that effective -document against a risk playbook retrieved from a separate session -- -indemnity mutuality, damages-waiver mutuality, notice periods, payment -terms, and termination-for-convenience notice, each against a deliberately -non-"standard" numeric threshold (11 business days, 23 days, 17 days, not -the round 10/14/30 a model would guess from generic contract knowledge). +document against a risk playbook -- indemnity mutuality, damages-waiver +mutuality, notice periods, payment terms, and termination-for-convenience +notice, each against a deliberately non-"standard" numeric threshold (11 +business days, 23 days, 17 days, not the round 10/14/30 a model would +guess from generic contract knowledge). All content is synthetic: a fictional owner (Riverside Medical Partners LLC), a fictional contractor (Meridian Builders LLC), and a fictional @@ -16,20 +16,26 @@ internal risk playbook. Two things were deliberately engineered to be independently verifiable, not just plausible-looking -- see [Verified result](#verified-result) for -which one actually held up: - -1. The risk playbook is established in a separate prior session and - referenced only via `cross_session_search: true` -- never re-pasted - into the redline instruction. A correct flag against one of its - specific, arbitrary thresholds would be evidence the search genuinely - retrieved the playbook, not that the model pattern-matched typical - contract norms. +which one actually held up, and for what changed between the two real +runs documented there: + +1. **Originally**: the risk playbook was established in a separate prior + session and referenced only via `cross_session_search: true` -- never + re-pasted into the redline instruction, so a correct flag against one + of its specific, arbitrary thresholds would be evidence the search + genuinely retrieved the playbook, not that the model pattern-matched + typical contract norms. This surfaced a real SuperDocs bug (see below) + and was replaced with the playbook loaded into the main session as a + background document instead, the same pattern used for the two + Exhibits -- trading that specific evidentiary property for a redline + step that actually works. 2. The base agreement's payment term (45 days) genuinely violates the playbook's threshold (>23 days) on its own -- but the Supplementary Conditions amend it to 21 days, which is compliant. If the final document treats payment terms as compliant, that's evidence real reconciliation happened *before* redlining, not that the base document - was redlined in isolation while ignoring the amendment. + was redlined in isolation while ignoring the amendment. This property + is untouched by the fix above and still holds. ## What it does @@ -41,11 +47,11 @@ which one actually held up: Conditions, find its numbered amendments, and edit the corresponding Articles in the base agreement in place -- every other Article must stay present and unchanged. -4. **Redline step**: instructs SuperDocs to retrieve the risk playbook via - `cross_session_search`, check the now-reconciled document's actual - terms against each of the playbook's five thresholds in turn, and - insert a red `RISK FLAG:` paragraph after any Article that violates - its threshold. +4. **Redline step**: instructs SuperDocs to read the risk playbook (open + in the same session as a background document), check the + now-reconciled document's actual terms against each of the playbook's + five thresholds in turn, and insert a red `RISK FLAG:` paragraph after + any Article that violates its threshold. 5. Exports the result as `.docx` and verifies it programmatically against six checks (five per-Article flag/no-flag expectations plus the reconciliation check itself) by inspecting the real returned HTML, not @@ -65,9 +71,8 @@ python build.py # runs it for real: ~5 uploads, 2 chat turns, 1 export ## SuperDocs features used - **Multi-document sessions** (`open_mode: "replace"` / `"background"`) -- - four related contract documents open together, one focused -- **Cross-session memory + `cross_session_search`** -- the risk playbook - lives in a separate session and is retrieved by search, not re-pasted + five related documents open together (base agreement, Supplementary + Conditions, two Exhibits, risk playbook), one focused - **Chat / async edit** (`POST /v1/chat/async`) with `approval_mode: "ask_every_time"` across two sequential instructions on the same focused document @@ -124,20 +129,50 @@ bug above, not to two independent problems -- and the check that was actually the point of the exercise (real reconciliation before redlining) passed cleanly. +### Later result: the mitigation, run for real + +Dropped `cross_session_search` and loaded the playbook into the main +session as a background document instead (the same pattern already used +for the Exhibits). Ran again for real: + +| Check | Expected | Actual | Result | +|---|---|---|---| +| Article 7 (indemnification) flagged | yes | yes | PASS | +| Article 6 (notice period) flagged | yes | yes | PASS | +| Article 8 (damages waiver) flagged | no | no | PASS | +| Article 5 (payment terms) flagged | no | no | PASS | +| Article 9 (termination) flagged | no | no | PASS | +| Reconciliation applied (21 days, not 45) | yes | yes | PASS | + +Overall: **PASS, 6 of 6.** Reconciliation this run also picked up two +further Supplementary Conditions amendments (a submittal-schedule +addition and a site-access clause) that earlier runs had judged as new +obligations rather than amendments -- a more thorough read, not a +regression; every Article is still present and every change carries its +own "(As amended by SC-N)" note. + +One verification-script bug turned up along the way, in this repo's own +`verify()`, not the platform: it checked each Article for a nearby +`RISK FLAG` using a fixed 1200-character window, which was short enough +that Article 5's window ran into Article 6's own (correct) flag and +misattributed it, briefly reporting a false failure. Fixed by bounding +each Article's window to the next Article heading instead of a fixed +length. Full trace in [`PROGRESS.md`](PROGRESS.md). + +This result doesn't replace the one above -- the bug that first run found +is real and still worth reporting on its own; this is what fixing it +looks like once you actually apply the known mitigation. + ## Honest limitations -- The redline step does not reliably produce risk flags today because of - the platform bug described above. Re-running without - `cross_session_search` (uploading the playbook into the main session as - a background document instead, the same pattern used for the Exhibits) - would very likely produce a clean pass, but that would remove the part - of the design meant to prove genuine cross-session retrieval rather - than pattern-matched generic contract knowledge -- left as-is rather - than quietly working around the bug it was built to demonstrate. +- The original `cross_session_search` design (proof the playbook was + retrieved via genuine cross-session search, not re-pasted) was traded + away to get a working redline step -- see the two results above for + why. The reconciliation-order evidentiary property is untouched. - `output/` is gitignored; run `python build.py` to regenerate - `reconciled_and_redlined_agreement.docx` (reflects the correctly - reconciled, not-yet-redlined state), `final_document.html`, and - `verification_result.json`. + `reconciled_and_redlined_agreement.docx`, `final_document.html`, and + `verification_result.json` -- reflects the mitigated version's PASS + result as of the current `build.py`. ## Files diff --git a/use-cases/shivansh193/owner-contractor-redline-workspace/build.py b/use-cases/shivansh193/owner-contractor-redline-workspace/build.py index c246d16f..792c0dfd 100644 --- a/use-cases/shivansh193/owner-contractor-redline-workspace/build.py +++ b/use-cases/shivansh193/owner-contractor-redline-workspace/build.py @@ -5,24 +5,29 @@ effective document against a risk playbook -- indemnity, damages waiver, notice periods, payment terms, termination for convenience. -Two things are deliberately engineered to be independently verifiable, -not just plausible-looking: - -1. The risk playbook is established in a *separate* prior session and - referenced only via `cross_session_search: true` in the redline step -- - never re-pasted into the redline instruction. Its thresholds are - arbitrary, non-"standard" numbers (11 business days, 23 days, 17 days -- - not the round 10/14/30 a model would guess from generic contract - knowledge). A correct flag against one of these specific numbers is - evidence the search genuinely retrieved the playbook, not that the - model pattern-matched typical contract norms. - -2. The base agreement's payment term (45 days) genuinely violates the - playbook's threshold (>23 days) on its own -- but the Supplementary - Conditions amend it to 21 days, which is compliant. If the final - redline treats payment terms as compliant, that's evidence real - reconciliation happened *before* redlining, not that the base - document was redlined in isolation while ignoring the amendment. +The playbook was originally established in a *separate* prior session and +referenced only via `cross_session_search: true` in the redline step, to +prove the search genuinely retrieved it rather than the model pattern- +matching generic contract norms. That version is preserved as history in +PROGRESS.md: it surfaced a real SuperDocs bug (`cross_session_search` can +silently re-open a stale, pre-reconciliation snapshot of a document +already open in the same session, so an "approved" edit never actually +lands). This version implements the known mitigation instead -- the +playbook is uploaded into the *main* session as a background document, +the same pattern already used for the two Exhibits, so nothing needs +cross-session retrieval at all. This trades away one evidentiary property +(proof the retrieval was genuinely cross-session) for a working redline +step. The other evidentiary property below still holds either way. + +The base agreement's payment term (45 days) genuinely violates the +playbook's threshold (>23 days) on its own -- but the Supplementary +Conditions amend it to 21 days, which is compliant. If the final redline +treats payment terms as compliant, that's evidence real reconciliation +happened *before* redlining, not that the base document was redlined in +isolation while ignoring the amendment. Its thresholds are also arbitrary, +non-"standard" numbers (11 business days, 23 days, 17 days -- not the +round 10/14/30 a model would guess from generic contract knowledge), so a +correct flag still isn't just pattern-matching typical contract norms. Run `python build.py --dry-run` first: prints the full plan (uploads, exact chat instructions, what verification will check) with zero API @@ -150,36 +155,38 @@ def export_html(self, html: str, filename: str, fmt: str = "docx") -> Path: ) REDLINE_INSTRUCTION = ( - "Search your memory of previous sessions for a document called the Meridian Builders Owner Contract " - "Risk Playbook, and retrieve its content -- do not ask me what it says. It lists five numbered risk " - "categories, each with one specific numeric threshold. Work through the five categories one at a time, " - "in order. For each one: find the Article in this document that covers that category, compare this " - "document's actual current term for it against that category's threshold, and only if it violates the " - "threshold, insert one new paragraph directly after that Article's text: start it with the literal text " - "'RISK FLAG:', explain which threshold is violated and by how much, and make the whole paragraph red " - "using style=\"color:#b00\". If a category's term already meets the threshold, insert nothing for it and " - "move to the next category. Some of the five will need a flag and some won't." + "There is another document open in this session called risk_playbook. Read it specifically. It lists " + "five numbered risk categories, each with one specific numeric threshold. Work through the five " + "categories one at a time, in order. For each one: find the Article in THIS document (not " + "risk_playbook) that covers that category, compare this document's actual current term for it against " + "that category's threshold, and only if it violates the threshold, insert one new paragraph directly " + "after that Article's text: start it with the literal text 'RISK FLAG:', explain which threshold is " + "violated and by how much, and make the whole paragraph red using style=\"color:#b00\". If a category's " + "term already meets the threshold, insert nothing for it and move to the next category. Some of the " + "five will need a flag and some won't. Do not edit risk_playbook itself." ) def print_dry_run() -> None: print("=== DRY RUN -- no API calls will be made ===\n") - print("Documents that would be uploaded to a setup session (session A):") - print(f" - {CONTENT_DIR / 'risk_playbook.html'}") + print("Mitigated version: no setup session, no cross_session_search. The risk playbook goes into the") + print("main session as a fifth background document, same pattern as the two Exhibits.") print() - print("Documents that would be uploaded to the main session (session B), in order:") + print("Documents that would be uploaded to the main session, in order:") for name, mode in [ ("base_agreement.html", "replace (becomes focused)"), ("supplementary_conditions.html", "background"), ("exhibit_a_scope.html", "background"), ("exhibit_b_insurance.html", "background"), + ("risk_playbook.html", "background"), ]: print(f" - {CONTENT_DIR / name} [{mode}]") print() print("Chat instruction 1 (reconcile, targets the focused base_agreement doc, no document_id set):") print(f" {RECONCILE_INSTRUCTION[:200]}...") print() - print("Chat instruction 2 (redline, same focused doc, cross_session_search=true):") + print("Chat instruction 2 (redline, same focused doc, playbook read from the same session, no") + print("cross_session_search):") print(f" {REDLINE_INSTRUCTION[:200]}...") print() print("Expected verification result (against the source documents as authored):") @@ -191,7 +198,7 @@ def print_dry_run() -> None: print(" compliant with the 23-day threshold -- this is the reconciliation check") print(" NO FLAG expected: Article 9 (termination) -- 30 days >= 17-day threshold") print() - print("API calls this would make for real: 5 uploads, 2 chat turns (+ approvals), 1-2 exports.") + print("API calls this would make for real: 5 uploads, 2 chat turns (+ approvals), 1 export.") print("Re-run without --dry-run once this plan looks right.") @@ -223,11 +230,22 @@ def verify(html: str) -> dict: "payment_terms_flagged": ("Article 5", False), "termination_flagged": ("Article 9", False), } + # Each Article's window is bounded by the *next* "Article N" heading (or end of + # document), not a fixed character count -- a fixed window can overrun into the + # next Article's own RISK FLAG and misattribute it, which a short, unflagged + # Article immediately followed by a flagged one will actually trigger. + all_article_starts = sorted(m.start() for m in re.finditer(r"Article\s+\d+", html, re.IGNORECASE)) + results = {} for check_name, (article, should_be_flagged) in checks.items(): m = re.search(re.escape(article), html, re.IGNORECASE) idx = m.start() if m else -1 - window = html[idx : idx + 1200] if idx != -1 else "" + if idx == -1: + window = "" + else: + next_starts = [s for s in all_article_starts if s > idx] + end = next_starts[0] if next_starts else len(html) + window = html[idx:end] has_flag = "RISK FLAG" in window results[check_name] = { "article_found": idx != -1, @@ -266,18 +284,7 @@ def main() -> None: sys.exit(1) client = Client(api_key) - # --- setup session: establish the risk playbook for cross-session search --- - setup_session = f"playbook-setup-{uuid.uuid4()}" - log(f"setup session: {setup_session}") - client.upload_document(CONTENT_DIR / "risk_playbook.html", setup_session, open_mode="replace") - job = client.start_chat( - "Read this risk playbook and confirm you understand it. Just summarize the five categories in one line each.", - setup_session, - approval_mode="auto-apply", - ) - client.wait_for_job(setup_session, job["job_id"], "playbook setup") - - # --- main session: open all four contract documents together --- + # --- main session: open all five documents together, playbook included --- main_session = f"redline-{uuid.uuid4()}" log(f"main session: {main_session}") for name, mode in [ @@ -285,6 +292,7 @@ def main() -> None: ("supplementary_conditions.html", "background"), ("exhibit_a_scope.html", "background"), ("exhibit_b_insurance.html", "background"), + ("risk_playbook.html", "background"), ]: client.upload_document(CONTENT_DIR / name, main_session, open_mode=mode) log(f" opened {name} ({mode})") @@ -293,8 +301,8 @@ def main() -> None: job = client.start_chat(RECONCILE_INSTRUCTION, main_session, approval_mode="ask_every_time") client.wait_for_job(main_session, job["job_id"], "reconciliation") - log("redlining against the risk playbook (cross-session search, playbook not re-pasted)") - job = client.start_chat(REDLINE_INSTRUCTION, main_session, approval_mode="ask_every_time", cross_session_search=True) + log("redlining against the risk playbook (same-session document, no cross_session_search)") + job = client.start_chat(REDLINE_INSTRUCTION, main_session, approval_mode="ask_every_time") client.wait_for_job(main_session, job["job_id"], "redline") docs = client.session_documents(main_session, include_html=True) diff --git a/use-cases/shivansh193/self-healing-structure-agent/PROGRESS.md b/use-cases/shivansh193/self-healing-structure-agent/PROGRESS.md index 9dc18b0c..df9f2df6 100644 --- a/use-cases/shivansh193/self-healing-structure-agent/PROGRESS.md +++ b/use-cases/shivansh193/self-healing-structure-agent/PROGRESS.md @@ -149,3 +149,62 @@ where the standing instruction says to stop rather than continue. `output/verification_result.json` and `output/final_document.html` are left as Run 2 produced them -- an accurate record of the failure, not patched over. + +## 2026-08-21 -- a verify-then-retry wrapper around the one non-deterministic step + +Everything above stays as it happened. This is a third run, with a +different mitigation targeted specifically at the non-determinism, not a +replacement for the earlier two. + +The renumber step is the one that showed non-determinism. Wrapped just +that step in a loop: each attempt uploads a *fresh* copy of the source +document into a *fresh* session (not a follow-up turn on a half-broken +document -- a genuinely independent attempt, matching how the original +two runs were also independent), sends the same `RENUMBER_INSTRUCTION`, +and checks the result against ground truth with a new `verify_headings()` +function (numbers 1-10 in order, titles unchanged) before deciding +whether to keep it or discard and retry, up to `MAX_RENUMBER_ATTEMPTS = 3`. +The cross-reference and Table of Contents turns are unchanged and stay +single-shot -- the non-determinism only ever showed up in renumbering. + +`verify_headings()` was checked against known ground truth before spending +any API calls on it, same discipline as the original `verify()`: run +against a hand-repaired copy (all correct) and the original broken source +(all incorrect), both came back as expected. + +**Result: converged immediately, attempt 1 of 3.** Real run against the +live API: `attempt 1 numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] correct: True`. +No retry was needed this time -- which is itself real information, not a +non-event: it means the instruction *can* succeed reliably when nothing +else changes, consistent with Run 1's original clean pass. The retry +wrapper's actual value would show up on a run where the first attempt +fails and a later one succeeds, or where all three fail and that gets +reported honestly instead of silently retried into oblivion; this run +didn't need to exercise that path, but the mechanism is now in place and +its per-attempt log (`output/renumber_attempts.json`) makes every +attempt's real outcome inspectable regardless of which path a given run +takes. + +**The Table of Contents step failed again, the same way as before, now +confirmed twice.** Cross-refs and headings this run: clean. The TOC step, +asked to fix the same three planted defects, made no edit at all to any +of the nine existing `<p id="toc-N">` lines -- same stale title, same +duplicated/wrong number, same missing tenth entry as the unmodified +source. Instead it inserted an empty `<div class="table-of-contents" +data-toc=""></div>` widget in the middle of the list (this time between +the first and second entries; last time, replacing the whole block). This +happened on the only two real runs that got far enough to reach this +step, with different instruction wording each time (bundled, then +single-shot) -- reasonable evidence this is a specific, reproducible +platform behavior (asking to edit a literal, hand-authored Table of +Contents triggers a native "live TOC" substitution instead) rather than a +fluke. Left as-is per the current priority order, which scoped the retry +wrapper to renumbering only -- fixing the TOC step is separate, +not-yet-scoped work. + +**Final result: FAIL, 4 of 8** (`headings_sequential_1_to_10`, +`titles_unchanged_and_in_order`, both crossref checks: PASS; all four TOC +checks: FAIL). `output/repaired_manual.docx`, +`output/verification_result.json` (now also carries `renumber_attempts` +and `renumber_converged`), and `output/renumber_attempts.json` all +reflect this run. diff --git a/use-cases/shivansh193/self-healing-structure-agent/README.md b/use-cases/shivansh193/self-healing-structure-agent/README.md index a79a5e0c..09a0ce23 100644 --- a/use-cases/shivansh193/self-healing-structure-agent/README.md +++ b/use-cases/shivansh193/self-healing-structure-agent/README.md @@ -28,7 +28,10 @@ different, real problem on its own. 1. Uploads the broken manual to a session. 2. **Renumber step**: asks SuperDocs to renumber the ten Section headings sequentially 1-10, in the order they already appear, without touching - titles, body text, or the Table of Contents. + titles, body text, or the Table of Contents. Wrapped in a + verify-then-retry loop (up to 3 attempts, each from a fresh session and + a fresh copy of the source) since this exact instruction showed real + run-to-run non-determinism -- see [Verified result](#verified-result-fail-and-a-genuinely-interesting-one). 3. **Cross-reference step**: asks SuperDocs to find two body sentences that reference another Section by number and correct each number to match its target Section's new, corrected number. @@ -108,39 +111,68 @@ attempt would be spending operations on a re-roll with no new diagnostic basis, not a verified fix. Full turn-by-turn diagnosis, including the exact job diffs, is in [`PROGRESS.md`](PROGRESS.md). -| Check | Run 1 | Run 2 | -|---|---|---| -| Headings renumbered 1-10 sequentially | PASS | FAIL | -| Titles unchanged and in order | PASS | FAIL | -| Confidentiality cross-ref -> Section 8 | PASS | FAIL | -| Termination cross-ref -> Section 9 | PASS | FAIL | -| TOC: stale title fixed | FAIL | FAIL | -| TOC: stale number fixed | FAIL | FAIL | -| TOC: missing entry added | FAIL | FAIL | -| TOC: exactly 10 entries | FAIL | FAIL | -| **Overall** | **FAIL (6/8)** | **FAIL (0/8)** | +| Check | Run 1 | Run 2 | Run 3 (retry wrapper) | +|---|---|---|---| +| Headings renumbered 1-10 sequentially | PASS | FAIL | PASS | +| Titles unchanged and in order | PASS | FAIL | PASS | +| Confidentiality cross-ref -> Section 8 | PASS | FAIL | PASS | +| Termination cross-ref -> Section 9 | PASS | FAIL | PASS | +| TOC: stale title fixed | FAIL | FAIL | FAIL | +| TOC: stale number fixed | FAIL | FAIL | FAIL | +| TOC: missing entry added | FAIL | FAIL | FAIL | +| TOC: exactly 10 entries | FAIL | FAIL | FAIL | +| **Overall** | **FAIL (6/8)** | **FAIL (0/8)** | **FAIL (4/8)** | + +### Run 3: a verify-then-retry wrapper around the one non-deterministic step + +Run 1 and Run 2 used the byte-identical renumber instruction and got very +different results -- real non-determinism, not a wording problem. Run 3 +wraps just that step (the only one that showed non-determinism) in a +verify-then-retry loop: each attempt starts from a fresh session and a +fresh copy of the source document, and the result gets checked against +ground truth before deciding whether to keep it or discard and try again, +up to 3 attempts. Cross-refs and TOC are unchanged, single-shot turns. + +It converged on the first attempt: `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]`, +correct, no retry needed. That's a real result, not a non-event -- Run 1 +already showed the instruction can succeed cleanly; Run 3 confirms it +again under the same fresh-session conditions. Every attempt's real +outcome is logged to `output/renumber_attempts.json` regardless of +whether a given run needs 1 attempt or all 3. + +The Table of Contents step failed again, the same way as Run 2: no edits +to any of the nine existing TOC lines, and an empty auto-generated +`<div class="table-of-contents">` widget inserted instead. Two real runs +that reached this step, two different instruction phrasings, the same +outcome both times -- this now reads as a specific, reproducible platform +behavior around editing a literal, hand-authored Table of Contents, +not a fluke. Out of scope for this pass (the retry wrapper was scoped to +renumbering only); fixing the TOC step is separate, not-yet-scoped work. ## Honest limitations -- Structural repair (renumbering, cross-reference correction) is not - reliable run-to-run against the live API today, based on two identical - attempts producing very different outcomes. This isn't a claim about - SuperDocs generally -- it's what two real runs of this specific, - narrowly-scoped task actually showed. -- The Table of Contents step in particular showed a second, distinct - behavior worth flagging on its own: given a literal, hand-authored TOC - to edit, it can replace the whole thing with an empty auto-generated - widget rather than editing the existing text -- something this build's - HTML-based verification has no way to see through. +- Single-shot structural repair (renumbering specifically) was not + reliable run-to-run against the live API, based on two identical + attempts producing very different outcomes. The retry wrapper (Run 3) + mitigates this for renumbering by discarding a failed attempt and + trying again fresh, up to 3 times -- but that's a mitigation, not proof + the underlying non-determinism is gone; a run where all 3 attempts fail + is still possible and would be reported honestly if it happened. +- The Table of Contents step still fails, the same way, on both real runs + that reached it: given a literal, hand-authored TOC to edit, it replaces + the whole thing with an empty auto-generated widget rather than editing + the existing text -- something this build's HTML-based verification has + no way to see through, and not yet mitigated (out of scope for the + retry-wrapper pass). - `output/` is gitignored; run `python build.py` to regenerate - `repaired_manual.docx`, `final_document.html`, and - `verification_result.json`. Regenerating may reproduce either the Run 1 - or Run 2 outcome, per the finding above. + `repaired_manual.docx`, `final_document.html`, + `verification_result.json` (now also carries the renumber retry log), + and `renumber_attempts.json`. ## Files -- `build.py` -- upload -> renumber -> fix cross-refs -> fix TOC -> - verify -> export flow, plus `--dry-run` +- `build.py` -- upload -> renumber (verify-then-retry, up to 3 attempts) + -> fix cross-refs -> fix TOC -> verify -> export flow, plus `--dry-run` - `content/manual.html` -- the driver safety manual, authored with the three planted structural defects described above - `PROGRESS.md` -- full diagnostic trace of both runs, including the diff --git a/use-cases/shivansh193/self-healing-structure-agent/build.py b/use-cases/shivansh193/self-healing-structure-agent/build.py index b96898c5..e83d2bad 100644 --- a/use-cases/shivansh193/self-healing-structure-agent/build.py +++ b/use-cases/shivansh193/self-healing-structure-agent/build.py @@ -22,6 +22,19 @@ makes verification exact rather than approximate: the correct final state is fully known in advance, not just "plausible." +Two real runs of the byte-identical RENUMBER_INSTRUCTION produced very +different outcomes (see PROGRESS.md): one clean pass, one run with a false +"updated all 10 sections" claim covering near-zero real progress. That's +real run-to-run non-determinism, not a wording problem -- so this version +wraps the renumber turn specifically in a verify-then-retry loop: run it +against a fresh session and a fresh copy of the source document, check the +actual resulting headings against ground truth, and if it doesn't match, +throw the attempt away and try again from scratch, up to +MAX_RENUMBER_ATTEMPTS times. Every attempt's real outcome is logged, +whether or not retrying converges to a pass -- both are real information. +The cross-reference and Table of Contents turns are left single-shot; the +non-determinism only showed up in renumbering. + Run `python build.py --dry-run` first: prints the full plan with zero API calls. Only run for real (`python build.py`) after reading that output. """ @@ -46,6 +59,8 @@ OUTPUT_DIR = HERE / "output" OUTPUT_DIR.mkdir(exist_ok=True) +MAX_RENUMBER_ATTEMPTS = 3 + def log(msg: str) -> None: print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True) @@ -200,13 +215,19 @@ def print_dry_run() -> None: print(" Reporting's entry), correct final number is 6") print(" TOC: no entry at all for the 10th Section ('Miscellaneous Provisions')") print() - print("Chat instruction 1 (renumber headings only, no document_id set):") + print(f"Chat instruction 1 (renumber headings only, no document_id set), retried up to") + print(f"{MAX_RENUMBER_ATTEMPTS} times against a fresh session + fresh document each attempt,") + print("verified against ground truth after every attempt, since this exact instruction produced") + print("two very different real outcomes on two identical prior runs (see PROGRESS.md):") print(f" {RENUMBER_INSTRUCTION[:200]}...") print() - print("Chat instruction 2 (fix the two body cross-refs against the corrected numbers, only):") + print("Chat instruction 2 (fix the two body cross-refs against the corrected numbers, only),") + print("single-shot, run once against whichever session's renumber attempt succeeded (or the") + print("last attempt, if none did):") print(f" {CROSSREF_INSTRUCTION[:200]}...") print() - print("Chat instruction 3 (fix the Table of Contents against the corrected numbers, only):") + print("Chat instruction 3 (fix the Table of Contents against the corrected numbers, only),") + print("single-shot:") print(f" {TOC_INSTRUCTION[:200]}...") print() print("Split into three narrow, single-purpose turns rather than two: an earlier run bundled") @@ -217,8 +238,9 @@ def print_dry_run() -> None: print("updated (8 and 9 respectively); TOC has 10 correct entries, no stale title, no stale") print("number, no missing entry.") print() - print("API calls this would make for real: 1 upload, 3 chat turns (+ approvals), 1 export.") - print("No cross_session_search used -- single document, single session throughout.") + print(f"API calls this would make for real: 1-{MAX_RENUMBER_ATTEMPTS} uploads + renumber turns") + print("(1 per attempt, until one verifies correct or the cap is hit), plus 2 more chat turns") + print("(crossref, TOC) and 1 export. No cross_session_search used anywhere.") print("Re-run without --dry-run once this plan looks right.") @@ -238,6 +260,26 @@ def print_dry_run() -> None: ] +def verify_headings(html: str) -> dict: + """Narrow check used by the renumber retry loop: just the heading numbers + and titles, not cross-refs or TOC (those haven't run yet at this point).""" + headings = re.findall(r"SECTION\s+(\d+)\s*[—\-]\s*([A-Z ,&]+?)(?:</h\d>|\n)", html) + heading_numbers = [int(n) for n, _ in headings] + found_titles = [t.strip().rstrip(".") for _, t in headings] + expected_numbers = list(range(1, 11)) + numbers_correct = heading_numbers == expected_numbers + titles_correct = len(found_titles) == 10 and all( + SECTION_TITLES_IN_ORDER[i] in found_titles[i] for i in range(min(10, len(found_titles))) + ) + return { + "found_numbers": heading_numbers, + "found_titles": found_titles, + "numbers_correct": numbers_correct, + "titles_correct": titles_correct, + "correct": numbers_correct and titles_correct, + } + + def verify(html: str) -> dict: results = {} @@ -324,14 +366,36 @@ def main() -> None: sys.exit(1) client = Client(api_key) - session_id = f"self-heal-{uuid.uuid4()}" - log(f"session: {session_id}") - client.upload_document(CONTENT_DIR / "manual.html", session_id, open_mode="replace") - log(" opened manual.html") + # --- renumber: verify-then-retry, fresh session + fresh document each attempt --- + attempts_log = [] + session_id = None + for attempt in range(1, MAX_RENUMBER_ATTEMPTS + 1): + attempt_session = f"self-heal-{uuid.uuid4()}" + log(f"renumber attempt {attempt}/{MAX_RENUMBER_ATTEMPTS}, session: {attempt_session}") + client.upload_document(CONTENT_DIR / "manual.html", attempt_session, open_mode="replace") + job = client.start_chat(RENUMBER_INSTRUCTION, attempt_session, approval_mode="ask_every_time") + client.wait_for_job(attempt_session, job["job_id"], f"renumber (attempt {attempt})") + + docs = client.session_documents(attempt_session, include_html=True) + html = find_document_html(docs, "manual") + check = verify_headings(html) + attempts_log.append({"attempt": attempt, "session_id": attempt_session, **check}) + log(f" attempt {attempt} numbers: {check['found_numbers']} correct: {check['correct']}") + + if check["correct"]: + session_id = attempt_session + log(f" attempt {attempt} verified correct, proceeding with this session") + break + elif attempt < MAX_RENUMBER_ATTEMPTS: + log(f" attempt {attempt} failed verification, discarding and retrying fresh") + else: + log(f" attempt {attempt} failed verification, cap reached -- proceeding anyway with") + log(" this session's (incorrect) result, to see how the rest of the pipeline handles it") + session_id = attempt_session - log("renumbering Section headings") - job = client.start_chat(RENUMBER_INSTRUCTION, session_id, approval_mode="ask_every_time") - client.wait_for_job(session_id, job["job_id"], "renumber") + (OUTPUT_DIR / "renumber_attempts.json").write_text(json.dumps(attempts_log, indent=2), encoding="utf-8") + converged = any(a["correct"] for a in attempts_log) + log(f"renumber retry summary: {len(attempts_log)} attempt(s), converged to a correct result: {converged}") log("fixing cross-references") job = client.start_chat(CROSSREF_INSTRUCTION, session_id, approval_mode="ask_every_time") @@ -345,6 +409,8 @@ def main() -> None: html = find_document_html(docs, "manual") result = verify(html) + result["renumber_attempts"] = attempts_log + result["renumber_converged"] = converged log("verification:") for name, detail in result["details"].items(): log(f" {name}: {json.dumps(detail)}")