diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/.gitignore b/use-cases/rahul-dhakshin/background-check-consent-generator/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/README.md b/use-cases/rahul-dhakshin/background-check-consent-generator/README.md new file mode 100644 index 0000000..df579d6 --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/README.md @@ -0,0 +1,80 @@ +# Background-Check Consent Generator + +**Built with:** SuperDocs API + a thin Python CLI +**Difficulty:** S2 — real employer compliance pain (FCRA disclosure/authorization +separation), genuine document generation, a real correctness bar to hit. + +## What this is + +Employers running background checks must, under the FCRA, give candidates a +disclosure document that contains **nothing else** (15 U.S.C. § 1681b(b)(2)(A)), +a **separately** signed authorization, and a summary-of-rights notice. Getting +this wrong — mixing a liability waiver or release clause into the disclosure — +is a common, real compliance mistake. This generator takes a company's +templates plus one candidate's details and produces the three documents as +genuinely separate files, then **verifies** (doesn't just assert) that the +disclosure stayed clean. + +## How it works + +1. `merge_fields()` — deterministic Python string-replace fills in + `{{company_name}}`, `{{candidate_name}}`, etc. from `data/company.json` + + `data/candidate.json` into the three HTML templates in `data/`. +2. Each merged document is sent to SuperDocs (`POST /v1/chat`) with a single, + narrow instruction: format into a clean professional layout, add nothing. + (Same design principle as the family-communication-engine build: keep + deterministic work in Python, use the LLM for exactly one job at a time.) +3. `cmd_verify()` scans the generated `disclosure.html` for waiver/liability/ + release/authorization language, sentence-by-sentence, negation-aware — see + `output/RECONCILIATION_REPORT.md` for the real bug this caught in the + verifier itself during testing, and how it was fixed. +4. `cmd_export()` renders all three to PDF via `soffice --headless`. + +## Run it + +```bash +export SUPERDOCS_API_KEY=sk_... +python bgcheck_generator.py generate # calls SuperDocs, writes output/*.html +python bgcheck_generator.py verify # PASS/FAIL on disclosure standalone-ness +python bgcheck_generator.py export # output/*.pdf +python -m unittest tests/test_verify_stdlib.py -v # 6 tests, all passing +``` + +## What strong looks like, mapped to what's actually here + +- **Disclosure contains no unrelated clauses** — verified programmatically, + not eyeballed. See `output/RECONCILIATION_REPORT.md` §3 for the real + false-positive/fix cycle this went through. +- **Authorization and rights notice are separate documents** — three files, + three PDFs, checked to exist independently. +- **Fictional test data throughout** — Northfield Logistics Inc., Clearwater + Screening Services, candidate Jordan T. Ellis. No real personal data. + +## What's not built (deliberate v1 cuts) + +- Real CFPB-mandated Summary of Rights text — `rights_notice.html` is a + clearly labeled placeholder; real use requires the actual published text + and counsel sign-off. +- E-signature integration — outputs are hand-off-ready PDFs, not signed. +- State-specific disclosure variations (some states require additional + disclosure language) — out of scope for this demo. +- Multi-candidate batch generation — this build handles one candidate; the + family-communication-engine build (the companion Task 2 submission) + demonstrates the batch/multi-record pattern instead, so it wasn't + duplicated here. + +## Files + +``` +bgcheck_generator.py CLI: generate / verify / export +data/company.json fictional employer + CRA +data/candidate.json fictional candidate +data/disclosure_template.html standalone disclosure +data/authorization_template.html separate signed authorization +data/rights_notice_template.html separate rights notice (placeholder text) +output/disclosure.{html,pdf} live SuperDocs output, verified clean +output/authorization.{html,pdf} +output/rights_notice.{html,pdf} +output/RECONCILIATION_REPORT.md field-merge + verification evidence +tests/test_verify_stdlib.py 6 stdlib tests, all passing +``` diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/bgcheck_generator.py b/use-cases/rahul-dhakshin/background-check-consent-generator/bgcheck_generator.py new file mode 100644 index 0000000..a514dcf --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/bgcheck_generator.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +bgcheck_generator.py — Background-check consent-form generator, built on SuperDocs. + +Merges a company's template and a candidate's details into an FCRA-style +disclosure-and-authorization PAIR structured the way FCRA requires: a +standalone disclosure document (containing nothing else), a separately +signed authorization, and a summary-of-rights notice, each a distinct +output ready for an external e-signature step. The disclosure is verified, +not just assumed, to contain no unrelated clauses (waivers, liability +language, releases) — that separation is the part real employers get wrong, +per the assignment brief, so this generator checks for it automatically. + +LEGAL NOTE: this produces a structural template for testing/demo purposes. +It is not legal advice and the FCRA Summary of Rights text here is a clearly +labeled placeholder, not the real CFPB-mandated text — see +data/rights_notice_template.html. Any real use requires counsel review. + +Usage: + export SUPERDOCS_API_KEY=sk_... + python bgcheck_generator.py generate + python bgcheck_generator.py verify + python bgcheck_generator.py export +""" +import argparse +import json +import os +import re +import subprocess +import sys +import urllib.request + +API_URL = "https://api.superdocs.app/v1/chat" +DATA_DIR = os.path.join(os.path.dirname(__file__), "data") +OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "output") + +DOCUMENTS = ["disclosure", "authorization", "rights_notice"] + +# The exact failure mode the assignment brief calls out: a disclosure that's +# no longer standalone because a waiver/liability/release clause got mixed +# in. This is what `verify` scans the disclosure for. +FORBIDDEN_IN_DISCLOSURE = [ + r"\bwaiv(e|er|ers|ing)\b", + r"\bliabilit(y|ies)\b", + r"\bindemnif", + r"\breleas(e|es|ed|ing)\b", + r"\bhold\s+harmless\b", + r"\bI\s+authorize\b", # authorization language belongs in authorization.html, not here +] + +# Words that, appearing earlier in the same sentence as a forbidden term, +# flip its meaning from "this document contains X" to "this document does +# NOT contain X" — e.g. "No release, waiver, ... is contained in this +# document" is the disclosure correctly asserting its own standalone-ness, +# not a smuggled-in clause. Found live: the first version of this scanner +# flagged that exact sentence as a violation. Negation-aware sentence +# scanning fixed it. This is a heuristic, not real NLP — a sentence that +# buries a real waiver behind an unrelated negated clause earlier in the +# same sentence could still slip through. Good enough for this generator's +# purpose (catching accidental clause-mixing from an LLM formatting pass), +# not a substitute for legal review. +NEGATION_CUES = re.compile(r"\b(no|not|without|never|none)\b", re.IGNORECASE) + + +def load_json(name): + with open(os.path.join(DATA_DIR, name), encoding="utf-8") as f: + return json.load(f) + + +def load_template(doc_name): + with open(os.path.join(DATA_DIR, f"{doc_name}_template.html"), encoding="utf-8") as f: + return f.read() + + +def merge_fields(template: str, fields: dict) -> str: + out = template + for key, value in fields.items(): + out = out.replace("{{" + key + "}}", value) + return out + + +def call_superdocs(message: str, document_html: str, session_id: str, api_key: str) -> str: + body = json.dumps( + {"message": message, "session_id": session_id, "document_html": document_html} + ).encode("utf-8") + req = urllib.request.Request( + API_URL, + data=body, + method="POST", + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=60) as resp: + data = json.loads(resp.read()) + return data["document_changes"]["updated_html"] + + +def cmd_generate(api_key: str) -> None: + company = load_json("company.json") + candidate = load_json("candidate.json") + fields = {**company, **candidate} + os.makedirs(OUTPUT_DIR, exist_ok=True) + + for doc_name in DOCUMENTS: + template = load_template(doc_name) + merged = merge_fields(template, fields) + # Deliberately explicit: format only, never add content. This is + # the instruction `verify` below is checking actually held. + message = ( + "Format this document into a clean, professional layout with clear " + "spacing and heading structure. Do not add any additional clauses, " + "waivers, releases, liability language, or any sentence not already " + "present below. Preserve every existing sentence's meaning exactly; " + "formatting only." + ) + html = call_superdocs(message, merged, f"bgcheck-{doc_name}", api_key) + out_path = os.path.join(OUTPUT_DIR, f"{doc_name}.html") + with open(out_path, "w", encoding="utf-8") as f: + f.write(f"{html}") + print(f"generated {out_path}") + + +def _strip_tags(html: str) -> str: + return re.sub(r"<[^>]+>", " ", html) + + +def _split_sentences(text: str) -> list: + return re.split(r"(?<=[.!?])\s+", text) + + +def cmd_verify() -> bool: + """The actual proof, not a claim: scans the generated disclosure for + language that would make it no longer standalone. Sentence-scoped and + negation-aware, so a sentence like "No release, waiver, authorization + ... is contained in this document" (the disclosure correctly asserting + its own standalone-ness) is not mistaken for a smuggled-in clause.""" + disclosure_path = os.path.join(OUTPUT_DIR, "disclosure.html") + if not os.path.exists(disclosure_path): + sys.exit("Run `generate` first.") + with open(disclosure_path, encoding="utf-8") as f: + raw = f.read() + + plain = _strip_tags(raw) + violations = [] + for sentence in _split_sentences(plain): + for pattern in FORBIDDEN_IN_DISCLOSURE: + for m in re.finditer(pattern, sentence, re.IGNORECASE): + preceding = sentence[: m.start()] + if NEGATION_CUES.search(preceding): + continue # negated mention, not an actual clause + violations.append((pattern, m.group(0), sentence.strip())) + + if violations: + print("FAIL: disclosure.html is no longer standalone. Found:") + for pattern, matched, sentence in violations: + print(f" - pattern {pattern!r} matched {matched!r} in: {sentence!r}") + return False + + print("PASS: disclosure.html contains no waiver/liability/release/authorization language.") + print("Authorization and rights-notice content confirmed to live in separate files:") + for doc_name in ("authorization", "rights_notice"): + path = os.path.join(OUTPUT_DIR, f"{doc_name}.html") + print(f" - {path}: {'exists' if os.path.exists(path) else 'MISSING'}") + return True + + +def cmd_export() -> None: + os.makedirs(OUTPUT_DIR, exist_ok=True) + for doc_name in DOCUMENTS: + html_path = os.path.join(OUTPUT_DIR, f"{doc_name}.html") + if not os.path.exists(html_path): + continue + subprocess.run( + ["soffice", "--headless", "--convert-to", "pdf", "--outdir", OUTPUT_DIR, html_path], + check=True, + ) + print(f"exported {html_path.replace('.html', '.pdf')}") + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("command", choices=["generate", "verify", "export"]) + args = ap.parse_args() + + if args.command == "generate": + api_key = os.environ.get("SUPERDOCS_API_KEY") + if not api_key: + sys.exit("Set SUPERDOCS_API_KEY first.") + cmd_generate(api_key) + elif args.command == "verify": + ok = cmd_verify() + sys.exit(0 if ok else 1) + elif args.command == "export": + cmd_export() + + +if __name__ == "__main__": + main() diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/data/authorization_template.html b/use-cases/rahul-dhakshin/background-check-consent-generator/data/authorization_template.html new file mode 100644 index 0000000..83fdbab --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/data/authorization_template.html @@ -0,0 +1,8 @@ +
+

Authorization for Background Investigation

+

I, {{candidate_name}}, acknowledge that I have received and read the "Disclosure Regarding Background Investigation" provided by {{company_name}}, as a separate, standalone document from this authorization.

+

I authorize {{company_name}} and its agents, including {{cra_name}}, to obtain the consumer report(s) described in that disclosure for employment purposes in connection with my application for the position of {{position_title}}, dated {{application_date}}.

+

Candidate signature (to be completed via e-signature): ______________________

+

Date: ______________

+

Candidate printed name: {{candidate_name}}

+
diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/data/candidate.json b/use-cases/rahul-dhakshin/background-check-consent-generator/data/candidate.json new file mode 100644 index 0000000..4ab88bb --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/data/candidate.json @@ -0,0 +1,5 @@ +{ + "candidate_name": "Jordan T. Ellis", + "position_title": "Warehouse Operations Supervisor", + "application_date": "August 6, 2026" +} diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/data/company.json b/use-cases/rahul-dhakshin/background-check-consent-generator/data/company.json new file mode 100644 index 0000000..89a00d8 --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/data/company.json @@ -0,0 +1,5 @@ +{ + "company_name": "Northfield Logistics Inc.", + "cra_name": "Clearwater Screening Services", + "hr_contact_email": "hr@northfieldlogistics.example" +} diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/data/disclosure_template.html b/use-cases/rahul-dhakshin/background-check-consent-generator/data/disclosure_template.html new file mode 100644 index 0000000..06b52a6 --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/data/disclosure_template.html @@ -0,0 +1,7 @@ +
+

Disclosure Regarding Background Investigation

+

{{company_name}} may obtain one or more consumer reports (background reports) about you for employment purposes, from {{cra_name}}, a consumer reporting agency, in connection with your application for the position of {{position_title}}.

+

These reports may include information about your criminal history, employment history verification, education verification, and other background information as permitted by applicable law.

+

You have the right to request, at any time, a summary of your rights under the Fair Credit Reporting Act, and to request disclosure of the nature and scope of any investigation.

+

This document is a disclosure only. No release, waiver, authorization, or other acknowledgment is contained in or attached to this document.

+
diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/data/rights_notice_template.html b/use-cases/rahul-dhakshin/background-check-consent-generator/data/rights_notice_template.html new file mode 100644 index 0000000..534752b --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/data/rights_notice_template.html @@ -0,0 +1,6 @@ +
+

Summary of Your Rights Under the Fair Credit Reporting Act

+

TEMPLATE PLACEHOLDER — federal law requires the current, official Summary of Consumer Rights text as published by the CFPB to be provided verbatim, in the exact prescribed format. The paragraph below is structural placeholder content only, standing in for that text to demonstrate this is generated as a third, standalone document. Replace with the current official CFPB-published text, and have counsel confirm the version in use, before this is sent to any real candidate.

+

The Fair Credit Reporting Act (FCRA) promotes the accuracy, fairness, and privacy of information in the files of consumer reporting agencies. This notice describes your rights, including the right to know what is in your file, to dispute inaccurate information, and to obtain a copy of your report.

+

Provided to: {{candidate_name}}, in connection with your application for {{position_title}} at {{company_name}}.

+
diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/output/RECONCILIATION_REPORT.md b/use-cases/rahul-dhakshin/background-check-consent-generator/output/RECONCILIATION_REPORT.md new file mode 100644 index 0000000..f7d8e89 --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/output/RECONCILIATION_REPORT.md @@ -0,0 +1,101 @@ +# Reconciliation Report — Background-Check Consent Generator + +All three documents below were generated live against the real SuperDocs API +(`POST https://api.superdocs.app/v1/chat`), not simulated. Field values come +from `data/company.json` and `data/candidate.json` (fictional test data: +Northfield Logistics Inc., Clearwater Screening Services, candidate Jordan T. +Ellis). + +## 1. Field-merge verification + +Every `{{field}}` placeholder in the three templates was deterministically +replaced by `merge_fields()` (plain Python string replace, no LLM involved) +before the SuperDocs call. Confirmed by inspecting each output for leftover +`{{` markers — none found. + +| Field | Value | Appears in | +|---|---|---| +| company_name | Northfield Logistics Inc. | disclosure, authorization | +| cra_name | Clearwater Screening Services | disclosure, authorization | +| position_title | Warehouse Operations Supervisor | disclosure, authorization, rights_notice | +| candidate_name | Jordan T. Ellis | authorization, rights_notice | +| application_date | August 6, 2026 | authorization | + +## 2. What SuperDocs was asked to do, and what it actually did + +Each merged document was sent with one instruction: *"Format this document +into a clean, professional layout... Do not add any additional clauses, +waivers, releases, liability language, or any sentence not already present +below... formatting only."* + +SuperDocs returned each document restructured into headed sections with +inline styling (`data-chunk-id` grounding on every paragraph, its per-chunk +tracking mechanism) — and, checked sentence-by-sentence against the source +templates, added no new sentences or clauses to any of the three documents. +It reorganized the *disclosure* into four subsections (Consumer Report +Disclosure / Background Information / Your Rights / Disclosure Statement) +and gave *rights_notice* card-style visual grouping, but the wording of every +existing sentence was preserved verbatim. + +## 3. Finding: the disclosure stayed genuinely standalone — but proving that + required fixing my own verification script, not just SuperDocs' output + +`cmd_verify()` scans `disclosure.html` for waiver/liability/release/ +authorization language — the exact failure mode the assignment brief calls +out (a disclosure that's no longer standalone because unrelated clauses got +mixed in). Running it against the live output initially **failed**: + +``` +FAIL: disclosure.html is no longer standalone. Found: + - pattern '\bwaiv(e|er|ers|ing)\b' matched 'waiver' + - pattern '\breleas(e|es|ed|ing)\b' matched 'release' +``` + +The match was this sentence, which is in the *original template*, not +something SuperDocs added: + +> "This document is a disclosure only. **No release, waiver**, authorization, +> or other acknowledgment is contained in or attached to this document." + +That's the disclosure correctly *asserting* it contains no such clauses — a +false positive from naive keyword matching that doesn't understand negation. +Fixed `cmd_verify()` to scan sentence-by-sentence and skip any match preceded +by a negation cue (`no`, `not`, `without`, `never`, `none`) earlier in the +same sentence. Re-ran: + +``` +PASS: disclosure.html contains no waiver/liability/release/authorization language. +Authorization and rights-notice content confirmed to live in separate files: + - output/authorization.html: exists + - output/rights_notice.html: exists +``` + +Added `tests/test_verify_stdlib.py` (6 tests, all passing) to pin this in +both directions: the negated template sentence must pass, and synthetic +sentences with a *real* (non-negated) waiver, release, liability, or +"I authorize" clause must still fail. This is a heuristic, not real NLP — +a real clause buried behind an unrelated negated word earlier in the same +sentence could still slip past it. Documented as a known limitation rather +than papered over. + +## 4. Separation confirmed + +- `disclosure.html` — contains only disclosure content, verified clean. +- `authorization.html` — separate file, contains the signature/authorization + language (including "I authorize...", which is correctly *absent* from + the disclosure). +- `rights_notice.html` — separate file, clearly labeled as a template + placeholder pending the real CFPB-published text (see file header). + +Three distinct files, three distinct PDFs, no cross-contamination. + +## 5. Exports + +`cmd_export()` converts each HTML file to PDF via `soffice --headless` +(same tool used in the family-communication-engine build, for the same +reason: pandoc's LaTeX path doesn't handle this cleanly, and PDF is the +expected hand-off format ahead of an external e-signature step). + +- `disclosure.pdf` +- `authorization.pdf` +- `rights_notice.pdf` diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/output/authorization.html b/use-cases/rahul-dhakshin/background-check-consent-generator/output/authorization.html new file mode 100644 index 0000000..0d62928 --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/output/authorization.html @@ -0,0 +1,6 @@ +

Authorization for Background Investigation

+

I, Jordan T. Ellis, acknowledge that I have received and read the "Disclosure Regarding Background Investigation" provided by Northfield Logistics Inc., as a separate, standalone document from this authorization.

+

I authorize Northfield Logistics Inc. and its agents, including Clearwater Screening Services, to obtain the consumer report(s) described in that disclosure for employment purposes in connection with my application for the position of Warehouse Operations Supervisor, dated August 6, 2026.

+

Candidate signature (to be completed via e-signature): ______________________

+

Date: ______________

+

Candidate printed name: Jordan T. Ellis

diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/output/authorization.pdf b/use-cases/rahul-dhakshin/background-check-consent-generator/output/authorization.pdf new file mode 100644 index 0000000..c1d230e Binary files /dev/null and b/use-cases/rahul-dhakshin/background-check-consent-generator/output/authorization.pdf differ diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/output/disclosure.html b/use-cases/rahul-dhakshin/background-check-consent-generator/output/disclosure.html new file mode 100644 index 0000000..5b1aa2c --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/output/disclosure.html @@ -0,0 +1,9 @@ +

Disclosure Regarding Background Investigation

+

Consumer Report Disclosure

+

Northfield Logistics Inc. may obtain one or more consumer reports (background reports) about you for employment purposes, from Clearwater Screening Services, a consumer reporting agency, in connection with your application for the position of Warehouse Operations Supervisor.

+

Background Information

+

These reports may include information about your criminal history, employment history verification, education verification, and other background information as permitted by applicable law.

+

Your Rights Under the Fair Credit Reporting Act

+

You have the right to request, at any time, a summary of your rights under the Fair Credit Reporting Act, and to request disclosure of the nature and scope of any investigation.

+

Disclosure Statement

+

This document is a disclosure only. No release, waiver, authorization, or other acknowledgment is contained in or attached to this document.

diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/output/disclosure.pdf b/use-cases/rahul-dhakshin/background-check-consent-generator/output/disclosure.pdf new file mode 100644 index 0000000..eeeae42 Binary files /dev/null and b/use-cases/rahul-dhakshin/background-check-consent-generator/output/disclosure.pdf differ diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/output/rights_notice.html b/use-cases/rahul-dhakshin/background-check-consent-generator/output/rights_notice.html new file mode 100644 index 0000000..745b3d9 --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/output/rights_notice.html @@ -0,0 +1,12 @@ +

Summary of Your Rights Under the Fair Credit Reporting Act

+
+

Notice of Consumer Rights

+

TEMPLATE PLACEHOLDER — federal law requires the current, official Summary of Consumer Rights text as published by the CFPB to be provided verbatim, in the exact prescribed format. The paragraph below is structural placeholder content only, standing in for that text to demonstrate this is generated as a third, standalone document. Replace with the current official CFPB-published text, and have counsel confirm the version in use, before this is sent to any real candidate.

+
+
+

Fair Credit Reporting Act (FCRA) Overview

+

The Fair Credit Reporting Act (FCRA) promotes the accuracy, fairness, and privacy of information in the files of consumer reporting agencies. This notice describes your rights, including the right to know what is in your file, to dispute inaccurate information, and to obtain a copy of your report.

+
+
+

Provided to: Jordan T. Ellis, in connection with your application for Warehouse Operations Supervisor at Northfield Logistics Inc..

+
diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/output/rights_notice.pdf b/use-cases/rahul-dhakshin/background-check-consent-generator/output/rights_notice.pdf new file mode 100644 index 0000000..58b65eb Binary files /dev/null and b/use-cases/rahul-dhakshin/background-check-consent-generator/output/rights_notice.pdf differ diff --git a/use-cases/rahul-dhakshin/background-check-consent-generator/tests/test_verify_stdlib.py b/use-cases/rahul-dhakshin/background-check-consent-generator/tests/test_verify_stdlib.py new file mode 100644 index 0000000..89144b1 --- /dev/null +++ b/use-cases/rahul-dhakshin/background-check-consent-generator/tests/test_verify_stdlib.py @@ -0,0 +1,103 @@ +""" +Stdlib-only regression tests for the disclosure scanner in bgcheck_generator.py. + +Covers the real bug found during live testing: the first version of +cmd_verify's regex flagged the disclosure template's own self-description +sentence ("No release, waiver, authorization ... is contained in this +document") as a violation, because it matched the words without noticing +they were negated. Fixed with sentence-scoped, negation-aware scanning. +These tests pin both directions: the negated sentence must PASS, and a +real (non-negated) injected clause must still FAIL. + +Run: python -m unittest tests/test_verify_stdlib.py -v +""" +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import bgcheck_generator as gen # noqa: E402 + + +def scan(html: str): + """Runs the same sentence-scoped, negation-aware scan cmd_verify uses, + against arbitrary HTML, without touching the filesystem.""" + plain = gen._strip_tags(html) + violations = [] + for sentence in gen._split_sentences(plain): + for pattern in gen.FORBIDDEN_IN_DISCLOSURE: + for m in re.finditer(pattern, sentence, re.IGNORECASE): + preceding = sentence[: m.start()] + if gen.NEGATION_CUES.search(preceding): + continue + violations.append((pattern, m.group(0))) + return violations + + +class TestNegationAwareScan(unittest.TestCase): + def test_real_disclosure_self_description_sentence_passes(self): + # The actual sentence from data/disclosure_template.html, live-generated + # through SuperDocs and originally (wrongly) flagged as a violation. + html = ( + "

This document is a disclosure only. No release, waiver, " + "authorization, or other acknowledgment is contained in or " + "attached to this document.

" + ) + self.assertEqual(scan(html), []) + + def test_real_waiver_clause_still_caught(self): + # A genuine smuggled-in clause, not negated — must still fail. + html = "

By signing below you waive any right to dispute this report.

" + violations = scan(html) + self.assertTrue(violations, "a real waiver clause must still be caught") + + def test_real_liability_release_clause_still_caught(self): + html = ( + "

Candidate releases the company from all liability arising " + "from this background check.

" + ) + violations = scan(html) + patterns_matched = {p for p, _ in violations} + self.assertTrue( + any("releas" in p for p in patterns_matched), + "a real release clause must still be caught", + ) + self.assertTrue( + any("liabilit" in p for p in patterns_matched), + "a real liability clause must still be caught", + ) + + def test_i_authorize_language_caught_outside_negation(self): + html = "

I authorize the company to run this report.

" + violations = scan(html) + self.assertTrue(violations) + + def test_end_to_end_verify_passes_on_actual_generated_output(self): + """Full cmd_verify() against the real, live SuperDocs-generated + output/disclosure.html — proves the fix holds on the actual + artifact being submitted, not just a synthetic snippet.""" + out_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "output") + disclosure_path = os.path.join(out_dir, "disclosure.html") + if not os.path.exists(disclosure_path): + self.skipTest("output/disclosure.html not generated yet — run `generate` first") + old_output_dir = gen.OUTPUT_DIR + gen.OUTPUT_DIR = out_dir + try: + self.assertTrue(gen.cmd_verify()) + finally: + gen.OUTPUT_DIR = old_output_dir + + def test_merge_fields_replaces_all_placeholders(self): + template = "

{{candidate_name}} applied for {{position_title}}.

" + merged = gen.merge_fields( + template, {"candidate_name": "Jordan T. Ellis", "position_title": "Supervisor"} + ) + self.assertNotIn("{{", merged) + self.assertIn("Jordan T. Ellis", merged) + self.assertIn("Supervisor", merged) + + +if __name__ == "__main__": + unittest.main() diff --git a/use-cases/rahul-dhakshin/family-communication-engine/README.md b/use-cases/rahul-dhakshin/family-communication-engine/README.md new file mode 100644 index 0000000..3c6dd87 --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/README.md @@ -0,0 +1,75 @@ +# Multilingual family communication engine + +**Assigned build — SuperDocs Round 2.** Who it serves: education / district +communications leads. Difficulty band S2. Surfaces touched: API, templates, +multi-document, export. + +Report-card comment sheets, permission slips, and district notices need to +reach families in their home language, merged with the child's and +teacher's details, with the district's layout and branding intact. This +generates each family's letter from one shared template and a shared +teacher comment bank, in whatever language that family has on file, and +produces both individual per-family PDFs and one combined print run. + +## What's in here + +- `family_letters.py` — the engine. Reusable, not a one-off script. +- `data/district_template.html` — one shared template with `{{merge_field}}` + placeholders. Fictional district, fictional students, all synthetic. +- `data/comment_bank.json` — teacher comments, referenced by id from the + roster rather than duplicated per family. +- `data/families.json` — the family roster: child, grade, teacher, home + language, which comment applies, and whether that letter's already been + sent. +- `output/` — real output from a real run against the live SuperDocs API: + three languages (Spanish, Arabic, Simplified Chinese), per-family PDFs, + one combined print-run PDF, and `RECONCILIATION_REPORT.md` proving the + merged fields are correct rather than just asserting it. + +## Run it yourself + +```bash +export SUPERDOCS_API_KEY=sk_... +python family_letters.py generate # generates every family with status "pending" +python family_letters.py edit-comment C1 "new text" # edits the comment bank, regenerates ONLY pending letters that use it +python family_letters.py export # per-family + combined print-run PDFs +``` + +`export` needs `soffice` (LibreOffice) and `pdfunite` (poppler-utils) on +PATH; both are common Linux package-manager installs. + +## What "strong" required here, and what actually happened testing it + +The assignment's own bar: *"layout survives right-to-left and CJK scripts +on the same template; merged fields correct at scale with a reconciliation +report proving it; a comment-bank edit affects only pending letters and +never already-sent ones."* All three are demonstrated in `output/`, but +none of them worked on the first try, and the fixes are the actual +engineering content of this build: + +1. **Merge-and-translate in one instruction is unreliable.** Asking + SuperDocs to both fill template fields and translate in a single prompt + left stale, untranslated placeholder text behind. Fixed by making field + merging a deterministic Python string-replace and giving SuperDocs one + job only: translate an already-complete document, preserving structure. +2. **RTL scripts render correctly but don't lay out correctly by default.** + SuperDocs' translated HTML carries no direction markup (correctly, that's + not a translation concern) but the PDF export path doesn't infer + right-to-left paragraph alignment from a single top-level `dir="rtl"`. + Fixed by tagging each block element explicitly before export. +3. **Comment-bank edits only reach pending letters** because that's an + explicit status filter in `edit-comment`, not an assumption — verified by + editing `C1` (used by both a pending and an already-sent family) and + confirming only the pending one regenerated. + +Full detail and the actual before/after evidence: `output/RECONCILIATION_REPORT.md`. + +## What's not built + +- OCR/parsing for source documents that arrive as scanned images rather + than clean HTML/text. +- A real district roster integration (SIS/SFTP import) — `families.json` + stands in for that here. +- Automatic language detection; `language_code`/`language_name` are + explicit roster fields, matching "recorded language" in the brief rather + than inferring it. diff --git a/use-cases/rahul-dhakshin/family-communication-engine/data/comment_bank.json b/use-cases/rahul-dhakshin/family-communication-engine/data/comment_bank.json new file mode 100644 index 0000000..e7f6c63 --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/data/comment_bank.json @@ -0,0 +1,5 @@ +{ + "C1": "Alex has shown excellent progress in reading comprehension AND writing this term, and now participates actively in every class discussion.", + "C2": "Please ensure homework is submitted on time; missed assignments are affecting overall performance this term.", + "C3": "Outstanding effort in mathematics this term — keep up the great work heading into next term." +} diff --git a/use-cases/rahul-dhakshin/family-communication-engine/data/district_template.html b/use-cases/rahul-dhakshin/family-communication-engine/data/district_template.html new file mode 100644 index 0000000..f028963 --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/data/district_template.html @@ -0,0 +1,10 @@ +
+

Riverbend Unified School District

+

Report Card Comment Sheet — Term 2

+

Student: {{child_name}}   Grade: {{grade}}

+

Teacher: {{teacher_name}}

+

Teacher Comment

+

{{comment_text}}

+

Please sign and return this form to the school office within 5 school days.

+

Parent/Guardian signature: ______________________

+
diff --git a/use-cases/rahul-dhakshin/family-communication-engine/data/families.json b/use-cases/rahul-dhakshin/family-communication-engine/data/families.json new file mode 100644 index 0000000..bed0f11 --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/data/families.json @@ -0,0 +1,42 @@ +[ + { + "id": "F1", + "child_name": "Alex Rivera", + "grade": "5", + "teacher_name": "Ms. Chen", + "language_code": "es", + "language_name": "Spanish", + "comment_id": "C1", + "status": "pending" + }, + { + "id": "F2", + "child_name": "Yusuf Al-Sayed", + "grade": "3", + "teacher_name": "Mr. Patel", + "language_code": "ar", + "language_name": "Arabic", + "comment_id": "C2", + "status": "pending" + }, + { + "id": "F3", + "child_name": "Mei Lin", + "grade": "4", + "teacher_name": "Ms. Chen", + "language_code": "zh", + "language_name": "Simplified Chinese", + "comment_id": "C3", + "status": "pending" + }, + { + "id": "F4", + "child_name": "Camille Dubois", + "grade": "2", + "teacher_name": "Mr. Patel", + "language_code": "fr", + "language_name": "French", + "comment_id": "C1", + "status": "sent" + } +] diff --git a/use-cases/rahul-dhakshin/family-communication-engine/family_letters.py b/use-cases/rahul-dhakshin/family-communication-engine/family_letters.py new file mode 100644 index 0000000..25e8681 --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/family_letters.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +family_letters.py — Multilingual family communication engine, built on SuperDocs. + +Takes a district template + a teacher comment bank + a family roster, and +generates each family's report-card comment letter in their recorded home +language: merges in the child's and teacher's details and the selected +teacher comment, and asks SuperDocs to translate the whole letter while +preserving the district template's layout across scripts (including +right-to-left and CJK). + +Usage: + export SUPERDOCS_API_KEY=sk_... + python family_letters.py generate # generate all pending letters + python family_letters.py edit-comment C1 "new text" # edit a comment, regenerate ONLY affected pending letters + python family_letters.py export # render per-family PDFs + one combined print-run PDF + +Data lives in data/: district_template.html, comment_bank.json, +families.json. All fictional/synthetic, safe to commit. +""" +import argparse +import glob +import json +import os +import subprocess +import sys +import urllib.request + +API_URL = "https://api.superdocs.app/v1/chat" +DATA_DIR = os.path.join(os.path.dirname(__file__), "data") +OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "output") + +# SuperDocs' translated HTML carries no dir/alignment markup of its own -- +# correctly so, translation and layout direction are different concerns. +# But PDF export (LibreOffice's HTML->PDF path) does NOT infer RTL +# paragraph alignment from a single dir="rtl" on ; without the +# explicit per-block styling below, Arabic/Hebrew/etc. render with correct +# script but left-aligned paragraphs. See RECONCILIATION_REPORT.md. +RTL_LANGUAGE_CODES = {"ar", "he", "fa", "ur"} + + +def wrap_html(inner_html: str, language_code: str, language_name: str) -> str: + if language_code in RTL_LANGUAGE_CODES: + # tag every block element dir="rtl" individually -- a single dir on + # / is not enough for LibreOffice's PDF export to honor + # right alignment per paragraph. + import re + + inner_html = re.sub(r"<(h1|h2|h3|p)(?![^>]*dir=)", r'<\1 dir="rtl"', inner_html) + style = ( + "body{direction:rtl;text-align:right;}" + "p,h1,h2,h3{direction:rtl;text-align:right;unicode-bidi:embed;}" + ) + return ( + f'' + f"{inner_html}" + ) + return f'{inner_html}' + + +def load_json(name): + with open(os.path.join(DATA_DIR, name), encoding="utf-8") as f: + return json.load(f) + + +def save_json(name, data): + with open(os.path.join(DATA_DIR, name), "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + +def load_template(): + with open(os.path.join(DATA_DIR, "district_template.html"), encoding="utf-8") as f: + return f.read() + + +def call_superdocs(message: str, document_html: str, session_id: str, api_key: str) -> str: + body = json.dumps( + {"message": message, "session_id": session_id, "document_html": document_html} + ).encode("utf-8") + req = urllib.request.Request( + API_URL, + data=body, + method="POST", + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=60) as resp: + data = json.loads(resp.read()) + return data["document_changes"]["updated_html"] + + +def merge_fields(template: str, family: dict, comment_text: str) -> str: + """Deterministic, byte-exact field merge -- done in plain Python, not by + asking the LLM to do it. Tried the single "merge AND translate" prompt + first (ask SuperDocs to fill fields and translate in one instruction); + it left stale, untranslated {{comment_text}}/{{teacher_name}} scaffolding + behind alongside a duplicated, partially-filled paragraph -- a real + product limitation worth designing around, not papering over. Splitting + "merge" (deterministic, Python, always correct) from "translate" (one + single-purpose SuperDocs call) fixed it completely: see PROGRESS.md.""" + return ( + template.replace("{{child_name}}", family["child_name"]) + .replace("{{grade}}", family["grade"]) + .replace("{{teacher_name}}", family["teacher_name"]) + .replace("{{comment_text}}", comment_text) + ) + + +def build_translate_prompt(family: dict) -> str: + return ( + f"Translate this entire document into {family['language_name']}, preserving the " + f"exact layout, heading structure, and formatting. Do not translate proper names " + f"(student, teacher, or district names)." + ) + + +def generate_letter(family: dict, comment_bank: dict, template: str, api_key: str) -> str: + comment_text = comment_bank[family["comment_id"]] + merged_html = merge_fields(template, family, comment_text) + message = build_translate_prompt(family) + html = call_superdocs(message, merged_html, f"family-letter-{family['id']}", api_key) + wrapped = wrap_html(html, family["language_code"], family["language_name"]) + os.makedirs(OUTPUT_DIR, exist_ok=True) + out_path = os.path.join(OUTPUT_DIR, f"{family['id']}_{family['language_code']}.html") + with open(out_path, "w", encoding="utf-8") as f: + f.write(wrapped) + return out_path + + +def cmd_generate(api_key: str) -> None: + families = load_json("families.json") + comment_bank = load_json("comment_bank.json") + template = load_template() + for family in families: + if family["status"] != "pending": + print(f"skip {family['id']} (status={family['status']})") + continue + print(f"generating {family['id']} ({family['language_name']})...") + path = generate_letter(family, comment_bank, template, api_key) + print(f" -> {path}") + + +def cmd_edit_comment(comment_id: str, new_text: str, api_key: str) -> None: + """The whole point of a shared comment bank: edit once, and the change + flows to every generated letter that's still pending. Letters already + marked `sent` must never change retroactively.""" + comment_bank = load_json("comment_bank.json") + if comment_id not in comment_bank: + sys.exit(f"unknown comment id {comment_id}") + comment_bank[comment_id] = new_text + save_json("comment_bank.json", comment_bank) + print(f"updated {comment_id} in comment bank") + + families = load_json("families.json") + template = load_template() + affected = [f for f in families if f["comment_id"] == comment_id and f["status"] == "pending"] + untouched = [f for f in families if f["comment_id"] == comment_id and f["status"] != "pending"] + + for family in untouched: + print(f" leaving {family['id']} untouched (status={family['status']}, already sent)") + for family in affected: + print(f" regenerating {family['id']} ({family['language_name']}) with updated comment...") + path = generate_letter(family, comment_bank, template, api_key) + print(f" -> {path}") + + +def _html_to_pdf(html_path: str) -> None: + # pandoc's default LaTeX engine cannot render Arabic (babel has no + # "arabic" language file in a typical install) or CJK (no matching + # Unicode font mapping) -- it errors out on exactly the scripts this + # build exists to support. LibreOffice's HTML->PDF path handles both + # correctly, so it's used for every export here, not just as a fallback. + subprocess.run( + ["soffice", "--headless", "--convert-to", "pdf", "--outdir", os.path.dirname(html_path), html_path], + check=True, + ) + + +def cmd_export() -> None: + """Per-family PDFs first, then the combined print run is assembled by + merging those already-rendered PDFs (via `pdfunite`), not by relying on + CSS page-break-* surviving an HTML->PDF conversion -- LibreOffice's + Writer/Web HTML import filter does not honor page-break-before/-after + at all (tried both; tried routing through an intermediate .odt too), + so every "combined print run" attempt built that way silently ran every + letter onto the same page. Merging real PDF pages sidesteps the whole + problem and is the more robust design regardless.""" + os.makedirs(OUTPUT_DIR, exist_ok=True) + html_files = sorted( + p for p in glob.glob(os.path.join(OUTPUT_DIR, "*.html")) if "_print_run_combined" not in p + ) + pdf_paths = [] + for html_path in html_files: + _html_to_pdf(html_path) + pdf_path = html_path.replace(".html", ".pdf") + print(f"exported {pdf_path}") + pdf_paths.append(pdf_path) + + if pdf_paths: + combined_pdf_path = os.path.join(OUTPUT_DIR, "_print_run_combined.pdf") + subprocess.run(["pdfunite", *pdf_paths, combined_pdf_path], check=True) + print(f"exported combined print run -> {combined_pdf_path}") + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + sub = ap.add_subparsers(dest="command", required=True) + sub.add_parser("generate", help="generate all pending family letters") + sub.add_parser("export", help="render per-family + combined print-run PDFs") + edit_p = sub.add_parser("edit-comment", help="edit a comment bank entry and regenerate affected pending letters") + edit_p.add_argument("comment_id") + edit_p.add_argument("new_text") + args = ap.parse_args() + + api_key = os.environ.get("SUPERDOCS_API_KEY") + if args.command in ("generate", "edit-comment") and not api_key: + sys.exit( + "Set SUPERDOCS_API_KEY first (use.superdocs.app -> Settings -> API Keys -> Create)." + ) + + if args.command == "generate": + cmd_generate(api_key) + elif args.command == "edit-comment": + cmd_edit_comment(args.comment_id, args.new_text, api_key) + elif args.command == "export": + cmd_export() + + +if __name__ == "__main__": + main() diff --git a/use-cases/rahul-dhakshin/family-communication-engine/output/F1_es.html b/use-cases/rahul-dhakshin/family-communication-engine/output/F1_es.html new file mode 100644 index 0000000..b7d123d --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/output/F1_es.html @@ -0,0 +1,10 @@ + +

Riverbend Unified School District

+

Hoja de comentarios de calificaciones — Periodo 2

+

Estudiante: Alex Rivera   Grado: 5

+

Maestra: Ms. Chen

+

Comentario del maestro

+

Alex ha mostrado un excelente progreso en comprensión lectora Y escritura este trimestre, y ahora participa activamente en todas las discusiones de clase.

+

Por favor, firme y devuelva este formulario a la oficina de la escuela dentro de los 5 días escolares.

+

Firma del padre/tutor: ______________________

+ diff --git a/use-cases/rahul-dhakshin/family-communication-engine/output/F1_es.pdf b/use-cases/rahul-dhakshin/family-communication-engine/output/F1_es.pdf new file mode 100644 index 0000000..e494f4a Binary files /dev/null and b/use-cases/rahul-dhakshin/family-communication-engine/output/F1_es.pdf differ diff --git a/use-cases/rahul-dhakshin/family-communication-engine/output/F2_ar.html b/use-cases/rahul-dhakshin/family-communication-engine/output/F2_ar.html new file mode 100644 index 0000000..57de4fa --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/output/F2_ar.html @@ -0,0 +1,13 @@ + +

Riverbend Unified School District

+

ورقة تعليقات تقرير الدرجات — الفصل الدراسي الثاني

+

الطالب: Yusuf Al-Sayed   الصف: 3

+

المعلم: Mr. Patel

+

تعليق المعلم

+

يرجى التأكد من تسليم الواجبات المنزلية في الوقت المحدد؛ فالواجبات الفائتة تؤثر على الأداء العام لهذا الفصل الدراسي.

+

يرجى التوقيع على هذا النموذج وإعادته إلى مكتب المدرسة في غضون 5 أيام دراسية.

+

توقيع ولي الأمر/الوصي: ______________________

+ diff --git a/use-cases/rahul-dhakshin/family-communication-engine/output/F2_ar.pdf b/use-cases/rahul-dhakshin/family-communication-engine/output/F2_ar.pdf new file mode 100644 index 0000000..86a5550 Binary files /dev/null and b/use-cases/rahul-dhakshin/family-communication-engine/output/F2_ar.pdf differ diff --git a/use-cases/rahul-dhakshin/family-communication-engine/output/F3_zh.html b/use-cases/rahul-dhakshin/family-communication-engine/output/F3_zh.html new file mode 100644 index 0000000..884f89d --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/output/F3_zh.html @@ -0,0 +1,10 @@ + +

Riverbend Unified School District

+

成绩单评语表 — 第二学期

+

学生: Mei Lin   年级: 4

+

教师: Ms. Chen

+

教师评语

+

本学期数学表现出色——请在下学期继续保持优秀的成绩。

+

请在5个校内工作日内签署并将此表格交回学校办公室。

+

家长/监护人签名:______________________

+ diff --git a/use-cases/rahul-dhakshin/family-communication-engine/output/F3_zh.pdf b/use-cases/rahul-dhakshin/family-communication-engine/output/F3_zh.pdf new file mode 100644 index 0000000..bbdfbac Binary files /dev/null and b/use-cases/rahul-dhakshin/family-communication-engine/output/F3_zh.pdf differ diff --git a/use-cases/rahul-dhakshin/family-communication-engine/output/RECONCILIATION_REPORT.md b/use-cases/rahul-dhakshin/family-communication-engine/output/RECONCILIATION_REPORT.md new file mode 100644 index 0000000..701cbf9 --- /dev/null +++ b/use-cases/rahul-dhakshin/family-communication-engine/output/RECONCILIATION_REPORT.md @@ -0,0 +1,56 @@ +# Reconciliation report — generated 2026-08-06 + +Proves merged fields are correct at scale, not just claimed. Each row was +checked by confirming the target-language output contains the exact +student name, grade, and teacher name from `families.json`, unmodified and +untranslated, alongside a comment whose content matches the corresponding +entry in `comment_bank.json` at generation time. + +| Family | Child | Grade | Teacher | Language | Comment ref | Status | Fields verified | +|---|---|---|---|---|---|---|---| +| F1 | Alex Rivera | 5 | Ms. Chen | Spanish (es) | C1 | pending → generated | ✅ name, grade, teacher, comment all present untranslated/translated correctly | +| F2 | Yusuf Al-Sayed | 3 | Mr. Patel | Arabic (ar) | C2 | pending → generated | ✅ same, RTL layout applied (`dir="rtl"`) | +| F3 | Mei Lin | 4 | Ms. Chen | Simplified Chinese (zh) | C3 | pending → generated | ✅ same | +| F4 | Camille Dubois | 2 | Mr. Patel | French (fr) | C1 | **sent — not regenerated** | N/A by design, see below | + +## Comment-bank edit propagation, verified live + +1. `C1` originally read: *"Alex has shown excellent progress in reading + comprehension this term and participates actively in class + discussions."* +2. Edited via `family_letters.py edit-comment C1 "..."` to: *"...reading + comprehension AND writing... participates actively in every class + discussion."* +3. **F1** (status `pending`, references `C1`) was regenerated. Its Spanish + letter now reads *"...comprensión lectora Y escritura... participa + activamente en todas las discusiones de clase"* — the edit is present. +4. **F4** (status `sent`, also references `C1`) was **not** regenerated — + confirmed by `cmd_edit_comment`'s own filter (`status == "pending"`), + which is the actual mechanism preventing an already-sent letter from + silently changing after the fact, not just a claim about it. + +## Two real limitations found and designed around, not hidden + +1. **Combined merge+translate is unreliable.** The first working version + sent one instruction to SuperDocs asking it to both fill the merge + fields *and* translate in a single call. That left stale + `{{teacher_name}}`/`{{comment_text}}` placeholder paragraphs behind, + alongside a separate, partially-filled paragraph — SuperDocs followed + part of the instruction but not all of it. Fixed by splitting the two + concerns: field merging is now a deterministic Python string-replace + (always correct, no LLM involved), and SuperDocs is asked to do only the + one thing it's actually suited for — translating an already-complete + document while preserving its structure. Every output above was + generated with this corrected, two-step approach. + +2. **PDF export doesn't inherit RTL layout automatically.** SuperDocs' + translated HTML for Arabic carries no `dir`/text-alignment markup of its + own (correct — that's a rendering concern, not a translation one), and + LibreOffice's HTML→PDF path (used for export here) does not infer + right-to-left paragraph alignment from a single `dir="rtl"` on ``. + The first export rendered Arabic script correctly but left every + paragraph left-aligned. Fixed by applying `dir="rtl"` and explicit + `text-align: right` per block element before export — confirmed visually + in `F2_ar.pdf`, fully right-aligned, script correct. This is the actual + place "layout survives right-to-left scripts" has to be made true, not + assumed true. diff --git a/use-cases/rahul-dhakshin/family-communication-engine/output/_print_run_combined.pdf b/use-cases/rahul-dhakshin/family-communication-engine/output/_print_run_combined.pdf new file mode 100644 index 0000000..997a520 Binary files /dev/null and b/use-cases/rahul-dhakshin/family-communication-engine/output/_print_run_combined.pdf differ