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..186b073c --- /dev/null +++ b/.changeset/ratchet-ruled-ceiling-banner-pins-the-measurement.md @@ -0,0 +1,62 @@ +--- +'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. + +`test/source-token-ratchet.test.ts` learns that second kind. It modelled every +ceiling as `anchor()` of a measured reading, so a maintainer grant could not be +expressed in it at all. It now parses a ruled row as well as a worked one, still +asserts one row per committed ceiling in the committed order, and keeps every +anchored-row case exactly as it was. The kind is not a free choice: a ruled row +must restate the committed constant, and no reading recorded in the header may +anchor to it — which is what stops a ceiling being filed as "ruled" to dodge +arithmetic that did in fact apply. That is a new invariant, so the suite is +strictly more expressive than before, not weaker. + +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); } }); }); 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,}(?