From a4334e8f98ec9096f3387c8a8b4303abe457bfab Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 23:10:10 +0000 Subject: [PATCH 1/3] feat(ratchet): rule the business-semantics ceiling to 100,000 and decouple the banner Raise CEILINGS['business semantics'] 85,000 -> 100,000 on the 2026-09-05 maintainer ruling, quoted verbatim in the PR body as this gate's header requires. The interaction layer and the authored total are untouched. Introduce the distinction the raise forces: a ceiling is now either ANCHORED (anchor(reading), shrink-only, owes a worked row) or RULED (a maintainer grant, derived from no reading). No real reading anchors to 100,000 -- that needs one between 94,286 and 95,238 and the tree measures 84,579 -- so the header records the ruled kind and writes no worked row rather than fabricating a reading. Decouple the README banner from the ceiling. The banner rule was asserting that each layer's band closed above its ceiling and reasoning that the ratchet failed first on growth; that coupling would have made the banner band the effective cap and defeated the raise. It is re-aimed, not deleted: the banner must track the MEASURED reading within the ruled buffer. The case tying each row's restated ceiling to CEILINGS stays. Both banner figures move to the measurement -- business semantics ~81k -> ~85k, interaction layer ~39k -> ~37k -- because it is one rule and leaving a row on the old basis would mean two. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018xtjdpZFjgWh4Ad9Wcx68J --- ...led-ceiling-banner-pins-the-measurement.md | 52 +++++++++++ README.md | 4 +- scripts/check-source-token-ratchet.mjs | 75 +++++++++++---- test/docs-readme-token-figures.test.ts | 92 +++++++++++++------ 4 files changed, 174 insertions(+), 49 deletions(-) create mode 100644 .changeset/ratchet-ruled-ceiling-banner-pins-the-measurement.md diff --git a/.changeset/ratchet-ruled-ceiling-banner-pins-the-measurement.md b/.changeset/ratchet-ruled-ceiling-banner-pins-the-measurement.md new file mode 100644 index 00000000..0374c4a7 --- /dev/null +++ b/.changeset/ratchet-ruled-ceiling-banner-pins-the-measurement.md @@ -0,0 +1,52 @@ +--- +'hotcrm': patch +--- + +Re-anchor the business-semantics token ratchet to 100,000 on a maintainer ruling, +and decouple the README banner from the ceiling. + +Maintainer ruling, 2026-09-05, quoted verbatim and untranslated as +`scripts/check-source-token-ratchet.mjs`'s own header requires of any PR that +raises a ceiling: + +> business-semantics 棘轮 提升到 100000 + +and the shape it was given, from the same exchange, 选项 A: + +> 解耦:banner 钉实测,ceiling 独立 + +**What moved.** `CEILINGS['business semantics']` 85,000 -> 100,000. The +interaction layer (40,000) and the authored total (140,000) are untouched — the +ruling names one layer. The README banner now states the measurement instead of a +figure that had drifted from it: business semantics ~81k -> **~85k** (measured +84,579), interaction layer ~39k -> **~37k** (measured 37,429). Both rows move, +because the rule that governs them is one rule. + +**Why it is not a constant change.** The banner and the ceiling used to be tied +together: `test/docs-readme-token-figures.test.ts` asserted that each layer's +banner band closed *above* its committed ceiling, and reasoned from that the +ratchet would fail first on growth. With that coupling intact, raising the +ceiling would not have created headroom — the banner band's upper edge would have +become the effective cap, and the assertion would simply have gone red. So the +two numbers are separated and each is given one job. The ceiling is the growth +budget, alone. The banner rule becomes a truthfulness rule: the README figure +must track the measured reading within the ruled 5% buffer. It caps nothing; it +stops the README advertising a size the app does not have. The case is re-aimed, +not deleted, and the case tying each row's restated ceiling to `CEILINGS` stays — +that is what stops the README quoting a ceiling the gate no longer commits. + +**A ceiling can now be RULED rather than anchored.** Every ceiling until now was +`anchor(reading)` — a reading the gate printed, plus the buffer, rounded up. A +maintainer grant is not derived from any reading, and on this layer none could +derive it: anchoring to 100,000 would need a reading between 94,286 and 95,238 +and the tree measures 84,579. The header records it as a ruled ceiling and +deliberately writes no worked row for it, rather than reverse-engineering a +reading that would produce the constant. + +Two consequences are recorded in the header rather than silently absorbed. The +gate's opportunistic-tightening advisory now fires on this layer every run, +suggesting a re-anchor down to ~89,000 — following it would hand back the +headroom the ruling created, so a ruled ceiling is not tightened on that line +alone. And the shrink-only "declined as a raise" line for business semantics is +retired: at a 100,000 ceiling its `anchor()` lands below, so the reasoning that +line recorded no longer describes the layer. diff --git a/README.md b/README.md index 7f9a2bdc..0ea08478 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ > **The reference app for AI-written enterprise software.** A complete CRM — > 18 objects, 28 flows, 5 dashboards, 6 AI skills, 4 languages — carries its -> whole business semantics (objects, flows, actions, hooks) in **~81k tokens** +> whole business semantics (objects, flows, actions, hooks) in **~85k tokens** > of typed [ObjectStack](https://github.com/objectstack-ai/objectstack) metadata, > and its whole interaction layer (views, pages, dashboards, app shell) in -> another **~39k**. An agent holds every business rule of a real enterprise CRM +> another **~37k**. An agent holds every business rule of a real enterprise CRM > in a fraction of one context window — and the entire authored app still fits > comfortably — so it can reason about the system whole and refactor it safely. > **Install it online in one click, or fork it and build & ask with Claude diff --git a/scripts/check-source-token-ratchet.mjs b/scripts/check-source-token-ratchet.mjs index a16e8f04..8bb0b432 100644 --- a/scripts/check-source-token-ratchet.mjs +++ b/scripts/check-source-token-ratchet.mjs @@ -178,10 +178,40 @@ export const BUFFER = 0.05; export const anchor = (tokens) => Math.ceil((tokens * (1 + BUFFER)) / 1000) * 1000; /** - * Shrink-only ceilings, in estimated tokens. + * Ceilings, in estimated tokens — and since #1601 there are two KINDS of them. * - * A ceiling moves only in the PR that shrinks its own scope, so re-anchoring is - * per-layer and these three no longer come from one run. Both anchoring runs: + * An ANCHORED ceiling is `anchor(reading)`: a reading this gate actually + * printed, plus the ruled 5% working buffer, rounded up to the next 1,000. It + * is shrink-only, it moves in the PR that shrinks its own scope, and it owes a + * worked row below so anyone can re-run the arithmetic. + * + * A RULED ceiling is a maintainer grant. It is NOT derived from any reading, + * so there is no `anchor()` arithmetic to show for it and no worked row that + * could honestly be written: the only record it can carry is the ruling. Do + * not reverse-engineer a reading that would produce it — on this layer no real + * reading can (a reading between 94,286 and 95,238 would be needed to anchor + * to 100,000, and the tree measures 84,579), so any such row would be a + * fabricated measurement dressed as arithmetic. + * + * `business semantics` became a ruled ceiling on 2026-09-05 (#1601). The + * ruling, verbatim and untranslated: + * + * 「business-semantics 棘轮 提升到 100000」 + * + * and the shape it was given, from the same exchange, 选项 A: + * + * 「解耦:banner 钉实测,ceiling 独立」 + * + * ⚠️ A ruled ceiling still prints this gate's opportunistic-tightening + * advisory: at 84,579 against 100,000 the headroom is over twice the buffer, so + * every run now suggests re-anchoring down to ~89,000. Following that advice + * would hand back the headroom the ruling was made to create. The advisory is + * left as-is deliberately — changing when it fires is gate behaviour, and this + * card was scoped to the constant, the docs and the banner rule — but ⛔ a + * ruled ceiling is not tightened on the strength of that line alone. It moves + * on another ruling, or on a PR that genuinely shrinks the layer and says so. + * + * The anchoring runs the two anchored ceilings below come from: * * node scripts/check-source-token-ratchet.mjs # 2026-08-17 03:20 UTC, `main` at d038b957 * #1189 — removed the account renewal fields, their view and their seed values @@ -191,39 +221,46 @@ export const anchor = (tokens) => Math.ceil((tokens * (1 + BUFFER)) / 1000) * 10 * #1316 — removed the inert `list.tabs[]` block from every view file * business semantics ~82,489 · interaction layer ~37,424 · authored total ~133,840 * - * Each ceiling is `anchor(reading)` — the reading plus the ruled 5% working - * buffer, rounded up to the next 1,000. `headroom` is the headroom **at anchor - * time** (`ceiling - reading`, on that row's own run): it is a derivation of the - * constant beside it, not a live figure, so it deliberately does not track what - * the gate prints today — the tree keeps moving between re-anchorings. + * `headroom` is the headroom **at anchor time** (`ceiling - reading`, on that + * row's own run): it is a derivation of the constant beside it, not a live + * figure, so it deliberately does not track what the gate prints today — the + * tree keeps moving between re-anchorings. * - * business semantics 80,356 × 1.05 = 84,374 -> ceil 1k -> 85,000 (headroom 4,644, 5.8%) 2026-08-17 + * business semantics ruled 100,000 — a maintainer grant, no reading derives it 2026-09-05 * interaction layer 37,424 × 1.05 = 39,295 -> ceil 1k -> 40,000 (headroom 2,576, 6.9%) 2026-08-26 * authored total 133,302 × 1.05 = 139,967 -> ceil 1k -> 140,000 (headroom 6,698, 5.0%) 2026-08-17 * - * The rounding step is what carries two of the three a little past 5%; it is + * The rounding step is what carries the anchored rows a little past 5%; it is * kept because a ceiling a reader can hold in their head is worth more than the * last few hundred tokens of precision on a number estimated as `chars / 4`. * - * Only the interaction layer re-anchored on 2026-08-26, and the other two rows - * are left alone on purpose: on that same run `anchor()` of each of their - * readings lands ABOVE the ceiling committed below it. One worked line per - * declined re-anchoring, in the shape of the table above — reading, its - * `anchor()`, and the committed ceiling it would have raised: + * The ruled row is deliberately NOT in the worked-row shape, and that is a + * measurement in itself: `test/source-token-ratchet.test.ts` pins one worked + * row per committed ceiling and reads every ceiling as `anchor()` of the + * reading beside it. That model has no ruled ceiling in it, so it goes red on + * this row — correctly, and with the message that says teach the shape. ⛔ The + * fix there is to teach that suite the ruled kind, never to relax the pin and + * never to fabricate a reading for this row. + * + * Of the ceilings left alone by the latest ANCHORING run (2026-08-26), one + * worked line each — reading, its `anchor()`, and the committed ceiling it + * would have raised: * - * business semantics anchor( 82,489) = 87,000 > ceiling 85,000 2026-08-26 * authored total anchor(133,840) = 141,000 > ceiling 140,000 2026-08-26 * - * Re-anchoring either would therefore be a RAISE, and a raise sits on the + * Re-anchoring it would therefore be a RAISE, and a raise sits on the * maintainer floor. So a shrink-only ratchet re-anchors a layer only when * `anchor(reading) < ceiling`; when it is greater the committed ceiling is - * already the tighter of the two and stands. + * already the tighter of the two and stands. `business semantics` has no such + * line any more: it left the anchored kind entirely, and on the reading current + * at #1601 (~84,579, `main` at a4e5ea3e) `anchor()` lands at 89,000 — BELOW its + * ruled ceiling, so the declined-raise reasoning no longer describes it at all. * * Lower them whenever the tree shrinks — that is free and encouraged. Raising * one requires a maintainer ruling quoted in the raising PR's body. */ export const CEILINGS = new Map([ - ['business semantics', 85000], + ['business semantics', 100000], ['interaction layer', 40000], ['authored total', 140000], ]); diff --git a/test/docs-readme-token-figures.test.ts b/test/docs-readme-token-figures.test.ts index 72d797af..65f86a16 100644 --- a/test/docs-readme-token-figures.test.ts +++ b/test/docs-readme-token-figures.test.ts @@ -44,20 +44,37 @@ import { REPO_ROOT } from './helpers/repo-root'; * * So the banner is held to the maintainer's already-ruled working buffer rather * than to a fresh number invented here: 「给 5% 缓冲」 (2026-08-17), the same 5% - * the ratchet's ceilings carry via its `anchor()`. Reusing it is the point — the - * README figure and the ratchet ceiling then go stale at nearly the same - * reading, so re-stating the claim is a maintainer-ruling moment, not a chore. - * Against today's committed ceilings: + * the ratchet's ceilings carry via its `anchor()`. Reusing it keeps one buffer + * in this repo rather than two that drift apart. * - * business semantics banner ~81k -> band 76,950–85,050 · ceiling 85,000 - * interaction layer banner ~39k -> band 37,050–40,950 · ceiling 40,000 + * ## What this rule is FOR, since #1601: honesty, not a budget * - * On both layers the band's upper edge sits past the committed ceiling, so on - * growth the ratchet fails first and the banner cannot be advertising a surface - * CI is already configured to reject. Shrinkage is the direction this rule owns - * alone: a banner left behind by a shrinking tree breaks no ceiling, and only - * the band's lower edge objects. Early is the safe direction for a doc guard; - * late is the one that cost #1187. + * Maintainer ruling, 2026-09-05 (verbatim, kept untranslated): + * + * 「解耦:banner 钉实测,ceiling 独立」 + * + * The banner is pinned to the MEASURED reading; the ceiling stands on its own. + * So this rule caps nothing. Its whole job is that the README may not advertise + * a size the app does not have — the ratchet owns the growth budget, alone, and + * a raise there sits on the maintainer floor where it always did. + * + * business semantics banner ~85k -> band 80,750–89,250 · ceiling 100,000 + * interaction layer banner ~37k -> band 35,150–38,850 · ceiling 40,000 + * + * ⚠️ The ceiling column is still worked out per row, and still pinned to + * `CEILINGS` below — that is what stops the README quoting a ceiling the gate no + * longer commits. What it is NOT any more is an input to this rule. Until #1601 + * the paragraph here compared each band's upper edge against its ceiling and + * argued from the result that the ratchet failed first on growth; that + * comparison was the coupling, and it is gone. Which of the two reddens first on + * growth is deliberately no longer a fact this file reasons from, because the + * two reds are not paid in the same currency: this one is cleared by restating + * the banner, which any PR may do, and the ratchet's only by shrinking the layer + * or by another ruling. + * + * Shrinkage is the direction this rule owns alone: a banner left behind by a + * shrinking tree breaks no ceiling, and only the band's lower edge objects. + * Early is the safe direction for a doc guard; late is the one that cost #1187. * * ⚠️ That paragraph read the other way round until #1320 re-anchored the * interaction ceiling 42,000 -> 40,000. The table above went on saying 42,000, @@ -374,25 +391,44 @@ describe('the README headline figures match the token gate (#1187)', () => { } }); - it('still supports the claim the paragraph draws from it', () => { - // The prose argues that on growth the ratchet fails before this rule - // does, on BOTH layers. That is a relation between two constants, so it - // is pinned as one — it was true of one layer only before #1320, and read - // as true of both for the eleven days after. + it('works from a band that actually holds the measured reading', () => { + // The relation the paragraph draws from the table, re-aimed by the #1601 + // ruling 「解耦:banner 钉实测,ceiling 独立」. It used to compare the band's + // upper edge against the ceiling and argue that the ratchet failed first; + // that comparison WAS the coupling the ruling removed, so it is not + // inverted here, it is replaced. What the rows must support now is the + // truthfulness claim: the band each row prints is one today's reading + // actually falls inside. // - // Deliberately read against the ROW's ceiling rather than the committed - // one: the case above already ties the row to `CEILINGS`, so a moved - // constant reddens exactly one case with an exact message, and this one - // fires next — when the row has been corrected and the paragraph over it - // has not. One cause, one red, in the order a maintainer fixes them. + // Read against the ROW's own edges rather than recomputed from the + // README: the two cases above already tie the row to the banner and to + // `BUFFER`, so a moved figure reddens exactly one of them with an exact + // message and this one fires next — when the rows are self-consistent and + // the tree has moved out from under them anyway. One cause, one red, in + // the order a maintainer fixes them. The edges read here are also the + // ROUNDED, published ones, so what is checked is the band as a reader + // reads it rather than the unrounded band the tolerance case computes. for (const row of rows()) { + const tokens = measured[row.label]?.tokens; + expect( + tokens, + `${where(row)}the gate reports no reading for '${row.label}', so this row's band is ` + + 'checked against nothing. Vacuity, not a pass.', + ).toBeTypeOf('number'); + expect( + tokens, + `${where(row)}the gate measures ${tokens === undefined ? 'nothing' : fmt(tokens)}, ` + + `below the band this row publishes (${fmt(row.low)}–${fmt(row.high)}). The banner ` + + 'is advertising a bigger surface than the app has: restate it from the gate and ' + + 'correct this row.', + ).toBeGreaterThanOrEqual(row.low); expect( - row.high, - `${where(row)}the docstring argues the ratchet fails first on growth, but this ` + - `layer's band now closes at ${fmt(row.high)}, at or below the committed ceiling ` + - `${fmt(row.ceiling)} — so this rule fires first and the paragraph above needs ` + - 'rewriting, not relaxing.', - ).toBeGreaterThan(row.ceiling); + tokens, + `${where(row)}the gate measures ${tokens === undefined ? 'nothing' : fmt(tokens)}, ` + + `above the band this row publishes (${fmt(row.low)}–${fmt(row.high)}). The banner ` + + 'understates the surface: restate it from the gate and correct this row. This is ' + + 'not a ceiling — the ratchet owns growth, and it has its own verdict on it.', + ).toBeLessThanOrEqual(row.high); } }); }); From 9cfd0540f0841b41263be09875b087231921d940 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 23:26:03 +0000 Subject: [PATCH 2/3] test(ratchet): teach the header-table pin that a ceiling can be RULED The suite modelled every ceiling as anchor() of a measured reading, so a maintainer grant could not be expressed in it at all: the "one worked row per committed ceiling" case went red on the ruled row no matter how the header was written, and no honest reading exists that anchors to 100,000 (that needs 94,286..95,238; the tree measures 84,579). Teach it the second kind rather than relax it. RULED_ROW parses a granted ceiling; the roster case now merges both kinds on document position, so it still asserts one row per committed ceiling in the committed order and remains the vacuity guard for both parsers. Every anchored-row case is untouched. The new case is an added invariant, not a weaker one: a ruled row's ceiling must be the committed constant, and no reading recorded in the header may anchor to it. Stated that way on purpose -- "no integer anchors here" is false of every multiple of 1,000, so the checkable claim is that the readings this file records did not produce the constant. That is what stops a ceiling being filed as ruled to dodge arithmetic that did in fact apply. Ruled ceilings are outside the declined-as-a-raise ledger by construction, which is now stated where that case reads its input. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018xtjdpZFjgWh4Ad9Wcx68J --- test/source-token-ratchet.test.ts | 122 ++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 7 deletions(-) diff --git a/test/source-token-ratchet.test.ts b/test/source-token-ratchet.test.ts index 66d12c60..34b6dd01 100644 --- a/test/source-token-ratchet.test.ts +++ b/test/source-token-ratchet.test.ts @@ -403,6 +403,16 @@ describe('source token ratchet — this repository, today', () => { * the constants, so it rots the moment one moves — which is exactly what #1317 * found when it re-anchored the interaction layer and the row went false. * + * Since #1601 the table carries TWO shapes, because a ceiling can now be + * reached two ways. An ANCHORED row shows its arithmetic — `anchor()` of a + * reading this gate printed — and every case below still holds it to that. A + * RULED row is a maintainer grant: no reading derives it, so there is no + * arithmetic to show and the row states the constant and the date it was ruled. + * Both kinds are pinned and neither may go missing. The kind is not a free + * choice either: the ruled case checks that no reading recorded here could have + * anchored the constant, which is what stops a ceiling being filed as "ruled" + * to dodge arithmetic that did in fact apply. + * * ⚠️ `headroom` on a row is the headroom **at anchor time** — that row's own * reading against its own ceiling — and is deliberately NOT what the gate prints * today: the tree keeps moving between re-anchorings, and since #1320 the three @@ -437,17 +447,19 @@ describe('source token ratchet — the header table is derived from the ceilings pct: string; date: string; line: string; + index: number; } function rows(): Row[] { return source() .split('\n') - .flatMap((line) => { + .flatMap((line, index) => { const found = TABLE_ROW.exec(line); if (!found?.groups) return []; const g = found.groups; return [ { + index, label: g.label, reading: num(g.reading), multiplier: g.multiplier, @@ -485,6 +497,55 @@ describe('source token ratchet — the header table is derived from the ceilings }); } + /** + * One RULED row — a ceiling granted by a maintainer rather than anchored: + * + * * business semantics ruled 100,000 — a maintainer grant, no reading derives it 2026-09-05 + * + * There is no arithmetic in it because there is none to state: the constant + * comes from a ruling, not from a reading. The reason it is captured at all + * is that it still restates a committed ceiling in prose, which is the whole + * hazard this describe block exists for. Same discipline as `TABLE_ROW`: the + * column widths and the prose between the constant and the date are free, but + * the label, the ceiling and the date are captured, so a row that quietly + * loses one stops parsing instead of passing. + * + * ⛔ Never widen this to make an anchored row match it. A ceiling that a + * recorded reading anchors to is an anchored ceiling and owes a worked row; + * the case below is what holds that line. + */ + const RULED_ROW = + / \* {2,}(?