Decision log

Choices made and why, newest first. A decision recorded with its reasoning is what lets somebody six months from now disagree with it on the merits instead of re-deriving it.

Wed Sep 16 · permalink

"City-scoped" and "a place can hold a value" are different questions

probe/scope.py judges whether an indicator is something a city could report, and it is good at what it does: it reads the topic — health, water, education — and scores it municipal. It cannot see that "extent to which countries have laws and regulations that guarantee full and equal access to sexual and reproductive health care" measures a legislature, not a place. The topic is municipal; the subject is a country.

28 of the 67 US-silent "city-scoped" indicators were country-as-subject, 18 of them one family of SDG 5.6.2 legal provisions — and the matcher paired every one with NYC's Local Law 37/2011, on the token "Law".

The filter for this is lexical, not a second classifier: an indicator beginning "extent to which…", "countries that…", "proportion of countries…" is excluded, while "extent of human made wetlands" is not. The distinction is a property of how the sentence is phrased, and a rule that can be read and argued with beats a score that cannot. The published count went 67 → 39 as a result.

Tue Sep 15 · permalink

Match against the 519 named indicators, not all 689

170 of the 689 base indicators carry no name in any source we hold: they return neither metadata nor observations. The inverse crosswalk originally fell back to the DCID mnemonic, so a quarter of "the framework" was represented by query strings like DI ILL OUT.

Nothing can match that. It inflated the tail — datasets looked further from the framework than they were — and put mnemonics in the nearest-indicator column, where VC VAW SXVLN was reported as a cluster's closest SDG concept. Excluding them moved every weak positive control up by 4–16 percentile points.

The count of what was excluded is printed on the page. An analysis that quietly matches against garbage text and reports the resulting distance as a finding is measuring its own inputs.

Tue Sep 15 · permalink

Pool the non-English catalogs into one run, never one run per language

Cosine similarities from two different embedding models are not comparable, so the language groups cannot be merged after the fact. But per-language runs would give five score spaces with two to four cities each, and the evidence unit in this analysis is how many independent cities a theme appears in — which two cities cannot support.

Pooled under the multilingual model they share one space and eleven cities. The cost is that the weakest language sets the floor: Portuguese controls land at the 32nd and 44th percentile against 89th for Italian, so the Brazilian portion of the tail is the least trustworthy part of the run, and the page says so.

Tue Sep 15 · permalink

A cluster carries its own coherence, or it is not a finding

k-means returns k clusters whether or not k themes exist in the data. The inverse crosswalk's first run reported 24 of them as "themes"; reading the rows showed roughly half were mush — one labelled "school" held building violations and lobbyist registrations.

Every cluster now carries the mean cosine of its members to its own centroid, and below 0.62 it is published as diffuse rather than read as a theme. Cluster labels also require a term to cover a fifth of the members, not merely to be distinctive: lift alone labelled a 144-member cluster "inch, sea, rise" off a handful of coastal datasets.

The rule generalises past clustering: any method that always returns an answer has to be made to report how much of an answer it actually found.

Tue Sep 15 · permalink

Evidence in the inverse crosswalk is counted in cities, never in datasets

A dataset far from every SDG indicator may be a gap in the framework, or may be a dataset the matcher missed — and we cannot tell which from the dataset alone. A positive control proved it: NYC's Housing Maintenance Code Violations is a hand-verified match for 11.1.1 and lands at the 24th percentile, inside the tail.

So a theme counts only through the number of independent city catalogs it appears in. One city publishing forty parking files is a filing habit; twenty cities each publishing one is a category of municipal governance. This is the same discipline as reporting a table across all cases rather than testing one, applied to a corpus instead of a function.

Tue Sep 15 · permalink

An indicator is its own control group

The smell test's first sweep returned 41,350 findings — 5.4% of every observation in the graph, which is a broken instrument rather than a result. 23,172 of them were negative percentages, and nearly every one was correct: the Percent unit covers bounded proportions and signed growth rates, balances and changes, with nothing in the unit string to separate them.

What separates them is frequency. A rule broken by most of an indicator's observations is its definition; a rule broken by three country-years out of three thousand is an error. So checks are suppressed per-indicator when they fire often enough to be structural, and the outlier check compares each value against its own indicator's 99th percentile rather than against what a unit is supposed to mean.

Suppressions are published, not dropped. The list of indicators that publish signed values under a Percent unit is a finding about the graph's vocabulary, and a report that hid it would look cleaner than the data is.

Tue Sep 15 · permalink

Gate checks on the shape of the data, never on a list of units

The first attempt to quiet the jump check whitelisted percent/count/rate units. It worked, and it silently discarded Mauritius' food waste going 207 tonnes to 177,570 tonnes in one year, because WEIGHT_TN was not on the list — precisely what the check exists to find. Unit vocabularies are long, inconsistent and not ours to enumerate.

The durable test is a property of the values: does this indicator ever go negative anywhere? If so it has no meaningful zero and no ratio on it means anything. That one question replaced the whitelist and costs nothing to maintain.

Tue Sep 15 · permalink

The launch check is a diff against a recorded baseline, not a health check

A health check asks "does it answer?" and the platform would have passed one all week. The thing that could break the demo is subtler: a DCID quietly withdrawn or redefined, which returns an empty result — the same shape as a country that does not report. So probe/launch_diff.py records a full snapshot (every crosswalk variable, every series value by value, every peer comparator, the enumerated corpus, the tool list) and compares the next run against it field by field.

Two consequences worth keeping. The baseline is only updated deliberately (--set-baseline), because a checker that re-baselines on every run reports "no change" forever. And the diff has its own self-test (--self-test), which injects each drift class into a copy of the baseline and asserts it is reported: a diff that returns "no change" and a diff that cannot see change look identical from the outside.

Tue Sep 15 · permalink

Hardcode human-resolved DCIDs; never resolve by search at runtime

Between 14 and 16 Sep, with no code change on our side, search_indicators returned 44 candidate variables for our fifteen topic queries and then 56 — nine topics gained, none lost, and every row present on both dates identical in every field. The graph did not move; retrieval over it did. Notably, "road traffic deaths" began returning undata/sdg/SH_STA_TRAF, the exact DCID our road-deaths pair uses, which it had not on the 14th.

Discovery through search stays the rule (REST is federated and will answer for other publishers). But once a human has resolved and graded a DCID it goes in crosswalk.json as a literal. A demo that resolves by search at showtime can change its answer while the data stands still.

Sun Sep 13 · permalink

The human grade vetoes every mechanical check

The pair probe initially reported BLOCKED and CONTEXT pairs as chartable: resolution, unit and overlap checks all passed, so it waved through pairs a human had judged incomparable. Now the grade overrides, and a trend requires five overlapping years rather than a shared endpoint.

Kept as a decision because the pull to let the automated check "win" will recur every time someone adds a pair. Units agreeing is necessary and nowhere near sufficient.

Sun Sep 13 · permalink

Missing denominator years stay empty

There is no ACS 1-year release for 2020, so NYC rates have no 2020 value — on the year homicides spiked. Interpolating would produce a number visually identical to a measured one, in precisely the case someone would quote it. The probe records dropped years explicitly.

Sun Sep 13 · permalink

Comparators are tiered; city-level is the exception, not the rule

URBANIZATION--DOU_CITY gives a genuine NYC-vs-national-city-aggregate comparison across many countries, but only for five indicators — it clusters in gridded-geospatial families and the administrative indicators have no spatial dimension. Tier 1 (city aggregates, ~5 indicators), Tier 2 (urban/rural, ~8, low NYC relevance), Tier 3 (national totals, everything else).

Plan against Tier 3 as the default. An earlier version of this entry recommended city aggregates as the primary framing, generalising from PM2.5 alone; corrected the same day.

Caveat to carry onto any Tier 1 chart: NYC sits inside the US city aggregate, roughly 7% of it.

Superseded framing, kept for the record. The question was first posed as: compare NYC to peer cities (which needs a non-UN source) or to nations? The DOU_CITY finding gave a third and better answer, which is the decision above.

⚠️ This paragraph was its own entry in the old single-file decision log, headed "Superseded — peer-comparator framing" and carrying no date. It is folded in here rather than given one: a dateless entry cannot be ordered, and inventing a date to make it sort is the same defaulting this project refuses everywhere else. It is the history of this decision, so it lives inside it.

Sun Sep 13 · permalink

The pair probe does not assign comparability grades

The probe verifies that a mapping still resolves and that units agree. The grade itself stays a human judgment recorded in crosswalk.json. A machine that scores comparability would be confidently wrong exactly where it matters — the waste pair has agreeing units and is still not an apples-to-apples comparison, because NYC counts residential collection and the UN counts all municipal waste.

Sun Sep 13 · permalink

Road safety is out as the headline demo indicator

SDG 3.6.1 returns one observation for the US (2021). Our application used road safety as the worked example. Replacing it with a GREEN-graded indicator from the coverage report; homicide rate and municipal waste recycling are the strongest candidates because NYC publishes closely matching series.

Sun Sep 13 · permalink

Collaboration hub is a fetchable site, not an MCP server

A URL works in every Claude surface with zero setup for collaborators. An MCP connector is richer but each person has to configure it, and it is only available on some surfaces. The site is a prerequisite for the MCP option anyway, so this is sequencing rather than exclusion.

Sun Sep 13 · permalink

Probe harness is stdlib-only Python

Three people on different machines need to run it without a virtualenv debugging session. certifi is used when importable and falls back to the system trust store.