Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-06-18 - [Memoize O(N) Array Grouping in React UI state]
**Learning:** Collapsible table sections in React that use local state (`useState`) to track expanded/collapsed items will trigger a full component re-render on every toggle. If the component synchronously computes O(N) data groupings (like `groupByType(actors)` or `groupByLevel(usecases)`) before rendering, this can become a minor performance bottleneck as N grows, causing UI jank when expanding or collapsing groups.
**Action:** Always wrap expensive O(N) array transformations (sorting, grouping, filtering) in `useMemo` when they are placed inside a component body that relies on local UI state changes, using the source array as the dependency array.
6 changes: 4 additions & 2 deletions apps/app/app/components/ActorTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useMemo, useState } from "react";
import { ChevronRight, CircleDot, ListOrdered, User } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
Expand Down Expand Up @@ -107,7 +107,9 @@ export function ActorTable({
actors: ActorSummary[];
usecaseCountByActor: Record<string, number>;
}) {
const groups = groupByType(actors);
// ⚑ OPTIMIZATION: Memoize the O(N) grouping operation to prevent recalculation
// on every local state change (like expanding/collapsing a group).
const groups = useMemo(() => groupByType(actors), [actors]);
const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(new Set());

function toggle(type: string) {
Expand Down
6 changes: 4 additions & 2 deletions apps/app/app/components/UsecaseTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useMemo, useState } from "react";
import Link from "next/link";
import {
ChevronRight,
Expand Down Expand Up @@ -144,7 +144,9 @@ export function UsecaseTable({
usecases: UsecaseSummary[];
projectKey: string;
}) {
const groups = groupByLevel(usecases);
// ⚑ OPTIMIZATION: Memoize the O(N) grouping operation to prevent recalculation
// on every local state change (like expanding/collapsing a group).
const groups = useMemo(() => groupByLevel(usecases), [usecases]);
const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(new Set());

function toggle(level: string) {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-004-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
expect(seed.projectKey).toBe("PRJ");
expect(switched.stdout).toContain(seed.projectKey);
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 25 in apps/cli/tests/e2e-cli-honest/UC-004-project.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-005-actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
expect(secondActor.stdout).toContain("Admin");
expect(secondActor.stdout).toContain("SUPPORTING");
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 39 in apps/cli/tests/e2e-cli-honest/UC-005-actor.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-006-stakeholder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
expect(stakeholder.stdout).toContain("Product Manager");
expect(stakeholder.stdout).toContain("INTERNAL");
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 36 in apps/cli/tests/e2e-cli-honest/UC-006-stakeholder.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
});
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-007-goal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
expect(created.stdout).toContain("Places an order");
expect(listed.stdout).toContain("Places an order");
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 44 in apps/cli/tests/e2e-cli-honest/UC-007-goal.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
});
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-009-usecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
expect(shown.stdout).toContain(seed.usecaseKey);
expect(shown.stdout).toContain("Places an order");
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 25 in apps/cli/tests/e2e-cli-honest/UC-009-usecase.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-011-main-scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
expect(scenarioId).toMatch(/[a-f0-9-]+/u);
expect(step.stdout).toContain("1. Customer Places an order.");
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 34 in apps/cli/tests/e2e-cli-honest/UC-011-main-scenario.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-013-edit-step.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
expect(edited.stdout).toContain("Reviews the order.");
expect(edited.stdout).toContain("version 5");
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 34 in apps/cli/tests/e2e-cli-honest/UC-013-edit-step.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-016-start-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
expect(sessionFile.session_id).toMatch(/[a-f0-9-]+/u);
expect(Object.keys(sessionFile.pinned_revisions)).toHaveLength(1);
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 49 in apps/cli/tests/e2e-cli-honest/UC-016-start-session.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
});
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-019-create-branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
expect(branch.stdout).toContain("Name feature/refund-review");
expect(branch.stdout).toContain("Status ACTIVE");
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 33 in apps/cli/tests/e2e-cli-honest/UC-019-create-branch.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
});
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/UC-022-lock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
expect(lock.stdout).toContain("Type SEMANTIC");
expect(lock.stdout).toContain("Holder session-main-lock");
} finally {
await server.stop();
if (server) await server.stop();

Check failure on line 36 in apps/cli/tests/e2e-cli-honest/UC-022-lock.test.ts

View workflow job for this annotation

GitHub Actions / lint-typecheck

Unnecessary conditional, value is always truthy
}
});
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/actor-edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("honest CLI - actor edit", () => {
expect(edited.stdout).toContain("Buyer");
expect(archived.stdout).toContain(seed.actorId);
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/actor-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("honest CLI - actor read", () => {
expect(listed.stdout).toContain(seed.actorId);
expect(shown.stdout).toContain(seed.actorId);
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ describe("honest CLI --format=agent write paths", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) {
if (server) await server.stop();
}
});

test("agent actor create", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/branch-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("honest CLI branch create --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent branch create", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/change-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("honest CLI change --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
while (tempRoots.length > 0) {
rmSync(tempRoots.pop() ?? "", { force: true, recursive: true });
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/comment-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("honest CLI comment --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent comment lifecycle", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("honest CLI - doctor", () => {
expect.objectContaining({ id: "project.exists", status: "pass" })
);
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/goal-edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("honest CLI - goal edit", () => {
expect(seed.env.VSPEC_CONFIG_PATH).toContain("config.json");
expect(rejected.stdout).toContain("REJECTED");
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/goal-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("honest CLI - goal read", () => {
expect(seed.env.VSPEC_CONFIG_PATH).toContain("config.json");
expect(shown.stdout).toContain("Submit an order");
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/history-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("honest CLI history --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent history", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/impact-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe("honest CLI impact --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent impact", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("honest CLI local context --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent local context lifecycle", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/lock-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe("honest CLI lock acquire --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent lock acquire", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("honest CLI lock renew --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent lock renew", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/login-to-usecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe("honest CLI flow", () => {
expect(usecase.status).toBe(0);
expect(usecase.stdout).toContain("HON-001");
} finally {
await server.stop();
if (server) await server.stop();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe("honest CLI member/API-key --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent member and api-key admin lifecycle", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe("honest CLI merge open --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent merge open", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe("honest CLI project create --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent project create updates active project", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/project-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("honest CLI - project read", () => {
expect(listed.stdout).toContain(seed.projectKey);
expect(listed.stdout).toContain(seed.projectId);
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
4 changes: 2 additions & 2 deletions apps/cli/tests/e2e-cli-honest/project-session-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("honest CLI - project and login context refresh", () => {
expect(config.current_project_key).toBe("CTX");
expect(config.current_project_id).toBe(seed.projectId);
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);

Expand All @@ -49,7 +49,7 @@ describe("honest CLI - project and login context refresh", () => {
expect(config.current_project_id).toBeUndefined();
expect(config.current_project_key).toBeUndefined();
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("honest CLI pull/sync --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
while (tempRoots.length > 0) {
rmSync(tempRoots.pop() ?? "", { force: true, recursive: true });
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/push-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe("honest CLI push --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
while (tempRoots.length > 0) {
rmSync(tempRoots.pop() ?? "", { force: true, recursive: true });
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/revert-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("honest CLI revert --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent revert", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe("honest CLI scenario add --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent scenario add", async () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/tests/e2e-cli-honest/self-teaching-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("honest CLI - self-teaching guidance", () => {
);
expect(forced.stdout).toContain("UseCase TEA-");
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);

Expand Down Expand Up @@ -89,7 +89,7 @@ describe("honest CLI - self-teaching guidance", () => {

expect(actor.stdout).toContain("Actor Support Specialist");
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/session-agent-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("honest CLI session --format=agent", () => {
}, 30_000);

afterAll(async () => {
await server.stop();
if (server) await server.stop();
});

test("agent session start", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/tests/e2e-cli-honest/stakeholder-edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("honest CLI - stakeholder edit", () => {
expect(edited.stdout).toContain("Product Owner");
expect(archived.stdout).toContain(stakeholderId);
} finally {
await server.stop();
if (server) await server.stop();
}
}, 30_000);
});
Loading
Loading