activity: compute the Activity rows at build time, with tests - #285
Conversation
Add _plugins/activity/rows.rb, a pure Ruby module that ports the round-2 handoff's display rules (buildRows, week, range, railRows, byWeek, byMonth and panelSummary) with the amendments and decisions recorded on #276. A generator, _plugins/activity_generator.rb, exposes the result to Liquid as site.data.activity_view: every row, the rail's week groups, the log's months and days, the panel summary, the strip and the first date. Its header comment documents every key and field, the contract that the Activity views will build on. - A complete sort order, so no output depends on file or entry order: type, then project (lower-cased, then exact), version, url, summary and the PRs. - One row per series per day. A lecture series or book that published twice becomes one ordinary row, a series' translations become one editions row, and a day's releases form one row whose title lists each project once. The copies of one release (the same url on the same day) merge first, keeping every summary and PR. - A book type, after lecture updates within a day ("Book update:"). - A stable row id from the merge key, such as 2026-08-02/release. - Dates stay calendar days and every label is precomputed. Nothing is converted to Time, since Jekyll sets TZ to Australia/Sydney. - Rows hold plain, unescaped strings: the templates escape every field. - Bad data stops the build: an unknown type, a date that isn't a YYYY-MM-DD day, or a malformed file or entry. The generator logs the message, which names the file and entry, as the build's first error line, then re-raises the error. Empty data gives empty views. Nothing renders the view yet, so the site is unchanged. Part of #276. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.github/scripts/test-activity.rb is a minitest suite run with plain ruby: minitest ships with Ruby but isn't in the Gemfile. It checks the acceptance numbers on .github/scripts/fixtures/activity/, a verbatim copy of _data/activity/ at 4c47f03 (23 files, 40 entries): the rail under each filter, the strip, the panel ("6 updates in September · latest Sep 27") and the log's 30 rows (6/8/11/5). It also covers shuffled entries and files, Date against string dates, timezones, ISO weeks, row ids, a synthetic case for each merge rule, names A to Z ignoring case, the version, url and PR-url tie-breakers in either input order, the book type, bad and empty data, and an entry full of HTML-special characters, and it checks invariants on the live data. Two synthetic days carry most of this: a busy day that needs every merge rule, and an order day whose names sort differently byte by byte and ignoring case. The build job runs the suite right after the data check, and keeps its name: build is the required check. Part of #276. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README gains an "Activity rows" subsection: what the view holds, the row order and merges, the tests and the fixture, and a note that the interim /activity/ page keeps its file order until #277 switches it to the view. copilot-instructions.md lists _plugins/ and the generator, replaces "No formal testing infrastructure exists" with the checks CI runs and how to run them, and explains an "Activity data: ..." build error. The list of checks states no count, so a check added later needs only a new line. Part of #276. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
✅ Deploy Preview for grand-swan-ca5201 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It introduces a large, algorithmically intricate build-time plugin that gates the site build and is the shared foundation for several downstream PRs, so a human should give final sign-off despite its extensive test coverage.
Review effort: Balanced
Findings: None
What changed in this PR
This PR ports the round-2 Activity display rules into a build-time Jekyll plugin. It adds a pure Ruby module (_plugins/activity/rows.rb) that groups the entries in _data/activity/*.yml into rows (with same-day merges, a complete input-order-independent sort, stable ids, and precomputed date labels), a Jekyll::Generator that exposes the result to Liquid as site.data.activity_view, and a minitest suite run in CI. Nothing renders the view yet, so the published site is unchanged; this is foundational work for downstream PRs (#277–#280) that will consume the view. It closes #276 and is part of #271.
Changes:
- New
_plugins/activity/rows.rb(grouping/sort/merge/id/label rules) and_plugins/activity_generator.rb(generator that setssite.data.activity_viewand fails the build loudly on bad data, naming the offending file/entry). - New
.github/scripts/test-activity.rb(51 tests) plus a frozen fixture copy of_data/activity/, wired into the existingbuildjob inbuild.yml. - Documentation updates in
README.mdand.github/copilot-instructions.mddescribing the rows plugin, the CI checks, and the build-error message.
| File | Description |
|---|---|
_plugins/activity/rows.rb |
Pure module computing the grouped Activity rows/rail/months/panel/strip with a total sort order and stable ids. |
_plugins/activity_generator.rb |
Jekyll generator that sets site.data.activity_view; logs and re-raises DataError. Header comment documents the view contract. |
.github/scripts/test-activity.rb |
51 minitest tests over the frozen fixture, synthetic cases, and live-data invariants. |
.github/scripts/fixtures/activity/*.yml |
Frozen, byte-identical copy of _data/activity/ (23 files) for fixed-number tests. |
.github/workflows/build.yml |
Adds a "Test Activity rows" step after the data check in the build job. |
README.md |
Adds an "Activity rows" subsection describing the plugin, ordering/merge rules, and tests. |
.github/copilot-instructions.md |
Documents _plugins/, the generator, the CI checks, and the Activity data: build error. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This PR computes the Activity rows at build time and exposes them to Liquid as
site.data.activity_view, with tests that run in thebuildjob. Every Activity view (the/news/rail, the home strip, the restyled/activity/and the feed) builds on one set of grouped rows, and Liquid can't do that grouping reliably: it needs a regex and keyed merges. The code ports the round-2 handoff's display rules with the issue's amendments and the build decisions recorded on the issue. Nothing renders the view yet, so the site is unchanged.What changes
_plugins/activity/rows.rb, a pure Ruby module with no Jekyll orTimedependency (stdlibdateonly). It ports the round-2 handoff'sbuildRows,week,range,railRows,byWeek,byMonthandpanelSummary, with the issue's amendments and the decisions:id: the merge key, such as2026-08-02/release,2026-08-03/translation/python-programming-for-economics-and-financeor2026-09-27/lectures/intermediate-quantitative-economics-with-python.Time. Weeks useDate#cwyearand#cweek, and every label is precomputed ("Sep 27", "Sep 21–27",2026-W39, "September 2026", ISO dates)._plugins/activity_generator.rb, aJekyll::Generatorthat setssite.data["activity_view"]. Its header comment documents every view key and row field, the contract that Restyle /activity/ with the shared rows, a type filter and day links #277–Publish an Activity RSS feed at /activity/feed.xml #280 build on. When the data is bad, it logs the message, which names the file and entry, as the build's first error line..github/scripts/test-activity.rb, 51 minitest tests run with plainruby. Their frozen fixture,.github/scripts/fixtures/activity/, is a verbatim copy of_data/activity/at 4c47f03 (23 files, 40 entries; byte-identical tomaintoday).build.yml: a step "Test Activity rows" right after "Check Activity data", in the existingbuildjob, whose name is unchanged./activity/page keeps its file order until Restyle /activity/ with the shared rows, a type filter and day links #277. I left the file-naming paragraph alone, since Harden the Activity data contract before automatic merges #284 rewrites it.copilot-instructions.mdlists_plugins/and the generator, replaces "No formal testing infrastructure exists" with the CI checks, and explains anActivity data: …build error.How it was tested
The commands, run from the repository root:
Every result below was checked on this branch's final tree.
Tests: 51 runs, 0 failures, 0 errors, 0 skips.
TZ=Australia/SydneyandTZ=UTC, with no warnings under-w.What they cover, on the fixture:
in_all, weeks W39/W38/W34/W33/W32/W30);What they cover beyond the fixture:
Dateagainst string input, and five timezones in-process;2026-W53, 2029-12-31 is2030-W01, and every day from 2024 to 2032 against the ISO rule);PH_EDITIONS_DIFF;<,&, both quote marks and a backtick pair) passed through unchanged;_data/activity/: the view builds, every entry lands in exactly one row, days run strictly newest first, and every key is a string.Mutation check: 40 deliberate regressions in a scratch copy of the final
rows.rbeach fail at least one test (40 applied, 0 survived):per_editionwithout the editions that lack summaries, and PRs not deduplicated;Timeconversion ofDateinput, a symbol key, HTML-escaped titles, a four-row strip, a panel that counts days, and the book type word without its colon.Before review, four regressions survived, and each led to a change: the day order now comes from the type table, and tests now cover PRs in edition order, merged summaries A to Z ignoring case, and escaping in single rows. In the first review round, dropping the url, version or PR-url tie-breaker, sorting names by exact case, or reversing the order of translated series still passed every test; the order day and two new tests now catch each of these.
Cross-check against
activity-data.jsunder node, on the same 40 entries:week()for all 25,933 days from 1990 to 2060.#21, Persian#145, Simplified Chinese#80), where the prototype uses file order.PH_EDITIONS_DIFFmatches in tag, count and per-edition summaries. It differs only in PR order and in the doubled edition's url, which the prototype takes from input order.Jekyll:
in_allflags, the months'row_countandentry_count, the Aug 2 group and the Aug 3 editions row.Activity data: _data/activity/2026-10-01-lectures, entry 2 (QuantEcon Podcast): unknown type "podcast" (expected lectures, book, translation, release)._data/activity/builds.No visible change: production builds of this branch and
main(cd6a5e2), diffed after normalising the build stamps, differ in exactly one file,README.md, which is published as a static file. The fixture and tests never reach_site.Data check:
ruby .github/scripts/check-activity-data.rbgives "Activity data OK (23 files)".Choices for review
Each choice below is a call made where the sources leave room, with its alternative. Where the decisions, the issue and the handoff are silent, it takes the readiness review's default, with one exception, marked below: the copies of one release merge, where the review would drop the repeats.
kindsingle, no tag, type word "Lecture update:" ("Book update:" for a book),nthe number of entries merged. Its url is the first entry's in the complete order. The distinct summaries are sorted A to Z (ignoring case, then exact) and joined with a space. Every PR appears once, by url, in the complete entry order and then each entry's own order. The tests' example:n= 2#1071,#1070#1070,#1072#1070,#1072,#1071Row id format. Four forms:
<date>/release(a day has one release row, single or group);<date>/translation/<series-slug>;<date>/lectures/<project-slug>;<date>/book/<project-slug>.A slug is the name lower-cased, with a hyphen for each run of anything but letters and digits (letters in any script are kept). The id replaces the prototype's
key, which changes fromdate|release|projecttodate|release-groupwhen a second release arrives. Two series on one day whose names differ only in case or punctuation would share an id, so the build stops and names both. Disambiguating them silently would make ids unstable.Month counts. Months give
row_count(6/8/11/5 on the fixture) andentry_count(6/13/15/6), with non. The panel's count isentry_counttoo. Rows keep the prototype'sn, documented as the entries merged into the row.Empty summaries in merged translations. This follows the handoff's literal rule. When the entries have exactly one distinct summary (entries without one don't count), it is shown once for the row, even for an edition that had none. With two or more,
per_editionkeeps every edition, and an edition without a summary has an emptysummarieslist, so the log keeps its link. The alternative is per-edition mode whenever some editions lack a summary.Book. The type word is "Book update:", with the colon like the other type words. The filter key is
book, which is the type itself, as for every type.Copies of one release (the exception). Entries with the same url on the same day are copies of one release. They merge into the first in the complete order, which gives the project and version, so a differently spelled project name or version for the same url resolves the same way every time. Like a double publish, the merged entry joins the distinct summaries (A to Z, with a space) and lists every PR once, so a copy without a summary or PRs never hides one with them. The tag counts distinct urls. Two versions of one project have two urls, so they stay two releases, and the title lists the project once. The alternative, the readiness review's default, is to keep one copy as it is and drop the repeats: the first in the complete order, which would be the copy without a summary, or the one with the most detail. Harden the Activity data contract before automatic merges #284's data check will reject a repeated release url, so this is a safety net.
Version order. Two versions of one project on one day are ordered by version as plain text, so a release group lists v0.7.10 before v0.7.3. This is the readiness review's default; the alternative is a version-aware sort.
The extra tie-breaker. PR titles come after PR urls, so entries that tie on every key are identical. Case-insensitive comparison uses
downcase, where the prototype useslocaleCompareat base sensitivity (which also ignores accents). The two give the same order on all current data.An edition with differing urls. When one language's entries have different urls, the edition links to the first in the complete order. This happens only in
PH_EDITIONS_DIFF.Uniform rows. Every row has every field:
releasesandeditionsare[]andper_editionis nil where the prototype omits them. PRs are deduplicated by url in every row.Extra view fields. Beyond the issue's list:
first_date_label("Jun 12, 2026", for the log's footer);date_labelon each row;datetime(the prototype'sdt).Fail loudly. Beyond unknown types, the build also stops for a date that isn't a
Dateor a validYYYY-MM-DDstring (aTimeincluded), a file that isn't a list, and an entry or change that isn't a mapping. An empty data file is skipped. A missing project, url, version or summary becomes "". The generator logs the message on a line of its own and re-raises the error, so--tracestill shows where it came from. Raising Jekyll'sFatalExceptioninstead is meant to print only the message, but Jekyll 4.4.1's graceful-fail handler then aborts with the backtrace of theSystemExitand its causes: 124 lines of output rather than 52.The escaping entry is a synthetic case in the test file, not an entry in the fixture, so that the frozen 40 entries and every acceptance number stay exact.
Notes
rows,rail,months,panel,strip,first_dateandfirst_date_label.id,date,date_label,type(also the filter key),kind(single, group or editions),n,type_word,title,version,url,tag,summary,changes(title, url, num),releases,editions,per_edition, andin_allon rail rows.start,end,label,datetime,rows,has_all.id,label,row_count,entry_count,days(id, date, rows).entry_count,month,latest,latest_iso,text.<code>after escaping.months[].days[].id, and group and editions rows already link to them./activity/layout to the view, and rewrite README's sentence about file order..github/scripts/fixtures/activity/copied over_data/activity/.railas is: rows within_allfalse and weeks withhas_allfalse gethiddenfor the no-JavaScript All view.summariesis empty.strip(3 rows).panelis nil when there is no data.id, for example as anisPermaLink="false"tag URI.row.dateat 12:00 UTC. Don't usedate_to_rfc822, which shifts the day: Jekyll's timezone is Australia/Sydney, so 2026-09-24 renders as 00:00 +1000, which is Sep 23 in UTC.build.ymland README's Activity section, next to each other, yet the two branches merge without conflicts in either order. I checked this again today in a scratch clone against Harden the Activity data contract before automatic merges #284's branch at 724c304, and the merged tree passes Harden the Activity data contract before automatic merges #284's data check and its 38 tests, these 51 tests and a production build. So nothing will prompt the one follow-up they need. Harden the Activity data contract before automatic merges #284 doesn't touchcopilot-instructions.md, so whichever PR merges second should add Harden the Activity data contract before automatic merges #284's test,ruby .github/scripts/test-check-activity-data.rb, to that file's list of CI checks, and mention the step that limits the reporter's PRs. The list no longer states a count, so this is one more bullet.Closes #276.
Part of #271.
🤖 Generated with Claude Code