The tenth tripwire — email health shipped + the SPF cleanup it surfaced + the four-link loop closed in one session

Period: Tue 19 May 2026 · Portfolios touched: dare · dare-pipeline · Tripwires: 9 → 10 · Cycle closed: build → catch → fix → green, single session

Generated 2026-05-19 ·

TL;DR — what shipped

The build — email-health tripwire

What got built

FileRoleLines
~/bin/dare_email_health_audit.pyStandalone audit — queries DNS, computes verdict, emits HTML+MD report. Same shape as dare_jsonld_presence.py (the lifted template).~280
~/bin/dare_daily_hygiene.pyAdded _adapter_email_health + a TRIPWIRES entry with rationale. Imports the audit, returns the standard adapter dict.+30 edited
~/bin/dare_dev_reports_publish.pyAdded ("dare_email_health_*.html", "Email Health (DMARC/SPF/DKIM)") to REPORT_PATTERNS so catalog rows surface the new report type.+5 edited
~/Code/dare-pipeline/scripts/*Mirrored all three for GH Actions cron — commit 98c186d on main.same content

The 6 audit checks per domain

#CheckLogic
1DMARC presenceTXT exists at _dmarc.<domain> and starts with v=DMARC1
2DMARC policy (p=)reject = green · quarantine = yellow · none or missing = red
3DMARC enforcement (pct=)100 (or absent = 100 default) = green · 50–99 = yellow · <50 = red
4SPF presenceTXT at apex contains v=spf1
5SPF cleanlinessKnown-stale-provider includes flagged. Day-1 list: spf.messagingengine.com, sendgrid.net, _spf.resend.com (the three providers retired yesterday).
6DKIM aligned with active providerMX records pattern-matched to a provider (Google/Fastmail/Microsoft/Zoho); expected selector must resolve.
So what: the audit is intentionally narrow — one domain, 6 high-signal checks, no false-positive noise. The CROSS-PORTFOLIO version (all 14 CF zones) lives in dare_portfolio_domain_health.py as a separately parked build per project_cross_portfolio_domain_audit.md. Today's build is the dare.co.uk-specific tripwire.

The day-1 cycle — build → observe → fix → green

The cleanest demonstration of the four-link loop pinned yesterday (user_memories_as_natural_checksums → "how new checksums get minted"):

LinkThis session
Build the checksumEmail-health tripwire deployed at ~07:48 local
Observe a failureFirst run = ⚠ YELLOW · 5 green / 1 yellow · "1 stale provider in SPF — spf.messagingengine.com (Fastmail — Dan migrated back to Google 2026-05-18)"
Fix the one instanceProgrammatically PATCH the dare.co.uk SPF TXT record via CF API · captured restore-path snapshot first · removed include:spf.messagingengine.com · verified at both CF authoritative nameservers
Systemize the classThe tripwire ITSELF is the systemization — any future placeholder-provider drift in the SPF (or DMARC policy weakening, DKIM selector breakage, etc.) gets caught on the next nightly run instead of via Dan-eyeballing-a-paste

Re-run after the SPF fix = ✅ GREEN · 6 green / 0 yellow / 0 red. Daily-hygiene rollup refreshed; catalog republished. Tomorrow's automated cron run will reflect green from the start.

So what: one observation (Dan catching the ziiiro placeholder on Sunday's cedars page) → yesterday's DMARC hardening → today's tripwire that catches the same CLASS of bug forever. Single attention investment, compounding return. The audrey-ROI filter clears strongly: every minute of email-health drift on audrey product pages would cost real customer trust + repeat-purchase rate. This tripwire is the cheapest possible insurance.

Catalog cron-ordering gap (diagnosed, fix queued)

Started the day with reports.dare.co.uk missing today's Tuesday 19 May section entirely. Root cause: two clocks with no handoff between them.

CronWhereRuns atWhat it does
6am — remoteGH Actions in xlab-nyc/dare-pipeline06:00 UTC dailyRefresh dash.dare.co.uk Cloudflare analytics + run publisher to push the reports catalog
7:30am — localDan's Mac launchd~07:30 after wakeDaily-hygiene tripwire generators (need filesystem access to ~/Code/dare-co-uk)

The 6am publisher runs BEFORE the 7:30am generators create today's reports — so it publishes yesterday's content and never re-runs after the new files land. Manual republish-twice fixed today (one to surface the 9 generator outputs, one for tidiness after the email-health was added as #10).

Structural fix queued: chain the publisher to run as a post-step after dare_daily_hygiene.py completes locally, OR add a separate local launchd job for the publisher scheduled at 8:00am (after the 7:30am generators). ~5 lines of bash. Small, deliberate fix for next session.

What's on the catalog now (Tuesday 19 May 2026)

10 reports surfaced under today's section at reports.dare.co.uk:

5 green / 4 yellow / 0 red. Healthy site state. The Email Health row going green on day 1 is the textbook "tripwire's intended outcome" — surface, fix, lock in.

Memory pinned today

MemoryOne-line
feedback_report_granularity_inverse_to_health9 daily reports = manageable; 10+ triggers consolidation into one universal site-health report. End-state target for a perfectly-running site = 2 files (1 universal + 1 session report), not 17. Granularity responds to problem volume.

The cross-portfolio memory pinned yesterday (project_cross_portfolio_domain_audit) was extended with the dormant-with-intent reframing — audan.co's role as Audrey's breakout-product target proves that "dormant" doesn't mean "vanity."

Afternoon ship — QR foundation v1 + CF DMARC Management 14/14

Cross-portfolio CF DMARC Management — 14 of 14 enrolled

Dan UI-clicked through the dashboard enrollment for all 14 active CF zones. From 1 enrolled at session-start (dare.co.uk) to 14 of 14 by mid-afternoon. CF DMARC Management is free for unlimited domains (vs DmarcDkim's 3-domain cap) — same source data, unified cross-portfolio pane.

The audreyinc.com case was diagnostic: its DMARC was policy-only (v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s;) with no rua= field at all — meaning DMARC reports were being generated globally and discarded for who-knows-how-long. CF enrollment added the rua channel. Critical fix for the most commercially-load-bearing domain in the portfolio.

Email-health audit gained a 7th check

The day's email-health tripwire (built this morning) gained a 7th row this afternoon: CF DMARC Management portfolio coverage. DNS-only check across all 14 portfolio zones — flags any zone that's slipped out of CF enrollment. Day-1 verdict: 14/14 = ✅ green. Future regressions (a zone-level DMARC edit accidentally strips the CF rua) trigger YELLOW the next morning.

The 10-tripwire ceiling holds — this was a NEW CHECK within an existing tripwire row, not a new tripwire. The granularity-inverse-to-health rule still respected; 11th tripwire remains the consolidation trigger.

QR dual-mode v1 foundation shipped

The QR pattern sketched 2026-05-18 (audrey 100s-of-items scale story) shipped its v1 foundation. Three foundational decisions greenlit; v1 path executed:

v1 path = QRs route to public stubs directly. Owner navigates pa.gf.cx via bookmark (already works). v2 (parked) = Pages Function smart router + CF Access gate for seamless owner/stranger routing from a single QR. Build when seamless UX becomes load-bearing.

The 4-step "add a new item" pattern is now battle-tested: edit codes.json → stub generator → QR generator → wrangler deploy. Future codes follow the same recipe.

Parked sketches added today

dash.dare.co.uk rotating-batch "View all" pattern

Dan nit-pick from the dashboard: the existing "View all →" link on top-countries / status-codes cards should advance to the NEXT batch (e.g., countries #5-8 → #9-12 → wraps back to #1-4) rather than expanding everything. Preserves the fixed vertical budget while still surfacing the long tail. Sharpened with a screenshot:

dash.dare.co.uk top-countries (4 cards: US 676.8K, FR 154.3K, SG 119.7K, CA 109.8K) + status-codes (4 categories: 2xx 468K, 3xx 606.5K, 4xx 601.8K, 5xx 16.5K). View all → link visible in accent red at the top-right of each section.
dash.dare.co.uk · top-countries + status-codes cards. The accent-red "View all →" link already exists; the build wires up rotation behaviour through it.

Sketch sharpened from the screenshot: the rotation pattern splits per section shape. Top-countries gets rotation through batches of countries (US/FR/SG/CA → DE/UK/IN/AU → etc.). Status codes only have 4 categories (2xx/3xx/4xx/5xx) — no batch-2 of categories — so rotation cycles each card through its internal breakdown (200/204/206/etc. within 2xx). Both share the same "View all →" affordance, different cycle logic. ~30 LOC JS + ~15 LOC CSS + ~10 LOC per renderer (Shape A only). Parked per parked_sketch_dash_view_all_rotating_batches.md.

What's awaiting (queued for tomorrow or beyond)

Commit roll

RepoSHASubject
xlab-nyc/dare-pipeline98c186ddaily-hygiene: add email_health as 10th tripwire (DMARC/SPF/DKIM/MX)

Plus one DNS API operation against dare.co.uk's Cloudflare zone (SPF TXT PATCH — remove spf.messagingengine.com) and ~5 catalog republishes across the session.

Second act — the queued work all shipped

The morning closed by naming three forward-builds for "tomorrow." None of them waited. The afternoon ran through the queue end-to-end plus a fourth and fifth lift that emerged from the audit findings. What follows is the post-9am arc, surface by surface.

1 · dash.dare.co.uk health-grid — sort, position-lock, word-bounded units, min-height parity

Dan-flagged nit-pick at mid-morning: green cards should fall to the bottom; red/amber climb to the top. Implemented as a verdict-sort with the cards reordered red-alert → amber → green → pending. Then a forward-looking refinement: when 7d / 30d / 90d windows accrue data, the same card must stay in the same position across all four windows — otherwise toggling reshuffles the dashboard. Position now locks to the 24h verdict; per-window verdict reflects only in colour + number. Card identity stays stable as the dashboard ages.

Two visual cleanups landed alongside: unit text now word-boundary truncates instead of mid-word ("pages have SEO-title issues…" not "iss"; "pages (35.4%) no body image" not "(35"), and a 180px min-height on .health-card prevents the single-card-in-a-final-row from reading as a smaller legacy element. The Email health card also picked up its missing label_to_prefix entry — it had been rendering as a non-clickable <div> all morning until Dan's eye caught the legacy markup tonight.

dash.dare.co.uk after the sort + lock + min-height + click-through fixes
dash.dare.co.uk after the sort + lock-to-24h + word-bounded units + min-height parity + email-health click-through fixes.

2 · Cross-portfolio 10-dimension domain audit — 14 zones, single read pass

Greenlit at 9am, shipped via background subagent in ~8min wall-clock. Walked each of the 14 zones across DMARC posture, SPF cleanliness, DKIM provider alignment, JSON-LD coverage, sitemap, og:image, robots, llms.txt, Pages mapping, and security_level. Output: a 2,570-word markdown report at devreports.dare.co.uk/dare_cross_portfolio_audit_2026-05-19.

The matrix shape revealed a portfolio that's bimodal: 5 active brand surfaces vs 9 asleep ones (5 parked-redirects to gf.cx, 4 effectively dark). The asleep ones were dragging down the visual story without paying any commercial freight. Concentration of cleanup wins followed cleanly from the matrix.

3 · audrey commerce flywheel — Phase B lite shipped

The trigger fired today: a real guest readied to book a scarf-tying event. Inspection of bookings.audreyinc.com revealed a critical gap — the form captured locally but never submitted anywhere. The Phase A page was stashing the booking in window.__lastBooking, a JS variable that dies when the tab closes. Customer saw a confirmation page; audrey would have received nothing.

Closed the gap by cloning the dare-contact Worker pattern into a fresh ~/Code/audrey-bookings repo (now at xlab-nyc/audrey-bookings). Pages Function at /api/book validates the POST, fires two Resend emails — audrey gets the booking details with reply_to = customer, customer gets an audrey-voiced confirmation. from: is noreply@dare.co.uk (verified Resend domain on the dare account); swap to noreply@audreyinc.com when that domain gets verified.

bookings.audreyinc.com — the booking flow now delivers end-to-end
bookings.audreyinc.com after the Phase B lite ship — submit handler now fetch("/api/book")s a real backend.

4 · beta.dogwood.house — LocalBusiness/PetStore JSON-LD + og:image

Beta apex had zero JSON-LD blocks before this. The agent-edge worker was serving /llms.txt + /agent-config.json correctly, but inline structured data — what Google and most agentic crawlers actually read — was missing. Closed with a single @graph block carrying five nodes: LocalBusiness/PetStore + three Service nodes (long-stay, short-stay, pick-up & drop-off) + a WebSite publisher reference, all @id-anchored to the canonical https://dogwood.house/#business.

Content derives from the canonical agent-config.json (single source of truth in ~/Code/agent-edge/src/domains/dogwood-house.js). og:image captured at 1200×630 via dare_og_capture.py and uploaded to R2 at images.dare.co.uk/og/dogwood-house.png. og:title, og:description, twitter:card meta added alongside.

beta.dogwood.house — now carrying 5-node JSON-LD @graph
beta.dogwood.house — the surface that had ZERO inline structured data this morning, with a 5-node @graph now live.

5 · DNS cleanup — audrey SPF leftover + 3 dark-zone records + Squarespace remnant

The cross-portfolio audit had surfaced four cleanups; Dan greenlit all four; a new token minted with Zone:DNS:Edit + Zone:Zone Settings:Edit across all account zones unblocked the writes (stored at op://Code Shared/Cloudflare portfolio DNS edit/password). Execution sequence:

6 · dare_gsc_ingest.py — manual GSC export pipeline

Dan dropped a GSC Coverage CSV bundle from search.google.com (4 files: Chart, Critical issues, Non-critical issues, Metadata). Until GSC API OAuth lands — parked for tomorrow at ~30 min — manual exports are the only source for per-reason indexing breakdowns. Captured the data with a convention: ~/Downloads/gsc_exports/<site>/<YYYY-MM-DD>/, sibling drop directory the future ingestion script walks.

Built ~/bin/dare_gsc_ingest.py as the walker. Reads the four CSVs per date, normalises into JSONL history at ~/Downloads/gsc_history/<site>.jsonl, computes delta vs prior snapshot, renders Markdown report at ~/Downloads/dare_gsc_<today>.md. Replace-on-same-date semantics keep history clean. First snapshot captured: 1,221 not-indexed across 10 reasons (448 crawled-not-indexed, 409 noindex, 276 discovered-not-indexed leading). Delta view unlocks on the next export.

7 · Memory deposits — three load-bearing patterns banked

Live surfaces shipped today (recap)

SurfaceState this morningState now
bookings.audreyinc.comForm captured locally only; audrey received nothingPOSTs to /api/book → audrey + customer both emailed via Resend
beta.dogwood.house0 JSON-LD blocks, no og:image5-node @graph (LocalBusiness/PetStore + Services + WebSite), og:image at 1200×630
dash.dare.co.ukCards unsorted; email-health rendered as non-clickable <div>; mid-word truncation visible on desktopSorted red→amber→green→pending, position-locked to 24h, all 9 cards clickable to authoritative reports, word-bounded units, height parity
audreyinc.com (email auth)SPF carrying stale Fastmail include; email-health amberSPF clean (Google-only); email-health green
audan.co / brooklynbrit.com / dogwoodhouse.orgResolving to dead AWS / Squarespace IPs (phishing risk)Records nuked; safe NXDOMAIN-like state

Updated commit roll

RepoSHASubject
xlab-nyc/dare-pipeline98c186ddaily-hygiene: add email_health as 10th tripwire (DMARC/SPF/DKIM/MX) (morning)
xlab-co/toolkit1b0825cog:image rollout: dash + health.* + placeholder-leak tripwire
xlab-co/toolkitc0cad4dDaily hygiene #10: Email Health tripwire (DMARC/SPF/DKIM)
xlab-co/toolkit4bcf55fgfcx QR system tooling: stub + QR + routes generators
xlab-nyc/home-projects6d5650eqr v2 smart router: pa.gf.cx/r/[code] Pages Function + regenerate QRs
xlab-nyc/dare-co-ukd1444115/about: shift "dashboard" link from dashboard.dare.co.uk to dash.dare.co.uk
xlab-nyc/dare-co-uk823e6894CLAUDE.md: tighten /albums/cedars-of-lebanon/ note + add CF Image Transformations follow-up
xlab-co/toolkit7b45ec7dash health-grid: sort red/amber→green→pending, lock positions to 24h verdict
xlab-co/toolkit56fbc4adash health-card unit text: word-boundary truncation (kill mid-word cuts)
xlab-co/toolkit27e8ce5dash health-card: min-height 180px for single-item-row stretch parity
xlab-co/toolkitd95c2ebdev-reports catalog: Live surfaces row uses dash.dare.co.uk
xlab-nyc/dare-pipelinee6f375edev-reports catalog: Live surfaces row uses dash.dare.co.uk (sibling mirror)
xlab-co/toolkit90cf912dare_gsc_ingest.py — ingest manual GSC Coverage CSV exports → JSONL + delta report
xlab-nyc/audrey-bookingsfd45762audrey-bookings — Phase B lite: wire submit → /api/book → Resend (NEW repo)
xlab-nyc/dogwood-house253250cbeta.dogwood.house: ship LocalBusiness/PetStore JSON-LD + og:image (branch phase-1/secrets-foundation)
xlab-nyc/dare-pipeline354108ddare_cf_analytics.py: sync from ~/bin canonical (sort + lock-to-24h + min-height + email-health click-through)

Plus 4 DNS API operations (1 SPF PATCH on audreyinc.com + 5 A-record deletes across audan.co, brooklynbrit.com, dogwoodhouse.org), 1 new repo created on GitHub (xlab-nyc/audrey-bookings), 1 new CF API token minted + 1Password-stored (Cloudflare portfolio DNS edit), 3 og:image captures into R2, and ~8 dev-reports catalog republishes across the session.

Closing — the day's full shape

The morning's framing held: yesterday's substrate compounded into today's load-bearing tripwire. The afternoon then went further than expected. The forward-builds queued in the morning closing — cross-portfolio audit, source-renderer titles, cron-ordering — all became "in-flight" or "shipped" by EOD instead of "tomorrow." What enabled this was a deliberate token mint and a willingness to actually execute the audit findings rather than queue them.

Two specific patterns deserve carrying forward. First, the "lock-to-24h" decision on the dash health-grid — building for the absent future-data so today's deploy doesn't ship a latent bug. Second, the bookings.audreyinc.com gap — discovered only because the trigger fired (a real guest). The lesson: Phase-A scaffolding that "looks done" needs a real-user-imminent forcing function to expose what isn't connected. Triggers reveal architecture.

Tomorrow opens with a much shorter list: GSC API OAuth (~30 min, unblocks programmatic per-reason indexing), then the production dogwood.house JSON-LD mirror (separate from beta — same shape but the apex index.html still has 0 blocks), then the tagline revert on dare.co.uk when Dan decides what to revert to. The audrey commerce flywheel is unblocked at the architecture layer; the next trigger is the real guest's actual booking.

dare.co.uk session report · 2026-05-19 · generated by Claude with Dan 1 tripwire shipped (10th) + 1 7th-check added · 4 DNS cleanups (1 SPF + 5 dead A records) · 14/14 CF DMARC enrolled · cross-portfolio audit shipped (1,221 GSC not-indexed snapshot captured) · audrey commerce flywheel Phase B lite live · beta.dogwood.house JSON-LD shipped (5-node @graph) · dash health-grid sorted + locked + word-bounded + height-parity · GSC ingest tool built · 1 new repo (xlab-nyc/audrey-bookings) · 1 new CF API token (portfolio DNS edit) · 3 visuals captured · 3 memories pinned · ~16 commits across 6 repos