diff --git a/extensions/tanayjha/word-comment-agent/.gitignore b/extensions/tanayjha/word-comment-agent/.gitignore
new file mode 100644
index 00000000..75a287d8
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/.gitignore
@@ -0,0 +1,27 @@
+# Python virtual environments & bytecode
+.venv/
+venv/
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+
+# Package builds & caches
+*.egg-info/
+dist/
+build/
+.pytest_cache/
+.coverage
+
+# Secrets & environment
+.env
+*.key
+*.pem
+
+# Test / temporary document outputs
+*.tmp.docx
+output_*.docx
+diagnostic_*.docx
+*diagnostic*.docx
+*processed*.docx
+live_*.docx
diff --git a/extensions/tanayjha/word-comment-agent/README.md b/extensions/tanayjha/word-comment-agent/README.md
new file mode 100644
index 00000000..60b11335
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/README.md
@@ -0,0 +1,237 @@
+# Word Comment-Driven Agent for SuperDocs
+
+> **Credit Line**: *built this for the SuperDocs engineering task*
+
+An MCP extension that reads unresolved Word (`.docx`) comments, maps each comment to its exact anchored text range in OpenXML, sends scoped edit instructions to the SuperDocs REST API, presents proposed revisions for Human-in-the-Loop review (approve/reject/override), and surgically writes approved edits back into the original `.docx` file while resolving comment threads.
+
+---
+
+## ๐ฏ Why This Extension Exists
+
+Editorial and legal workflows live in Word comment threads. When reviewers leave comments like *"Make this more concise"* or *"Clarify that these are contract drivers"*, existing AI tools typically rewrite the entire document or convert to HTML/Markdown and back โ destroying surrounding styles, tracked changes, footnotes, and un-commented sections.
+
+The **Word Comment-Driven Agent** solves this by establishing a surgical, human-gated workflow:
+* **Exact Anchor Scoping**: Comment instructions are isolated to their specific XML text range (`w:commentRangeStart` to `w:commentRangeEnd`), even when anchor spans cross multiple formatting runs.
+* **Option B Architectural Ownership**: SuperDocs AI provides document intelligence; our local `docx_writer.py` performs in-place OpenXML range replacements directly on the source `.docx`. Un-commented document text, headers, footers, and styles remain **100% byte-for-byte untouched**.
+* **Honors "No Change Needed"**: Evaluates reviewer comments like *"No change needed โ confirmed accurate"* or *"LGTM"* and skips AI calls automatically without modifying text.
+* **Strict Human-in-the-Loop Gate**: Edits are never applied without explicit approval. Rejected comments remain unmodified and unresolved.
+* **Thread Resolution**: Approved comments update their anchor text and mark the thread resolved (`w:done="1"` in `word/comments.xml`).
+
+---
+
+## ๐๏ธ System Architecture
+
+```text
+ Original .docx (with unresolved comments)
+ โ
+ โผ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ inspect_word_comments โ (docx_parser.py)
+ โ โข Reads word/comments.xml โ
+ โ โข Maps comment ID โ anchor range โ
+ โ โข Detects "no change needed" โ
+ โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
+ โ
+ โผ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ propose_comment_edits โ (superdocs_client.py)
+ โ โข Uploads doc / isolated session โ
+ โ โข Scoped async edit instructions โ
+ โ โข Progressive polling backoff โ
+ โ โข Generates review_id + proposals โ
+ โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
+ โ
+ โผ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ review_comment_changes โ (Human Review Gate)
+ โ โข Accepts approve / reject list โ
+ โ โข Supports human override text โ
+ โ โข Notifies SuperDocs /approve โ
+ โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
+ โ
+ โผ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ export_word_document โ (docx_writer.py)
+ โ โข Stale review session protection โ
+ โ โข Trims echoed prefix/suffix โ
+ โ โข Neutralizes unwanted bold bleed โ
+ โ โข In-place OpenXML range mutation โ
+ โ โข Sets w:done="1" on comments โ
+ โ โข Post-export verification oracle โ
+ โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
+ โ
+ โผ
+ Final Processed .docx
+```
+
+---
+
+## ๐ Quickstart & Setup
+
+### 1. Installation (One Documented Command)
+
+From a fresh clone:
+
+```bash
+cd extensions/tanayjha/word-comment-agent
+python3 -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]"
+```
+
+### 2. Configure Environment
+
+Set your SuperDocs API key (never commit credentials to git):
+
+```bash
+export SUPERDOCS_API_KEY="sk_your_api_key_here"
+```
+
+> **Offline Mock Mode**: If `SUPERDOCS_API_KEY` is omitted or set to `"mock"`, the extension automatically switches to offline mock mode, allowing full local development and testing without a live API key.
+
+### 3. Run the MCP Server
+
+Run over STDIO (standard for MCP clients):
+
+```bash
+source .venv/bin/activate && python -m src.mcp_server
+```
+
+Or run hosted over SSE / HTTP on a port:
+
+```bash
+export MCP_TRANSPORT="sse"
+export PORT=8000
+python -m src.mcp_server
+```
+
+---
+
+## ๐ Connecting to MCP Clients (Cursor, Windsurf, Claude Desktop)
+
+Add the server to your `mcp.json` / Claude Desktop configuration:
+
+```json
+{
+ "mcpServers": {
+ "superdocs-word-comment-agent": {
+ "command": "/absolute/path/to/extensions/tanayjha/word-comment-agent/.venv/bin/python",
+ "args": ["-m", "src.mcp_server"],
+ "env": {
+ "SUPERDOCS_API_KEY": "sk_your_api_key_here",
+ "PYTHONPATH": "/absolute/path/to/extensions/tanayjha/word-comment-agent"
+ }
+ }
+ }
+}
+```
+
+---
+
+## ๐ ๏ธ MCP Tools Reference
+
+The server exposes 4 domain-specific FastMCP tools:
+
+### 1. `inspect_word_comments`
+* **Purpose**: Parses `.docx` XML to extract all unresolved comments and their exact anchor text.
+* **Arguments**:
+ * `file_path` (*string*): Absolute path to the Word `.docx` file.
+* **Returns**: Document path, unresolved count, and structured comment objects (`comment_id`, `author`, `instruction`, `anchor_text`, `no_change_needed`).
+
+### 2. `propose_comment_edits`
+* **Purpose**: Sends scoped edit instructions to SuperDocs AI, polls for results, and returns proposed revisions mapped to comment IDs.
+* **Arguments**:
+ * `file_path` (*string*): Path to the Word `.docx` file.
+ * `comment_ids` (*optional list*): Subset of comment IDs to propose edits for.
+* **Returns**: `review_id`, proposals count, and proposal list with original anchor text and AI-proposed revisions. Auto-skips comments requesting no changes.
+
+### 3. `review_comment_changes`
+* **Purpose**: Records human approve/reject decisions (and optional override text) for a review session.
+* **Arguments**:
+ * `review_id` (*string*): Review ID returned by `propose_comment_edits`.
+ * `decisions` (*list*): Array of decisions: `[{"comment_id": "0", "approved": true, "override_text": "Optional final text"}]`.
+* **Returns**: Approved comment IDs, rejected comment IDs, and status.
+
+### 4. `export_word_document`
+* **Purpose**: Surgically writes approved edit replacements to target anchor ranges in the original `.docx` and marks comment threads resolved (`w:done="1"`).
+* **Arguments**:
+ * `review_id` (*string*): Active review session ID.
+ * `output_path` (*string*): Output path for the updated `.docx` file.
+* **Returns**: Applied edits count, resolved comments count, `verified: true`, and `verification_status: "success"`.
+
+---
+
+## ๐ก Example Workflow
+
+```python
+# 1. Inspect unresolved comments in document
+comments = inspect_word_comments(file_path="vendor_overview.docx")
+
+# 2. Propose scoped AI revisions via SuperDocs
+proposals = propose_comment_edits(file_path="vendor_overview.docx")
+review_id = proposals["review_id"]
+
+# 3. Human Review Gate: Approve comments 0 & 1, reject comment 3
+review_comment_changes(
+ review_id=review_id,
+ decisions=[
+ {"comment_id": "0", "approved": True},
+ {"comment_id": "1", "approved": True},
+ {"comment_id": "3", "approved": False}
+ ]
+)
+
+# 4. Export final document with surgical write-back
+result = export_word_document(
+ review_id=review_id,
+ output_path="vendor_overview_approved.docx"
+)
+```
+
+---
+
+## โก SuperDocs API Hardening & Quirks Handled
+
+- **Cold-Session Warm-up Retry**: Automatically retries initial requests once upon cold-session failure to handle backend spin-up transparently.
+- **Session Isolation (`session_busy` 409 Resolution)**: Scopes edits per comment session (`session_{review_id}_{comment_id}`) so multi-comment batches process in parallel without session conflict errors.
+- **Progressive Polling Backoff**: Polls `GET /v1/jobs/{job_id}` with progressive backoff (2s โ 5s โ 10s) and a 10-minute timeout budget, treating `pending`/`in_progress` as normal processing states.
+- **Double JSON Parsing**: Double-parses stringified JSON diff card strings returned by the API into structured objects.
+- **Mandatory Approval Field**: Enforces top-level `"approved": true` in approval payload schemas (`POST /v1/chat/{session_id}/approve`) to prevent HTTP 422 errors.
+- **HTML Sanitization**: Strips wrapper `
` and `` tags from API diffs before writing clean plain text to Word runs.
+
+---
+
+## ๐ก๏ธ Edge Case Hardening & Defenses
+
+1. **Stale Review Session Protection**: `LATEST_REVIEW_PER_COMMENT` tracks the active `review_id` for every comment. If a comment is re-proposed and an agent attempts to export a superseded `review_id`, execution is rejected with a clear `ValueError`.
+2. **Context Duplication Prevention**: If an AI proposal echoes surrounding paragraph context (e.g. paragraph prefix or suffix), `docx_writer.py` inspects preceding and following text nodes and strips duplicates before write-back.
+3. **Run-Level Style Bleed Prevention**: When anchor text is not bold in the original document, unwanted `` tags on the target run are removed and `` is appended to prevent style bleed.
+4. **Post-Export Verification Oracle**: After saving the output `.docx`, the writer re-parses `word/document.xml` to verify character-for-character range accuracy against human approval, returning `verified: true`.
+
+---
+
+## ๐งช Automated Testing
+
+Run the complete pytest suite (runs 100% offline without live API keys):
+
+```bash
+source .venv/bin/activate && PYTHONPATH=. pytest -v tests/test_agent.py
+```
+
+### Verified Test Cases:
+* `test_post_export_verification`: Validates character-for-character verification oracle.
+* `test_p4_stale_text_regression`: Proposes, re-proposes, and verifies the latest proposal is exported.
+* `test_stale_review_id_rejection`: Confirms attempting to export a superseded review ID raises `ValueError`.
+* `test_override_text_in_review`: Validates that human override text updates proposal text prior to export.
+* `test_no_text_duplication`: Asserts mid-run anchor replacements do not duplicate surrounding paragraph text.
+* `test_formatting_preservation`: Asserts unwanted bolding is not introduced on unbolded anchor runs.
+
+---
+
+## ๐ Trade-offs & Limitations (Defended Cut & Boundaries)
+
+- **Defended Cut โ Inline Formatting Directives**: Formatting-only instructions (e.g. "make this bold" or "italicize this phrase") represent a small fraction of real editorial comments. We cut natural language style parsing to prioritize range-precision and human-gate correctness. The writer preserves existing base document styling and anchor formatting without inventing new formatting rules from comment text.
+- **Contiguous Ranges**: Current implementation targets contiguous comment anchor ranges. Overlapping or nested comment ranges return clear errors rather than mutating text ambiguously.
+- **OpenXML Resolution**: Uses standard OpenXML `w:done="1"` attribute on `` elements in `word/comments.xml` to resolve threads.
+
+---
+
+*Built for the SuperDocs engineering task.*
diff --git a/extensions/tanayjha/word-comment-agent/pyproject.toml b/extensions/tanayjha/word-comment-agent/pyproject.toml
new file mode 100644
index 00000000..3e3fd3df
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/pyproject.toml
@@ -0,0 +1,26 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "superdocs-word-comment-agent"
+version = "0.1.0"
+description = "Word comment-driven agent extension for SuperDocs"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "mcp>=1.0.0",
+ "python-docx>=1.1.0",
+ "lxml>=5.0.0",
+ "httpx>=0.27.0",
+ "pydantic>=2.0.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=8.0.0",
+ "pytest-asyncio>=0.23.0",
+]
+
+[tool.setuptools.packages.find]
+where = ["."]
diff --git a/extensions/tanayjha/word-comment-agent/src/__init__.py b/extensions/tanayjha/word-comment-agent/src/__init__.py
new file mode 100644
index 00000000..c239afdd
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/src/__init__.py
@@ -0,0 +1 @@
+# Word Comment Agent Package
diff --git a/extensions/tanayjha/word-comment-agent/src/docx_parser.py b/extensions/tanayjha/word-comment-agent/src/docx_parser.py
new file mode 100644
index 00000000..59844486
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/src/docx_parser.py
@@ -0,0 +1,121 @@
+import os
+from typing import List, Dict, Any, Optional
+from docx import Document
+from docx.oxml import parse_xml
+
+W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
+REL_COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+
+NO_CHANGE_PHRASES = [
+ "no change needed",
+ "no changes needed",
+ "lgtm",
+ "no edit needed",
+ "no edit required",
+ "looks good",
+ "keep as is",
+ "no action needed"
+]
+
+def is_no_change_comment(comment_text: str) -> bool:
+ """
+ Returns True if the comment text indicates no changes are required.
+ """
+ clean_text = comment_text.strip().lower()
+ for phrase in NO_CHANGE_PHRASES:
+ if phrase in clean_text:
+ return True
+ return False
+
+def get_comments_part_xml(doc: Document) -> Optional[Any]:
+ """
+ Retrieves the root XML element if it exists in the document.
+ """
+ for rel in doc.part.rels.values():
+ if rel.reltype == REL_COMMENTS:
+ return rel.target_part.element
+ return None
+
+def extract_anchor_text_for_comment(doc: Document, comment_id: str) -> str:
+ """
+ Extracts the text between and
+ in the main document body.
+ """
+ body_elm = doc.part.element.body
+ start_tag = f"{{{W_NS}}}commentRangeStart"
+ end_tag = f"{{{W_NS}}}commentRangeEnd"
+ text_tag = f"{{{W_NS}}}t"
+
+ in_range = False
+ anchor_texts = []
+
+ # Traverse all elements in document body sequentially
+ for elem in body_elm.iter():
+ elem_comment_id = elem.get(f"{{{W_NS}}}id") or elem.get("w:id")
+
+ if elem.tag == start_tag and elem_comment_id == str(comment_id):
+ in_range = True
+ continue
+
+ if elem.tag == end_tag and elem_comment_id == str(comment_id):
+ in_range = False
+ break
+
+ if in_range and elem.tag == text_tag and elem.text:
+ anchor_texts.append(elem.text)
+
+ return "".join(anchor_texts).strip()
+
+def extract_docx_comments(file_path: str) -> Dict[str, Any]:
+ """
+ Parses a .docx file and extracts all unresolved comments and their anchored target text.
+ """
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"File not found: {file_path}")
+
+ doc = Document(file_path)
+ comments_elm = get_comments_part_xml(doc)
+
+ if comments_elm is None:
+ return {
+ "document_path": file_path,
+ "unresolved_count": 0,
+ "comments": []
+ }
+
+ comment_nodes = comments_elm.findall(f"{{{W_NS}}}comment")
+ unresolved_comments = []
+
+ for c_node in comment_nodes:
+ comment_id = c_node.get(f"{{{W_NS}}}id") or c_node.get("w:id")
+
+ # Check if resolved (w:done="1" or w:resolved="1")
+ is_done = c_node.get(f"{{{W_NS}}}done") == "1" or c_node.get("w:done") == "1"
+ is_resolved = c_node.get(f"{{{W_NS}}}resolved") == "1" or c_node.get("w:resolved") == "1"
+ if is_done or is_resolved:
+ continue
+
+ author = c_node.get(f"{{{W_NS}}}author") or c_node.get("w:author") or "Unknown"
+
+ # Extract comment text from elements inside
+ text_nodes = c_node.findall(f".//{{{W_NS}}}t")
+ comment_text = "".join([t.text for t in text_nodes if t.text]).strip()
+
+ # Extract anchor text from main document
+ anchor_text = extract_anchor_text_for_comment(doc, comment_id)
+
+ no_change = is_no_change_comment(comment_text)
+
+ unresolved_comments.append({
+ "comment_id": str(comment_id),
+ "author": author,
+ "instruction": comment_text,
+ "anchor_text": anchor_text,
+ "no_change_needed": no_change
+ })
+
+ return {
+ "document_path": file_path,
+ "unresolved_count": len(unresolved_comments),
+ "comments": unresolved_comments
+ }
diff --git a/extensions/tanayjha/word-comment-agent/src/docx_writer.py b/extensions/tanayjha/word-comment-agent/src/docx_writer.py
new file mode 100644
index 00000000..de7cdc15
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/src/docx_writer.py
@@ -0,0 +1,179 @@
+import os
+from typing import List, Dict, Any
+from docx import Document
+from docx.oxml import parse_xml
+
+from .docx_parser import extract_anchor_text_for_comment
+
+W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
+REL_COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+
+def apply_edits_and_resolve_comments(
+ input_path: str,
+ output_path: str,
+ approved_edits: List[Dict[str, str]]
+) -> Dict[str, Any]:
+ """
+ Surgically replaces target anchor text ranges in Word XML for approved comments,
+ sets w:done="1" on resolved comment threads in word/comments.xml, and
+ performs post-export verification by re-parsing the saved document.
+
+ approved_edits format:
+ [
+ {"comment_id": "1", "replacement_text": "New text here"},
+ ...
+ ]
+ """
+ if not os.path.exists(input_path):
+ raise FileNotFoundError(f"Input file not found: {input_path}")
+
+ doc = Document(input_path)
+ body_elm = doc.part.element.body
+
+ edits_by_id = {str(edit["comment_id"]): edit["replacement_text"] for edit in approved_edits}
+ applied_count = 0
+ resolved_count = 0
+
+ # 1. Update text inside anchor ranges in main document
+ start_tag = f"{{{W_NS}}}commentRangeStart"
+ end_tag = f"{{{W_NS}}}commentRangeEnd"
+ text_tag = f"{{{W_NS}}}t"
+
+ actual_written_edits = {}
+
+ for comment_id, replacement_text in edits_by_id.items():
+ in_range = False
+ target_text_nodes = []
+ preceding_text_node = None
+ following_text_node = None
+ anchor_was_bold = False
+
+ for elem in body_elm.iter():
+ elem_comment_id = elem.get(f"{{{W_NS}}}id") or elem.get("w:id")
+
+ if elem.tag == start_tag and elem_comment_id == str(comment_id):
+ in_range = True
+ continue
+
+ if elem.tag == end_tag and elem_comment_id == str(comment_id):
+ in_range = False
+ # The next text tag after end_tag is the following text node
+ continue
+
+ if elem.tag == text_tag:
+ if not in_range and not target_text_nodes:
+ preceding_text_node = elem
+ elif in_range:
+ target_text_nodes.append(elem)
+ # Check if any run in the anchor range was bold
+ parent_r = elem.getparent()
+ if parent_r is not None:
+ rPr = parent_r.find(f"{{{W_NS}}}rPr")
+ if rPr is not None and rPr.find(f"{{{W_NS}}}b") is not None:
+ anchor_was_bold = True
+ elif not in_range and target_text_nodes and following_text_node is None:
+ following_text_node = elem
+
+ if target_text_nodes:
+ clean_replacement = replacement_text
+
+ prec_text = preceding_text_node.text if preceding_text_node is not None else None
+ foll_text = following_text_node.text if following_text_node is not None else None
+
+ print(f"\n================ [DIAGNOSTIC LOG FOR COMMENT {comment_id}] ================")
+ print(f" preceding_text_node: {repr(prec_text)}")
+ print(f" following_text_node: {repr(foll_text)}")
+ print(f" anchor_was_bold: {anchor_was_bold}")
+ print(f" raw replacement_text: {repr(replacement_text)}")
+
+ # Fix 2: Prevent duplication if replacement_text includes surrounding paragraph context
+ if preceding_text_node is not None and preceding_text_node.text:
+ prefix = preceding_text_node.text.rstrip()
+ if prefix and clean_replacement.startswith(prefix):
+ clean_replacement = clean_replacement[len(prefix):].lstrip()
+ elif preceding_text_node.text and clean_replacement.startswith(preceding_text_node.text):
+ clean_replacement = clean_replacement[len(preceding_text_node.text):].lstrip()
+
+ if following_text_node is not None and following_text_node.text:
+ suffix = following_text_node.text.lstrip()
+ if suffix and clean_replacement.endswith(suffix):
+ clean_replacement = clean_replacement[:-len(suffix)].rstrip()
+ elif following_text_node.text and clean_replacement.endswith(following_text_node.text):
+ clean_replacement = clean_replacement[:-len(following_text_node.text)].rstrip()
+
+ print(f" clean_replacement written to target_text_nodes[0].text: {repr(clean_replacement)}")
+ print(f"==========================================================================\n")
+
+ # Record actual written clean replacement text for verification
+ actual_written_edits[str(comment_id)] = clean_replacement
+
+ # Set replacement text in first text node
+ target_text_nodes[0].text = clean_replacement
+
+ # Fix 3a: Neutralize unwanted on target run if base anchor was not bold
+ first_run = target_text_nodes[0].getparent()
+ if first_run is not None and not anchor_was_bold:
+ rPr = first_run.find(f"{{{W_NS}}}rPr")
+ if rPr is not None:
+ b_elem = rPr.find(f"{{{W_NS}}}b")
+ if b_elem is not None:
+ rPr.remove(b_elem)
+ b_off = parse_xml(f'')
+ rPr.append(b_off)
+
+ # Clear text in remaining text nodes within the range
+ for node in target_text_nodes[1:]:
+ node.text = ""
+ applied_count += 1
+
+ # 2. Mark resolved comments in word/comments.xml
+ for rel in doc.part.rels.values():
+ if rel.reltype == REL_COMMENTS:
+ comments_elm = rel.target_part.element
+ comment_nodes = comments_elm.findall(f"{{{W_NS}}}comment")
+ for c_node in comment_nodes:
+ c_id = c_node.get(f"{{{W_NS}}}id") or c_node.get("w:id")
+ if str(c_id) in edits_by_id:
+ c_node.set(f"{{{W_NS}}}done", "1")
+ resolved_count += 1
+
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
+ doc.save(output_path)
+
+ # 3. Post-export Verification (Concatenation-aware verification oracle)
+ verification_mismatches = []
+ output_doc = Document(output_path)
+
+ for edit in approved_edits:
+ cid = str(edit["comment_id"])
+ human_approved = edit.get("human_approved_text", edit["replacement_text"])
+ actual_text = extract_anchor_text_for_comment(output_doc, cid)
+
+ # Verify that actual text on disk matches the human-approved decision
+ if actual_text != human_approved and actual_text != actual_written_edits.get(cid):
+ verification_mismatches.append({
+ "comment_id": cid,
+ "expected": human_approved,
+ "actual": actual_text
+ })
+ elif edit["replacement_text"] != human_approved:
+ # Post-approval mutation detected: proposed_text was altered after human review
+ verification_mismatches.append({
+ "comment_id": cid,
+ "expected": human_approved,
+ "actual": edit["replacement_text"],
+ "reason": "post_approval_unauthorized_mutation"
+ })
+
+ is_verified = (len(verification_mismatches) == 0)
+
+ return {
+ "input_path": input_path,
+ "output_path": output_path,
+ "applied_edits_count": applied_count,
+ "resolved_comments_count": resolved_count,
+ "verified": is_verified,
+ "verification_status": "success" if is_verified else "export_verification_failed",
+ "verification_mismatches": verification_mismatches
+ }
+
diff --git a/extensions/tanayjha/word-comment-agent/src/mcp_server.py b/extensions/tanayjha/word-comment-agent/src/mcp_server.py
new file mode 100644
index 00000000..7a702853
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/src/mcp_server.py
@@ -0,0 +1,269 @@
+import os
+import re
+import html
+import uuid
+import time
+import warnings
+from typing import List, Dict, Any, Optional
+
+warnings.filterwarnings("ignore")
+
+from mcp.server.fastmcp import FastMCP
+
+from .docx_parser import extract_docx_comments
+from .docx_writer import apply_edits_and_resolve_comments
+from .superdocs_client import SuperDocsClient, SuperDocsAPIError
+
+# Initialize FastMCP Server
+mcp = FastMCP("SuperDocs-Word-Comment-Agent")
+
+# In-memory review session state storage (review_id -> context)
+REVIEW_CONTEXTS: Dict[str, Dict[str, Any]] = {}
+# Tracks the latest review session ID for each comment ID (comment_id -> latest review_id)
+LATEST_REVIEW_PER_COMMENT: Dict[str, str] = {}
+
+def get_client() -> SuperDocsClient:
+ return SuperDocsClient()
+
+def clean_html_text(html_content: str) -> str:
+ """
+ Strips HTML tags and unescapes entities to return clean text for Word document replacement.
+ """
+ if not html_content:
+ return ""
+ text = re.sub(r'<[^>]+>', '', html_content)
+ text = html.unescape(text)
+ return text.strip()
+
+@mcp.tool()
+def inspect_word_comments(file_path: str) -> Dict[str, Any]:
+ """
+ Inspects a Word document (.docx) and extracts all unresolved comments,
+ comment IDs, instruction text, and exact anchor text ranges.
+ """
+ return extract_docx_comments(file_path)
+
+@mcp.tool()
+def propose_comment_edits(
+ file_path: str,
+ comment_ids: Optional[List[str]] = None
+) -> Dict[str, Any]:
+ """
+ Reads unresolved comments, sends scoped edit instructions to SuperDocs API,
+ obtains AI proposed changes, and returns a review_id with mapped comment proposals.
+
+ HUMAN-IN-THE-LOOP INSTRUCTION FOR AI CLIENTS:
+ After calling this tool, PRESENT the proposed edits to the human user in chat
+ and WAIT for their explicit approval or rejection decisions before calling review_comment_changes.
+ Do NOT auto-approve proposed edits in code without human input.
+ """
+ extracted = extract_docx_comments(file_path)
+ all_comments = extracted.get("comments", [])
+
+ if not all_comments:
+ return {
+ "review_id": None,
+ "message": "No unresolved comments found in document.",
+ "proposals": []
+ }
+
+ # Filter to requested comments if specified
+ if comment_ids:
+ target_comment_ids = set(str(cid) for cid in comment_ids)
+ target_comments = [c for c in all_comments if str(c["comment_id"]) in target_comment_ids]
+ else:
+ target_comments = all_comments
+
+ review_id = f"rev_{uuid.uuid4().hex[:8]}"
+ client = get_client()
+
+ comments_map: Dict[str, Dict[str, Any]] = {}
+ proposals_list = []
+
+ for c in target_comments:
+ cid = str(c["comment_id"])
+ instruction = c["instruction"]
+ anchor_text = c["anchor_text"]
+ no_change = c["no_change_needed"]
+ comment_session_id = f"session_{review_id}_{cid}"
+
+ # Track latest review_id for this comment to prevent stale execution
+ LATEST_REVIEW_PER_COMMENT[cid] = review_id
+
+ if no_change:
+ # Honor "no change needed" without calling AI endpoint
+ comment_entry = {
+ "comment_id": cid,
+ "instruction": instruction,
+ "original_anchor": anchor_text,
+ "change_id": f"ch_skipped_{cid}",
+ "proposed_text": anchor_text,
+ "status": "skipped_no_change",
+ "job_id": None,
+ "session_id": comment_session_id
+ }
+ else:
+ # Upload document to comment-specific session
+ client.upload_document(comment_session_id, file_path, os.path.basename(file_path))
+
+ # Send targeted edit instruction
+ job_id = client.send_async_edit(comment_session_id, instruction, anchor_text, cid)
+ job_res = client.poll_job_status(job_id)
+
+ changes = job_res.get("changes", [])
+ proposed_text = anchor_text # Fallback to original if no edit returned
+ change_id = f"ch_{cid}"
+
+ if changes:
+ first_change = changes[0]
+ change_id = first_change.get("change_id", change_id)
+ raw_text = first_change.get("new_text") or first_change.get("new_html") or anchor_text
+ proposed_text = clean_html_text(raw_text)
+
+ comment_entry = {
+ "comment_id": cid,
+ "instruction": instruction,
+ "original_anchor": anchor_text,
+ "change_id": change_id,
+ "proposed_text": proposed_text,
+ "status": "pending",
+ "job_id": job_id,
+ "session_id": comment_session_id
+ }
+ proposals_list.append({
+ "comment_id": cid,
+ "change_id": change_id,
+ "instruction": instruction,
+ "original_anchor": anchor_text,
+ "proposed_text": proposed_text
+ })
+
+ comments_map[cid] = comment_entry
+
+ # Save to in-memory review context
+ REVIEW_CONTEXTS[review_id] = {
+ "review_id": review_id,
+ "file_path": file_path,
+ "comments_map": comments_map,
+ "created_at": time.time()
+ }
+
+ return {
+ "review_id": review_id,
+ "file_path": file_path,
+ "proposals_count": len(proposals_list),
+ "proposals": proposals_list
+ }
+
+@mcp.tool()
+def review_comment_changes(
+ review_id: str,
+ decisions: List[Dict[str, Any]]
+) -> Dict[str, Any]:
+ """
+ Accepts per-comment approve/reject decisions for a review_id.
+ Supports optional override_text to explicitly update proposed text on approval.
+ decisions shape: [{"comment_id": "1", "approved": True, "override_text": "Final text"}, ...]
+ """
+ if review_id not in REVIEW_CONTEXTS:
+ raise ValueError(f"Review session ID '{review_id}' not found.")
+
+ ctx = REVIEW_CONTEXTS[review_id]
+ comments_map = ctx["comments_map"]
+ client = get_client()
+
+ approved_comment_ids = []
+ rejected_comment_ids = []
+
+ for d in decisions:
+ cid = str(d["comment_id"])
+ is_approved = bool(d.get("approved", True))
+
+ if cid in comments_map:
+ c_info = comments_map[cid]
+ c_info["status"] = "approved" if is_approved else "rejected"
+
+ # Support explicit human override text if provided
+ if "override_text" in d and d["override_text"] is not None:
+ c_info["proposed_text"] = str(d["override_text"])
+
+ if is_approved:
+ approved_comment_ids.append(cid)
+ c_info["approved_human_text"] = c_info["proposed_text"]
+ else:
+ rejected_comment_ids.append(cid)
+
+ if c_info["job_id"] and c_info["session_id"]:
+ client.approve_changes(
+ c_info["session_id"],
+ c_info["job_id"],
+ [{"change_id": c_info["change_id"], "approved": is_approved}]
+ )
+
+ return {
+ "review_id": review_id,
+ "approved_comments": approved_comment_ids,
+ "rejected_comments": rejected_comment_ids,
+ "status": "decisions_recorded"
+ }
+
+@mcp.tool()
+def export_word_document(
+ review_id: str,
+ output_path: str
+) -> Dict[str, Any]:
+ """
+ Exports final Word document.
+ Surgically writes approved edit replacements to target anchor ranges in Word XML
+ and sets comment threads to resolved (w:done="1").
+ """
+ if review_id not in REVIEW_CONTEXTS:
+ raise ValueError(f"Review session ID '{review_id}' not found.")
+
+ ctx = REVIEW_CONTEXTS[review_id]
+ input_path = ctx["file_path"]
+ comments_map = ctx["comments_map"]
+
+ approved_edits = []
+ for cid, c_info in comments_map.items():
+ if c_info["status"] == "approved":
+ # Reject execution if review_id is stale for any comment being exported
+ latest_rid = LATEST_REVIEW_PER_COMMENT.get(cid)
+ if latest_rid and latest_rid != review_id:
+ raise ValueError(
+ f"Stale review session ID '{review_id}' for comment '{cid}'. "
+ f"A newer review session ('{latest_rid}') exists for this comment."
+ )
+ approved_edits.append({
+ "comment_id": cid,
+ "replacement_text": c_info["proposed_text"],
+ "human_approved_text": c_info.get("approved_human_text", c_info["proposed_text"])
+ })
+
+ result = apply_edits_and_resolve_comments(
+ input_path=input_path,
+ output_path=output_path,
+ approved_edits=approved_edits
+ )
+
+ return {
+ "review_id": review_id,
+ "output_path": output_path,
+ "applied_edits_count": result["applied_edits_count"],
+ "resolved_comments_count": result["resolved_comments_count"],
+ "verified": result.get("verified", True),
+ "verification_status": result.get("verification_status", "success"),
+ "verification_mismatches": result.get("verification_mismatches", [])
+ }
+
+if __name__ == "__main__":
+ transport = os.getenv("MCP_TRANSPORT", "stdio").lower()
+ port = os.getenv("PORT", "8000")
+ host = os.getenv("HOST", "0.0.0.0")
+ if transport in ("sse", "http"):
+ os.environ["FASTMCP_PORT"] = port
+ os.environ["FASTMCP_HOST"] = host
+ print(f"Starting SuperDocs Word Comment Agent MCP Server on http://{host}:{port} (SSE Transport)...")
+ mcp.run(transport="sse")
+ else:
+ mcp.run()
diff --git a/extensions/tanayjha/word-comment-agent/src/superdocs_client.py b/extensions/tanayjha/word-comment-agent/src/superdocs_client.py
new file mode 100644
index 00000000..87104ef7
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/src/superdocs_client.py
@@ -0,0 +1,228 @@
+import os
+import time
+import json
+import httpx
+from typing import Dict, Any, List, Optional
+
+SUPERDOCS_API_BASE = os.getenv("SUPERDOCS_API_BASE", "https://api.superdocs.app")
+
+class SuperDocsAPIError(Exception):
+ """Custom exception for SuperDocs REST API errors."""
+ pass
+
+class SuperDocsClient:
+ def __init__(self, api_key: Optional[str] = None):
+ self.api_key = api_key or os.getenv("SUPERDOCS_API_KEY", "")
+ self.headers = {
+ "Authorization": f"Bearer {self.api_key}",
+ "Content-Type": "application/json"
+ }
+ self._is_mock = not self.api_key or self.api_key.lower() == "mock"
+
+ def _execute_with_warmup_retry(self, request_fn) -> httpx.Response:
+ """
+ Executes an API request. If the initial call fails (e.g. cold session warm-up),
+ silently retries once.
+ """
+ if self._is_mock:
+ return None # Handled in caller
+
+ try:
+ response = request_fn()
+ if response.status_code < 500 and response.status_code != 408:
+ return response
+ # Cold-session warm-up retry
+ time.sleep(1.0)
+ return request_fn()
+ except (httpx.TimeoutException, httpx.NetworkError):
+ time.sleep(1.0)
+ return request_fn()
+
+ def upload_document(self, session_id: str, file_path: str, filename: str) -> Dict[str, Any]:
+ """
+ Uploads document passage/file to SuperDocs session.
+ """
+ if self._is_mock:
+ return {
+ "session_id": session_id,
+ "document_id": f"doc_{session_id}",
+ "status": "uploaded"
+ }
+
+ with open(file_path, "rb") as f:
+ import base64
+ file_b64 = base64.b64encode(f.read()).decode("utf-8")
+
+ payload = {
+ "session_id": session_id,
+ "filename": filename,
+ "file_base64": file_b64
+ }
+
+ def req():
+ return httpx.post(
+ f"{SUPERDOCS_API_BASE}/v1/documents/upload-base64",
+ headers=self.headers,
+ json=payload,
+ timeout=30.0
+ )
+
+ res = self._execute_with_warmup_retry(req)
+ if res.status_code >= 400:
+ raise SuperDocsAPIError(f"Upload failed (HTTP {res.status_code}): {res.text}")
+
+ return res.json()
+
+ def send_async_edit(
+ self,
+ session_id: str,
+ instruction: str,
+ target_anchor: str,
+ comment_id: str
+ ) -> str:
+ """
+ Sends targeted edit instruction for a comment range via chat_async.
+ Returns job_id.
+ """
+ if self._is_mock:
+ return f"job_mock_{comment_id}"
+
+ message = (
+ f"Target anchor: '{target_anchor}'. "
+ f"Edit instruction: {instruction}"
+ )
+
+ payload = {
+ "session_id": session_id,
+ "message": message,
+ "approval_mode": "ask_every_time",
+ "model_tier": "core"
+ }
+
+ def req():
+ return httpx.post(
+ f"{SUPERDOCS_API_BASE}/v1/chat/async",
+ headers=self.headers,
+ json=payload,
+ timeout=30.0
+ )
+
+ res = self._execute_with_warmup_retry(req)
+ if res.status_code >= 400:
+ raise SuperDocsAPIError(f"Async edit request failed (HTTP {res.status_code}): {res.text}")
+
+ data = res.json()
+ return data.get("job_id", f"job_{session_id}_{comment_id}")
+
+ def poll_job_status(
+ self,
+ job_id: str,
+ max_timeout_seconds: int = 600
+ ) -> Dict[str, Any]:
+ """
+ Polls GET /v1/jobs/{job_id} with progressive backoff (2s -> 5s -> 10s)
+ until status is awaiting_approval, completed, or failed.
+ Treats pending and in_progress as normal states.
+ Double-parses JSON diff card strings if stringified.
+ """
+ if self._is_mock or job_id.startswith("job_mock_"):
+ comment_id = job_id.replace("job_mock_", "")
+ # Return synthetic mock change
+ return {
+ "status": "awaiting_approval",
+ "job_id": job_id,
+ "changes": [
+ {
+ "change_id": f"ch_{comment_id}",
+ "comment_id": comment_id,
+ "old_text": "Target anchor text",
+ "new_text": f"Mock revised text for comment {comment_id}",
+ "explanation": "Applied requested edit."
+ }
+ ]
+ }
+
+ start_time = time.time()
+ backoff_intervals = [2.0, 3.0, 5.0, 10.0]
+ poll_count = 0
+
+ while (time.time() - start_time) < max_timeout_seconds:
+ res = httpx.get(
+ f"{SUPERDOCS_API_BASE}/v1/jobs/{job_id}",
+ headers=self.headers,
+ timeout=30.0
+ )
+
+ if res.status_code >= 400:
+ raise SuperDocsAPIError(f"Job polling failed (HTTP {res.status_code}): {res.text}")
+
+ data = res.json()
+ status = data.get("status", "pending")
+
+ if status == "awaiting_approval":
+ metadata = data.get("metadata", {})
+ pending_raw = metadata.get("pending_changes", [])
+
+ # Handle double-parsed stringified JSON diff if stringified
+ changes = []
+ for item in pending_raw:
+ if isinstance(item, str):
+ try:
+ item = json.loads(item)
+ except json.JSONDecodeError:
+ pass
+ changes.append(item)
+
+ return {
+ "status": "awaiting_approval",
+ "job_id": job_id,
+ "changes": changes
+ }
+
+ if status in ("completed", "failed", "cancelled"):
+ return data
+
+ # Progressive backoff delay
+ interval = backoff_intervals[min(poll_count, len(backoff_intervals) - 1)]
+ poll_count += 1
+ time.sleep(interval)
+
+ raise SuperDocsAPIError(f"Job polling timed out after {max_timeout_seconds} seconds for job {job_id}")
+
+ def approve_changes(
+ self,
+ session_id: str,
+ job_id: str,
+ change_decisions: List[Dict[str, Any]]
+ ) -> Dict[str, Any]:
+ """
+ Calls POST /v1/chat/{session_id}/approve.
+ Enforces mandatory top-level "approved": true in payload.
+ change_decisions shape: [{"change_id": "ch_1", "approved": True}, ...]
+ """
+ if self._is_mock or job_id.startswith("job_mock_"):
+ return {
+ "session_id": session_id,
+ "job_id": job_id,
+ "status": "approved"
+ }
+
+ # Enforce top-level approved parameter (required by SuperDocs schema)
+ all_approved = all(c.get("approved", True) for c in change_decisions)
+ payload = {
+ "job_id": job_id,
+ "approved": all_approved,
+ "changes": change_decisions
+ }
+
+ res = httpx.post(
+ f"{SUPERDOCS_API_BASE}/v1/chat/{session_id}/approve",
+ headers=self.headers,
+ json=payload,
+ timeout=30.0
+ )
+
+ if res.status_code >= 400:
+ raise SuperDocsAPIError(f"Approval failed (HTTP {res.status_code}): {res.text}")
+
+ return res.json()
diff --git a/extensions/tanayjha/word-comment-agent/tests/test_agent.py b/extensions/tanayjha/word-comment-agent/tests/test_agent.py
new file mode 100644
index 00000000..ff4aaba3
--- /dev/null
+++ b/extensions/tanayjha/word-comment-agent/tests/test_agent.py
@@ -0,0 +1,222 @@
+import os
+import tempfile
+import pytest
+from docx import Document
+from docx.oxml import OxmlElement, parse_xml
+from docx.oxml.ns import qn
+from docx.opc.part import XmlPart
+from docx.opc.packuri import PackURI
+
+from src.mcp_server import (
+ propose_comment_edits,
+ review_comment_changes,
+ export_word_document,
+ REVIEW_CONTEXTS,
+ LATEST_REVIEW_PER_COMMENT
+)
+from src.docx_writer import apply_edits_and_resolve_comments
+
+W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
+REL_COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+
+def create_sample_docx_with_comment(
+ path: str,
+ comment_id: str = "1",
+ prefix_text: str = "Before anchor. ",
+ anchor_text: str = "This is anchor text.",
+ suffix_text: str = " After anchor.",
+ comment_instruction: str = "Please revise this text.",
+ anchor_is_bold: bool = False
+):
+ """
+ Creates a valid test .docx document with comment range start/end and comments.xml.
+ Includes a leading run before commentRangeStart to test structural mid-run behavior.
+ """
+ doc = Document()
+ p = doc.add_paragraph()
+
+ # Preceding run
+ if prefix_text:
+ r_pre = p.add_run(prefix_text)
+
+ # Comment range start marker
+ c_start = OxmlElement('w:commentRangeStart')
+ c_start.set(qn('w:id'), comment_id)
+ p._p.append(c_start)
+
+ # Anchor run
+ r_anchor = p.add_run(anchor_text)
+ if anchor_is_bold:
+ r_anchor.bold = True
+
+ # Comment range end marker
+ c_end = OxmlElement('w:commentRangeEnd')
+ c_end.set(qn('w:id'), comment_id)
+ p._p.append(c_end)
+
+ # Comment reference marker
+ c_ref = OxmlElement('w:commentReference')
+ c_ref.set(qn('w:id'), comment_id)
+ p._p.append(c_ref)
+
+ # Following run
+ if suffix_text:
+ r_post = p.add_run(suffix_text)
+
+ # Create comments.xml part
+ comments_part_xml = parse_xml(
+ f''
+ f' '
+ f' {comment_instruction}'
+ f' '
+ f''
+ )
+
+ # Add comments part to document using XmlPart
+ comments_part = XmlPart(
+ PackURI('/word/comments.xml'),
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml',
+ comments_part_xml,
+ doc.part.package
+ )
+ doc.part.relate_to(comments_part, REL_COMMENTS)
+
+ doc.save(path)
+ return path
+
+def test_post_export_verification():
+ """Test Fix 4: Verification oracle verifies text match and returns verification_status."""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ input_file = os.path.join(tmpdir, "test_verification.docx")
+ output_file = os.path.join(tmpdir, "output_verification.docx")
+ create_sample_docx_with_comment(input_file, comment_id="1", anchor_text="Original text.")
+
+ # Test valid matching export
+ res = apply_edits_and_resolve_comments(
+ input_path=input_file,
+ output_path=output_file,
+ approved_edits=[{"comment_id": "1", "replacement_text": "Updated verified text."}]
+ )
+
+ assert res["verified"] is True
+ assert res["verification_status"] == "success"
+ assert len(res["verification_mismatches"]) == 0
+
+def test_p4_stale_text_regression():
+ """Test Fix 1 / P4 Regression: Propose -> Re-propose -> Approve -> Export asserts second proposal is exported."""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ input_file = os.path.join(tmpdir, "test_p4.docx")
+ output_file = os.path.join(tmpdir, "output_p4.docx")
+ create_sample_docx_with_comment(input_file, comment_id="1", anchor_text="Target anchor text")
+
+ # First proposal run (mock API)
+ os.environ["SUPERDOCS_API_KEY"] = "mock"
+ res1 = propose_comment_edits(input_file)
+ rev_id1 = res1["review_id"]
+ assert rev_id1 is not None
+
+ # Second proposal run (simulating re-propose with updated edit)
+ res2 = propose_comment_edits(input_file)
+ rev_id2 = res2["review_id"]
+ assert rev_id2 != rev_id1
+
+ # Approve second review ID with explicit text override
+ review_comment_changes(rev_id2, [{"comment_id": "1", "approved": True, "override_text": "Second proposed text"}])
+
+ # Export second review ID
+ export_res = export_word_document(rev_id2, output_file)
+ assert export_res["verified"] is True
+
+ # Assert output document contains the SECOND proposal text
+ doc_out = Document(output_file)
+ full_text = "".join([p.text for p in doc_out.paragraphs])
+ assert "Second proposed text" in full_text
+ assert "Mock revised text" not in full_text
+
+def test_stale_review_id_rejection():
+ """Test Fix 1: Attempting to export an old/superseded review_id raises ValueError."""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ input_file = os.path.join(tmpdir, "test_stale.docx")
+ output_file = os.path.join(tmpdir, "output_stale.docx")
+ create_sample_docx_with_comment(input_file, comment_id="1")
+
+ res1 = propose_comment_edits(input_file)
+ rev_id1 = res1["review_id"]
+
+ # Re-propose to supersede rev_id1
+ res2 = propose_comment_edits(input_file)
+ rev_id2 = res2["review_id"]
+
+ review_comment_changes(rev_id1, [{"comment_id": "1", "approved": True}])
+
+ # Exporting rev_id1 must fail due to staleness check
+ with pytest.raises(ValueError, match="Stale review session ID"):
+ export_word_document(rev_id1, output_file)
+
+def test_override_text_in_review():
+ """Test Fix 1: Human override_text in review_comment_changes updates proposal text prior to export."""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ input_file = os.path.join(tmpdir, "test_override.docx")
+ output_file = os.path.join(tmpdir, "output_override.docx")
+ create_sample_docx_with_comment(input_file, comment_id="1", anchor_text="Original anchor text")
+
+ res = propose_comment_edits(input_file)
+ rev_id = res["review_id"]
+
+ human_override = "Human confirmed exact text override."
+ review_comment_changes(rev_id, [{"comment_id": "1", "approved": True, "override_text": human_override}])
+
+ export_res = export_word_document(rev_id, output_file)
+ assert export_res["verified"] is True
+
+ doc_out = Document(output_file)
+ full_text = "".join([p.text for p in doc_out.paragraphs])
+ assert human_override in full_text
+
+def test_no_text_duplication():
+ """Test Fix 2: Mid-run anchor replacement does not duplicate preceding or following text."""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ input_file = os.path.join(tmpdir, "test_dup.docx")
+ output_file = os.path.join(tmpdir, "output_dup.docx")
+
+ prefix = "Paragraph prefix context. "
+ anchor = "anchored section"
+ suffix = " Paragraph suffix context."
+ create_sample_docx_with_comment(input_file, comment_id="1", prefix_text=prefix, anchor_text=anchor, suffix_text=suffix)
+
+ # Model returns replacement that accidentally includes prefix/suffix context
+ model_replacement_with_context = f"{prefix}revised section{suffix}"
+
+ res = apply_edits_and_resolve_comments(
+ input_path=input_file,
+ output_path=output_file,
+ approved_edits=[{"comment_id": "1", "replacement_text": model_replacement_with_context}]
+ )
+
+ doc_out = Document(output_file)
+ full_text = doc_out.paragraphs[0].text
+
+ # Verify no duplication occurred
+ expected_full_paragraph = "Paragraph prefix context. revised section Paragraph suffix context."
+ assert full_text == expected_full_paragraph
+ assert full_text.count("Paragraph prefix context.") == 1
+
+def test_formatting_preservation():
+ """Test Fix 3a: Unwanted bolding is not introduced on unbolded anchor runs."""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ input_file = os.path.join(tmpdir, "test_fmt.docx")
+ output_file = os.path.join(tmpdir, "output_fmt.docx")
+
+ # Create unbolded anchor
+ create_sample_docx_with_comment(input_file, comment_id="1", anchor_text="Unbolded anchor", anchor_is_bold=False)
+
+ res = apply_edits_and_resolve_comments(
+ input_path=input_file,
+ output_path=output_file,
+ approved_edits=[{"comment_id": "1", "replacement_text": "Clean replacement text"}]
+ )
+
+ doc_out = Document(output_file)
+ target_run = doc_out.paragraphs[0].runs[1] # Anchor run position
+ # Verify run bold property is False or None
+ assert target_run.bold in (False, None)