Skip to content

feat: VER-496 add goal management controls - #33

Merged
mgunnin merged 1 commit into
stagingfrom
feat/ver-496-goal-management
Jul 31, 2026
Merged

feat: VER-496 add goal management controls#33
mgunnin merged 1 commit into
stagingfrom
feat/ver-496-goal-management

Conversation

@mgunnin

@mgunnin mgunnin commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add reusable goal creation, child-goal, and edit workflows for hierarchy, owner, status, and progress
  • enforce company-scoped owners, acyclic parent relationships, and ordered hierarchy levels in the API
  • add typed mutations, cache synchronization, visible query/save failures, and persistence coverage

Verification

  • pnpm test:run server/src/__tests__/goals.test.ts (6 passed)
  • pnpm --dir ui exec vitest run --config vitest.config.ts test/GoalFormModal.test.tsx test/GoalTree.test.tsx test/goal-hooks.test.tsx (13 passed)
  • pnpm typecheck
  • pnpm build
  • node .gitnexus/run.cjs detect-changes --scope staged (medium, expected goal/API/UI flows)
  • ~/.agents/skills/autoreview/scripts/autoreview --mode local (clean, 0.98)

CodeRabbit / Review Notes

  • CodeRabbit is currently rate-limited and intentionally skipped per handoff.
  • Ready for CI, Codesmith, Qodo, GitGuardian, and other available review gates.

Risk / Rollout Notes

  • Goal parent, owner, and level validation is stricter for new edits; existing imported rows are not migrated.
  • No migrations or environment changes.

Screenshots / UI Notes

  • Goal controls follow the existing operational UI and use one accessible shared modal.

Fixes VER-496


View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

Summary by CodeRabbit

  • New Features

    • Added goal creation and editing through a validated form.
    • Added support for nested goals, owners, levels, statuses, descriptions, and progress tracking.
    • Goal trees now display badges, progress, owners, actions, and accessible expand/collapse controls.
    • Added child-goal creation and clearer loading and error states.
  • Bug Fixes

    • Improved validation for titles, hierarchy, parent goals, cycles, and company-scoped owners.
    • Prevented invalid cross-company goal updates.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
eidolon Ready Ready Preview Jul 31, 2026 4:00am

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds hierarchical goal validation and persistence on the server. Adds typed goal API mutations and cache updates. Adds a validated goal editor and integrates goal creation, editing, ownership, metadata, loading, and error states into the goal tree.

Changes

Goal management

Layer / File(s) Summary
Server goal validation
server/src/routes/goals.ts, server/src/__tests__/goals.test.ts
The goals router validates trimmed titles, company-scoped parents and owners, hierarchy levels, cycles, and company-scoped updates. Integration tests cover persistence and validation cases.
Typed goal API and cache mutations
ui/src/lib/api.ts, ui/src/lib/hooks.ts, ui/test/goal-hooks.test.tsx
The client adds typed goal levels, statuses, create and update inputs, an update API method, and React Query mutations that update and invalidate cached goals.
Validated goal editor
ui/src/components/goals/GoalFormModal.tsx, ui/test/GoalFormModal.test.tsx
GoalFormModal supports goal creation and editing with field validation, hierarchy-aware parent options, owner handling, mutation errors, and pending-state controls.
Goal tree editing and state display
ui/src/pages/GoalTree.tsx, ui/test/GoalTree.test.tsx
GoalTree wires root, child, and edit actions to the modal. Nodes display metadata, progress, owners, accessible controls, loading states, and query errors.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GoalTree
  participant GoalFormModal
  participant useCreateGoal
  participant goalsApi
  participant goalsRouter
  GoalTree->>GoalFormModal: open create or edit form
  GoalFormModal->>useCreateGoal: submit validated goal data
  useCreateGoal->>goalsApi: createGoal(companyId, data)
  goalsApi->>goalsRouter: send goal request
  goalsRouter-->>goalsApi: return persisted goal
  goalsApi-->>useCreateGoal: return ApiResponse<Goal>
  useCreateGoal-->>GoalFormModal: update cache and complete mutation
  GoalFormModal-->>GoalTree: close editor and refresh goal tree
Loading

Possibly related PRs

  • VerticalLabs-ai/eidolon#32: Both PRs add typed entity creation flows, API persistence, React Query mutation hooks, validation, UI forms, and tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding goal management controls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ver-496-goal-management

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

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add goal create/edit controls with hierarchy validation and UI mutations

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add a shared modal workflow to create, add child goals, and edit goal details.
• Enforce company-scoped owners, acyclic parent relationships, and ordered hierarchy levels.
• Introduce typed goal mutations with cache sync plus server/UI test coverage.
Diagram

graph TD
  UI["GoalTree page"] --> MODAL["GoalFormModal"] --> HOOKS["Goal mutations"] --> API["UI API client"] --> SRV["Goals API router"] --> DB[("Goals & Agents")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. DB-enforced hierarchy constraints (recursive checks / constraints)
  • ➕ Centralizes invariants close to the data (harder to bypass)
  • ➕ Avoids O(n) scans by not loading all goals per request
  • ➖ More complex across environments/DBs
  • ➖ Harder to return specific, user-friendly validation errors
2. Adopt a form library (react-hook-form) for GoalFormModal
  • ➕ Less manual state/error wiring; easier scaling as fields grow
  • ➕ Better standardized accessibility and validation integration
  • ➖ Adds dependency and patterns to learn
  • ➖ Current modal complexity is manageable with local state + zod

Recommendation: The PR’s approach is solid for current scope: server-side validation is authoritative (company scoping, cycle prevention, level ordering) while the UI adds guardrails and preserves inputs on failure. If companies are expected to have many goals, consider moving parent/cycle validation to a targeted/recursive query strategy or DB-side enforcement to avoid loading all company goals on each relevant request.

Files changed (9) +1394 / -105

Enhancement (5) +737 / -105
goals.tsHarden goal create/update validation and company scoping +116/-4

Harden goal create/update validation and company scoping

• Trims titles on create/update to reject whitespace-only values. Adds reference validation for parent/owner scoping, cycle prevention, and parent/child level ordering (including preventing parent level changes that would invalidate existing children). Ensures PATCH updates are scoped by companyId in the update query.

server/src/routes/goals.ts

GoalFormModal.tsxAdd reusable goal create/child/edit modal with validation and save-failure UX +348/-0

Add reusable goal create/child/edit modal with validation and save-failure UX

• Implements a shared modal for creating goals, adding child goals, and editing existing goals with zod field validation. Filters parent options to avoid self/descendant cycles and enforce parent-above-child level selection, handles agent directory unavailable states, and shows visible API save errors while preserving form inputs.

ui/src/components/goals/GoalFormModal.tsx

api.tsIntroduce typed goal inputs/status/levels and add updateGoal API call +27/-15

Introduce typed goal inputs/status/levels and add updateGoal API call

• Defines GoalLevel/GoalStatus unions and CreateGoalInput/UpdateGoalInput types. Updates createGoal to accept typed input and return ApiResponse<Goal>, and adds a typed updateGoal PATCH wrapper.

ui/src/lib/api.ts

hooks.tsAdd React Query goal mutations with cache synchronization +29/-0

Add React Query goal mutations with cache synchronization

• Adds useCreateGoal and useUpdateGoal mutations that unwrap ApiResponse payloads, update the cached goals list optimistically with canonical server results, and invalidate the goals query for refresh.

ui/src/lib/hooks.ts

GoalTree.tsxAdd goal management controls, owner labels, and explicit query error states +217/-86

Add goal management controls, owner labels, and explicit query error states

• Adds New Goal / Add child / Edit controls that open the shared GoalFormModal. Integrates agent loading to render owner labels (including loading/unavailable states), improves progress accessibility, and displays alert UI for goals/agents query failures instead of showing an empty state.

ui/src/pages/GoalTree.tsx

Tests (4) +657 / -0
goals.test.tsAdd Goals API integration tests for hierarchy, ownership, and title rules +178/-0

Add Goals API integration tests for hierarchy, ownership, and title rules

• Adds end-to-end tests for creating nested goals, updating progress/status, and listing goals. Covers validation failures for cross-company parents/owners, self/descendant cycles, blank titles, and invalid level ordering.

server/src/tests/goals.test.ts

GoalFormModal.test.tsxTest GoalFormModal create/edit flows and validation edge cases +260/-0

Test GoalFormModal create/edit flows and validation edge cases

• Covers creating nested goals, editing an existing goal, excluding self/descendants from parent selection, validating progress inputs, enforcing parent-above-level rules, surfacing API failures while preserving input, and preserving current owner when directory data is unavailable.

ui/test/GoalFormModal.test.tsx

GoalTree.test.tsxTest GoalTree editor launch modes and owner/error labeling +135/-0

Test GoalTree editor launch modes and owner/error labeling

• Verifies the shared editor opens for root create, add child, and edit workflows. Ensures goal query failures show alerts (not empty state) and owner labels correctly reflect agent loading/unavailable scenarios.

ui/test/GoalTree.test.tsx

goal-hooks.test.tsxTest goal mutations update cache and invalidate queries +84/-0

Test goal mutations update cache and invalidate queries

• Validates that create and update mutations write the canonical goal response into the React Query cache and invalidate the goals query to keep the tree consistent.

ui/test/goal-hooks.test.tsx

@mgunnin
mgunnin merged commit 110b82b into staging Jul 31, 2026
5 of 6 checks passed
@mgunnin
mgunnin deleted the feat/ver-496-goal-management branch July 31, 2026 04:04

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (4)
server/src/__tests__/goals.test.ts (1)

138-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the company-scoped update.

The router now constrains the PATCH write with both goal ID and company ID. No test covers a cross-company update. Add a case that patches a goal through another company's path and expects 404.

💚 Proposed test
  it('rejects updates through another company', async () => {
    const goal = await request(app)
      .post(`/api/companies/${companyId}/goals`)
      .send({ title: 'Scoped goal' })
      .expect(201);

    const otherCompany = await request(app)
      .post('/api/companies')
      .send({ name: 'Scope Corp' })
      .expect(201);

    await request(app)
      .patch(`/api/companies/${otherCompany.body.data.id}/goals/${goal.body.data.id}`)
      .send({ title: 'Hijacked' })
      .expect(404);
  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/__tests__/goals.test.ts` around lines 138 - 142, Add a test
alongside the existing PATCH goal tests that creates a goal for companyId,
creates a separate company, then patches the original goal through the other
company’s route and asserts a 404 response. Use the existing request setup and
response data conventions in goals.test.ts.
ui/src/lib/api.ts (1)

159-171: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

CreateGoalInput cannot express targetDate or metrics.

The server CreateGoalBody accepts targetDate and metrics and persists them (server/src/routes/goals.ts, POST handler). The typed client contract omits both fields, so callers cannot set them without a cast. Consider adding them as optional fields to keep the client contract aligned with the route.

♻️ Optional addition
 export interface CreateGoalInput {
   title: string;
   description?: string;
   level: GoalLevel;
   status: GoalStatus;
   parentId: string | null;
   ownerAgentId: string | null;
   progress: number;
+  targetDate?: string | null;
+  metrics?: Record<string, unknown>;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/lib/api.ts` around lines 159 - 171, Update CreateGoalInput to include
optional targetDate and metrics fields matching the server CreateGoalBody
contract, so callers can provide both values without casts. Keep UpdateGoalInput
derived from CreateGoalInput so the new fields remain available for updates.
ui/test/GoalFormModal.test.tsx (1)

10-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The pending-state flags are never exercised.

mocks.createPending and mocks.updatePending are declared and reset, but no test sets either to true. The pending behavior is untested: dismissible={!isPending}, the disabled fields, and the loading submit button. Add one test that sets mocks.createPending = true and asserts the disabled controls.

Also applies to: 86-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/test/GoalFormModal.test.tsx` around lines 10 - 12, Add a test in the
GoalFormModal test suite that sets mocks.createPending to true before rendering
and asserts the modal is non-dismissible, form fields are disabled, and the
submit button shows its loading/disabled state; keep the existing reset behavior
and cover the analogous pending-control behavior through the rendered UI.
ui/src/components/goals/GoalFormModal.tsx (1)

153-165: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use z.flattenError(parsed.error) for Zod 4 error formatting.

ZodError.flatten() is deprecated in the resolved Zod 4.3.6 dependency. The schema is flat, so this preserves the current fieldErrors behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/goals/GoalFormModal.tsx` around lines 153 - 165, Update the
validation error handling in GoalFormModal’s parsed failure branch to use Zod
4’s z.flattenError(parsed.error) instead of the deprecated
parsed.error.flatten() call. Preserve the existing fieldErrors mapping and
setErrors behavior for all listed fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/routes/goals.ts`:
- Around line 244-261: Wrap the goal update flow in a database transaction,
moving the existing-goal read inside it and locking the company’s goals with FOR
UPDATE before validation. Update validateGoalReferences and all related queries
to use the transaction executor, then perform the goals update within the same
transaction so reference validation and persistence are serialized; add
PostgreSQL coverage for the concurrent cycle race.

In `@ui/src/components/goals/GoalFormModal.tsx`:
- Around line 117-125: Clear parentId whenever the selected level change makes
it absent from parentOptions, so state stays aligned with the visible selection
and validation. Update the level-change handling near parentOptions and ensure
the Select value and handleSubmit use the cleared, selectable parent value
without altering valid parent selections.

---

Nitpick comments:
In `@server/src/__tests__/goals.test.ts`:
- Around line 138-142: Add a test alongside the existing PATCH goal tests that
creates a goal for companyId, creates a separate company, then patches the
original goal through the other company’s route and asserts a 404 response. Use
the existing request setup and response data conventions in goals.test.ts.

In `@ui/src/components/goals/GoalFormModal.tsx`:
- Around line 153-165: Update the validation error handling in GoalFormModal’s
parsed failure branch to use Zod 4’s z.flattenError(parsed.error) instead of the
deprecated parsed.error.flatten() call. Preserve the existing fieldErrors
mapping and setErrors behavior for all listed fields.

In `@ui/src/lib/api.ts`:
- Around line 159-171: Update CreateGoalInput to include optional targetDate and
metrics fields matching the server CreateGoalBody contract, so callers can
provide both values without casts. Keep UpdateGoalInput derived from
CreateGoalInput so the new fields remain available for updates.

In `@ui/test/GoalFormModal.test.tsx`:
- Around line 10-12: Add a test in the GoalFormModal test suite that sets
mocks.createPending to true before rendering and asserts the modal is
non-dismissible, form fields are disabled, and the submit button shows its
loading/disabled state; keep the existing reset behavior and cover the analogous
pending-control behavior through the rendered UI.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 876e0fce-5fca-4e56-bc2c-767e310fa9c7

📥 Commits

Reviewing files that changed from the base of the PR and between fdaad1d and 5a8e91a.

📒 Files selected for processing (9)
  • server/src/__tests__/goals.test.ts
  • server/src/routes/goals.ts
  • ui/src/components/goals/GoalFormModal.tsx
  • ui/src/lib/api.ts
  • ui/src/lib/hooks.ts
  • ui/src/pages/GoalTree.tsx
  • ui/test/GoalFormModal.test.tsx
  • ui/test/GoalTree.test.tsx
  • ui/test/goal-hooks.test.tsx

Comment on lines +244 to +261
const relationChanged = body.parentId !== undefined || body.level !== undefined;
await validateGoalReferences({
companyId,
goalId: id,
ownerAgentId: body.ownerAgentId,
parentId: relationChanged
? body.parentId !== undefined ? body.parentId : existing.parentId
: undefined,
level: relationChanged ? body.level ?? existing.level : undefined,
});

const progressChanged =
body.progress !== undefined && body.progress !== existing.progress;

const [updated] = await db.drizzle
.update(goals)
.set({ ...body, updatedAt: new Date() })
.where(eq(goals.id, id))
.where(and(eq(goals.id, id), eq(goals.companyId, companyId)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file="$(git ls-files | rg '(^|/)server/src/routes/goals\.ts$' | head -n1)"
printf '%s\n' "FILE=$file"
wc -l "$file"
printf '%s\n' '--- route structure ---'
ast-grep outline "$file" --lang typescript
printf '%s\n' '--- target and nearby code ---'
sed -n '180,330p' "$file"
printf '%s\n' '--- validation and tree references ---'
rg -n -C 4 'validateGoalReferences|/tree|nodeMap|parentId|for\(|transaction' "$file" server/src server/test test tests 2>/dev/null | head -n 500
printf '%s\n' '--- relevant tracked files ---'
git ls-files | rg -i '(goal|pglite|drizzle|migration|schema|test|package\.json)' | head -n 500

Repository: VerticalLabs-ai/eidolon

Length of output: 42814


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- all goal route definitions and imports ---'
file="$(git ls-files | rg '(^|/)server/src/routes/goals\.ts$' | head -n1)"
sed -n '1,180p' "$file"
printf '%s\n' '--- exact validation implementation ---'
rg -n -l 'function validateGoalReferences|validateGoalReferences' server/src server 2>/dev/null | while IFS= read -r f; do
  echo "FILE=$f"
  rg -n -C 25 'function validateGoalReferences|validateGoalReferences' "$f"
done
printf '%s\n' '--- database setup and transaction support ---'
rg -n -C 5 'PGlite|pglite|drizzle\(|drizzle-orm|forUpdate|FOR UPDATE|for\(' server package.json '**/package.json' 2>/dev/null | head -n 500
printf '%s\n' '--- goal tree implementation ---'
rg -n -C 25 'nodeMap|goals/tree|tree' server/src server 2>/dev/null | head -n 500

Repository: VerticalLabs-ai/eidolon

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu
find . -path '*/server/src/routes/goals.ts' -print

Repository: VerticalLabs-ai/eidolon

Length of output: 190


🏁 Script executed:

rg -n -C 3 "validateGoalReferences|nodeMap|PGlite|pglite|transaction|for\('update'\)" .

Repository: VerticalLabs-ai/eidolon

Length of output: 47285


🌐 Web query:

PGlite PostgreSQL FOR UPDATE row locking transaction concurrent transactions support

💡 Result:

PGlite is a WebAssembly-based distribution of PostgreSQL that operates as a single-process, single-user instance [1]. Because of its architecture, it behaves differently regarding row locking and transaction concurrency compared to a standard, multi-process PostgreSQL server [1]. Row Locking (SELECT FOR UPDATE) PGlite supports the standard PostgreSQL SQL syntax, including row-level locking commands like SELECT FOR UPDATE [2][3]. However, because PGlite operates as a single-process system [1], these locks do not function as a mechanism for handling concurrency between multiple independent client connections in the same way they would on a standard Postgres server. Within the context of a single PGlite instance, there is only one user/connection [1], so there are no concurrent transactions from other users to block. The commands will execute, but they will not provide the cross-connection protection that row-level locks typically offer in a multi-user environment [1][2]. Transaction Concurrency PGlite does not support concurrent transactions in the traditional sense because it is fundamentally a single-user, single-process engine [1]. 1. Single-Process Architecture: PGlite runs in a single-threaded WASM environment [1]. It does not fork processes for new connections [1]. 2. Transaction Handling: While PGlite supports transactions via its.transaction API (which ensures atomic operations for the single connection) [3], it cannot execute multiple transactions simultaneously. 3. Multi-connection management: Tools like pglite-socket can facilitate interactions that appear to manage multiple clients or connection pools (e.g., from Node.js) by queueing and scheduling protocol messages [4]. These tools help maintain isolation and prevent corruption when multiple clients attempt to use the same PGlite instance, but they do so by serializing the work rather than enabling true parallel transaction processing [4]. If your application requires true concurrent transaction support, a standard PostgreSQL server deployment is required, as PGlite's architecture is specifically optimized for single-process, WASM-based environments [1].

Citations:


Serialize goal-reference validation and the update in one transaction.

Concurrent production requests can both pass the cycle check and persist A.parentId = B and B.parentId = A. GET /goals/tree then returns neither goal as a root. Move the existing read into the transaction, lock the company’s goals with FOR UPDATE, and run every validation query through the same transaction executor. PGlite supports these operations, but its single-connection harness cannot test cross-connection locking; cover this race against PostgreSQL.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/routes/goals.ts` around lines 244 - 261, Wrap the goal update flow
in a database transaction, moving the existing-goal read inside it and locking
the company’s goals with FOR UPDATE before validation. Update
validateGoalReferences and all related queries to use the transaction executor,
then perform the goals update within the same transaction so reference
validation and persistence are serialized; add PostgreSQL coverage for the
concurrent cycle race.

Comment on lines +117 to +125
const parentOptions = [
{ value: "", label: "No parent (root goal)" },
...goals
.filter(
(item) => !unavailableParents.has(item.id)
&& goalLevelRank[item.level] < goalLevelRank[level],
)
.map((item) => ({ value: item.id, label: item.title })),
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stale parentId remains after a level change, so the visible Select value and the validated value disagree.

parentOptions is recomputed from level, but parentId state is not. If the user raises the level, the previously selected parent is filtered out. The Select then has a value with no matching option, so the browser displays the first option, "No parent (root goal)". The state still holds the removed id. On submit, the code rejects with "Choose a parent above this goal's level.", which contradicts the displayed selection. The test at ui/test/GoalFormModal.test.tsx Line 215 shows the option is removed while submission still fails.

Clear parentId when it is no longer selectable.

🐛 Proposed fix
   const parentOptions = [
     { value: "", label: "No parent (root goal)" },
     ...goals
       .filter(
         (item) => !unavailableParents.has(item.id)
           && goalLevelRank[item.level] < goalLevelRank[level],
       )
       .map((item) => ({ value: item.id, label: item.title })),
   ];
+  const selectableParentId = parentOptions.some((option) => option.value === parentId)
+    ? parentId
+    : "";

Then use selectableParentId for the Select value and in handleSubmit, or reset the state in the level onChange:

-              onChange={(event) => setLevel(event.target.value as GoalLevel)}
+              onChange={(event) => {
+                const nextLevel = event.target.value as GoalLevel;
+                setLevel(nextLevel);
+                const parent = goals.find((item) => item.id === parentId);
+                if (parent && goalLevelRank[parent.level] >= goalLevelRank[nextLevel]) {
+                  setParentId("");
+                }
+              }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/goals/GoalFormModal.tsx` around lines 117 - 125, Clear
parentId whenever the selected level change makes it absent from parentOptions,
so state stays aligned with the visible selection and validation. Update the
level-change handling near parentOptions and ensure the Select value and
handleSubmit use the cleared, selectable parent value without altering valid
parent selections.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 2 rules

Grey Divider


Remediation recommended

1. Unneeded full goals scan 🐞 Bug ➹ Performance
Description
POST /goals always runs validateGoalReferences()'s full-company SELECT+Map build even for root
goals, because CreateGoalBody defaults parentId to null and the guard only checks for undefined.
This makes every goal creation O(number of company goals) in DB work and memory allocation,
increasing latency/load as goal counts grow.
Code

server/src/routes/goals.ts[R60-66]

+    if (parentId !== undefined || (goalId && level !== undefined)) {
+      const companyGoals = await db.drizzle
+        .select({ id: goals.id, parentId: goals.parentId, level: goals.level })
+        .from(goals)
+        .where(eq(goals.companyId, companyId));
+      const goalsById = new Map(companyGoals.map((goal) => [goal.id, goal]));
+
Relevance

●●● Strong

Team accepted similar null/undefined guard fixes to avoid incorrect predicates/extra work (PR #1).

PR-#1

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
CreateGoalBody sets parentId to default to null, so body.parentId is never undefined in the
POST handler. The new validateGoalReferences() guard treats null as present (`parentId !==
undefined), so it always selects all goals for the company and builds a Map` even when there is no
parent/child-level validation to perform for a create without goalId.

server/src/routes/goals.ts[10-20]
server/src/routes/goals.ts[47-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`validateGoalReferences()` loads every goal for the company whenever `parentId !== undefined`, but `CreateGoalBody` defaults `parentId` to `null`. This means root goal creation (no parent) still triggers a full company goals query + `Map` allocation that is not used for any validation.

## Issue Context
- Root creates pass `parentId: null` into `validateGoalReferences()`.
- The guard `parentId !== undefined` treats `null` as “present”, so it performs the company-wide scan.

## Fix Focus Areas
- server/src/routes/goals.ts[10-20]
- server/src/routes/goals.ts[47-66]

## Recommended fix
Update the guard so it only loads company goals when a *non-null* parent is being validated, or when `goalId && level !== undefined` is needed for child-level checks. For example:

```ts
if ((parentId !== undefined && parentId !== null) || (goalId && level !== undefined)) {
 ...
}
```

Optionally, also consider making `CreateGoalBody.parentId` `optional()` (no default) and normalize to `null` only at insert time, so omitted parent values don’t look “set” to validation logic.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. GoalTree reimplements ProgressBar 📘 Rule violation ⚙ Maintainability
Description
ui/src/pages/GoalTree.tsx renders a custom progress bar instead of using the existing
design-system ProgressBar component, duplicating shared UI behavior and styling. This increases
maintenance burden and risks inconsistent accessibility/visual behavior across the app.
Code

ui/src/pages/GoalTree.tsx[R169-181]

+            <div
+              className="h-2 overflow-hidden rounded-full bg-surface-overlay"
+              role="progressbar"
+              aria-label={`${goal.title} progress`}
+              aria-valuemin={0}
+              aria-valuemax={100}
+              aria-valuenow={goal.progress}
+            >
              <div
                className="h-full rounded-full bg-accent transition-all duration-500 ease-out"
-                style={{ width: `${goal.progress ?? 0}%` }}
+                style={{ width: `${goal.progress}%` }}
              />
            </div>
-            <span className="text-[10px] text-text-secondary font-display tabular-nums mt-1 inline-block">
-              {goal.progress ?? 0}%
-            </span>
Relevance

●● Moderate

No repo history found enforcing ProgressBar reuse; UI feedback accepted but unrelated to component
duplication.

PR-#1
PR-#8

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1791705 requires using existing design system components instead of re-implementing
equivalents. The diff adds a custom progress bar markup in GoalTree.tsx, while the repo already
provides a reusable ProgressBar component in ui/src/components/ui/ProgressBar.tsx.

Rule 1791705: Prefer existing design system components over creating new equivalents
ui/src/pages/GoalTree.tsx[169-185]
ui/src/components/ui/ProgressBar.tsx[26-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Goals tree uses an inline/custom progress bar implementation even though a shared design-system `ProgressBar` component already exists.

## Issue Context
This duplicates existing UI primitives and can cause inconsistent styling/accessibility behaviors across pages.

## Fix Focus Areas
- ui/src/pages/GoalTree.tsx[169-185]
- ui/src/components/ui/ProgressBar.tsx[26-69]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread ui/src/pages/GoalTree.tsx
Comment on lines +169 to 181
<div
className="h-2 overflow-hidden rounded-full bg-surface-overlay"
role="progressbar"
aria-label={`${goal.title} progress`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={goal.progress}
>
<div
className="h-full rounded-full bg-accent transition-all duration-500 ease-out"
style={{ width: `${goal.progress ?? 0}%` }}
style={{ width: `${goal.progress}%` }}
/>
</div>

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.

Remediation recommended

1. Goaltree reimplements progressbar 📘 Rule violation ⚙ Maintainability

ui/src/pages/GoalTree.tsx renders a custom progress bar instead of using the existing
design-system ProgressBar component, duplicating shared UI behavior and styling. This increases
maintenance burden and risks inconsistent accessibility/visual behavior across the app.
Agent Prompt
## Issue description
The Goals tree uses an inline/custom progress bar implementation even though a shared design-system `ProgressBar` component already exists.

## Issue Context
This duplicates existing UI primitives and can cause inconsistent styling/accessibility behaviors across pages.

## Fix Focus Areas
- ui/src/pages/GoalTree.tsx[169-185]
- ui/src/components/ui/ProgressBar.tsx[26-69]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +60 to +66
if (parentId !== undefined || (goalId && level !== undefined)) {
const companyGoals = await db.drizzle
.select({ id: goals.id, parentId: goals.parentId, level: goals.level })
.from(goals)
.where(eq(goals.companyId, companyId));
const goalsById = new Map(companyGoals.map((goal) => [goal.id, goal]));

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.

Remediation recommended

2. Unneeded full goals scan 🐞 Bug ➹ Performance

POST /goals always runs validateGoalReferences()'s full-company SELECT+Map build even for root
goals, because CreateGoalBody defaults parentId to null and the guard only checks for undefined.
This makes every goal creation O(number of company goals) in DB work and memory allocation,
increasing latency/load as goal counts grow.
Agent Prompt
## Issue description
`validateGoalReferences()` loads every goal for the company whenever `parentId !== undefined`, but `CreateGoalBody` defaults `parentId` to `null`. This means root goal creation (no parent) still triggers a full company goals query + `Map` allocation that is not used for any validation.

## Issue Context
- Root creates pass `parentId: null` into `validateGoalReferences()`.
- The guard `parentId !== undefined` treats `null` as “present”, so it performs the company-wide scan.

## Fix Focus Areas
- server/src/routes/goals.ts[10-20]
- server/src/routes/goals.ts[47-66]

## Recommended fix
Update the guard so it only loads company goals when a *non-null* parent is being validated, or when `goalId && level !== undefined` is needed for child-level checks. For example:

```ts
if ((parentId !== undefined && parentId !== null) || (goalId && level !== undefined)) {
  ...
}
```

Optionally, also consider making `CreateGoalBody.parentId` `optional()` (no default) and normalize to `null` only at insert time, so omitted parent values don’t look “set” to validation logic.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo Fixer

No findings are within the configured fix scope. To change which findings are fixed, adjust the setting on your Qodo configuration page.

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