Skip to content

refactor(frontend): readonly on graph cache/API types#1592

Merged
adamflagg merged 3 commits into
mainfrom
feature/readonly-graph-types
May 21, 2026
Merged

refactor(frontend): readonly on graph cache/API types#1592
adamflagg merged 3 commits into
mainfrom
feature/readonly-graph-types

Conversation

@adamflagg
Copy link
Copy Markdown
Owner

@adamflagg adamflagg commented May 21, 2026

Backlog row §3 #8 (docs/reference/modernization-backlog.md), first slice — types/graph.ts.

What it does

Adds readonly to every field of the hand-written graph interfaces (GraphNode, GraphEdge, GraphMetrics, CrossScopeEdge, GraphData) and readonly T[] on the array fields (nodes, edges, cross_scope_edges, cross_scope_nodes). Pure type-level — no runtime change.

Why

GraphData is fetched once, cached in GraphCacheService, and read by ~5 graph components plus the layout worker. A stray in-place mutation (.push/.sort/field reassignment) on the shared cached instance would silently corrupt every subscriber; readonly turns that into a compile error. This is the clearest instance of the shared-cache bug-class in the frontend.

Does NOT affect node movement

Dragging/layout repositioning operates on Cytoscape's own NodeSingular model (node.position(pos) in SocialNetworkGraph.tsx, layoutWorkerGuards.ts), never our GraphData. GraphData.layout_positions is read-only seed data — only ever read in production (the lone writes are = {} in two test mocks).

Verification

  • npm run type-check: zero tsc cascade across all 5 consumers / 42 references — the codebase already copies-before-mutating, so nothing fights readonly.
  • Graph vitest suites: 203 passed, 0 failed.
  • Full lefthook run pre-push: green.

Generated type files (pocketbase-types.ts, api-generated/) deliberately skipped (hand-edits lost on regen). app-types.ts composites are the remaining slice of this row.

Also (bundled doc reconciliation)

Reconciles backlog §3c against git: marks #1587/#1588/#1589/#1591 shipped, retires the error-cause row (3a#5) as a false positive (0 wrap-rethrow-without-cause sites), adds Retired rows for error-cause + the satisfies tuple-widening trap, and corrects the readonly count (24 → 29).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation
    • Updated modernization backlog to correct frontend execution details, clarify calibration decisions, and refine the ranked execution plan and retired items.
  • Refactor
    • Strengthened immutability and caching contracts for the graph data model to reduce accidental mutations and improve runtime reliability.

Review Change Stack

§3 #8 first slice. Adds `readonly` to every field of the hand-written
`types/graph.ts` interfaces (GraphNode, GraphEdge, GraphMetrics,
CrossScopeEdge, GraphData) and `readonly T[]` on the array fields.

Why: GraphData is fetched once, cached in GraphCacheService, and read by
~5 graph components + the layout worker. A stray in-place mutation
(.push/.sort/field reassignment) on the shared cached instance would
corrupt every subscriber; readonly makes that a compile error. Pure
type-level — no runtime change. Node dragging/layout repositioning is
unaffected: positions live on Cytoscape's NodeSingular model, and
GraphData.layout_positions is read-only seed data (only written `= {}`
in test mocks).

Verified zero tsc cascade across all 5 consumers / 42 references — the
codebase already copies-before-mutating, so nothing fights readonly.
Generated type files (pocketbase-types.ts, api-generated/) deliberately
skipped; app-types.ts composites are the remaining slice of this row.

Also reconciles backlog §3c against git: marks #1587/#1588/#1589/#1591
shipped, retires the error-cause row (3a#5) as a false positive, adds
Retired rows for error-cause + the satisfies tuple-widening trap, and
corrects the readonly count (24 → 29).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 02d0f7ee-9dea-410c-b28d-680f3e4e7e58

📥 Commits

Reviewing files that changed from the base of the PR and between 17d7ea3 and 797fa2c.

📒 Files selected for processing (1)
  • frontend/src/types/graph.ts

📝 Walkthrough

Walkthrough

Graph type definitions are made immutable by adding readonly modifiers to all property declarations across GraphNode, GraphEdge, GraphMetrics, CrossScopeEdge, and GraphData interfaces. The modernization backlog is updated to document this progress (count increased from 24 to 29 readonly fields), correct a false-positive audit finding on error cause chaining, and refine the execution plan.

Changes

Graph type readonly enforcement and modernization tracking

Layer / File(s) Summary
Core graph type contracts — readonly properties
frontend/src/types/graph.ts
GraphNode, GraphEdge, and GraphMetrics interfaces have all fields marked readonly, including legacy optional fields and numeric properties, preventing reassignment at the type level.
Cross-scope and composite graph types — readonly collections
frontend/src/types/graph.ts
CrossScopeEdge fields and GraphData collections (nodes, edges, communities, warnings, layout_positions, cross_scope_edges, cross_scope_nodes) are declared as readonly arrays and records.
Modernization backlog and audit corrections
docs/reference/modernization-backlog.md
Backlog updated with readonly count progression (24 → 29), error cause chaining confirmed as false positive (zero matching sites), execution order table marked retired for that item, and satisfies-vs-as substitution limits clarified (see issue #1590).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • adamflagg/kindred#1221: Adjusts CrossScopeEdge typing in frontend/src/types/api-generated.ts, which intersects with the CrossScopeEdge immutability changes here.
✨ 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 feature/readonly-graph-types

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@frontend/src/types/graph.ts`:
- Line 43: The field types using mutable Record maps (e.g., the metadata
property declared as readonly metadata?: Record<string, unknown>) allow callers
to mutate cached objects via index assignment; replace those mutable maps with
readonly equivalents by changing Record<string, unknown> to
Readonly<Record<string, unknown>> (and similarly for any other occurrences in
this file, including the fields around lines 76-79) so the type system enforces
immutability of nested map entries for functions/types like metadata and the
other map-typed properties.
🪄 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

Run ID: a1163cfd-3b93-43b5-bf3a-ea350b84ac8c

📥 Commits

Reviewing files that changed from the base of the PR and between 5420ba0 and 17d7ea3.

📒 Files selected for processing (2)
  • docs/reference/modernization-backlog.md
  • frontend/src/types/graph.ts

Comment thread frontend/src/types/graph.ts Outdated
Wrap the four mutable Record-typed fields (GraphEdge.metadata,
GraphData.communities/layout_positions/edge_type_counts) in Readonly<>
so the immutability contract covers nested map contents, not just the
field reference. Previously `data.edge_type_counts!['x'] = 9` still
type-checked, contradicting the header doc comment's "the compiler
forbids it" claim. Addresses CodeRabbit review on #1592. Type-only;
type-check green across all 5 consumers / 42 references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@adamflagg adamflagg enabled auto-merge (squash) May 21, 2026 20:42
@adamflagg adamflagg merged commit 1970b93 into main May 21, 2026
24 checks passed
@adamflagg adamflagg deleted the feature/readonly-graph-types branch May 21, 2026 20:44
adamflagg added a commit that referenced this pull request May 21, 2026
Backlog row **§3 #8**, second/final slice — `types/app-types.ts`
(graph.ts shipped in #1592). This closes out #8.

## What it does
Adds `readonly` to every field of the hand-written app-types interfaces
— `Camper`, `BunkRequest`, `Constraint`, `SolverRun`, `DragItem`,
`BunkWithCampers` — with `readonly T[]` on array fields and
`Readonly<Record<…>>` on map fields (matching the nested-Record pattern
#1592 picked up from CodeRabbit). **Pure type-level — no runtime
change.**

The generated PB aliases (`Session`, `Bunk`, `SavedScenario`) are left
mutable — their immutability belongs in codegen, not a hand-edit.

## Why
`Camper`/`BunkRequest` are the domain shapes that live in the React
Query cache and flow through the board, panels, and drag-and-drop. A
stray in-place mutation on a shared cached instance would corrupt every
subscriber; `readonly` makes that a compile error.

## Cascade — measured, tiny
A full-readonly `tsc` probe surfaced **exactly 2** consumer sites (even
with `Camper` used in 105 files), both the predicted `readonly
T[]`→mutable-param class, neither a real mutation:

| Site | Fix |
|------|-----|
| `csvExportHelpers.ts` `buildCamperRows(campers)` | widened param to
`readonly Camper[]` — it only `filter`/`toSorted`/`map`s (all return
fresh arrays) |
| `RequestForm.tsx` `useState(constraint?.campers ?? [])` | copies into
a mutable working copy `[...]`; source stays immutable |

## Verification
- `npm run type-check` (both tsconfigs): green
- `csvExportHelpers` vitest: 19/19
- full `lefthook run pre-push`: green

Consistent with #1592's finding: zero in-place mutations of cached data
exist today, so `readonly` is forward-looking insurance with near-zero
cascade. Marks §3c #8 done (both slices); next live row is **#9** (React
Query `queryOptions`/`skipToken`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed potential issue in request form where camper selections could
inadvertently modify source data.

* **Documentation**
* Updated modernization backlog to reflect completion of type system
improvements.

* **Refactor**
* Improved internal data immutability across core application types to
enhance code reliability and prevent unintended data mutations.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/adamflagg/kindred/pull/1595?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
adamflagg added a commit that referenced this pull request May 21, 2026
…70: #1593, #1594, #1590) (#1597)

Closes #1593, closes #1594, closes #1590.

Group 70 — type-safety follow-ups deferred out of the §3
frontend-modernization PRs (#1585#1592). Pure type-only hardening; no
runtime behavior change. Each fix makes a type honest so a load-bearing
cast can be deleted or flipped to `satisfies`.

## Changes
- **#1593** — widen `createGraphElements`'s four array params to
`readonly` + narrow graph.ts `CrossScopeEdge.edge_type` to `'request'`
(matches the generated type; the API only emits `'request'` cross-scope
edges). All four `as Parameters<…>[N]` casts at `SocialNetworkGraph.tsx`
are deleted.
- **#1594** — seal the local `BunkGraphData.nodes`/`.edges` `readonly`.
The `as unknown as BunkGraphData` stays (the cached `GraphData`
structurally diverges —
`bunk_cm_id`/`bunk_name`/`metrics`/`health_score`), but downstream
read-only protection is restored.
- **#1590** — make two literals honest under
`exactOptionalPropertyTypes`:
- `useCamperEnrollment`: flipping `as Camper` → `satisfies Camper`
surfaced two masked violations — `assigned_bunk_cm_id` (`number |
undefined` into optional `?: number`, now conditional-spread) and
`expand.session`/`.assigned_bunk` (explicit `undefined` into `?: … |
null`, now `?? null`). Also tightened a now-redundant `expand?.`
optional chain.
- `useSiblings`: `as SiblingWithEnrollment` → `satisfies`;
conditional-spread the optional `session` key; typed `session_type` as
the `CampSessionsSessionTypeOptions` enum; dropped the manual `s is
SiblingWithEnrollment` predicate in favor of TS 6's inferred narrowing
(the precise literal type is a subtype, satisfying the hook's return
boundary).

## Test Plan
- [x] `npm run type-check` passes (both `tsconfig.json` +
`tsconfig.node.json`) — each cast was verified load-bearing (removed →
tsc fails) before its supporting type change.
- [x] `npm run lint` — 0 errors, no new warnings (the two introduced by
the type-honesty change are fixed).
- [x] `npx vitest run` — 4450 pass, 0 fail.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved internal type safety by strengthening immutability
constraints on data structures used for graph rendering and camper
enrollment information.
* Enhanced type accuracy for cross-scope edge data and session
information to better align with backend contracts.
* Simplified null-checking logic in enrollment and sibling data
processing.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/adamflagg/kindred/pull/1597?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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