From 72efd43f911d5c55279834e99c6ae7e85ac54b88 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Mon, 21 Sep 2026 19:02:57 +0200 Subject: [PATCH] guard: the summary has a ceiling and holds no state, and the regression table is one document CLAUDE.md is read at every message of every session. Measured 2026-09-21 it had grown to 310 036 bytes, about 90 thousand tokens a message, and 98 KB of it was a journal: twenty dated state blocks, fourteen struck through, one added on top by each session and none removed. The owner's decision: a ceiling of about 30 KB, the state in one document rewritten rather than appended to, the verdict table of the regression surface in REGRESSION.md above the paragraphs that justify it. - summaryshape_test.go: CLAUDE.md fits a ratcheting ceiling, holds no dated state block and keeps the sections a new session is promised; docs/STATE.md fits its own ceiling and has exactly one "where we are". Broken by hand in all five directions before it was trusted. - regressiontable_test.go: the verdict table and the justifications are two halves of REGRESSION.md now, split at a heading the guard asserts is there, and a table citing nothing is a refusal rather than a pass. - mutationcoverage_test.go: the new guard is proven by probe, as every guard reading documents outside the repository is. Both documents are outside the repository, so both guards skip loudly on a fresh clone, as the other document guards do. Co-Authored-By: Claude Opus 5 --- internal/guard/mutationcoverage_test.go | 6 +- internal/guard/regressiontable_test.go | 63 ++++++++------ internal/guard/summaryshape_test.go | 110 ++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 internal/guard/summaryshape_test.go diff --git a/internal/guard/mutationcoverage_test.go b/internal/guard/mutationcoverage_test.go index c703e8d1..937f500b 100644 --- a/internal/guard/mutationcoverage_test.go +++ b/internal/guard/mutationcoverage_test.go @@ -143,11 +143,15 @@ var provenByProbe = map[string]string{ "A probe rather than a mutation entry because there is no product code underneath: it reads the test files of this package, " + "so a substitution in a shipped .go file never reaches it.", + "TestTheSummaryFitsItsCeilingAndHoldsNoState": "broken by hand on 2026-09-21 in all five directions it can fail - a dated STAN NA block in CLAUDE.md, a promised heading gone, " + + "2 KB past the ceiling, STATE.md with a second \"Gdzie jestesmy\", STATE.md past its ceiling - red each time, green with the files put back byte for byte (cmp). " + + "A probe rather than a mutation entry because there is no product code underneath: it reads CLAUDE.md and docs/STATE.md, neither of which is in the repository.", + "TestEveryGuardCitedInTheSummaryIsJustifiedInRegression": "broken by hand on 2026-08-26, in all three directions it can fail, and put back byte for byte through the checked restore in tools/mutate/runner.py. " + "Taking keyboard_test.go off notYetJustified made it red, because CLAUDE.md cites that guard and REGRESSION.md still says nothing about it. " + "Adding verify_test.go to the list made it red the other way, which is the direction that matters most: a list nobody prunes is where this drift would hide next. " + "Naming a guard that does not exist made it red as well. Green again after each, and the run before them all was green, so none of the three was red for a reason that was already there. " + - "A probe rather than a mutation entry because there is no product code underneath: it reads CLAUDE.md and docs/REGRESSION.md, neither of which is in the repository, " + + "A probe rather than a mutation entry because there is no product code underneath: it reads docs/REGRESSION.md (both halves of it since 2026-09-21), which is not in the repository, " + "so a substitution in a .go file never reaches it.", "TestARunIsStoppableFromTheMomentItStarts": "checked 2026-08-23 with tools/probes/run-stop-order.py, which reproduces the original defect faithfully: it moves the assignment of r.stop " + diff --git a/internal/guard/regressiontable_test.go b/internal/guard/regressiontable_test.go index de9f8180..182fb1f2 100644 --- a/internal/guard/regressiontable_test.go +++ b/internal/guard/regressiontable_test.go @@ -9,8 +9,8 @@ import ( "testing" ) -// Every guard file CLAUDE.md cites as evidence has a paragraph in -// REGRESSION.md saying what it defends. +// Every guard file the verdict table of REGRESSION.md cites as evidence has a +// paragraph further down the same document saying what it defends. // // The two tables were one table until 2026-08-12, when the justifications were // moved out because they were 12.8k tokens of a file that is read at every step @@ -27,6 +27,13 @@ import ( // What is actually at stake is the verdict column. "JEST" with nothing behind // it reads as an assurance, which is precisely what the header of REGRESSION.md // warns against. +// +// Until 2026-09-21 the verdict table lived in CLAUDE.md and the paragraphs +// here, so this compared two files. That day CLAUDE.md was cut from 310 KB to +// a guarded ceiling and the table moved in above the paragraphs, so the two +// halves are now two sections of one document, split at the heading below. +// The drift this watches for is unchanged: a row can be added to the table +// without a paragraph, whichever file the table is in. // notYetJustified is the sixteen this check started with, and it may only // shrink. @@ -59,6 +66,10 @@ var notYetJustified = []string{ var guardFileName = regexp.MustCompile(`[A-Za-z0-9_]+_test\.go`) +// justificationsHeading is where the verdict table ends and the paragraphs +// begin in REGRESSION.md. +const justificationsHeading = "\n## Uzasadnienia" + // citedInTables is every guard file named inside a table row. // // Table rows rather than the whole file, because both documents also mention @@ -80,30 +91,28 @@ func citedInTables(body string) map[string]bool { func TestEveryGuardCitedInTheSummaryIsJustifiedInRegression(t *testing.T) { root := repoRoot(t) - summary := filepath.Join(root, "CLAUDE.md") - detail := filepath.Join(root, "docs", "REGRESSION.md") - - for _, path := range []string{summary, detail} { - if _, err := os.Stat(path); err != nil { - t.Logf("SKIPPED: %s is not here, so nothing was compared. "+ - "The internal documents are excluded from the repository, so this check only "+ - "runs on a machine that has them. (%v)", filepath.Base(path), err) - return - } + body, err := os.ReadFile(filepath.Join(root, "docs", "REGRESSION.md")) + if err != nil { + t.Logf("SKIPPED: REGRESSION.md is not here, so nothing was compared. "+ + "The internal documents are excluded from the repository, so this check only "+ + "runs on a machine that has them. (%v)", err) + return } - summaryBody, err := os.ReadFile(summary) - if err != nil { - t.Fatalf("reading CLAUDE.md: %v", err) + // The document is two halves: the verdict table, then the paragraphs. The + // split is asserted rather than assumed - a document without the heading + // is one this guard cannot read, not one it may pass - and so is the table + // having rows to compare, because two empty sets agree about everything. + summaryBody, detailBody, ok := strings.Cut(string(body), justificationsHeading) + if !ok { + t.Fatalf("REGRESSION.md has no heading %q, so the verdict table and the paragraphs cannot be told apart", strings.TrimSpace(justificationsHeading)) } - detailBody, err := os.ReadFile(detail) - if err != nil { - t.Fatalf("reading REGRESSION.md: %v", err) + cited := citedInTables(summaryBody) + justified := citedInTables(detailBody) + if len(cited) == 0 { + t.Fatal("the verdict table of REGRESSION.md cites no guard file at all, so this guard has nothing to compare") } - cited := citedInTables(string(summaryBody)) - justified := citedInTables(string(detailBody)) - allowed := map[string]bool{} for _, name := range notYetJustified { allowed[name] = true @@ -126,7 +135,7 @@ func TestEveryGuardCitedInTheSummaryIsJustifiedInRegression(t *testing.T) { sort.Strings(stale) for _, name := range unjustified { - t.Errorf("CLAUDE.md cites %s as evidence and REGRESSION.md says nothing about it.\n"+ + t.Errorf("the verdict table of REGRESSION.md cites %s as evidence and the paragraphs below say nothing about it.\n"+ "Reason: a verdict of JEST with no paragraph behind it reads as an assurance, which is\n"+ "what the header of REGRESSION.md warns against.\n"+ "What to do: write what that guard defends, from its commits and its code rather than\n"+ @@ -149,10 +158,10 @@ func TestEveryGuardCitedInTheSummaryIsJustifiedInRegression(t *testing.T) { } } - // Said rather than asserted. A row in REGRESSION.md whose file CLAUDE.md - // does not cite is not a defect - plenty of rows there carry their evidence - // in the paragraph and leave the summary's last column empty - but the - // number is worth seeing, because it is the same drift facing the other way. + // Said rather than asserted. A paragraph whose file the verdict table does + // not cite is not a defect - plenty of rows carry their evidence in the + // paragraph and leave the table's last column empty - but the number is + // worth seeing, because it is the same drift facing the other way. var onlyJustified []string for name := range justified { if !cited[name] { @@ -160,7 +169,7 @@ func TestEveryGuardCitedInTheSummaryIsJustifiedInRegression(t *testing.T) { } } sort.Strings(onlyJustified) - t.Logf("%d guard file(s) cited in CLAUDE.md, %d justified in REGRESSION.md, %d still excused", + t.Logf("%d guard file(s) cited in the verdict table, %d justified in the paragraphs, %d still excused", len(cited), len(justified), len(notYetJustified)) if len(onlyJustified) > 0 { t.Logf("justified but not cited in the summary, which is the same drift the other way: %v", diff --git a/internal/guard/summaryshape_test.go b/internal/guard/summaryshape_test.go new file mode 100644 index 00000000..6e70e905 --- /dev/null +++ b/internal/guard/summaryshape_test.go @@ -0,0 +1,110 @@ +package guard + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// CLAUDE.md and STATE.md have a size each, and CLAUDE.md holds no state. +// +// CLAUDE.md is read by the assistant at every message of every session, so +// every byte in it is paid for as many times as there are messages. Measured +// 2026-09-21: it had grown to 310 036 bytes, about 90 thousand tokens a +// message, and 98 KB of that was a journal - twenty blocks headed "STAN NA +// ", fourteen of them struck through, one added at the top by each +// session and none removed. The owner's decision that day: a ceiling of +// roughly 30 KB, the state in one document that is rewritten rather than +// appended to (docs/STATE.md), the history in HISTORY.md. +// +// Both ceilings are guarded rather than asked for, because the growth was not +// one session forgetting: it was the rhythm every session followed. And the +// ceiling of CLAUDE.md is a ratchet - it may only come down - because a ceiling +// far above the measurement is room to grow back into (ratchet_test.go). +// +// Skips loudly on a fresh clone: neither file is in the repository. + +// summaryCeiling is today's size of CLAUDE.md, rounded up to the next +// kilobyte. Lower it when the file shrinks. It may not go up. +const summaryCeiling = 37 * 1024 + +// summaryRatchetBand is how far below its ceiling CLAUDE.md may sit before +// the ceiling has to follow it down. +const summaryRatchetBand = 4 * 1024 + +// stateCeiling is the most docs/STATE.md may hold. A state that does not fit +// is a state carrying history: move what is finished to HISTORY.md. +const stateCeiling = 12 * 1024 + +// journalBlock is the shape of the blocks that turned CLAUDE.md into a +// journal: a dated state heading. The sentence forbidding them, which names +// the phrase without a date, is not a match. +var journalBlock = regexp.MustCompile(`STAN NA \d{4}-\d{2}-\d{2}`) + +// summaryHeadings are the sections a new session is promised. A CLAUDE.md +// missing one of them was cut in the wrong place. +var summaryHeadings = []string{ + "## Od czego zacząć w nowej sesji", + "## Pułapki, które już kosztowały", + "## Rytm weryfikacji", + "## Tryb pracy: analiza przed działaniem", + "## Jak mam myśleć i meldować", + "## Reguły nietykalne", + "## Czego nie robić", + "## Powierzchnia regresji", + "## Dokumenty", + "## Zasady GUI", +} + +func TestTheSummaryFitsItsCeilingAndHoldsNoState(t *testing.T) { + root := repoRoot(t) + body, err := os.ReadFile(filepath.Join(root, "CLAUDE.md")) + if err != nil { + t.Logf("SKIPPED: CLAUDE.md is not here (%v) - it is excluded from the repository", err) + return + } + size := len(body) + if size > summaryCeiling { + t.Errorf("CLAUDE.md is %d bytes and the ceiling is %d.\n"+ + "Reason: every byte here is paid for at every message of every session.\n"+ + "What to do: move what is a date, a measurement or a story to docs/ - state to STATE.md,\n"+ + "history to HISTORY.md, the machine to ENVIRONMENT.md. Rules and procedures stay. Do not raise the ceiling.", + size, summaryCeiling) + } + if summaryCeiling-size > summaryRatchetBand { + t.Errorf("CLAUDE.md is %d bytes and the ceiling is %d, which is more than %d bytes of room to grow back into.\n"+ + "What to do: lower summaryCeiling to today's size rounded up to the next kilobyte.", + size, summaryCeiling, summaryRatchetBand) + } + text := string(body) + if m := journalBlock.FindAllString(text, -1); len(m) > 0 { + t.Errorf("CLAUDE.md holds %d dated state block(s) (%q ...).\n"+ + "Reason: this is how it grew to 310 KB - one block per session, none removed.\n"+ + "What to do: the state lives in docs/STATE.md and is rewritten there. Move the block.", len(m), m[0]) + } + for _, h := range summaryHeadings { + if !strings.Contains(text, "\n"+h) { + t.Errorf("CLAUDE.md has no section %q - a new session is promised it", h) + } + } + t.Logf("CLAUDE.md is %d bytes under a ceiling of %d", size, summaryCeiling) + + state, err := os.ReadFile(filepath.Join(root, "docs", "STATE.md")) + if err != nil { + t.Logf("SKIPPED the state half: docs/STATE.md is not here (%v)", err) + return + } + if len(state) > stateCeiling { + t.Errorf("docs/STATE.md is %d bytes and the ceiling is %d.\n"+ + "Reason: a state that does not fit is a state carrying history.\n"+ + "What to do: move what is finished to HISTORY.md and rewrite the rest. Do not raise the ceiling.", + len(state), stateCeiling) + } + if strings.Count(string(state), "\n## Gdzie jestesmy") != 1 { + t.Errorf("docs/STATE.md has %d sections headed \"Gdzie jestesmy\" and it has to have exactly one - "+ + "it is rewritten, not appended to", strings.Count(string(state), "\n## Gdzie jestesmy")) + } + t.Logf("docs/STATE.md is %d bytes under a ceiling of %d", len(state), stateCeiling) +}