Skills and theme contract: a redone or migrated theme can reintroduce the quadratic partials, and nothing measures it #363
Labels
No labels
bug
enhancement
needs-triage
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#363
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A redone, ported or forked theme can reintroduce the quadratic partials #360
fixed, and no skill measures it: the only guard is
harness check'ssite-wide-collection warning, which a per-pager re-filter or a large
section scan slips past. Ship the measurement as
harness bench, give itone owner shared with the theme guard test, and make the skills run it when
a change touches a template that lists pages.
Current behavior
PR #364 (merged 2026-11-01) landed most of the original report:
holds the scan, never the argument; don't compute what a branch won't
draw) and names the shipped owners
footer-pages.html,news-items.htmland the header's
hideChildrenguard. The theme contract (item 20), theskills
redo-theme,migrate-site,create-feature,verify-site,docs/features.mdanddocs/harness.mdpoint at it.harness checkwarns, in areatemplates, on a template that reads asite-wide collection outside a partial every caller reaches through
partialCached.internal/tplrefs.UncachedSiteScansowns the rule; asection's own
.Pagesis deliberately not warned on.internal/theme/scale_test.gogenerates a site in the reporting site'sshape (N news items, 12 categories, 13 news-list pages), builds it at N and
2N on one core with real
hugo, fails above a ratio of 2.7, and checks ina subtest that the two cached partials execute once per key. It also holds
a
--templateMetricstable parser. All of it is test code.What is still missing:
verify-sitebuilds once and reads warnings;redo-themestep 4 andmigrate-sitestep 7 describe the pattern inprose. A fork that re-filters a section's items on every pager page, or
scans a large section's children per page, passes
harness checkandevery skill.
harness bench; the generator exists only inside the test.Measured during triage on 2026-11-01 (Hugo 0.167.0 extended, the guard
test's generated site, old partials taken from the commit before #364,
GOMAXPROCS=1unless noted; outputs byte-identical, no Hugo warnings):Three facts from that measurement rule out the thresholds the original
report proposed:
descheduled: on one core the fixed header averages 9.5 ms and
list.html18.8 ms (maximum 890 ms); on 16 cores the same header averages 2.9 ms.
"No partial above a few ms average" fails the fixed theme and depends on
the machine.
--templateMetricsHintsreports the fixed theme'sheader.htmlat 98 %cache potential,
document/close.htmlandbackendlayout/resolve.htmlat100 %,
ce/element.htmlat 91 %: cheap partials with alike output. "Nouncached partial over 90 %" fails the fixed theme too.
--templateMetricsHintsdeadlocked Hugo on the old partials at 2,565pages in two runs of two (one core, 10 min; 16 cores, 3 min; zero CPU).
The goroutine dump:
metrics.(*Store).TrackValuehashes a partial'sreturn value under the store mutex, the hash reaches
resourceAdapter.Key→lazycache.GetOrCreateand waits for an entryanother goroutine is creating while that goroutine waits for the same
mutex. The 1,281-page old-partials site and the fixed 2,565-page site
completed.
Desired behavior
harness bench --site <dir> --pages <n> [--section <path>] [--json]copies the site to a temporary directory (the site itself is never
written), fills the named section (default: the first section of type
news; an error naming the flag when the site has none) with generateditems in the guard test's shape, builds the copy at
nand2nitems onone core with the pinned Hugo and
--templateMetrics, and reports: thepage count and wall time of each build, the ratio, the verdict against the
limit the guard test uses, and the top rows of the larger build's metrics
table by cumulative duration (template, count, cumulative, average) so the
developer knows where to look. Exit 0 when the ratio is within the limit,
1 when it is over, 2 when the bench cannot run (no
hugo, no section, afailed build, whose Hugo output is printed).
--hintsadds--templateMetricsHintsto the larger build under atimeout (default 5 minutes,
--timeout); on expiry the bench reports thatHugo hung in its hints mode and still gives the ratio verdict from the
plain builds. Without the flag hints are never requested.
themes/deckle/README.md"Navigation" as the fix.--jsonis one objectwith
formatVersion,status(ok,slow,error), the two builds(
items,pages,seconds),ratio,limit, thetemplatesrows,and, with
--hints,hints(ok,timeout) and acachePotentialcolumn on the rows.
limit live in one non-test package the command and
internal/theme's guard test both call. The guard test keeps itsbehaviour (N = 2,000, limit 2.7, once-per-key subtest, skips without
hugoand under-short) and stops carrying its own copies.verify-sitegets one step, afterharness checkand the productionbuild: when the change touched a template that lists pages (a theme
rewrite or port, a new section page type, a feature with a section list,
a changed header, footer or pager), run
harness bench --site . --pages 2500and stop on aslowverdict; the step says what the ratio means inone sentence and points at
themes/deckle/README.md"Navigation" for thefix.
redo-theme(before "Verify"),migrate-site(step 7 and step 9),create-featureandadd-page-typeeach name the bench where they pointat
verify-site, as the thing that decides whether their templates scale;none restates the thresholds or the procedure.
harness checkis not changed. The existing warning stays in areatemplates; no.Paginateor$src.Pagesheuristic is added.Decisions
the hints column — the measurement above shows both thresholds the
original report proposed fail the fixed theme and depend on core count.
the guard test import, because the bench and the test evaluate the same
rule ("does this theme build linearly in the page count") and CLAUDE.md
forbids a second evaluation. The calibration comment on the limit (2.7:
stock 3.1–3.4x, fixed 2.0–2.2x) moves with it.
the theme the bench exists to catch, and a hung bench in a skill step is
worse than no bench.
GOMAXPROCS=1), as the guard test does — the ratio is whatmatters and one core makes it independent of the developer's machine;
the bench says so in its output.
.Paginate/$src.Pagescheck heuristic and noperformancearea —the shipped
term.htmlpaginates a filtered$src.Pagesbuilt in the pagetemplate (9.6 ms per term page on 16 cores, no gain from caching per
#360), and the header,
page-teasersandnews-listall read.Pagesthrough
where; any such rule trips the stock theme, and the benchmeasures what the rule would guess. The
--jsonarea list indocs/harness.mdstays as it is.verify-siteonce, conditioned on what thechange touched; the other four skills point at it — skills are
procedures with one owner each (CLAUDE.md), and every other skill already
ends in
verify-site.news-itemshape(
type,date,teaser,author,categories, atextelementbody) — that is the shape that exposed the problem and the one the guard
test uses;
--sectionselects which section is filled, not the itemtype. A site without the news bundle gets exit 2 with a message saying
the bench needs it.
bugas the category; the maintainer applied it.Key interfaces
cmd/harness: a newbenchentry in thecommandstable (its summaryline is what
usageText()prints and the usage test compares withdocs/harness.md); abenchCmd(args, stdout, stderr) intin the style ofcheckCmd,--jsonthrough the same one-object convention.internal/sitebench) exporting the generator(
scaleSiteContenttoday: items → site-relative paths → canonicalcontent; the canonical-content check the test already applies must keep
passing), the one-core build runner with
--templateMetrics(
runScaleBuildtoday), the metrics parser (parseTemplateMetricsandmetricsRowtoday), the limit (scaleMaxRatiotoday) and a result typethe command serialises. The test's site assembly (
assembleScaleSite,which copies theme and bundles the way
harness addvendors them) istest-only and stays; the command works on a real site directory.
internal/theme/scale_test.go: calls the package;TestBuildTimeScalesLinearlyand its
templateMetricssubtest keep their assertions.docs/harness.md— a### harness benchsection (flags, output, exit codes, the hints caveat with the deadlock)
and a
### harness bench --jsonshape under the--jsonlist;README.md— one row in the command table, no new section;scaffold/site/AGENTS.md— the bench beside the other harness commands itnames (the "Verifying a change" paragraph), so the skills have a section
to point at;
scaffold/themes/deckle/README.md"Navigation" — onesentence that
harness benchmeasures the property, so the fact and itsmeasurement sit together;
docs/agents/docs-map.md— the new package inthe
docs/harness.mdrow.verify-site/SKILL.md(the new step),redo-theme/SKILL.md(before "## 4. Verify"),
migrate-site/SKILL.md(steps 7 and 9),create-feature/SKILL.mdandadd-page-type/SKILL.mdwhere they point atverify-site.scaffold/skills_test.gochecks everyharness <command>a skill names is real, so the command must exist before the skills name
it.
Acceptance criteria
harness bench --site <fresh site with news and categories> --pages 500builds a 500- and a 1,000-item copy on one core, prints bothpage counts and times, the ratio,
ok, and the top metrics rows; thesite directory is unchanged afterwards (
git statusclean) and thetemporary copy is removed.
header.html,footer.htmlandpagetypes/news.html(the versions before #364, asinternal/theme's guard was proven against) at--pages 2000reportsslowand exits 1; the fixed site at--pages 2000reportsok.--jsonemits the documented object and nothing else on stdout;docs/harness.mddocuments it;go test ./cmd/harness/ ./docs/pass.--hintson the fixed site adds the cache-potential column; with a--timeout 1sagainst the old partials at 2,534 items the benchreports the hints timeout and still prints the ratio verdict (this is
the reproduction of the deadlock; it may not hang every run — the
test for the timeout path uses a stub or a short timeout, not the
deadlock).
hugoon PATH: exit 2 and a message; on a site without thenews bundle: exit 2 naming the requirement.
internal/theme/scale_test.gohas no generator, runner, parser orlimit of its own;
TestBuildTimeScalesLinearlypasses in CI with thesame limit.
go test ./scaffold/passes: the five skills nameharness bench,every quoted heading exists, every link resolves.
harness checkis unchanged:go test ./internal/checkcmd/ ./internal/tplrefs/pass with no new warning text.How to verify
go test ./...withhugoon PATH (the guard test, the usage test, thedocs drift tests, the skills drift test).
the triage table above (fixed ≈ 2.0x, old ≈ 3.1x).
diff -ris not needed: the bench builds a copy and writes nothing tothe site.
Assumptions to check
through
assembleScaleSite; a real site'shugo.tomlmay needbuildFutureor the categories taxonomy the test's config sets — thebench must build the site as it is and report Hugo's error, not patch
the config.
GOMAXPROCS=1reaches Hugo through the environment the test alreadysets; confirm the command does the same on macOS.
treat it as a race, not a size threshold.
harness check's usage test and the README command-table test mayrequire the new row in a fixed format; read
cmd/harness/*_test.gofirst.
Out of scope
harness check(no.Paginate,$src.Pagesorperformancearea).
term.html,elements/news-list.html,page-teasers.html.for bundles.
Blocked by
None - can start immediately
Original report
What happens
The theme performance issue (the quadratic header, footer and news list) was produced by the shipped theme and then copied into gbv-aktuell's fork by the
redo-themeprocedure. Nothing in the scaffold prevents it:themes/deckle/README.mdhas no rule about a template's cost,redo-themestep 4 asks only that menus and lists read the source page's flags,verify-sitebuilds once and greps warnings, andharness checkknows nothing about templates beyond unresolved partial names. A site of 2,500 pages built in 28.6 s where 9.1 s was possible, and every save after visiting/news/cost 5.8 s where 3.1 s was possible.Fix
Following CLAUDE.md: the fact goes to the owner, the skills point at it, a test enforces the mechanical half.
themes/deckle/README.md, new section "Cost on a large site": a template that reads a whole collection (site.Pages,hugo.Sites.Default.Pages, a section's.Pages) does so once per build, in apartialCachedpartial keyed by language and, for a section, by.RelPermalink, never in a page template, because thewhereargument ofpartialCachedis evaluated before the cache is consulted; a list a flag suppresses (hideChildren) is not computed before the flag is checked; a paginated section's items come from one such partial. Name the shipped owners:footer-pages.html,news-items.html, the header'shideChildrenguard.redo-theme(before "Verify"),migrate-site,create-featureandadd-page-type(anything that lists pages): one step — build a generated large site withhugo --templateMetrics --templateMetricsHintsand require no partial above a few ms average and no uncached partial with a cache potential over 90 %. The generator ships asharness bench --site . --pages 2500(a copy of the site whose news section, or a section the flag names, is filled with generated items) so the skills do not restate how to make one.harness check: atemplateswarning forwhereoversite.Pages,hugo.Sites.Default.Pagesor$src.Pagesoutside a partial that is only ever called throughpartialCached, and for.Paginateon a collection not read from one. This is the check a fork cannot skip.Caution: this is a text match over templates, and a fork that renamed its partials or built its lists differently will trip it without being slow. It is a warning, never an error, reported under its own key (
performance) inharness check --json, so a site can acknowledge it andverify-sitecan still treat the rest of the report as clean. The measurement in item 2 is what decides; the warning only points at where to look.Tests:
scaffold/skills_test.goalready requires quoted headings to exist, so the new README section is pinned by the skills that quote it. Add a theme test that builds the generated site at N and 2N pages and fails when the ratio of build times exceeds 2.5.Evidence
Measured 2026-10-31 on a synthetic 2,562-page site with the stock theme and Hugo 0.167.0: header 12.4 s of 28.6 s, footer 6.1 s, news list 2.0 s, all flagged cacheable by
--templateMetricsHints; the three fixes in the theme issue give 9.1 s with byte-identical output. gbv-aktuell's fork (TYPO3-lookalike markup) must be checked for the same patterns by hand until item 3 exists.