๐ค fix: two bugs on the sources page that only looking at it could find
The section was built, the tests were green, and the page was wrong in two ways โ both invisible to a template test, because neither changes a string anything was checking for. **A warning-toned callout containing the words "No refusal anomaly".** One `Alarming` flag carried two unrelated lines, so a sibling that had merely not been polled yet dragged the breaker's verdict into the alarm tone with it. A warning box with reassurance in it is the notice a reader learns to skip โ ยง8.1's whole argument, arriving through the markup rather than the predicate. Two lines, two states, toned independently, and `breakerSentence` no longer takes an incoming flag to inherit. **A warning on every row of every freshly-woken instance.** memoria hibernates, so "not polled yet" is the ordinary state of a wake's first seconds while the warm-up is still running. Firing for the routine case is the same failure again. It is plain text now; a warm-up that keeps failing is not thereby invisible, because the breaker beneath it says cannot-determine and every search from that sibling carries its own notice. **`tools/drive` now seeds three siblings**, in the three states the breaker has. It had none, so `/connectors` rendered the connector list and nothing else โ the section was shipped without anybody seeing it, which is exactly the gap drive exists to close. Three rather than one, because what a person must tell apart at a glance is the difference between them: a number, a longer sentence that must not read as reassurance, and a short line that must not shout. `SeedTallyForDrive` writes counters into the past and is named so it cannot be reached by accident, under the same `cmd/` gate as `AppForTest`: the breaker's entire claim is that its counters record what happened when it happened. AGENTS.md gains both lessons, and the rule that follows from them โ if you add a section to a page, add it to drive, because the section that is not seeded is the section nobody sees. `make ci` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H3VKtXnq6fSmM2F11Bjcsy
7 files changed,
+203
−21
AGENTS.md+15 −0internal/memoria/breaker.go+25 −0internal/memoria/breaker_test.go+55 −2internal/memoria/handlers_sources.go+36 −13internal/memoria/templates/connectors.html+12 −6internal/memoriatest/gates_test.go+4 −0tools/drive/main.go+56 −0
diff --git a/AGENTS.md b/AGENTS.md| index b5f1e6f..cb7b0f0 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -152,11 +152,26 @@ so; it never renders as a confident zero. |
| now checks the vendored files this app copies. |
| - The attribute grammar (`rst-btn="primary compact"`), never the class |
| spelling. Utilities stay classes. |
| +- **The design system has a vocabulary, and a word outside it renders as |
| + nothing.** `rst-callout="danger"` styles nothing: tokens.css carries the |
| + tone in `data-tone`, so a value on the attribute itself matches the base |
| + selector and no tone. Every flash in this app rendered untoned for |
| + months and **three tests pinned it there**, written from the same guess |
| + as the markup. Grep the stylesheet for the attribute before writing it. |
| + Gate: `TestEveryTemplateAttributeIsInTheDesignSystem`, which checks |
| + names *and* whether a value is one anything selects on. |
| - **Look at the page.** `tools/drive` serves the app with sign-in off so a |
| browser can see it. It found three bugs in its first run that the whole |
| Go suite passed over โ a heading trail rendering as "โบ Revenue", a |
| snippet repeating its own heading, and a submit button floating over the |
| textarea. A template test proves markup exists, not that it renders. |
| + It then found two more in the sources page the day that page was built: |
| + a warning-toned callout containing the words "No refusal anomaly", |
| + because one flag carried two unrelated states; and a warning on every |
| + row of every freshly-woken instance, because "not polled yet" is the |
| + ordinary state of a wake. Neither changes a string a test looks for. |
| + **If you add a section to a page, add it to drive too** โ the section |
| + that is not seeded is the section nobody sees. |
| **Auth** |
| - `requireSession` **fails closed**. The test seam is `AppForTest`, named |
diff --git a/internal/memoria/breaker.go b/internal/memoria/breaker.go| index 946ee5c..9ba151b 100644 |
| --- a/internal/memoria/breaker.go |
| +++ b/internal/memoria/breaker.go |
| @@ -461,3 +461,28 @@ func (s *Store) recordForBreaker(ctx context.Context, asker suite.Subject, cands |
| } |
| } |
| } |
| + |
| +// SeedTallyForDrive writes a counter directly, for tools/drive only. |
| +// |
| +// **A seam, named so it cannot be reached by accident**, and held by the |
| +// same `cmd/` gate as AppForTest: it writes into the past, which is |
| +// exactly what a production caller must never do โ the breaker's whole |
| +// claim is that the counters record what actually happened when it |
| +// happened. |
| +// |
| +// drive needs it because the alternative is seeding a week of real S2 |
| +// traffic against three fake siblings to look at one page. |
| +func (s *Store) SeedTallyForDrive(ctx context.Context, app, who string, when time.Time, visible, notVisible int) { |
| + answers := make([]suite.Authorization, 0, visible+notVisible) |
| + for i := 0; i < visible; i++ { |
| + answers = append(answers, suite.VISIBLE) |
| + } |
| + for i := 0; i < notVisible; i++ { |
| + answers = append(answers, suite.NOT_VISIBLE) |
| + } |
| + if len(answers) == 0 { |
| + return |
| + } |
| + _ = s.recordAuthorizationsAt(ctx, app, |
| + suite.Subject{Kind: suite.SubjectAddress, Value: who}, answers, when) |
| +} |
diff --git a/internal/memoria/breaker_test.go b/internal/memoria/breaker_test.go| index de03387..cfb44b5 100644 |
| --- a/internal/memoria/breaker_test.go |
| +++ b/internal/memoria/breaker_test.go |
| @@ -506,7 +506,7 @@ func TestTheBreakerNoticeReachesTheOwnerAndNoReader(t *testing.T) { |
| t.Fatalf("the fixture did not fire: %v โ nothing below is about "+ |
| "placement if there is no notice to place", b.Verdict) |
| } |
| - sentence, alarming := breakerSentence(b, false) |
| + sentence, alarming := breakerSentence(b) |
| // It names the anomaly rather than gesturing at it: an owner has to be |
| // able to act on this without opening a log. |
| @@ -559,7 +559,7 @@ func TestCannotDetermineSaysWhichHalfIsMissing(t *testing.T) { |
| } { |
| got, alarming := breakerSentence(Breaker{ |
| App: "Sheets", Verdict: BreakerCannotDetermine, Lacking: c.lacking, |
| - }, false) |
| + }) |
| for _, want := range c.want { |
| if !strings.Contains(got, want) { |
| t.Errorf("lacking %v produced %q, want it to contain %q", |
| @@ -580,3 +580,56 @@ func TestCannotDetermineSaysWhichHalfIsMissing(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// A row with nothing wrong carries no alarm, and the two lines are toned |
| +// independently. |
| +// |
| +// **This exists because looking at the rendered page found two bugs the |
| +// whole Go suite passed over**, on markup every template test agreed was |
| +// correct: |
| +// |
| +// 1. one `Alarming` flag carried both lines, so a sibling that had merely |
| +// not been polled yet rendered "No refusal anomaly." inside a warning |
| +// callout โ a warning box with reassurance in it; |
| +// 2. "not polled yet" was itself alarming, and memoria hibernates, so |
| +// every row of every freshly-woken instance warned about the ordinary |
| +// case. ยง8.1's argument about a breaker that fires for ordinary people, |
| +// arriving in the freshness line instead. |
| +// |
| +// Both are about what a person sees and neither changes a string a test |
| +// was checking for. What this pins is the property underneath: the alarm |
| +// is reserved for something actually being wrong. |
| +// |
| +// MUTATION-TESTED 2026-09-07, each verified red then reverted: |
| +// |
| +// breakerSentence takes an incoming flag again -> quiet-row case red |
| +// "not polled yet" sets CurrentAlarming -> quiet-row case red |
| +// a firing breaker returns false -> firing case red |
| +// a cannot-determine returns false -> cannot-determine red |
| +func TestOnlyAWrongThingIsAlarming(t *testing.T) { |
| + for _, c := range []struct { |
| + name string |
| + b Breaker |
| + alarm bool |
| + }{ |
| + {"quiet", Breaker{App: "Docs", Verdict: BreakerQuiet}, false}, |
| + {"firing", Breaker{App: "Sheets", Verdict: BreakerFiring, Askers: 3}, true}, |
| + {"cannot determine", Breaker{App: "Cal", Verdict: BreakerCannotDetermine, |
| + Lacking: []string{"the baseline"}}, true}, |
| + } { |
| + _, alarm := breakerSentence(c.b) |
| + if alarm != c.alarm { |
| + t.Errorf("%s: alarming = %v, want %v", c.name, alarm, c.alarm) |
| + } |
| + } |
| + |
| + // And the two lines cannot infect each other: a quiet breaker stays |
| + // quiet whatever the freshness line is doing. Asserted by the |
| + // signature โ breakerSentence takes no incoming flag โ and here by |
| + // the value, because a signature is not a test. |
| + if _, alarm := breakerSentence(Breaker{App: "Docs", Verdict: BreakerQuiet}); alarm { |
| + t.Error("a quiet breaker reported an alarm; the first version " + |
| + "inherited one from the freshness line and rendered " + |
| + "\"No refusal anomaly.\" in a warning box") |
| + } |
| +} |
diff --git a/internal/memoria/handlers_sources.go b/internal/memoria/handlers_sources.go| index 4fbb41b..d1205a8 100644 |
| --- a/internal/memoria/handlers_sources.go |
| +++ b/internal/memoria/handlers_sources.go |
| @@ -18,13 +18,22 @@ type sourceRow struct { |
| // says it is not known. ยง8.1: a failed or stale S1 states what the |
| // corpus is current as of rather than implying freshness. |
| Current string |
| + // CurrentAlarming and BreakerAlarming are **separate on purpose**, and |
| + // the first version had one flag for both. |
| + // |
| + // Looking at the rendered page is what caught it: a sibling that had |
| + // simply not been polled yet set the flag, and the breaker sentence โ |
| + // "No refusal anomaly." โ was then rendered inside a warning callout. |
| + // A warning box with reassurance in it is the notice a reader learns |
| + // to skip, which is the failure ยง8.1 spends a paragraph avoiding, |
| + // arriving through the markup rather than the predicate. |
| + // |
| + // Two lines, two states, toned independently. A template test would |
| + // have proved both strings were present and told me nothing. |
| + CurrentAlarming bool |
| // Breaker is the refusal breaker's sentence. |
| - Breaker string |
| - // Alarming marks the row for the template so a firing breaker and a |
| - // cannot-determine read differently from a quiet one. Two states, not |
| - // three: "I cannot tell" and "something is wrong" both want the |
| - // owner's attention, and only "nothing is wrong" does not. |
| - Alarming bool |
| + Breaker string |
| + BreakerAlarming bool |
| } |
| // siblingRows builds the sibling section of the owner's sources page. |
| @@ -58,10 +67,21 @@ func (a *app) siblingRows(r *http.Request) []sourceRow { |
| // that fails is not a count of zero, and the same rule governs |
| // a timestamp that cannot be read. |
| row.Current = "Could not read when this source was last checked." |
| - row.Alarming = true |
| + row.CurrentAlarming = true |
| case at.IsZero(): |
| - row.Current = "Not yet checked since this instance started." |
| - row.Alarming = true |
| + // **Not alarming, and that is the second thing looking at the |
| + // page taught.** memoria hibernates, so "not polled yet" is |
| + // the ordinary state of every wake's first seconds โ the |
| + // warm-up is running while this renders. A warning on every |
| + // row of every freshly-woken instance is a notice owners |
| + // learn to skip, which is ยง8.1's argument about a breaker |
| + // firing for ordinary people, arriving in the freshness line |
| + // instead. |
| + // |
| + // A warm-up that keeps failing is not invisible for it: the |
| + // breaker below says cannot-determine, and every search from |
| + // that sibling carries its own withholding notice. |
| + row.Current = "Not checked yet since this instance woke." |
| default: |
| row.Current = "Current as of " + at.Format("2 January 2006, 15:04 UTC") + "." |
| } |
| @@ -69,9 +89,9 @@ func (a *app) siblingRows(r *http.Request) []sourceRow { |
| b, err := a.store.RefusalBreaker(r.Context(), app.Name, now) |
| if err != nil { |
| row.Breaker = "Could not read this source's refusal history." |
| - row.Alarming = true |
| + row.BreakerAlarming = true |
| } else { |
| - row.Breaker, row.Alarming = breakerSentence(b, row.Alarming) |
| + row.Breaker, row.BreakerAlarming = breakerSentence(b) |
| } |
| out = append(out, row) |
| } |
| @@ -84,7 +104,10 @@ func (a *app) siblingRows(r *http.Request) []sourceRow { |
| // is missing**, because ยง10 gate 8 requires it and asserts the string: an |
| // owner who is told only "cannot determine" cannot tell a fresh instance |
| // from a broken one. |
| -func breakerSentence(b Breaker, alarming bool) (string, bool) { |
| +// It takes no incoming flag. The first version threaded one through, so |
| +// a quiet breaker inherited the alarm from an unrelated line and rendered |
| +// "No refusal anomaly." in a warning box. |
| +func breakerSentence(b Breaker) (string, bool) { |
| switch b.Verdict { |
| case BreakerFiring: |
| return fmt.Sprintf( |
| @@ -92,7 +115,7 @@ func breakerSentence(b Breaker, alarming bool) (string, bool) { |
| "%s last week were refused everything today. Nothing has been "+ |
| "deleted.", b.Askers, b.App), true |
| case BreakerQuiet: |
| - return "No refusal anomaly.", alarming |
| + return "No refusal anomaly.", false |
| default: |
| return "Cannot determine whether this source is refusing people: " + |
| joinWords(b.Lacking) + " missing. This is not the same as no anomaly.", true |
diff --git a/internal/memoria/templates/connectors.html b/internal/memoria/templates/connectors.html| index 9082028..7cfdf38 100644 |
| --- a/internal/memoria/templates/connectors.html |
| +++ b/internal/memoria/templates/connectors.html |
| @@ -26,12 +26,18 @@ |
| {{range .Siblings}} |
| <li rst-list-item> |
| <span rst-list-title>{{.App}}</span> |
| - <p rst-cell-mut>{{.Current}}</p> |
| - {{/* A toned callout only when something wants attention. A row that |
| - is fine says so in plain text; if every row carried a callout the |
| - tone would stop meaning anything, which is the same argument ยง8.1 |
| - makes about a breaker that fires for ordinary people. */}} |
| - {{if .Alarming}} |
| + {{/* Two lines, two states, toned separately. One flag for both put |
| + "No refusal anomaly." inside a warning callout for any sibling |
| + that simply had not been polled yet โ found by looking at the |
| + page, which is the only thing that would have. */}} |
| + {{if .CurrentAlarming}} |
| + <div rst-callout data-tone="warning"> |
| + <p rst-callout-body>{{.Current}}</p> |
| + </div> |
| + {{else}} |
| + <p rst-cell-mut>{{.Current}}</p> |
| + {{end}} |
| + {{if .BreakerAlarming}} |
| <div rst-callout data-tone="warning"> |
| <p rst-callout-body>{{.Breaker}}</p> |
| </div> |
diff --git a/internal/memoriatest/gates_test.go b/internal/memoriatest/gates_test.go| index f3a1499..2801e3e 100644 |
| --- a/internal/memoriatest/gates_test.go |
| +++ b/internal/memoriatest/gates_test.go |
| @@ -443,6 +443,10 @@ func TestProductionDoesNotUseTheTestConstructor(t *testing.T) { |
| // gate that greps for AppForTest alone. Found by a code review. |
| for _, seam := range []string{ |
| "AppForTest", "AsPerson", "UseFakeSource", "WaitForSync", |
| + // SeedTallyForDrive writes breaker counters into the past. |
| + // The breaker's whole claim is that they record what |
| + // happened when it happened. |
| + "SeedTallyForDrive", |
| // sibling.NewForTest builds a plain http.Client with no team |
| // binding. In production that is ยง6.4's cross-team refusal |
| // removed โ the guard that runs before the transport dials โ |
diff --git a/tools/drive/main.go b/tools/drive/main.go| index 6697401..0bb1d58 100644 |
| --- a/tools/drive/main.go |
| +++ b/tools/drive/main.go |
| @@ -31,6 +31,7 @@ import ( |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| "amadan.net/oficina/memoria/internal/memoria/notion" |
| "amadan.net/oficina/memoria/internal/memoria/seal" |
| + "amadan.net/oficina/memoria/internal/memoria/sibling" |
| ) |
| func main() { |
| @@ -84,6 +85,7 @@ func main() { |
| // exists; it cannot show that a status page reads sensibly with real |
| // numbers in it. |
| seedConnector(app) |
| + seedSiblings(app) |
| fmt.Printf("drive serving on http://%s (no sign-in)\n", *addr) |
| if err := http.ListenAndServe(*addr, mux); err != nil { |
| @@ -163,3 +165,57 @@ func seedConnector(app interface { |
| memoria.NewSyncer(app.Store(), slog.New(slog.NewTextHandler(os.Stderr, |
| &slog.HandlerOptions{Level: slog.LevelWarn}))).RunWith(ctx, src, c.ID, run.ID) |
| } |
| + |
| +// seedSiblings puts three suite apps on the Sources page in the three |
| +// states the breaker can be in, so the section can actually be looked at. |
| +// |
| +// **It exists because the section was built and shipped without anybody |
| +// seeing it.** drive had no roster, so `/connectors` rendered the |
| +// connector list and nothing else โ the markup was tested, the page was |
| +// not, which is the exact gap AGENTS.md says drive is for. |
| +// |
| +// The three states are seeded rather than one, because they differ in |
| +// tone and in length and those are what a person has to be able to tell |
| +// apart at a glance: a firing breaker is a sentence with a number in it, |
| +// a cannot-determine is a longer sentence that must not read as |
| +// reassurance, and a quiet one is a short line that must not shout. |
| +func seedSiblings(app interface { |
| + UseSiblings(memoria.Custody) |
| + Store() *memoria.Store |
| +}) { |
| + ctx := context.Background() |
| + apps := []sibling.Sibling{ |
| + {Name: "Docs", BaseURL: "https://drive.docs.oficina.works", Token: "drive"}, |
| + {Name: "Sheets", BaseURL: "https://drive.sheets.oficina.works", Token: "drive"}, |
| + {Name: "Calendar", BaseURL: "https://drive.calendar.oficina.works", Token: "drive"}, |
| + } |
| + custody, err := memoria.NewSiblings("drive", nil, apps) |
| + if err != nil { |
| + fmt.Fprintln(os.Stderr, "seed siblings:", err) |
| + return |
| + } |
| + app.UseSiblings(custody) |
| + |
| + store := app.Store() |
| + now := time.Now().UTC() |
| + |
| + // Docs: quiet, and polled a moment ago. |
| + if err := store.StartCounting(ctx, "Docs", now.AddDate(0, 0, -30)); err != nil { |
| + fmt.Fprintln(os.Stderr, "seed siblings:", err) |
| + } |
| + |
| + // Sheets: firing. Three people who could read last week, refused today. |
| + if err := store.StartCounting(ctx, "Sheets", now.AddDate(0, 0, -30)); err != nil { |
| + fmt.Fprintln(os.Stderr, "seed siblings:", err) |
| + } |
| + for _, who := range []string{"ana@example.com", "ben@example.com", "cleo@example.com"} { |
| + store.SeedTallyForDrive(ctx, "Sheets", who, now.AddDate(0, 0, -3), 5, 0) |
| + store.SeedTallyForDrive(ctx, "Sheets", who, now, 0, 4) |
| + } |
| + |
| + // Calendar: cannot determine โ configured two days ago, so the |
| + // baseline is short. This is the row that must not read as "fine". |
| + if err := store.StartCounting(ctx, "Calendar", now.AddDate(0, 0, -2)); err != nil { |
| + fmt.Fprintln(os.Stderr, "seed siblings:", err) |
| + } |
| +} |