Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SUPERDOCS_API_KEY=your-key-here
SUPERDOCS_BASE_URL=https://api.superdocs.app

7 changes: 7 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv/
.pytest_cache/
__pycache__/
.spec-doc-sync-state.json
plan.json
.env

7 changes: 7 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: demo test

demo:
PYTHONPATH=src uv run --extra dev python scripts/demo.py

test:
PYTHONPATH=src uv run --extra dev pytest
102 changes: 102 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Spec Doc Sync

Spec Doc Sync keeps human-written API reference documentation aligned with an evolving OpenAPI specification. It compares two committed specification snapshots, classifies additions/removals/renames/deprecations, locates every affected documentation reference, and creates narrow edit proposals. A writer approves or rejects each proposal. Approved intents can run through SuperDocs review mode before export; uncertain cases are escalated instead of guessed.

Built by **Ashish Huddar** for the SuperDocs task.

![Verified no-spend demo output](assets/demo.svg)

## Proof in one command

Install [uv](https://docs.astral.sh/uv/), then run:

```bash
make demo
```

The demo uses two committed Nimbus API snapshots and a copied documentation tree. It runs a no-spend preview, approves only high-confidence edits, applies them, proves the rename was updated everywhere, and prints SHA-256 evidence showing untouched pages remained byte-identical. No API key is needed. Run `make test` for the full offline suite.

## The workflow

```text
spec v1 + spec v2
|
v
semantic diff -> classify -> search every Markdown page -> proposal plan
|
writer approves / rejects / escalates
|
local proof mode OR SuperDocs review mode
|
changed pages + byte-identity manifest + stop state
```

Preview is always free and makes zero network calls:

```bash
PYTHONPATH=src uv run python -m spec_doc_sync.cli plan \
--previous fixtures/openapi-v1.yaml \
--current fixtures/openapi-v2.yaml \
--docs fixtures/docs \
--out plan.json \
--state .spec-doc-sync-state.json
```

Review and deterministic proof mode:

```bash
PYTHONPATH=src uv run python -m spec_doc_sync.cli review plan.json
PYTHONPATH=src uv run python -m spec_doc_sync.cli apply plan.json --docs fixtures/docs
```

Live SuperDocs review mode (server-side key only):

```bash
export SUPERDOCS_API_KEY=your-key-here
PYTHONPATH=src uv run python -m spec_doc_sync.cli live plan.json --docs fixtures/docs --export-dir reviewed
```

Live mode sends only approved intents to `POST /v1/chat/async` with `approval_mode: ask_every_time`, polls the durable job, renders each proposed change, posts explicit per-change decisions to `/v1/chat/{session_id}/approve`, and optionally exports the reviewed session through `/v1/documents/export`. The key is environment-only and is never returned to a browser or written to state.

For a credentialed smoke test without placing the key in shell history or a file, copy the key and pipe the clipboard directly to the verifier:

```bash
pbpaste | PYTHONPATH=src uv run python scripts/live_verify.py --output reviewed-reference.docx
```

**Live verification (22 August 2026):** SuperDocs completed the async review job, surfaced one change, accepted one explicit approval, renamed `customer_id` to `account_id`, and exported a 36,683-byte DOCX. The rendered export was visually clean and contained no remaining `customer_id` token.

## What is measured

- **Exhaustive rename:** every exact token occurrence of a confidently renamed field is counted before editing; applying a stale plan fails rather than partially updating.
- **Surgical output:** every Markdown page is hashed before and after. Untouched pages must be byte-identical.
- **Uncertainty:** zero or multiple plausible rename targets create an escalation, never a silent edit.
- **Reproducibility:** both specification snapshots and the synthetic docs are committed.
- **No re-trigger:** the state file records the processed specification-pair hashes. Generated documentation changes do not trigger another run; only a new spec snapshot pair does.
- **No-spend preview:** `plan` never imports or constructs the SuperDocs client and makes no network request.

## SuperDocs surfaces used

- API: asynchronous document editing
- Review: item-level `ask_every_time` approval
- Chat: natural-language, narrowly scoped wording instruction
- Search: local exhaustive reference discovery before any spend
- Multi-document: one plan spans every affected reference page
- Export: the live job's reviewed session can be exported through `/v1/documents/export`

## Honest boundaries

- This submission edits Markdown reference sources. SuperDocs receives structured HTML derived from a page; a production adapter should map the final chunk HTML back to the site's AST or use the document export endpoint.
- Rename inference is intentionally conservative: one removed and one structurally identical added property in the same schema. More ambiguous semantic renames escalate.
- Added-section wording uses the specification description when present. A missing description is low confidence and should be rejected or rewritten by the writer.
- The live client is implemented against the current documented async/poll/approve contract, but this repository contains no credential and the offline tests do not claim a successful paid operation.
- The tool watches specification snapshots, not its generated documentation, which is the stopping rule that prevents self-triggering loops.

## Repository layout

- `src/spec_doc_sync/diff.py` - structured OpenAPI diff and conservative rename inference
- `src/spec_doc_sync/planner.py` - cross-page reference search and uncertainty routing
- `src/spec_doc_sync/apply.py` - exact edits plus byte-identity manifest
- `src/spec_doc_sync/superdocs.py` - server-side async review client
- `fixtures/` - reproducible synthetic snapshots and reference docs
- `tests/` - offline behavioral proof
18 changes: 18 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/assets/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Authentication

Send a bearer token in the Authorization header. Tokens are scoped to one Nimbus workspace.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Quickstart

Create a draft with `customer_id`, then store the returned invoice identifier.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Invoice reference

`customer_id` identifies the customer account that owns the invoice.

The optional `memo` field appears on draft invoices.

20 changes: 20 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/fixtures/openapi-v1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
openapi: 3.1.0
info: {title: Nimbus Billing API, version: 1.4.0}
paths:
/invoices:
post:
operationId: createInvoice
summary: Create an invoice
description: Creates a draft invoice for a customer.
components:
schemas:
Invoice:
type: object
properties:
customer_id:
type: string
description: Stable account identifier.
memo:
type: string
description: Free-form invoice note.

29 changes: 29 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/fixtures/openapi-v2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
openapi: 3.1.0
info: {title: Nimbus Billing API, version: 1.5.0}
paths:
/invoices:
post:
operationId: createInvoice
summary: Create an invoice
description: Creates a draft invoice for an account.
/invoices/{invoice_id}/finalize:
post:
operationId: finalizeInvoice
summary: Finalize an invoice
description: Locks a draft invoice and queues it for delivery.
components:
schemas:
Invoice:
type: object
properties:
account_id:
type: string
description: Stable account identifier.
memo:
type: string
description: Free-form invoice note.
deprecated: true
delivery_method:
type: string
description: Delivery channel selected for the finalized invoice.

24 changes: 24 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[project]
name = "spec-doc-sync"
version = "0.1.0"
description = "Keep human-written API reference docs aligned with OpenAPI changes through SuperDocs review"
requires-python = ">=3.11"
dependencies = ["httpx>=0.28,<1", "pyyaml>=6,<7"]

[project.optional-dependencies]
dev = ["pytest>=8.4,<9"]

[project.scripts]
spec-doc-sync = "spec_doc_sync.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/spec_doc_sync"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
pythonpath = ["src"]
31 changes: 31 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/scripts/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

import shutil
import tempfile
from pathlib import Path

from spec_doc_sync.apply import apply_plan
from spec_doc_sync.planner import build_plan


root = Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as directory:
demo = Path(directory)
docs = demo / "docs"
shutil.copytree(root / "fixtures" / "docs", docs)
state = demo / "state.json"
plan = build_plan(root / "fixtures" / "openapi-v1.yaml", root / "fixtures" / "openapi-v2.yaml", docs, state)
print(f"No-spend preview: {len(plan.changes)} spec changes -> {len(plan.proposals)} proposals")
for proposal in plan.proposals:
if proposal.decision == "pending": proposal.decision = "approved" if proposal.confidence >= 0.75 else "rejected"
proof = apply_plan(plan, docs, state)
reference_text = (docs / "reference.md").read_text(encoding="utf-8")
quickstart_text = (docs / "quickstart.md").read_text(encoding="utf-8")
assert "customer_id" not in reference_text + quickstart_text
assert "account_id" in reference_text and "account_id" in quickstart_text
untouched = [name for name, item in proof["proof"].items() if item["byte_identical"]]
print(f"Rename search proof: all 2 occurrences updated")
print(f"Byte-identical untouched pages: {', '.join(untouched)}")
stopped = build_plan(root / "fixtures" / "openapi-v1.yaml", root / "fixtures" / "openapi-v2.yaml", docs, state)
print(f"Second invocation: {stopped.stop_reason}")

59 changes: 59 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/scripts/live_verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

from spec_doc_sync.planner import build_plan
from spec_doc_sync.superdocs import SuperDocsClient


def main() -> None:
parser = argparse.ArgumentParser(description="Run one credentialed SuperDocs review round-trip")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()

api_key = sys.stdin.read().strip()
if not api_key.startswith("sk_"):
raise SystemExit("expected a SuperDocs sk_ API key on standard input")

root = Path(__file__).resolve().parents[1]
docs = root / "fixtures" / "docs"
plan = build_plan(root / "fixtures" / "openapi-v1.yaml", root / "fixtures" / "openapi-v2.yaml", docs)
proposal = next(item for item in plan.proposals if item.operation == "replace_all" and item.file == "reference.md")

decisions: list[dict[str, object]] = []

def decide(change: dict) -> tuple[bool, str]:
old_html = str(change.get("old_html", ""))
new_html = str(change.get("new_html", ""))
approved = "customer_id" in old_html and "account_id" in new_html
decisions.append({"approved": approved, "change_id_present": bool(change.get("change_id"))})
feedback = "" if approved else "Only rename customer_id to account_id; do not change unrelated text."
return approved, feedback

client = SuperDocsClient(api_key=api_key)
result = client.propose(docs / proposal.file, proposal, decide, poll_seconds=1.0)
args.output.parent.mkdir(parents=True, exist_ok=True)
exported = client.export(result["session_id"], args.output.name)
args.output.write_bytes(exported)

print(
json.dumps(
{
"status": result.get("status"),
"review_decisions": len(decisions),
"approved": sum(bool(item["approved"]) for item in decisions),
"rejected": sum(not bool(item["approved"]) for item in decisions),
"export_bytes": len(exported),
"output": str(args.output),
},
indent=2,
)
)


if __name__ == "__main__":
main()

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Specification-aware, review-gated documentation synchronization."""

__version__ = "0.1.0"

55 changes: 55 additions & 0 deletions use-cases/ashish921998/spec-doc-sync/src/spec_doc_sync/apply.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

import hashlib
import json
import re
from pathlib import Path

from .models import EditProposal, Plan


def replace_exact(text: str, needle: str, replacement: str) -> tuple[str, int]:
pattern = re.compile(rf"(?<![A-Za-z0-9_]){re.escape(needle)}(?![A-Za-z0-9_])")
return pattern.subn(replacement, text)


def apply_plan(plan: Plan, docs_dir: Path, state_path: Path) -> dict:
changed: list[str] = []
proof: dict[str, dict] = {}
before = {str(path.relative_to(docs_dir)): hashlib.sha256(path.read_bytes()).hexdigest() for path in docs_dir.rglob("*.md")}
for proposal in plan.proposals:
if proposal.decision != "approved" or not proposal.file:
continue
path = docs_dir / proposal.file
text = path.read_text(encoding="utf-8")
if proposal.operation == "replace_all":
assert proposal.needle and proposal.replacement
updated, count = replace_exact(text, proposal.needle, proposal.replacement)
if count != proposal.occurrences:
raise RuntimeError(f"stale plan for {proposal.file}: expected {proposal.occurrences} matches, found {count}")
elif proposal.operation == "append_section":
assert proposal.replacement
updated = text + proposal.replacement
elif proposal.operation == "mark_deprecated":
assert proposal.needle
updated, count = replace_exact(text, proposal.needle, f"{proposal.needle} (deprecated)")
if count != proposal.occurrences:
raise RuntimeError(f"stale plan for {proposal.file}: expected {proposal.occurrences} matches, found {count}")
else:
continue
if updated != text:
path.write_text(updated, encoding="utf-8")
changed.append(proposal.file)

after = {str(path.relative_to(docs_dir)): hashlib.sha256(path.read_bytes()).hexdigest() for path in docs_dir.rglob("*.md")}
for name, checksum in before.items():
proof[name] = {"before": checksum, "after": after[name], "byte_identical": checksum == after[name]}
state = {
"previous_spec_sha256": plan.previous_spec_sha256,
"current_spec_sha256": plan.current_spec_sha256,
"changed_files": sorted(set(changed)),
"proof": proof,
}
state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
return state

Loading