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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
@@ -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><body>{html}</body></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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div>
<h1>Authorization for Background Investigation</h1>
<p>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.</p>
<p>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}}.</p>
<p>Candidate signature (to be completed via e-signature): ______________________</p>
<p>Date: ______________</p>
<p>Candidate printed name: {{candidate_name}}</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"candidate_name": "Jordan T. Ellis",
"position_title": "Warehouse Operations Supervisor",
"application_date": "August 6, 2026"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"company_name": "Northfield Logistics Inc.",
"cra_name": "Clearwater Screening Services",
"hr_contact_email": "hr@northfieldlogistics.example"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div>
<h1>Disclosure Regarding Background Investigation</h1>
<p>{{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}}.</p>
<p>These reports may include information about your criminal history, employment history verification, education verification, and other background information as permitted by applicable law.</p>
<p>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.</p>
<p>This document is a disclosure only. No release, waiver, authorization, or other acknowledgment is contained in or attached to this document.</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<h1>Summary of Your Rights Under the Fair Credit Reporting Act</h1>
<p><em>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.</em></p>
<p>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.</p>
<p>Provided to: {{candidate_name}}, in connection with your application for {{position_title}} at {{company_name}}.</p>
</div>
Loading