Skip to content

#364 Add An Application Status Transition Graph With Next-Step Quick Actions - #499

Merged
cielbellerose merged 11 commits into
devfrom
364-add-application-status-transition-graph
Aug 19, 2026
Merged

#364 Add An Application Status Transition Graph With Next-Step Quick Actions#499
cielbellerose merged 11 commits into
devfrom
364-add-application-status-transition-graph

Conversation

@cielbellerose

Copy link
Copy Markdown
Collaborator

Closes #364

Summary

  • Replaces the six-item status dropdown with graph-driven quick actions that render only the legal next moves, so pipeline direction is visible in the UI instead of relying on reviewer discipline.
  • Enforces the same transition graph server-side in both updateApplicationStatus and updateApplicationStatuses, so an illegal move is impossible even from a forged request or a stale tab.
  • accepted/rejected stay reversible by a reviewer (naming the explicit target, never a bare "Undo") while remaining terminal for the applicant.

Changes

  • lib/constants.tsAPPLICATION_STATUS_TRANSITIONS (the graph, as const satisfies Record<...> so a new enum member breaks the build), REJECTABLE_APPLICATION_STATUSES, getAllowedApplicationStatusTransitions/isAllowedApplicationStatusTransition/getApplicationStatusSources (the last one derived by inverting the graph), APPLICATION_STATUS_ACTION_LABELS, and TERMINAL_DECISION_STATUS_NOTES.
  • prisma/actions/applications.tsupdateApplicationStatus now re-checks the freshly-read status against the graph before writing, and scopes the updateMany to getApplicationStatusSources(target) to close the check-then-write race. updateApplicationStatuses gains the same source scoping; a zero-count result now returns a target-naming { error } instead of throwing, since a mismatched bulk target is a normal, reachable outcome.
  • components/features/application-status-actions.tsx — new. Renders forward/reject/move-back groups derived from the graph, with a compact variant (ellipsis + DropdownMenu) for table cells and a roomy variant (stacked buttons) for the detail page. Accept/Reject route through the shared ConfirmDialog, rendered as a sibling of the menu so it survives the menu closing.
  • components/features/application-status-control.tsx — deleted; fully superseded.
  • app/(main)/(auth)/applications/[id]/page.tsx, components/features/position-applications-table.tsx — swapped in the new component; the status cell now shows the badge plus the actions menu.
  • tests/unit/application-transitions.test.ts — new; graph invariants (totality, no self-loops, rejected reachability, source/target inversion).
  • tests/db/application-transitions.test.ts — replaced the "every target reachable from applied" loop with the full source × target matrix, plus bulk mixed-selection cases.
  • tests/db/authorization.test.ts — updated the out-of-scope bulk case to expect the new { error } return instead of a throw, since it's now indistinguishable from (and superseded by) the graph-legality check.

Testing plan

  • As an admin on /applications/[id] for an applied application: confirm exactly "Mark reached out", "Move to reviewing", "Reject" appear and no move-back control does.
  • Click "Mark reached out" → toast "Moved to Reached out", badge updates, and the panel now offers "Schedule interview", "Move to reviewing", "Reject", "Move back to Applied".
  • From reviewing, click "Accept" → confirm dialog names the applicant; cancel leaves the status unchanged; confirm shows "Moved to Accepted".
  • On the accepted application: only the terminal note plus "Move back to Reviewing" / "Move back to Interview scheduled" appear — no Accept, no Reject. Click one and confirm the status moves.
  • Open the same application in two tabs. In tab A move it to accepted; in tab B (still showing applied) click "Move to reviewing" → error toast telling you it's now Accepted and to refresh. Verify the DB status is still accepted.
  • As a manager (non-admin) of the position, repeat one forward move and one move-back; then confirm a position you don't manage still 404s.
  • On a position detail page, use the Status cell menu on a row: same options as the detail page, and the badge updates in place. Check at 375px that the mobile card's menu opens and is tappable.
  • On /applications, select a mix of applied and accepted rows, set status to "Reviewing", Apply → the applied rows move, the accepted rows are skipped, and the toast reports both counts.
  • Select only accepted rows, apply "Reviewing"; then select only applied rows and apply "Accepted" → expect the "None of the selected applications can move to Accepted." error toast.
  • View a withdrawn application's detail page → no controls, existing withdrawn note still shown.
  • As an applicant, withdraw an application and resubmit it from /my-applications → still lands back in applied (unchanged path).
  • Verify both light and dark themes on the Status card and the menu.

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test:unit — pass (77/77); npm run test (db project) could not run locally — Postgres wasn't reachable in this environment (npm run db:start hit a port conflict from a preexisting container). CI's db-check job should confirm the new/updated db tests.

Notes

  • The bulk target picker (REVIEWER_APPLICATION_STATUS_OPTIONS in applications-bulk-bar.tsx) intentionally still lists all six statuses rather than narrowing to the selection's common legal targets — narrowing would be empty for most mixed selections. The action enforces the graph; the toast (already wired up from Warn Before Bulk Status Changes and Report Skipped Rows #365's skipped-row reporting) explains what happened.
  • tests/db/authorization.test.ts's "every id out of scope" case now asserts a returned { error } rather than a throw, because the new source-status scoping makes a zero-count bulk result reachable through normal use (a mismatched target), not just through an authorization miss — the decision test in ENGINEERING.md §4 calls for { error } once that's true.

@cielbellerose cielbellerose self-assigned this Aug 18, 2026
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aplio Ready Ready Preview Aug 19, 2026 10:15pm

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 18, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 1 · needs revision

4 open — 1 🔴 Critical, 2 🟡 Low, 1 ⚪ Nit

Comment thread tests/db/application-transitions.test.ts
Comment thread lib/constants.ts Outdated
Comment thread lib/constants.ts Outdated
Comment thread components/features/application-status-actions.tsx
@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Aug 18, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-C1, R1-L1, R1-N1, R1-L2 · 3617070

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Aug 18, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 18, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 2 · needs revision

1 open — 1 🔴 Critical (see inline)

Comment thread lib/constants.ts
@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Aug 18, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 2

fixed R2-C1 · e5d7dbf

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Aug 18, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 18, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 3 · approved

1 open — 1 🟡 Low (see inline)

Comment thread lib/constants.ts
@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 18, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 5 · needs revision

1 open — 1 🔴 Critical (see inline)

Comment thread tests/db/authorization.test.ts Outdated
@cielbellerose cielbellerose added needs revision Review found issues that need fixing needs human Pipeline escalation: 3 review cycles without convergence and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Aug 19, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Pipeline Escalation

PR has reached 5 review cycles without convergence — cockpit cycle-cap reached.

Cycle 5 finding still open (Critical):

  • tests/db/authorization.test.ts:427 — the cycle-5 commit (b463f8ea, "fix bulk skip messaging to use forward-only sources") correctly switched the bulk "unreachable target" message to getApplicationStatusForwardSources in prisma/actions/applications.ts and components/features/applications-bulk-bar.tsx, but left the test assertion at the old two-source string. getApplicationStatusForwardSources('reviewing') now resolves to [applied, reached_out, interview_scheduled], so the action's actual message is "...reachable from Applied, Reached out, or Interview scheduled." — the test still asserts "...reachable from Applied or Reached out.", which is why run-test-check is red.

This is a small, well-understood fix (update one test assertion string), but 5 cycles without landing it suggests either the fix keeps regressing or something about the review/revise loop for this PR needs a human look before continuing automated cycles.

@cielbellerose
cielbellerose force-pushed the 364-add-application-status-transition-graph branch from b463f8e to f2b44af Compare August 19, 2026 20:09
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 5

fixed R5-C1 · f2b44af

Also rebased onto origin/dev to clear the 10-commit-old tooltip conflict (this branch's own #511 restoration vs. #512's canonical version — took dev's, same resolution as #498/#500) · rebase: components/ui/tooltip.tsx, package.json, package-lock.json (took dev's canonical version — the only PR-side change was the now-redundant dependency addition, confirmed via git show)

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed needs human Pipeline escalation: 3 review cycles without convergence labels Aug 19, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 19, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 6 · approved

0 open — the R5-C1 stale assertion fix is correct and introduces no regression

@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision requested (human testing)

Manual testing found a bug in the status transition flow:

  • Attempting to update an applied status to "Interview Scheduled" does not work — the transition fails/doesn't apply.
  • While that failure occurs, the UI incorrectly displays "The skipped rows are still selected" even when no rows are selected.

Please either:

  1. Fix the "Interview Scheduled" transition so it actually applies, and correct the "skipped rows" messaging so it only shows when rows are genuinely still selected, or
  2. If "skipped rows still selected" is meant to guard a different scenario that doesn't apply here, remove/adjust the wording so it isn't shown incorrectly.

@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Pipeline Escalation

Rebasing 364-add-application-status-transition-graph onto origin/dev hits a
conflict in components/features/applications-table.tsx that needs the
original author's judgment, not an autonomous resolution.

The conflict

Both sides restructured the same JSX block — the mobile stacked-card header
(the flex items-start justify-between gap-2 row that holds the applicant
name and the status controls):

  • dev side — commit 172a3c1 (#443 show current name beside a renamed applicant, part of the #443 rename-tracking work already merged to
    dev) wraps the applicant Link (plus a new renamedTo span) in a new
    <div className="min-w-0 truncate">, and leaves ApplicationStatusBadge
    as a bare sibling.
  • PR side — this PR's commit 3aa9cc7 (#364 apply human-directed status label, quick-action, and bulk copy fixes, the cycle-3 commit that
    wired the compact quick-actions menu into this file's mobile card) keeps
    the applicant Link un-wrapped but wraps ApplicationStatusBadge +
    the new ApplicationStatusActions in a <div className="flex shrink-0 items-center gap-1">.
<<<<<<< HEAD (dev, via #443)
                    <div className="min-w-0 truncate">
                      <Link href={`/applications/${app.id}`} className="font-medium hover:underline">
                        {displayName}
                      </Link>
                      {renamedTo && (
                        <span className="text-muted-foreground ml-1 text-xs">({renamedTo})</span>
                      )}
                    </div>
                    <ApplicationStatusBadge status={app.status} />
=======
                    <Link href={`/applications/${app.id}`} className="truncate font-medium hover:underline">
                      {displayName}
                    </Link>
                    <div className="flex shrink-0 items-center gap-1">
                      <ApplicationStatusBadge status={app.status} />
                      <ApplicationStatusActions
                        applicationId={app.id}
                        currentStatus={app.status}
                        applicantName={displayName}
                        compact
                      />
                    </div>
>>>>>>> 3aa9cc7 (#364 apply human-directed status label, quick-action, and bulk copy fixes)

Why this isn't safe to auto-resolve

This is a same-lines/same-hunk overlap, not two clearly separate sections —
per the classification matrix (.claude/docs/PIPELINE.md → "Rebase conflict
protocol") that puts it in the escalate column ("logic changes on the same
lines from both sides"). Mechanically combining them (adopt dev's
renamedTo wrapper for the name, and nest ApplicationStatusActions inside
dev's structure alongside the badge) looks like the obvious merge, but I
don't have a way to verify it against the actual rendered layout, and this
same file/section is exactly where the just-reported human-testing bugs live
(the "Interview Scheduled" transition failure and the incorrect "skipped
rows are still selected" messaging both surface from this table's quick
actions / bulk-selection interplay). A bad structural merge here could mask
or worsen either bug in a way that's hard to catch without the original
author's eyes on the render.

Separately worth flagging: dev's tip already carries d4b2b96 (#365 warn before bulk status changes and report skipped rows), which is very likely
where the human-reported "skipped rows are still selected" copy originates
(handleApplied(retainedIds) now re-selects skipped rows after a bulk
action instead of clearing selection) — that logic lives in this same file,
just outside the conflicting hunk, so it survived the rebase attempt
untouched. Once the conflict above is resolved, the actual bug fix will
likely touch handleApplied/ApplicationsBulkBar's messaging in this file
too.

What I did

  • git fetch origin, git checkout --detach origin/364-add-application-status-transition-graph
  • git rebase origin/dev → conflict as above (components/features/applications-table.tsx only)
  • git rebase --abort (clean tree restored, no partial resolution, no code changes, no commits pushed)

Requested decision

Please confirm the intended merged structure for the mobile-card header in
components/features/applications-table.tsx — i.e. whether to nest the
renamedTo-wrapped name div and the actions-wrapped badge div as siblings
inside the existing flex items-start justify-between gap-2 row (the
straightforward reading), or something else. Once confirmed, revision work
on the two human-testing bugs (Interview Scheduled transition, skipped-rows
messaging) can proceed on a clean rebase.

cielbellerose and others added 11 commits August 19, 2026 18:12
Single source of truth for the six-status pipeline direction, so the
server guard and the rendered quick actions can't drift apart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
updateApplicationStatus and updateApplicationStatuses now scope their
writes to the graph's legal source states, so an illegal move fails
even from a forged request or a stale tab. Bulk failures and
concurrent-change misses now return a user-facing error naming the
target instead of throwing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ApplicationStatusActions renders only the legal next moves for the
current status — primary/secondary forward buttons, a destructive
Reject, and explicit move-back controls — as a roomy panel on the
review detail page and a constrained menu in the per-position table.
Accept and Reject route through the shared confirm dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unit tests assert the graph invariants (totality, no self-loops,
rejected reachability, source/target inversion). DB tests replace the
old "every target reachable from applied" loop with the full 6x6
source x target matrix and add mixed-selection bulk cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scope bulk status updates to forward-only sources so a batch move
can't silently walk an already-decided row backward, matching the
PR's stated bulk behavior. Also closes the confirm dialog on a
failed move and trims two comments to the one/two-line rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getApplicationStatusForwardSources returned [] for 'applied' since no
state's forward list ever contains it, making bulk moves to Applied
always no-op. Fall back to back-sources when a target has no forward
source at all.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds direct unit invariants for getApplicationStatusForwardSources'
back-only fallback, per R3-L1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ixes

Reword interview_scheduled to a status label, wire the compact quick-
actions menu into the live applications table, and name reachable
source statuses in bulk-move error/toast copy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The bulk "unreachable target" copy computed sources with the full
source set instead of the forward-only set that actually scopes the
updateMany where-clause, so it could list back-only statuses as valid
bulk targets. Match the message to the query and update the stale
test assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getApplicationStatusForwardSources('reviewing') resolves to
[applied, reached_out, interview_scheduled] now that the action uses
forward-only sources; the test still expected the old two-source text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The bulk-status toast said skipped rows were "still selected", but
onApplied only retains ids skipped for a non-reviewable status —
rows skipped for a forward-source mismatch (e.g. applied ->
interview_scheduled) are deselected, making the claim false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 5

Per human manual-testing feedback (not a review finding): the "Interview
Scheduled" bulk transition failure is by design (applied's forward
targets are reached_out/reviewing only), so those rows are correctly
skipped server-side. Fixed the wording only — the bulk toast claimed
"The skipped rows are still selected," which is false whenever rows are
skipped for a forward-source mismatch rather than a non-reviewable status;
removed that sentence · 06531be

rebase: components/features/applications-table.tsx (kept both — dev's
renamedTo wrapper for the applicant name and this PR's actions wrapper
for the badge/quick-actions, as sibling divs, per relayed human decision)

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 7 · approved

0 open — the skipped-selection copy fix (06531be) is accurate, no regression

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

Labels

approved Review passed, ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an Application Status Transition Graph With Next-Step Quick Actions

1 participant