Skip to content

fix(dashboard): the time indicator was gated on a hardcoded legacy lane set - #3014

Merged
gsxdsm merged 1 commit into
mainfrom
fix/time-indicator-renamed-lanes
Jul 31, 2026
Merged

fix(dashboard): the time indicator was gated on a hardcoded legacy lane set#3014
gsxdsm merged 1 commit into
mainfrom
fix/time-indicator-renamed-lanes

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

This is a correction to #2996, and that's why it exists

#2996 fixed the subscription: wantsLiveTimeIndicator kept a pre-load answer, so the card never joined the shared ticker. I described it as making renamed-lane cards "show their live elapsed-time indicator."

It made them eligible to. They still rendered nothing, because a second gate rejects them first — and I didn't look past the seam I'd just fixed.

const TIME_INDICATOR_COLUMNS = new Set<ColumnId>(["in-progress", "in-review", "done"]);

Both the timeIndicator memo and the chipFarRight layout test task.column against that set directly, so a card in a renamed WIP, review or completion lane returns null whatever its resolved traits say.

Why no check saw it

The census counts comparisons against legacy ids. This is a Set literal — a definition. Nothing in the backlog ever pointed here, which is the same blind spot that hid BLOCKER_ESCALATION_COLUMNS until someone read the code rather than the report.

The fix

The gate becomes a role question, with the legacy set kept as the no-flags fallback and marked DELIBERATE-LITERAL. A card whose traits haven't resolved — first paint, or a lane its workflow no longer declares — behaves exactly as before.

Measured

check result
test written first red for the right reason — control and negative passed, only the renamed case failed (expected false to be true)
after the fix 3 passed
reverting the memo gate to the raw set that case fails again
five TaskCard suites 418 tests green
gates all five green; lint and tsc clean

The negative case

Resolving traits must not put a live timer on every lane. A card in the renamed intake lane hasn't started, so it stays out — otherwise the fix trades a missing indicator for a running clock on work that hasn't begun.

Worth noting for the pattern

Two of my last four findings came from re-examining my own merged work rather than from new code: this one, and the bounded heuristic correction in #3012. Fixing one seam and declaring the symptom gone is its own failure mode — the user-visible behaviour needed both halves, and I only checked the half I'd touched.

Summary by CodeRabbit

  • Bug Fixes
    • Time indicators on task cards now correctly display across renamed board lanes and customized column names. Elapsed-time information appears consistently on work-in-progress, review, and completion-stage columns regardless of naming configuration.

…ne set

CORRECTION TO #2996, and the reason this exists. That PR fixed the SUBSCRIPTION —
`wantsLiveTimeIndicator` kept a pre-load answer, so the card never joined the shared ticker
— and I described it as making renamed-lane cards "show their live elapsed-time indicator".
It made them eligible to. They still rendered nothing, because a second gate rejects them
first, and I did not look past the seam I had just fixed.

`TIME_INDICATOR_COLUMNS` is `{in-progress, in-review, done}`. Both the `timeIndicator` memo
and the `chipFarRight` layout test `task.column` against it directly, so a card in a renamed
WIP, review or completion lane returns null whatever its resolved traits say.

WHY NO CHECK SAW IT: the census counts COMPARISONS against legacy ids, and this is a Set
literal — a DEFINITION. Nothing in the backlog ever pointed here, the same blind spot that
hid `BLOCKER_ESCALATION_COLUMNS` until someone read the code.

The legacy set stays as the no-flags fallback, marked DELIBERATE-LITERAL, so a card whose
traits have not resolved — first paint, or a lane its workflow no longer declares — behaves
exactly as before.

MEASURED
- test red first for the right reason: control and negative passed, only the renamed case
  failed, `expected false to be true`
- after: 3 passed
- reverting the memo gate to the raw set fails that case again
- five TaskCard suites: 418 tests green
- all five gates green; lint and tsc clean

The negative case: resolving traits must not put a live timer on every lane. A card in the
renamed INTAKE lane has not started, so it stays out — otherwise the fix trades a missing
indicator for a running clock on work that has not begun.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gsxdsm

gsxdsm commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Verified — 3/3, and reverting showsTimeIndicator to the hardcoded set fails 1 of 3.

This one matters more than its diff suggests, and the self-correction is the reason:

It made them eligible to. They still rendered nothing, because a second gate rejects them first — and I didn't look past the seam I'd just fixed.

That is the most useful sentence in this batch. #2996 was correct, well-tested, and mutation-verified — I checked it myself and signed off — and the feature it claimed to restore still didn't work. A passing test at the seam you fixed says nothing about the seam downstream of it. Both of us verified the subscription and neither of us asked whether anything rendered.

Worth pairing with #3012's severity correction, because they are the same failure at different scopes: we were reasoning about the mechanism we had just touched rather than what the user sees. The dep-list heuristic sorted by identifier name; this one verified by hook eligibility. Both stopped one layer short of the observable.

TIME_INDICATOR_COLUMNS also shows why a second gate is easy to miss: it is a module-level Set<ColumnId> 700 lines from the memo that consumes it, so nothing at the call site looks like a lane assumption. My derivation-aware scan flagged timeIndicator at TaskCard.tsx:1756 for the dependency problem and could never have found this, because a hardcoded constant is not a stale dependency — it is a different defect wearing the same symptom.

Keeping the legacy set as the no-flags fallback is right, and the DELIBERATE-LITERAL marker is the correct way to say so — that is the same report, don't guess reasoning as #2999's !target refusal and #3013's conditional shapes.

One process note, on my own probe

My first mutation targeted isTimeIndicatorLane — a variable that does not exist in this file. Nothing changed, the suite reported 3/3, and it looked like the tests were vacuous. Sixth time today a probe of mine produced a confident wrong answer from the harness rather than the instrument. The fix that finally stuck: substitute an exact literal string and assert it appears exactly once before replacing, so a missed anchor is an error rather than a silent no-op.

@gsxdsm
gsxdsm merged commit 3b55e2c into main Jul 31, 2026
5 checks passed
@gsxdsm
gsxdsm deleted the fix/time-indicator-renamed-lanes branch July 31, 2026 07:36
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d0009bc-0b53-4d94-9bac-da57c4dfc225

📥 Commits

Reviewing files that changed from the base of the PR and between f10261f and bee27d4.

📒 Files selected for processing (3)
  • .changeset/taskcard-time-indicator-renamed-lanes.md
  • packages/dashboard/app/components/TaskCard.tsx
  • packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx

📝 Walkthrough

Walkthrough

TaskCard now uses resolved workflow column roles to control elapsed-time indicators. It retains legacy column IDs as a fallback and adds regression tests for legacy, renamed work-in-progress, and intake lanes.

Changes

Renamed lane time indicators

Layer / File(s) Summary
Role-based time-indicator gating
packages/dashboard/app/components/TaskCard.tsx
showsTimeIndicator uses resolved column flags with a legacy-ID fallback. Both time-indicator render paths use this condition.
Regression validation and release note
packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx, .changeset/taskcard-time-indicator-renamed-lanes.md
Tests cover legacy, renamed work-in-progress, and intake lanes. A patch Changeset records the fix.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/time-indicator-renamed-lanes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

gsxdsm added a commit that referenced this pull request Jul 31, 2026
… entry was wrong (#3018)

It hid two real defects — and it explicitly told the next reader not to
re-probe them.

## What the entry did

Counted **declarations** (48, then 49) and concluded the population was
benign because each one is a fallback vocabulary, a builtin column list,
or an already-converted seam.

All true of the declarations. **The declaration isn't where the defect
lives.**

## Measure the use, not the declaration

A collection used as a **membership gate against a column**. Nine exist,
and two were live user-visible defects sitting inside a population this
doc had marked clean:

| site | defect |
|---|---|
| `TIME_INDICATOR_COLUMNS.has(task.column)` — `TaskCard` | elapsed-time
indicator never rendered on a renamed board (#3014) |
| `PLANNER_ACTIVITY_COLUMN_IDS.has(task.column)` — `useTasks` | planning
border and pulsing badge never appeared (#3017) |

The other seven are genuinely fine, and the reasons are kept because
they're the shapes worth recognising: the no-flags fallback *inside* a
role helper, a seam that seeds the legacy pair then unions resolved
lanes, a marked `DELIBERATE-LITERAL` fallback chain, and a plugin with
no trait source at all.

## The tell

One question separates the two groups: **does a flags path exist in this
file at all?**

Both defects had none — the gate was the only decision, with nothing to
degrade from. Every benign case had a resolved path sitting right next
to the literal.

## Why this is worth its own PR

A "do not re-probe" note that is wrong is **worse than no note**: it
converts one person's incomplete measurement into everybody's blind
spot. That's the same failure this document already records for
`sortTasksForDisplayColumn`, one level up — there an annotation told
readers to skip a *row*, here it told them to skip a *population*.

I wrote the original entry, and I'd read past it twice myself before
#3014 forced the re-measurement.

Docs only. No code, no baselines.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gsxdsm added a commit that referenced this pull request Jul 31, 2026
…e lane (#3017)

## How this was found — by re-testing a claim of mine

The learnings doc records "named legacy-id collections" as **measured
and clean**: 48 declarations, all fallback vocabularies, builtin column
lists, or already-converted seams. #3014 disproved that conclusion —
`TIME_INDICATOR_COLUMNS` was in that population and was a live defect.

So I re-measured over the shape that actually matters: **collections
used as a membership gate against a column.** Nine exist.

| site | verdict |
|---|---|
| `columnRoles.ts` ×2, `useSessionFiles.ts` | the no-flags fallback
*inside* the role helpers — correct by design |
| `branch-group-ops.ts` | seeds the legacy pair then unions resolved
lanes — already converted |
| `DocumentsView.tsx` | marked `DELIBERATE-LITERAL` fallback chain |
| `TaskCard.tsx` ×2 | fixed in #3014 |
| `plugins/…/reconciler.ts` | plugin with no trait source — same class
as #3003 |
| **`useTasks.ts`** | **no flags path anywhere in the file** |

## The defect

`useTasks` stamps `recentAgentActivityAt` only for cards in `{triage,
todo}`. The note at that set argues over-stamping is harmless because
every consumer re-checks for an intake lane before showing anything.

That's true, and it **only protects against false positives**. On a
board whose intake and hold lanes are renamed, the pair matches nothing
— so no stamp is ever written, and a correct downstream role check has
nothing to filter. The planning border and pulsing badge never appear
while the planner is actively working the card.

## The supplier ships with the seam

An optional resolver with no caller is the first failure shape in the
learnings doc, and my own gate would flag it — so `App` supplies it in
the same commit. `useBoardWorkflows` moved above `useTasks` to make that
expressible; it depends on `projectId` alone, nothing about tasks, so
reading it first is safe.

Remote rows deliberately get **no** flags — they belong to another
store, and local board-workflow metadata must never be applied to their
ids. That's the rule the footer index already follows.

## Measured

| check | result |
|---|---|
| `useTasks` suite | 124 → **126**, all green |
| reverting the gate to the legacy pair | fails exactly the renamed
case; the negative (renamed WIP is not planning) still passes |
| `App.test` + `useTasks` together | **269 green** |
| gates | all five green; lint and `tsc` clean |

## One observation I could not reproduce

The `App`+`useTasks` pair failed once, on a single unnamed test, and
passed on **four** subsequent runs including three consecutive. The
captured output showed jsdom URL-parse noise from `MissionManager`
fetches rather than an assertion failure, and the same pair is green on
unmodified `main`.

I'm not quarantining another file's test on one unreproducible
observation, but recording it rather than letting a green rerun bury it
— if it resurfaces in CI, this is the prior sighting.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gsxdsm added a commit that referenced this pull request Jul 31, 2026
…amed board (#3022)

Closes #3020 — which I filed **instead of** fixing, on a rationale that
turned out to be wrong.

I said the plugin had no scaffolding for faking `CePipelineStore` +
`taskStore` together. It does: `_harness.ts` already builds a real
`PluginContext` over a live PostgreSQL layer. The gap was **two missing
readers on its task-store stub**, not missing infrastructure. I checked
the harness only after filing.

## The defect

`TERMINAL_COLUMNS` is `{in-review, done}`, and the reconciler advances a
pipeline only when **every** current-stage board task is in that set. On
a board whose review and completion lanes are renamed that's false for
every task, permanently:

- the pipeline never advances a stage
- it never creates its outbound task
- it sits `running` indefinitely

Nothing errors, so it reads as work that hasn't finished. Unlike the
display defects in this family (#3014, #3017), the CE flow actually
**stops**.

## Shape

The decision is extracted to an exported `isStageTerminalColumn` because
it *is* the whole decision. Left private it could only be reached
through a pipeline-state + links + board-tasks fixture, and the half
that needed proving is that a renamed board resolves to its own lanes
through this store.

It uses `resolveReviewColumns` rather than re-deriving the union — that
helper is the documented review **set** (`mergeOrchestration ∪
mergeBlocker ∪ humanReview`), so a board splitting those across a merge
lane and a human lane is covered without this site drifting from it.

## Two things my first attempt got wrong

**The fixture spelled traits in camelCase** — `{ trait: "humanReview"
}`. Trait **ids** are kebab-case (`human-review`, `merge-blocker`,
`wip`); the camelCase names are the resolved **flags**. Those columns
therefore resolved to *no roles at all*, silently, because an unknown
trait isn't an error. `complete` is spelled identically in both
vocabularies, which is exactly what made the first run look like
*"complete works, review is broken"* rather than *"the fixture is
wrong"* — I nearly went debugging the production union.

**The harness extension is additive** and inert until a test seeds it,
so all 24 existing plugin suites see the previous shape.

## Measured

| check | result |
|---|---|
| new suite | **4/4** |
| reverting to the literal-only gate | fails **exactly 2** — the
renamed-terminal case, and a board declaring a NON-terminal column named
`done` — while the legacy control and the WIP/intake negative still pass
|
| plugin suite | **24 files, 184 tests green** |
| `tsc` + all five gates | clean |

That second row is the one that matters: the `done`-without-`complete`
board is the only shape where a real resolution and a legacy fallback
disagree, so it's what separates the fix from a lucky agreement.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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