Skip to content

activity: compute the Activity rows at build time, with tests - #285

Merged
mmcky merged 3 commits into
mainfrom
activity-rows-plugin
Sep 29, 2026
Merged

mmcky merged 3 commits into
mainfrom
activity-rows-plugin

Conversation

@mmcky

@mmcky mmcky commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

This PR computes the Activity rows at build time and exposes them to Liquid as site.data.activity_view, with tests that run in the build job. 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 or Time dependency (stdlib date only). It ports the round-2 handoff's buildRows, week, range, railRows, byWeek, byMonth and panelSummary, with the issue's amendments and the decisions:
    • Types. release, lectures, translation and book. Within a day come lecture updates, then book updates, then translations, then releases. An unknown type stops the build, and the message names the file and entry.
    • A complete sort order. Days run newest first, then type, then project (lower-cased, then exact), version, url, summary, the entry's PR urls and, last, its PR titles. No output depends on file or entry order.
    • Same-day merges, keyed as the handoff keys them. A lecture series or book that published twice becomes one ordinary row. A series' translations become the editions row, with PRs in edition order. A day's releases form one row, whose title lists each project once. First, the copies of one release (the same url on the same day) merge into one, keeping every summary and PR.
    • A stable row id: the merge key, such as 2026-08-02/release, 2026-08-03/translation/python-programming-for-economics-and-finance or 2026-09-27/lectures/intermediate-quantitative-economics-with-python.
    • Dates stay calendar days and are never converted to Time. Weeks use Date#cwyear and #cweek, and every label is precomputed ("Sep 27", "Sep 21–27", 2026-W39, "September 2026", ISO dates).
    • Output. Empty or nil data gives empty views. The module returns plain strings and string-keyed hashes, unescaped: the templates escape every field.
  • _plugins/activity_generator.rb, a Jekyll::Generator that sets site.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 plain ruby. Their frozen fixture, .github/scripts/fixtures/activity/, is a verbatim copy of _data/activity/ at 4c47f03 (23 files, 40 entries; byte-identical to main today).
  • build.yml: a step "Test Activity rows" right after "Check Activity data", in the existing build job, whose name is unchanged.
  • Docs. README gains an "Activity rows" subsection after "Activity data". It says that the interim /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.md lists _plugins/ and the generator, replaces "No formal testing infrastructure exists" with the CI checks, and explains an Activity data: … build error.

How it was tested

The commands, run from the repository root:

ruby .github/scripts/test-activity.rb
ruby .github/scripts/check-activity-data.rb
JEKYLL_ENV=production bundle exec jekyll build

Every result below was checked on this branch's final tree.

  • Tests: 51 runs, 0 failures, 0 errors, 0 skips.

    • Run on Ruby 4.0.7 with minitest 6.0.0, Ruby 3.4.1 (Netlify's version) with minitest 5.25.4, and Ruby 3.1.3 with minitest 5.25.4, each under TZ=Australia/Sydney and TZ=UTC, with no warnings under -w.
    • CI runs Ruby 3.4 with minitest 5.25.
  • What they cover, on the fixture:

    • the rail under All (6 rows under "Sep 21–27" and "Sep 14–20");
    • Releases (back to jlgametheory v0.2.0, Aug 16, under three weeks);
    • Lectures (Sep 27 back to Jul 23, under four weeks);
    • Translations (2 rows) and Books (none);
    • the rendered rail (14 rows, 6 in_all, weeks W39/W38/W34/W33/W32/W30);
    • the strip, the panel ("6 updates in September · latest Sep 27"), and the log (30 rows, 6/8/11/5);
    • the Aug 2 release group and the Aug 3 editions row.
  • What they cover beyond the fixture:

    • 60 shuffles of files and entries, and 60 of entries moved between files, over the fixture and two synthetic days: a busy day that needs every merge rule, and an order day whose names sort differently byte by byte and ignoring case, with pairs of entries that tie on everything but their version, url or PR urls;
    • Date against string input, and five timezones in-process;
    • ISO weeks (2026-12-28 is 2026-W53, 2029-12-31 is 2030-W01, and every day from 2024 to 2032 against the ISO rule);
    • row ids: unique, stable under shuffling, and unchanged when a same-day release, a second edition or a second publish joins a row;
    • a synthetic case for each amendment and decision, including the handoff's PH_EDITIONS_DIFF;
    • names A to Z ignoring case (lecture rows, translated series, the editions in a row, and a release group's title and releases), and the version, url and PR-url tie-breakers in either input order;
    • copies of one release merging their summaries and PRs;
    • unknown types and malformed data, empty and nil data, and the escaping entry (<, &, both quote marks and a backtick pair) passed through unchanged;
    • invariants on the live _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.rb each fail at least one test (40 applied, 0 survived):

    • each key of the entry order dropped in turn: the lower-cased project, the exact project, version, url, summary, PR urls and PR titles;
    • lecture rows, translated series or languages sorted by exact case; series Z to A; languages or summaries left unsorted; case-sensitive comparison of names and summaries;
    • book updates after translations, and days oldest first;
    • copies of a release not merged, kept as the first or the last copy, merged with only the first copy's summary or PRs, or matched on project and version rather than url; a group title that repeats a project, and a tag that counts projects;
    • merged rows that take the last entry's url, editions PRs in project order, empty summaries counted as distinct, per_edition without the editions that lack summaries, and PRs not deduplicated;
    • the prototype's per-release id and case-sensitive ids, Sunday-start weeks, calendar-year week numbers, a Time conversion of Date input, 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.js under node, on the same 40 entries:

    • Compared: the rows field by field, the rail under every filter, the months, the panel, the strip, the first date, and week() for all 25,933 days from 1990 to 2060.
    • Exactly one difference, the expected one: the Aug 3 editions row lists its PRs in edition order (French #21, Persian #145, Simplified Chinese #80), where the prototype uses file order.
    • PH_EDITIONS_DIFF matches 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.
    • The order day's rows come in the prototype's order: lecture rows, series, languages and release projects, all A to Z ignoring case. This comparison sets aside what the amendments change: the prototype keeps one row per lecture entry and lists a repeated release twice.
  • Jekyll:

    • A production build of a scratch copy with a probe page (never committed) shows that Liquid reads the whole view: 30 rows, the strip, the panel text, the rail's weeks and in_all flags, the months' row_count and entry_count, the Aug 2 group and the Aug 3 editions row.
    • A scratch copy with an unknown type fails to build (exit 1). The build's first error line, before Jekyll's backtrace, is Activity data: _data/activity/2026-10-01-lectures, entry 2 (QuantEcon Podcast): unknown type "podcast" (expected lectures, book, translation, release).
    • A copy with no _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.rb gives "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.

  • Panel count. It follows the handoff: the entries in the newest entry's month. So a same-day double publish counts twice although it shows one row: two IQ entries on Sep 27 would read "2 updates" over one row. The alternative, which the readiness review recommends, is to count a merged lecture or book row once. The fixture's "6 updates in September" is the same either way. The copies of one release count once.
  • Merged lecture or book row. It is one ordinary row: kind single, no tag, type word "Lecture update:" ("Book update:" for a book), n the 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:
Entry 1 Entry 2 Row
Project Intermediate Quantitative Economics with Python the same the same, with no tag and n = 2
Summary Added exercises. Added a lecture. Added a lecture. Added exercises.
PRs #1071, #1070 #1070, #1072 #1070, #1072, #1071
  • Row 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 from date|release|project to date|release-group when 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) and entry_count (6/13/15/6), with no n. The panel's count is entry_count too. Rows keep the prototype's n, 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_edition keeps every edition, and an edition without a summary has an empty summaries list, 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 uses localeCompare at 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: releases and editions are [] and per_edition is 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_label on each row;
    • the week groups' ISO week is named datetime (the prototype's dt).
  • Fail loudly. Beyond unknown types, the build also stops for a date that isn't a Date or a valid YYYY-MM-DD string (a Time included), 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 --trace still shows where it came from. Raising Jekyll's FatalException instead is meant to print only the message, but Jekyll 4.4.1's graceful-fail handler then aborts with the backtrace of the SystemExit and 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

  • History. The branch is three commits: the rows module and generator; the tests, fixture and CI step; and the docs. Their messages describe the final code, since the squash merge uses them as its body. The tree is identical to the reviewed one (local head abd6974).
  • The contract. The generator's header comment is the contract, and the templates should read the view rather than regroup entries in Liquid. At the top level it holds rows, rail, months, panel, strip, first_date and first_date_label.
    • Row fields: 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, and in_all on rail rows.
    • Week groups: start, end, label, datetime, rows, has_all.
    • Months: id, label, row_count, entry_count, days (id, date, rows).
    • The panel: entry_count, month, latest, latest_iso, text.
  • Escaping. Every string arrives unescaped: escape it in the template, and turn backtick spans into <code> after escaping.
  • Restyle /activity/ with the shared rows, a type filter and day links #277:
    • The day anchors are months[].days[].id, and group and editions rows already link to them.
    • Switch the interim /activity/ layout to the view, and rewrite README's sentence about file order.
    • To check the fixed numbers, build a scratch copy with .github/scripts/fixtures/activity/ copied over _data/activity/.
  • Add the Activity rail and mobile panel to /news/ #278 and Add the "Latest activity" strip to the home page #279:
    • Render rail as is: rows with in_all false and weeks with has_all false get hidden for the no-JavaScript All view.
    • In the two-line per-edition text, skip editions whose summaries is empty.
    • The strip is strip (3 rows). panel is nil when there is no data.
  • Publish an Activity RSS feed at /activity/feed.xml #280:
    • Take each item's guid from the row's id, for example as an isPermaLink="false" tag URI.
    • Build pubDate from row.date at 12:00 UTC. Don't use date_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.
    • Once the feed is public, the id format is frozen.
  • Overlap with Harden the Activity data contract before automatic merges #284. Both PRs edit build.yml and 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 touch copilot-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

mmcky and others added 3 commits September 29, 2026 18:54
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>
@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for grand-swan-ca5201 ready!

Name Link
🔨 Latest commit 79e7fb7
🔍 Latest deploy log https://app.netlify.com/projects/grand-swan-ca5201/deploys/6abb7e95b0fc72000868ed38
😎 Deploy Preview https://deploy-preview-285--grand-swan-ca5201.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 sets site.data.activity_view and 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 existing build job in build.yml.
  • Documentation updates in README.md and .github/copilot-instructions.md describing 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compute the Activity rows at build time, with tests

2 participants