Repository navigation
feat(gate): the token ratchet ceilings every package, not just sales - #1947
Merged
Merged
Conversation
…1928) `scripts/check-source-token-ratchet.mjs` ceilinged `src/sales/` alone and printed the other three packages with no ceiling, explicitly so the per-module budget ADR-0130 §4 promises would have starting figures. This spends them: twelve ceilings, three per package, each `anchor()` of that package's own reading under the same ruled 5% buffer that set the committed ones. Four independent gates, not one wider surface: a module past its own ceiling reddens that module and leaves the other three green, so headroom cannot be borrowed across a package boundary. The package roster is read off disk (a directory under `src/` with an `objects/` directory — the predicate `test/helpers/src-roster.ts` uses since #1940) rather than hand-listed, and the gate refuses to run on a package that carries no ceiling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
… spent The message read "this surface is the app's headline claim" — true of the one ceilinged package, false on the eleven rows that are not it. A `src/marketing` failure told the author they had breached the app's headline claim, sending them to argue about the wrong number. It now says the budget is that package's own and no other package's headroom can pay for it, which is the whole content of a per-module budget. Pinned in `test/source-token-ratchet.test.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…tgrew "Its `LAYERS` list names `src/objects` … `src/apps`" was true when the section was measured and has been false since #1905 re-pointed it at `src/sales/`. This PR makes it more false — the list holds metadata-type names now, not paths — so it is dated here rather than left to mislead the next reader re-deriving the gate's surface from a planning document. The rule the paragraph states survives both moves and is kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
os-elon-musk
marked this pull request as ready for review
September 16, 2026 08:21
This was referenced Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #1928
The token ratchet went from one ceilinged package to four. Every package under
src/now carries its ownbusiness semantics,interaction layerandauthored totalceiling — twelve in all.The maintainer ruling this PR stands on
scripts/check-source-token-ratchet.mjsrequires a maintainer ruling quoted in the raising PR's body before any ceiling goes up. Two ofsrc/sales's three did. The ruling, verbatim and untranslated (2026-09-16):The gate's own header carries it beside the constants, and
test/source-token-ratchet.test.tspins that it is still there.Why this is spending a budget, not inventing one
The gate's docstring already said what the three un-ceilinged packages were for:
This PR spends those starting figures. Nine of the twelve ceilings are a package's first, which is an anchoring and not a raise — there was no committed number to move.
The readings, re-measured on this branch
node scripts/check-source-token-ratchet.mjsonorigin/mainat4d7ae9f. Not one line undersrc/is touched by this PR, so these are also what the branch reads today.The 31,000 self-proof — the rule is the committed rule, not a new one
The anchoring rule is the gate's own
anchor(): the ruled 5% working buffer (「给 5% 缓冲」, 2026-08-17), rounded up to the next 1,000. Applied tosrc/sales's interaction layer:31,000 is exactly the ceiling that file already carried, set by #1905 from a different reading (29,344). So the arithmetic applied to the other eleven rows is demonstrably the arithmetic that set the committed one — this is the existing rule extended to four packages, not a new rule invented for this card.
How every ceiling was computed
Same
anchor(), no exceptions, no hand-picked numbers. Each row is in the gate's header with its worked arithmetic, andtest/source-token-ratchet.test.tsre-derives all twelve fromanchor()rather than reading them:Three ceilings existed before and are re-anchored onto today's reading:
src/salesbusiness semantics 53,000 to 55,000 (a raise, under the ruling above), authored total 97,000 to 100,000 (a raise, same ruling), interaction layer 31,000 to 31,000 (unmoved — the self-proof).The small modules carry more than 5% because
anchor()rounds up to the next 1,000 and on a reading of 876 that step alone is 14%. The rounding is kept — a ceiling a reader can hold in their head beats the last few hundred tokens of precision on a number estimated aschars / 4— but it is why the re-anchoring advisory changed; see below.Core acceptance: four gates, not one
Driven on the real tree, not only in the sandbox. A probe file was written into
src/marketing/objects/, proven on disk, and deleted with atrap:One module red, the other three untouched and green, and the overflow carried into that module's own total and no other's. That is the property a single summed ceiling could not have:
src/marketingcannot spend headroomsrc/salesis not using.All four packages are covered, and both layers, by
test/source-token-ratchet.test.ts:reddens only the package whose business semantics went overandreddens only the package whose interaction layer went overeach loop over the roster, drive every package past its own ceiling in turn and assert the other three still print✓. Loops overPACKAGE_DIRS, never over four literals — a fifth package is covered the day it lands.The measurement basis is unchanged, and now pinned
The gate's header says a change that lets comment mass affect the number breaks the measurement basis — "a gate that rewarded deleting them would be a gate against explanation". Measured on the real tree: 600 comment lines and 38,930 bytes added to
src/marketing/objects/campaign.object.ts, restored viagit checkout HEAD --:The blob hashes are the on-disk proof that the mutation landed and that the restore was exact. A new case,
never lets comment mass move any package's number, pins the invariant on every ceilinged row of every package — #1928 multiplied the readings by four, and a regression moving onlysrc/marketing's would pass every existing stripping case.The exclusion ruling is unchanged, and now pinned per package
「translations + seed 肯定是不需要算 token 的」. Two new cases:
never measures translations/ or seed data, in ANY package, however large they grow— 400KB written into every excluded directory of every package; every package's total is unmoved and the run stays green. A module's own seed rows must not eat the module's own budget either.resolves the exclusion against each package root, never by trailing segment— a directory nameddatainsideobjects/is still counted. The ruling is two directory names under a package root, not a segment match anywhere.Which lists went away
The package roster is read off disk: a directory under
src/is a package when it carries anobjects/directory — the same predicate, in the same words, thattest/helpers/src-roster.tsandtest/docs-src-tree-paths.test.tsuse, established in #1941/#1940.PACKAGE_DIRSwas a hand-written four-element array; here that array would have been worse than it was there, because a package missing from it is a package with no ceiling, and an unratcheted package reads exactly like a package under its ceiling.src/docs/falls out of the predicate rather than out of an exception list — four.mdfiles, noobjects/, and it is named nowhere in the gate.does not treat a directory without objects/ as a package — src/docs stays outpins that.EXCLUDEDis derived the same way: two directory names (the ruling's whole content) resolved against every package that has one.LAYERSnow holds subdirectory names, which is what makes the same two layers read on every package; before, they were paths under the one ceilinged package and the per-package readings were produced by slicing that prefix back off.*.hook.tsstill counts inbusiness semantics: a hook sits beside the*.object.tsit names, so it is insideobjects/and is counted there, once. The per-package totals above reconcile exactly with the pre-#1928 per-package table, which is the check that nothing was dropped or double-counted.New refusals, each driven rather than described
Deriving the roster retires the old stale-path failure and creates three new quiet ones, so the gate refuses to run on each. Every one is asserted by causing it:
src/holding no package at all;src/salesmoving out from under the README's headline claim.One of these is worth reading twice:
objects/is both a business-semantics directory and the roster predicate, so losing it everywhere is not "the business layer vanished" but "there are no packages".falls to the empty-roster refusal when objects/ itself disappearsasserts exactly that, rather than pretending the layer refusal covers a state it cannot reach.Two bugs this change surfaced, both fixed in the same file
anchor()rounds up to the next 1,000; onsrc/marketing's interaction ceiling of 1,000 that rounding is the ceiling, since every reading from 1 to 952 anchors to exactly 1,000. Four of the twelve freshly anchored rows sat permanently past the relative 10% trigger whileanchor()had nothing lower to offer. It now also requiresanchor(reading) < ceiling. An advisory that cries wolf on every green run teaches a reader to skip the line on the run where it means something. Pinned byoffers a re-anchor only when anchor() would commit a LOWER number, which asserts both branches with a measured non-vacuity count on each.src/marketingfailure told the author they had breached the app's headline claim, sending them to argue about the wrong number. It now says the budget is that package's own and no other package's headroom can pay for it.Also corrected: the "outside the ratchet by ruling" line named all five excluded directories but measured only the app package's two (43 files, ~85,452 tokens). It now measures what it names (51 files, ~94,648 tokens).
The README banner is deliberately untouched
「解耦:banner 钉实测,ceiling 独立」 (#1601). The banner is pinned to the measured reading; the ceiling stands on its own. So the ceiling moving 53,000 to 55,000 moves the ceiling column of the band table in
test/docs-readme-token-figures.test.tsand nothing else — the band edges did not move, because the banner did not.CEILINGSstill exposes only the app package's three rows under bare labels, and the gate's--jsonstill carries them under a top-levelscopeskey, because ADR-0130 §1.3(b) is a claim about the package a customer installs, not about the four summed.keeps the app package's rows under bare labels, for the README pinpins that split from the producer side; re-pointing either at the twelve-row table would silently check a sentence about sales againstsrc/marketing's ceiling.What the README sub-line did need: it said the gate "prints a reading for each of the four packages, so a per-module budget has real starting figures", which this PR makes false. It now says each package carries its own three ceilings.
Docs swept, not recalled
grepfirst, then read — every reference point, not the ones that came to mind:README.md— the sub-line above.docs/architecture/module-split-plan.mditem 8 — "only sales carries ceilings" is superseded; marked landed, with the ruling quoted.docs/architecture/psa-module-plan.md— "PSA, like every package other than sales, is outside the gate" is superseded, and in PSA's favour: its ceilings will be its own, so its 15–20k estimate is weighed against its own budget rather than against sales'..github/tasks/new-package.mdstep 5 — "add the package directory toPACKAGE_DIRS" no longer exists as an action; it is now "commit the package's ceilings", with the anchoring-is-not-a-raise note.test/docs-readme-token-figures.test.ts— the band table's ceiling column, plus a paragraph on why this rule reads one package of four.test/script-main-guard.test.ts— its red leg pinnedmissing: src/sales/objects, a message the derived roster retires.docs/architecture/module-split-plan.md"The token budget" item 3 — "itsLAYERSlist namessrc/objects…src/apps" has been false since Layout: move every authored file into its package directory (src/sales·src/service·src/revenue·src/marketing) — one stack still registers everything, artifact bit-identical #1905 and is made more false here (the list holds metadata-type names now, not paths). Dated in place rather than left to mislead the next reader re-deriving the gate's surface from a planning document; the rule it states survives both moves and is kept.Two premises in the card did not survive checking, and are reported rather than worked around:
docs/adr/directory in this repository. ADR-0130's numbered sections live indocs/architecture/module-split-plan.md(the split plan) and are referenced fromdocs/ARCHITECTURE.mdanddocs/README.md. The §4 starting figures were updated where they actually live.docs/STATUS.mdcites none of these figures.grep -niE "token|ratchet|ceiling|budget"over it returns nothing; it is 100 lines and carries no measurement. Nothing to update there.Changeset: empty frontmatter, deliberately
.changeset/per-module-token-ratchet.mdcarries empty frontmatter — the sanctioned "this PR releases nothing" declaration.github/workflows/changeset-check.ymldocuments, on par with theskip-changesetlabel, and the form the sibling gate PR.changeset/config-globs-must-match-something.mdused.The judgement, measured rather than asserted: the diff opens
scripts/,test/,README.md,.github/tasks/anddocs/architecture/. It opens no fileobjectstack buildreads — notobjectstack.config.ts, notobjectstack.composition.ts, not one line undersrc/(includingsrc/docs/, the ADR-0046 in-product docs path) — and nothing undercontent/docs/. So the published artifact and the documentation site are byte-identical tomain, and a patch bump would advertise a release that ships nothing. The entry itself is written for the release-notes reader anyway, because the gate's behaviour changed materially.Verification
pnpm verify— exit code captured to a file before any pipe:node scripts/check-source-token-ratchet.mjson this branch: exit 0, twelve✓rows, no advisory on any of them.test/source-token-ratchet.test.ts: 40 passed. The whole chain above was re-run at the final HEAD,5f1ab82, after the last documentation commit landed — the figures and the ratchet verdict are that tree's, not an earlier one's.Acceptance notes
src/**is untouched —git diff --stat origin/main..HEADnames no file under it. This card changes the gate, not the thing the gate measures.src/sales's business layer will reddentest/docs-readme-token-figures.test.tsand should restate it from the gate. Noted, not filed: the guard is working as designed and nothing is broken today.🤖 Generated with Claude Code
https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
Generated by Claude Code