Repository navigation
ci: read the Go limits page by what it carries, and keep the catalog refresh going when it breaks - #167
Conversation
…refresh going when it breaks Every nightly refresh since 2026-11-07 failed in the limits generator with `no <TabItem label="Go"> section in the doc`, and because the two generators ran in one fail-fast step, no catalog refresh was proposed either, while the vendor added six models. The cause is in the reader, not the parser. The HTML path always emits the tab sections, so the error can only come from the first branch: `go.md` answered 200 again, with a body that has no tab sections, and the reader handed any 2xx body to the markdown parser. - `scripts/go-limits-html.ts` gains `docFrom`, which reads one answer by its body: the tabbed markdown as it is, an HTML page carrying both monthly-limit tables through `htmlToDoc`, anything else (or a status outside 2xx) not at all. `noDocMessage` names what every source answered (status, redirect, content type, size, which shape it carried), so the next layout change names itself in the log. - The generator asks the page the docs live on, then the old URL, and takes the first answer that carries the tables. `--from` accepts a saved page in either shape. - The nightly step keeps `src/go-limits-data.ts` as it was when the limits generator fails, still proposes the catalog refresh, and fails at the end, naming the cause. Red first: the five new cases in test/go-limits-html.test.ts failed on main. Offline round trip: an HTML page rebuilt from the shipped table reads back through `--from` as "up to date (28 models)". The live page is measured by this PR's own `limits:shim` gate, since the vendor host is not reachable from the session that wrote it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
|
The fixed reader took the moved page and parsed it with no unreadable rows. The vendor has changed the allowances since the last refresh on 11-06, so the shipped Why the fix is not pushed from here: the table must be regenerated, never hand-edited, and The nightly step's change is also on its own PR, #168. It touches no generator input, so it can merge without the table, and from then on the catalog refresh is proposed even while the limits generator fails. Generated by Claude Code |
…test the source walk
reader-1, tests-2: docFrom took bodies the parser then threw on. The
markdown arm now needs both the Go and the Go Plus tab; the HTML arm
needs the allowance tables to each carry a model row of six cells
(modelRows, shared with htmlToDoc). Tests for a Go-only body, empty
tbodies, short rows, and one readable table beside an unreadable one.
reader-2: htmlToDoc took the first two monthly-limit tables by position.
It now throws unless there are exactly two, naming the count, and
docFrom requires exactly two, so a third table falls through to the
next source. The "fewer than two" test now reads "exactly 2"; a
three-table case ($5/$60/$120) is added for both functions.
reader-5, tests-1, rules-docs-2: the walk moved out of the import-time
generator into scripts/go-limits-html.ts as firstDoc (injected reader,
answering { doc, source, answer }; the script logs), with the ordered
GO_LIMITS_SOURCES beside it. Tested with a fake reader: first accepted
-> second never asked; first refused -> second read; none -> rejects
with the whole noDocMessage, both sources in order, the 2026-11-07
go.md shape as the second; no sources -> the bare message.
freshness-scope still answers "report" for catalog:shim.
tests-3, rules-docs-4: "by what its body carries, never its status"
now reads "a 2xx answer is read by what its body carries; the status
alone is never trusted" in docs/testing.md, AGENTS.md, the docFrom
JSDoc, the module doc and the generator header.
tests-5: the full 404 line is asserted, with no "(redirected"; an
empty content type reads "no content type"; HTML bodies report their
table count and, when fewer have a model row, how many; a Go tab
without Go Plus is named as such.
rules-docs-5: the generator header says what the code does now (the
moved page first, go.md second, read by content), with the history in
the past tense; the future-work paragraph is gone. GO_LIMITS_DOC_URL's
comment names it the old URL, asked second (src/go-limits.ts is an
input of limits:shim only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Make this branch carry exactly #168's versions of the files the two pull requests share, so the nightly "propose the refresh" step and its tests are one change wherever they land. - .github/workflows/ci.yml: the nightly step now guards both generators the same way (each failure annotated where it happens, its file put back as committed, the other refresh still proposed, the step failing at the end), with the catalog restored before the limits generator starts. Byte-identical to #168. - CONTRIBUTING.md: the "one broken source" bullet takes #168's text. This branch had no other CONTRIBUTING.md edit, so nothing was merged by hand. Byte-identical to #168. - test/freshness-scope.test.ts: #168's tests that run the step itself in a scratch repository. Byte-identical to #168. No findings carried in this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…multi-line cells reader1-1: docFrom accepted a body on its shape (both tab strings, or two allowance tables each with a six-cell row), which is weaker than what parseGoLimitsDoc reads, so the walk could stop at a source the parser then threw on and never ask the next one. Acceptance is now the parser's own verdict: a body is tried as the tabbed markdown (both tabs) and then as the converted HTML page (exactly two allowance tables), and a shape is taken only when parseGoLimitsDoc reads it without throwing. noDocMessage appends the parser's reason for each shape it was handed. The module, docFrom and generator headers and docs/testing.md say so. Tests: both tabs with no rows or a five-cell table; two tables whose rows end in Unlimited, $15 or a seventh empty cell; a page that quotes both tab strings and carries valid tables; the walk asking the second source when the first only looks readable; the parser's reasons in the error. reader1-2: tableRows now reads a <br> as a space and collapses whitespace inside a cell to one space, so a cell spanning lines no longer splits the emitted markdown row and drops the model without an unmapped entry. Tests: the cell reading, and a round trip through docFrom and the parser with a newline in an input cell and <br> plus newline in a label. ci1-1: carried byte-identical from the refresh-decouple branch: the nightly step warns, after a successful push, which files a partial refresh withdrew from an open refresh pull request; CONTRIBUTING.md says so; and test/freshness-scope.test.ts pins it with a pre-existing refresh branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…ghtly step's fixes reader2-2: tableRows stripped tags and never decoded character references, so `$15` was no dollar amount (a valid page refused), `$15 ` read as NaN, `Grok 4.7` keyed as `grok-nbsp-4.7` and `Q&A 2` as `q-amp-a-2`, and names shipped with a literal `>`. tableRows now decodes after stripping tags and before collapsing whitespace, in one pass (`&lt;` stays the text `<`): `& < > " ' ≤ ≥` and numeric `&#N;`/`&#xN;`, leaving an unknown name (looked up in a Map, so `&constructor;` is unknown) or a number that names no character as written. The `$15` case no longer stands for "no dollar amount"; new cases read entity-encoded amounts, key a label with ` `, `>`, `&` and `≤`, decode in one pass and leave unknown references alone. docs/testing.md's row says so. reader2-3: firstDoc's JSDoc said a later source is asked only when every earlier one carried neither shape; it is asked whenever the earlier ones answered nothing the parser reads (a status outside 2xx, neither shape, or a shape the parser rejected). Reworded. ci2-1, ci2-2, ci2-3: .github/workflows/ci.yml, CONTRIBUTING.md and test/freshness-scope.test.ts copied byte-identical from #168 (no inherited GIT_* reaches the scratch-repo tests; the "both current" line only when both regenerated; an open refresh pull request is asked for, not assumed, and a failed creation fails the step). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…ble the page carries The 2026-11-12 CI run measured the page carrying three monthly-limit tables, behind both URLs, which now redirect to the canonical https://opencode.ai/docs/console/go/. A choice by position or by count cannot say which two are Go and Go Plus, so each table is now chosen by the label the page attaches to it. - scripts/go-limits-html.ts: monthlyLimitTables labels each allowance table from (a) its innermost role="tabpanel" ancestor's aria-labelledby, resolved to the element whose id attribute it names, (b) the panel's own data-label/aria-label (an unprocessed <starlight-tab-item data-label> counts as a panel), else (c) the nearest preceding h2-h4. Tags are scanned with attributes in any order and either quote, comments and script/style contents blanked first. chooseTables reads Go and Go Plus from the one table labelled each (case-insensitive), ignores other labelled tables, and refuses a missing or repeated plan label; with no plan or tab label, exactly two tables keep the measured order and any other count is refused. The Starlight markup is assumed, not measured, and the module header says so. - Logs: noDocMessage lists every allowance table with its label source and text, its model-row count and its first row, and why the tables were not chosen; FoundDoc.read names which table was read as Go and as Go Plus and which were ignored, and the generator's source: line prints it, with the redirect. - GO_LIMITS_DOC_MOVED_TO is the canonical https://opencode.ai/docs/console/go/; GO_LIMITS_SOURCES stays [that page, go.md]. - test/go-limits-html.test.ts: a three-tab Starlight page in a non-plan order, reordered single-quoted attributes with decoy ids, innermost and unprocessed panels, a tag inside a script or comment, heading labels, two unlabelled tables in order, three refused with each table named, a duplicated Go label, two tabs naming no plan, an empty labelled Go table refused by the parser, and the success description. - AGENTS.md, docs/testing.md, the generator header and src/go-limits.ts's URL comment describe label-based selection and the unmeasured markup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… it, and name the markdown tabs as markdown
- scripts/go-limits-html.ts: labelAt's heading fallback skips an h2-h4 that
lies inside a tab panel not holding the table, so a table after a tab group
is no longer labelled by the last panel's heading (which refused an
unambiguous page, or with a "Plus" tab read the wrong table as Go Plus).
- scripts/go-limits-html.ts: tabsCarried names the markdown shape ("no
markdown Go <TabItem>" and its two siblings), so the error line no longer
says "no Go tab" beside a table described as tab "Go".
- test/go-limits-html.test.ts: three cases for the panel-scoped heading
(refused-before page now read by label, wrong-table page now refused, a
heading inside the table's own panel still labels it), the reworded phrases
in every expected error line, and the Go/Go duplicate case asserting the
markdown phrase beside the tab labels.
- AGENTS.md and the module header: describe the choice as the code makes it
(panel label step, panel-scoped headings, two tables with no plan label and
no tab label kept in order, "rather than" in place of "never" by position).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… literal The "asks the page the docs live on first" case compared a test-local constant with its own literal, so it passed whatever the reader exported. - test/go-limits-html.test.ts: import GO_LIMITS_DOC_MOVED_TO and assert it names the canonical page, in place of the self-comparing assertion. Under a mutation that points the constant back at the /v2/ URL, the old line passed and the new one fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…9n-go-limits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…bctz9n-go-limits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
|
Merging with On The only remaining error is that the committed So the order is: merge this, let tonight's nightly regenerate the table and propose it as a refresh PR, then port and merge that refresh. Every other check is green. Generated by Claude Code |
Why
Every nightly
catalog freshnessrun since 2026-11-07 has failed in the Go limits generator:Cause, and what the live page carries now
The reader handed any 2xx body to the markdown parser.
go.mdanswered 404 on 2026-10-12, but since 11-07 it answers 200. Measured by this PR's CI on 2026-11-12:Both
https://opencode.ai/docs/go.mdand the oldhttps://opencode.ai/v2/docs/console/go/now redirect tohttps://opencode.ai/docs/console/go/. It is an HTML page of about 98,000 characters.The page carries three monthly-limit tables, one per Starlight tab:
GoGo PlusGo TeamsThe third tab means a reader that takes the first two tables by position can read the wrong pair. An earlier commit on this branch did exactly that, before the review caught it.
What changed
The reader is in
scripts/go-limits-html.ts, which is pure and tested.docFrom(answer)parseGoLimitsDocreads it without an error. Anything else falls through to the next source.Table choice by plan label (
monthlyLimitTables,chooseTables)role="tabpanel"ancestor'saria-labelledby, matched to therole="tab"element's text. A panel's own label or the nearest heading is the fallback.Go, and Go Plus the one labelledGo Plus. Other tabs, such asGo Teams, are ignored.tableRows<br>becomes a space, character references are decoded, and whitespace collapses.firstDoc(GO_LIMITS_SOURCES, read)go.md, and takes the first answer the parser reads.noDocMessagesource:line names which tab it read as Go and as Go Plus, and which it ignored.Elsewhere
scripts/generate-go-limits.tsis glue only.--fromtakes a saved page in either shape.AGENTS.md,docs/testing.mdand theGO_LIMITS_DOC_URLcomment insrc/go-limits.tsmatch the code.main(ci: propose the catalog refresh even when the limits generator fails #168, test: a harness that is the real host #110, fix: disabling or reloading the plugin restores the host methods it wrapped #112) is merged in. The nightly-step change this PR used to carry is onmainthrough ci: propose the catalog refresh even when the limits generator fails #168, so this diff is the reader only.Review
Four adversarial rounds. Each finding went to two skeptics, and only upheld findings were fixed.
Upheld and fixed:
noDocMessagebranches;firstDoc's JSDoc;Declined by both skeptics:
Verification
89e7049:vp check: 0 errors, 18 warnings.pnpm run buildandscripts/check.ts: passed.--fromasup to date (28 models). A scratch three-tab page reads Go and Go Plus from their tabs and ignores the third.Why
catalog freshnessis red hereThe reader is right. The table is out of date: the vendor now lists 40 models per plan, where the shipped
src/go-limits-data.tshas 28, and the amounts moved.This PR changes the limits generator, so under D23 it gates on that table. The table is regenerated, never hand-edited:
That needs
opencode.ai, which the session that wrote this PR cannot reach. Run those two commands on this branch and commit the result, and the gate turns green.Once this is on
main, the nightly refresh regenerates the table on its own and proposes it.🤖 Generated with Claude Code
https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD