diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..5e140e6 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,103 @@ +# AGENT.md — Patchy Codegen Agent Contract (Python 3.11, PyQt6) + +> Single source of truth for converting codegen-ready pseudocode in `/modules/*.pseudocode.md` into production Python in `/src/` with tests in `/tests/`. +> This file is loaded for every Codex Cloud task. + +## 0. Operating Mode +- **Language**: Python 3.11 +- **UI stack**: PyQt6 (widgets; no async GUI frameworks) +- **Test**: pytest +- **Lint/Format**: ruff (`ruff check .` and `ruff format --check .`) +- **Filesystem layout**: + - Input pseudocode: `/modules/{slug}.pseudocode.md` + - Output code: `/src/{path}.py` (exact path comes from META.target_file) + - Output tests: `/tests/test_{slug}.py` + +## 1. Pseudocode DSL the agent must follow +The source files contain this structure (sections are mandatory unless noted): +- ` ... ` — machine-readable. Must be parsed and used verbatim. + - Keys: `slug`, `target_file`, `language`, `runtime.python`, `index_base`, `newline`, `dependencies`, `acceptance.lint`, `acceptance.tests` +- `## PURPOSE` — produce a module that does exactly this and nothing more. +- `## SCOPE` — enforce **in-scope** and reject **out-of-scope** behavior. +- `## IMPORTS - ALLOWED ONLY` — only import from these modules. Prefer absolute imports under `src/` layout (e.g., `from core.contracts import ...`) mapped to package root. +- `## CONSTANTS` — copy verbatim; do not modify names or values. +- `## TYPES - USE ONLY SHARED TYPES` — use types from `core.contracts` where referenced. +- `## INTERFACES` — treat each bullet as a concrete signature; implement exactly. Respect pre/post/error contracts. +- `## STATE` — implement state exactly as specified; no hidden globals. +- `## THREADING` — adhere to policy. UI thread only mutates widgets; heavy work in worker threads. +- `## I/O` — only the specified inputs/outputs/side effects. Newline policy: normalize to **LF** on read; write **LF**; preserve final newline. +- `## LOGGING` — use `logging.getLogger("patchy")`. Log INFO at start, WARNING for recoverable issues, ERROR before raising exceptions. +- `## ALGORITHM`, `### EDGE CASES`, `## ERRORS`, `## COMPLEXITY`, `## PERFORMANCE`, `## TESTS - ACCEPTANCE HOOKS`, `## EXTENSIBILITY`, `## NON-FUNCTIONAL` — implement and honor all items. + +## 2. Global Conventions (enforced) +- **Dataclasses**: use Python `@dataclass` for `FilePatch`, `Hunk`, `HunkLine`, `ApplyResult` (declared in `core.contracts`). +- **Exceptions**: raise only from the hierarchy (`PatchyError`, `ParseError`, `ApplyError`, `ValidationError`, `IOErrorCompat`). +- **Indices**: all line indices are **0-based** (`INDEX_BASE=0`). Document in code. +- **EOL**: read-normalize to LF; write LF; preserve trailing newline where applicable. +- **Determinism**: no randomness, no time-sensitive formatting in code paths under test. +- **Threading**: UI widgets and signals on GUI thread; background work via `concurrent.futures.ThreadPoolExecutor` with marshaling back to the UI thread using Qt queued calls (`QMetaObject.invokeMethod`/`QTimer.singleShot` or an equivalent signal bridge). Never block the UI thread with long work. +- **Logging**: do not print; use `logging`. Include start/warn/error markers noted in pseudocode. +- **No hidden dependencies**: only imports listed under `IMPORTS - ALLOWED ONLY`. +- **No API drift**: function/class names and signatures must match pseudocode exactly. + +## 3. File and Package Mapping +- Treat `src/` as the project root package. Example: `src/core/contracts.py` is importable as `core.contracts`. +- Do not create new top-level packages or nested packages not implied by `target_file`. +- For each pseudocode file: + - Write exactly **one** `.py` module to `META.target_file`. + - Write exactly **one** test file to `/tests/test_{slug}.py`. + +## 4. Test Generation Rules +- Convert **## TESTS - ACCEPTANCE HOOKS** bullets into concrete `pytest` tests. +- Use deterministic fixtures (small inline content). Do not hit the network or filesystem unless allowed by the module’s I/O. +- For UI tests, focus on logic-level verification (signals emitted, function outputs) rather than rendering. +- Ensure full test isolation and idempotence. No global state leaks across tests. +- Respect linting: generated tests must also pass ruff. + +## 5. Module-Specific Guidance (common patterns) +- **core.contracts**: define dataclasses, constants (e.g., `SKIP_PREFIXES`, `UNIFIED_HUNK_HEADER_REGEX`, `CONTEXT_HUNK_HEADER_REGEX`, `INDEX_BASE`, `NEWLINE_POLICY`), exception classes, and a `get_logger()` helper returning `logging.getLogger("patchy")` configured once. +- **core.diff_parser**: implement exact unified/context diff grammar using constants; build `FilePatch`/`Hunk`/`HunkLine`; keep order stable; strict validation; no filesystem writes. +- **core.diff_applier**: implement strict-and-fuzzy application with bounded search; produce `ApplyResult` with `added_lines`, `removed_original_indices`, and `origin_map`; enforce 0-based, sorted indices. +- **utils.state**: JSON-only, atomic writes (temp + replace), schema-lite validation per key; no YAML. +- **utils.theme**: single source of palette; publish changes via callbacks or a small signal bridge (no OS polling here). +- **ui.code_editor**: define explicit signals `on_content_changed(str)`, `on_cursor_moved(int,int)`, `on_scroll(int,int)`; validate indices; LF-only content. +- **ui.highlighters**: consume palette from Theme; batch large updates; no OS detection. +- **ui.navigation**: compute contiguous blocks from `ApplyResult.added_lines` and `.removed_original_indices`; provide wrap-around next/prev. +- **ui.main_window**: orchestrate collaborators; all heavy work in workers; safe error surfaces; save/restore state; connect/disconnect signals cleanly. +- **app.entry_point**: parse CLI, bootstrap logging, create `QApplication` before widgets, safe shutdown with exit code. + +## 6. Failure Policy +- If a pseudocode section contradicts another, prefer: CONSTANTS > INTERFACES > ALGORITHM > SCOPE > PURPOSE. +- If a section is missing, **fail the task** with a clear explanation rather than inventing behavior. +- If allowed imports are insufficient to satisfy the interfaces, fail with a deterministic list of missing imports. + +## 7. Output Requirements +- The generated module file must be **complete and runnable** without manual edits. +- Every public function/class must have a short docstring reflecting pre/post/error conditions. +- Include `from __future__ import annotations` where useful. +- **All** logging, warnings, and significant actions must be issued via the module logger. +- Tests must pass: run `pytest -q` for the specific file and ensure `ruff check .` passes. + +## 8. Command Map (what Codex Cloud should run) +- Lint: `python -m ruff check . && python -m ruff format --check .` +- Tests (single module): `pytest -q tests/test_{slug}.py` +- Full suite (optional): `pytest -q` + +## 9. Examples of transforming acceptance hooks to tests +Given a hook: `- assert len(result.origin_map) == len(result.text.splitlines())` +Produce a pytest test: +```python +def test_origin_map_length_matches_output(apply_sample): + result = apply_sample() + assert len(result.origin_map) == len(result.text.splitlines()) +``` + +## 10. Prohibited Behaviors +- No third-party dependencies not listed in `IMPORTS - ALLOWED ONLY`. +- No network or disk access outside declared I/O. +- No random sleeps, time-based variability, or OS-specific assumptions without guards. +- No mutation of other modules unless explicitly allowed by SCOPE. + +--- + +**End of AGENT.md.** The agent must treat this document as normative and refuse ambiguous generation rather than guessing. \ No newline at end of file diff --git a/modules/app.entry_point.pseudocode.md b/modules/app.entry_point.pseudocode.md index 7493c02..67df07f 100644 --- a/modules/app.entry_point.pseudocode.md +++ b/modules/app.entry_point.pseudocode.md @@ -1,37 +1,122 @@ -# Application Entry Point Pseudocode - -## main() -1. CREATE QApplication -2. SET application metadata: - - Name: "Patchy" - - Version: "2.0.0" - - Organization: "Patchy Project" -3. INITIALIZE exception handler -4. CREATE MainWindow -5. HANDLE command line arguments: - - IF file path provided: - - LOAD file - - IF diff path provided: - - LOAD diff -6. SHOW main window -7. START event loop -8. ON exit: - - SAVE state - - CLEANUP resources - -## Exception Handler -1. SET global exception handler -2. ON unhandled exception: - - LOG to console - - SHOW error dialog - - SAVE crash report - - GRACEFUL exit - -## CLI Arguments -1. PARSE arguments: - - --file: Open specific file - - --diff: Open specific diff - - --folder: Open specific folder - - --theme: Force theme (light/dark) - - --debug: Enable debug mode -2. APPLY parsed arguments \ No newline at end of file +# app.entry_point - Codegen-Ready Pseudocode Template + + + +{ + "slug": "app.entry_point", + "target_file": "src/main.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Start application: parse CLI, initialize logging, create main window, run event loop with graceful shutdown. + +## SCOPE +- In-scope: + - CLI parse + - Global exception handler + - Startup banner + - Exit code propagation +- Out-of-scope: + - Business logic + - Heavy computation + +## IMPORTS - ALLOWED ONLY + +- - import sys +- - import argparse +- - from core.contracts import PatchyError +- from PyQt6.QtWidgets import QApplication + +## CONSTANTS +- APP_NAME = 'Patchy' + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def main(argv: list[str]) -> int + - pre: argv is list[str] + - post: returns 0 on success else non-zero + - errors: none + + +## STATE +- none: stateless + +## THREADING +- ui_thread_only: true +- worker_policy: none +- handoff: n/a + +## I/O +- inputs: ["argv"] +- outputs: ["exit code"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start app.entry_point" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Parse CLI args +2) Initialize logging +3) Create QApplication and MainWindow +4) Run event loop +5) Catch top-level exceptions, log, set exit code + +### EDGE CASES +- - No args → default behavior +- - Unknown arg → exit with usage 2 +101) Create QApplication before constructing MainWindow + +## ERRORS +- - ValidationError on bad CLI combinations + +## COMPLEXITY +- time: O(1) +- memory: O(1) +- notes: none + +## PERFORMANCE +- max input size: n/a +- max iterations: n/a +- timeout: none +- instrumentation: +- record startup time + +## TESTS - ACCEPTANCE HOOKS +- assert exit code semantics respected + +## EXTENSIBILITY +- - Subcommands can be added later + +## NON-FUNCTIONAL +- security: no elevated privileges +- i18n: UTF-8 \ No newline at end of file diff --git a/modules/core.contracts.pseudocode.md b/modules/core.contracts.pseudocode.md new file mode 100644 index 0000000..7b0d9e4 --- /dev/null +++ b/modules/core.contracts.pseudocode.md @@ -0,0 +1,126 @@ +# core.contracts - Codegen-Ready Pseudocode Template + + + +{ + "slug": "core.contracts", + "target_file": "src/core/contracts.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Provide shared dataclasses, exceptions, constants, logging bootstrap for Patchy modules. + +## SCOPE +- In-scope: + - Define FilePatch, Hunk, HunkLine, ApplyResult dataclasses + - Provide exception hierarchy: PatchyError, ParseError, ApplyError, ValidationError, IOErrorCompat + - Expose constants: SKIP_PREFIXES, UNIFIED_HUNK_HEADER_REGEX, CONTEXT_HUNK_HEADER_REGEX, INDEX_BASE, NEWLINE_POLICY + - Initialize module logger 'patchy' +- Out-of-scope: + - Business logic + - Qt widgets + - Disk I/O beyond reading config + +## IMPORTS - ALLOWED ONLY + +- - from dataclasses import dataclass +- - from typing import Optional, List, Tuple, Dict, Any +- - import logging + +## CONSTANTS +- SKIP_PREFIXES = ["diff --git ", "index ", "new file mode ", "deleted file mode ", "--- ", "+++ ", "*** ", "rename from ", "rename to ", "similarity index ", "Binary files "] +- UNIFIED_HUNK_HEADER_REGEX = "^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@.*$" +- CONTEXT_HUNK_HEADER_REGEX = "^\*\*\* (\d+),(\d+) \*\*\*\*$" +- INDEX_BASE = 0 + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- class Contracts: ... + - pre: none + - post: types and constants are importable + - errors: ValidationError on inconsistent constants + + +## STATE +- none: stateless + +## THREADING +- ui_thread_only: false +- worker_policy: none +- handoff: callback + +## I/O +- inputs: ["importers"] +- outputs: ["dataclasses", "exceptions", "constants"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start core.contracts" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Define dataclasses and exceptions +2) Define constants and export in __all__ +3) Configure logger with INFO level by default +4) Return nothing + +### EDGE CASES +- - Constants values must be import-safe strings or lists + +## ERRORS +- - ValidationError when constants conflict (e.g., index_base not 0) + +## COMPLEXITY +- time: O(1) +- memory: O(1) +- notes: none + +## PERFORMANCE +- max input size: n/a +- max iterations: n/a +- timeout: none +- instrumentation: +- logging configured exactly once + +## TESTS - ACCEPTANCE HOOKS +- assert types importable +- assert logger name equals 'patchy' +- assert INDEX_BASE == 0 +- assert isinstance(SKIP_PREFIXES, list) and 'rename from ' in SKIP_PREFIXES and 'deleted file mode ' in SKIP_PREFIXES + +## EXTENSIBILITY +- - New constants can be added without breaking imports + +## NON-FUNCTIONAL +- security: no secrets +- i18n: UTF-8 +- compliance: no telemetry \ No newline at end of file diff --git a/modules/core.diff_applier.pseudocode.md b/modules/core.diff_applier.pseudocode.md index 10e2320..ff9ee9c 100644 --- a/modules/core.diff_applier.pseudocode.md +++ b/modules/core.diff_applier.pseudocode.md @@ -1,80 +1,138 @@ -# Diff Applier Pseudocode - -## Main Function: apply_patch -1. INPUT: original_text (string), file_patch (FilePatch) -2. SPLIT original_text into lines -3. INITIALIZE: - - result_lines = copy of original_lines - - added_lines = empty list - - removed_original_indices = empty list - - line_bias = 0 - - origin_map = [0, 1, 2, ...] # Maps result index to original index -4. FOR each hunk in file_patch.hunks: - - guess_index = calculate_guess_index(hunk, line_bias) - - anchor_index = find_anchor_index(result_lines, hunk, guess_index) - - IF anchor_index is None: - - RAISE ApplyError("Cannot locate hunk") - - VERIFY hunk matches at anchor_index: - - IF verification fails: - - RAISE ApplyError("Context mismatch") - - APPLY hunk: - - FOR each hunk_line in hunk.lines: - - IF hunk_line.kind == ' ': - - IF hunk_line.text is empty: - - SKIP any blank lines - - ELSE: - - ADVANCE cursor - - ELIF hunk_line.kind == '-': - - RECORD original index from origin_map - - DELETE line from result_lines - - DELETE from origin_map - - DECREMENT line_bias - - ELIF hunk_line.kind == '+': - - INSERT line into result_lines - - INSERT None into origin_map - - ADD current index to added_lines - - INCREMENT line_bias -5. RETURN ApplyResult(joined_lines, added_lines, sorted(removed_original_indices)) - -## Function: find_anchor_index -1. INPUT: lines, hunk, guess_index -2. consuming_lines = filter hunk.lines for [' ', '-'] types -3. IF no consuming_lines: - - RETURN max(0, min(length(lines), guess_index)) -4. min_needed = calculate_min_needed(consuming_lines) -5. max_start = max(0, length(lines) - min_needed) -6. guess = clamp(guess_index, 0, max_start) -7. IF hunk_matches_at(lines, consuming_lines, guess): - - RETURN guess -8. TRY fuzzy search within fuzzy_context of guess -9. IF found, RETURN found index -10. TRY global scan of entire file -11. IF found, RETURN found index -12. RETURN None - -## Function: hunk_matches_at -1. INPUT: lines, consuming_lines, start_pos -2. cursor = start_pos -3. FOR each line in consuming_lines: - - IF line.kind == ' ': - - IF line.text is empty: - - SKIP any blank lines - - ELSE: - - IF cursor >= length(lines) OR lines[cursor] != line.text: - - RETURN False - - cursor += 1 - - ELIF line.kind == '-': - - IF cursor >= length(lines) OR lines[cursor] != line.text: - - RETURN False - - cursor += 1 -4. RETURN True - -## Function: calculate_min_needed -1. INPUT: consuming_lines -2. needed = 0 -3. FOR each line in consuming_lines: - - IF line.kind == '-': - - needed += 1 - - ELIF line.kind == ' ' AND line.text != '': - - needed += 1 -4. RETURN needed \ No newline at end of file +# core.diff_applier - Codegen-Ready Pseudocode Template + + + +{ + "slug": "core.diff_applier", + "target_file": "src/core/diff_applier.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Apply a parsed FilePatch to original text using strict or fuzzy anchoring and report line mappings. + +## SCOPE +- In-scope: + - Strict application of hunks + - Fuzzy recovery within bounded window + - Origin map generation +- Out-of-scope: + - Parsing diffs + - UI rendering + +## IMPORTS - ALLOWED ONLY + +- - from core.contracts import FilePatch, Hunk, ApplyResult, ApplyError +- - from typing import List, Optional + +## CONSTANTS +- INDEX_BASE = 0 + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def apply(original: str, patch: FilePatch, strict: bool = True) -> ApplyResult + - pre: original LF normalized; patch well-formed + - post: returns ApplyResult; origin_map length equals output line count + - errors: ApplyError on context mismatch +- def preview(original: str, patch: FilePatch) -> ApplyResult + - pre: same as apply + - post: returns ApplyResult without writing + - errors: none +- def calculate_guess_index(hunk: Hunk, prior_offset: int) -> int + - pre: prior_offset is int + - post: returns non-negative int + - errors: ApplyError on negative result +- def fuzzy_context() -> int + - pre: none + - post: returns default fuzz window in lines + - errors: none +- ApplyResult fields are 0-based and sorted: added_lines, removed_original_indices + +## STATE +- window_bias: int - current guessed offset + +## THREADING +- ui_thread_only: false +- worker_policy: none +- handoff: callback + +## I/O +- inputs: ["original string", "FilePatch"] +- outputs: ["ApplyResult"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start core.diff_applier" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Split original into lines +2) For each hunk, locate anchor line using strict match else fuzzy within window +3) Apply additions and deletions, updating running offset +4) Track added_lines and removed_original_indices (0-based) +5) Build origin_map mapping output line to original or null +6) Return ApplyResult + +### EDGE CASES +- - Empty patch → return original unchanged with empty deltas +- - Overlapping hunks → raise ApplyError +- - Excess fuzz window → clamp to limit + +## ERRORS +- - ApplyError when context cannot be located +- - ValidationError when patch inconsistent + +## COMPLEXITY +- time: O(n + m) +- memory: O(n + m) +- notes: none + +## PERFORMANCE +- max input size: 50MB +- max iterations: bounded by window per hunk +- timeout: none +- instrumentation: +- count fuzzy attempts +- measure per-hunk search time + +## TESTS - ACCEPTANCE HOOKS +- assert origin_map length equals output lines +- assert removed_original_indices sorted and unique +- assert sorted(result.removed_original_indices) == result.removed_original_indices +- assert len(result.origin_map) == len(result.text.splitlines()) + +## EXTENSIBILITY +- - Alternative search strategies can be injected later + +## NON-FUNCTIONAL +- security: pure in-memory +- i18n: UTF-8 +- compliance: no telemetry \ No newline at end of file diff --git a/modules/core.diff_parser.pseudocode.md b/modules/core.diff_parser.pseudocode.md index 4b0993c..c2d82a3 100644 --- a/modules/core.diff_parser.pseudocode.md +++ b/modules/core.diff_parser.pseudocode.md @@ -1,93 +1,134 @@ -# Diff Parser Pseudocode - -## Main Function: parse_diff_content -1. INPUT: diff_content (string) -2. IF diff_content is empty or only whitespace: - RETURN empty list -3. SPLIT diff_content into lines -4. INITIALIZE: - - patches = empty list - - current_patch = None - - index = 0 -5. WHILE index < length(lines): - - line = lines[index] - - IF line should be skipped (matches skip prefixes): - - index += 1 - - CONTINUE - - IF line starts file header: - - patch = parse_file_header(lines, index) - - IF patch is valid: - - APPEND patch to patches - - SET current_patch = patch - - index += 2 # Skip header pair - - CONTINUE - - IF line starts hunk header ('@@'): - - IF current_patch is None: - - RAISE ParseError("Hunk before file header") - - hunk, consumed = parse_hunk(lines, index) - - APPEND hunk to current_patch.hunks - - index += consumed - - CONTINUE - - index += 1 -6. RETURN patches - -## Function: parse_file_header -1. INPUT: lines, start_index -2. IF start_index + 1 >= length(lines): - RETURN None -3. DETECT header style: - - IF line matches unified pattern ('---'): - - old_path = extract path after '---' - - EXPECT next line starts with '+++' - - new_path = extract path after '+++' - - ELIF line matches context pattern ('***'): - - old_path = extract path after '***' - - EXPECT next line starts with '---' - - new_path = extract path after '---' - - ELSE: - RETURN None -4. CLEAN both paths: - - Remove timestamp after tab - - Remove 'a/' or 'b/' prefixes - - Strip whitespace -5. RETURN FilePatch(old_path, new_path, []) - -## Function: parse_hunk -1. INPUT: lines, start_index -2. header_line = lines[start_index] -3. PARSE header using regex: - - Extract old_start (default 1) - - Extract old_len (default 0) - - Extract new_start (default 1) - - Extract new_len (default 0) -4. CREATE hunk object with parsed values -5. index = start_index + 1 -6. WHILE index < length(lines): - - line = lines[index] - - IF line starts new section: - BREAK - - IF line is empty: - - ADD HunkLine(' ', '') - - ELIF first character is in [' ', '+', '-']: - - ADD HunkLine(kind, line[1:]) - - ELIF line is '\\ No newline...': - - SKIP - - ELSE: - - RAISE ParseError("Invalid hunk line") - - index += 1 -7. RETURN (hunk, index - start_index) - -## Function: should_skip_line -1. INPUT: line -2. FOR each skip_prefix in skip_prefixes: - - IF line starts with skip_prefix: - RETURN True -3. RETURN False - -## Function: clean_path -1. INPUT: path_string -2. REMOVE everything after tab character -3. IF path starts with 'a/' or 'b/': - - REMOVE first 2 characters -4. STRIP whitespace -5. RETURN cleaned path \ No newline at end of file +# core.diff_parser - Codegen-Ready Pseudocode Template + + + +{ + "slug": "core.diff_parser", + "target_file": "src/core/diff_parser.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Parse unified or context diff text into structured FilePatch and Hunk objects. + +## SCOPE +- In-scope: + - Parse headers, file sections, and hunks + - Skip noise lines using SKIP_PREFIXES + - Normalize EOL to LF +- Out-of-scope: + - Applying patches + - Filesystem writes + +## IMPORTS - ALLOWED ONLY + +- - from core.contracts import FilePatch, Hunk, HunkLine, ParseError, UNIFIED_HUNK_HEADER_REGEX, CONTEXT_HUNK_HEADER_REGEX, SKIP_PREFIXES +- - from typing import List, Tuple +- - import re + +## CONSTANTS +- UNIFIED_HUNK_HEADER_REGEX = "^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@.*$" +- CONTEXT_HUNK_HEADER_REGEX = "^\*\*\* (\d+),(\d+) \*\*\*\*$" +- SKIP_PREFIXES = ["diff --git ", "index ", "new file mode ", "deleted file mode ", "--- ", "+++ ", "*** ", "rename from ", "rename to ", "similarity index ", "Binary files "] +- INDEX_BASE = 0 + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def parse(content: str) -> list[FilePatch] + - pre: content is str; not None; LF normalized + - post: returns list of FilePatch; stable order by appearance + - errors: ParseError on grammar violation +- def validate(content: str) -> tuple[bool, list[tuple[int,str]]] + - pre: content is str + - post: returns validity and sorted error list + - errors: none +- def split_lines(content: str) -> list[str] - pre: content is str; post: LF-only lines; errors: none + +## STATE +- line_no: int - current line index + +## THREADING +- ui_thread_only: false +- worker_policy: none +- handoff: callback + +## I/O +- inputs: ["diff content string"] +- outputs: ["list[FilePatch]"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start core.diff_parser" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Split content into lines with LF +2) Iterate, skipping SKIP_PREFIXES and non-hunk noise +3) Detect file boundaries and create FilePatch entries +4) Match hunk headers via UNIFIED_HUNK_HEADER_REGEX or CONTEXT_HUNK_HEADER_REGEX +5) Accumulate HunkLine with kinds ' ', '+', '-' +6) Validate counts against header spans +7) Return FilePatch list + +### EDGE CASES +- - Empty content → return [] +- - Malformed header → raise ParseError with line number +- - Unknown line kind → raise ParseError + +## ERRORS +- - ParseError when grammar is violated + +## COMPLEXITY +- time: O(n) +- memory: O(n) +- notes: none + +## PERFORMANCE +- max input size: 50MB +- max iterations: n +- timeout: none +- instrumentation: +- count lines processed +- record header matches + +## TESTS - ACCEPTANCE HOOKS +- assert lines count equals sum of hunks +- assert only kinds in {' ', '+', '-'} +- assert re.compile(UNIFIED_HUNK_HEADER_REGEX) +- assert all(k in {' ', '+', '-'} for fp in parse(sample) for h in fp.hunks for k in [ln.kind for ln in h.lines]) + +## EXTENSIBILITY +- - Add context-diff support without changing parse signature + +## NON-FUNCTIONAL +- security: no file paths are executed +- i18n: UTF-8 only +- compliance: no telemetry \ No newline at end of file diff --git a/modules/ui.code_editor.pseudocode.md b/modules/ui.code_editor.pseudocode.md index 1a56825..11a9ff2 100644 --- a/modules/ui.code_editor.pseudocode.md +++ b/modules/ui.code_editor.pseudocode.md @@ -1,46 +1,132 @@ -# Code Editor Widget Pseudocode - -## Initialization -1. INHERIT from QPlainTextEdit -2. SET monospace font -3. CREATE LineNumberArea widget -4. CONNECT signals: - - blockCountChanged -> update_line_number_area_width - - updateRequest -> update_line_number_area -5. INITIALIZE line number area - -## Line Number Area -1. CREATE custom QWidget -2. OVERRIDE paintEvent: - - GET first visible block - - FOR each visible block: - - DRAW line number - - MOVE to next block -3. OVERRIDE sizeHint: - - CALCULATE width based on digit count - -## Methods - -### update_line_number_area_width -1. CALCULATE required width: - - GET max line count - - COUNT digits - - ADD padding -2. SET viewport margins - -### update_line_number_area -1. IF scroll delta provided: - - SCROLL line number area -2. ELSE: - - UPDATE line number area -3. IF viewport rect contains update rect: - - UPDATE line number area width - -### line_number_area_paint_event -1. CREATE painter for line number area -2. FILL background with alternate base color -3. GET first visible block -4. WHILE block is valid and within paint rect: - - IF block is visible: - - DRAW line number - - MOVE to next block \ No newline at end of file +# ui.code_editor - Codegen-Ready Pseudocode Template + + + +{ + "slug": "ui.code_editor", + "target_file": "src/ui/code_editor.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Provide a text editor widget API with line navigation, highlights, and change notifications. + +## SCOPE +- In-scope: + - Set/get content + - Scroll to line + - Highlight lines + - Emit signals on edits +- Out-of-scope: + - Disk I/O + - Diff parsing + +## IMPORTS - ALLOWED ONLY + +- - from typing import List +- - from core.contracts import ValidationError + +## CONSTANTS +- HIGHLIGHT_LIMIT = 20000 + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def set_content(content: str) -> None + - pre: content is str; LF normalized + - post: content stored; signals emitted + - errors: ValidationError on None +- def get_content() -> str + - pre: content may be empty + - post: returns string + - errors: none +- def scroll_to_line(line: int, centered: bool = True) -> None + - pre: line >= 0 + - post: viewport moved + - errors: ValidationError if out of range +- def highlight_lines(lines: list[int]) -> None + - pre: all >= 0 + - post: applies highlight up to HIGHLIGHT_LIMIT + - errors: ValidationError on negatives +- Signals: on_content_changed(str), on_cursor_moved(int,int), on_scroll(int,int) + +## STATE +- content: str +- highlights: list[int] + +## THREADING +- ui_thread_only: true +- worker_policy: none +- handoff: queued signal + +## I/O +- inputs: ["strings", "line indices"] +- outputs: ["signals"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start ui.code_editor" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Normalize input newlines to LF +2) Set internal buffer +3) Emit onContentChanged +4) Scroll and highlight as requested with bounds checks + +### EDGE CASES +- - Empty content → still valid +- - Out-of-range line → ValidationError + +## ERRORS +- - ValidationError on bad params + +## COMPLEXITY +- time: O(n) +- memory: O(n) +- notes: none + +## PERFORMANCE +- max input size: 10MB +- max iterations: n +- timeout: none +- instrumentation: +- count highlights applied + +## TESTS - ACCEPTANCE HOOKS +- assert no ERROR logs during typical operations +- assert set(['on_content_changed','on_cursor_moved','on_scroll']) == set(defined_signals) + +## EXTENSIBILITY +- - Future syntax highlight strategies can inject color providers + +## NON-FUNCTIONAL +- security: none +- i18n: UTF-8 only \ No newline at end of file diff --git a/modules/ui.highlighters.pseudocode.md b/modules/ui.highlighters.pseudocode.md index bf8a749..e71d038 100644 --- a/modules/ui.highlighters.pseudocode.md +++ b/modules/ui.highlighters.pseudocode.md @@ -1,48 +1,124 @@ -# Syntax Highlighters Pseudocode - -## DiffHighlighter (QSyntaxHighlighter) -1. INITIALIZE with color palette -2. DEFINE formats: - - Added lines: green foreground - - Removed lines: red foreground - - Headers: blue foreground -3. OVERRIDE highlightBlock: - - IF line starts with '@@', '+++', '---', or '***': - - APPLY header format - - ELIF line starts with '+': - - APPLY added format - - ELIF line starts with '-': - - APPLY removed format - -## PatchHighlighter (QSyntaxHighlighter) -1. INITIALIZE with color palette -2. DEFINE format: - - Added lines: green background -3. METHOD set_added_lines(indices): - - STORE indices - - REHIGHLIGHT -4. OVERRIDE highlightBlock: - - GET current line number - - IF line in added indices: - - APPLY added format - -## RemovedHighlighter (QSyntaxHighlighter) -1. INITIALIZE with color palette -2. DEFINE format: - - Removed lines: red background (transparent) -3. METHOD set_removed_lines(indices): - - STORE indices - - REHIGHLIGHT -4. OVERRIDE highlightBlock: - - GET current line number - - IF line in removed indices: - - APPLY removed format - -## Color Palette Detection -1. DETECT if dark mode: - - Windows: Check registry - - Other: Check palette brightness -2. IF dark mode: - - SET dark color scheme -3. ELSE: - - SET light color scheme \ No newline at end of file +# ui.highlighters - Codegen-Ready Pseudocode Template + + + +{ + "slug": "ui.highlighters", + "target_file": "src/ui/highlighters.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Render visual styles for diff and code regions based on Theme palette; no OS detection here. + +## SCOPE +- In-scope: + - Apply styles for added, removed, headers, line numbers + - Batch updates to avoid jank +- Out-of-scope: + - Theme detection + - File I/O + +## IMPORTS - ALLOWED ONLY + +- - from typing import List +- - from core.contracts import ValidationError + +## CONSTANTS +- BATCH_SIZE = 1000 +- DELAY_MS = 0 + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def set_palette(palette: dict) -> None + - pre: required keys present: {'background','foreground','added','removed','header','lineNumber','selection'} + - post: palette stored + - errors: ValidationError on missing keys +- def highlight_diff(lines: list[str]) -> None + - pre: lines is list[str] + - post: styles applied + - errors: none +- def highlight_patch(added: list[int], removed: list[int]) -> None + - pre: indices >= 0 + - post: styles applied + - errors: ValidationError on negatives + + +## STATE +- palette: dict + +## THREADING +- ui_thread_only: true +- worker_policy: none +- handoff: queued signal + +## I/O +- inputs: ["palette dict", "text lines", "indices"] +- outputs: ["styled regions"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start ui.highlighters" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Validate palette keys exist +2) Apply styles in batches of BATCH_SIZE +3) Optionally delay between batches via DELAY_MS + +### EDGE CASES +- - Empty lines → no-op +- - Missing palette key → ValidationError + +## ERRORS +- - ValidationError on bad inputs + +## COMPLEXITY +- time: O(n) +- memory: O(1) +- notes: none + +## PERFORMANCE +- max input size: 200k lines +- max iterations: n +- timeout: none +- instrumentation: +- count batches + +## TESTS - ACCEPTANCE HOOKS +- assert indices are 0-based and sorted + +## EXTENSIBILITY +- - New token categories can be added by extending key map + +## NON-FUNCTIONAL +- security: none +- i18n: UTF-8 \ No newline at end of file diff --git a/modules/ui.main_window.pseudocode.md b/modules/ui.main_window.pseudocode.md index 45da4dd..d440772 100644 --- a/modules/ui.main_window.pseudocode.md +++ b/modules/ui.main_window.pseudocode.md @@ -1,115 +1,148 @@ -# Main Window Pseudocode - -## Initialization -1. CREATE QApplication instance -2. INITIALIZE ThemeManager -3. INITIALIZE StateManager -4. CREATE MainWindow instance -5. SETUP UI: - - CREATE central widget - - CREATE toolbar - - CREATE content area (3-panel splitter) - - CONNECT all signals -6. RESTORE saved state -7. SHOW window -8. START event loop - -## Toolbar Setup -1. CREATE horizontal layout -2. ADD buttons: - - Open File button - - Open Folder button - - Open Diff button - - Apply Patch button - - Save As button -3. ADD checkboxes: - - Create backups - - Live preview -4. CONNECT button clicks to handlers - -## Content Area Setup -1. CREATE horizontal splitter -2. CREATE left panel: - - QLabel "Files" - - QListWidget for file list -3. CREATE middle panel: - - QLabel "Original" - - CodeEditor widget -4. CREATE right panel: - - QTabWidget - - ADD tabs: - - Diff editor - - Preview editor - - Navigation widget -5. SET splitter sizes [200, 600, 800] - -## Event Handlers - -### on_open_file -1. OPEN file dialog -2. IF file selected: - - SET current_file - - SET root_folder = None - - LOAD file content into original editor - - CLEAR file list - - CLEAR preview - -### on_open_folder -1. OPEN folder dialog -2. IF folder selected: - - SET root_folder - - SET current_file = None - - CLEAR editors - - SCAN folder for files - -### on_open_diff -1. OPEN file dialog for diff -2. IF file selected: - - LOAD diff content into diff editor - - PARSE diff using DiffParser - - UPDATE file list - - SELECT first file - -### on_diff_changed -1. GET diff content -2. IF empty: - - CLEAR file list - - RETURN -3. TRY: - - PARSE diff using DiffParser - - UPDATE file list - - IF files exist: - - SELECT first file -4. CATCH ParseError: - - SHOW warning dialog - -### on_file_selected -1. GET selected patch from list item -2. IF no patch: - - RETURN -3. LOAD original content: - - TRY relative to root folder - - TRY absolute path -4. IF loaded: - - SET original editor content - - APPLY patch using DiffApplier - - UPDATE preview editor - - UPDATE navigation - -## State Management - -### save_state -1. CREATE state dict: - - window_size - - window_position - - root_folder - - current_file - - splitter_sizes -2. CALL StateManager.save(state) - -### restore_state -1. CALL StateManager.load() -2. IF state exists: - - RESTORE window size - - RESTORE window position - - RESTORE splitter sizes \ No newline at end of file +# ui.main_window - Codegen-Ready Pseudocode Template + + + +{ + "slug": "ui.main_window", + "target_file": "src/ui/main_window.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Assemble main UI, wire signals, orchestrate file open, diff parse/apply, navigation, and state/theme integration. + +## SCOPE +- In-scope: + - Toolbar actions + - Split panes + - Open file/folder/diff + - Apply patch + - Persist and restore state +- Out-of-scope: + - Implement diff parsing or applying logic inside + - Network access + +## IMPORTS - ALLOWED ONLY + +- - from core.contracts import PatchyError +- - from typing import Optional +- from core.diff_parser import parse +- from core.diff_applier import apply +- from utils.state import load, save +- from utils.theme import current, palette +- from ui.code_editor import set_content, get_content, scroll_to_line, highlight_lines +- from ui.highlighters import set_palette, highlight_diff, highlight_patch +- from ui.navigation import analyze_changes, next_change, prev_change + +## CONSTANTS +- SPLITTER_DEFAULTS = [300, 400, 300] + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def init_ui() -> None + - pre: called once + - post: widgets constructed and connected + - errors: none +- def load_state() -> None + - pre: state accessible + - post: restore splitter sizes and theme + - errors: none +- def save_state() -> None + - pre: on close + - post: persist state + - errors: none +- def on_open_file(path: str) -> None + - pre: path exists + - post: file loaded and editors updated + - errors: IOErrorCompat on fail +- def on_apply_diff() -> None + - pre: diff present + - post: apply result updates preview and nav + - errors: ApplyError on failure + + +## STATE +- widgets: dict - references to editors and panels + +## THREADING +- ui_thread_only: true +- worker_policy: threadpool +- handoff: queued signal + +## I/O +- inputs: ["user actions", "paths"] +- outputs: ["signals", "editor content updates"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start ui.main_window" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Construct widgets and connect signals +2) Wire state load and theme hookup +3) Handle file open and update editors +4) Invoke parser and applier in worker then post results to UI +5) Persist state on exit + +### EDGE CASES +- - Missing file → show error +- - Parse error → show message and keep state stable +- - Apply error → revert preview + +## ERRORS +- - IOErrorCompat, ParseError, ApplyError + +## COMPLEXITY +- time: O(1) per event +- memory: O(1) steady aside from loaded files +- notes: none + +## PERFORMANCE +- max input size: files up to 50MB +- max iterations: n/a +- timeout: none +- instrumentation: +- count UI events +- latency per apply + +## TESTS - ACCEPTANCE HOOKS +- assert no ERROR logs during normal operations +- assert call order: open -> parse -> apply -> navigation -> editors updated +- assert no ERROR logs on success path + +## EXTENSIBILITY +- - New panes can be added via widget registry + +## NON-FUNCTIONAL +- security: path validation +- i18n: UTF-8 only +- compliance: no telemetry \ No newline at end of file diff --git a/modules/ui.navigation.pseudocode.md b/modules/ui.navigation.pseudocode.md index 9c13aa2..40007c5 100644 --- a/modules/ui.navigation.pseudocode.md +++ b/modules/ui.navigation.pseudocode.md @@ -1,61 +1,127 @@ -# Navigation System Pseudocode - -## NavigationManager Class - -### Properties -- current_patch: FilePatch or None -- result: ApplyResult or None -- added_blocks: List[contiguous block starts] -- removed_blocks: List[contiguous block starts] - -### Methods - -#### update_for_patch(patch, apply_result) -1. STORE patch and result -2. BUILD added_blocks: - - SORT added_lines - - FIND contiguous blocks - - STORE block start positions -3. BUILD removed_blocks: - - SORT removed_lines_original - - FIND contiguous blocks - - STORE block start positions - -#### create_widget() -1. CREATE QWidget -2. CREATE vertical layout -3. ADD navigation buttons: - - Previous change - - Next change - - Jump to first/last -4. ADD change counter label -5. ADD keyboard shortcuts -6. RETURN widget - -#### navigate_prev() -1. DETERMINES active editor (original or preview) -2. GET current cursor line -3. FIND previous block start before cursor -4. IF found: - - MOVE cursor to block start -5. ELSE: - - WRAP to last block - -#### navigate_next() -1. DETERMINES active editor -2. GET current cursor line -3. FIND next block start after cursor -4. IF found: - - MOVE cursor to block start -5. ELSE: - - WRAP to first block - -#### find_contiguous_blocks(indices) -1. INPUT: sorted list of indices -2. IF empty: - RETURN empty list -3. INITIALIZE blocks = [indices[0]] -4. FOR each index starting from second: - - IF index != previous + 1: - - ADD index to blocks -5. RETURN blocks \ No newline at end of file +# ui.navigation - Codegen-Ready Pseudocode Template + + + +{ + "slug": "ui.navigation", + "target_file": "src/ui/navigation.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Compute contiguous change blocks from ApplyResult and provide next/prev navigation with wrap. + +## SCOPE +- In-scope: + - Build blocks of added or removed lines + - Provide navigation methods with wrap semantics +- Out-of-scope: + - Parsing diffs + - UI painting + +## IMPORTS - ALLOWED ONLY + +- - from core.contracts import ApplyResult +- - from typing import List, Tuple + +## CONSTANTS +- INDEX_BASE = 0 + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def analyze_changes(result: ApplyResult) -> list[tuple[int,int,str]] + - pre: result valid; indices 0-based + - post: returns blocks as (start,end,type) + - errors: ValidationError on bad indices +- def next_change(current_line: int) -> int + - pre: current_line >= 0 + - post: returns target line + - errors: ValidationError on negatives +- def prev_change(current_line: int) -> int + - pre: current_line >= 0 + - post: returns target line + - errors: ValidationError on negatives +- analyze_changes consumes only result.added_lines and result.removed_original_indices (0-based) + +## STATE +- blocks: list[tuple[int,int,str]] + +## THREADING +- ui_thread_only: false +- worker_policy: none +- handoff: callback + +## I/O +- inputs: ["ApplyResult"] +- outputs: ["blocks", "line indices"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start ui.navigation" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Merge adjacent indices of same type into blocks +2) Sort blocks by start +3) Implement wrap-around for next and previous + +### EDGE CASES +- - No changes → return empty list +- - Current line beyond end → clamp to end +101) Merge from result.added_lines and result.removed_original_indices only + +## ERRORS +- - ValidationError on unsorted indices + +## COMPLEXITY +- time: O(n log n) +- memory: O(n) +- notes: none + +## PERFORMANCE +- max input size: 200k indices +- max iterations: n +- timeout: none +- instrumentation: +- count blocks generated + +## TESTS - ACCEPTANCE HOOKS +- assert wrap behavior consistent +- assert blocks non-overlapping +- assert all(a>=0 for a in result.added_lines) +- assert all(r>=0 for r in result.removed_original_indices) + +## EXTENSIBILITY +- - Filtering by type can be added later + +## NON-FUNCTIONAL +- security: none +- i18n: UTF-8 \ No newline at end of file diff --git a/modules/utils.state.pseudocode.md b/modules/utils.state.pseudocode.md index 57b7050..db328f9 100644 --- a/modules/utils.state.pseudocode.md +++ b/modules/utils.state.pseudocode.md @@ -1,62 +1,137 @@ -# State Manager Pseudocode - -## StateManager Class - -### Properties -- config_dir: Path to config directory -- config_file: Path to state file -- state: Dict of current state - -### Methods - -#### __init__() -1. DETERMINE config directory: - - Windows: %APPDATA%/Patchy - - macOS: ~/Library/Application Support/Patchy - - Linux: ~/.config/Patchy -2. CREATE directory if not exists -3. SET config_file = config_dir / 'state.json' - -#### save_state(state_dict) -1. INPUT: state_dict containing: - - window_size - - window_position - - root_folder - - current_file - - splitter_states - - recent_files - - theme_preference -2. TRY: - - SERIALIZE state_dict to JSON - - WRITE to config_file -3. CATCH IOError: - - LOG warning - - CONTINUE - -#### load_state() -1. TRY: - - IF config_file exists: - - READ file content - - PARSE JSON - - RETURN state_dict -2. CATCH (FileNotFoundError, JSONDecodeError): - - RETURN default state: - - window_size: 1600x1000 - - window_position: center - - theme: 'auto' - - recent_files: [] -3. RETURN empty dict on any other error - -#### clear_state() -1. IF config_file exists: - - DELETE file -2. RESET to default state - -#### add_recent_file(file_path) -1. LOAD current recent_files -2. IF file_path exists in list: - - MOVE to top -3. ELSE: - - INSERT at top - - TRUNCATE to max 10 files -4. SAVE state \ No newline at end of file +# utils.state - Codegen-Ready Pseudocode Template + + + +{ + "slug": "utils.state", + "target_file": "src/utils/state.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Persist and retrieve JSON state with atomic writes and schema validation. + +## SCOPE +- In-scope: + - Load/save JSON + - Atomic temp-write and replace + - Keyed sections: window, ui, files +- Out-of-scope: + - YAML parsing + - Secrets storage + +## IMPORTS - ALLOWED ONLY + +- - from typing import Any, Optional, Dict +- - import json +- - from pathlib import Path +- - from core.contracts import ValidationError + +## CONSTANTS +- STATE_FILE = 'patchy.state.json' +- INDEX_BASE = 0 + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def load(key: str) -> dict | None + - pre: key in {'window','ui','files'} + - post: returns dict or None + - errors: ValidationError on unknown key +- def save(key: str, value: dict) -> None + - pre: value JSON-serializable + - post: file updated atomically + - errors: IOErrorCompat on write fail +- def delete(key: str) -> None + - pre: key valid + - post: removes key + - errors: IOErrorCompat on write fail +- def clear() -> None + - pre: none + - post: empties state file + - errors: IOErrorCompat + + +## STATE +- cache: dict - optional in-memory cache + +## THREADING +- ui_thread_only: false +- worker_policy: none +- handoff: callback + +## I/O +- inputs: ["state path", "dict values"] +- outputs: ["state file"] +- encoding: utf-8 +- atomic_write: true // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start utils.state" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) Resolve state file path +2) Read JSON if exists else default {} +3) Validate schema per key +4) Write via temp file then replace + +### EDGE CASES +- - Missing file → return defaults +- - Corrupt JSON → raise ValidationError +- - Permission denied → IOErrorCompat + +## ERRORS +- - ValidationError on schema mismatch +- - IOErrorCompat on filesystem errors + +## COMPLEXITY +- time: O(n) +- memory: O(n) +- notes: none + +## PERFORMANCE +- max input size: 1MB +- max iterations: n +- timeout: none +- instrumentation: +- count reads and writes + +## TESTS - ACCEPTANCE HOOKS +- assert round-trip save-load yields identical dict +- assert save()->load() round-trip equals input dict +- assert atomic write leaves valid file after simulated crash + +## EXTENSIBILITY +- - New keys can be added without breaking existing state + +## NON-FUNCTIONAL +- security: prevent path traversal +- i18n: UTF-8 +- compliance: no secrets in state \ No newline at end of file diff --git a/modules/utils.theme.pseudocode.md b/modules/utils.theme.pseudocode.md index 0693b9a..4fcf33d 100644 --- a/modules/utils.theme.pseudocode.md +++ b/modules/utils.theme.pseudocode.md @@ -1,60 +1,122 @@ -# Theme Manager Pseudocode - -## ThemeManager Class - -### Properties -- app: QApplication reference -- current_theme: 'light' or 'dark' -- timer: QTimer for theme monitoring - -### Methods - -#### __init__(app) -1. STORE app reference -2. INITIALIZE timer (30s interval) -3. CONNECT timer timeout to check_theme -4. START timer -5. CALL apply_theme - -#### detect_system_theme() -1. IF Windows: - - READ registry key: - - HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize - - Value: AppsUseLightTheme - - RETURN 'dark' if value == 0, 'light' otherwise -2. ELSE: - - CALCULATE palette brightness - - RETURN 'dark' if brightness < 0.5 - -#### apply_theme() -1. new_theme = detect_system_theme() -2. IF new_theme == current_theme: - - RETURN -3. SAVE scroll positions -4. IF dark theme: - - SET Fusion style - - APPLY dark palette: - - Window: dark gray - - Text: white - - Base: darker gray - - Highlight: blue -5. ELSE: - - RESET to system palette -6. RESTORE scroll positions -7. EMIT theme_changed signal - -#### create_dark_palette() -1. CREATE QPalette -2. SET colors: - - Window: QColor(53, 53, 53) - - WindowText: white - - Base: QColor(35, 35, 35) - - Text: white - - Button: QColor(53, 53, 53) - - Highlight: QColor(42, 130, 218) -3. RETURN palette - -#### check_theme() -1. new_theme = detect_system_theme() -2. IF new_theme != current_theme: - - CALL apply_theme() \ No newline at end of file +# utils.theme - Codegen-Ready Pseudocode Template + + + +{ + "slug": "utils.theme", + "target_file": "src/utils/theme.py", + "language": "python", + "runtime": { + "python": "3.11" + }, + "index_base": 0, + "newline": "LF", + "dependencies": [ + "core.contracts" + ], + "acceptance": { + "lint": [ + "ruff check .", + "ruff format --check ." + ], + "tests": [ + "pytest -q" + ] + } +} + + +## PURPOSE +- Manage theme preference 'light'|'dark'|'auto' and provide palette to UI consumers. + +## SCOPE +- In-scope: + - Store current theme + - Expose palette dict + - Signal listeners via callback hooks +- Out-of-scope: + - OS polling inside highlighters or editors + +## IMPORTS - ALLOWED ONLY + +- - from typing import Literal, Dict + +## CONSTANTS +- DEFAULT_THEME = 'auto' + +## TYPES - USE ONLY SHARED TYPES + +- Uses: FilePatch, Hunk, HunkLine, ApplyResult, ParseError, ApplyError // from core.contracts + +## INTERFACES +- def current() -> str + - pre: state initialized + - post: returns 'light'|'dark'|'auto' + - errors: none +- def set(name: str) -> None + - pre: name in {'light','dark','auto'} + - post: updates theme and emits callback + - errors: ValidationError on bad name +- def palette() -> dict + - pre: current theme set + - post: returns stable color mapping + - errors: none + + +## STATE +- subs: list[callable] - registered listeners + +## THREADING +- ui_thread_only: false +- worker_policy: none +- handoff: callback + +## I/O +- inputs: ["theme name"] +- outputs: ["palette dict", "callbacks"] +- encoding: utf-8 +- atomic_write: false // temp file + replace + +## LOGGING +- logger: patchy +- on_start: INFO "start utils.theme" +- on_warn: WARNING "condition" +- on_error: ERROR "condition raises ErrorType" + +## ALGORITHM +1) If name invalid raise ValidationError +2) Set internal theme +3) Notify subscribers +4) Return palette on request + +### EDGE CASES +- - Unknown theme → ValidationError + +## ERRORS +- - ValidationError on bad theme name + +## COMPLEXITY +- time: O(1) +- memory: O(1) +- notes: none + +## PERFORMANCE +- max input size: n/a +- max iterations: n/a +- timeout: none +- instrumentation: +- count theme changes + +## TESTS - ACCEPTANCE HOOKS +- assert palette keys include background, foreground, added, removed + +## EXTENSIBILITY +- - New palettes can be added by extending mapping + +## NON-FUNCTIONAL +- security: no external IO +- i18n: UTF-8 \ No newline at end of file