Skip to content

Make subscript(id:) O(1) on Catalog, add it to Lens and GroupedLens - #43

Merged
searls merged 1 commit into
mainfrom
claude/compassionate-visvesvaraya-ecfef1
May 3, 2026
Merged

Make subscript(id:) O(1) on Catalog, add it to Lens and GroupedLens#43
searls merged 1 commit into
mainfrom
claude/compassionate-visvesvaraya-ecfef1

Conversation

@bitsly

@bitsly bitsly commented May 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves #42.

Catalog.subscript(id:) was an O(n) linear scan over items. In selection-driven SwiftUI code a single body evaluation can resolve "which item is selected right now" several times across child views (sidebar, content, detail) on every keystroke, every .onChange, every selection transition that observers care about — the cost of the scan amplifies under high-churn observation.

Each of the three sequence-of-Resource types now keeps a private @ObservationIgnored Dictionary<Item.ID, Item> rebuilt via didSet on items. Subscripts read _ = items first so observers register on the public source of truth, then look up via the dict — O(1).

  • Catalog.subscript(id:) is now O(1) (was O(n)).
  • New Lens.subscript(id:) and GroupedLens.subscript(id:) returning items from the lens's filtered projection (an item filtered out returns nil even if present in the source).
  • Duplicate-id behavior preserved: first occurrence wins, matching the prior items.first { \$0.id == id } semantics.

Notes for reviewers

  • didSet does not fire for assignments inside an init body, so Catalog.init(initialItems:) calls rebuildItemsByID() explicitly after seeding. Lens/GroupedLens inits call refresh() (a method call), where didSet does fire — verified by the new subscriptReturnsMatchingItem tests, which exercise the subscript immediately after construction with no async wait.
  • @Observable + didSet is an existing pattern in this codebase (Setting.swift:21-29).
  • The dict is @ObservationIgnored so the cache is invisible to the observation system; items remains the only observed signal.

Test plan

  • swift test — all 145 tests pass.
  • script/test — full suite + 100% line coverage gate on `Sources/Splint/` (Catalog 67/67, Lens 43/43, GroupedLens 92/92).
  • script/lint — clean.
  • New tests in CatalogTests.swift, LensTests.swift, GroupedLensTests.swift cover: refresh reflection, post-clear nil, seeded subscript, filter exclusion (Lens/GroupedLens), filter-update reflection, and duplicate-id first-wins.

🤖 Generated with Claude Code

Prompt:
Take a look at issue #42 for me

Response:
Let me check the `Resource` protocol and benchmark setup to see what the existing patterns look like.I've read the issue and the relevant code. Before drafting a plan, I want to clarify scope on a couple of points the issue itself flags as open.I'll run /grill-me to stress-test the plan before writing it, as the workflow requires.Good question — let me verify by grepping rather than assume.Confirmed — `subscript(id:)` only exists on `Catalog` today. `Lens` and `GroupedLens` are the only other types holding a sequence of `Resource` values (`Selection`/`Job`/`Setting`/`Credential` are all single-value). So the candidate set is exactly those three.

Continuing the grill:One thing I want to verify before locking in `didSet`: does `@Observable` cleanly support `didSet` on its instrumented properties, or do we hit a macro/setter quirk? Let me check.`Setting.value` already uses `@Observable` + `didSet` (Setting.swift:21-29), so the pattern is proven in this codebase. Locking in `didSet`.

I have all the decisions I need. Summarizing:

- **Scope:** Catalog (existing subscript), Lens (new subscript), GroupedLens (new subscript). Confirmed those are the only sequence-of-Resource holders.
- **Duplicate ids:** `Dictionary(_:uniquingKeysWith: { first, _ in first })` to preserve current "first wins" silent behavior.
- **Observation:** dict is `private @ObservationIgnored`, subscript does `_ = items` first so observers register on `items` (the source of truth).
- **Rebuild site:** `didSet` on `items` — single rule, can't be forgotten across the 3 assignment sites in Catalog or the 1 in each Lens.
- **Lens.subscript[id:] semantics:** lookup in the lens's filtered `items` (an item filtered out returns nil). The only sensible reading.

Now writing the plan.Plan approved. Before coding, I need to flag one assumption I should verify: Swift's `didSet` does **not** fire for assignments inside `init` itself. For Catalog's `init(initialItems:)`, that means `self.items = initialItems` would not seed `itemsByID`. For Lens/GroupedLens, `init` calls `refresh()` which assigns `items` from inside a method call — semantics are murkier there.

I'll handle this defensively by calling `rebuildItemsByID()` explicitly at the end of each init regardless of whether `didSet` would fire. Idempotent and safe.

Loading TodoWrite to track the implementation, then writing tests first per the project's TDD rule.Let me first read the existing test files completely so I match their conventions before adding tests.Now writing the new failing Catalog tests.Run those new Catalog tests now to confirm three of the four fail (the seeded one will already pass since `items.first` works there).The 4 new Catalog tests pass against the current O(n) implementation — that's expected since the refactor preserves behavior. They're regression guards (especially the duplicate-ID and seeded cases). The Lens/GroupedLens tests will be the ones that actually fail-then-pass, since they exercise new API that doesn't exist yet. The SourceKit diagnostic is unrelated (IDE metadata, not a build error — `swift build` succeeded).Let me verify the Lens tests actually fail to compile (since `subscript(id:)` doesn't exist on `Lens` yet).Confirmed: the Lens tests fail to compile because `subscript(id:)` doesn't exist yet. Now adding GroupedLens tests.Now implementing. Catalog first.Now running tests to confirm Catalog still works.Test-bundle compile fails because Lens/GroupedLens don't have the new subscript yet. Implementing those next.Now GroupedLens.Running the test suite now to verify implementation. The key concern: did `didSet` fire when `refresh()` is called from Lens/GroupedLens init?All 145 tests pass. `didSet` fires correctly when `refresh()` is called from init. Now updating CHANGELOG, then running `script/test` for the coverage gate.Now running `script/test` for the full suite + coverage gate, and `script/lint` in parallel.I need to see the coverage gate result, which runs after tests. Let me check.100% coverage on all Splint sources. Lint also passed (empty output = clean).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@claude

claude Bot commented May 3, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@searls
searls merged commit 8031f49 into main May 3, 2026
2 checks passed
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.

Catalog.subscript(id:) is O(n); maintain an id-keyed index for O(1) lookup

2 participants