Skip to content

docs(checklist): rule 22 — a join across two measurements needs a time-invariant predicate (reopen of #1240) - #1427

Merged
lilyshen0722 merged 13 commits into
mainfrom
docs/checklist-rule-22-time-invariant-predicate
Sep 1, 2026
Merged

lilyshen0722 merged 13 commits into
mainfrom
docs/checklist-rule-22-time-invariant-predicate

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Reopen of #1240, which auto-closed when its stacked base (#1237's branch) merged and was deleted. Branch is rebased; diff vs main is rule 22 only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TdEJoXUmbHmW5TFk7hfkbK

lilyshen0722 and others added 13 commits September 1, 2026 03:30
…cution

@sprint-review's #1149 comment listed the backfillPending absence check as
"defensible as a barrier" and stopped there, which is a verdict about one test
rather than a rule anyone can reuse.

The generalisation makes it decidable: a behavioural test can show a branch
produces the right answer and cannot show a branch is gone. Dead code never
runs, contributes to no assertion, and waits for the next reader to revive it.
So the question is not "is this test grepping?" but "is the property
behavioural or structural?"

Keeps the default suspicion of source assertions intact and carves out the one
class they are the only instrument for. Riders: label which kind the test is,
or a reviewer applying the default suspicion correctly deletes it; and give an
absence assertion a positive control, since a grep matching nothing because the
pattern is wrong looks exactly like one matching nothing because the code is
gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sprint-review's near-miss, running rule 17's closing check against the fix
rule 17 was written for. `agentRuntimeAuth.ts:98` is `.select('_id').lean()`
on an interleaved DM-pod `Pod.find`, thirty-seven lines below the
`User.findOne` — read as the User projection, it would have condemned
`req.agentUser?.username` as dead on arrival. Both `User.findOne` calls are
unprojected; the term is live.

Grepping for `.select(` near an assignment finds the wrong query whenever two
run in the same block, so the check needs the same discipline it imposes: read
the call the projection is chained to, not the nearest one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five sites on main independently choose the noisy degraded path and none
cite each other: the #1115 ledger-row ruling, ThreadUserState's
cutoffUnknown CASE arm, resolveBotUserIds' empty-map catch,
reRootOrphanedChains' never-fail-the-delete wrapper, and
BARE_RUNTIME_ARTIFACTS as an observed set. Each argues the principle
locally, which is why the repetition is invisible.

The rule carries an audience clause the bare form lacks. The attempts
cap in agentEventService reads as a counterexample and is the sharpest
confirming instance: the retire pass exists to make exhaustion visible,
transitions to status 'failed' and warns — and list() hardcodes
status 'pending', so the agent owed the event is never told. Direction
correct, audience operator-only. Only a question that asks who hears it
surfaces that residue.

Stacked on #1209 (rule 18); merge that first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o edit

@sprint-review verified at heads d02a68f/6ef8022f that this branch contains
right conclusion from it: merge this one, close #1209 as redundant.

Two commits I pushed to #1209 afterwards broke that, in the direction most
likely to go unnoticed. #1209 is now 17d7580, and the diff between the two
branches carries a deletion rather than being append-only: the sharpened
rider two (which half of "matches nothing" did you close) exists on #1209 and
not here. Acting on the verified-and-now-stale containment would have merged
this branch and silently dropped the fix @sprint-review themselves asked for.

Ports just that sentence, so the containment claim is true again and their
resolution stands unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sprint-review points out the exemplar this rule is built on has no positive
control, and is sound anyway. Both halves are true, and the reason is worth
being the rule rather than a footnote.

"Matches nothing" has two causes and they are closed by different things.
An empty haystack is closed for free by the loader: `read()` in
threadUserState.test.js is an unguarded `readFileSync`, so the source string
can never quietly be '' and every absence assertion in that suite is already
controlled against that half. A wrong needle is not closed by anything the
loader does — a typo'd identifier matches nothing against a file where the
code is in plain sight.

The free half is the one people notice, so citing it reads as having
controlled the assertion while the live risk is untouched. Rider now asks
which half was closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sprint-review verified the attempts-cap residue at 8a674ac and found it
is stronger than stated: the only reader of status 'failed' in any route
or controller is routes/admin/agentEvents.ts :75/:92, an admin surface,
and garbageCollect deletes those rows past failedThreshold at :717. The
receipt is not just out of the owed agent's reach — it expires.

Also bounds what the audience clause licenses. It is not "every guard
must notify its consumer"; it is "the answer must be stated", because
'we logged it' and 'the party owed the outcome was told' are different
claims and only one is usually true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… 168h

@sprint-review corrected the count they had scoped and I had inherited
unscoped. `status: 'failed'` on AgentEvent has three readers tree-wide:
routes/admin/agentEvents.ts twice, plus scripts/seat-output.ts, which
aggregates it into a deadLettered column. All operator-side, so the
conclusion is unchanged — but "two readers only" without the
routes+controllers qualifier is simply false, and a scoped count is one
careless quotation away from a false one. That quotation was mine.

Adds the number to the expiry: failedRetentionHours defaults to 168h, so
the receipt is gone in seven days.

And the detail worth keeping: seat-output's own comment reads "Nothing
else shows them". Two operator surfaces, each written as though it were
the only one — the same independence problem the rule is about, one layer
down from the guards it cites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sprint-review: seat-output.ts's own header is a better citation than my
paraphrase of the guard, because it argues the rule from inside the
incident. Two opposite outcomes — working, and producing nothing —
rendered identically in the only reachable surface, while the state that
distinguished them sat on AgentEvent the whole time. 19 hours of a
confidently wrong answer out of a system that already held the right one.

The part that earns it a place in the checklist is where the fix landed.
Someone hit the audience gap, felt it, and built a surface for the
OPERATOR — a third operator-side reader beside two that existed. The agent
half is untouched: the party whose liveness is being adjudicated still
cannot see any of it, and a dead-lettered event still expires unmentioned
at 168h.

Not a criticism of the script, which was the urgent half and is good.
Feeling the audience gap acutely is not the same as closing it, and the
reflex is to build another surface for whoever is holding the question at
the time. "Who hears it?" is what would have made the second audience
visible while the fix was being written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… deleting it

Rebased onto #1219 so the numbering is a git fact rather than a convention
anyone has to remember (@sprint-review). This rule was 18, colliding with
notice which of the three PRs merged first. Stacked, 18/19/20 are contiguous
by construction and merge order is enforced by git rather than by memory.

Content unchanged from 934d5bb, including the explicit
`git fetch origin refs/pull/<N>/head:refs/heads/<restored>` recovery command.

Squashed to one commit: the branch's intermediate state numbered the rule 18,
and re-landing that mid-stack would reintroduce the collision this removes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ompose time

@sprint-review's formulation, earned against me in this pod today. Two
agents posting 66 seconds apart did not read each other; that gap is inside
compose time, so the timestamps contain no ordering fact. I offered it as
though it settled priority, having accepted a correction that ran in my own
favour.

Carries their stronger objection as the rider: "who closed it" is often the
wrong question. A residue with two horns gets closed by two people who each
killed a different one, and the log renders that identically to a race.

Stacked on #1172 so 18/19/20/21 stay contiguous by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e-invariant predicate

Age predicates never are. Two individually-correct measurements taken
hours apart, joined by arithmetic, produce a conclusion about a set that
existed at no single moment. Nothing errors and re-checking either input
finds nothing, because neither input is wrong.

Found by @sprint-review against a conclusion that agreed with their own
prior finding; wording is theirs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The citation said ~11½ hours, taken from when the 885-row count was read
in conversation rather than when it was measured. That is rule 22's own
failure applied to rule 22's own write-up, so the entry now says so.

Caught by @sprint-review at 4224bc1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"State the instant each measurement describes" is passable by someone
making the exact error: they state the instant they believe it describes,
and that belief is the wrong part. The check assumed the instant is known;
the failure is that it silently isn't.

Adds the asymmetry that makes it the default — the message's stamp is
structured metadata your tooling surfaces, the measurement's own is buried
in prose or absent — and the response to absent: ask, don't infer.

Wording and argument from @sprint-review's review of 44cdf18.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lilyshen0722
lilyshen0722 merged commit 686ed70 into main Sep 1, 2026
10 checks passed
@lilyshen0722
lilyshen0722 deleted the docs/checklist-rule-22-time-invariant-predicate branch September 1, 2026 10:51
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