0073 — Multi-source ingestion & reporting platform (learn-a-config, gated, reconciled)

✅ Validated on real data (2026-06-30), through the real automated pipeline — reproducible, not by hand. The generic structure-detect → config-driven extract → tie-out-judge pipeline (§13) — not a bespoke script — has run end-to-end against 13 properties across 8 structurally-different source formats in one workbook: four Yardi box-score variants (Asset Living / MRR / Ross / Ridge), a scalar label/value summary (Citrus Glen), a single-row portfolio rollup (Orchard Walk), a weekly time-series (Eagle Villas), and a two-tab-summed-plus-financials format (The Ashley) — property names anonymized, roster in the gtm repo. Result: 155 of 159 comparable fields tie to the cent against the operator's own published Weekly Report, 0 unexpected mismatches, overall tiedOut = true; a generated Weekly Report .xlsx built from the parsed data independently reproduces 155/182 cells to the cent, with every remaining cell honestly classified (5 mismatches + 22 source-gaps) rather than silently coerced. Three genuine source discrepancies were caught and held for review, not hidden or guessed past — see §3a. Full numbers in §3a/§9; §13 answers the learn-a-format engine (including the new non-tabular keyValue block kind), the verification layers, and onboarding (full spec in the companion ../architecture/clara-ingestion-platform.md).

Implementation status (2026-07-01): Accepted and shipped. The learn-a-config engine and the customer-facing Portfolio Intelligence feature this ADR describes are live in production — no longer a prototype. Shipped across:

Known gaps carried forward as follow-ups (tracked, not hidden): golden-set learn rate is ~16–17/21 on the real corpus with honest holds on the rest; multi-block composition (joining unrelated blocks across a sheet, e.g. Subsidy Delinquency) is the one strategy still descoped; a box-score sheet that splits one property across multiple sub-sections (Ash1BxScr) converges on only the first block; email Mode B ingestion is not yet built (upload-only). Phase 1 (Temporal-orchestrated per-report arrival) remains as described below.


1. Strategic intent (why this exists)

Client-specific GTM strategy (who we sell, pricing, stakeholders, competitive positioning) lives in the gtm client repo. This section states only the product motivation.

2. Problem & requirements

3. Proposed architecture (strawman — to be validated by the research fleet)

Layered, deterministic-first, agent-escalation-as-fallback, correctness-gated:

# Layer Responsibility Default posture
1 Intake & source detection Email webhook (reuse existing /api/email/ingest); extract attachments + inline tables; identify property / manager / report-type; idempotency key per message reuse
2 Deterministic parsing Format-specific parsers (xlsx, HTML/MIME tables) → map to canonical schema; strict validation. Most reports never touch an LLM. build/reuse OSS
3 Agentic escalation (fallback only) When deterministic parse fails or confidence is low → LLM extraction with schema-constrained structured output + validation/self-repair, progressive model strength (cheap→strong). LLM proposes, deterministic validator disposes. build
4 Canonical portfolio data model Normalized, versioned; single source the report + intelligence read from build
5 Report generation Render the operator's weekly template from canonical data (deterministic templating; LLM only for narrative) build
6 Intelligence layer Grounded analysis over portfolio data + PropFlow context (website, prior calls) → variance flags, trends, owner-ready commentary build
7 Durable orchestration + HITL Temporal (candidate) for retries/idempotency/versioning + human-in-the-loop review gate before a report sends; corrections feed back as parser/eval improvements evaluate
8 Interface Clara chat + specialized MCP server for their Claude Team (tool design = research output) build

Cross-cutting guardrails: fair-housing/PII handling, full audit log, and a golden-dataset regression eval so a parser change can't silently regress — this is the core format-drift defense.

3a. The reliability architecture — the GATE STACK (precision · determinism · consistency)

Governing principle: correctness and consistency over everything else. The LLM is the last resort, never the default. A layer may only pass data forward if it provably satisfies the canonical schema. Anything uncertain is held for a human — never guessed. We would rather produce no number than a wrong number.

The pipeline is a ladder of gates. Each gate either passes clean data forward or stops it. Most weeks never touch an LLM.

Gate What it does What it catches Determinism
G0 · Trust Process only reports from verified senders (SPF/DKIM + known manager addresses); dedupe per (manager, week) Poisoned/duplicate/wrong-source input pure code
G1 · Deterministic parse Per-manager profile extracts fields by semantic anchors (labels, sheet aliases, structural position) — never hard-coded cell addresses like the current Excel ~95% of weeks resolve here, zero LLM pure code, reproducible
G2 · Confidence/quality check Per-field confidence + deterministic sanity checks (ranges, required fields, totals reconcile). Escalate only the fields that fail Silent mis-maps; partial drift pure code
G3 · Agentic escalation (fallback) Schema-constrained LLM maps only the ambiguous columns — maps schema, never values; progressive model strength (cheap→strong) New/changed manager layout temp 0, structured output → malformed result structurally impossible
G4 · Vision (last resort) Only if source is image/PDF that defeats structured parse Scanned/exported-as-image reports constrained
G5 · Canonical schema validation (HARD GATE) Result must satisfy the PortfolioWeeklyReport Zod schema: required fields, type/range, cross-checks — the full unit identity occupied + vacant + notice + model + down + admin = units (NOT the naive occupied+vacant=units, which real files fail because of model/down/admin excluded units); aging buckets sum to Total Unpaid; Total + Prepays = Balance; WoW delta within sane bounds. Fail = do not publish → review Format drift, arithmetic inconsistency pure code
G6 · Cross-property reconciliation Merge across managers; detect overlapping-property conflicts; reconcile portfolio totals vs any source-provided totals Conflicting/duplicate property numbers pure code
G7 · HITL review/publish gate Auto-publish only when fully green (validated, no conflicts, prior successful week exists). Else hold for PM review with the exact flagged fields. Corrections → new golden cases Anything the machine isn't sure about human
G8 · Intelligence (grounded) Commentary generated only from validated fields; every claim cites a source field; no-invented-numbers enforced Hallucinated trends in a customer-facing artifact constrained + cited
G9 · Audit + golden-eval Every extraction traced (tier, confidence, escalation reason, validation result); every change re-run against real scrubbed weeks Silent regression over time pure code

Why the numbers are deterministic/reproducible:

  1. The LLM (G3) only ever decides which column = which field — it never emits a value. Values always come from deterministic extraction of the raw cell. Same input → same output.
  2. LLM calls are temperature 0 + schema-constrained (Anthropic structured outputs / strict tool use), so malformed mappings can't occur.
  3. Every layer is idempotent and cached on (manager, week).

Why this beats their Excel AND the Microsoft DIY path — and it's provable in their own file:

✅ Validated on real operator data (2026-06-30), through the real automated pipeline — reproducible, not by hand. The pipeline is the generic structure-detect → config-driven extract → tie-out-judge described in §13, run end-to-end (not a bespoke script) against 13 properties across 8 structurally-different source formats in one workbook (property names anonymized; the roster lives in the gtm repo):

  • Asset Living / MRR / Ross / Ridge — four Yardi box-score variants: occupied + vacant + notice + model + down + admin = units holds to the unit on every row; occupancy reproduces the published % Occ to the basis point.
  • Citrus Glen — a label/value scalar summary, no table at all — read via the new keyValue block kind (§13), same canonical schema as the tabular formats.
  • Orchard Walk — a single-row portfolio rollup.
  • Eagle Villas — a weekly time-series — the engine selects the row matching the report period.
  • The Ashleytwo per-floor-plan box-score tabs, summed, plus a separate financials tab.
  • Cross-tab tie-out — all 8 formats → the operator's own published Weekly Report, same week (the real proof): 155 of 159 comparable fields tie to the cent, 0 unexpected mismatches, overall tiedOut = true. A generated Weekly Report .xlsx built from the parsed data reproduces 155/182 cells to the cent; every remaining cell is honestly classified — 5 mismatches + 22 source-gaps — never silently coerced to a plausible-looking number.

Honest exceptions — surfaced, not hidden (live evidence for the hold-for-review design, G7):

  • Eagle Villas: its bespoke weekly tracker's delinquency does not reconcile to the Weekly Report — off by a consistent +$1,721.61 on both aging lines. Held for human review; never emitted as a silent number.
  • Dahlia Village: the Weekly Report's payables value equals Salt River Flats' exact payables, and Dahlia's own source has no A/P line at all — a probable copy error in their sheet, flagged rather than guessed at.
  • Orchard Walk: the Weekly Report stores a rounded occupancy (0.9314) against our more-precise derived value (0.931373) — judged to the Weekly Report's own stored precision rather than flagged as a mismatch.

This is the central thesis (§9) proven end-to-end on real files through the real pipeline — reproducible, not asserted — and it only works once the real Model/Down/Admin columns (and, for keyValue sources, the real labels) are present. The earlier scrubbed fixtures had tokenized headers away and were a different week than the answer key, which is exactly why an intermediate run once appeared to fail every real file — tying every source to the same-week Weekly Report closes it (see §6, test-fixture hygiene).

3b. Reverse-engineered target — the Weekly Report output spec

The Weekly Report tab (their literal output) stacks ~26 weekly blocks top-to-bottom (newest first = built-in history). Each block:

Col Field Source
Units units manager tab
Occupancy =(Units−Vacant)/Units derived
Vacant Units vacant manager tab
Leased Units leased manager tab
NTV ntv manager tab
Move-Ins / Move-Outs moveIns / moveOuts manager tab
Net Leased =ROUND(Leased+(Occ×Units)−NTV,0) derived
Net Occupancy =NetLeased/Units derived
TOTAL / 0-30 Days / 30+ Days =0-30 + 30+ / delinq0_30 / delinq30plus derived / manager tab
Cash / Payables cash / payables manager tab

4. Open research questions (the fleet is filling these)

  1. ▢ OSS ingestion/parsing toolkits (xlsx / email / PDF) — reuse vs build.
  2. ▢ Deterministic-first + agentic-escalation patterns; structured-output + self-repair; consistency/eval harnesses; format-drift resilience.
  3. ▢ AI-native ingestion platforms (build-vs-buy) and fit for a per-customer PM SaaS.
  4. ▢ Temporal + HITL patterns for document pipelines — is Temporal warranted here, or lighter-weight?
  5. ▢ Asset-manager domain + MCP interface design (what AMs analyze, report contents, tool shapes).
  6. ▢ Existing PropFlow email pipeline — strengths / weaknesses / what's reusable (inspection).

5. Decision (validated by the research + pipeline-inspection fleet, 2026-06-30)

Build ~90% on PropFlow's existing primitives — extend, don't greenfield, and don't buy a document-AI platform. The deterministic→LLM→Vision parsing cascade, sender-trust + idempotency + resilience infra, and the auto-publish-vs-HITL gate already exist and transfer directly. Reuse map by layer:

Layer Posture Existing asset
0 Intake + sender trust + dedupe reuse as-is api/email/ingest/route.ts, trust-data-sender.ts (yardi.com already trusted), detect-data-attachment.ts, idempotency.ts
1 Deterministic raw parse fork + harden properties/reports/weekly-parser.ts (closest precedent — untested, hard-coded to one wording; must generalize to per-manager profiles + golden tests), generic-csv.ts
2 Agent-escalation fallback reuse + upgrade rent-roll/registry.ts cascade + llm-fallback.ts → move onto Anthropic Structured Outputs / strict tool use; pdf-vision.ts only if PDF in scope
3 Canonical schema + correctness gate NET-NEW (small) none — new Zod PortfolioWeeklyReport, fail-loud to review
4 Cross-property reconciliation/aggregation NET-NEW none (no portfolio concept exists); model on entity-delta.ts
5 Intelligence layer reuse pattern extractOperationalSignal; every claim cites a validated field (no-invented-numbers)
6 Report renderer (operator's literal format) NET-NEW (thin) SheetJS (already in stack); fill their template, not a generic engine
7 HITL review + publish gate reuse pattern auto-sync-policy.ts + admin email-ingestion review UI
8 Durable orchestration reuse Temporal src/lib/temporal/ — Phase 1 (below)
9 Eval + observability reuse scaffold evals/golden + promptfoo; Langfuse tracedClaudeCall

Only 3 net-new pieces, all small: canonical schema (L3), reconciliation engine (L4), report renderer (L6).

Sequencing:

Build-vs-buy: do not buy Docling/Unstructured/LlamaParse/Sensible/Extend — none beats what we have, and the real differentiator (Yardi semantics + cross-manager reconciliation + the operator's format + no-invented-numbers intelligence) isn't purchasable. Only possible buy: narrow OCR fallback (AWS Textract, already in our stack) iff PDF is in scope — and the real files are multi-sheet xlsx/tabular, so PDF is likely out and no purchase is needed.

5a. Honest current-state correction (code-verified 2026-06-30)

A deep code read corrected the optimistic "~90% built" framing. Reuse is narrower and more specific than a layer table implies — and the component whose name matches our need is the weakest. The regulated-correctness core is genuinely net-new and hard.

Net: reuse the fail-soft ledger + the detection/testing shapes; design the canonical weekly-report schema, the 6 per-manager adapters, cross-property reconciliation, and a tabular invariant suite from scratch. That core is the build — and it's where the regulated stakes live, not in the plumbing.

5b. Newer PropFlow patterns to draw on — and the net-new-hard list (code-verified)

The email pipeline is one of the older parts. Newer subsystems offer better inspiration, assessed honestly:

Transferable disciplines (adopt deliberately):

Hard prerequisite if we use Temporal (learned the painful way): ADR-0052 — a deploy missing a patched() guard wedged every in-flight workflow for 13 days, silently, zero alerts. Fix = a replay-corpus CI gate (replays PII-scrubbed prod histories against new code). For a regulated weekly pipeline, build that gate day one. Temporal here is production-partial, not mature (4 real incidents in 7 weeks; flagship customer still holdover-only).

False friends — do NOT import:

Genuinely hard & net-new — nothing in the codebase solves it for us:

  1. A registry that confidently dispatches among several competing structured parsers (6 manager formats), not "one structured parser + AI fallback." Rent-roll registry is a skeleton, unproven at our scale.
  2. A batch / portfolio-wide "workflow per weekly ingestion run" — everything on Temporal today is reactive/per-entity.
  3. A field-level, immutable, retention-compliant audit store for HUD/LIHTC — TraceCollector doesn't meet this bar.
  4. A hard pass/reject extraction bar. Sobering proof it's hard: the closest existing analog — turnover dictation extraction — still has most of its 27 adversarial failure classes RED after sustained investment, and its dominant bug is exactly ours (multi-item value→line-item misassociation). Their tolerance model ("mostly right, PM catches it at recap") is invalid for LIHTC reporting, which has no per-number human confirmation step.

Honest stakes picture: the platform gives a running start on plumbing and a library of hard-won correctness disciplines — but reliable structured extraction from messy multi-format input is hard even for PropFlow with months of investment, and this regulated bar is higher than anything shipped. That is the build, and it's why it's a retainer, not a weekend.

6. Consequences / risks

7. Open decisions for Fede (business calls, not engineering defaults)

  1. Reconciliation conflict policy — when managers overlap on a property with conflicting numbers: one authoritative manager, or any conflict → HITL? (recommend HITL for pilot)
  2. Late-manager SLA — publish a partial portfolio report, or hold + escalate? (recommend hold + timer escalation)
  3. Scope — client-specific config, or the first instance of a general "portfolio report" product for all customers? (determines whether schema + manager-profiles are multi-tenant from day 1)
  4. MCP access model — read-only against approved reports (recommended), or also in-flight/pending-review data?
  5. Pilot mailbox — shared clara@ SES (reuse plumbing) or dedicated client alias (cleaner blast radius for a high-correctness customer)?
  6. Resolved by the real files: input format = multi-sheet xlsx/tabular, not PDF → no OCR purchase needed for the weekly report. (Utility PDFs are the separate use case.)

8. Scaling across clients & formats without hardcoding forever (the key scaling answer)

The worry: every client/property has a different format — hand-coding 6 adapters per client doesn't scale to N clients. The answer is a growing template-signature registry + a rule-harvesting loop, not infinite bespoke parsers:

⭐ This is now formalized as the platform vision — see ../architecture/clara-ingestion-platform.md. The north star: a self-authoring parser-config engine (the platform LEARNS and stores a parser config per source — data, not code, authored by the platform, never an engineer), driven by a two-mode flow (onboarding learns once → steady-state email path reuses), with input/output symmetry (learn the client's output template too) and a period-keyed completeness gate (no master report until every property for the period validates). Correction to earlier framing: we do NOT hand-code N adapters; PR #2740's box-score adapter is a Phase-0 stand-in for a learned config. The gate stack, canonical schema, and grounded intelligence carry over unchanged.

9. The hard semantic gate — reconciliation tie-outs (expands G5/G6)

Structural correctness ("did I read the cells right?") and semantic correctness ("do the numbers tie?") are separate gates; both must pass before the reasoning layer sees anything. Tie-outs:

10. LLM-checker discipline (use AI without it lying to us)

A stochastic checker validating a stochastic extractor gives correlated errors dressed as confidence. Rules:

11. Provenance, idempotency, time (expands G9 — and the regulated-grade audit store)

12. Landscape & build-vs-buy (externally validated)


13. How the engine learns a format · the verification layers · onboarding (the reviewer's three questions)

The three questions a reviewer asks — how does it learn a format, how is it verified, how does a client onboard — answered end-to-end. (Full spec: companion ../architecture/clara-ingestion-platform.md.)

1 · The engine that learns the format (no hard-coded parser). Format handling is learned by the platform, per source, and stored as a reusable config — data, not code, never authored by an engineer per client. On an unseen file:

2 · The multiple layers of verification (the gate stack, §3a — proven on real data above). The learned mapping only ever decides which column = which field; it never emits a number — values are read deterministically from the raw cells, so same input → same output. Then the ladder: G2 sanity/confidence → G5 hard reconciliation (the full unit identity + aging tie-outs, verified exact across the 8-format/13-property automated run, §3a) → G6 cross-property reconciliation → G7 human-in-the-loop for anything not fully green → G8 grounded intelligence (every number cites its source field; no invented numbers) → G9 immutable audit + golden-eval regression. Governing rule: produce no number rather than a wrong one.

3 · Onboarding — two modes.