spec-kitty / spec-kitty/spec-kitty
[Perf 1] Doctrine/config assets (glossary store, org-pack config, agent-profile repository) are re-parsed in full on every lookup with no per-invocation caching
- Dominant language
- Python
- Stars
- 1.6k
- Forks
- 165
- Avg merge
- 14h 52m
- Merged PRs (30d)
- 303
Description
## Problem
Several independent doctrine/config subsystems build a brand-new, fully-parsed representation of a repo-wide asset every time a narrow lookup needs it, with zero caching across calls within the same CLI invocation:
1. **Glossary annotation during template rendering.** `render_template_text()` (`src/specify_cli/template/renderer.py:74-102`) unconditionally calls `_annotate_glossary_refs_from_store()` (renderer.py:137-189) for every rendered command template. That helper constructs a fresh `GlossaryStore` and, for each of the 4 `GlossaryScope`s, calls `load_seed_file()` (`src/glossary/scope.py:109-149`), which builds a new `ruamel.yaml.YAML()` instance, re-reads the seed YAML from disk, and re-runs full Pydantic validation — every single render, not once per invocation.
2. **Global command materialization runs the same expensive render pipeline 2-3x per invocation.** `ensure_global_agent_commands()` → `_apply_command_assessment()` (`src/specify_cli/runtime/agent_commands.py:381-416`) passes a `rebuild` closure that `apply_with_reassess` may invoke again to verify convergence, re-running `_build()` → `_render_agent_commands()` (agent_commands.py:310-367) — the full render including (1) above — even with no contention.
3. **6-tier asset resolver re-parses `.kittify/config.yaml` on every `resolve_template`/`resolve_command` call.** `_resolve_asset()` (`src/charter/offering/resolver.py:151-264`) calls `resolve_org_roots(project_dir, quiet=True)` → `load_pack_registry(repo_root)` (`src/charter/offering/org_pack_config.py:432-483`) → `_load_yaml_data(_config_path(repo_root))` (org_pack_config.py:628-630) on every call, with a code comment acknowledging this "is a resolution hot path that may run many times per invocation" yet still doing no memoization.
## Evidence
Profiled `spec-kitty init --ai=claude --non-interactive` (single agent, scratch project): `render_command_template` called 104 times, cumtime 25.958s of 47.578s total profiled runtime (~55%); `_annotate_glossary_refs_from_store` 104 calls, cumtime 25.621s; `load_seed_file` 416 calls (4 scopes × 104 renders), cumtime 22.950s; `ruamel.yaml` load/compose/parse machinery totaled ~30s cumtime across 275 YAML loads. Real (non-profiled) wall-clock for the same single-agent `init` was 15.24s (12.97s user). `ensure_global_agent_commands` was called 2 times in one solo `init` run, cumtime 26.517s total (13.26s average per call) — the render pipeline ran multiple times in one uncontended invocation via `retry_torn_read(_build)` (agent_commands.py:373) wrapping `apply_with_reassess(..., rebuild, ...)` (agent_commands.py:392-397). No `lru_cache`/memoization present in `renderer.py`, `glossary/scope.py`, or `org_pack_config.py` for any of these paths.
## User impact
Every `spec-kitty init` and every command-template regeneration during `spec-kitty upgrade`/migrations pays this cost, scaling linearly with (selected agents) × (~12 command templates per mission) × (4 glossary scopes), then multiplied again by the 2-3x re-render amplification. This repo's own `.kittify/glossaries/spec_kitty_core.yaml` (686 lines/35KB) is a realistic size for any project that has adopted the glossary feature — this is not a dev-checkout artifact. Separately, any command that resolves many individual templates/commands through `resolve_template`/`resolve_command` in a loop (mission inspection, `doctor`/`show-origin` diagnostics, mission-setup-plan) re-parses `.kittify/config.yaml` once per file resolved.
## Suggested fix
- Build the glossary `term_surfaces` mapping (or the loaded `GlossaryStore`) once per `repo_root` per process and thread it through `render_command_template`/`generate_agent_assets`, e.g. via `functools.lru_cache` keyed on `repo_root`.
- Once rendering is cheap, the 2-3x re-render amplification in `_apply_command_assessment` stops mattering much; if further tightening is wanted, memoize `_render_agent_commands(agent_key, templates_dir, script_type)` per invocation since it's a pure function of its inputs.
- Cache `load_pack_registry(repo_root)` for the duration of a CLI invocation (`.kittify/config.yaml` never changes mid-invocation in these flows).
None of these files are mutated mid-invocation in any of the three call chains, so invocation-scoped caching is safe.
---
**Impact:** 5/5 · **Feasibility:** 5/5 · **Priority (I×F):** 25
Part of epic #4514.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Contributor guide
Research direction
Trace the named paths in src/specify_cli/template/renderer.py, src/glossary/scope.py, src/specify_cli/runtime/agent_commands.py, and src/charter/offering/org_pack_config.py, starting with the profiling evidence and existing resolution hot-path comments. Identify invocation-safe cache boundaries for glossary data, rendered commands, and the pack registry. Done means repeated lookups in one CLI invocation avoid redundant parsing or rendering while preserving the existing resolution and generation behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli, performance, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100