Skip to content

chore: codebase-review cleanups — serial I/O, keychain triple-read, type-registration drift, dead code #35

Closed

Description

@2000game

Verified cleanup/efficiency/altitude findings from the 2026-07-08 whole-codebase review that don't overlap #17. None are release blockers; batched so they don't get lost. Ordered: efficiency (user-visible latency) → altitude (future breakage) → dead code.

Efficiency (API round-trips are ~100ms each; these are user-visible)

  1. dynamicField.fold runs 2N serial round-trips (src/engine/synthetic.ts:82-103): ruleset + status awaited sequentially per group in a sequential loop. mapConcurrent (used by build.ts at concurrency 8) + Promise.all per pair → ~8× faster plan/apply with many dynamic groups.
  2. buildPlan and buildPermissionPlan awaited back-to-back though independent (commands/apply.ts:99-100, commands/plan.ts:34-35): Promise.all hides the slow instance-wide /permissions/group_role fetch behind the concurrent resource fetches.
  3. Permission writes fully serial (permissions/apply.ts:24-34): one PUT/DELETE per tuple; first rollout of a permissions blueprint (50+ grants) ≈ 5s. Modest concurrency (4–8) per item is safe — tuples are independent rows.
  4. Keychain read 3× per run: resolveConfig() (command) + resolveConfig() + readToken() (authedSession) each spawn security find-generic-password for the same entry (commands/plan.ts:25, src/api/session.ts:15-16). Memoize per process or have authedSession return its resolved config. Up to three keychain-unlock prompts on a locked keychain.

Altitude / future breakage

  1. --refresh hardcodes the dynamic field + demote sentinel in the command layer (commands/apply.ts:52-55): duplicates knowledge owned by synthetic.ts (:96-97, :113). SyntheticField has no post-apply hook — add one (postApply?(client, state, item, changes)) and move refresh behind it; renaming the field today silently breaks --refresh (find() matches nothing).
  2. Type registration is scattered, and TYPE_TIER carries phantom types: adding a resource type requires registry.ts + graph.ts TYPE_TIER (computePlan throws otherwise, plan.ts:85-89) + config/context.ts DSL entry — despite registry.ts:7 claiming "adding a type = adding an entry here". TYPE_TIER lists group-status etc. that exist nowhere else. Consolidate: registry entry owns tier + DSL name (which also gives configSnippet the correct function name — see the groupRole-collision bug) and derive TYPE_TIER/ConfigContext from it.
  3. Engine trusts raw arrays — dup-key silently last-wins: dup-key/reference validation lives only in the DSL path (context.ts:146-167, :200); computePlan builds desiredByKey that silently collapses duplicates while the plan loop emits both (plan.ts:92-97). All CLI paths go through evaluateConfig today, but the first programmatic caller (import command, test harness) gets state-corrupting behavior. Cheap fix: computePlan throws on duplicate keys.
  4. Envelope/bootstrap duplication in ctClient (ctClient.ts:50-67, :110-125): refreshCsrfToken can be this.get('/csrftoken') (it runs with a cookie; GET skips the CSRF branch). authenticate genuinely can't reuse request() (cookie bootstrap) — but both currently assume .data exists while request() tolerates its absence; align the unwrap.

Dead code / micro

  1. Unreachable host check in commands/plan.ts:29-31 — loadState(path, config.host) two lines above already throws on mismatch (state.ts:70-73), with a different message. Delete.
  2. Opt-in predicate encoded 3× per fold (hierarchy.ts:79,83,93; synthetic.ts:79,83,105): the optedIn-Set inverted-iteration pattern; a single pass over desired with a state.resources[d.key] lookup is behavior-identical (preserve the managed-type guard) and leaves one copy of the predicate.
  3. No-op sort in permissions/grants.ts:23-24 (dataId is [] or single-element; tupleKey re-sorts anyway) — implies a multi-id invariant that doesn't exist; drop it.
  4. loadCatalog() naming (permissions/catalog.ts:16-18): wraps a build-time import constant, "loads" nothing; export the constant (callers already defensively snapshot it).

Acceptance

  • Items 1–4: plan/apply wall-clock measurably improves on a config with ≥10 dynamic groups + grants (before/after timing note in the PR).
  • Items 5–7: registry-driven tier/DSL-name; postApply hook; computePlan dup-key throw — each with a test.
  • Items 9–12: deleted/simplified with tests still green.

Activity

  1. added theissue type on Jul 8, 2026
  2. 2000game commented on Jul 9, 2026

    @2000game
    MemberAuthor

    Addendum from the PR #18 three-agent review — minor follow-ups that belong in this cleanup batch:

    1. Permissions: un-declaring a block never revokes its grants (no state tracking). Grants are reconciled statelessly from the current config only; a deleted groupRole/groupTypeRole block leaves its applied grants live forever, and ct destroy never touches permissions. Terraform-contract violation; needs state-tracked permission domains so removal plans deletions. (This is the largest item here — promote to its own issue when picked up.)
    2. Permission apply has no partial-progress recovery messaging (permissions/apply.ts bare loop): a mid-loop failure throws a raw stack after resources were already applied, without the resumable "Stopped at …" messaging executePlan provides. Re-running recovers, but the UX contradicts the resource path.
    3. Status-only dynamic change re-PUTs the full ruleset (synthetic.ts apply path): a pure status flip (active→inactive) also rewrites the byte-identical ruleset; wasteful and may trigger server-side recalculation.
    4. ct plan --json output shape changed from the bare plan object to { plan, permissions } — breaking for script consumers; note in changelog/README when a changelog exists.
  3. added a commit that references this issue on Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions