enrich_writeback.py
Language Python5.6 KB150 lines
#!/usr/bin/env python3"""enrich_writeback.py: deterministic write-back for the client-enrich skill.Honors the quarterly-outreach scraping-dump contract:- writes ONLY under `## Research` (its four subsections) + frontmatter- NEVER touches `## Human Notes`, the title line, Order History, Outreach Log, Links- updates frontmatter keys only if currently empty or "[TBD]" (unless --force)- always bumps the stub's existing date key (`last_modified`, else `updated`)- leaves the rest of the file byte-identicalUsage:python3 enrich_writeback.py --stub PATH --payload payload.json [--force] [--date YYYY-MM-DD]payload.json schema (all keys optional; omit what you don't have):{"frontmatter": {"title": "Head of Events & Brand Marketing","seniority": "director","city": "Denver","state": "Colorado","linkedin": "https://www.linkedin.com/in/example-jordan-reyes","company": "Brightwater Provisions","company_linkedin": "https://www.linkedin.com/company/example-brightwater-provisions"},"person": ["- Headline: ...", "- Current: Head of Events ... at Brightwater Provisions (since 2024-09)", "..."],"company": ["- Industry: consumer goods", "- Employees: ~60", "..."],"signals": ["- Headcount up ~25% over 12 months", "..."],"sources": ["Apollo enrichment (people-match), 2026-06-16", "https://..."]}person/company/signals/sources are lists of pre-formatted markdown lines(strings). Bullets ("- ") are written as-is; the script does not reformat them."""import argparse, json, re, sys, datetimefrom _stub import fm_set, split_frontmatterSUBSECTIONS = [("person", "### Person / LinkedIn"),("company", "### Company"),("signals", "### Recent News & Signals"),("sources", "### Sources"),]# canonical order in the stub templateRESEARCH_ORDER = ["### Company", "### Person / LinkedIn","### Recent News & Signals", "### Sources"]KEY_BY_HEADING = {h: k for k, h in SUBSECTIONS}FM_KEYS = ["title", "seniority", "city", "state", "linkedin","industry", "company_website","company", "company_linkedin"]def load(path):with open(path, "r", encoding="utf-8") as f:return f.read()DATE_KEYS = ("last_modified", "updated") # vault stubs use last_modifieddef bump_updated(fm, date):"""Stamp the file's existing date key. Never introduce a second one.Vault stubs (and everything the weekly driver writes) carry `last_modified`;older hand-made files may carry `updated`. Writing a fixed `updated:` keywould add a second date key that then drifts from the first, so stampwhichever key is present, preferring last_modified; add one only if neither is."""for key in DATE_KEYS:if re.search(rf'(?m)^{key}:[ \t]*.*$', fm):return re.sub(rf'(?m)^{key}:[ \t]*.*$', f"{key}: '{date}'", fm, count=1)return fm.rstrip() + f"\nlast_modified: '{date}'"def build_research_block(payload, date):out = ["## Research",f"<!-- Enriched {date} via client-enrich skill. Sourced below. Never overwrite Human Notes. -->"]for heading in RESEARCH_ORDER:key = KEY_BY_HEADING[heading]out.append(heading)lines = payload.get(key) or []if lines:out.extend(lines)else:out.append("[TBD]")return "\n".join(out) + "\n"def replace_research(body, new_block):# Research starts at "## Research" and ends just before the next "## " headingm = re.search(r'(?m)^## Research\s*$', body)if not m:sys.exit("ERROR: no '## Research' section found in stub.")start = m.start()nxt = re.search(r'(?m)^## (?!Research).*$', body[start + 1:])if not nxt:sys.exit("ERROR: no section after '## Research' (expected '## Human Notes').")end = start + 1 + nxt.start()return body[:start] + new_block + "\n" + body[end:]def main():ap = argparse.ArgumentParser()ap.add_argument("--stub", required=True)ap.add_argument("--payload", required=True)ap.add_argument("--force", action="store_true",help="overwrite non-empty frontmatter values too")ap.add_argument("--date", default=datetime.date.today().isoformat())args = ap.parse_args()text = load(args.stub)with open(args.payload, "r", encoding="utf-8") as f:payload = json.load(f)fm, body = split_frontmatter(text)before_human = body.split("## Human Notes", 1)human_snapshot = ("## Human Notes" + before_human[1]) if len(before_human) > 1 else Nonechanged = []for key in FM_KEYS:if key in payload.get("frontmatter", {}):val = str(payload["frontmatter"][key]).strip()if val:fm, did = fm_set(fm, key, val, args.force)if did:changed.append(key)fm = bump_updated(fm, args.date)body = replace_research(body, build_research_block(payload, args.date))# guard: Human Notes must be untouchedif human_snapshot is not None:after = body.split("## Human Notes", 1)after_human = ("## Human Notes" + after[1]) if len(after) > 1 else Noneif after_human != human_snapshot:sys.exit("ABORT: Human Notes section would change; refusing to write.")out = f"---\n{fm}\n---\n{body}"with open(args.stub, "w", encoding="utf-8") as f:f.write(out)print(f"OK {args.stub}")print(f" frontmatter updated: {', '.join(changed) if changed else '(none - already filled)'}")print(f" research subsections written: "f"{', '.join(k for k,_ in SUBSECTIONS if payload.get(k))}")if __name__ == "__main__":main()