Skip to content

refactor: remove duplicated declarations and unreferenced code (-1,819 lines) - #60

Merged
fathiraz merged 62 commits into
mainfrom
chore/ponytail-cleanup
Aug 22, 2026
Merged

refactor: remove duplicated declarations and unreferenced code (-1,819 lines)#60
fathiraz merged 62 commits into
mainfrom
chore/ponytail-cleanup

Conversation

@fathiraz

@fathiraz fathiraz commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Why

The codebase grew through several architecture migrations that were additive — the old code
stayed and each new layer wrapped it. The result was a repo that declared the same message
contract twice, wrapped 36 octicons in 35 identical hand-written wrappers, inlined one sx
preset across 15 files while the shared helper for it already existed, and shipped 234 lines of
Effect Schema with zero importers.

This PR removes the accumulated duplication without changing any user-visible behaviour.

−1,819 lines from src/ (32,384 → 30,565), 17 files deleted, 5 dependencies dropped.

What changed

Five commits, each independently green and revertible.

chore: remove unreferenced code and dead exports

Code nothing referenced, verified by grep across src/, scripts/, config files and CI:

  • schemas-github.ts (234 lines) — zero importers; every GraphQL call goes through gql(),
    which hardcodes Schema.Unknown
  • renderPatError + its 5-branch Match block — only its own test called it; token-setup.tsx
    uses buildPatError, which is the better implementation (scoped token URL, expired-vs-invalid)
  • effect-test-helpers.ts — its sole consumer destructured the layer and discarded the recorded
    calls array, so the whole recording apparatus was dead
  • SearchSelectPanel debug apparatus, TokenSetupCard's mode/onOpenOptions (its only call
    site passes no props), getFields threaded through the sprint UI (both terminal consumers
    destructured it as _getFields), recentAssignees (never passed)
  • src/assets/images/old/ — 7 tracked binaries, 2.3 MB, referenced nowhere
  • 13 package.json scripts nothing invokes — CI calls pnpm wxt submit and the coverage script
    directly, not via these aliases

refactor: collapse duplicated icon and button-motion declarations

src/ui/icons.tsx contained zero hand-drawn SVGs — it imported 36 icons from
@primer/octicons-react and re-exported each behind an identical 3-line wrapper, 35 times.
Replaced with one factory. No call-site changes across the 28 consuming files. 217 → 93 lines.

Also replaced 16 byte-identical inline copies of the button-motion block with the
primerCss.buttonMotion() preset that already existed and already had 10 callers.

test: replace the custom toEqualValue matcher with vitest's toEqual

effect-assert.ts registered a matcher that wrapped values in Effect Data.* containers to get
deep equality — which toEqual already does. Verified rather than assumed: swapped all 39 call
sites and re-ran the suite. Deleting it empties vitest.setup.ts, so suite setup drops to 0 ms.

refactor: inline single-caller Effect service wrappers

project-service.ts, cache-service.ts, services.ts and runtime-ext.ts existed only so four
call sites could write yield* svc.foo() instead of yield* Effect.promise(() => foo()). Each
wrapped an already-existing async helper and had exactly one consumer.

refactor: replace derived message schemas with a hand-written ProtocolMap

schemas-messages.ts (688 lines) declared the message contract a second time. Its docstring
claimed handlers validated payloads with Schema.decodeUnknownSync/encodeSync — a repo-wide
grep found those calls in no handler. Six payload types were declared twice, and every
consumer imported the hand-written twin from messages.ts, never the schema.

chore: drop unused dependencies

@effect/platform-browser, @resvg/resvg-js, @types/marked, @vitejs/plugin-react,
vite-node — all with no source references, confirmed by a clean production build.

How equivalence was proven, not assumed

The ProtocolMap rewrite is the one change tsc alone cannot fully guarantee: method parameters
are bivariant, so a widened input (string where the schema said 'a' | 'b') would slip
through a plain A extends B check.

A temporary type-level scaffold compared each of the 35 entries' input and output mutually,
with tuple-wrapped conditionals so unions don't distribute. It was negative-tested — widening
bulkClose's reason to string made the build fail by name (Type 'true' is not assignable to type '"bulkClose"') — then removed once both sides matched exactly.

Deliberately not cut

  • Tippy.jsdesign-system/rgp/MASTER.md:184 makes it canonical and bans Primer's <Tooltip>
  • @primer/live-region-element — looks unused, but wxt.config.ts aliases it to a local stub
  • SelectionControl's variant prop — flagged as dead by the audit, but
    checkbox-portal-host.tsx does pass it
  • 9 near-miss buttonMotion copiesmakePreset shallow-merges, so an override supplying
    its own hover rule would replace the base and silently drop transform
  • The 8 per-file test render helpers — half wrap in ThemeProvider/BaseStyles, half render
    raw, and return shapes differ. Similar-looking, not duplicated.
  • Everything in the anti-abuse path: sequential queue, sleep(1000) between mutations,
    403/429 Retry-After handling

Test plan

Gate Before After
pnpm typecheck clean clean
pnpm lint 0 errors, 56 warnings 0 errors, 48 warnings
pnpm format clean clean
pnpm test 424 pass, 1 FAIL 408 pass, 0 fail
pnpm build green green (3.08 MB)

Test count fell 425 → 408 because 17 tests were deleted alongside the dead code they covered —
they asserted that exported strings were non-empty strings, that deliberately-empty functions
don't throw, and that Effect's Schema round-trips.

The pre-existing pnpm test failure is fixed: bulk-transfer-modal.test.tsx › renders count in title was timing out at 5 s while costing ~500 ms of real work — worker contention across the
43-file parallel suite. Raised testTimeout to 15 s.

Verified in a real browser

Loaded the built dist/chrome-mv3 unpacked in Chrome. Service worker starts with no errors; the
options page (TokenSetupCard, gear/check icons) and popup (eye icon, keyboard chips) both render
correctly with zero console errors.

Still to do before merge

  • Full 12-verb regression sweep on a live project board (needs an authenticated session and a
    configured PAT; bulk transfer additionally needs a destination repo)

Summary by cubic

Replaces the Effect stack with plain async/fetch, unifies bulk progress/queue handling, and removes duplicated/unreferenced code (−1,819 lines). Fixes item ID resolution: the hyphen form (issue-N) now matches by issue number in both hierarchy fetches and reorder; previously it was treated as a databaseId and failed, so deep-linked panes and Custom position now work reliably and preserve selection order.

  • Shared runners and queue: runBulkVerb and queue-run standardize progress frames and pacing across state-change verbs; reorder stays separate (no sleeps, caller-overridable labels). Queue cancellation uses AbortController; the tracker frame is typed once as QueueFrame.
  • Networking/logging: a single gql fetch client replaces the service stack (3 rate‑limit retries, 30s per‑attempt timeout, same backoff/jitter). Error classes are plain Error subclasses; logging calls console directly.
  • Reorder path fix: GET_PROJECT_ITEMS_FOR_REORDER now selects number, and createIssueRefIndex resolves both issue:DBID and issue-N consistently.
  • Caches/UI dedup: hover/hierarchy caches use Map+timestamp with TTL, in‑flight dedupe, FIFO cap, and “never cache rejects”. Shared plural(), newProcessId(), and Kbd; modal backdrops use primerCss.modalOverlay(). Duplicate/relationship section UI is centralized.
  • Tests/tooling/deps: add suites for bulk verbs, cache TTL/dedupe/eviction, reorder, overlay state, relationship task sequences, and ID parsing. Simplify coverage script/workflow. Drop effect, @effect/platform*, @primer/live-region-element, and other unused packages. Typecheck/lint/build remain green.

Written for commit e46930e. Summary will update on new commits.

Review in cubic

Deletes code that nothing in the repo references, verified by grep across
src/, scripts/, config files and CI workflows:

- src/lib/schemas-github.ts (234 lines) — zero importers; every production
  GraphQL call goes through gql(), which hardcodes Schema.Unknown
- renderPatError + its 5-branch Match block in errors.ts — only its own test
  called it; token-setup.tsx uses its own buildPatError, which is the better
  implementation (scoped token URL, expired-vs-invalid distinction)
- src/lib/effect-test-helpers.ts — its sole consumer destructured the layer
  and discarded the recorded calls array, so the whole recording apparatus
  was dead; inlined the two lines that test actually uses
- SearchSelectPanel debug apparatus (debugName prop, log callback,
  summarizeDebugValue, 10 call sites) — console.log-based instrumentation
- src/ui/step-indicator.tsx and src/features/field-helpers.ts — one export,
  one caller each; inlined into that caller
- src/background/bulk-handlers.ts — 13 lines calling four register functions
- TokenSetupCard mode/onOpenOptions props — its only call site passes no
  props, so every compact branch and the secondary button were unreachable
- getFields threaded through the sprint UI — both terminal consumers already
  destructured it as _getFields
- recentAssignees prop on BulkRandomAssignFlyout — never passed
- primerCss.borderedContainer and card (no callers); footerBorder was a
  byte-identical alias of divider
- duplicated fmt/daysLeft in sprint-progress-view — sprint-utils exports both
- duplicated 30-line "Sprint settings" button in sprint-table-widget
- clearMousedownPath, logger.info, BulkRandomAssignData — no callers
- 13 package.json scripts nothing invokes (CI calls `pnpm wxt submit` and
  the coverage script directly, not via these aliases)
- src/assets/images/old/ — 7 tracked binaries, 2.3 MB, referenced nowhere
- update-coverage-badge.mjs dual-mode: only the json-summary path is used

Also raises vitest testTimeout to 15s: bulk-transfer-modal's render test
costs ~500ms of work but exceeded the 5s default under worker contention.

typecheck clean, 413 tests pass, lint 0 errors (56 -> 52 warnings).
src/ui/icons.tsx contained zero hand-drawn SVGs: it imported 36 icons from
@primer/octicons-react and re-exported each behind an identical 3-line
wrapper, 35 times. Replaced with one `icon()` factory and one export per
glyph. No call-site changes across the 28 consuming files; still static
named exports, so tree-shaking is unaffected. 217 -> 93 lines.

The `Octicon` adapter's runtime `typeof size === 'string'` branch was dead —
every call site passes a numeric size — but the *type* must stay wide,
because Primer's TextInput.Action `icon` prop requires a component accepting
its own Size union. Kept the union, dropped the unreachable branch.

Also replaced 16 byte-identical inline copies of the button motion block
with the primerCss.buttonMotion() preset that already existed in
primer-css-helper.ts and already had 10 callers.

Left the 9 near-miss copies alone: makePreset shallow-merges, so an override
supplying its own '&:hover:not(:disabled)' would replace the base rule and
silently drop the transform. Those need a different fix, not this one.

typecheck clean, 413 tests pass, lint 0 errors (52 -> 51 warnings).
src/lib/effect-assert.ts registered a `toEqualValue` matcher that recursively
wrapped values in Effect Data.* containers so Equal.equals would do a deep
comparison — something vitest's built-in toEqual already does.

Verified rather than assumed: swapped all 39 call sites to toEqual and ran
the suite. 413/413 still pass, so the custom matcher was buying nothing. It
was in fact strictly weaker for Errors, since wrap() collapsed them to
{ name, message }.

Deleting it empties vitest.setup.ts (its only job was registering the
matcher), so that file and the setupFiles entry go too. Suite setup time
drops from ~11.7s to 0.

Left the eight per-file render helpers alone: they look alike but differ in
substance — half wrap in ThemeProvider/BaseStyles and half render raw, and
the return shapes differ (container vs {container, root} vs added
find/findAll). That is similar-looking code, not duplication.
project-service.ts, cache-service.ts, services.ts and runtime-ext.ts existed
only so four call sites could write `yield* svc.foo()` instead of
`yield* Effect.promise(() => foo())`. Each wrapped an async helper that
already existed, and each had exactly one consumer.

Replaced with four direct calls:
  field-handlers      getProjectFieldsData, resolveProjectItemIdsWithTitles
  hierarchy-handlers  getOrCachePreview, getOrCacheHierarchy

and dropped provideBackground from those runHandler calls.

Also collapsed the remaining
  Effect.tryPromise({ try: fn, catch: e => e as unknown }).pipe(Effect.orDie)
sites to Effect.promise(fn) — identical semantics, since both end with the
rejection as a fiber defect that runHandler's cause printer surfaces.

typecheck clean, 413 tests pass, lint 0 errors.
…lMap

schemas-messages.ts declared the message contract a second time — 688 lines
of Effect Schema whose only production consumer was a type-only import in
messages.ts. Its docstring claimed background handlers validated payloads
with Schema.decodeUnknownSync/encodeSync; a repo-wide grep found those calls
in no handler. The validation it documented did not happen.

Six payload types were declared twice (IssueRelationshipData,
DuplicateItemPlan, ItemPreviewData, HierarchyData, SprintProgressData,
IssueSearchResultData) and every consumer imported the hand-written twin from
messages.ts, never the schema. Three type-level helpers (SchemaInput,
SchemaOutput, DeepMutable) existed only to undo the readonly that Schema
introduced.

The replacement ProtocolMap is built from the interfaces messages.ts already
declared. Equivalence was proven, not assumed: a temporary scaffold compared
each of the 35 entries' input and output types mutually, using tuple-wrapped
conditionals so unions don't distribute and so method-parameter bivariance
can't hide a widened type. Negative-tested by widening bulkClose's `reason`
to string, which failed the check by name. The scaffold is removed in this
commit now that both sides match exactly.

Deleting it orphans schemas-errors.ts and schemas-storage.ts (their only
importer) and schema-snapshots.test.ts, which round-tripped schemas nothing
decodes with — it exercised effect/Schema, not this repo.

typecheck clean, 408 tests pass, lint 0 errors (51 -> 48 warnings).
Removed five packages with no source references, verified by grep across
src/, scripts/ and every config file, then confirmed by a clean production
build:

- @effect/platform-browser  never imported anywhere
- @resvg/resvg-js           no references (leftover from icon generation)
- @types/marked             marked has shipped its own types since v4
- @vitejs/plugin-react      supplied by @wxt-dev/module-react
- vite-node                 supplied by wxt

Kept @primer/live-region-element: it looks unused in src/ but wxt.config.ts
aliases it to src/lib/primer-live-region-stub.ts to avoid a customElements
error in content scripts, so the package name has to resolve.

Also removed the minimumReleaseAgeExclude block from pnpm-workspace.yaml —
it pinned exclusions for a minimumReleaseAge policy configured nowhere.

pnpm install / format / test / lint / typecheck all pass; wxt build green.
@ecc-tools

ecc-tools Bot commented Aug 8, 2026

Copy link
Copy Markdown

Analyzing 200 commits...

@ecc-tools

ecc-tools Bot commented Aug 8, 2026

Copy link
Copy Markdown

Analysis Complete

Generated ECC bundle from 6 commits | Confidence: 70%

View Pull Request #61

Repository Profile
Attribute Value
Language TypeScript
Framework React
Commit Convention conventional
Test Directory colocated
Changed Files (71)
Metric Value
Files changed 71
Additions 558
Deletions 2601

Top hotspots

Path Status +/-
src/lib/schemas-messages.ts removed +0 / -688
src/lib/messages.ts modified +258 / -7
src/ui/icons.tsx modified +56 / -180
src/lib/schemas-github.ts removed +0 / -234
pnpm-lock.yaml modified +0 / -166

Top directories

Directory Files Total changes
src/lib 27 1806
src/ui 5 412
src/features 16 372
src/background 8 309
. 4 199
Analysis Depth Readiness (evidence-backed, 43%)

ECC Tools uses this to decide whether recommendations should stay at commit-history/setup guidance or expand into CI, security, harness, reference-set, AI-routing, and team backlog work.

Area Status Evidence / Next Step
Commit history Ready 6 commits sampled
CI/CD signals Ready .github/workflows/coverage.yml, vitest.config.ts
Security evidence Missing Add AgentShield, audit, SARIF, SBOM, or security review evidence so recommendations can cover security posture.
Harness configuration Missing Add Claude, Codex, OpenCode, Zed, dmux, MCP, plugin, or cross-harness config evidence for harness-agnostic recommendations.
Reference/eval evidence Missing Add fixtures, golden traces, reference sets, or evaluator benchmarks so deeper recommendations have regression evidence.
AI routing and cost controls Ready src/features/token-setup.tsx
Team handoff and project tracking Missing Add roadmap, runbook, project, Linear, or follow-up tracking docs so generated work can land in a team queue.
Reference Set Readiness (0/7, 0%)
Area Status Evidence / Next Step
Deep analyzer corpus Missing Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions.
RAG/evaluator comparison Missing Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior.
PR salvage/review corpus Missing Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation.
Discussion triage corpus Missing Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications.
Harness compatibility Missing Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces.
Security evidence Missing Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs.
CI failure-mode evidence Missing Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes.
Likely Future Issues (4)
Severity Signal Why it may show up
MEDIUM Runtime config changes may ship without example or template updates 1 runtime config paths changed; 0 example or template config files changed
MEDIUM User-facing UI changes may ship without browser coverage 5 user-facing UI paths changed; 0 browser or e2e coverage files changed
MEDIUM Cost or token-risk changes may ship without budget evidence 1 cost/token-risk paths changed; 0 budget, usage, or cost validation artifacts changed
MEDIUM CI workflow changes may ship without failure-mode evidence 2 CI/test-runner paths changed; 0 CI failure-mode evidence artifacts changed
  • Runtime config changes may ship without example or template updates: The PR changes runtime config or deployment settings but does not update any obvious example env file or config template.
  • User-facing UI changes may ship without browser coverage: The PR changes components, pages, or other user-facing UI files without touching any obvious browser or end-to-end coverage.
  • Cost or token-risk changes may ship without budget evidence: The PR changes AI routing, usage, token budget, or model-call surfaces without touching obvious budget, usage-limit, or cost regression evidence.
  • CI workflow changes may ship without failure-mode evidence: The PR changes CI workflows or test-runner entrypoints without touching CI failure fixtures, captured logs, troubleshooting notes, or regression evidence.
Suggested Follow-up Work (4)
Type Suggested title Targets
PR chore: sync config templates for vitest.config.ts vitest.config.ts
PR test: add browser coverage for src/ui/icons.tsx + src/ui/modal-shell.tsx src/ui/icons.tsx, src/ui/modal-shell.tsx
PR test: add budget evidence for src/features/token-setup.tsx src/features/token-setup.tsx
PR ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts .github/workflows/coverage.yml, vitest.config.ts
  • chore: sync config templates for vitest.config.ts: Backfill example env files or config templates before a fresh setup drifts from the shipped runtime surface.
  • test: add browser coverage for src/ui/icons.tsx + src/ui/modal-shell.tsx: Backfill browser coverage before another user-facing UI change lands on the touched surface.
  • test: add budget evidence for src/features/token-setup.tsx: Backfill cost, token, or usage-limit validation before another model-routing change lands on the touched surface.
  • ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts: Backfill CI failure-mode evidence before another workflow or test-runner change lands on the touched surface.

Copy-ready bodies

chore: sync config templates for vitest.config.ts

## Summary
- Update the example env files, sample configs, or deployment templates that should mirror the changed runtime configuration surface.

## Why
- Backfill example env files or config templates before a fresh setup drifts from the shipped runtime surface.

## Touched paths
- `vitest.config.ts`

## Validation
- Update the repo example env file or config template that should reflect the new runtime settings.
- Run the setup, boot, or deployment validation flow that depends on the changed config surface.

test: add browser coverage for src/ui/icons.tsx + src/ui/modal-shell.tsx

## Summary
- Add browser or end-to-end coverage for the recently changed user-facing surface.

## Why
- Backfill browser coverage before another user-facing UI change lands on the touched surface.

## Touched paths
- `src/ui/icons.tsx`
- `src/ui/modal-shell.tsx`

## Validation
- Add or extend browser / e2e coverage for the changed component, page, or flow.
- Exercise the visible user journey that depends on the touched UI surface.

test: add budget evidence for src/features/token-setup.tsx

## Summary
- Add budget or usage-limit validation for the recently changed AI routing or model-call surface.

## Why
- Backfill cost, token, or usage-limit validation before another model-routing change lands on the touched surface.

## Touched paths
- `src/features/token-setup.tsx`

## Validation
- Add or extend budget, token, usage-limit, or model-routing regression coverage for the changed path.
- Verify the route still enforces plan limits, retry caps, fallback behavior, or explicit cost controls.

ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts

## Summary
- Add CI failure-mode evidence for the recently changed workflow or test-runner surface.

## Why
- Backfill CI failure-mode evidence before another workflow or test-runner change lands on the touched surface.

## Touched paths
- `.github/workflows/coverage.yml`
- `vitest.config.ts`

## Validation
- Add or update a CI failure fixture, captured failing log, troubleshooting note, workflow dry-run evidence, or regression test for the changed CI/test-runner behavior.
- Run the affected workflow or test-runner entrypoint locally or in CI and record pass/fail evidence.
Detected Workflows (1)
Workflow Description
refactoring Code refactoring and cleanup workflow
Generated Instincts (12)
Domain Count
git 2
code-style 3
architecture 1
testing 5
workflow 1

After merging, import with:

/instinct-import .claude/homunculus/instincts/inherited/refined-github-projects-instincts.yaml

Files

  • .claude/ecc-tools.json
  • .claude/skills/refined-github-projects/SKILL.md
  • .agents/skills/refined-github-projects/SKILL.md
  • .agents/skills/refined-github-projects/agents/openai.yaml
  • .claude/identity.json
  • .codex/config.toml
  • .codex/AGENTS.md
  • .codex/agents/explorer.toml
  • .codex/agents/reviewer.toml
  • .codex/agents/docs-researcher.toml
  • .claude/homunculus/instincts/inherited/refined-github-projects-instincts.yaml
  • .claude/commands/refactoring.md

ECC Tools | Everything Claude Code

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor auto review

No actionable issues found on changed lines.

No actionable issues found.

Generated automatically when this PR was submitted using Cursor CLI with --model auto.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 71 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/ui/icons.tsx">

<violation number="1" location="src/ui/icons.tsx:48">
P2: Consumers using the previously supported CSS-length or numeric-string `size` values can no longer type-check, and the runtime sizing fallback has been removed. Preserving the `number | string` prop and equivalent conversion/wrapper logic would maintain the existing icon sizing contract.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/ui/icons.tsx

export type IconProps = {
size?: number | string
size?: OcticonSize

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Consumers using the previously supported CSS-length or numeric-string size values can no longer type-check, and the runtime sizing fallback has been removed. Preserving the number | string prop and equivalent conversion/wrapper logic would maintain the existing icon sizing contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ui/icons.tsx, line 48:

<comment>Consumers using the previously supported CSS-length or numeric-string `size` values can no longer type-check, and the runtime sizing fallback has been removed. Preserving the `number | string` prop and equivalent conversion/wrapper logic would maintain the existing icon sizing contract.</comment>

<file context>
@@ -30,188 +30,64 @@ import {
+
 export type IconProps = {
-  size?: number | string
+  size?: OcticonSize
   color?: string
-}
</file context>

bulk-state.ts, bulk-position.ts, bulk-rename.ts and cache.ts had no test
coverage, so the upcoming shared-runner extraction had nothing to prove
parity against.

Adds characterization tests asserting the exact broadcastQueue payload
sequence each of the 12 bulk verbs emits today: label wording, per-item
progress strings, processId prefix, failedItems passthrough, the Done!
frame, the isBulkFull short-circuit and bulkClose's Undo reverse hint
including its completed-slice and failed-id filtering.

Also pins two easily-lost distinctions: the mandatory sleep(1000) between
content-creating mutations, and its deliberate absence from reorder, which
creates no content.

For cache.ts, pins the four properties the implementation must keep
regardless of mechanism: TTL expiry, in-flight dedupe, FIFO cap, and
never caching a rejected fetch.

Each suite was negative-tested by mutating the source (progress verbs,
label wording, sleep interval, undo filter, cache TTL, failure
invalidation) and confirming it goes red.

Tests 408 -> 524. No production code changed.
Adds lib/format.ts with two helpers that replace hand-copied expressions:
plural() for the 43 sites spelling out `${n} item${n !== 1 ? 's' : ''}`,
and newProcessId() for the 16 spelling out the same Date.now()/Math.random
id. Two sites keep their inline form — a bare suffix with no count, and one
with a word interposed between count and noun — where the helper would read
worse.

format.ts is a zero-dependency leaf on purpose: newProcessId initially went
into queue.ts, which dragged the Effect queue machinery, the logger and
storage into toast-store and broke its test suite.

Removes 66 `export` keywords from symbols only ever used inside their own
file. That is what surfaced the rest of this commit: with the exports gone
the linter could finally see decodeProjectId, decodePat, decodeLogin, the
FieldMeta alias, and applyRule — the last self-described as "retained for
the dying modal" that no longer exists.

Also drops getAllNativeItemIds (no callers anywhere), resolveDChord (a
one-line ternary whose logic is already inlined in bulk-actions-bar), and
the unused runFork/runSync runtime wrappers. PanelCard and StatusBanner lose
the variants and props no call site passes.

Lint warnings 48 -> 47. Tests 524 -> 522 (the two resolveDChord cases).
Eight handlers — close, open, delete, lock, unlock, pin, unpin, transfer —
were the same fifty-line run copied eight times: concurrency gate, Resolving
frame, resolve items, one task per item, processQueue with a progress
broadcast, Done! frame, release. Only the mutation and the words on screen
differed.

runBulkVerb now owns that shell. Each onMessage call site is kept rather than
generated from a table, so every message payload stays exactly typed —
handler parameters are bivariant, and a generated dispatcher would let a
widened field through unnoticed.

Close keeps its Undo hint via an optional buildReverse, which receives only
the ids that were actually processed and did not fail. Transfer keeps its
two-step preparation via an optional prepare, which is handed a setStatus so
it can still narrate "Resolving target repository..." then "Resolving
items...".

Rename, random-assign and the two reorder handlers are deliberately left
alone. None of them resolves one task per project item, and bending them
through the runner would have meant five more optional callbacks to serve
four call sites — trading duplication for a worse abstraction.

bulk-state.ts 526 -> 142 lines. The 124 background characterization tests
pass unchanged, including a new one asserting transfer resolves the target
repository before it resolves items.

One deliberate cosmetic change: the rejection and abort console lines now
name the id prefix (close) rather than the message (bulkClose), matching the
processId they sit next to.
…e table

runBulkUpdate switched on dataType twice — once to build the progress detail,
once to build the mutation — nine branches each, in two places, with nothing
holding them together. Cognitive complexity was 617, four times worse than
anything else in the repo, and adding a dataType meant remembering to touch
both switches.

Both now live as one row per dataType. Rows that only need a different
progress line (SINGLE_SELECT, ITERATION) supply just that and fall through to
the shared project-field mutation.

The ~12 `update.value as any` casts collapse into one documented
BulkFieldValue narrowing at the message boundary, which is where the contract
genuinely types the payload as unknown. Lint warnings 47 -> 34.

Honest accounting: this does not save lines. The file goes 319 -> 363,
because the row structure and its types cost more than the two bare switches
did. It buys the complexity drop, the type safety, and the guarantee that
detail and mutation cannot drift.

Adds 46 tests covering every dataType's detail string and mutation. They were
run against the pre-refactor implementation as well and pass there unchanged,
which is what proves this rewrite is behaviour-preserving rather than merely
self-consistent.
bulk-duplicate-modal.tsx was the largest file in the repo at 1,259 lines,
most of it renderValueSection opening ten branches with the same twenty-line
Tippy-wrapped label, two near-identical pressable chips, and three scalar
inputs differing only in their type attribute.

SectionLabel and OptionChip now live in bulk-duplicate-section-ui.tsx.
Assignees/labels, blocked-by/blocking and text/number/date each collapse to
one branch parameterised over what actually differs.

The LOADING skeleton repeated the same shimmer sx five times, and declared
its @Keyframes on only the first block — the other four were silently relying
on that sibling to define the animation they referenced. shimmerSx carries
its own keyframes so each block stands alone.

The hand-rolled backdrops in the duplicate and transfer modals were
byte-identical to primerCss.modalOverlay()/modalPanel() and now use them.

Not routed through ModalShell, despite the audit proposing it: the shell
wraps children in its own padded scroll container, and both modals render
their own headers and padded bodies, so it would have double-padded and
nested two scroll areas. Sharing the presets gets the deduplication without
the layout risk.

bulk-duplicate-modal 1259 -> 886 lines, transfer 471 -> 453, plus a 148-line
shared module. Lint warnings 47 -> 35. All 569 tests pass, including the
duplicate and transfer modal suites.
… in the file

cache.ts held three caching strategies. Two were a plain Map keyed on
{ data, expiresAt }. The third needed four helper functions, a sibling Map of
eviction fibers so re-insertions could interrupt a stale timer, and a double
TTL — Effect.cachedWithTTL wrapped inside a scheduled eviction, both set to
one minute.

Its comment justified the fibers as TestClock-friendly. Nothing ever used
TestClock here; cache.ts had no test at all until this branch added one.

The hover caches now check expiry on read, which is why there are no timers
left to cancel. All four properties the old code went to such lengths for are
preserved and now asserted: TTL expiry, in-flight deduplication of concurrent
hovers, the 500-entry oldest-first cap, and never caching a rejected fetch.

182 -> 126 lines, and cache.ts no longer imports effect at all.

sprint-store.ts held two byte-identical get/set/subscribe stores differing
only in variable names; they share a local factory now.

Two audited items are deliberately not here. Migrating the remaining stores to
useSyncExternalStore was costed at ~130 lines on the assumption consumers
carried useState+useEffect+subscribe triads — they do not, they are already
one-liners, so the real saving is ~28 for churn through the selection hot
path. And the SPA-navigation helper assumed both injectors patched
history.pushState; only issue-detail does, leaving a ~10-line rAF debounce
that a shared module would not pay for.
Two places enumerated all ten overlay setters to dismiss them — the
selection-cleared handler and the Escape shortcut. Adding an overlay meant
remembering both lists, and missing one left a stuck overlay behind.

closeAllOverlays() replaces both.

The full ten-booleans-to-one-enum rewrite from the audit is not here. It
would touch 61 setter and 32 reader references in an 884-line component with
no direct test file, to save roughly 35 lines. The duplication that actually
caused bugs was the two dismissal lists, and that is gone at a fraction of
the exposure.

Registering the bar chords into shortcutRegistry is also dropped: the audit
claimed the help overlay does not list them, which is wrong — it carries a
static barChordLegend covering all eleven. Nothing to fix.
@ecc-tools

ecc-tools Bot commented Aug 15, 2026

Copy link
Copy Markdown

Analyzing 200 commits...

@ecc-tools

ecc-tools Bot commented Aug 15, 2026

Copy link
Copy Markdown

Analysis Complete

Generated ECC bundle from 13 commits | Confidence: 75%

View Pull Request #62

Repository Profile
Attribute Value
Language TypeScript
Framework React
Commit Convention conventional
Test Directory colocated
Changed Files (126)
Metric Value
Files changed 126
Additions 3121
Deletions 4268

Top hotspots

Path Status +/-
src/features/bulk-duplicate-modal.tsx modified +186 / -555
src/lib/schemas-messages.ts removed +0 / -688
src/background/bulk-state.ts modified +87 / -471
src/background/__tests__/bulk-state.test.ts added +393 / -0
src/background/bulk-update.ts modified +215 / -170

Top directories

Directory Files Total changes
src/lib 41 1949
src/background 20 1699
src/background/__tests__ 5 1534
src/features 34 1415
src/ui 10 516
Analysis Depth Readiness (evidence-backed, 43%)

ECC Tools uses this to decide whether recommendations should stay at commit-history/setup guidance or expand into CI, security, harness, reference-set, AI-routing, and team backlog work.

Area Status Evidence / Next Step
Commit history Ready 13 commits sampled
CI/CD signals Ready .github/workflows/coverage.yml, vitest.config.ts
Security evidence Missing Add AgentShield, audit, SARIF, SBOM, or security review evidence so recommendations can cover security posture.
Harness configuration Missing Add Claude, Codex, OpenCode, Zed, dmux, MCP, plugin, or cross-harness config evidence for harness-agnostic recommendations.
Reference/eval evidence Missing Add fixtures, golden traces, reference sets, or evaluator benchmarks so deeper recommendations have regression evidence.
AI routing and cost controls Ready src/features/token-setup.tsx
Team handoff and project tracking Missing Add roadmap, runbook, project, Linear, or follow-up tracking docs so generated work can land in a team queue.
Reference Set Readiness (0/7, 0%)
Area Status Evidence / Next Step
Deep analyzer corpus Missing Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions.
RAG/evaluator comparison Missing Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior.
PR salvage/review corpus Missing Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation.
Discussion triage corpus Missing Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications.
Harness compatibility Missing Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces.
Security evidence Missing Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs.
CI failure-mode evidence Missing Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes.
Likely Future Issues (5)
Severity Signal Why it may show up
MEDIUM Runtime config changes may ship without example or template updates 1 runtime config paths changed; 0 example or template config files changed
HIGH Async job or webhook changes may ship without reliability coverage 1 async surface paths changed; 0 async-focused integration or e2e tests changed
MEDIUM User-facing UI changes may ship without browser coverage 10 user-facing UI paths changed; 0 browser or e2e coverage files changed
MEDIUM Cost or token-risk changes may ship without budget evidence 1 cost/token-risk paths changed; 0 budget, usage, or cost validation artifacts changed
MEDIUM CI workflow changes may ship without failure-mode evidence 2 CI/test-runner paths changed; 0 CI failure-mode evidence artifacts changed
  • Runtime config changes may ship without example or template updates: The PR changes runtime config or deployment settings but does not update any obvious example env file or config template.
  • Async job or webhook changes may ship without reliability coverage: The PR changes queue, worker, cron, or webhook-sensitive files without touching any obvious async-focused integration or end-to-end tests.
  • User-facing UI changes may ship without browser coverage: The PR changes components, pages, or other user-facing UI files without touching any obvious browser or end-to-end coverage.
  • Cost or token-risk changes may ship without budget evidence: The PR changes AI routing, usage, token budget, or model-call surfaces without touching obvious budget, usage-limit, or cost regression evidence.
  • CI workflow changes may ship without failure-mode evidence: The PR changes CI workflows or test-runner entrypoints without touching CI failure fixtures, captured logs, troubleshooting notes, or regression evidence.
Suggested Follow-up Work (5)
Type Suggested title Targets
PR chore: sync config templates for vitest.config.ts vitest.config.ts
PR test: add async coverage for src/lib/queue.ts src/lib/queue.ts
PR test: add browser coverage for src/ui/bulk-flyout.tsx + src/ui/icons.tsx src/ui/bulk-flyout.tsx, src/ui/icons.tsx
PR test: add budget evidence for src/features/token-setup.tsx src/features/token-setup.tsx
PR ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts .github/workflows/coverage.yml, vitest.config.ts
  • chore: sync config templates for vitest.config.ts: Backfill example env files or config templates before a fresh setup drifts from the shipped runtime surface.
  • test: add async coverage for src/lib/queue.ts: Backfill async reliability coverage before another queue, worker, or webhook change lands on the touched surface.
  • test: add browser coverage for src/ui/bulk-flyout.tsx + src/ui/icons.tsx: Backfill browser coverage before another user-facing UI change lands on the touched surface.
  • test: add budget evidence for src/features/token-setup.tsx: Backfill cost, token, or usage-limit validation before another model-routing change lands on the touched surface.
  • ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts: Backfill CI failure-mode evidence before another workflow or test-runner change lands on the touched surface.

Copy-ready bodies

chore: sync config templates for vitest.config.ts

## Summary
- Update the example env files, sample configs, or deployment templates that should mirror the changed runtime configuration surface.

## Why
- Backfill example env files or config templates before a fresh setup drifts from the shipped runtime surface.

## Touched paths
- `vitest.config.ts`

## Validation
- Update the repo example env file or config template that should reflect the new runtime settings.
- Run the setup, boot, or deployment validation flow that depends on the changed config surface.

test: add async coverage for src/lib/queue.ts

## Summary
- Add async reliability coverage for the recently changed queue, worker, cron, or webhook surface.

## Why
- Backfill async reliability coverage before another queue, worker, or webhook change lands on the touched surface.

## Touched paths
- `src/lib/queue.ts`

## Validation
- Add or extend integration / e2e coverage for the changed queue, worker, cron, or webhook behavior.
- Exercise retries, idempotency, failure handling, or equivalent async boundary cases.

test: add browser coverage for src/ui/bulk-flyout.tsx + src/ui/icons.tsx

## Summary
- Add browser or end-to-end coverage for the recently changed user-facing surface.

## Why
- Backfill browser coverage before another user-facing UI change lands on the touched surface.

## Touched paths
- `src/ui/bulk-flyout.tsx`
- `src/ui/icons.tsx`

## Validation
- Add or extend browser / e2e coverage for the changed component, page, or flow.
- Exercise the visible user journey that depends on the touched UI surface.

test: add budget evidence for src/features/token-setup.tsx

## Summary
- Add budget or usage-limit validation for the recently changed AI routing or model-call surface.

## Why
- Backfill cost, token, or usage-limit validation before another model-routing change lands on the touched surface.

## Touched paths
- `src/features/token-setup.tsx`

## Validation
- Add or extend budget, token, usage-limit, or model-routing regression coverage for the changed path.
- Verify the route still enforces plan limits, retry caps, fallback behavior, or explicit cost controls.

ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts

## Summary
- Add CI failure-mode evidence for the recently changed workflow or test-runner surface.

## Why
- Backfill CI failure-mode evidence before another workflow or test-runner change lands on the touched surface.

## Touched paths
- `.github/workflows/coverage.yml`
- `vitest.config.ts`

## Validation
- Add or update a CI failure fixture, captured failing log, troubleshooting note, workflow dry-run evidence, or regression test for the changed CI/test-runner behavior.
- Run the affected workflow or test-runner entrypoint locally or in CI and record pass/fail evidence.
Review Activity (2 reviews, 1 inline comments, 1 unresolved threads)
Signal Count
Approvals 0
Change requests 0
Comment-only reviews 2
Dismissed reviews 0
Pending reviews 0
Review threads 1
Unresolved threads 1
Outdated threads 0
Latest review Commented
Latest submitted at 2026-08-08T01:00:32Z

Top unresolved thread files

Path Unresolved Outdated
src/ui/icons.tsx 1 0

Latest reviewer states

Reviewer State Submitted
@cubic-dev-ai[bot] Commented 2026-08-08T01:00:32Z
@github-actions[bot] Commented 2026-08-08T00:58:40Z
Review Follow-up Signals (2)
Severity Signal Evidence
HIGH Clear unresolved review threads 1 unresolved review thread(s) remain
MEDIUM Get an explicit approval No approving review is recorded for this PR

Recommended next actions

  • Resolve, reply to, or explicitly carry forward each unresolved thread before treating the PR as review-complete.
  • Ask for an approval after requested changes and unresolved discussions are addressed.
  • Prioritize src/ui/icons.tsx (1 unresolved thread(s), 0 outdated).
Detected Workflows (1)
Workflow Description
refactoring Code refactoring and cleanup workflow
Generated Instincts (12)
Domain Count
git 2
code-style 3
architecture 1
testing 5
workflow 1

After merging, import with:

/instinct-import .claude/homunculus/instincts/inherited/refined-github-projects-instincts.yaml

Files

  • .claude/ecc-tools.json
  • .claude/skills/refined-github-projects/SKILL.md
  • .agents/skills/refined-github-projects/SKILL.md
  • .agents/skills/refined-github-projects/agents/openai.yaml
  • .claude/identity.json
  • .codex/config.toml
  • .codex/AGENTS.md
  • .codex/agents/explorer.toml
  • .codex/agents/reviewer.toml
  • .codex/agents/docs-researcher.toml
  • .claude/homunculus/instincts/inherited/refined-github-projects-instincts.yaml
  • .claude/commands/refactoring.md

ECC Tools | Everything Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 64 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/background/cache.ts">

<violation number="1" location="src/background/cache.ts:35">
P2: When an expired key is refreshed while the cache is full, `Map.set` keeps its original FIFO position, so later hovers can evict this freshly fetched entry before older entries. Delete the expired key before the capacity loop so the replacement is appended and avoids unnecessary refetches.</violation>
</file>

<file name="src/background/__tests__/cache.test.ts">

<violation number="1" location="src/background/__tests__/cache.test.ts:4">
P3: The test file's header comment says cache.ts "currently implements the preview/hierarchy TTL with Effect fibers" that "can be swapped for the plain Map + timestamp pattern" — but this PR already performed that swap. The comment now describes the pre-change state and is factually wrong about the current code. Update it to describe the plain Map + timestamp implementation actually being characterized.</violation>
</file>

<file name="src/background/__tests__/bulk-state.test.ts">

<violation number="1" location="src/background/__tests__/bulk-state.test.ts:78">
P3: The new characterization tests duplicate the same vi.hoisted mock scaffold, `runVerb` driver, and `frames()` helper across all four added files (bulk-state, bulk-rename, bulk-position, bulk-update-fields). Extract the shared mock-setup + runVerb/frames helpers into a common test util (e.g. `__tests__/helpers/bulk-test-utils.ts`) parameterized by the per-module mocks, so the four files only declare the mocks and cases that differ. This trims ~60 lines of near-identical scaffolding and keeps future bulk-verb tests consistent.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/background/cache.ts
fetchFn: () => Promise<T>,
): Promise<T> {
const existing = cache.get(key)
if (existing && existing.expiresAt > Date.now()) return existing.value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an expired key is refreshed while the cache is full, Map.set keeps its original FIFO position, so later hovers can evict this freshly fetched entry before older entries. Delete the expired key before the capacity loop so the replacement is appended and avoids unnecessary refetches.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/background/cache.ts, line 35:

<comment>When an expired key is refreshed while the cache is full, `Map.set` keeps its original FIFO position, so later hovers can evict this freshly fetched entry before older entries. Delete the expired key before the capacity loop so the replacement is appended and avoids unnecessary refetches.</comment>

<file context>
@@ -1,130 +1,71 @@
+  fetchFn: () => Promise<T>,
+): Promise<T> {
+  const existing = cache.get(key)
+  if (existing && existing.expiresAt > Date.now()) return existing.value
 
-function capSetupMap<V>(
</file context>
Suggested change
if (existing && existing.expiresAt > Date.now()) return existing.value
if (existing) {
if (existing.expiresAt > Date.now()) return existing.value
cache.delete(key)
}

// Characterization tests for the hovercard preview/hierarchy caches and the
// resolved-item handoff cache.
//
// cache.ts currently implements the preview/hierarchy TTL with Effect fibers,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The test file's header comment says cache.ts "currently implements the preview/hierarchy TTL with Effect fibers" that "can be swapped for the plain Map + timestamp pattern" — but this PR already performed that swap. The comment now describes the pre-change state and is factually wrong about the current code. Update it to describe the plain Map + timestamp implementation actually being characterized.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/background/__tests__/cache.test.ts, line 4:

<comment>The test file's header comment says cache.ts "currently implements the preview/hierarchy TTL with Effect fibers" that "can be swapped for the plain Map + timestamp pattern" — but this PR already performed that swap. The comment now describes the pre-change state and is factually wrong about the current code. Update it to describe the plain Map + timestamp implementation actually being characterized.</comment>

<file context>
@@ -0,0 +1,184 @@
+// Characterization tests for the hovercard preview/hierarchy caches and the
+// resolved-item handoff cache.
+//
+// cache.ts currently implements the preview/hierarchy TTL with Effect fibers,
+// justified in a code comment as "TestClock-friendly" — a justification no test
+// ever cashed in. These pin the four properties that actually matter, so the
</file context>

@@ -0,0 +1,393 @@
// Characterization tests for the 7 bulk state-change verbs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new characterization tests duplicate the same vi.hoisted mock scaffold, runVerb driver, and frames() helper across all four added files (bulk-state, bulk-rename, bulk-position, bulk-update-fields). Extract the shared mock-setup + runVerb/frames helpers into a common test util (e.g. __tests__/helpers/bulk-test-utils.ts) parameterized by the per-module mocks, so the four files only declare the mocks and cases that differ. This trims ~60 lines of near-identical scaffolding and keeps future bulk-verb tests consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/background/__tests__/bulk-state.test.ts, line 78:

<comment>The new characterization tests duplicate the same vi.hoisted mock scaffold, `runVerb` driver, and `frames()` helper across all four added files (bulk-state, bulk-rename, bulk-position, bulk-update-fields). Extract the shared mock-setup + runVerb/frames helpers into a common test util (e.g. `__tests__/helpers/bulk-test-utils.ts`) parameterized by the per-module mocks, so the four files only declare the mocks and cases that differ. This trims ~60 lines of near-identical scaffolding and keeps future bulk-verb tests consistent.</comment>

<file context>
@@ -0,0 +1,393 @@
+}
+
+/** Drive a verb end-to-end, feeding `processQueue` the supplied progress states. */
+async function runVerb(
+  type: string,
+  data: Record<string, unknown>,
</file context>

The eleven-field frame the background broadcasts to the queue tracker was
written out three times: as broadcastQueue's parameter, as the
queueStateUpdate entry in the ProtocolMap, and — for its first seven fields
— as QueueState in lib/queue. Sender and protocol entry are the two ends of
the same message, so a field added to one and not the other type-checked
fine and silently dropped on the wire.

QueueFrame now extends QueueState and the ProtocolMap entry is that type.
ReverseHint moves to messages.ts alongside it, since it is part of the
payload, and rest-helpers re-exports it so its two consumers are untouched.

QueueState is imported as `import type`, so messages.ts gains no runtime
dependency on lib/queue — and therefore none on the logger or WXT storage,
which its suite does not mock.

rest-helpers 154 -> 133; messages 488 -> 492.
field-handlers still hand-rolled `/^issue:(\d+)$/ || /^issue-(\d+)$/` twice,
in getReorderContext's selection map and its DOM-order re-sort — the third
and fourth copies of a regex 99a09a4 already extracted as
parseIssueDatabaseId and routed bulk-position through.

primerCss.chipButton re-declared buttonMotion verbatim and added two lines.
Both now compose from one BUTTON_MOTION object rather than one preset
overriding the other: makePreset shallow-merges, so an override supplying
its own '&:hover:not(:disabled)' would replace the base rule and drop the
transform — the near-miss recorded on this branch back in 891ad10.
The reorder flyout's "Recent targets" and "All items" labels were the
SectionHeader from bulk-edit-field-row, copied out twice with its nine sx
lines intact. The second copy adds a borderTop, which is now an sx override
on the shared component rather than a reason to fork it.

bulk-reorder-flyout 397 -> 371.
Five copies of the same loop — findDialog, findFooter and readInput in the
create-issue injector, findPanel and findSidebar in the issue-detail one —
each walking a selector list and returning the first hit. Three of them also
carried their own try/catch with their own comment explaining it.

queryFirst in project-table-dom is that loop once, and keeps the per-selector
try/catch with the reason stated properly: the lists contain :has() selectors
that an older engine rejects, and joining them with commas would let one bad
selector take the whole list down.

create-issue-injections 433 -> 409; issue-detail-injections 208 -> 188.
The injector assembled createRoot + StyleSheetManager + isPropValid +
ShadowThemeProvider by hand, and tore it down by id. createLightDomUi in
shadow-ui-factory is that exact sequence, already used by the checkbox
portal host, and it adds the ErrorBoundary this mount never had.

The card's 'margin-bottom: 16px' inline cssText — which the design system
bans — becomes mb: 3 on a Primer Box. The double-mount guard moves from a
hand-assigned element id to the data attribute createLightDomUi sets.

The injector now takes ctx, like the create-issue one next to it, which
already calls createFeatureUi per dialog open, so the per-mount lifecycle is
the pattern the codebase had already settled on.

+6 lines, and styled-components and @emotion/is-prop-valid drop from two
importers to one.
Audited as a candidate for deletion — 231 lines reimplementing show/hide
delay scheduling around a library that ships a `delay` prop — and it earns
every one of them.

@tippyjs/react 4.2.6 (last released 2021, peer react: ">=16.8") clones the
trigger child and calls preserveRef(children.ref, node) at
dist/tippy-react.esm.js:360 and :517. React 19 removed element.ref in favour
of element.props.ref, so that path silently drops the consumer's ref. The
wrapper never hands children to BaseTippy at all: it clones the trigger
itself, reads the ref from children.props.ref, and drives Tippy in
controlled visible mode — which is why the delay scheduling lives here.

Comment only, no behaviour change.
ba598f2 removed thirteen npm scripts as "unreferenced code". Nothing in a
repo ever references an npm script by name — humans and CI invoke them — so
that test deletes the release workflow and reports it as dead code. main
still carries all of them.

Restored: build:chrome/firefox/edge/safari (all four verified building),
zip and zip:safari, and publish:chrome/firefox/edge/safari/all. The zip
removal was also internally inconsistent: zip:chrome/firefox/edge survived
and .github/workflows/release.yml calls them.

Left deleted: `submit`, which shells out to a `webstore` binary that is not
a dependency, so it could not have run. test:coverage-badge stays out too —
.github/workflows/coverage.yml invokes scripts/update-coverage-badge.mjs
directly rather than through the alias.

pnpm test && pnpm install && pnpm typecheck && pnpm build:chrome &&
pnpm build:firefox && pnpm build:edge now completes; safari builds as well.
"[runHandler:getHierarchyData] failed … Item issue-58 not found in project
— it may belong to a different project" fired on essentially every issue
pane opened without clicking a table row first.

Root cause: an item reaches the background under one of two spellings, and
the separator is what says which number it carries.

  issue:4140984079  colon, from data-hovercard-subject-tag  -> databaseId
  issue-58          hyphen, scraped from an /issues/58 href -> issue NUMBER

Both name the same issue — verified on the live board, where #58 is
databaseId 4140984079. But parseIssueDatabaseId collapsed both with
/^issue[:-](\d+)$/ and collectMatchingItems matched the result against
content.databaseId. An issue number never equals a databaseId, so
resolveProjectItemIds returned [] and fetchHierarchyData threw.

The hyphen spelling was unavoidable on that path: extractItemIdFromPanel
only gets the good id from a row carrying data-rgp-active, which is set
solely by clicking an injected row. A page load with ?pane=issue in the
URL, back/forward, or a click landing before injection settles all fall
through to the href-scraping fallback.

parseIssueRef now preserves which number it parsed, and the shared walker
matches databaseId ids against content.databaseId and number ids against
content.number. The resolution query already selected number; the rename
query now does too, so both callers of the walker are fixed.

parseIssueDatabaseId keeps its tolerant behaviour on purpose. Its only
remaining callers are bulk-position and getReorderContext, which match
against content.databaseId and would need `number` added to
GET_PROJECT_ITEMS_FOR_REORDER to be correct. That is pre-existing and rare
— table rows almost always carry the colon spelling — and changing it here
would have meant rewriting a characterization test to fit the fix.

Verified live: the deep link that reproduced the error now renders the card
with real data and logs nothing.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 issues found across 41 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/lib/schemas-decode.ts">

<violation number="1" location="src/lib/schemas-decode.ts:27">
P2: These casts remove the decoder’s runtime `string` check, so malformed API data or an untyped caller can flow as a branded string and fail later during ID operations. Keep a shared `typeof raw === 'string'` check in the string decoders instead of casting directly.</violation>
</file>

<file name="src/lib/errors.test.ts">

<violation number="1" location="src/lib/errors.test.ts:29">
P3: The first test now asserts `expect(a).toEqual(b)` twice in a row — the newly added line duplicates the unchanged line directly beneath it. Given this PR is a deduplication refactor, drop one of the two identical assertions.</violation>
</file>

<file name="src/lib/graphql-client.ts">

<violation number="1" location="src/lib/graphql-client.ts:111">
P2: When `X-RateLimit-Remaining` is present but nonnumeric, this code converts it to zero and retries a 403 permission response three times. Preserve invalid values as missing so only an actual zero remaining count produces `GithubRateLimitError`.

(Based on your team's feedback about distinguishing 403 permission errors from rate limits.)</violation>
</file>

<file name="src/lib/queue.test.ts">

<violation number="1" location="src/lib/queue.test.ts:251">
P2: The test claims to verify that cancelling while the queue is parked in the 60s wait abandons the run, but cancellation is delivered synchronously inside the paused-state broadcast (notify()), before sleep() is entered. The abort signal is already aborted when sleep() starts, so sleep resolves through its `signal.aborted` early-return, and the abort-listener path that actually interrupts an in-flight wait is never exercised. A regression that removes sleep()'s abort listener would still pass this test. Defer the cancellation (e.g. setTimeout(() => cancelQueue(processId), 10)) so the queue is genuinely parked in the 60s wait when the abort lands.</violation>
</file>

<file name="package.json">

<violation number="1" location="package.json:20">
P3: Each `publish:*` script runs the build twice: `wxt zip --browser X` already runs a production build before creating the zip, so the leading `wxt build --browser X &&` before it is redundant and doubles the release build time and dist output churn. Drop the explicit `wxt build` and keep only `wxt zip --browser X` for each `publish:*` script.</violation>
</file>

<file name="src/background/__tests__/relationship-tasks.test.ts">

<violation number="1" location="src/background/__tests__/relationship-tasks.test.ts:124">
P3: This characterization test's stated purpose is to pin 'the exact request sequence each branch emits' so the collapse of the blocked-by/blocking branches is provably equivalent, but the `remove()` mutation path inside the collapsed `run` loop is never exercised — only `add`, `clear`, and skip cases are covered. Since remove differs between the two kinds through `endpointFor` (blocked_by removes by the other issue's database id on the item's repo; blocking removes the item's id from the other issue's repo), add a case for each kind, e.g. a spec with `remove: [issue(5, 505)]` and a stubbed listing that returns that dependency, asserting the DELETE path and id.</violation>
</file>

<file name="src/features/issue-detail-injections.tsx">

<violation number="1" location="src/features/issue-detail-injections.tsx:101">
P2: Each panel swap creates a new `createLightDomUi`, which registers a `ctx.onInvalidated` callback. `unmountCard()` only calls `destroy()`, so callbacks from every previously opened panel accumulate until invalidation; reuse one UI or make the registration unregisterable.</violation>
</file>

<file name="src/lib/queue.ts">

<violation number="1" location="src/lib/queue.ts:96">
P2: When `onStateChange` throws synchronously after a task succeeds, this broad `try` records the callback failure as the task failure and can falsify progress. Catch only `task.run()` so notification and bookkeeping defects propagate instead of marking the mutation failed.</violation>

<violation number="2" location="src/lib/queue.ts:113">
P2: When `cancelQueue` runs while a task is awaiting `task.run()`, the new controller cannot interrupt that promise because tasks receive no signal. `processQueue` remains blocked until the GitHub request resolves, unlike the target implementation; retain an interruptible wrapper or propagate an abort signal into task requests.</violation>
</file>

<file name="src/background/project-helpers.ts">

<violation number="1" location="src/background/project-helpers.ts:170">
P1: When a project contains the same issue number in multiple repositories, this match can resolve the wrong project item because `issue-58` carries no repository identity. Preserve the repository from the DOM link or reject ambiguous number matches before bulk operations target the first result.</violation>
</file>

<file name="src/features/__tests__/bulk-actions-bar.test.tsx">

<violation number="1" location="src/features/__tests__/bulk-actions-bar.test.tsx:470">
P3: The test name promises escape and select-all remain, but the body asserts `['escape']` only — select-all is unregistered once a flyout opens. Rename the test to match the assertion.</violation>

<violation number="2" location="src/features/__tests__/bulk-actions-bar.test.tsx:479">
P3: The test name says "registers nothing", but the assertion expects `['escape', 'select-all']` — two shortcuts are registered. Rename the test to describe what it actually verifies so maintainers don't misread the bar's behavior with an empty selection.</violation>
</file>

<file name="src/features/bulk-edit-field-row.tsx">

<violation number="1" location="src/features/bulk-edit-field-row.tsx:11">
P2: The new `sx` prop is typed as `Record<string, unknown>`, which discards Primer's system-prop type safety: callers can pass invalid keys or values (e.g. `fontWeight: 123`) with no compile error, and editor autocomplete for prop keys is lost. The codebase already uses Primer's `BetterSystemStyleObject` for exactly this (pass-through sx overrides) in `src/lib/primer-css-helper.ts` and `src/ui/modal-shell.tsx`. Type the prop as `BetterSystemStyleObject` to match convention and keep callers type-checked.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

)
remainingDbIds.delete(content.databaseId)
}
if (content.number !== undefined && remainingNumbers.has(content.number)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a project contains the same issue number in multiple repositories, this match can resolve the wrong project item because issue-58 carries no repository identity. Preserve the repository from the DOM link or reject ambiguous number matches before bulk operations target the first result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/background/project-helpers.ts, line 170:

<comment>When a project contains the same issue number in multiple repositories, this match can resolve the wrong project item because `issue-58` carries no repository identity. Preserve the repository from the DOM link or reject ambiguous number matches before bulk operations target the first result.</comment>

<file context>
@@ -117,19 +157,32 @@ async function collectMatchingItems<TContent extends { databaseId: number }, TOu
+        )
+        remainingDbIds.delete(content.databaseId)
+      }
+      if (content.number !== undefined && remainingNumbers.has(content.number)) {
+        results.push(collect({ id: item.id, content }, wanted.byNumber.get(content.number)!))
+        remainingNumbers.delete(content.number)
</file context>

Comment thread src/lib/schemas-decode.ts
export const decodeRepoOwner: (raw: string) => RepoOwner = Schema.decodeSync(RepoOwner)
export const decodeRepoName: (raw: string) => RepoName = Schema.decodeSync(RepoName)

export const decodeProjectItemId = (raw: string): ProjectItemId => raw as ProjectItemId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These casts remove the decoder’s runtime string check, so malformed API data or an untyped caller can flow as a branded string and fail later during ID operations. Keep a shared typeof raw === 'string' check in the string decoders instead of casting directly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/schemas-decode.ts, line 27:

<comment>These casts remove the decoder’s runtime `string` check, so malformed API data or an untyped caller can flow as a branded string and fail later during ID operations. Keep a shared `typeof raw === 'string'` check in the string decoders instead of casting directly.</comment>

<file context>
@@ -16,23 +14,33 @@ import {
-export const decodeRepoOwner: (raw: string) => RepoOwner = Schema.decodeSync(RepoOwner)
-export const decodeRepoName: (raw: string) => RepoName = Schema.decodeSync(RepoName)
+
+export const decodeProjectItemId = (raw: string): ProjectItemId => raw as ProjectItemId
+
+export const decodeProjectItemDomId = (raw: string): ProjectItemDomId => raw as ProjectItemDomId
</file context>

Comment thread src/lib/graphql-client.ts
const retryAfter = numericHeader(res.headers.get('retry-after'))
const rateLimitRemainingHeader = res.headers.get('x-ratelimit-remaining')
const rateLimitRemaining =
rateLimitRemainingHeader === null ? null : numericHeader(rateLimitRemainingHeader)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When X-RateLimit-Remaining is present but nonnumeric, this code converts it to zero and retries a 403 permission response three times. Preserve invalid values as missing so only an actual zero remaining count produces GithubRateLimitError.

(Based on your team's feedback about distinguishing 403 permission errors from rate limits.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/graphql-client.ts, line 111:

<comment>When `X-RateLimit-Remaining` is present but nonnumeric, this code converts it to zero and retries a 403 permission response three times. Preserve invalid values as missing so only an actual zero remaining count produces `GithubRateLimitError`.

(Based on your team's feedback about distinguishing 403 permission errors from rate limits.) </comment>

<file context>
@@ -1,32 +1,171 @@
+    const retryAfter = numericHeader(res.headers.get('retry-after'))
+    const rateLimitRemainingHeader = res.headers.get('x-ratelimit-remaining')
+    const rateLimitRemaining =
+      rateLimitRemainingHeader === null ? null : numericHeader(rateLimitRemainingHeader)
+
+    logger.error('HTTP error', { op, status: res.status, retryAfter })
</file context>
Suggested change
rateLimitRemainingHeader === null ? null : numericHeader(rateLimitRemainingHeader)
rateLimitRemainingHeader !== null && /^\d+$/.test(rateLimitRemainingHeader)
? Number(rateLimitRemainingHeader)
: null

Comment thread src/lib/queue.test.ts
(s) => {
if (s.paused && !cancelled) {
cancelled = true
cancelQueue(processId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The test claims to verify that cancelling while the queue is parked in the 60s wait abandons the run, but cancellation is delivered synchronously inside the paused-state broadcast (notify()), before sleep() is entered. The abort signal is already aborted when sleep() starts, so sleep resolves through its signal.aborted early-return, and the abort-listener path that actually interrupts an in-flight wait is never exercised. A regression that removes sleep()'s abort listener would still pass this test. Defer the cancellation (e.g. setTimeout(() => cancelQueue(processId), 10)) so the queue is genuinely parked in the 60s wait when the abort lands.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/queue.test.ts, line 251:

<comment>The test claims to verify that cancelling while the queue is parked in the 60s wait abandons the run, but cancellation is delivered synchronously inside the paused-state broadcast (notify()), before sleep() is entered. The abort signal is already aborted when sleep() starts, so sleep resolves through its `signal.aborted` early-return, and the abort-listener path that actually interrupts an in-flight wait is never exercised. A regression that removes sleep()'s abort listener would still pass this test. Defer the cancellation (e.g. setTimeout(() => cancelQueue(processId), 10)) so the queue is genuinely parked in the 60s wait when the abort lands.</comment>

<file context>
@@ -213,6 +213,57 @@ describe('processQueue', () => {
+        (s) => {
+          if (s.paused && !cancelled) {
+            cancelled = true
+            cancelQueue(processId)
+          }
+        },
</file context>

// light DOM, not shadow: the card sits inside GitHub's own sidebar and
// inherits its layout. createLightDomUi brings the StyleSheetManager,
// ThemeProvider and ErrorBoundary this used to assemble by hand.
currentUi = createLightDomUi(ctx, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Each panel swap creates a new createLightDomUi, which registers a ctx.onInvalidated callback. unmountCard() only calls destroy(), so callbacks from every previously opened panel accumulate until invalidation; reuse one UI or make the registration unregisterable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/issue-detail-injections.tsx, line 101:

<comment>Each panel swap creates a new `createLightDomUi`, which registers a `ctx.onInvalidated` callback. `unmountCard()` only calls `destroy()`, so callbacks from every previously opened panel accumulate until invalidation; reuse one UI or make the registration unregisterable.</comment>

<file context>
@@ -108,34 +93,35 @@ function mountCard(panel: Element, projectContext: ProjectContext): void {
+  // light DOM, not shadow: the card sits inside GitHub's own sidebar and
+  // inherits its layout. createLightDomUi brings the StyleSheetManager,
+  // ThemeProvider and ErrorBoundary this used to assemble by hand.
+  currentUi = createLightDomUi(ctx, {
+    name: UI_NAME,
+    anchor: sidebar,
</file context>

Comment thread src/lib/errors.test.ts

expect(Equal.equals(a, b)).toBe(true)
expect(a).toEqualValue(b)
expect(a).toEqual(b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The first test now asserts expect(a).toEqual(b) twice in a row — the newly added line duplicates the unchanged line directly beneath it. Given this PR is a deduplication refactor, drop one of the two identical assertions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/errors.test.ts, line 29:

<comment>The first test now asserts `expect(a).toEqual(b)` twice in a row — the newly added line duplicates the unchanged line directly beneath it. Given this PR is a deduplication refactor, drop one of the two identical assertions.</comment>

<file context>
@@ -23,31 +22,29 @@ describe('GithubRateLimitError (canonical 429 variant)', () => {
     const b = new GithubRateLimitError({ status: 429, message: 'Too Many', retryAfter: 30 })
 
-    expect(Equal.equals(a, b)).toBe(true)
+    expect(a).toEqual(b)
     expect(a).toEqual(b)
   })
</file context>

Comment thread package.json
Comment on lines 20 to 22
"publish:chrome": "wxt build --browser chrome && wxt zip --browser chrome",
"publish:firefox": "wxt build --browser firefox && wxt zip --browser firefox",
"publish:edge": "wxt build --browser edge && wxt zip --browser edge",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Each publish:* script runs the build twice: wxt zip --browser X already runs a production build before creating the zip, so the leading wxt build --browser X && before it is redundant and doubles the release build time and dist output churn. Drop the explicit wxt build and keep only wxt zip --browser X for each publish:* script.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At package.json, line 20:

<comment>Each `publish:*` script runs the build twice: `wxt zip --browser X` already runs a production build before creating the zip, so the leading `wxt build --browser X &&` before it is redundant and doubles the release build time and dist output churn. Drop the explicit `wxt build` and keep only `wxt zip --browser X` for each `publish:*` script.</comment>

<file context>
@@ -6,11 +6,22 @@
     "zip:firefox": "wxt zip --browser firefox",
     "zip:edge": "wxt zip --browser edge",
+    "zip:safari": "wxt zip --browser safari",
+    "publish:chrome": "wxt build --browser chrome && wxt zip --browser chrome",
+    "publish:firefox": "wxt build --browser firefox && wxt zip --browser firefox",
+    "publish:edge": "wxt build --browser edge && wxt zip --browser edge",
</file context>

])
})

it('clears by deleting every currently listed dependency', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This characterization test's stated purpose is to pin 'the exact request sequence each branch emits' so the collapse of the blocked-by/blocking branches is provably equivalent, but the remove() mutation path inside the collapsed run loop is never exercised — only add, clear, and skip cases are covered. Since remove differs between the two kinds through endpointFor (blocked_by removes by the other issue's database id on the item's repo; blocking removes the item's id from the other issue's repo), add a case for each kind, e.g. a spec with remove: [issue(5, 505)] and a stubbed listing that returns that dependency, asserting the DELETE path and id.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/background/__tests__/relationship-tasks.test.ts, line 124:

<comment>This characterization test's stated purpose is to pin 'the exact request sequence each branch emits' so the collapse of the blocked-by/blocking branches is provably equivalent, but the `remove()` mutation path inside the collapsed `run` loop is never exercised — only `add`, `clear`, and skip cases are covered. Since remove differs between the two kinds through `endpointFor` (blocked_by removes by the other issue's database id on the item's repo; blocking removes the item's id from the other issue's repo), add a case for each kind, e.g. a spec with `remove: [issue(5, 505)]` and a stubbed listing that returns that dependency, asserting the DELETE path and id.</comment>

<file context>
@@ -0,0 +1,200 @@
+    ])
+  })
+
+  it('clears by deleting every currently listed dependency', async () => {
+    hoisted.rest.mockResolvedValueOnce([
+      {
</file context>

])
})

it('keeps only escape and select-all while an overlay is open', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The test name promises escape and select-all remain, but the body asserts ['escape'] only — select-all is unregistered once a flyout opens. Rename the test to match the assertion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/__tests__/bulk-actions-bar.test.tsx, line 470:

<comment>The test name promises escape and select-all remain, but the body asserts `['escape']` only — select-all is unregistered once a flyout opens. Rename the test to match the assertion.</comment>

<file context>
@@ -294,3 +304,182 @@ describe('BulkActionsBar — overlay state', () => {
+    ])
+  })
+
+  it('keeps only escape and select-all while an overlay is open', async () => {
+    const m = renderBar()
+    await click(el(m, 'rgp-bar-mark-chip'))
</file context>

expect(registeredTable().map((s) => s.id)).toEqual(['escape'])
})

it('registers nothing while the selection is empty', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The test name says "registers nothing", but the assertion expects ['escape', 'select-all'] — two shortcuts are registered. Rename the test to describe what it actually verifies so maintainers don't misread the bar's behavior with an empty selection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/__tests__/bulk-actions-bar.test.tsx, line 479:

<comment>The test name says "registers nothing", but the assertion expects `['escape', 'select-all']` — two shortcuts are registered. Rename the test to describe what it actually verifies so maintainers don't misread the bar's behavior with an empty selection.</comment>

<file context>
@@ -294,3 +304,182 @@ describe('BulkActionsBar — overlay state', () => {
+    expect(registeredTable().map((s) => s.id)).toEqual(['escape'])
+  })
+
+  it('registers nothing while the selection is empty', () => {
+    hoisted.selection.clear()
+    renderBar()
</file context>

ed43dff fixed the shared resolver but left bulk-position and
getReorderContext matching every id against content.databaseId, so a
hyphen-spelled id there resolved to nothing — or, worse, to an unrelated
item that happened to have that databaseId. The test added below proves the
wrong-item case: before this change, selecting `issue-20` built a move task
for the item whose contentDbId is 20.

GET_PROJECT_ITEMS_FOR_REORDER now selects `number`, and both call sites use
a shared createIssueRefIndex: items go in indexed by databaseId AND by issue
number, lookups go through parseIssueRef, and the spelling picks the map.

This also removes duplication rather than adding any. bulk-position kept two
parallel maps (contentDbToMemex, contentDbToNode) plus a third lookup for the
insertion target; getReorderContext kept selectedDbIdMap and
contentDbIdToEntry and assembled selectedItems inside the pagination loop.
All five collapse into one index per call site, and selectedItems becomes a
flatMap over the caller's own ids — which also fixes its ordering, since it
previously came out in project-page order rather than selection order.

parseIssueDatabaseId is gone: it had no callers left, and its whole purpose
was to answer a question the caller could not ask correctly.

The characterization test that asserted "accepts both issue:N and issue-N
spellings" is replaced by four that pin the real contract. Its fixture used
contentDbId 10/20/30, small enough for an issue number to collide with a
databaseId; the new fixture gives each item a databaseId and a number that
differ, as they do in production.

Verified live: moved a row to the top and back via Custom position. Both
moves landed, the board order is unchanged, console clean.
`Pat` was referenced nowhere. `IssueRef` and `parseIssueRef` are used only
inside project-helpers.ts, so they no longer need to be exported.
concurrency.ts carried a live TODO to adopt `Effect.Semaphore`, a dependency
that was deliberately deleted. cache.ts narrated its old fiber-based eviction,
which git already records. The three test comments explained timer behaviour
via `Effect.sleep`; the real mechanism is a plain setTimeout, and the client's
retry awaits its own `sleep`.
AGENTS.md told agents `pnpm typecheck` was the only automated validation.
There are 48 vitest files and 632 cases; CI just doesn't gate them.
nearestUpcoming and nextAfter were the same filter-sort-first with different
predicates, and four sites built the same UTC-midnight Date by hand.
The same six-field parent mapping was hand-copied at three sites across two
handler modules.
The panel and the group-header widget each declared the same six-state union,
the same four useState calls, the same getSprintStatus fetch with the same
four-branch derivation, and a byte-identical acknowledge handler. They render
side by side, so two copies of that machine could disagree about which sprint
is active.
The popup and the options card each rendered the same danger Flash (title,
message, action link, dismiss) and the same nine-line input sx with the same
error-colour conditional. The popup keeps its tighter type scale via `dense`.

Left the FormControl/TextInput itself duplicated: sharing it needed three
props for two call sites and measured net -1 line.
CardContent asked hasProjectBlock, which built the meta, then ProjectBlock
built the same meta again. Both field lookups already require the value in
their predicate, so a truthiness sweep over the meta is the same test the
nine-term disjunction was making.
The elapsed/remaining/await block was written identically in the fetch's then
and catch arms.
iterationEndDate is start + duration, i.e. exclusive, which is what
isActive, nextAfter and daysLeft need. The four render sites printed that
value directly, so every range read one day later than GitHub's own —
"Aug 19 – Sep 2" against GitHub's "Aug 19 - Sep 01".

fmtRange steps back a day for display only; the exclusive semantics and
the message payload are untouched. It also collapses the four hand-copied
`fmt(start) – fmt(end)` renders into one call.
SprintState was exported with no consumer; prefixLabelIcon was imported
into the duplicate modal but only used in its four sibling modules; three
test files imported hooks they never called.

bulk-delete-modal's `itemTitles ?? []` allocated a fresh array every
render, so the useMemo keyed on it never hit.

Lint: 35 -> 30 warnings, still 0 errors.
@primer/react declares @primer/live-region-element as a regular
dependency and resolves its own 0.7.2, so our direct 0.8.0 was a second
copy nothing consumed. Every import of the specifier is aliased to
src/lib/primer-live-region-stub.ts in wxt.config.ts — globally, including
Primer's own internal imports — so neither version was ever bundled.

The stub and vitest's deps.inline entry stay: the stub is what the alias
points at, and the transitive 0.7.2 still resolves under tests.

Verified: content.js has zero `customElements` references before and
after, and background.js is byte-identical.
Eleven sites hand-copied the same tracker frame — total, completed 0,
paused false, a status string, and the run's processId/label/tabId. One of
them, bulk-position, had already generalised it into a local
`broadcastStart` that simply never moved to a shared home.

broadcastStatus lives in queue-run.ts beside broadcastDone, not in
rest-helpers, because the bulk suites mock rest-helpers wholesale — a
helper added there would be undefined in those tests. All 639 cases pass
with every test file untouched.

sprint-handlers and duplicate-handlers gained a QueueRun object on the way
through; both were already rebuilding that shape inline for
runQueueWithProgress and broadcastDone.
The duplicate modal's Parent section and the blocked-by/blocking list
carried byte-identical copies of the same card — eleven sx props, a bold
title, a muted reference and an invisible remove button. The Parent
section simply never routed through the module that already existed for
this shape.

Worth about ten lines; the point is that the parent card and the
relationship rows can no longer drift apart.
fetchItemPreviewData and fetchHierarchyData each carried their own copy of
the id-resolution guard and a byte-identical blocked_by/blocking fetch.
The id paths are what the two hyphen-spelling fixes on this branch had to
correct, so routing both through resolveItemNodeId is the point rather
than the thirteen lines.

Concurrency is unchanged: loadBlockingPair starts both reads in the same
tick, so the hierarchy lookup still has all three requests in flight
together.

Error strings are preserved exactly, including the two that already
matched between the copies.
packageManager is pnpm@11.5.0, not 10.32.1. The suite is at 639 cases.
Z_MODAL's comment named Primer's Dialog, which nothing in this codebase
uses — modals compose primerCss.modalOverlay/modalPanel instead.

The design-system checklists carrying the same stale Dialog claim live in
untracked agent-instruction files and were corrected there too.
@ecc-tools

ecc-tools Bot commented Aug 22, 2026

Copy link
Copy Markdown

Analyzing 200 commits...

@ecc-tools

ecc-tools Bot commented Aug 22, 2026

Copy link
Copy Markdown

Analysis Complete

Generated ECC bundle from 62 commits | Confidence: 90%

View Pull Request #64

Repository Profile
Attribute Value
Language TypeScript
Framework React
Commit Convention conventional
Test Directory colocated
Changed Files (158)
Metric Value
Files changed 158
Additions 5751
Deletions 6974

Top hotspots

Path Status +/-
src/features/bulk-duplicate-modal.tsx modified +192 / -592
src/lib/schemas-messages.ts removed +0 / -688
src/background/bulk-state.ts modified +87 / -471
src/features/__tests__/bulk-actions-bar.test.tsx added +485 / -0
src/features/bulk-actions-bar.tsx modified +177 / -308

Top directories

Directory Files Total changes
src/lib 53 3527
src/background 25 3078
src/features 41 2588
src/background/__tests__ 7 1875
src/ui 12 702
Analysis Depth Readiness (evidence-backed, 43%)

ECC Tools uses this to decide whether recommendations should stay at commit-history/setup guidance or expand into CI, security, harness, reference-set, AI-routing, and team backlog work.

Area Status Evidence / Next Step
Commit history Ready 62 commits sampled
CI/CD signals Ready .github/workflows/coverage.yml, vitest.config.ts
Security evidence Missing Add AgentShield, audit, SARIF, SBOM, or security review evidence so recommendations can cover security posture.
Harness configuration Missing Add Claude, Codex, OpenCode, Zed, dmux, MCP, plugin, or cross-harness config evidence for harness-agnostic recommendations.
Reference/eval evidence Missing Add fixtures, golden traces, reference sets, or evaluator benchmarks so deeper recommendations have regression evidence.
AI routing and cost controls Ready src/features/token-setup.tsx
Team handoff and project tracking Missing Add roadmap, runbook, project, Linear, or follow-up tracking docs so generated work can land in a team queue.
Reference Set Readiness (0/7, 0%)
Area Status Evidence / Next Step
Deep analyzer corpus Missing Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions.
RAG/evaluator comparison Missing Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior.
PR salvage/review corpus Missing Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation.
Discussion triage corpus Missing Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications.
Harness compatibility Missing Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces.
Security evidence Missing Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs.
CI failure-mode evidence Missing Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes.
Likely Future Issues (5)
Severity Signal Why it may show up
MEDIUM Runtime config changes may ship without example or template updates 1 runtime config paths changed; 0 example or template config files changed
HIGH Async job or webhook changes may ship without reliability coverage 1 async surface paths changed; 0 async-focused integration or e2e tests changed
MEDIUM User-facing UI changes may ship without browser coverage 12 user-facing UI paths changed; 0 browser or e2e coverage files changed
MEDIUM Cost or token-risk changes may ship without budget evidence 1 cost/token-risk paths changed; 0 budget, usage, or cost validation artifacts changed
MEDIUM CI workflow changes may ship without failure-mode evidence 2 CI/test-runner paths changed; 0 CI failure-mode evidence artifacts changed
  • Runtime config changes may ship without example or template updates: The PR changes runtime config or deployment settings but does not update any obvious example env file or config template.
  • Async job or webhook changes may ship without reliability coverage: The PR changes queue, worker, cron, or webhook-sensitive files without touching any obvious async-focused integration or end-to-end tests.
  • User-facing UI changes may ship without browser coverage: The PR changes components, pages, or other user-facing UI files without touching any obvious browser or end-to-end coverage.
  • Cost or token-risk changes may ship without budget evidence: The PR changes AI routing, usage, token budget, or model-call surfaces without touching obvious budget, usage-limit, or cost regression evidence.
  • CI workflow changes may ship without failure-mode evidence: The PR changes CI workflows or test-runner entrypoints without touching CI failure fixtures, captured logs, troubleshooting notes, or regression evidence.
Suggested Follow-up Work (5)
Type Suggested title Targets
PR chore: sync config templates for vitest.config.ts vitest.config.ts
PR test: add async coverage for src/lib/queue.ts src/lib/queue.ts
PR test: add browser coverage for src/ui/bulk-flyout.tsx + src/ui/icons.tsx src/ui/bulk-flyout.tsx, src/ui/icons.tsx
PR test: add budget evidence for src/features/token-setup.tsx src/features/token-setup.tsx
PR ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts .github/workflows/coverage.yml, vitest.config.ts
  • chore: sync config templates for vitest.config.ts: Backfill example env files or config templates before a fresh setup drifts from the shipped runtime surface.
  • test: add async coverage for src/lib/queue.ts: Backfill async reliability coverage before another queue, worker, or webhook change lands on the touched surface.
  • test: add browser coverage for src/ui/bulk-flyout.tsx + src/ui/icons.tsx: Backfill browser coverage before another user-facing UI change lands on the touched surface.
  • test: add budget evidence for src/features/token-setup.tsx: Backfill cost, token, or usage-limit validation before another model-routing change lands on the touched surface.
  • ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts: Backfill CI failure-mode evidence before another workflow or test-runner change lands on the touched surface.

Copy-ready bodies

chore: sync config templates for vitest.config.ts

## Summary
- Update the example env files, sample configs, or deployment templates that should mirror the changed runtime configuration surface.

## Why
- Backfill example env files or config templates before a fresh setup drifts from the shipped runtime surface.

## Touched paths
- `vitest.config.ts`

## Validation
- Update the repo example env file or config template that should reflect the new runtime settings.
- Run the setup, boot, or deployment validation flow that depends on the changed config surface.

test: add async coverage for src/lib/queue.ts

## Summary
- Add async reliability coverage for the recently changed queue, worker, cron, or webhook surface.

## Why
- Backfill async reliability coverage before another queue, worker, or webhook change lands on the touched surface.

## Touched paths
- `src/lib/queue.ts`

## Validation
- Add or extend integration / e2e coverage for the changed queue, worker, cron, or webhook behavior.
- Exercise retries, idempotency, failure handling, or equivalent async boundary cases.

test: add browser coverage for src/ui/bulk-flyout.tsx + src/ui/icons.tsx

## Summary
- Add browser or end-to-end coverage for the recently changed user-facing surface.

## Why
- Backfill browser coverage before another user-facing UI change lands on the touched surface.

## Touched paths
- `src/ui/bulk-flyout.tsx`
- `src/ui/icons.tsx`

## Validation
- Add or extend browser / e2e coverage for the changed component, page, or flow.
- Exercise the visible user journey that depends on the touched UI surface.

test: add budget evidence for src/features/token-setup.tsx

## Summary
- Add budget or usage-limit validation for the recently changed AI routing or model-call surface.

## Why
- Backfill cost, token, or usage-limit validation before another model-routing change lands on the touched surface.

## Touched paths
- `src/features/token-setup.tsx`

## Validation
- Add or extend budget, token, usage-limit, or model-routing regression coverage for the changed path.
- Verify the route still enforces plan limits, retry caps, fallback behavior, or explicit cost controls.

ci: add failure-mode evidence for .github/workflows/coverage.yml + vitest.config.ts

## Summary
- Add CI failure-mode evidence for the recently changed workflow or test-runner surface.

## Why
- Backfill CI failure-mode evidence before another workflow or test-runner change lands on the touched surface.

## Touched paths
- `.github/workflows/coverage.yml`
- `vitest.config.ts`

## Validation
- Add or update a CI failure fixture, captured failing log, troubleshooting note, workflow dry-run evidence, or regression test for the changed CI/test-runner behavior.
- Run the affected workflow or test-runner entrypoint locally or in CI and record pass/fail evidence.
Review Activity (5 reviews, 19 inline comments, 19 unresolved threads)
Signal Count
Approvals 0
Change requests 0
Comment-only reviews 2
Dismissed reviews 0
Pending reviews 0
Review threads 19
Unresolved threads 19
Outdated threads 0
Latest review Commented
Latest submitted at 2026-08-16T22:31:32Z

Top unresolved thread files

Path Unresolved Outdated
src/features/__tests__/bulk-actions-bar.test.tsx 3 0
src/lib/queue.ts 2 0
package.json 1 0
src/background/__tests__/bulk-state.test.ts 1 0
src/background/__tests__/cache.test.ts 1 0

Latest reviewer states

Reviewer State Submitted
@cubic-dev-ai[bot] Commented 2026-08-16T22:31:32Z
@github-actions[bot] Commented 2026-08-08T00:58:40Z
Review Follow-up Signals (2)
Severity Signal Evidence
HIGH Clear unresolved review threads 19 unresolved review thread(s) remain
MEDIUM Get an explicit approval No approving review is recorded for this PR

Recommended next actions

  • Resolve, reply to, or explicitly carry forward each unresolved thread before treating the PR as review-complete.
  • Ask for an approval after requested changes and unresolved discussions are addressed.
  • Prioritize src/features/__tests__/bulk-actions-bar.test.tsx (3 unresolved thread(s), 0 outdated).
  • Prioritize src/lib/queue.ts (2 unresolved thread(s), 0 outdated).
  • Prioritize package.json (1 unresolved thread(s), 0 outdated).
Detected Workflows (1)
Workflow Description
refactoring Code refactoring and cleanup workflow
Generated Instincts (12)
Domain Count
git 2
code-style 3
architecture 1
testing 5
workflow 1

After merging, import with:

/instinct-import .claude/homunculus/instincts/inherited/refined-github-projects-instincts.yaml

Files

  • .claude/ecc-tools.json
  • .claude/skills/refined-github-projects/SKILL.md
  • .agents/skills/refined-github-projects/SKILL.md
  • .agents/skills/refined-github-projects/agents/openai.yaml
  • .claude/identity.json
  • .codex/config.toml
  • .codex/AGENTS.md
  • .codex/agents/explorer.toml
  • .codex/agents/reviewer.toml
  • .codex/agents/docs-researcher.toml
  • .claude/homunculus/instincts/inherited/refined-github-projects-instincts.yaml
  • .claude/commands/refactoring.md

ECC Tools | Everything Claude Code

@fathiraz
fathiraz merged commit f95280f into main Aug 22, 2026
5 checks passed
@fathiraz
fathiraz deleted the chore/ponytail-cleanup branch August 22, 2026 12:57
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