Skip to content

feat(cards): a refusal says refused; a crash still says failed - #13

Closed
lucy-wolfe wants to merge 2 commits into
tribes-protocol:mainfrom
lucy-wolfe:feat/refusal-verb
Closed

feat(cards): a refusal says refused; a crash still says failed#13
lucy-wolfe wants to merge 2 commits into
tribes-protocol:mainfrom
lucy-wolfe:feat/refusal-verb

Conversation

@lucy-wolfe

Copy link
Copy Markdown
Contributor

A card headline for a caller refusal now reads refused; a genuine system fault keeps failed.

Hiring teammate refused · The root department id is 'executive' — 'acme-capital' names the company…
Hiring teammate failed  · chiefd returned an invalid outcome  (system fault)

Why the word matters

It is a claim about whose fault the failure was, and the two invite opposite recoveries. "Refused" invites a corrected call. "Failed" invites a retry. Using either for the other sends the reader to the wrong action — and calling a crash a refusal is the worse direction, because it sends somebody to fix a call that was never wrong.

This is only implementable because the classification already exists to follow. The verb reads that, rather than matching on message text, which this file explicitly bans.

The split follows the classification, not a list

One predicate, in one place. That matters more than it looks: three renderers build a failure title, and two of them are bespoke ones for the bench/recall and start/stop batches. Those would have gone on saying "failed" for a refusal, because they never reach the default path.

They were found by sweeping for the word rather than trusting the default renderer to be the only producer — the same both-arms property that has cost this codebase several rounds. A rule copied into three renderers is three rules waiting to disagree at the first bespoke card.

One case was not what the classification said it was

A partial hire carries status: "hire_partial" so the card can name the people already hired — a retry needs that, or it hires them twice and fails on the wrong person.

But that status is data a retry needs, not an assertion about fault, and the error it wraps may be either kind: a mid-batch caller refusal (an unknown department on person four), or a genuine crash. So "carries a status" and "is the caller's fault" are not the same property, and treating them as one would have labelled a real crash a refusal.

That path now asks the wrapped error's own type, exactly as every other site does. And a producer in the same position marks itself with fault: true rather than being enumerated in the renderer — a marker, not a list, because a list is where the next case is missed.

Pinned by a discriminating pair

Per the rule this codebase keeps re-learning: a test asserting only "a refusal says refused" is satisfiable by returning true for everything, which would relabel every crash a refusal and delete the distinction rather than use it.

  • a classified refusal is refused;
  • an unclassified failure is not;
  • a status carried as context with fault: true is not.

Verified by mutation: making the predicate return true unconditionally fails both negative cases.

The collapsed card

"Refused" is one character longer than "failed". The 120-character window applies to the summary tag rather than the title, and the title is not what gets truncated, so no card's truncation point moves. Checked rather than assumed, since it was asked.

Scope note

The (system fault) tag keeps the rule it was given previously: it is driven by the absence of a status. A hire_partial result that wraps a real crash therefore says "failed" but does not carry the tag. That is pre-existing behaviour and not changed here — flagged as an observation rather than fixed silently, because it is a separate decision about what the tag means.

Checks

bun run typecheck, test, lint, lint:reactive, knip, all 98 repository guards, and CI.

The word is a claim about whose fault the failure was, and the two invite
opposite recoveries: refused invites a corrected call, failed invites a retry.
Calling a crash a refusal is the worse direction, because it sends somebody to
fix a call that was never wrong.

The verb follows the classification rather than a list, so a refusal added next
month gets the right word without anyone remembering. One predicate in one
place: three renderers build a failure title, and two of them are bespoke
titles that would otherwise have kept saying failed for a refusal -- the
both-arms property again, found by sweeping for the word rather than trusting
the default path to be the only one.

One case was not what the classification said. A partial hire carries a status
so the card can name the people already hired, which a retry needs so it does
not double-hire them -- but the error it wraps may be a genuine crash. A status
carried for CONTEXT is not a claim about fault. That path now asks the wrapped
error's own type, and a producer in the same position marks itself rather than
being enumerated in the renderer, because a list is where the next case is
missed.

Pinned by a discriminating pair: a classified refusal is refused, an
unclassified failure is not, and a context-carried status with fault:true is
not. Verified by mutation -- making the predicate return true fails both
negative cases.

Checked the collapsed card: the verb is one character longer and the title is
not what gets truncated, so no card's summary window moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lucy-wolfe
lucy-wolfe requested a review from hishboy as a code owner August 28, 2026 15:57
The verb moved to the fault marker and the tag did not. The tag still measured
only the ABSENCE of a status, so a partial batch carrying one for context while
wrapping a real crash said "failed" -- correctly -- and then dropped the crash
marker. Somebody debugging that would see a list of people already hired, no
fault marker, and reasonably conclude they had passed bad input.

One classification, two surfaces. The tag now reads the same marker, and a test
asserts the two are complementary rather than merely each correct: nothing may
be both a refusal and a system fault, which fails if either rule moves without
the other. Verified by mutation -- putting the tag back on the old instrument
fails the crash case and only that case.

This was the scope note I flagged rather than fixed in the first pass. Review
ruled it fix-here, and that was right: shipping the verb on the new instrument
and the tag on the old one is a divergence nobody would find again except by
hitting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lucy-wolfe

Copy link
Copy Markdown
Contributor Author

Superseded by #16, which merges this branch together with the other two changes so main takes one commit and mints one release. The review that happened here still stands; nothing was rewritten on the way in.

@lucy-wolfe lucy-wolfe closed this Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant