The biggest finding is new: since roughly February 2026 the ingest has been silently discarding every hotel but one for taxpayers that file multiple properties on one account. 149 STR-matched hotels went dark while their owners kept filing (verifier SELECT, 2026-07-19). Everything downstream of that, contamination fixes, handover stitching, compset math, has been polishing the survivors. The distance to launch-grade is short because the failures are specific and upstream-concentrated, but the gate that proves a month of data correct does not exist yet, and it goes first.
A daily 6 AM job on the Mac Mini logs into the Texas Comptroller SIFT portal, lists all published months, and re-downloads every month back to 2025-01, absorbing amendments within 24 hours as a side effect. 63 periods exist; 16 are listed and processed; the rest are frozen.
A hand-rolled CSV parser maps the 23-column filing format. Rows are dropped for malformed shapes (some counted, some not), numbers are force-coerced, and the outlet sequence column is read into documentation and discarded in code.
Records collapse to one row per taxpayer per month (the G1 defect), get an STR identity stamp from a location-keyed lookup, and upsert into hotel_tax_receipts. No deletes, ever. Materialized views refresh after.
hotel_tax_summary rows carry each taxpayer's identity and STR link; ownership changes are stitched as predecessor/successor chains (48 PropCo/OpCo pairs are encoded as mutual cycles). Four stamping layers plus a nightly align job try to keep receipts, chains, and summaries agreeing.
Compset leaderboards, monthly audits, tract peers, teasers, GM emails, discovery, and deal intake all read receipts keyed through the chain walk. RevPAR is receipts divided by rooms and days, over the last twelve FILED months (the G4 defect).
A 7 AM watchdog checks freshness and sentinels and writes a local status file read by the weekday ops brief on the same machine. Discord alerting has been dead (401) in every retained attempt. A detector fleet files issues into a queue with no reader.
Andrej F1 (partial: numbers refined) + ChaseAI F1 (confirmed)
The database key is one row per taxpayer per month, but corporate masters file one return per hotel. The parser documents the outlet column (sift-sync.mjs:312) and never reads it; a keep-last dedupe (sift-sync.mjs:419-425) throws away every outlet but one. Verified live: 149 STR-matched hotels have receipts in 2025-12 and none in 2026-05 while their taxpayer still files; 134 multi-address masters summed $98.8M in 2025-12 vs $29.7M in 2026-05; the 2026-05 file parsed 13,304 records and wrote 7,082. Running since roughly 2026-02. Bonus defect: the surviving row roulettes between different physical hotels month to month while carrying one STR stamp, re-manufacturing the identity contamination class fixed on 2026-07-15.
Andrej F2 + ChaseAI F2 (both confirmed)
The 2026-06 file parses to 0 records on every run since 2026-07-06 and the database holds zero 2026-06 rows; nothing fired. There is no comparison of any period's row count or dollar total against anything: not the prior file version, not the neighboring month, not a floor. Short rows are skipped without a counter (sift-sync.mjs:317). The Zod validation layer is decorative: parseFloat||0 pre-coerces every number, and zodFailures is 0 in every retained log.
Andrej F4 + ChaseAI F3 (both confirmed)
Discord notify: 68 attempts in the retained log, 68 failures, all 401, zero successes. The watchdog writes a local JSON read only by the weekday interactive ops cockpit running on the same Mac Mini as the jobs it watches; its rollup has read STALE/FAILED four days running with no page. Precedent: May 2026, 20 consecutive FATAL runs before a human noticed. A powered-off Mini silences the failure and the failure detector at once.
Andrej F3 (partial: count refined to 28 active spanning 15+ months)
The TTM view takes the last 12 months a hotel FILED, with no span check (mig 118). 28 active STR-matched hotels ship an estimated RevPAR computed over windows of 15 months to 6.5 years, including repaired sentinel Embassy Suites Hughes Landing (window 2024-09 to 2026-05, live RevPAR 200.77). These numbers reach external surfaces: teasers, GM emails, owner one-pagers. The fix is one line of arithmetic: window span must equal 11 months or the number does not ship.
Andrej F6 + ChaseAI F4 (confirmed) + ChaseAI F7 (partial, verifier found a live defect)
The monthly audit counts a month complete only when every historical filer files (compset-engine.ts:803-807), so one dead taxpayer link freezes an entire market's external reporting at the pre-sale month. Separately, the CI receipt-read gate allowlists useCompsets.ts, the exact dual-brand-blind reader shape the gate exists to stop, and the verifier caught it live: the HOURP compset renders Hyatt Place with the combined Hyatt Place + Hyatt House return, RevPAR inflated roughly 1.9x ($237 vs $125, verifier SELECT 2026-07-19).
| ID | Severity | Gap |
|---|---|---|
| G1 | CRITICALLB | The ingest silently discards multi-outlet filings Andrej F1 (partial: numbers refined) + ChaseAI F1 (confirmed) The database key is one row per taxpayer per month, but corporate masters file one return per hotel. The parser documents the outlet column (sift-sync.mjs:312) and never reads it; a keep-last dedupe (sift-sync.mjs:419-425) throws away every outlet but one. Verified live: 149 STR-matched hotels have receipts in 2025-12 and none in 2026-05 while their taxpayer still files; 134 multi-address masters summed $98.8M in 2025-12 vs $29.7M in 2026-05; the 2026-05 file parsed 13,304 records and wrote 7,082. Running since roughly 2026-02. Bonus defect: the surviving row roulettes between different physical hotels month to month while carrying one STR stamp, re-manufacturing the identity contamination class fixed on 2026-07-15. |
| G2 | CRITICALLB | No month-close eval: a zero-row month ran green for 13 days Andrej F2 + ChaseAI F2 (both confirmed) The 2026-06 file parses to 0 records on every run since 2026-07-06 and the database holds zero 2026-06 rows; nothing fired. There is no comparison of any period's row count or dollar total against anything: not the prior file version, not the neighboring month, not a floor. Short rows are skipped without a counter (sift-sync.mjs:317). The Zod validation layer is decorative: parseFloat||0 pre-coerces every number, and zodFailures is 0 in every retained log. |
| G3 | HIGHLB | Alerting is dead end to end, and everything shares one machine Andrej F4 + ChaseAI F3 (both confirmed) Discord notify: 68 attempts in the retained log, 68 failures, all 401, zero successes. The watchdog writes a local JSON read only by the weekday interactive ops cockpit running on the same Mac Mini as the jobs it watches; its rollup has read STALE/FAILED four days running with no page. Precedent: May 2026, 20 consecutive FATAL runs before a human noticed. A powered-off Mini silences the failure and the failure detector at once. |
| G4 | HIGHLB | TTM RevPAR is not trailing-twelve-months for 28 live hotels Andrej F3 (partial: count refined to 28 active spanning 15+ months) The TTM view takes the last 12 months a hotel FILED, with no span check (mig 118). 28 active STR-matched hotels ship an estimated RevPAR computed over windows of 15 months to 6.5 years, including repaired sentinel Embassy Suites Hughes Landing (window 2024-09 to 2026-05, live RevPAR 200.77). These numbers reach external surfaces: teasers, GM emails, owner one-pagers. The fix is one line of arithmetic: window span must equal 11 months or the number does not ship. |
| G5 | HIGHLB | Compsets freeze on one quiet filer, and the leaderboard is dual-brand blind, live Andrej F6 + ChaseAI F4 (confirmed) + ChaseAI F7 (partial, verifier found a live defect) The monthly audit counts a month complete only when every historical filer files (compset-engine.ts:803-807), so one dead taxpayer link freezes an entire market's external reporting at the pre-sale month. Separately, the CI receipt-read gate allowlists useCompsets.ts, the exact dual-brand-blind reader shape the gate exists to stop, and the verifier caught it live: the HOURP compset renders Hyatt Place with the combined Hyatt Place + Hyatt House return, RevPAR inflated roughly 1.9x ($237 vs $125, verifier SELECT 2026-07-19). |
| G6 | HIGH | Two stamping regimes with incomplete arbitration Andrej F5 (partial: nightly align job does arbitrate) The daily ingest mass-recomputes STR stamps for all 16 listed periods from the summary map, including overwriting to null, while doctrine says the chain is truth and enrichment is fill-only. A nightly align job (migs 249/251) does copy chain into summary, but 183 address-mismatch residuals and mismatched keying (era-less vs era-keyed) keep the regimes divergent, and frozen periods (2025-04, 2025-05, all pre-2025) live under the opposite regime forever. |
| G7 | HIGH | The pipeline verifies itself; independent ground truth covers 1.4% ChaseAI F5 (confirmed) Enrichment fills stamps from a chain built from the same receipts; sentinels are 8 hotels; the one cross-source check (STR report reconciliation) covers 55 subjects of roughly 4,056 STR-matched hotels. For the other 98.6%, the second set of eyes is a customer. Both recent gap classes were found by Ace noticing a wrong number on a screen. |
| G8 | MEDIUM | The detector queue has no reader: 1,366 open issues Andrej F8 + ChaseAI F6 (both confirmed) data_quality_issues holds 1,366 open rows across 15 issue types, oldest open since 2026-05-09, with zero consumers: no UI, no brief, no report. The handover detector shipped 2026-07-19 writes into the same void. Detection without a reader is a diary. |
| G9 | MEDIUM | Live credentials in git and in a stored cron command Andrej F9 + ChaseAI F8 (both confirmed) The SIFT portal login and a Discord bot token are hardcoded in the committed ingest script (values not reproduced here), service-role keys sit in several repair scripts, and one of 19 cron jobs embeds a bearer token in its stored command. The SIFT credential is in git history, so the fix is rotation, not deletion. Violates the standing SECRETS.md rule. |
| G10 | MEDIUM | Amendments add but never remove; zero is unrepresentable; old periods frozen Andrej F10, F12 + ChaseAI F10 (confirmed) Upsert-only ingest means withdrawn or zeroed filings persist forever (2025-01: 7,592 DB rows vs 6,817 in the current file). Filed-zero and did-not-file are indistinguishable downstream. Periods the Comptroller stops listing (2025-04, 2025-05) and everything pre-2025 can never receive amendments. |
| G11 | MEDIUM | No creation path for new taxpayer identities: 442 filers invisible Andrej F7 (confirmed) 442 of 26,522 receipt-filing taxpayers have no hotel_tax_summary row because the refresher became UPDATE-only, so they cannot be stamped, browsed, or matched. A new-build hotel opening next month has no path into the product. The nightly refresher is officially deprecated while being load-bearing. |
| G12 | MEDIUM | A health check asserts checks it never ran ChaseAI F9 (confirmed) checkStrCollection promises five signals, implements one, and prints a green line claiming no discarded or held files for checks that never executed (pipeline-health-check.mjs:376-406). A watchdog that lies green is worse than no watchdog. |
| G13 | MEDIUM | Production schema and git history disagree ChaseAI F11 (confirmed) Migration 271 and the handover repair scripts run in production but are uncommitted. For a one-machine shop the repo is the disaster recovery plan; a clone cannot reproduce the running schema. |
| G14 | LOW | Doc drift and scar tissue Andrej F11 + ChaseAI F12 (confirmed) The ingest header says monthly-on-the-10th, the cron says daily. Mig 251's root-cause comment blames a function containing no stamp logic. Two legacy matviews refresh nightly with zero product consumers. 296 migrations and about 15 repair scripts of careful downstream scar tissue sit on top of an upstream key that cannot represent the data. The hand-rolled CSV parser already produced one 742-row bug class; csv-parse exists. |
I built this pipeline, so this review is the one where I stop grading my own homework and reconcile against two outside sets of eyes. Andrej and ChaseAI each ran independently against the live repo and database; 24 findings came back, 20 verified CONFIRMED and 4 PARTIAL by adversarial re-check, 0 refuted. I re-read their evidence rather than my assumptions. They are right, including about the work I shipped this week.
Every incident we have had is a projection of a single decision: the pipeline's identity spine is the Comptroller's taxpayer number, and we treat it as if it were a hotel identifier. It is not. It is a tax-entity identifier that is:
Contamination (2026-07-15), handovers (2026-07-19), outlet collapse (found today): three surface expressions of the same mistake. I have been repairing projections downstream instead of fixing the model upstream. The fix plan below starts at the model.
Detection without a reader is a diary, and I wrote a lot of diary this year:
The pattern: I keep adding detection capability and never closing the loop to a human who acts. One alarm that reaches a phone beats fifteen detectors feeding a table.
1. Repair-script culture. 296 migrations, ~15 one-shot repair scripts, five stamping layers. Each incident got a careful fix and a guardrail for THAT class, never the invariant that would catch the class family. Andrej's phrasing is the keeper: the invariants were never made executable. 2. The gate with a door in it. The CI receipt-read wall I shipped on 7-18 allowlists useCompsets.ts, which is exactly the dual-brand-blind reader shape the wall exists to stop (check-receipt-reads.mjs:18-22). A gate with a grandfather clause is a to-do list, not a gate. 3. Prod/git drift. Mig 271 and the repair scripts run in prod and sit uncommitted in my working tree. For a one-Mini shop the repo IS disaster recovery. Committing is part of shipping; I know this and skipped it under the autonomy of a Saturday session. 4. Two stamping regimes with no arbitration (Andrej F5, the one that surprised me most): the daily ingest mass-recomputes str_number for all 16 listed periods from the summary map, including overwriting to null, while the doctrine since 7-15 says the chain is truth and enrichment is fill-only. Both regimes are mine. I wrote the doctrine and then left a daily job contradicting it. That is the next contamination incident if left alone.
P0, launch gates, nothing ships to a customer before all four are green: 1. Put the outlet in the key. Ingest reads outlet_seq, constructs the _O-suffixed taxpayer id that mig 116's convention already defines, drop the keep-last dedupe, backfill --since 2026-01, then re-run enrichment for the restored rows. Restores the ~149 dark hotels and retires the outlet-roulette mis-stamping vector. 2. Month-close eval. Deterministic PASS/FAIL per period after every ingest: parsed vs written rows AND dollars, current vs prior file version, vs neighboring month, listed-month-zero-rows is an automatic red. Andrej's design is correct as written; I will implement it to his spec. 3. An alert path that does not share fate with the Mini. Rotate the dead Discord token (it is in git history, so rotation not deletion), or replace with a cloud-side notifier; watchdog and month-close both deliver there; a missed heartbeat is itself an alert. 4. TTM span gate. Window span must equal 11 months or the RevPAR does not ship to any external surface (28 live violators today, incl. one repaired sentinel, Andrej F3 verified).
P1, the week after: 5. DQ queue gets a reader: a triage section in the morning brief plus a weekly drain cadence with an owner (me), or the queue gets deleted honestly. 6. One stamping writer. Ingest becomes fill-only; the summary-map mass-restamp dies; a convergence check runs nightly and its failure pages. 7. maxFilers tolerance in compset-engine (one dead link must not freeze a market's external reporting). 8. Commit mig 271 + repair scripts; add a schema-drift check (prod migrations vs repo) to the health check. 9. Secrets: SIFT portal credential and Discord token out of the repo into Vaultwarden/.env, rotate both, scrub the cron.job command carrying a bearer token.
P2, structural: 10. Summary-row creation path for new taxpayers (the new-build-hotel case). 11. Retire scar tissue: legacy matviews with zero consumers, the deprecated-but-scheduled tombstone, the dead --fix path, the stale headers. 12. Replace the hand-rolled CSV parser with csv-parse. 13. Model PropCo/OpCo pairs explicitly instead of mutual-cycle hacks.
Not launchable on this data today, and I say that as the person most motivated to say otherwise. The four P0 items are each small (the outlet fix is the largest and it is days, not weeks, of work, estimate mine, unverified until scoped). The distance to launch-grade is short precisely because the failures are specific, verified, and upstream-concentrated. What was missing was never effort; it was an eval. That is now the first thing we build, not the last.
I re-read both advisor reviews in full against their cited evidence, the verifier verdicts (20 CONFIRMED, 4 PARTIAL, 0 refuted), the six stage maps, scripts/sift-sync.mjs (the dedupe, key, and stamping paths), and my own session notes from 7-15 and 7-19. I did not independently re-run their SQL this session; where a number appears above it carries the reviewer's citation and the verifier's verdict. The PARTIAL corrections are reflected in the unified register, not glossed: Andrej F1's 47% is the file-level collapse ceiling, the DB-verified floor is 134 multi-address masters at $98.8M (2025-12) vs $29.7M (2026-05); F3's precise count is 28 active STRs with window span of 15+ months (36 unfiltered); F5's "no arbitration" overstates, align_summary_str_to_chain (migs 249/251) does arbitrate nightly, but 183 address-mismatch residuals and the era-less ingest key keep the two regimes divergent; ChaseAI F7's core claim held and its verifier went further, catching a live defect: the HOURP compset leaderboard renders Hyatt Place with the combined Hyatt Place + Hyatt House return, RevPAR inflated ~1.9x ($237 vs $125, verifier SELECT 2026-07-19). A verification pass that finds a NEW live bug is the strongest argument for keeping verification adversarial.
You asked whether this can carry a launch. It cannot, yet. Not because the engineering is lazy, it is not, the repair migrations are careful work. It is because the pipeline has no eval. There is no computed pass/fail anywhere for the sentence "this month of data is correct." Every gap class you have hit weekly (contamination 2026-07-15, handovers 2026-07-19) was found by a human noticing a weird number on a screen. That is works.any(). A product is works.all(), and works.all() is only reachable through an explicit, boring, deterministic month-close check that does not exist. I verified everything below myself against the repo and the live DB unless marked otherwise.
The schema key is UNIQUE(taxpayer_number, report_period) (supabase/migrations/001_initial_schema.sql). The parser documents outlet_seq as field 8 (scripts/sift-sync.mjs:312) and then never reads it (record construction at sift-sync.mjs:348-363). The pre-upsert dedupe keeps the LAST row per (taxpayer, period) (sift-sync.mjs:419-425). So a taxpayer that files one return per hotel loses every hotel but one.
Live numbers:
This is also the contamination class you just declared fixed, recurring. The surviving 12016274339 row was at 1311 AIRPORT COMMERCE DR in 2025-12 and 12121 COIT RD in 2026-05, both stamped str_number 70253 (SELECT this session). 70253 is Extended Stay America Austin Airport at 1311 Airport Commerce Dr (str_census SELECT). So the Austin ESA's STR identity is receiving a Dallas hotel's revenue, every month, right now, feeding TTM and every surface downstream. The 2026-07-15 fix made stamping location-grained; the ingest then physically discards the location grain before stamping can use it. A guardrail behind a data model that cannot represent the truth is decoration. Note mv 248's tripwire (vw_duplicate_suffixed_receipts, 0 rows today) watches the opposite failure, double-counting, and is blind to this one.
The 2026-06 file has parsed "0 records, 0 upserted" on every run since it appeared (logs/sift-sync.log; results entry {'period': '2026-06', 'records': 0, 'upserted': 0} in logs/sift-sync-2026-07-19.json) and hotel_tax_receipts has zero 2026-06 rows (period SELECT this session). No alert of any kind. There is no comparison of a period's row count or dollar total against the prior month, the prior file version, or any floor, anywhere in sift-sync.mjs (whole file read). Rows with fewer than 23 fields are skipped with no counter (sift-sync.mjs:317), so a Comptroller column removal would produce a plausible-looking run ingesting nothing. Zod is decorative: parseFloat(...)||0 at lines 326-327 guarantees types before Zod sees them; zodFailures is 0 in every retained run log. The nearest guards are the watchdog's 45-day insert-age check (fires after six weeks of total silence), an STR-vs-SIFT ratio check covering only the ~100 subjects with STR reports, and 8 sentinels (scripts/pipeline-health-check.mjs). Roughly 4,000 STR-linked hotels have no check at all. Assume it fails silently, because it did: May 2026, 19 consecutive FATAL runs, caught on day 20 (pipeline-health-check.mjs:17-18, log grep).
mv_str_ttm_revenue takes each STR's last 12 DISTINCT report_periods via DENSE_RANK (migrations/118_ttm_window_per_str.sql:23-39) with no span constraint. 28 live STRs pass months_covered >= 11 while their window spans 15+ calendar months (SELECT over mv_str_ttm_revenue, this session). Concrete: Comfort Suites Alamo Riverwalk (57045), "TTM" window 2019-11 to 2026-05, six and a half years, months_covered 12. Embassy Suites Hughes Landing (64490), one of your eight repaired SENTINELS, window 2024-09 to 2026-05, 21 months, $15,014,030 total, live estimated_revpar 200.77 (mv + str_hotel_summary SELECT). The sentinel check watches day-over-day jumps >15% (pipeline-health-check.mjs:274-318), so a stably wrong level never trips it. estimated_revpar divides by months_covered * 30.4 (mig 105), so a filer with catch-up or quarterly lumps in the window inflates, and a gap-filled window mixes 2024 revenue into a 2026 number. The DB has 5,791 taxpayers filing only in quarter-end months vs 6,565 monthly filers in the 2025-07..2026-05 window (SELECT this session), so lumpy filing is a population, not an edge case. deal-compset's ttm_months_covered >= 11 guard (deal-compset/index.ts:300 per stage map) passes all 28. These numbers go to external audiences: teaser pages, emailed owner one-pagers, unmasked GM emails (teaser-page/index.ts, gm-market-audit-emails.ts, per stage map and commit fc44f15). The missing eval is one line of arithmetic: window span equals 11 months or the number does not ship.
Discord notify: 68 failures, 0 successes in the retained log (grep -c "Discord notification failed" / "sent", logs/sift-sync.log, this session), every one a 401; the failure is console-logged only (sift-sync.mjs:596-603). The watchdog pushes nothing: it writes ~/.revparpro/last_pipeline_status.json and stdout (pipeline-health-check.mjs:428-436, no webhook or mail code in the file). Its only reader is the interactive weekday ops cockpit (agents-runtime/skills/ops/SKILL.md:131-133, per stage map). The rollup has read STALE/FAILED four days running on known-open items (logs/pipeline-health.log tail), which is how ok:false stops being a signal. The STR-collection check promises five signals in its comment and implements one; the fetched state/ignored/held/failed results at pipeline-health-check.mjs:386-392 are never referenced, and the green-path note claims "no discarded or held files" for checks that never ran (verified, lines 370-408 read this session). And everything, jobs, both watchdogs, and their only reader, runs on one Mac Mini; a powered-off Mini silences the failure and the failure detector simultaneously. The only off-machine signal is Ace noticing a missing email.
Doctrine since 2026-07-15: the chain is the source of truth, enrichment is fill-only, wrong non-null stamps are corrected only by hand (mig 245/251; vault doc). The stage map repeats it: "a WRONG non-null stamp is never corrected by any nightly job." That claim is wrong, and the truth is worse: the daily ingest recomputes str_number for every record in all 16 listed periods from the hotel_tax_summary lookup and includes it in the upsert (sift-sync.mjs:404-410, 439-448; resolve() at 145-154 returns null on no match). So every morning is a full mass-restamp of 16 months keyed off the summary map, overwriting, including overwriting to null. Consequences: hand repairs to receipt stamps in listed periods survive only while summary stays aligned with the chain; the 183 residual summary-vs-chain disagreements (vault doc, open verification) are re-stamped daily with the summary's value; and periods the Comptroller stops listing (2025-04, 2025-05, frozen at 7,616 and 7,614 rows per my period SELECT) plus everything pre-2025 (DEFAULT_SINCE at sift-sync.mjs:79) live under the opposite regime, frozen forever. Two regimes, no arbitration statement, no test that they converge. The 2026-07-15 oscillation (restamp stripped 5,890, enrich re-applied 3,625 in 9 minutes, vault doc) was exactly this shape; the convergence proof holds only while summary equals chain, and nothing gates on that.
buildMonthlyAudit keeps only periods where filer count equals the historical max (compset-engine.ts:803-807, read this session). An unlinked successor permanently reduces the count, so completePeriods stops at the pre-sale month for the whole compset, for the GM email, the Market Audit tab, and the public teaser. The 77-hotel handover class from 2026-07-19 was this. The detector (mig 271) now exists but has zero cron executions so far (map's cron.job_run_details SELECT) and is uncommitted (git status: ?? supabase/migrations/271_unlinked_successor_detector.sql, ?? scripts/repair-handover-gaps.mjs, verified this session), so prod schema and git history disagree. Separately, useCompsetMonthlyLeaderboard is allowlisted for raw receipts (check-receipt-reads.mjs:13-26, read this session) without combo awareness or overlap guarding (useCompsets.ts:1862-1875 chain sum, read this session): the dual-brand and double-count classes the engine was rebuilt to kill are still live in the customer-facing leaderboard.
sync_summary_from_receipts is UPDATE-only (migs 041/101 per stage map) and sift-sync writes only hotel_tax_receipts (scope lock, sift-sync.mjs:433-438). Verified live: 442 of 26,522 distinct receipt taxpayers have no hotel_tax_summary row (SELECT this session), so they cannot be stamped by pass 2, cannot appear in taxpayer browse, and only the handover stitcher can ever create them. A new-build hotel opening next month has no path into the product. Also the nightly refresher is officially DEPRECATED (mig 177) while being load-bearing (migs 233/251 schedule it), and is_canonical means "is a THM property" while the schema comment says dedupe flag (mig 038b vs 001:300). Legibility debt like this is where next month's gap class comes from.
data_quality_issues has 1,366 open rows across 15 issue types (SELECT WHERE resolved_at IS NULL, this session), oldest open since 2026-05-09, and zero consumers: no UI (RLS service_role only, mig 149), no brief, no report; every existing read is a writer's own dedup/throttle (stage map greps, spot-consistent with mig 149 and coord-audit code). chain_integrity 131 and dual_brand_split_mismatch 144 are standing debt in the exact subsystems that produced the last two incident classes. A queue nobody drains is a log, not a queue.
Credential present at scripts/sift-sync.mjs:74-75 (SIFT portal login) and 76-77 (Discord bot token), committed at HEAD (git show HEAD grep, this session), plus service-role and vendor keys in other scripts per the stage map (match-str-to-tax.mjs:41-43, dual-brand-setup.mjs:28-32). The dual-brand-refresh-monthly cron job embeds a JWT in its stored command (SELECT jobname, command LIKE '%Bearer ey%' FROM cron.job, this session: true for exactly that job). This violates the standing SECRETS.md rule, and the SIFT credential is now in git history, which means rotation, not deletion.
Rows with total and taxable both zero are dropped at parse (sift-sync.mjs:328) and there is no delete pass (upsert-only, 439-448), so a filing the Comptroller zeroes or withdraws persists forever: 2025-01 holds 7,592 rows in the DB vs 6,817 produced by the current file (period SELECT vs run JSON). "Filed zero" and "did not file" are also indistinguishable downstream, which corrupts the pending-vs-closed logic every compset surface leans on.
I checked the mapping agents' claims where I relied on them. Two corrections: (a) "a wrong non-null stamp is never corrected by any nightly job" is false for the 16 listed periods, the daily ingest overwrites all stamps from the summary map (Finding 5); (b) the Marriott 2025-12 figure "$1,203,008.84" is the 10 outlet rows only, the full 11-row total is $1,389,178.85 (SELECT this session), phrased ambiguously enough to mislead. The rest of what I re-tested held: 48 mutual successor cycles, 442 missing summary rows, 0 str orphans, 1,366 open DQ rows, 0 duplicate-suffixed rows, Discord 68/0 (all SELECTs and greps this session).
Every incident so far is the same failure: an invariant the product depends on (one row per hotel-month, stamps match buildings, TTM means twelve months, chain edges exist at sales) was never written down as a computed check, so it degraded silently until a human saw a wrong number. You do not fix that class by fixing each instance. You fix it by making the invariants executable. The next class is already visible from here: window-span integrity (Finding 3) and the two-regime stamping arbitration (Finding 5). I would bet on one of those two being the incident that a customer, not Ace, finds first.
Build the month-close eval: a deterministic gate that runs after every ingest and emits PASS/FAIL per period, off-machine, with hard checks: (1) rows parsed vs rows written, with dollars for both sides, so the 6,222-row / unmeasured-dollar discard in the 2026-05 file (logs/sift-sync-2026-07-19.json) becomes a number instead of a mystery; (2) rows and dollars vs the prior version of the same file and vs the neighboring month, so a 0-record published month (2026-06, live today) or a 47% collapse is a red page, not a green log line; (3) TTM window span equals 11 months for every STR that ships a RevPAR (28 violators live); (4) every check delivered somewhere that does not share fate with the Mac Mini. The first thing this gate fails on is the outlet collapse, which forces the real fix underneath it: put the outlet in the key (taxpayer_number, outlet_seq, report_period) so the database can represent what Texas actually files. Until that gate exists, every green run is vibes, and this pipeline has already shown you, twice, what its vibes are worth.
I read scripts/sift-sync.mjs in full, pipeline-health-check.mjs (lines 1-455, close-read 370-455), check-receipt-reads.mjs, the compset-engine complete-month gate (790-830), useCompsets.ts receipt aggregation (1820-1880), migration 118, the crontab, the run logs (sift-sync.log greps, sift-sync-2026-07-19.json), and both vault docs (SIFT Data Accuracy 2026-07-15, Entity Handover 2026-07-19). I ran 14 read-only SQL queries against prod (edkqocoospynebgorwro) reproduced inline above. I did NOT download or reparse a Comptroller file, so the composition of the 6,222 discarded 2026-05 rows (distinct outlets vs repeated amendments) is bounded but not decomposed; I did not re-verify the pg_cron run history, the teaser/GM email code paths, or the stage map's line numbers for files I did not open, those are marked "per stage map" throughout. Where I could not verify, I said so.
Your pipeline is quietly deleting about 4,000 rows of Texas Comptroller data every single morning, and every dashboard downstream is polishing the survivors. On the 2026-07-19 run, period 2025-01 parsed 10,901 records and upserted 6,817 (logs/sift-sync-2026-07-19.json), a 37% collapse, and I traced where the other 4,084 went: validateAndTransform collapses everything sharing (taxpayer_number, report_period) to the LAST occurrence (scripts/sift-sync.mjs:419-425), because the table's unique key (migrations/001_initial_schema.sql) can't hold two outlets. The outlet column that would fix it, outlet_seq (field 8), is documented in the parser's own column map (sift-sync.mjs:312) and then never read (record builder, lines 348-363). You built the filing cabinet with one slot per company and you're throwing away every folder but the last one that comes off the truck.
That is not a theoretical nit. Live DB, reproducible SELECTs, all run 2026-07-19:
GROUP BY split_part(taxpayer_number,'_O',1) HAVING COUNT(*)>1).You spent the last week heroically fixing identity contamination (2026-07-15) and entity handovers (2026-07-19). Both were downstream repairs. This one is upstream of all of it, it's been running since roughly February, and it's the launch blocker. (Full detail: finding F1.)
You asked which class breaks next. It's already loaded: last-write-wins outlet roulette. Because the keep-last dedupe keeps whichever outlet row appears last in the CSV, the surviving row per master can be a *different physical hotel from month to month*, the surviving Marriott row's address is a Katy Fwy/Campbell Rd spread in 2025-12 and 12121 COIT RD in 2026-03 and 2026-05 (SELECT, 2026-07-19), while carrying one str_number stamp. That's the 2026-07-15 contamination class being re-manufactured at ingest, daily, with a mechanism none of migrations 245-251 can see, because enrichment is fill-only (245_location_grained_receipt_enrichment.sql:32,61) and never revisits a non-null stamp. The next customer-visible bug is a hotel-intel page showing a Frisco Courtyard's revenue under an Airport Commerce Dr address, and the four-layer stamping machinery will swear it's converged.
Chase rule: skills and automations are the backbone; the dashboard is theater. Here the *detectors* are backbone and the *delivery* is theater:
1. The pager is dead. All 68 Discord notification attempts in the retained log failed 401; zero ever succeeded (grep -c on logs/sift-sync.log, 2026-07-19). The failure is console-logged only (sift-sync.mjs:596-603). The bot token is hardcoded in the committed script (credential present at sift-sync.mjs:76-77) and evidently rotated out from under it. 2. The watchdog reports to a terminal tab. pipeline-health-check.mjs writes a JSON file; its only reader is the weekday-only interactive ops cockpit (agents-runtime/skills/ops/SKILL.md:131-133). No webhook, no email, no exit-code consumer. The rollup is ok:false *right now* and the last 4 lines of logs/pipeline-health.log are all STALE/FAILED, the alarm has already become wallpaper. 3. The queue has no reader. data_quality_issues holds 1,366 open rows across 15 issue types (SELECT, 2026-07-19); grep -rl data_quality_issues src/ returns zero files, RLS is service-role-only ("no UI exposure yet", mig 149:257-268), and coord_drift has 609 rows accumulating since 2026-05-09 with re-alerts deliberately suppressed (coord-audit-weekly/index.ts:419-431). You ask if the detector queue is backbone or theater: the detection is real, the queue is a diary nobody opens. 4. A published month of ZERO rows raises nothing. The 2026-06 file has parsed "0 records | 0 upserted | 0 validation failures" on every run since 2026-07-06 (logs/sift-sync.log:3911, 3987) and the DB has no 2026-06 rows. There is no row-count-vs-prior-period check anywhere in sift-sync.mjs (whole file read), and the <23-field row filter is a bare uncounted continue (line 317), a Comptroller column removal produces a green run ingesting nothing. 5. One check literally lies. checkStrCollection promises five signals (pipeline-health-check.mjs:376-381), implements one (only the contacts check at 394-397; the fetched state/ignored/held/failed results are never referenced), then prints "STR collection pipeline live, no discarded or held files" (line 406), asserting checks that never ran.
And the May 2026 precedent proves the cost: 20 consecutive FATAL: Cannot find module 'amazon-cognito-identity-js' runs, 2026-05-10 through 2026-05-28, caught on day ~20 (grep + JSON-log gap). Everything, ingest, both watchdogs, and their only reader, runs on one Mac Mini, so an outage is self-concealing.
The verification stack is SIFT checking SIFT. Enrichment fills stamps from a chain that was built from the same receipts; align copies chain→summary; sentinels are 8 hotels. The Zod layer is decorative: parseFloat(...) || 0 pre-coerces every number before Zod sees it (sift-sync.mjs:326-327 vs schema 167-182), and zodFailures: 0 in every retained run log since 2026-04-06. The ONE cross-source gate, STR-report reconciliation, covers 55 subjects in the current rollup (~/.revparpro/last_pipeline_status.json) out of ~4,056 STR-matched hotels: 1.4%. For the other 98.6%, the second set of eyes is a customer. Both of your last two gap classes were found by Ace noticing a wrong number on a screen, not by a gate. That's the echo chamber, and it will keep producing a new class weekly until an independent source (more STR ingests, or even a dumb month-over-month delta check that doesn't trust the stamps) sits in judgment.
Two more launch-relevant confirmations from code:
m.size === maxFilers. One member with a dead taxpayer link caps the filer count forever, so the ENTIRE monthly audit, public teaser, Market Audit tab, and the unmasked GM emails, freezes at the pre-sale month for the whole compset. The 149 outlet-collapsed hotels from F1 look identical to unlinked successors to this gate.useCompsets.ts as a raw-receipts reader, and useCompsets.ts:1829-1875 has no brand_splits union-find, it is exactly the dual-brand-blind reader shape the gate was built to fail. The wall works; the grandfather clause is a door in it. The gate also only greps *.ts/*.tsx under src/ and functions/, so every .mjs script walks around it.296 migration files, five stamping layers, ~15 repair scripts, 19 pg_cron jobs, and two legacy matviews (mv_hotel_ttm_revenue / mv_hotel_yoy_revenue) still refreshed nightly at 08:00 UTC with zero product consumers (grep: only type defs and two scripts). Meanwhile the actual root cause, a primary key that cannot represent the Comptroller's own data model of taxpayer × outlet × period, was never touched; migration 116 even *defines* the outlet-suffix convention and hand-wrote the normalizer, then the ingest that should produce suffixes ignores outlet_seq. That's the anti-pattern in one sentence: sophisticated scar tissue downstream, missing backbone upstream. Load-bearing today: sift-sync, enrichment v3 (245), align (251), the chain, sync_str_summary, the CI gate, the watchdog. Scar tissue you're paying rent on: the legacy matview refresh, the deprecated-but-scheduled sync_summary_from_receipts tombstone contradiction (mig 177 vs 233/251), the dead --fix path in the matcher, and stale headers (sift-sync.mjs:22-23 says monthly on the 10th; the crontab says daily).
Also: don't hand-roll what a tool solves. The quote-aware CSV splitter plus the >23/<23 shift heuristics (sift-sync.mjs:255-272, 317-344) is a homemade parser for a format class that csv-parse handles with correct escaped-quote semantics; the 742-row date-leak class from 2026-07-15 was a hand-rolled-parser bug wearing a data-quality costume.
Secrets, quickly, because the standing rule exists: SIFT portal login and the (dead) Discord bot token are hardcoded in the committed script (credential present at scripts/sift-sync.mjs:74-77), and the dual-brand-refresh-monthly cron job is the only one of 19 storing an inline Authorization: Bearer token in its command (SELECT jobname, command LIKE '%Authorization%Bearer%' FROM cron.job, one true, eighteen false, 2026-07-19). Rotate, move to .env/Vaultwarden, done. And mig 271 plus the handover repair scripts are live in prod but uncommitted (git status: ??), so a repo clone cannot reproduce the running schema, for a one-machine shop that IS your disaster recovery plan.
Stop deleting the outlets. Read outlet_seq, construct the _O-suffixed taxpayer id the schema convention (mig 116) already expects, drop the keep-last dedupe, backfill --since 2026-01, and put a two-line row-count floor on every period (parsed < 50% of prior run, or a listed month parsing to zero → red run). This one change restores ~150 vanished hotels, kills the ~$8M+/month of silently missing revenue rows, retires the outlet-roulette mis-stamping vector before it becomes next week's gap class, and gives the 2026-06 hole a tripwire. Everything else, the alert channel, the queue reader, the maxFilers tolerance, matters, but they are instruments; this is the engine. A flagged short month beats a fake green run.
Independently verified this session: sift-sync.mjs read in full (parse, dedupe, upsert, notify, main); pipeline-health-check.mjs checkStrCollection; check-receipt-reads.mjs in full; compset-engine.ts:790-830; live SELECTs against prod (period/suffix counts, Marriott master, multi-outlet blast radius, 149-hotel darkening with master-still-filing, mutual cycles = 48, open DQ = 1,366, orphans = 0, dup-suffixed tripwire = 0, cron.job inventory + inline-token scan, 442 taxpayers without summary rows); logs (Discord 68/0, FATAL x20, 2026-06 zero-record lines, 2026-07-19 run JSON, health-log tail); crontab; ~/.revparpro/last_pipeline_status.json; git status for mig 271; all three vault background docs.
Taken from the mapping agents' report without independent re-verification: exact line numbers inside migrations 036/072/106/109/135/149-151/233/245-252, the live pg_get_functiondef comparisons (mig 251 comment vs live function, refresh_and_sync_str_data body), useCompsets.ts internals beyond the receipts aggregation lines, the matcher's dead --fix path, cron.job_run_details timings, and the hardcoded credentials in match-str-to-tax.mjs / dual-brand-setup.mjs / geocode-missing-hotels.mjs. One number tension I did find in the map: it reports the Marriott 2025-12 outlets at $1,203,008.84; my SELECT sums all 11 rows (master + outlets) to $1,389,178.85, same conclusion, different row scope. And per my own rule: this review is still one vendor's eyes. Before launch, hand F1 and F2 to a second model with this evidence pack and let it try to refute them.