Skip to content

Commit be4303c

Browse files
mpstatonclaude
andcommitted
changelog: catch the log up on the design-system floor and the NATS outage
Two entries the log was missing. 2026-08-06_01 was written mid-flow and four more things shipped the same day after it; the NATS fix shipped 08-08 with no entry at all. 2026-08-06_02 leads with gate A20, because it is the one a reader outside the team can learn something from. Seventeen micro-frontends inherit design tokens from the shell at runtime, and nobody had asked what happens to a member that deploys before the shell catches up. The answer was the worst available one: not an error, not a fallback, but black text on a transparent background, reported as success, and invisible locally because the developer's machine always has the latest theme. The entry shows the browser measurement including the counterfactual, because "the degraded case looked fine" proves nothing on its own. Carries the three smaller items behind it — React evicted from the one config line that was its entire footprint, the shell's typecheck passing for the first time, and five services that had all named a function registerHandlers. 2026-08-08_01 is the debugging story. It leads with the eliminations rather than the fix, since seven wrong answers are the part a reader can reuse, and it owns the one that was our own instrument-misreading rather than quietly dropping it. The two broker counters that collapsed the search space are the centrepiece. Both follow the audience cascade — ledes kept to subtitle length with the context moved into Why Care?, marketing voice up top dissolving into engineering detail further down, and a What's Next? that states plainly what did NOT get fixed: 112 instances of the same idiom remain, and the shared-package fix for them is gated on a Dockerfile change. Files changed: - changelog/2026-08-06_02_Federated-Tokens-Get-A-Floor-And-React-Finally-Leaves-The-Building.md (new) - changelog/2026-08-08_01_The-Corpora-Were-Never-Missing-One-Bad-Message-Had-Killed-The-Subject.md (new) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019a8tSPbFdvF1pKtADnWyDg
1 parent 2aa4e62 commit be4303c

2 files changed

Lines changed: 262 additions & 0 deletions
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
---
2+
date_created: 2026-08-06
3+
date_modified: 2026-08-08
4+
title: "Federated tokens get a floor — and React finally leaves the building"
5+
lede: "Ask a hard question about a micro-frontend architecture and you sometimes learn the answer is 'it renders invisible text and tells you everything is fine.' We measured that failure in a browser, then gave every design token a floor so it can't happen."
6+
publish: true
7+
authors:
8+
- Michael Staton
9+
augmented_with:
10+
- Claude Code on Claude Opus 5
11+
files_changed:
12+
- packages/theme/token-baseline.css
13+
- packages/federation/src/index.ts
14+
- scripts/generate-token-baseline.mjs
15+
- tsconfig.json
16+
- tsconfig.base.json
17+
- shell/src/css.d.ts
18+
- shell/src/App.svelte
19+
- services/record-surrealdb-resolver/src/handlers.ts
20+
- services/row-store/src/handlers.ts
21+
tags:
22+
- Augment-It
23+
- Design-System
24+
- Module-Federation
25+
- Design-Tokens
26+
- Accessibility
27+
- Refactor
28+
---
29+
30+
# Federated tokens get a floor
31+
32+
## Why Care?
33+
34+
augment-it is seventeen micro-frontends that load into one shell at runtime, each built and deployed on its own schedule. That independence is the whole point — but it raises a question we hadn't answered: **the shell owns the design tokens, so what happens to a micro-frontend that ships before the shell catches up?**
35+
36+
The honest answer turned out to be *the worst possible one*. Not an error. Not a fallback. The component renders **black text on a transparent background** and reports success. In production. And you can't see it locally, because your machine has the latest theme.
37+
38+
That's now impossible. Every design token has a floor underneath it.
39+
40+
## What's New?
41+
42+
- **A20 closed** — every federated token is registered with `@property` and an initial value, so a missing or stale shell degrades to a legible dark surface instead of nothing
43+
- **`token-baseline.css` is generated, not hand-written**`pnpm tokens:baseline` derives it from `theme.css`; `pnpm tokens:check` fails CI if they drift
44+
- **Federated bundles got 3KB smaller** — they carry a 24-token floor instead of the whole theme
45+
- **React is gone** — one config line was its entire footprint, in a codebase that bans it
46+
- **The shell's typecheck passes for the first time**
47+
- **Five services stopped sharing a function name**`registerHandlers` was ambiguous five ways
48+
49+
## The question that started it
50+
51+
> *"How do we make sure the microfrontends inherit the global federated classes so there's no failure when one thing breaks somewhere?"*
52+
53+
Good question. CSS custom properties inherit down the document, so a member picks up whatever the shell declares. Fine when everyone's current. But token **removal** and token **introduction** are not symmetric:
54+
55+
| Change | Old member, new shell | New member, old shell |
56+
|---|---|---|
57+
| **Remove** a token | Alias it forever. Fine. | n/a |
58+
| **Add** a token | n/a | **Resolves to nothing.** |
59+
60+
"Nothing" is not a metaphor. An undeclared custom property is *invalid at computed-value time*`color` falls back to inherited, `background-color` to transparent.
61+
62+
## We measured it rather than argued about it
63+
64+
Rather than reason from the spec, we drove the real built stylesheets through a browser in three configurations:
65+
66+
```
67+
healthy (shell theme + member bundle)
68+
tokens resolve to the shell's values · all three modes distinct ✅
69+
70+
degraded (member bundle ONLY — a stale shell)
71+
#e8eaf0 on #13151b · contrast 15.17:1 · neither transparent ✅
72+
73+
counterfactual (a token neither declared nor registered)
74+
rgb(0,0,0) on rgba(0,0,0,0) ← black text, transparent background
75+
```
76+
77+
That third line is the bug, reproduced. It's also why we ran it: without the counterfactual, "the degraded case looked fine" proves nothing — it might have worked for unrelated reasons.
78+
79+
## How the floor works
80+
81+
CSS has had the answer since `@property` shipped. Registering a custom property gives it a **typed initial value** that applies when nothing declares it:
82+
83+
```css
84+
@property --color-surface {
85+
syntax: '<color>';
86+
inherits: true;
87+
initial-value: #13151b;
88+
}
89+
```
90+
91+
Now `var(--color-surface)` can never resolve to nothing. Worst case it's the floor. The shell's real declarations still win whenever they exist — so this costs exactly nothing when the stack is healthy, and saves you when it isn't.
92+
93+
The shell stays the canonical source. Members carry only the floor. Standalone entries still load the full theme, because they have no shell to inherit from.
94+
95+
### Generated, because a hand-maintained copy would drift by Thursday
96+
97+
```bash
98+
pnpm tokens:baseline # regenerate from theme.css
99+
pnpm tokens:check # CI: fail if stale
100+
```
101+
102+
The generator flattens each token through its Tier-1 reference to a literal — `initial-value` forbids `var()` — and picks `<color>` or the universal syntax per token. That last part matters: registering a box-shadow as `<color>` invalidates the whole rule and CSS drops it *silently*. Zero dependencies, pure Node.
103+
104+
## Under the hood: three smaller things
105+
106+
**React was one line.** The root `tsconfig.json` set `"jsx": "react-jsx"` — the only React reference anywhere in a repo where React is a hard prohibition, with zero `.tsx` files and no `react` dependency. Scaffold vestige that survived because nothing extends that file, so nobody ever read it. Now `"jsx": "preserve"`: TSX stays legal, no runtime is bound.
107+
108+
**The shell's typecheck had never passed.** Two errors, and the interesting part is *why nobody fixed them*: the shell needed a `css.d.ts` shim, but its tsconfig omitted `src/**/*.d.ts` from `include` — so adding the shim wouldn't have helped. Two halves of one bug. Fixing it also revealed the shell as the **eighteenth** copy of a tsconfig we'd converged earlier that day.
109+
110+
**Five services, one function name.** `registerHandlers` existed five times, once per NATS service, and three of them ranked in the codebase graph's top ten by connectivity — the most-connected symbols in the system were also the least searchable. Now service-qualified.
111+
112+
That last one came with a lesson: our refactor doc said there were *three*, because three was what the graph's top-ten showed. God-node rankings report the top of a distribution, not a census.
113+
114+
## Verified
115+
116+
- 19 packages build · **1,587 files typecheck clean** (up from 1,494 with the shell failing outright) · all suites pass
117+
- `record-collector`'s built CSS is **byte-identical** before and after the federation change — same content hash
118+
119+
## What's Next?
120+
121+
The floor covers the 24 tokens that exist today. Members can still invent tokens *after* a baseline is generated, so the 939 `var()` calls with no inline fallback are worth sweeping — belt and braces.
122+
123+
And the bigger one: 1,386 of 1,392 CSS selectors are unique to a single app. There's almost nothing to deduplicate; there are seventeen independently invented vocabularies. The component library is greenfield extraction, not consolidation.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
---
2+
date_created: 2026-08-08
3+
date_modified: 2026-08-08
4+
title: "The corpora were never missing — one bad message had killed the subject"
5+
lede: "A client's research corpora vanished from the workspace. The data was fine, the database was fine, the service was healthy and logging nothing. Two counters found it: a single unanswerable message had taken the whole request channel down until restart."
6+
publish: true
7+
authors:
8+
- Michael Staton
9+
augmented_with:
10+
- Claude Code on Claude Opus 5
11+
files_changed:
12+
- services/record-surrealdb-resolver/src/nats-loop.ts
13+
- services/record-surrealdb-resolver/test/nats-loop.test.ts
14+
- services/record-surrealdb-resolver/src/surreal.ts
15+
- services/record-surrealdb-resolver/src/domains.ts
16+
- context-v/issues/One-Stuck-Message-Kills-A-NATS-Subject-Until-Restart.md
17+
tags:
18+
- Augment-It
19+
- NATS
20+
- Resilience
21+
- Debugging
22+
- Error-Handling
23+
- Microservices
24+
---
25+
26+
# The corpora were never missing
27+
28+
## Why Care?
29+
30+
A workspace opened and its research corpora weren't there. Nine of them — months of collected reading on adult literacy, workforce development, rural income — gone from the surface. The console said:
31+
32+
```
33+
❯ domain.list: timeout
34+
```
35+
36+
The obvious readings are all wrong. The data hadn't been lost. The database was reachable in about a second. The service was running, healthy, `RestartCount=0`, and writing **nothing at all** to its logs.
37+
38+
This is the failure mode that should scare you most in a message-driven system: **silent, total for one channel, and invisible everywhere you'd normally look.** Every dashboard is green. Only the replies stop.
39+
40+
It's fixed, it has regression tests, and the diagnosis is written down so the next person doesn't re-walk it.
41+
42+
## What's New?
43+
44+
- **`domain.list` answers again** — reach-edu's 9 corpora, humain-vc's 7, 16 unfiltered
45+
- **A hardened consumer** (`nats-loop.ts`) that can't be killed by one bad message
46+
- **A bounded database handshake** — the unbounded one is what parked the whole channel
47+
- **9 regression tests** for failure modes the old suite structurally could not reach
48+
- **A written diagnosis** listing the seven causes we ruled out, so nobody re-walks them
49+
50+
## The seven things it wasn't
51+
52+
Most of debugging is elimination, and the eliminations are the useful part:
53+
54+
| Suspected | Verdict |
55+
|---|---|
56+
| The client's data was lost | **No.** All 9 in the database |
57+
| Something client-specific | **No.** Every client timed out, including no filter |
58+
| A rename we'd shipped that week | **No.** Different file, untouched |
59+
| Service crashed | **No.** Up, zero restarts, clean boot |
60+
| Database unreachable | **No.** Connect → sign in → query in **1,107ms** |
61+
| A full-table migration on cold start | **No.** Measured **538ms** |
62+
| Nothing subscribed to the channel | **No** — and this one was *our* mistake |
63+
64+
That last row is worth owning. We read the message broker's monitoring output, saw an empty `subs_detail` array, and concluded nothing was listening. Wrong key. The real one is `subscriptions_list_detail`, and the subscription was there all along. A misread instrument sent us down a blind alley for a while.
65+
66+
## The two numbers that found it
67+
68+
The broker tracks messages in and out per connection. Take a reading, send one request, take another:
69+
70+
```
71+
BEFORE: in_msgs=4 out_msgs=15
72+
PROBE: timeout
73+
AFTER: in_msgs=4 out_msgs=16
74+
```
75+
76+
`out_msgs` went up — the broker **delivered** the message. `in_msgs` didn't — the service **replied with nothing**.
77+
78+
So the message arrived, was consumed, and vanished. That single observation collapses the search space: it isn't networking, it isn't the database, it isn't the data. It's the code between receiving a message and answering it.
79+
80+
## What was actually wrong
81+
82+
Every handler in the service was registered like this:
83+
84+
```ts
85+
void (async () => {
86+
const sub = nc.subscribe(subject);
87+
for await (const msg of sub) {
88+
const args = msg.json(); // ← outside the try
89+
try {
90+
const db = await getDb(); // ← no timeout
91+
...
92+
} catch { respond({ ok: false }) }
93+
}
94+
})();
95+
```
96+
97+
Three defects that individually look survivable and together are fatal:
98+
99+
**`for await` is strictly sequential.** It processes one message completely before pulling the next. So one call that never settles doesn't slow the channel — it *stops* it. Forever. Later messages get delivered and dropped.
100+
101+
**The database handshake had no deadline.** `connect()` over a WebSocket can stall indefinitely. When it did, the connection was never cached, and the loop parked on it with nothing to retry.
102+
103+
**The JSON parse sat outside the `try`.** One malformed payload throws out of the loop entirely — and because the loop is a bare `void (async () => {})()` with no `.catch()`, that death is an unhandled rejection nobody sees. The subscription stays registered, so monitoring keeps reporting the channel as healthy while nothing consumes it.
104+
105+
## The fix, and one deliberate non-fix
106+
107+
`nats-loop.ts` now owns the parse, a per-message deadline, the always-answer guarantee, and the loop's own death.
108+
109+
It takes any `AsyncIterable` of reply-shaped messages — which is the whole trick. **The original bug was unreachable from our existing test suite** because every test went through a real database and none exercised the loop. Making the loop take a plain async iterable made all three failure modes testable with no broker and no database.
110+
111+
We **kept it sequential on purpose.** Processing messages concurrently would also fix head-of-line blocking, but it would reorder writes on channels like `domain.create` — a real semantic change this bug doesn't require. Bounded-sequential turns "dead forever" into "one slow message," which is the actual defect.
112+
113+
## Under the hood: proof it's fixed
114+
115+
Container rebuilt, then probed live:
116+
117+
```
118+
domain.list reach-edu 1857ms ok=true domains=9
119+
upward-mobility, grant-prospecting-tools, future-of-work,
120+
workforce-development, frontier-job-demand, agent-workflow-maxxing,
121+
adult-literacy-numeracy, ncad-forge, rural-income-boosts
122+
domain.list humain-vc 111ms ok=true domains=7
123+
domain.list (no filter) 161ms ok=true domains=16
124+
125+
malformed payload 3ms ok=false "not json{{" is not valid JSON
126+
domain.list reach-edu (after) 112ms ok=true domains=9 ← SUBJECT SURVIVED
127+
```
128+
129+
The last two lines are the regression. Under the old code that malformed payload would have ended the channel for the rest of the process's life. It now answers in 3ms and the channel keeps serving.
130+
131+
**87 tests across 7 suites**, all passing.
132+
133+
## What's Next?
134+
135+
We fixed one file. The same idiom appears **112 times** across ten services — including three more files in the very service we just fixed, which serve the organization, affiliation and person channels.
136+
137+
One thing does protect them today: the bounded database handshake sits in front of every one of those loops, so the *hang* is closed service-wide. What's still exposed is the malformed-payload path.
138+
139+
The rollout is tracked, and it has a prerequisite worth naming: our services build from their own directory with plain `npm install` and **cannot see the shared packages directory at all.** So "put the helper in a shared package" isn't free — it's a Dockerfile change first. That's the same seam we hit earlier this week when consolidating duplicated utilities, which is a good sign it's the real constraint and not a one-off.

0 commit comments

Comments
 (0)