← back to Animals
fix(events): codex P1 XSS in /shows map + P2 dedupe collision
b70d15d8a93d1e184934bd58fe06503b6294854d · 2026-05-01 18:50:22 -0700 · Steve
P1 (security) — render.js:389-396
The /shows page injected scraped event titles via raw JSON.stringify in a
<script> block, then built popups via 'string' + p.title concat fed to
Leaflet's bindPopup (which treats it as innerHTML). Event titles come from
third-party AKC/UKC pages — a malformed or hostile title containing
</script> or HTML became stored XSS on a public route.
Switched to:
- <script type="application/json" id="show-pts">…</script> + JSON.parse
(with a <-escape on the JSON output for an extra layer in case the
pattern is ever copy-pasted into a non-JSON context)
- Popup built via document.createElement('strong') + .textContent so
titles can never become HTML.
P2 (data integrity) — dog_shows_ca.js:158 + migrations/007_*.sql
ON CONFLICT (source, source_url, start_at) collided for AKC/UKC where
source_url is the shared search/results page. Two distinct shows on the
same date silently overwrote each other in the events table.
Added title to the dedupe key + new migration that drops the old unique
index and creates idx_events_dedupe (source, source_url, start_at, title).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
A migrations/007_events_dedupe_with_title.sqlA src/ingest/dog_shows_ca.jsM src/server/render.js
Diff
commit b70d15d8a93d1e184934bd58fe06503b6294854d
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 1 18:50:22 2026 -0700
fix(events): codex P1 XSS in /shows map + P2 dedupe collision
P1 (security) — render.js:389-396
The /shows page injected scraped event titles via raw JSON.stringify in a
<script> block, then built popups via 'string' + p.title concat fed to
Leaflet's bindPopup (which treats it as innerHTML). Event titles come from
third-party AKC/UKC pages — a malformed or hostile title containing
</script> or HTML became stored XSS on a public route.
Switched to:
- <script type="application/json" id="show-pts">…</script> + JSON.parse
(with a <-escape on the JSON output for an extra layer in case the
pattern is ever copy-pasted into a non-JSON context)
- Popup built via document.createElement('strong') + .textContent so
titles can never become HTML.
P2 (data integrity) — dog_shows_ca.js:158 + migrations/007_*.sql
ON CONFLICT (source, source_url, start_at) collided for AKC/UKC where
source_url is the shared search/results page. Two distinct shows on the
same date silently overwrote each other in the events table.
Added title to the dedupe key + new migration that drops the old unique
index and creates idx_events_dedupe (source, source_url, start_at, title).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
migrations/007_events_dedupe_with_title.sql | 15 ++
src/ingest/dog_shows_ca.js | 221 ++++++++++++++++++++++++++++
src/server/render.js | 115 ++++++++++++++-
3 files changed, 347 insertions(+), 4 deletions(-)
diff --git a/migrations/007_events_dedupe_with_title.sql b/migrations/007_events_dedupe_with_title.sql
new file mode 100644
index 0000000..44edc77
--- /dev/null
+++ b/migrations/007_events_dedupe_with_title.sql
@@ -0,0 +1,15 @@
+-- 007_events_dedupe_with_title.sql
+-- Fixes the ON CONFLICT key mismatch flagged by codex on 2026-05-01.
+--
+-- The original idx_events_dedupe (source, source_url, start_at) collided for
+-- AKC/UKC imports where source_url is the shared search/results page —
+-- multiple distinct events on the same date silently overwrote each other.
+-- Adding `title` to the unique key so distinct events on the same source/date
+-- survive.
+--
+-- Idempotent: drops the old index if present, recreates the new one.
+
+DROP INDEX IF EXISTS idx_events_dedupe;
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_events_dedupe
+ ON events (source, source_url, start_at, title);
diff --git a/src/ingest/dog_shows_ca.js b/src/ingest/dog_shows_ca.js
new file mode 100644
index 0000000..860e2fa
--- /dev/null
+++ b/src/ingest/dog_shows_ca.js
@@ -0,0 +1,221 @@
+#!/usr/bin/env node
+// Dog show + competition scraper for California.
+//
+// Sources:
+// 1. AKC event search JSON (https://www.akc.org/wp-admin/admin-ajax.php → action=event_search_loadmore)
+// 2. UKC event search (https://www.ukcdogs.com/event-search) — HTML, parsed with cheerio
+// 3. Misc: each major California kennel club website if directly listed
+//
+// All inserts go into the `events` table with kind one of:
+// 'dog_show','agility_trial','obedience_trial','rally_trial','field_trial',
+// 'herding_trial','tracking_test','lure_coursing','barn_hunt','dock_diving',
+// 'flyball','breed_meet'.
+//
+// Geocoding: best-effort. We rely on the source's address field; if no
+// lat/lng is provided, we leave the columns NULL and the city map page
+// just won't drop a marker for that event (acceptable for v1).
+//
+// Run:
+// node src/ingest/dog_shows_ca.js # CA, next 90 days
+// node src/ingest/dog_shows_ca.js --days 180
+
+import 'dotenv/config';
+import { fetch as undiciFetch } from 'undici';
+import * as cheerio from 'cheerio';
+import { pool, one } from '../lib/db.js';
+import { log } from '../lib/log.js';
+
+const args = process.argv.slice(2);
+const arg = (k, d = null) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d; };
+const days = parseInt(arg('--days'), 10) || 120;
+
+const UA = 'AnimalsDirectoryBot/1.0 (+https://animalsdirectory.com/bot)';
+const today = new Date();
+const until = new Date(today.getTime() + days * 24 * 60 * 60 * 1000);
+
+const stats = { akc: 0, ukc: 0, errors: 0 };
+
+await scrapeAKC().catch(e => { stats.errors++; log.error('akc failed', { err: e.message }); });
+await scrapeUKC().catch(e => { stats.errors++; log.error('ukc failed', { err: e.message }); });
+
+log.info('dog shows CA done', stats);
+await pool.end();
+
+// ── AKC event search ─────────────────────────────────────────────────────
+// Best-effort: AKC frequently changes their endpoint. We try the public
+// admin-ajax POST first (returns rendered HTML w/ event cards), fall back
+// to the search-results HTML page.
+async function scrapeAKC() {
+ log.info('=== AKC events (CA) ===');
+
+ // Public results page — server-renders next ~50 events for the filters
+ const url = 'https://www.akc.org/sports/?_event_state%5B%5D=ca';
+ let html;
+ try {
+ const r = await undiciFetch(url, {
+ headers: { 'user-agent': UA, accept: 'text/html,*/*' },
+ signal: AbortSignal.timeout(20000),
+ });
+ if (!r.ok) { log.warn(`AKC HTTP ${r.status}`); return; }
+ html = await r.text();
+ } catch (err) { log.warn(`AKC fetch err: ${err.message}`); return; }
+
+ const $ = cheerio.load(html);
+
+ // AKC marks each event card as .event-card or article.event — we accept either
+ const cards = $('article.event, .event-card, .results-card, [data-event-id]').toArray();
+ log.info(` AKC cards found: ${cards.length}`);
+
+ for (const el of cards) {
+ try {
+ const $c = $(el);
+ const title = clean($c.find('h2, h3, .event-title, [data-event-title]').first().text());
+ if (!title) continue;
+ const dateText = clean($c.find('.event-date, .date, time').first().text())
+ || clean($c.find('[data-event-date]').first().attr('data-event-date'));
+ const venue = clean($c.find('.event-venue, .venue').first().text());
+ const location = clean($c.find('.event-location, .location, .city-state').first().text());
+ const eventUrl = $c.find('a').first().attr('href') || null;
+ const eventType = inferKind(title + ' ' + (clean($c.find('.event-type').first().text()) || ''));
+
+ const { startAt, endAt } = parseDateRange(dateText);
+ if (!startAt || startAt > until) continue;
+
+ const { city, state } = parseLocation(location);
+ if (state && state !== 'CA') continue;
+
+ await upsertEvent({
+ kind: eventType,
+ title,
+ sponsor_org: 'AKC',
+ venue: venue || null,
+ city: city || null,
+ state: state || 'CA',
+ start_at: startAt,
+ end_at: endAt,
+ url: absUrl(eventUrl, 'https://www.akc.org'),
+ source_url: url,
+ source: 'akc',
+ });
+ stats.akc++;
+ } catch (err) { log.warn(` AKC card parse err: ${err.message}`); }
+ }
+}
+
+// ── UKC event search ─────────────────────────────────────────────────────
+async function scrapeUKC() {
+ log.info('=== UKC events (CA) ===');
+ const url = 'https://www.ukcdogs.com/event-search?_state=California';
+ let html;
+ try {
+ const r = await undiciFetch(url, {
+ headers: { 'user-agent': UA, accept: 'text/html,*/*' },
+ signal: AbortSignal.timeout(20000),
+ });
+ if (!r.ok) { log.warn(`UKC HTTP ${r.status}`); return; }
+ html = await r.text();
+ } catch (err) { log.warn(`UKC fetch err: ${err.message}`); return; }
+
+ const $ = cheerio.load(html);
+ const cards = $('.event-listing, article, .search-result').toArray();
+ log.info(` UKC cards found: ${cards.length}`);
+ for (const el of cards) {
+ try {
+ const $c = $(el);
+ const title = clean($c.find('h2, h3, .title').first().text());
+ if (!title) continue;
+ const dateText = clean($c.find('.date, time').first().text());
+ const venue = clean($c.find('.venue, .location').first().text());
+ const eventUrl = $c.find('a').first().attr('href') || null;
+ const { startAt, endAt } = parseDateRange(dateText);
+ if (!startAt || startAt > until) continue;
+ const { city, state } = parseLocation(venue);
+ if (state && state !== 'CA') continue;
+ await upsertEvent({
+ kind: inferKind(title),
+ title,
+ sponsor_org: 'UKC',
+ venue: venue || null,
+ city: city || null,
+ state: state || 'CA',
+ start_at: startAt,
+ end_at: endAt,
+ url: absUrl(eventUrl, 'https://www.ukcdogs.com'),
+ source_url: url,
+ source: 'ukc',
+ });
+ stats.ukc++;
+ } catch (err) { log.warn(` UKC card parse err: ${err.message}`); }
+ }
+}
+
+// ── helpers ──────────────────────────────────────────────────────────────
+async function upsertEvent(e) {
+ await pool.query(`
+ INSERT INTO events (kind, title, organizer, sponsor_org, venue, address, city, state, zip,
+ start_at, end_at, url, source_url, source, status)
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,'scheduled')
+ -- (source, source_url, start_at) collides for AKC/UKC where source_url is
+ -- the shared search/results page — multiple events on same date silently
+ -- overwrite each other. Add title to the dedupe key so distinct events on
+ -- the same source/date survive. (codex P2, 2026-05-01)
+ ON CONFLICT (source, source_url, start_at, title) DO UPDATE SET
+ title = EXCLUDED.title,
+ venue = COALESCE(EXCLUDED.venue, events.venue),
+ city = COALESCE(EXCLUDED.city, events.city),
+ end_at = EXCLUDED.end_at,
+ url = EXCLUDED.url,
+ updated_at = NOW()`,
+ [e.kind, e.title, e.organizer || null, e.sponsor_org || null, e.venue || null,
+ e.address || null, e.city || null, e.state || null, e.zip || null,
+ e.start_at, e.end_at || null, e.url || null, e.source_url, e.source]);
+}
+
+function inferKind(s) {
+ s = s.toLowerCase();
+ if (/agility/.test(s)) return 'agility_trial';
+ if (/obedience/.test(s)) return 'obedience_trial';
+ if (/rally/.test(s)) return 'rally_trial';
+ if (/field|hunt(ing)? test/.test(s)) return 'field_trial';
+ if (/herding/.test(s)) return 'herding_trial';
+ if (/tracking/.test(s)) return 'tracking_test';
+ if (/lure cours/.test(s)) return 'lure_coursing';
+ if (/barn hunt/.test(s)) return 'barn_hunt';
+ if (/dock div/.test(s)) return 'dock_diving';
+ if (/flyball/.test(s)) return 'flyball';
+ if (/conformation|specialty|all-breed|kennel club|cluster/.test(s)) return 'dog_show';
+ return 'dog_show';
+}
+
+function clean(s) { return String(s || '').replace(/\s+/g, ' ').trim(); }
+
+function parseLocation(s) {
+ const m = (s || '').match(/^(.+?)\s*,\s*([A-Z]{2})\b/);
+ if (m) return { city: m[1].trim(), state: m[2] };
+ if (/california|, ca\b|\bca\b/i.test(s)) return { city: null, state: 'CA' };
+ return { city: null, state: null };
+}
+
+function parseDateRange(s) {
+ if (!s) return { startAt: null, endAt: null };
+ // common shapes: "Jan 15, 2026" / "Jan 15-17, 2026" / "Jan 15 - 17, 2026" / "January 15-17 2026"
+ const m1 = s.match(/(\b[A-Z][a-z]{2,8})\s+(\d{1,2})(?:\s*[-–]\s*(?:[A-Z][a-z]{2,8}\s+)?(\d{1,2}))?,?\s*(\d{4})/);
+ if (m1) {
+ const [, mon, d1, d2, yr] = m1;
+ const start = new Date(`${mon} ${d1}, ${yr}`);
+ const end = d2 ? new Date(`${mon} ${d2}, ${yr}`) : null;
+ if (!isNaN(start.getTime())) return { startAt: start, endAt: end && !isNaN(end.getTime()) ? end : null };
+ }
+ // ISO-ish "2026-01-15"
+ const m2 = s.match(/(\d{4})-(\d{2})-(\d{2})/);
+ if (m2) {
+ const start = new Date(`${m2[0]}T00:00:00`);
+ if (!isNaN(start.getTime())) return { startAt: start, endAt: null };
+ }
+ return { startAt: null, endAt: null };
+}
+
+function absUrl(href, base) {
+ if (!href) return null;
+ try { return new URL(href, base).href; } catch { return null; }
+}
diff --git a/src/server/render.js b/src/server/render.js
index d304b94..8d14d9b 100644
--- a/src/server/render.js
+++ b/src/server/render.js
@@ -105,8 +105,10 @@ export function renderHome({ breeds, counts, categories }) {
// the db uses singular for some. Map explicitly so counts always render.
const URL_TO_DB = {
'vets':'vet_clinic','emergency-vets':'emergency_vet','specialty-hospitals':'specialty_hospital',
- 'groomers':'groomer','pet-stores':'pet_store','shelters':'shelter','breeders':'breeder',
- 'trainers':'trainer','boarding':'boarding','daycare':'daycare','dog-parks':'dog_park',
+ 'groomers':'groomer','pet-stores':'pet_store',
+ 'shelters':'shelter','rescues':'rescue','pounds':'pound',
+ 'breeders':'breeder','trainers':'trainer',
+ 'boarding':'boarding','kennels':'kennel','daycare':'daycare','dog-parks':'dog_park',
};
const catCards = categories.filter(c => (counts[URL_TO_DB[c]]||0) > 0).map(c => `
<a href="/${c}" class="cat-card">
@@ -158,7 +160,7 @@ export function renderBreedsIndex({ breeds, filters }) {
</main>` + footer() + '</body></html>';
}
-export function renderBreed({ breed, relatedVets, ad }) {
+export function renderBreed({ breed, relatedVets, images = [], ad }) {
const traits = [
['Group', breed.group_name], ['Size', breed.size_class],
['Weight', breed.weight_lbs_min ? `${breed.weight_lbs_min}–${breed.weight_lbs_max} lb` : null],
@@ -171,20 +173,41 @@ export function renderBreed({ breed, relatedVets, ad }) {
['Good with pets', breed.good_with_pets ? `${breed.good_with_pets}/5` : null],
].filter(([_,v]) => v).map(([k,v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('');
const vets = relatedVets.map(v => `<li><a href="/clinic/${v.id}">${esc(v.name)}</a> <small>${esc(v.city||'')}, ${esc(v.state||'')}${v.rating?` · ★ ${v.rating}`:''}</small></li>`).join('');
+ const hero = breed.hero_image_url || images[0]?.image_url || null;
+ // Gallery skips the hero (so it doesn't show twice). Per Steve's standing
+ // no-stock-images rule, every image is PD/CC0/CC-BY/CC-BY-SA — caption
+ // includes author + license + Commons descriptionurl link.
+ const gallery = images
+ .filter(im => im.image_url !== hero)
+ .map(im => `
+ <figure class="gallery-card">
+ <a href="${esc(im.source_url)}" target="_blank" rel="noopener" title="View on Wikimedia Commons">
+ <img loading="lazy" src="${esc(im.thumb_url || im.image_url)}" alt="${esc(im.title || breed.common_name)}">
+ </a>
+ <figcaption>
+ <small><strong>${esc(im.title || '')}</strong></small>
+ <small class="muted">${esc(im.author || 'Unknown')} · <a href="${esc(im.license_url || im.source_url)}" target="_blank" rel="noopener">${esc(im.license_short || 'CC')}</a></small>
+ </figcaption>
+ </figure>`).join('');
return head(`${breed.common_name} — breed profile`) + nav() + `
<main class="container">
<article>
<header class="breed-header">
- <div class="breed-img-large" style="${breed.hero_image_url?`background-image:url('${esc(breed.hero_image_url)}')`:''}"></div>
+ <div class="breed-img-large" style="${hero?`background-image:url('${esc(hero)}')`:''}"></div>
<div>
<h1>${esc(breed.common_name)}</h1>
<p class="muted">${esc(breed.species)} · ${esc(breed.group_name||'')} group · ${esc(breed.size_class||'')}</p>
${breed.recognized_by?.length ? `<p class="badges">${breed.recognized_by.map(r=>`<span>${esc(r)}</span>`).join('')}</p>` : ''}
+ ${breed.hero_image_credit ? `<p class="muted small">Photo: ${esc(breed.hero_image_credit)}</p>` : ''}
</div>
</header>
<dl class="trait-list">${traits}</dl>
${breed.description_md ? `<section class="prose">${breed.description_md.split('\n\n').map(p=>`<p>${esc(p)}</p>`).join('')}</section>` : ''}
</article>
+ ${gallery ? `
+ <h2>Photos (${images.length} from Wikimedia Commons)</h2>
+ <p class="muted small">All images public-domain or Creative Commons. Click any photo for source + full license.</p>
+ <section class="breed-gallery">${gallery}</section>` : ''}
${adSlot(ad, 'native', `/breeds/${breed.slug}`)}
<h2>Top-rated vets that see ${esc(breed.common_name)}s</h2>
<ul class="business-list">${vets || '<li class="muted">No vets seeded yet — coming soon.</li>'}</ul>
@@ -321,3 +344,87 @@ export function renderAdmin({ upgradeQ, leadsQ, auditQ }) {
}
export function renderNotFound(msg='Not found') { return head(msg) + nav() + `<main class="container"><h1>${esc(msg)}</h1><p><a href="/">Back to home</a></p></main>` + footer() + '</body></html>'; }
+
+const KIND_LABEL = {
+ dog_show:'🏆 Dog show', cat_show:'🏆 Cat show', horse_show:'🏆 Horse show',
+ adoption_event:'❤️ Adoption event', vaccine_clinic:'💉 Vaccine clinic',
+ agility_trial:'🎯 Agility trial', obedience_trial:'📜 Obedience',
+ rally_trial:'📋 Rally', field_trial:'🌾 Field trial',
+ herding_trial:'🐑 Herding', tracking_test:'🐾 Tracking',
+ lure_coursing:'🐇 Lure coursing', barn_hunt:'🐀 Barn hunt',
+ dock_diving:'🌊 Dock diving', flyball:'🎾 Flyball',
+ breed_meet:'🐕 Breed meet', other:'📅 Event',
+};
+
+export function renderShows({ events, filters, ad }) {
+ const fmt = (d) => new Date(d).toLocaleDateString('en-US', { weekday:'short', month:'short', day:'numeric', year:'numeric' });
+ const groups = {};
+ for (const e of events) {
+ const k = fmt(e.start_at);
+ (groups[k] ||= []).push(e);
+ }
+ const list = Object.entries(groups).map(([day, evs]) => `
+ <section class="day-group">
+ <h3>${esc(day)}</h3>
+ <div class="event-list">
+ ${evs.map(e => `
+ <article class="event-card">
+ <span class="kind">${esc(KIND_LABEL[e.kind] || e.kind)}</span>
+ <h4>${e.url ? `<a href="${esc(e.url)}" target="_blank" rel="nofollow noopener">${esc(e.title)}</a>` : esc(e.title)}</h4>
+ <p class="meta">
+ ${e.sponsor_org ? `<strong>${esc(e.sponsor_org)}</strong> · ` : ''}
+ ${e.venue ? esc(e.venue) + ' · ' : ''}${esc(e.city || '')}${e.state ? ', ' + esc(e.state) : ''}${e.zip ? ' ' + esc(e.zip) : ''}
+ </p>
+ ${e.organizer ? `<p class="muted">Organizer: ${esc(e.organizer)}</p>` : ''}
+ ${e.end_at && new Date(e.end_at).getTime() !== new Date(e.start_at).getTime() ? `<p class="muted">Through ${esc(fmt(e.end_at))}</p>` : ''}
+ </article>`).join('')}
+ </div>
+ </section>`).join('');
+
+ const points = events.filter(e => e.latitude && e.longitude).map(e => ({
+ id:e.id, title:e.title, lat:Number(e.latitude), lng:Number(e.longitude), kind:e.kind,
+ }));
+
+ const states = ['','CA','TX','FL','NY','IL','WA','CO','GA','MA','PA'];
+ const kinds = ['', ...Object.keys(KIND_LABEL)];
+
+ return head('Dog shows + events') + nav() + `
+<main class="container">
+ <h1>Dog shows · trials · adoption events ${filters.state ? `in ${esc(filters.state)}` : ''}</h1>
+ <p class="muted">${events.length} upcoming events</p>
+ <form class="filters" method="GET">
+ <select name="state">${states.map(s => `<option value="${s}" ${filters.state===s?'selected':''}>${s||'All states'}</option>`).join('')}</select>
+ <select name="kind">${kinds.map(k => `<option value="${k}" ${filters.kind===k?'selected':''}>${k?KIND_LABEL[k]:'All kinds'}</option>`).join('')}</select>
+ <button type="submit">Filter</button>
+ </form>
+ ${points.length ? `<div id="show-map" class="map" style="height:380px"></div>` : ''}
+ ${list || '<p class="empty">No events scheduled yet — scraper run pending.</p>'}
+ ${adSlot(ad, 'native', '/shows')}
+</main>
+${points.length ? `
+<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
+<!-- Event titles are scraped from third-party pages (AKC, UKC, etc) and may
+ contain HTML or </script>. Inline JSON in a <script> block is unsafe with
+ bare JSON.stringify. Use type="application/json" + JSON.parse, and build
+ popup text via DOM/textContent so titles can never become HTML. (codex P1) -->
+<script type="application/json" id="show-pts">${JSON.stringify(points).replace(/</g, '\\u003c')}</script>
+<script>
+(function(){
+ const el = document.getElementById('show-map');
+ if (typeof L === 'undefined') { el.innerHTML = '<p class="muted" style="padding:1em">Map library failed to load.</p>'; return; }
+ const pts = JSON.parse(document.getElementById('show-pts').textContent);
+ if (!pts.length) return;
+ const map = L.map('show-map').setView([pts[0].lat, pts[0].lng], 6);
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap contributors',maxZoom:18}).addTo(map);
+ const grp = L.featureGroup(pts.map(p => {
+ const m = L.circleMarker([p.lat,p.lng], { radius:9, color:'#c97a3e', fillColor:'#c97a3e', fillOpacity:0.85, weight:2 });
+ const popup = document.createElement('strong');
+ popup.textContent = p.title;
+ m.bindPopup(popup);
+ return m;
+ })).addTo(map);
+ if (pts.length > 1) map.fitBounds(grp.getBounds().pad(0.15));
+ setTimeout(() => map.invalidateSize(), 100);
+})();
+</script>` : ''}` + footer() + '</body></html>';
+}
← bf214ed fix: codex review findings — creds + URL parse + license reg
·
back to Animals
·
yolo: iter 1 trivial patches from claude-codex debate 05cb1bc →