Skip to content

Spec and plan the card harness - #56

Merged
robertblust merged 4 commits into
mainfrom
card-harness-spec
Sep 2, 2026
Merged

Spec and plan the card harness#56
robertblust merged 4 commits into
mainfrom
card-harness-spec

Conversation

@robertblust

@robertblust robertblust commented Sep 2, 2026

Copy link
Copy Markdown
Owner

The design we agreed, written up as a spec, plus the implementation plan. Supersedes the tier 4 paragraph of the shared design system spec, which was wrong in two directions.

What the earlier spec got wrong

It called the recipe local because "95 of its 148 lines differ for real reasons" — but og-recipe.mjs is machinery and a card list sharing a file. Diffed apart:

machinery   ~62 lines each, differing by 5 to 9 lines
            almost all of it comment prose — blust.ch's says "four
            multi-megabyte PDFs", guestgraph's says "both"
card list   genuinely different, as the spec said

It also called tier 4 "the smallest saving of the four". On the corrected measurement it is the largest cluster left in the family. Both errors came from the duplication sweep, wrong by construction three times now; these numbers were taken after it got tests.

The line count is the weaker argument

export-og.mjs has drifted into three variants, each carrying a capability the other two lost:

companygraph guestgraph blust.ch
reduced-motion settling
hash — render a named view
recipe/renderer agreement guard
per-card deviceScaleFactor

The recipe tests drifted the same way — 29, 30 and 30 tests, and no one of them a superset of another. There is no copy here to consolidate onto. This is the shape of the defect that nearly shipped in the suite runner round, so "the package takes the union, never the intersection" is written in as a rule rather than left to judgement.

The design

No fences. A visitor never downloads any of these files, so the governing rule makes every piece an import. First tier where the answer is imports throughout.

Four modules under cards/recipe.mjs, check.mjs, export.mjs, recipe-tests.mjs — each taking the site's recipe module as input. Each site keeps og-recipe.mjs (its cards, frame, hide rules and REPO_ROOT) and three thin callers. REPO_ROOT stays site-side deliberately: that is the SITE_ROOT defect from the runner round, avoided by construction rather than by remembering.

No card's picture changes. The union makes settle: "reduced-motion" available everywhere but it applies only where a recipe asks, and blust.ch's does not. og:check is a fixed point across the whole change.

Self-review found three things

Fixed before this went out rather than discovered mid-implementation:

  • cardFor is not a union item. Two sites export it, which is what made it look like one — it is defined once in each and called nowhere in either. Deleted rather than moved; the union rule protects capabilities a site would lose, and an export nothing imports is not one.
  • Criterion 1 was unachievable as written. Dropping from from guestgraph's cards moves every guestgraph og.sha, because the recipe hashes every key of a card. Pictures identical, hashes not. Named as the one exception; the line holds everywhere else.
  • SHARED_TEST_COUNT was inside the function that must not contain it, and its test counted occurrences of test( in the source — which a comment defeats, exactly how three tests in this family were defeated before. It counts registrations now.

The plan

Eight tasks: four package modules, a release, three site adoptions. Every task that adds a gate proves it red by mutation first. The union recipe suite ends at 32 tests on every site, up from 29, 30 and 30.

🤖 Generated with Claude Code

The earlier spec scoped this work from two wrong readings. It called the
recipe local because 95 of its 148 lines differ — but og-recipe.mjs is
machinery and a card list sharing a file, and diffed apart the machinery is
~62 lines differing by 5 to 9, almost all comment prose. It also called this
the smallest saving of the four; on the corrected measurement it is the
largest cluster left in the family.

Both errors came from the duplication sweep, which has now been wrong by
construction three times. The numbers here were taken after it got tests.

The stronger argument is not the line count. export-og.mjs has drifted into
three variants and each carries a capability the other two lost — reduced
motion and hash rendering in companygraph, a recipe guard and per-card scale
in guestgraph, neither in blust.ch. The recipe tests drifted the same way:
29, 31 and 31 tests, none a superset. That is the shape of the defect that
nearly shipped in the suite runner round, so the union rule is written into
this spec rather than left to judgement.

No card's bytes change. The union makes capabilities available; a recipe has
to ask. og:check stays a fixed point on all three sites, which is what makes
the change reviewable at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review against the spec found three disagreements worth fixing before
anyone implements from it.

cardFor is not a union item. Companygraph and guestgraph both export it,
which is what made it look like one — but it is defined once in each and
called nowhere, in either repository. The union rule protects capabilities a
site would lose; an export nothing imports is not one. It is deleted rather
than moved, and the decision is recorded so the next reader does not
rediscover it as an omission.

Criterion 1 was unachievable as written. Dropping `from` from guestgraph's
cards moves every guestgraph og.sha, because the recipe hashes every key of a
card — the pictures are byte-identical, the hashes are not. The criterion now
names that as the one exception and holds the line everywhere else: a changed
og.png is a defect on any site, guestgraph included.

SHARED_TEST_COUNT was placed inside the function that must not contain it, and
its test counted occurrences of `test(` in the source — which a comment
defeats. It counts registrations now. Three tests in this family have already
been defeated exactly that way.

Eight tasks: four package modules, a release, three site adoptions. Every task
that adds a gate proves it red by mutation first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@robertblust robertblust changed the title Spec the card harness, and correct what tier 4 assumed Spec and plan the card harness Sep 2, 2026
robertblust and others added 2 commits September 2, 2026 11:26
The whole-branch review caught two defects, both in this spec rather than in
the code it describes.

Critical: all three sites run og:check and test:og BEFORE npm ci, which is
what made "imports nothing outside node's standard library" worth asserting.
Adoption makes both steps import the package — which is not on disk until
npm ci has run. Every push would have failed with ERR_MODULE_NOT_FOUND, and
Task 6's own verification would not have caught it, because node_modules
exists on a developer's machine. The steps move after npm ci and before the
playwright install; the property actually worth keeping is that og:check
needs no browser, and that one survives.

Major: the adoption snippet showed a FRAME carrying deviceScaleFactor. It
matched none of the three sites — blust.ch and companygraph carry clipY and
no deviceScaleFactor, guestgraph the reverse. The recipe hashes every key of
a card, so following the snippet moves every og.sha; measured at 8 of 8 on
blust.ch. The instruction is now to copy each site's FRAME verbatim and
retype nothing.

Two smaller corrections to the same snippet: the specifier has no .mjs, since
the exports map has no suffixed entry, and it must bind through recipeFor
rather than `export * from`, which leaves root unbound so state() throws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@robertblust
robertblust merged commit 9aa732a into main Sep 2, 2026
1 check passed
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.

1 participant