Repository navigation
Description
Chapter data lives in five places today: the website's data/chapters/*.json, the Airtable organiser base, the Airtable chapters intake table, GitHub teams and repos, and Meetup. Nothing keeps them honest, and they have already drifted.
The rule: one owner per fact
Bidirectional sync between five systems is 20 edges. One owner per fact is five.
| Fact | Owner | Everyone else |
|---|---|---|
| A chapter exists, its status, its organisers | website data/chapters |
reads |
| Organiser PII (email, GitHub, Meetup handle) | Airtable organiser base | reads |
| Intake: applications up to "Onboarded" | Airtable chapters table |
stops there; the website takes over |
| Team and repo existence | GitHub | reads |
| Events | Meetup | reads |
Two corollaries:
- Derive, don't store. The filename, the team slug, the repo name and the mailbox address are all functions of city/region/country. Storing them invites a second spelling. jinx already has
chapter_filename(); everything else should route through one function too. - No bidirectional sync. Nothing writes back to an owner it does not own. Airtable intake hands off to the website once, at "Onboarded", and stops.
Two mechanisms, not one
Prevention — a PR check on the website. Catches drift at the only moment it is cheap to fix. Scope: schema, filename matches the city/region/country it is derived from, urlname agrees with social_media.meetup, and no escaped byte placeholders. Only files the PR touches, so existing drift does not block unrelated work.
Detection — one weekly chapter_reconcile(). Everything the PR check cannot see because it is cross-system: a chapter with no Meetup group, a team with no chapter, a chapter whose organisers do not match Airtable. Reports to a single issue, updated in place, never a new issue per run.
Baseline drift, measured today
Running the checks over all 384 chapter files:
| Check | Files |
|---|---|
| Schema | 0 |
| Escaped byte placeholders | 57 |
| Filename mismatch | 32 |
urlname vs social_media.meetup |
5 |
The 57 are the urgent one and were not in the original proposal — I found them building the check. Accented characters have been written into the data as R's undecodable-byte placeholder: Ceará is stored as the literal seven characters Cear<e1>, Bogotá as Bogot<e1>. It affects city names, region names, and organiser names — Ana Betty Villaseñor, Isabella Velásquez, Bea Hernández and ~50 others are misspelled on the live site. The fix is mechanical (<xx> is the latin1 codepoint) but should be a reviewed PR, not a blind sweep.
The 32 filename mismatches are a mix of the same encoding damage and genuine data drift (chile-santiago.json holds city "Santiago de Chile" and region "Metropolitana"; two files spell the state "Missisippi").
Deduplication this exposes
scripts/json_shema/chapter.json in the website repo and inst/schemas/chapter.json in jinx are byte-identical copies. They have not drifted yet, which is the argument for consolidating now rather than after they do. jinx should own it; the website's validate_jsons.R drops its chapter block.
Order of work
Website PR check— jinx side in progress- Repair the 57 mojibake files
- Repair the 32 filenames and 5
urlnamedisagreements chapter_reconcile()weekly report- Route team slug, repo name and mailbox address through one derivation
Deferred
Checking country against the canonical Countries list in the Airtable World base would need Airtable credentials in the website repo's CI. Better placed in chapter_reconcile(), which already runs inside jinx with those credentials.
🤖 Generated with Claude Code
Activity
feat(chapter): check chapter data on the PR that changes it
feat(chapter): check chapter data on the PR that changes it (#162)
drmowinckels commentedon Sep 27, 2026
Correction to the drift table above: those counts were measured against my local checkout of the website, which sits on
feat/cloudflare-web-analytics, notmain. The filename andurlnamefigures were wrong.Measured properly against
origin/main, all 384 files:3212urlnamevssocial_media.meetup56The 57 was right, and is fixed in rladies.github.io#610 — that PR takes encoding damage to zero.
Afterwards the filename count reads 14 rather than 12, which is not a regression: jinx skips the filename check on a file whose text it knows is corrupt, so
argentina-cordoba-cordoba.jsonandbrazil-esperito-santo-vitoria.jsonbecome visible once the encoding is repaired. Both have real, pre-existing problems.Step 2 of the order of work is done. Steps 3 (filenames and
urlname) and 4 (chapter_reconcile()) remain.The sixth
urlnamecase needs a human:switzerland-geneva.jsonhassocial_media.meetupofrladies-Genève, which can't be a real Meetup slug.drmowinckels commentedon Sep 27, 2026
Two more findings from repairing the data, both pre-existing and neither addressed yet.
121 chapter files never become pages.
content/chapters/_content.gotmplkeys each page path onurlname, and only 263 of the 384 files have one, so a localhugobuild produces 263 chapter pages. Whether those 121 should be rendered, or are deliberately hidden (many areprospective), is a question for the chapters team.Form boilerplate is reaching the data.
mexico-chiapas-tuxtla-gutierrez.jsonlisted the literal string"comma separated"as an organiser — leftover placeholder text from whatever form created it. That file also had a lowercase country, a slug in the city field, and a zero-width space in its filename, so it looks like one bad intake path rather than a typo. Worth finding which form produced it before thechaptersAirtable table starts feeding this directory.Both are fixed for that one file in rladies.github.io#611; neither is fixed at the source.
drmowinckels commentedon Sep 27, 2026
Retracting the "121 chapter files never become pages" point from my previous comment — that is the expected state, not a defect. A chapter that has not finished setup has no Meetup group yet, so it has no
urlname, so it has no page. Working as intended.Cross-tabulating confirms it:
urlnameurlname115 of the 121 are prospective, exactly as expected.
The residue is the part that might be worth a look: 7 chapters are
activebut have nourlname, so they render no page on the site.croatia-zagreb·germany-marburg·kyrgyzstan-bishkek·norway-trondheim·united-kingdom-sheffield·usa-mississippi-oxford·venezuela-caracasI do not know whether that is wrong. A chapter could legitimately be active and organise somewhere other than Meetup —
usa-mississippi-oxfordhas an emptysocial_mediaentirely. If active-without-Meetup is not supposed to happen, this is a good cross-field rule forchapter_reconcile(): warn whenstatusisactiveandurlnameis missing. If it is legitimate, there is nothing to do and the 5 prospective-with-urlname are just chapters whose status has not been flipped after setup finished.Leaving it as a question rather than adding the check.
drmowinckels commentedon Sep 27, 2026
Checked the 7 active-without-
urlnamechapters against meetup_archive/data/chapters.json (generated from the Meetup API, and the same file the site merges at build time). Six of the seven have a live Meetup group we simply never recorded.kyrgyzstan-bishkekrladies-bishkekunited-kingdom-sheffieldrladies-sheffieldusa-mississippi-oxfordrladies-oxford-msvenezuela-caracasrladies-caracascroatia-zagrebrladies-zagrebnorway-trondheimrladies-trondheimOnly
germany-marburghas no group.Widening it to the whole archive: 17 of the 251 Meetup groups are not referenced by any chapter file. Eleven belong to a chapter we already have, which is just a missing link:
RLadiesJeddah(412) ·rladies-bishkek(206) ·rladies-oxford-ms(109) ·rladies-sheffield(90) ·rladies-aarhus(73) ·rladies-kuala-lumpur(69) ·rladies-caracas(48) ·rladies-waltham(27) ·rladies-cincinnati-oh(16) ·rladies-albany-ny(5) ·rladies-geneve(7)Six have no chapter file at all — roughly 850 members invisible to the site:
rladies-muscatOman (420) ·rladies-kanoNigeria (169) ·rladies-comitanMexico (161) ·rladies-tlalnepantlaMexico (55) ·rladies-la-paz-mexicoMexico (35) ·rladies-wake-forestUSA (8)Two traps for whoever does this:
rladies-la-paz-mexicois notbolivia-la-paz.json, andrladies-oxford-msis Mississippi, notunited-kingdom-oxford.json. Matching on city alone pairs both wrongly — I did exactly that on my first pass.The archive also settles four of the six
urlnamedisagreements, since only one side of each exists as a real group:iran-orumiyehrladies-urmia(332)social_media.meetupmexico-san-cristobal-de-las-casasrladies-sancristobal(129)social_media.meetupmorocco-rabatrladies-rabat-ma(700)social_media.meetupsingapore-singaporerladies-singapore-sg(54)urlnameswitzerland-genevarladies-geneve(7)mexico-chiapas-tuxtla-gutierrezThe three
social_media.meetup-only fixes are safe. Theurlnameones move the chapter's page URL, since the page path derives fromurlname— a maintainer call.This is a good argument for
chapter_reconcile()doing exactly this join on a schedule.drmowinckels commentedon Sep 27, 2026
Tallied
urlname againstsocial_media.meetupacross all 384 files onmain:social_media.meetuponly, nourlnameurlnameonly, nomeetupSo they agree in 256 of the 262 cases where both exist, and
urlnameis never the field that is missing.social_media.meetuphas no consumers. Nothing in the theme or the site config reads it.urlnamedoes all the work: the page path in_content.gotmpl, the archive join inmerge_chapters, the event filter and the "Join chapter" button inchapters/single.html(which buildsmeetup.com/{{ .Params.urlname }}directly).social_media.meetupis a second copy of the same fact that nothing looks at — exactly the "derive, don't store" case from the top of this issue, and the reason all 6 disagreements went unnoticed.Proposal: delete
social_media.meetuponce the two values it uniquely holds are moved intourlname, and drop theurlnamecheck from the validator, because there is nothing left to disagree with.Correcting myself from an earlier comment: I said six of the seven active-without-
urlnamechapters had a group "we simply never recorded". Not true for two of them.croatia-zagrebandnorway-trondheimhad their group recorded all along, insocial_media.meetup— just not in the field the site reads, which is why their pages do not exist. The accurate split is:croatia-zagreb->rladies-zagreb,norway-trondheim->rladies-trondheimkyrgyzstan-bishkek,united-kingdom-sheffield,usa-mississippi-oxford,venezuela-caracasgermany-marburgdrmowinckels commentedon Sep 27, 2026
Correcting myself again, and this one matters: I said
social_media.meetuphas no consumers. That is wrong. I grepped the layouts forsocial_mediaand for.meetupand concluded nothing read it, but the consumer reaches it generically —chapters/single.htmlandchapters/list.htmlboth pass the wholesocial_mediamap topartials/social_media.html, which dispatches on the key name intopartials/funcs/social_media.html, wheremeetupbuildshttps://www.meetup.com/%s.So it renders a Meetup social icon, and the duplication is visible on every chapter page. A correct chapter shows the same Meetup URL twice:
And a wrong one shows two different broken links. Geneva, live today:
Neither is the real group. That is the cost of storing the same fact twice: one copy went stale and nothing compared them.
The conclusion is unchanged, but for a better reason.
urlnamestays canonical — it drives the page path, the archive join, the event filter and the join button.social_media.meetupshould go, and the icon disappears with it, leaving exactly one Meetup link per chapter instead of two.Separately, 31 chapters link to a group that is not in the archive: 23 retired (their groups are gone), 6 active, 2 prospective. Gating the link on
meetup_id— whichmerge_chapterssets only on a successful archive match — removes all 31.drmowinckels commentedon Sep 27, 2026
Pinged all 272 chapter
urlnames directly against meetup.com to see which groups actually exist. This works despite the bot wall: a real group's HTML carries<meta property="og:title" content="<Group Name> | Meetup">, a dead slug carries none. Two gotchas worth recording — mixed-case slugs answer 308 and must be followed (urllib does not by default), and a request failure must never be counted as dead.The single dead active is
switzerland-geneva(rladies-geneva), fixed in rladies.github.io#612. With that merged, every active chapter has a live Meetup group.The ping is a better signal than the archive, which is missing real groups:
rladies-tucson-az("R-Ladies Tucson AZ") and the Warsaw collaboration are both live but absent from it. It also settlesoman-masqat, which 308-redirects to R-Ladies Muscat, so that value was non-canonical rather than broken.The important find is that
statusis stale. Seven chapters markedprospectivehave live groups — they finished setup and nobody flipped the field:algeria-algiers·denmark-aarhus·malaysia-kuala-lumpur·usa-colorado-aurora·usa-colorado-fort-collins·usa-massachusetts-waltham·usa-new-york-albanyAnd four marked retired still have live groups:
benin-cotonou,brazil-parana-curitiba,switzerland-lausanne,usa-ohio-cincinnati. Either those chapters are still running or the groups were never closed.That matters for rladies.github.io#613, which gates the Meetup link on status: it is right for the 24 genuinely dead, but wrong for those 11.
Recommendation, in the spirit of one owner per fact: do not add a stored "group is live" flag and do not ping at build time — that is ~270 requests per build against a host that rate-limits, and a transient failure would either hide a good link or fail the build. Put the ping in
chapter_reconcile()on a schedule and have it correctstatus. Thenstatusis trustworthy, and the simple template gate in #613 is correct by construction.drmowinckels commentedon Sep 27, 2026
Profiled all 120
prospectivechapters against the Meetup archive'sevents.json(5,240 events) and the liveness ping.urlname, no group, no event everThe two at the bottom are not prospective by any reading:
usa-colorado-aurorahas run 21 events, most recently 2026-01-14, andusa-colorado-fort-collins5 events, most recently 2026-04-10.algeria-algiersran 3 but nothing since 2020-11. Four more —denmark-aarhus,malaysia-kuala-lumpur,usa-massachusetts-waltham,usa-new-york-albany— have a live group but have never run an event.No new status needed
The guide already defines the categories, in global-team/monitoring:
So "never started properly" is Unbegun, and it expires after six months — past that a chapter with no events is simply Inactive. The website's
statusfield (active/prospective/retired on DATE) does not use these words at all, which is its own mismatch worth resolving.Mapped onto that vocabulary, the rule is mechanical:
active(2 chapters)The blocker
That rule needs to know when a chapter applied, and we cannot currently tell. 118 of the 120 prospective files were added to git in a single bulk import on 2023-01-04, so the git date is the import date, not the application date. Only
mexico-chiapas-tuxtla(2023-05-31) andusa-colorado-aurora(2023-09-19) have a real one.The intake date belongs to the Airtable
chapterstable, which this issue already assigns as the owner of intake. Until that is populated, the six-month clock can only run for chapters that come through it — which is fine going forward, and means the existing 111 need a one-off human pass rather than an automated flip.Happy to build
chapter_prospective_audit()as a read-only report on the two signals we do have (events and group liveness), so the 9 interesting cases surface without touching the 111.drmowinckels commentedon Sep 27, 2026
Correcting my last two comments on this. I claimed the Meetup signal was unreliable because chapters had moved off the platform, and cited RLadies+ Oslo as a chapter that was quiet on Meetup but still running.
Both wrong. Chapters are asked to create their events on Meetup even when they advertise them elsewhere, precisely so activity can be tracked — so an absent event is a real gap, not an artefact. And Oslo is genuinely inactive; I inferred otherwise from the fact that its organiser was in front of me, which was not evidence.
So the numbers stand as measured: 2 chapters to promote, 20 quiet for 6-12 months, 310 with nothing for a year or more. 193 of those 310 are currently stored
active.One thing to fix in the guide while this is fresh:
global-team/monitoringstill tells a reactivating chapter "There is no need to use the platform meetup.com, please let us know what platforms you plan to use" (Email template D, point 4c). That is the opposite of the current expectation, and it is aimed at exactly the organisers we most need posting events. It is also what led me astray.