Skip to content

fix(inbox): name only the board scope the Inbox list actually applies - #2580

Merged
Chris0Jeky merged 4 commits into
mainfrom
issue-1984/inbox-scope-truth
Sep 5, 2026
Merged

fix(inbox): name only the board scope the Inbox list actually applies#2580
Chris0Jeky merged 4 commits into
mainfrom
issue-1984/inbox-scope-truth

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

The Paper Inbox told the reader it had applied a column filter that it never applied. With a route query of ?boardId=B&columnId=C the scope chip read Board: {board} · Column: {column}, while useInboxOrchestrator.loadInboxInternal calls captureStore.fetchItems({ limit: 200, boardId }) with no column key at all. The scoped empty state interpolated the same label, so an empty result also blamed a filter that was never applied. The board column's + capture control fed that untruth: it pushed { boardId, columnId } and named itself "Capture a note into Inbox for {column}", promising a column-scoped capture that nothing delivers.

This PR removes the claim rather than building the contract. The chip and the empty state now name only the board, which is the scope the list request actually applies, and the + capture control carries and promises only what the Inbox can honour.

Nothing about honouring a column end to end is decided here. CaptureListQuery has no column key, CreateCaptureItemDto has no ColumnId, and triage targets the board's default column, so a real column contract needs a product ruling and its own slice.

Refs #1984 — not Closes. This lands finding 2 only. Finding 1 is struck as superseded by #2005, and the "honour columnId end-to-end" branch of the issue stays open pending that product ruling.

Changes

Inbox side:

  • src/views/paper/PaperInboxView.vuescopeLabel always renders inbox.scope.board. activeColumnId and activeColumnName are no longer destructured from the orchestrator because the view no longer has a truthful use for them.
  • src/locales/{en,it,es}/inbox.ts — the inbox.scope.boardAndColumn key is deleted in all three locales. It had no remaining caller and no truthful one.

Board side:

  • src/views/paper/PaperBoardView.vueopenCapture pushes { boardId } only.
  • src/locales/{en,it,es}/boardDetail.tsboardDetail.card.captureAria is reworded away from "Capture a note into Inbox for {column}". Round 2 below carries the final wording, which drops the placeholder entirely.

src/composables/useInboxOrchestrator.ts is deliberately untouched, but the two halves of that are not equal. clearScope's delete query.columnId still does real work: a bookmarked or hand-written URL that carries a column is still cleaned up. activeColumnId and activeColumnName, by contrast, now have no production consumer at all — a repo-wide grep finds them only in the orchestrator itself, its own spec, and the stub inside PaperInboxView.spec.ts. They are left in place rather than removed here because removing them is a separate slice with its own spec churn, and it is covered by the tracked follow-up issue #2589.

Specs:

  • src/tests/views/paper/inbox/PaperInboxScopeTruth.spec.ts (new) — mounts PaperInboxView over the REAL orchestrator with only the route, capture store, board store, session store and boards API stubbed, and asserts the rendered chip text and the outgoing fetchItems argument in one test. Neither existing spec could hold both halves: PaperInboxView.spec.ts replaces the orchestrator with a stub, and useInboxOrchestrator.spec.ts mocks the vue module itself so nothing can be mounted there. It sits under views/paper/inbox/ so the region's standing verification glob already covers it.
  • src/tests/views/paper/PaperInboxView.spec.ts — the pin that required Column: Ready in the chip is rewritten to require its absence, and a new test asserts the scoped empty state names only the board while the route still carries a column.
  • src/tests/views/paper/PaperBoardManagement.spec.ts — the + capture query pin is rewritten to the board-only query, and a new test pins the accessible name.
  • src/tests/composables/useInboxOrchestrator.spec.ts — a characterization pin that a columnId in the route still produces a board-only request. It was green before this PR by design: it records the fact the copy was contradicting.

Two previously green pins were rewritten on purpose. Both asserted the untruthful copy, so the suite was defending the defect.

Test plan

Verified in frontend/taskdeck-web at head bb35651fa:

  • npx vitest --run --maxWorkers=2 src/tests/views/paper/PaperInboxView.spec.ts src/tests/views/paper/inbox src/tests/views/paper/PaperBoardManagement.spec.ts src/tests/composables/useInboxOrchestrator.spec.ts src/tests/store/captureStore.spec.ts src/tests/i18n/catalogs.spec.ts src/tests/views/InboxView.spec.ts — 12 files, 471 tests passed. Baseline on origin/main at 46fb41d53 was 11 files, 466 tests passed, so this is +1 file and +5 tests with no test lost.
  • Red first: before the source changes, the same command showed 5 failures — the new coupled chip/request test, the two rewritten pins and the two new pins. The coupled test failed on exactly the defect: expected 'Board: Payments API Migration · Colum…' not to contain 'Column', while its fetchItems assertion already passed.
  • npm run typecheck — clean.
  • npx eslint over all 12 changed files — clean.
  • npm run build — succeeded.
  • git diff --check — clean.
  • src/tests/i18n/catalogs.spec.ts is inside the run above and is green, so ADR-0054 key, placeholder-set and plural-form-count parity hold across en, it and es.

NOT verified:

  • Playwright and any browser-level check. No E2E was run and no stack was started. Greps show no E2E or visual spec asserts the scope chip, the removed key or the capture control's accessible name, but that is a grep, not a run.
  • The it and es wording is a translation judgement, not a native-speaker review. Structure is machine-checked; register is not.
  • No backend change, so no backend test was run.
  • The full frontend unit suite was not run, only the region's verification glob.

Boundaries and risks

Choice made for the + capture affordance — both halves, not one. The issue's reconciliation allowed rewording the aria string OR dropping columnId from the pushed query. Doing only one leaves half the promise standing: dropping the query alone leaves the control still announcing itself as capturing "for {column}", and rewording alone leaves an inert columnId in the route that the next reader will assume means something. So the control now pushes { boardId } and its accessible name was reworded too. Round 1 kept the column in that name as "from {column}"; review showed that was still wrong, because the controls are now functionally identical, and Round 2 below replaces it with a board-level name carrying no column at all.

No capture-destination line was added, and this is the one place this PR departs from the residual's literal wording. The residual allowed the column to survive as a "separately labelled capture-destination line" if columnId stayed in the route. It is not a capture destination. Nothing in the pipeline reads it, and triage targets the board's default column, so such a line would replace one untruth with a fresher one. Since the pushed query no longer carries a column either, the case does not arise for any in-app navigation, and a hand-written URL now simply shows the board scope. If the maintainer wants the column disclosed as a destination, that needs the real contract first, which is the open half of #1984.

Risk that a link with ?columnId= is now inert. It always was. The only behaviour that changes for such a URL is that the chip and empty state stop naming the column; the rows returned are identical, because they were always board-only.

Risk of removing a locale key. inbox.scope.boardAndColumn had exactly one caller, which this PR deletes. Greps over src/ and tests/ find no other reference, and the catalogs parity spec passes with the key gone from all three locales.

Deliberately out of scope: the composer (finding 1, superseded by #2005), PaperScopeDisclosure.vue (shared with views/paper/review/ReviewQueueRail.vue, so its single-label prop is left alone), the Legacy skin, useInboxOrchestrator.ts, and every backend file.

Round 2

Head 6d08c38a1c14b914f1fcf0541a3b32d76ddf2ceb. Three review items taken, two declined by the reviewer.

MEDIUM — the accessible name still named the column. Rewording to "from {column}" left N functionally identical controls carrying N different names. openCapture now pushes { boardId } and nothing else, exactly like the board-level "Capture here" (PaperBoardView.vue), so a screen-reader user heard "from Backlog" / "from Today" / "from Done" for one behaviour, announcing a distinction the system does not make.

boardDetail.card.captureAria therefore drops its placeholder entirely and states the board-level truth:

  • en: Capture a note into this board's Inbox
  • it: Prendi un appunto nell'Inbox di questa bacheca
  • es: Toma una nota en el Inbox de este tablero

The placeholder set changed from {column} to none in all three locales together, so parity holds. boardDetail.card.addAria keeps its {column}, because adding a card really does differ per column. The board name was not used instead: PaperBoardColumn has no board-name prop, and adding one would mean threading it through PaperBoardView's template for no gain in truth.

The spec was rewritten red first. It now asserts all three capture controls carry the same accessible name and that addAria still carries its column. PaperInboxScopeTruth.spec.ts was named in the review as also holding an aria assertion; it does not — it asserts only the chip text and the fetchItems shape, so nothing there needed changing.

Body correction. The paragraph claiming useInboxOrchestrator.ts was left untouched "so activeColumnId and clearScope's delete still handle a hand-written URL" was half wrong and has been rewritten above. clearScope's delete does real work; activeColumnId and activeColumnName now have no production consumer, appearing only in the orchestrator, its spec and the PaperInboxView.spec.ts stub. Removing them is a separate slice, tracked in a follow-up issue.

LOW — the journey doc recorded a link the app can no longer emit. docs/testing/HORIZON_JOURNEY.md Step 20 and residual row 3d record the + CAPTURE deep link as /workspace/inbox?boardId=...&columnId=.... Both now carry a one-line dated addendum (2026-09-05, PR #2580) saying the link is board-only and the column branch stays open on #1984. The historical observation and the BROKEN verdict are left exactly as written: the run happened, and the residual is still real.

Declined by the reviewer, no action: the redundant not.toHaveBeenCalledWith assertion, and the weaker 'Column: Ready' negative (which is still red on revert).

Verified at this head:

  • npx vitest --run --maxWorkers=2 over the standing Inbox glob plus catalogs.spec.ts — 12 files, 471 tests passed, unchanged from round 1, so the reworded aria and its rewritten spec cost no coverage.
  • node scripts/check-docs-governance.mjs — passed.
  • npm run typecheck — clean.
  • npx eslint over the five files this round changed — clean.
  • git diff --check — clean.

NOT verified this round: unchanged from the original test plan. No browser or screen-reader check was run against the new accessible name; it is asserted in the unit spec only.

The Paper Inbox chip read "Board: {board} - Column: {column}" whenever the
route carried a columnId, while loadInboxInternal calls fetchItems({ limit:
200, boardId }) with no column key. The list was never column-filtered, so
the chip named a filter that did not exist, and the scoped empty state
interpolated the same label.

scopeLabel now always renders inbox.scope.board, and the untruthful
inbox.scope.boardAndColumn key is removed from en, it and es. The column is
not promoted to a second line: CaptureListQuery has no column key,
CreateCaptureItemDto has no ColumnId and triage targets the board's default
column, so there is no capture destination to disclose either.

useInboxOrchestrator is untouched, so activeColumnId and clearScope's delete
of query.columnId still handle a hand-written URL.

New spec inbox/PaperInboxScopeTruth.spec.ts mounts the view over the real
orchestrator and asserts the rendered chip and the outgoing fetchItems
argument in one test, which neither existing spec can do.

Refs #1984
…apture

The board column's "+ capture" pushed { boardId, columnId } to the Inbox and
named itself "Capture a note into Inbox for {column}". Nothing honours the
column: CaptureListQuery has no column key, CreateCaptureItemDto has no
ColumnId, and triage targets the board's default column. The pushed columnId
only fed the Inbox scope chip, which claimed a filter the list request never
applied.

The control now pushes the board scope alone, and the accessible name reads
"Capture a note into Inbox from {column}" in en, it and es: the destination
is the Inbox, and the column is only where the capture was taken from.

Refs #1984
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Review gate (Codex credits exhausted, SC-9): one fresh-context adversarial reviewer on head bb35651 (merge base 46fb41d). Verdict: SHIP. Confirmed: no surface still names a column as an applied Inbox filter or a capture destination (scope chip, empty state, eyebrow, route meta, Legacy inbox, nib/composer destination lines all board-only); the deleted inbox.scope.boardAndColumn key has zero remaining consumers and the three catalogs stay in parity; openCapture was the only producer of columnId into the Inbox route; no untouched spec or E2E pins the old contract; the coupled spec is the only place both halves (chip text and fetchItems call shape) can be observed together.

Findings by bin:

Merge order note: this PR's locale hunk is adjacent to PR #2584's empty/eyebrow hunks in src/locales/{en,it,es}/inbox.ts, so whichever lands second gets a merge of main and a re-prove before merging.

Evidence at the reviewed head (from the implementation run): PaperInboxScopeTruth.spec.ts red on exactly the defect then green, the standing Inbox glob 466 to 471 passed, typecheck, ESLint, build, diff check; ci-required green at bb35651. Not verified: Playwright, native it/es review. Merge after the fix commit ages and ci-required is green at the new head.

Review of PR #2580 (MEDIUM). Rewording captureAria to "from {column}" still
gave N functionally identical controls N different accessible names. Since
openCapture now pushes { boardId } and nothing else, exactly like the
board-level "Capture here", a screen-reader user heard "from Backlog" /
"from Today" / "from Done" for one behaviour, announcing a distinction the
system does not make.

captureAria drops its placeholder in en, it and es and states the board-level
truth: "Capture a note into this board's Inbox". addAria keeps {column},
because adding a card really does differ per column.

The spec now asserts all three controls carry the same name and that addAria
still carries its column.

Refs #1984
Review of PR #2580 (LOW). HORIZON_JOURNEY Step 20 and residual row 3d record
the "+ CAPTURE" deep link as /workspace/inbox?boardId=...&columnId=..., which
the Paper board can no longer produce.

Both get a one-line dated addendum. The historical observation and the BROKEN
verdict are left exactly as written: the run happened, and the column branch
of #1984 is still open.

Refs #1984
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Round 2 (head 6d08c38): the fix diff was verified by the coordinator reading it rather than a second reviewer pass, because it changes copy, one template binding and specs only: boardDetail.card.captureAria drops its placeholder in en, it and es together ("Capture a note into this board's Inbox" and its two translations), PaperBoardColumn passes no column to that key, the board-management spec asserts all three capture controls carry the same name while addAria still names its column (red first), the PR body states that only clearScope's delete still does real work and points at #2589 for the producer-less activeColumnId / activeColumnName pair, and docs/testing/HORIZON_JOURNEY.md Step 20 and row 3d carry a dated addendum with the historical text untouched. Counts at this head (implementation run): standing Inbox glob plus catalogs.spec.ts 12 files / 471 passed (unchanged), typecheck, scoped ESLint, diff check, docs governance. Not verified: npm run build this round, browser or screen-reader check of the new name. Round count: 2. Merge after ci-required is green at 6d08c38 and the head has aged; if PR #2584 lands first, this PR gets a merge of main and a re-prove because their locale hunks are adjacent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant