Skip to contentWolf-Rayet

Decision records

ADR-0018 Length ceilings are bootstrapped from real shipped copy, not chosen freehand

Superseded by ADR-00192026-08-31Phase 5

#Context

Playbook §6 names "string length ceilings per intensity level" as one of the content linter's mechanical checks, alongside sentence case, banned words and the rest — and, unlike those, it names no value. A ceiling needs a number per level before it can be a rule, and until this unit nothing in this repository measured what a real string at each level actually looks like: the six components that carried tone metadata before this phase each had exactly one real call site in their own Storybook story, and content-lint's real-copy scan (tools/scan-real-copy.js) had never been pointed at all of them at once. StatusIndicator's own retrofit onto the shared tone mechanism, earlier in this same unit, was what made the set complete enough to measure — the scan now covers all seven components' one real string apiece (three for Alert, two for InputField), the first time this system has had a real corpus to measure rather than a synthetic fixture.

A ceiling with an invented number is worse than no ceiling. banned-words.txt already carries a placeholder rather than guessed real words, for the same reason: a number typed from memory would look exactly like a considered limit while being nothing of the kind, and nobody reviewing a pull request could tell the difference.

#Decision

Each level's ceiling is the longest real string the real-copy scan measured shipping at that level, rounded up to the next multiple of ten. Computed directly from the scan's own output, not retyped from memory:

LevelLongest real string measuredLengthCeiling
1— none shipped—no ceiling
2"Batch 4471 · label mismatch" (QueueItem)2730
3"Enter a complete email address." (InputField)3140
4"Clear the jam at the seal press, then release the line." (Alert)5560

Level 1 carries no real string in the scan today — no component's one real story call renders its copy at level 1 — and gets no ceiling rather than an invented one. A rule that enforced a guessed number at level 1 would be indistinguishable, in a diff, from one enforcing a measured one; leaving the gap open is what keeps that distinction visible.

Implemented as @wolf-rayet/content-lint's eighth rule, length-ceiling, reading packages/content-lint/data/length-ceilings.json — a data file, the same discipline banned-words.js already applies to its own external list — rather than a number embedded in the rule's own source. A level absent from that file is a level this rule does not check, not a level checked against Infinity.

#Rejected options

Arbitrary round numbers with no basis in real content — pick something that sounds reasonable per level (say, 20/40/60/80) and ship it. What it had going for it: available immediately, no scan required, and round numbers are easy to remember. Rejected because "sounds reasonable" is exactly the natural-language judgment §6 forbids this linter from making, applied to a number instead of a word — a ceiling is supposed to be the mechanical form of a design decision, and a decision nobody made is not mechanical, it is a guess wearing a rule's clothing.

Borrow a word- or character-count convention from an outside style guide — several content-design style guides publish length guidance for buttons, toasts, or notification text. What it had going for it: real research behind the numbers, and no scan needed. Rejected because those conventions were built for their own systems' channels and audiences, not for a five-level intensity ladder where the same channel (a plain sentence) carries four different allocations of attention — a convention tuned for "a push notification" has no rung that means "Directed, one level below the region maximum," and translating one taxonomy onto the other would be inventing the mapping, not borrowing the number.

One universal ceiling for every level — pick the single longest string across all levels (55, from this scan) and apply it everywhere. What it had going for it: one number, trivial to implement, never wrong for lack of per-level data. Rejected because it throws away the one thing a *per-level* ceiling is for: catching a level-1 string that has quietly grown as long as a level-4 one. §2 keys tone to the ladder precisely so a quiet level and a loud one cannot say the same amount at the same length: a single ceiling would let a resting label spend a paragraph and still pass, while a Level 4 Alert's genuinely short imperative underspends the same shared limit. Per-level ceilings are the mechanical expression of "quiet levels stay short," and a universal ceiling has no way to express it.

#Consequences

This is a bootstrap value, not a final one. It is the longest string *currently shipping*, not the longest string a level should ever be allowed to carry — a real future string that is legitimately longer and still reads as Directed rather than Demanded will fail this rule, correctly surfacing a question ("has this level's real ceiling changed, or is this string too long") that nobody has had to answer before because nothing measured it. Answering that question is what raises a ceiling; it is not raised by editing the JSON file freehand. A superseding ADR is expected once enough new real copy exists to remeasure — this file's own header comment says so, so the gap between "current" and "final" cannot be missed by reading the code alone.

Level 1 is left without a ceiling, reported by both the rule (silently, as a level it does not check) and this record (explicitly): no component's real story call renders copy at level 1 today. The gap closes the same way the other three numbers were filled — a real Level 1 string enters the scan, and the ceiling is computed from it — not by assigning it the nearest neighbor's number or an arbitrary floor.

Enforcement: adds a length-ceiling rule to @wolf-rayet/content-lint, the eighth and last of playbook §6's named mechanical checks now built. Touches no other check's object or verdict; the real-copy scan's own zero violations were unaffected by this rule's addition, since every ceiling was derived from the same strings it now checks.