Skip to content

feat: implement high-performance atom cache and maintenance tools (Issue #29)#54

Open
c-ferrier wants to merge 6 commits into
Ian-stetsenko:mainfrom
c-ferrier:feat/issue-29-atom-cache
Open

feat: implement high-performance atom cache and maintenance tools (Issue #29)#54
c-ferrier wants to merge 6 commits into
Ian-stetsenko:mainfrom
c-ferrier:feat/issue-29-atom-cache

Conversation

@c-ferrier

Copy link
Copy Markdown
Contributor

This PR implements Phase 1: The Immutable Atom Cache for #29, providing a foundational performance boost for all Lore queries.

Key Improvements:

  • ~52% Speedup: Confirmed by eliminating the N+1 Git process bottleneck during repository scanning.
  • Sharded Storage: Implements an immutable, sharded plain-text cache in .lore/cache/atoms.
  • Setup Automation: lore init now automatically ensures .lore/cache is ignored by your .gitignore.
  • Maintenance Tools: Added lore cache --clean to safely manage or reset local storage.
  • Full Verification: 444 passing unit tests and updated architectural documentation.

This unified implementation replaces the draft approach in the previously closed PR #47.

@c-ferrier c-ferrier requested a review from Ian-stetsenko as a code owner May 13, 2026 22:49
@c-ferrier c-ferrier force-pushed the feat/issue-29-atom-cache branch from ba52aba to 47cd720 Compare May 13, 2026 22:52
…intenance tools

Optimizes repository scanning by caching git file lists in a sharded plain-text filesystem structure within .lore/cache/atoms. This eliminates the N+1 Git process bottleneck during metadata extraction. To provide a complete maintenance lifecycle, 'lore init' now automatically ensures '.lore/cache' is added to .gitignore, and a new 'lore cache --clean' command allows users to manually reset local storage. The implementation follows the Null Object pattern for safe bypass and is fully configurable via CLI flags and environment variables.

Lore-id: f6cc4348
Constraint: Must only cache file lists to remain agnostic to config changes
Constraint: Cache files must not have an extension to avoid redundancy
Constraint: Storage directory MUST be .lore/cache/atoms
Constraint: Service instantiation MUST happen inside main() for async config loading
Constraint: Bypass logic must reside in src/util to avoid side effects during tests
Constraint: Gitignore automation must be localized to the .lore initialization directory
Constraint: Cache cleaning MUST use recursive force removal for robustness
Rejected: Full JSON caching of trailers | Increased complexity and potential for stale data
Rejected: Keeping shouldBypassCache in main.ts | Caused unexpected execution during unit tests
Rejected: Automatic .gitignore update on every command | Too intrusive; belongs in 'init'
Rejected: Adding --clean to 'init' | Better as a dedicated 'cache' command for future extensibility
Tested: Verified all 444 unit tests pass (including 6 new tests for init/cache commands)
Tested: Benchmark: ~52% speedup confirmed by eliminating N+1 git processes
Tested: Verified bypass via --no-cache, LORE_NO_CACHE=true, and config flags
Tested: Verified .gitignore creation and idempotency in 'lore init'
Tested: Verified successful directory removal in 'lore cache --clean'
Confidence: high
Scope-risk: narrow
Reversibility: clean
Assisted-by: Gemini:CLI [lore-protocol]
@c-ferrier c-ferrier force-pushed the feat/issue-29-atom-cache branch from 47cd720 to d1b8d48 Compare May 13, 2026 22:53
@c-ferrier

Copy link
Copy Markdown
Contributor Author

Ready for feedback or merging.

@Ian-stetsenko Ian-stetsenko left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nice work on this one. Architecture looks good — clean interface, Null Object done right, files-only as we discussed. Few things to address:

  1. process.exit(1) in cache.ts — Project convention is process.exitCode = 1; return; (see CLAUDE.md). process.exit() kills the process immediately and can lose pending I/O output.

  2. error: any in cache.ts and atom-cache.ts — Should be error: unknown with type narrowing (error instanceof Error ? error.message : String(error) for messages, (error as NodeJS.ErrnoException).code for filesystem errors). Matches the pattern in config-loader.ts and head-lore-id-reader.ts.

  3. Corrupted cache file returns garbage — If a cache file is partially written or contains bad data, content.split('\n') returns garbage strings that silently corrupt query results. getFiles should return null (treat as cache miss) when content looks wrong (e.g., contains NUL bytes or is empty when it shouldn't be).

  4. No test for corrupted cache — Add a test that writes bad content to a cache path and verifies getFiles returns null instead of garbage.

… standards

Refines the AtomCache implementation to handle potential data corruption and align with established project patterns for error handling and process execution.

To prevent "garbage" results from partial writes or filesystem errors, getFiles now validates cache content for NUL bytes and emptiness before parsing. The write path has been upgraded to an atomic "write-then-rename" strategy using unique temporary files, minimizing the risk of corruption during concurrent access or process crashes.

Error handling has been migrated from any to unknown with explicit narrowing, and the cache command now uses process.exitCode instead of process.exit() to ensure clean I/O termination.

Lore-id: fbd91a65
Constraint: Must treat empty, whitespace-only, or NUL-containing files as cache misses
Constraint: Atomic writes MUST use randomUUID to prevent collision between concurrent processes
Constraint: Command errors MUST set process.exitCode and return to avoid abrupt termination
Constraint: Error narrowing MUST follow the pattern in config-loader.ts
Rejected: Using fs.writeSync | Prefer async atomic rename for better performance and consistency
Rejected: Trimming all whitespace | Must preserve single newline for --allow-empty commits
Tested: Added corruption tests covering NUL bytes, empty files, and whitespace
Tested: Verified project-wide suite (445 tests) passing
Depends-on: f6cc4348
Confidence: high
Assisted-by: Gemini:CLI [lore-protocol]
@c-ferrier c-ferrier requested a review from Ian-stetsenko May 14, 2026 03:16
@c-ferrier

Copy link
Copy Markdown
Contributor Author

Updates in a new commit in the pull request. Let me know what you think.

Implemented robust Lore root resolution to ensure the atom cache is shared across the entire project, even when running commands from subdirectories.

The resolution logic (now extracted to a testable utility) prioritizes the nearest '.lore' directory, then the git repository root, and finally falls back to the current working directory. This prevents the creation of redundant '.lore' directories in sub-folders and ensures cache hits are consistent throughout the workspace.

Lore-id: 8b23be8b
Constraint: Root resolution MUST prioritize nearest .lore directory for monorepo support
Constraint: Git root is the primary fallback for standard repositories
Constraint: Resolution MUST be non-throwing (fallback to CWD on error)
Tested: Added 5 unit tests for resolveLoreRoot utility
Tested: Manually verified cache reuse from src/ subdirectory after build
Tested: Full suite (450 tests) passing
Depends-on: fbd91a65
Confidence: high
Assisted-by: Gemini:CLI [lore-protocol]
Ian-stetsenko

This comment was marked as resolved.

- Add hex hash validation in AtomCache to prevent path traversal
- Remove concrete NullAtomCache import from AtomRepository (DIP)
- Replace unsafe `as readonly string[]` cast with null guard
- Add comment to shouldBypassCache explaining pre-parse argv access
- Clarify ensureCacheIgnored uses cwd to match lore init convention
- Remove unused CONFIG_DIR import from cache.ts
- Fix duplicate step numbering in main.ts
- Fix blank lines in atom-cache.ts and atom-repository.ts
- Add test for invalid hash rejection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Ian-stetsenko

Copy link
Copy Markdown
Owner

Pushed fixes for all review findings:

Security:

  • Added hex hash validation (/^[0-9a-f]{7,64}$/i) in AtomCache — invalid hashes return null/no-op instead of path traversal. Added test for ../../../etc/passwd rejection.

DIP compliance:

  • Removed concrete NullAtomCache import from AtomRepository — constructor now requires IAtomCache explicitly. Composition root handles the wiring.

Type safety:

  • Replaced filesPerCommit[index] as readonly string[] cast with null guard that throws on BUG condition.

Convention fixes:

  • Added pre-parse documentation to shouldBypassCache
  • Clarified ensureCacheIgnored uses cwd intentionally (matches where lore init creates .lore/)
  • Removed unused CONFIG_DIR import from cache.ts
  • Fixed duplicate step "8" numbering in main.ts
  • Fixed blank lines in atom-cache.ts and atom-repository.ts

451 tests pass, typecheck clean.

@c-ferrier

c-ferrier commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

I've pushed two new commits to address the final feedback items:

1. Architectural Refactor: Root Resolver

Moved root-resolver.ts from src/util/ to src/services/. This aligns with the project convention that util/ should be for pure leaf nodes, while services handle I/O and more complex orchestration. All internal references and tests have been updated and verified.

2. Robustness Verification: Self-Healing Cache

Added a dedicated unit test in tests/unit/services/atom-cache.test.ts to empirically verify the "self-healing" mechanism we discussed. The test simulates binary corruption (NUL bytes) and empty files, confirming they are correctly detected as cache misses. It then verifies that the repository successfully overwrites the corrupted file with fresh data via an atomic rename.


Discussion: .gitignore and Monorepo Strategy

Regarding the .gitignore location in subdirectories, I wanted to clarify the intended design before making changes.

The current implementation follows a Monorepo Override pattern. The Lore configuration system is designed to walk up the directory tree and merge configurations, allowing sub-projects to have their own .lore/config.toml (e.g., for project-specific custom trailers or stricter validation).

By creating the .gitignore and .lore folder in the current directory (where lore init is run):

  • We ensure that a sub-project's local cache remains ignored by its local .gitignore.
  • We support isolated configuration boundaries within a larger repository.

However, I agree this could lead to "accidental fragmentation" in a standard single-project repository if run from a subdirectory.

How would you like to handle this duality?

  1. Status Quo: Keep as-is to prioritize monorepo flexibility.
  2. Explicit Intent Lore: Add a check to detect if we are already inside a Lore-enabled project. If so, lore init could require a --force flag or prompt for confirmation before creating a nested configuration.
  3. Explicit Intent GIT: Add a check to detect if we are not at a git root. If so, lore init could require a --force flag or prompt for confirmation before creating a nested configuration.
  4. Strict Root: Force lore init to always resolve to the Git root.

I'd love your take on the preferred architectural direction for monorepos before finalizing this part.

c-ferrier added 2 commits May 18, 2026 13:48
Relocated root-resolver from util/ to services/ as requested during architectural review. This aligns with project standards for modules performing complex logic and I/O. Updated all internal and test references.

Lore-id: ebe7b9c4
Constraint: Root resolution must be non-throwing (fallback to CWD on error)
Tested: Successfully moved src/util/root-resolver.ts to src/services/
Tested: Updated imports in src/main.ts and tests/unit/root-resolver.test.ts
Tested: Verified all 5 root-resolver unit tests pass
Tested: Full project suite (452 tests) passing
Confidence: high
Assisted-by: Gemini:CLI [lore-protocol]
Added a unit test to demonstrate that the AtomCache correctly handles and recovers from corrupted files. When binary corruption or empty files are encountered, the system treats them as a cache miss, allowing the repository to fetch fresh data and overwrite the 'bad' file via an atomic rename.

Lore-id: 61f54538
Constraint: Must treat empty, whitespace-only, or NUL-containing files as cache misses
Tested: Added 'should recover from and overwrite corrupted cache files' test case
Tested: Verified detection of NUL bytes and whitespace-only corruption
Tested: Confirmed successful overwrite with valid data following a detected miss
Confidence: high
Assisted-by: Gemini:CLI [lore-protocol]
@c-ferrier c-ferrier force-pushed the feat/issue-29-atom-cache branch from 3e24008 to 7c87495 Compare May 18, 2026 20:48
@c-ferrier c-ferrier requested a review from Ian-stetsenko May 19, 2026 03:06
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.

2 participants