Skip to content

VALIDATION: independent review of the 2026-09-07 schema-decisions, rename and manifest-driven validator work #127

Description

@mmcky

Everything below was verified during the work by the same agent that did it, in one session on 2026-09-06/07: the four schema and naming decisions (#120, #121, #122, #113), the rename of business_cycle_data.csv, the shared manifest-driven validator and its PR check (#119), the close of #14, and the tracker and dashboard updates around them. Model for this issue: #45 and its validation run (#46 was found in the margin, not on the list).

Bias to test for. The session's verification monoculture is the validator it wrote: scripts/validate_datasets.py over builders/_validate.py, run locally through uv run under pandas 2.3.3 and 3.0.5, plus a scratchpad mutation script that was never committed. Every "44 of 44 pass" claim, both builder dry-runs, and the manifest facts the decisions rest on (the dtype tally, the null-position table, the 212-column comparison) were produced by that code or by ad-hoc pandas one-liners from the same author in the same environment. The second monoculture is a curl loop for the published-site probe. Confirm reader-facing outcomes without either: fetch URLs from a different tool, read files with a different reader, re-derive every count from the bytes, and run the validator only where a check says so — and then look for what it cannot see.

What landed

Repo Merged Issues Other writes
QuantEcon/data-lectures #124 (c4a286c, squash of two commits incl. Copilot fix a2feea8): rename + capture groups + nulls: blocks + dtype sweep + schema/AGENTS text, 24 files. #126 (818811b, squash incl. Copilot fix be76a6a): builders/_validate.py, scripts/validate_datasets.py, .github/workflows/validate-datasets.yml, both dynamic builders and _template.py rewired, countries.csv.yml and us_business_cycle_monthly.csv.yml contract fixes, 13 files. (#125, a one-line AGENTS.md change, merged between them and is not this session's.) Closed: #120 #121 #122 #113 (by #124), #119 (by #126), #14 (by hand, with a closing table). Decision comments on #120#122, #113; a correction on #121; two revision comments on #118; stamp of #14 re-dated then closed No data bytes changed; no tags pushed anywhere
QuantEcon/status-projects #64: projects.yml data-lectures-automationdone, ended: 2026-09-07 Opened #67 (a "Recently closed" strip)
QuantEcon/workspace-lectures Comment on #40: first verification pass, 0 of 60 Track X rows cleared, per-host publish table, the .ml keep_files finding
QuantEcon/lecture-wasm Comment on #70: the four final URLs and a call-by-call mapping

1. Reader-facing outcomes

The one thing a reader can see is a URL that stopped resolving. business_cycle_data.csv had been served publicly since 2025-02 (QuantEcon/data, then this repo) and the session renamed it on the strength of the manifest's consumers: []no org-wide sweep was run, which the workspace rules require before deleting anything another repo might read.

  • https://raw.githubusercontent.com/QuantEcon/data-lectures/main/lectures/business_cycle_data.csv answers 404 and …/lectures/gdp_growth_annual.csv answers 200 with sha256 41df23233ea238e4166c5c21cc383791015d4c9af6868b840901a8ce083c9da8 (the manifest's recorded value; compute it from the fetched bytes, not from the file in a clone).
  • Nobody reads the old name. gh search code 'business_cycle_data org:QuantEcon' (the token is distinctive, so search works here) returns only this repo's dated history notes (PLAN.md, migration.yml, scripts/audit_annotations.yml, the manifest header). Then the sweep search cannot do: a Trees-API pass over all ~277 org repos' default branches for the basename, including the translation repos, lecture-wasm's non-default branches (wasm, any repoint/*), course forks (tom-econ370-2025, 2026-tom-course) and .notebooks mirrors. Expected: zero readers outside this repo. Record the repo count swept. Swept 2026-09-07 (Sweep every QuantEcon repository for a reader of business_cycle_data.csv #134). Enumeration reconciles with the org: 215 public + 71 private = 286, so private and archived repositories are in scope and were swept. 283 of the 286 were swept twice — a recursive Trees-API pass over every default branch (34,179 entries, truncated: false on all 283, so no depth-1 fallback was needed) and a depth-1 clone with git grep for content, which is what covers the 26 archived repos and 3 forks that code search does not index. The 3 not swept (test-cli, numfocus, quantecon-book-dp) are empty — size: 0, zero branches, Trees returns HTTP 409 Git Repository is empty. Zero path hits for business_cycle_data anywhere in the org. The 14 content hits are all in this repository and all the dated history this box exempts (PLAN.md 27, 34, 301, 308, 309, 324, 343; AGENTS.md:58; builders/business_cycle.py:20; manifest-schema.yml 49 and 228 — a survival the checklist did not name; migration.yml:839; scripts/audit_annotations.yml:48; the gdp_growth_annual.csv.yml header). gh search code independently returned 9 lines, all from this repository. An adversarial re-run then closed three gaps beyond the box: every branch of the 8 audit repos and this one (268 branches, 43,118 entries) plus a full-history all-refs content grep of those 8 (883 refs, including lecture-wasm's 16 and its gh-pages) — the only path hits are 12, every one inside this repository on stale pre-rename branches; gh-pages org-wide (39 repos, 18,079 files), which no pass had covered and which is where the published site actually lives; and the org's only two submodules (both in lecture-mapping), which neither the Trees API nor a non-recursive clone expands. All zero. Residual, named rather than closed: non-default branches of ~245 low-risk repositories, fork PR heads outside the audit repos, and any out-of-org or non-GitHub reader.
  • The old URL at the previous org and repo namesgithub.com/QuantEcon/data/raw/main/lectures/business_cycle_data.csv and the pre-flatten path …/data/raw/main/business_cycle/business_cycle_data.csv if it existed — either 404 or redirect to a 404; neither serves bytes that would let a stale reader keep working silently.
  • The live audit dashboard https://quantecon.github.io/data-lectures/ still reports 40 static files, 40 migrated, 1 orphan, 23 live-API lectures (re-read from audit.json's stats, not the page), and CATALOG.md on main has a gdp_growth_annual.csv row and no business_cycle_data.csv row.
  • The status-projects home page https://quantecon.github.io/status-projects/ shows, under the infrastructure programme, a collapsed row reading "3 completed projects" and, expanded, data-lectures automation as "done September 2026"; the Pipeline page's done tab lists it newest-first above data-lectures scaffolding; its project page shows "ended 2026-09-07" and "4 of 4 work items closed" with no compliance findings (the scaffolding row carries three; the automation row must carry none). Reworded and confirmed 2026-09-07 (Decide how each of the eight open #127 boxes closes #133) against the live rendered DOM — these pages render from JSON client-side, so the served HTML carries none of this text. The collapsed row reads "▸ 3 completed projects": upstream-delta-register was set done/ended 2026-09-07 by QuantEcon/status-projects#53 at 00:41:34Z, 3h13m before PLAN: record the P3 data half, and retract a box that is now wrong #64 did the same here. Every other clause holds. Two wording notes: the visible work-items label is "all 4 closed" ("4 of 4 work items … closed" is its tooltip), and the done-tab tie order among the three 2026-09-07 projects rests on Array.prototype.sort stability over registry row order, not a documented rule.

2. Artifact integrity

  • Every data file under lectures/ on main hashes to its manifest's integrity.sha256: run python .github/scripts/check_consumed_files.py and an independent shasum -a 256 over the 44 files compared against grep sha256 lectures/*.yml. Expected 47 files hashed (44 datasets plus 3 under sources/ as LFS pointers), 0 mismatches. The two PRs claim no bytes changed: git diff --stat e318f06..818811b -- 'lectures/*' ':!lectures/*.yml' must show only the rename (R100).
  • The renamed pair is a pure rename: git log --follow --oneline lectures/gdp_growth_annual.csv reaches back to the 2026-07-16 flatten (Flatten the consumer-keyed tree into the published layout #10) and the blob hash at c4a286c equals the blob hash at e318f06 for the old path. Confirmed as written 2026-09-07 (Decide how each of the eight open #127 boxes closes #133) on a full clone: the box's own command returns 5 commits and reaches 52dbb89, the merge commit of Flatten the consumer-keyed tree into the published layout #10 (mergedAt 2026-07-16T22:45:04Z), then one further to b857c5c (2025-02-16); e318f06:lectures/business_cycle_data.csv and c4a286c:lectures/gdp_growth_annual.csv are the same blob 29250cae27e1422a69d0e42aabc7502ba08a14e5; the rename diff is exactly one R100 line. The verification comment's contrary finding — single root 931d626, 50 commits, no flatten commit — is a shallow-clone artefact: 931d626 is an ordinary commit with parent c044b8c and is precisely the 50th ancestor of 818811b, the head VALIDATION: independent review of the 2026-09-07 schema-decisions, rename and manifest-driven validator work #127 was written against. git clone --depth=50 there reproduces that finding bit for bit. The sole root is 77ece40 (2025-02-09); main carried 104 commits at 47017ea.

3. The highest-value claim to re-test: the validator catches what it says it catches

Every future refresh PR and every future manifest edit is gated by builders/_validate.py. The session proved it with a mutation script it never committed, on frames it built itself. Reproduce from scratch, in a fresh clone, without that script:

  • python scripts/validate_datasets.py on main under pandas 2.3.3 prints 44 manifest(s): 44 pass, 0 fail with conformance-only: {'xlsx': 5, 'npy': 2, 'dta': 4, 'json': 1, 'xls': 1}; the same under pandas 3.0.x (the lectures' anaconda=2026.07 pin). Record both pandas versions used.
  • Break one thing per rule, each as a one-line edit to a copy of the committed CSV or its manifest, and confirm the named failure: (a) append a column extra to gdp_growth_annual.csv → "unexpected column(s) not claimed"; (b) blank one Country cell in the same file → "Country: 1 nulls but not declared under known_nulls" (this is the Copilot-review fix in Manifest-driven validate(): the schema block is the spec, shared by the builders and a PR check #126, be76a6a); (c) blank DEU's YR2000 → "hole at YR2000 inside the series"; (d) blank DEU's YR2025passes (recent: 2); (e) blank UNRATE at 2020-05-01 in us_business_cycle_monthly.csv → "hole at 2020-05-01 inside the series is not declared under nulls.inner" — this one matters most, because Manifest-driven validate(): the schema block is the spec, shared by the builders and a PR check #126 removed the exact count that used to catch it, so placement alone must; (f) blank UNRATE at 2026-07-01 (the newest row) → passes (recent: 1); (g) blank INDPRO at 1919-01-01 → passes the shared validator (leading nulls allowed) and fails builders/business_cycle_fred.py's own validate() with "INDPRO: first observation is 1919-02-01"; (h) drop the YR1990 column → passes the shared validator (a regex cannot see a grid) and fails business_cycle.py's validate() with "gap in the year grid"; (i) set lingcod_msy_recovery.csv.yml's known_nulls to {F_over_Fmsy: 2} → "1 nulls, manifest says exactly 2". Cases (g) and (h) are the documented builder-layer margin; if either passes both layers, that is a regression.
  • The CI job actually gates: on a throwaway branch, commit mutation (a) and open a draft PR; validate-datasets must go red with a ::error file=lectures/gdp_growth_annual.csv.yml:: annotation in the checks tab, and consumed-file-check must go red too — mutation (a) rewrites the data file, and check_consumed_files.py hashes every file with a recorded sha256 whether or not consumers is empty. To see validate red while consumed-file-check stays green, use a manifest-only mutation such as (i). Close the PR without merging. Reworded and confirmed 2026-09-07 (Decide how each of the eight open #127 boxes closes #133) from the API: at 5e23ade (mutation (i)) validate failed (34085718978) and consumed-files succeeded (34085718933); at 93d0173 (mutation (a)) both failed (34085829077, 34085829084, the latter with gdp_growth_annual.csv: bytes do not match the manifest). The annotation path is confirmed through the check-runs API — gh run view --log renders ::error file=X::msg as ##[error]msg and drops the path, so the log alone cannot show it. Throwaway, do not merge: #127 §3.3 CI-gating check with a deliberate manifest mutation #130 is closed, not merged; its branch carries neither mutation.
  • The two builders still run end to end against the live sources, from a runner-like environment if possible (FRED stalls custom User-Agents from GitHub-hosted runners, builders/_fred.py docstring): python builders/business_cycle.py --out-dir /tmp/wb --summary-json /tmp/wb.json exits 0 and reports 0 of 585 cells revised for gdp_growth_annual.csv unless the World Bank has moved; python builders/business_cycle_fred.py --out-dir /tmp/fred --summary-json /tmp/fred.json exits 0 and its summary's overlap.new_columns lists the month(s) after 2026-07-01. Then python scripts/snapshots.py pr-body us_business_cycle_monthly.csv --summary /tmp/fred.json renders a title of the form Refresh us_business_cycle_monthly.csv: 2026-07-01 → 2026-0M-01.
  • The Monday canary is the real test the session could not run: the refresh-snapshots run of 2026-09-07 ~06:17 UTC (or the next one) has two green canary legs (one per builder) with the shared validator in the path, and the FRED leg's log shows the placement rule accepting the newest month's three blanks rather than failing on them. If it failed on "UMCSENT: N nulls" or "hole at 2026-0M-01", the recent: 1 fix did not reach the runner path.

4. Records written

  • All 44 sidecars, manifest-schema.yml and migration.yml parse with PyYAML; every filename equals its sidecar's name; every class: dynamic-snapshot manifest (4) has a nulls: block whose keys are a subset of {along, leading, recent, ended, inner}; no manifest has known_nulls_total at schema level (it survives only inside sheets[].known_nulls_total on assignat.xlsx.yml (4) and dette.xlsx.yml (5), each with read_as.header: null).
  • The dtype sweep was complete and did not over-reach: grep -h "dtype: " lectures/*.yml | sort | uniq -c shows no string, object or bare datetime; float32 remains only in the four .dta manifests; the 2 datetime64 in the hansen files were already there. Re-derive the sweep count the session claimed (21 string, 9 object, 2 datetime → 32 edits) from git diff e318f06 c4a286c -- lectures/.
  • The three capture-group patterns are 'YR(\d{4})' in gdp_growth_annual, unemployment_rate_annual, private_credit_to_gdp, and re.compile(p).groups == 1 for each; chapter_3.xlsx.yml's sheets_pattern: 'Table3\.\d+' was deliberately left alone (sheets, not columns) — confirm it still has no capture group and the conformance pass does not complain.
  • countries.csv.yml now declares Country code: 1 for Namibia: confirm with a reader that is not pandas (e.g. awk -F';' '$4=="\"NA\""' lectures/countries.csv or csvkit) that the bytes hold the string NA for Namibia and that pandas with keep_default_na=False yields 0 nulls in that column. Then check the two consumers named in the manifest — lecture-python-programming/lectures/pandas_panel.md and lecture-python.myst — for whether either uses Country code such that Namibia dropping out changes a figure or a table row on the published site.
  • forbes-billionaires.csv.yml now says government is bool: confirm the bytes hold True/False/empty (2606 empties of 2935 rows) and that its consumer lecture-python-intro (heavy_tails) never reads that column.
  • us_business_cycle_monthly.csv.yml says recent: 1 and known_nulls: {} and its comment gives first-vintage totals UNRATE 349, UMCSENT 616, CPILFESL 457, M0892AUSM156SNBR 1132; re-derive all four from the committed bytes with a non-pandas count of empty cells per column.
  • PLAN.md Phase 5's PR-validation box is ticked with a 2026-09-07 note and the two boxes below it (consumer fan-out, reusable workflow) are still unticked; manifest-schema.yml's header no longer says "nothing validates against it".

5. Tracker consistency

6. Known blind spots

  • Non-CSV formats are conformance-only (13 manifests: 5 xlsx, 4 dta, 2 npy, 1 json, 1 xls). Confirm the workflow log says so in words, and pick one — fp.dta (float32 columns) or dette.xlsx (positional ranges) — and check by hand that its manifest's dtypes/shape/known_nulls_total claims are true of the bytes, since nothing now or before does.
  • The published-site probe shares the settle policy's blind spot. The ws#40 comment says 0 of 60 rows cleared and attributes it to no publish tag since 2026-09-01. Re-derive: for each of the 8 hosts, the newest publish*-triggered run via gh run list --workflow publish.yml (tags are not date-ordered), and re-probe 5 rows per host with a different client (wget --spider or a browser). If any host published since 2026-09-01 and still serves, the cache theory needs revisiting. Ran 2026-09-07 (Re-probe the eight published hosts and record each host's newest publish run #135); the per-host table is on QuantEcon/workspace-lectures#40, which owns the rows. A census of all 60 rows, not a sample, on two clients that are not curl (wget, then Python urllib): 0 cleared, and every served body is byte-identical (sha256) to the pre-deletion git blob, so nothing cleared and nothing changed. Controls discriminate on all 8 hosts — eight distinct /intro.html bodies, one identical 9,379 B stock Pages 404. The 60 is now derived rather than asserted: the eight Track X commits removed 64 files, 60 of them under _static/lecture_specific/. The attribution is measured, not inferred — cache-buster GETs on a fresh CDN key returned identical bytes, and for the 3 gh-pages hosts the deleted file is still physically in the deployed tree. Seven hosts have not published since the deletions; .ml has deployed three times — including on the Track X commit itself — and still serves all six rows, which is positive proof of the structural keep_files: true exemption rather than an inference, so the cache theory is not impugned. Two things changed since the 00:30Z pass: the rebuild half of the gate is now met on all seven cache-bearing hosts (this morning's cache.yml runs), so only the tag is outstanding anywhere; and the deploy mechanism is now verified for all eight — quantecon/actions/publish-gh-pages is upload-pages-artifact + deploy-pages, a full replace, so every non-.ml host will prune on its next publish. Method note: gh run list createdAt is the run start, not the deploy; gh api repos/<r>/deployments?environment=github-pages is authoritative and works for all eight.
  • .ml never clears: lecture-python-programming.ml/.github/workflows/publish.yml deploys with peaceiris/actions-gh-pages@v4 and keep_files: true; confirm on the gh-pages branch that _static/lecture_specific/pandas/data/test_pwt.csv is still present in the tree, and check whether any other repo in the org deploys with keep_files: true (the session did not look).
  • The org sweep the rename skipped — item 1's second box — is the largest blind spot of the session; treat a hit there as a regression to file, not a caveat. Closed 2026-09-07 (Sweep every QuantEcon repository for a reader of business_cycle_data.csv #134): 286 repositories enumerated, 283 swept, zero hits outside this repository. No regression to file. See item 1's second box for the method.

7. Decisions settled today (re-checkable facts)

  • Under pandas 3.0.x a default read_csv yields dtype str for text and datetime64[us] with parse_dates; under 2.3.3, object and datetime64[ns]. Of 212 named CSV columns, 169 match exactly under pandas 3 and 142 under 2.3.3 before the sweep (at e318f06); after it, the family compare gives 44/44. Re-derive the 212 and the two match counts.
  • The FRED composite on 2026-09-07 ran to 2026-08-01 with only UNRATE and USREC populated in that month (the basis of recent: 1). Re-fetch https://fred.stlouisfed.org/graph/fredgraph.csv?id=UMCSENT,CPILFESL,INDPRO and check whether August has since been published; either way the rule stands. Re-measured 2026-09-07 (Record the date the FRED August publication lag closed #136) and confirmed: UMCSENT, CPILFESL and INDPRO all still end 2026-07-01, while UNRATE and USREC carry August — the exact state the rule was written against, and today's canary (run 34092578823) reports the same frame end independently. The lag had NOT closed when this issue was closed out, so there is no date to record; FRED's calendar puts the next releases at CPILFESL 2026-09-11, INDPRO 2026-09-18 and UMCSENT 2026-09-25, so the earliest full close is 2026-09-25. UMCSENT's 2026-08-28 release delivered no new observation at all (its ALFRED vintages for 08-27 and 08-31 are identical), so that date is the least reliable of the three. The date the lag closed is carried as an accepted residual, not a gate — the rule stands either way.
  • Namibia's ISO alpha-2 code is NA and pandas' default na_values includes the string NA (pandas docs, read_csv na_values).
  • The weekly cache.yml cron is 0 3 * * 1 UTC on lecture-python-programming, lecture-python.myst, lecture-python.zh-cn, lecture-python-programming.zh-cn; lecture-dp, .fr and .fa had push-triggered clean rebuilds on 2026-09-04.

8. Deliberately not done

Where the reasoning lives

AGENTS.md ("Naming a published file", "The schema block is executable", "Builders"), manifest-schema.yml (the comments beside filename, pattern, dtype, known_nulls, nulls), PLAN.md Phase 5 and Phase 8 P4, the decision comments on #120 #121 #122 #113 (with the #121 correction), the closing table on #14, the work plan #118, and the PR descriptions of #124 and #126 (each lists what it deliberately left out).

For the validator: work in a session that did not do this work. Do not use the tool named under "Bias to test for" except where a check explicitly says to run it. Re-derive counts rather than confirming them. Where a check can be run against a surface the original session did not exercise, do that too — the margin beyond the checklist is where regressions hide. Deliver: one comment on this issue with a per-item verdict (confirmed / confirmed with caveat / refuted / not completable, with evidence), a new issue for any regression found (do not bury findings in the comment), and leave the checkboxes to the issue owner unless told otherwise.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions