From fc6e851230e45f33e175e0a420889b53d287d938 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Thu, 18 Sep 2025 16:26:50 +0800 Subject: [PATCH] refactor: Update workflow plan system and template organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove --analyze|--deep parameters from plan.md, use default analysis - Change .analysis to .process directory structure for better organization - Create ANALYSIS_RESULTS.md template focused on verified results - Add .process folder to workflow-architecture.md file structure - Template emphasizes verification of files, methods, and commands - Prevent execution errors from non-existent references πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/commands/workflow/plan.md | 51 +- .claude/python_script/__init__.py | 35 ++ .../context_analyzer.cpython-313.pyc | Bin 0 -> 15922 bytes .../embedding_manager.cpython-313.pyc | Bin 0 -> 22854 bytes .../__pycache__/file_indexer.cpython-313.pyc | Bin 0 -> 19571 bytes .../__pycache__/path_matcher.cpython-313.pyc | Bin 0 -> 22406 bytes .claude/python_script/analyzer.py | 305 +++++++++++ .../python_script/cache/embedding_index.json | 156 ++++++ .claude/python_script/cache/embeddings.pkl | Bin 0 -> 35109 bytes .claude/python_script/cache/file_index.json | 276 ++++++++++ .claude/python_script/cli.py | 207 ++++++++ .claude/python_script/config.yaml | 158 ++++++ .claude/python_script/core/__init__.py | 25 + .../core/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 816 bytes .../core/__pycache__/config.cpython-313.pyc | Bin 0 -> 14061 bytes .../context_analyzer.cpython-313.pyc | Bin 0 -> 15927 bytes .../embedding_manager.cpython-313.pyc | Bin 0 -> 23228 bytes .../__pycache__/file_indexer.cpython-313.pyc | Bin 0 -> 21560 bytes .../gitignore_parser.cpython-313.pyc | Bin 0 -> 7930 bytes .../__pycache__/path_matcher.cpython-313.pyc | Bin 0 -> 22421 bytes .claude/python_script/core/config.py | 327 ++++++++++++ .../python_script/core/context_analyzer.py | 359 +++++++++++++ .../python_script/core/embedding_manager.py | 453 ++++++++++++++++ .claude/python_script/core/file_indexer.py | 383 ++++++++++++++ .../python_script/core/gitignore_parser.py | 182 +++++++ .claude/python_script/core/path_matcher.py | 500 ++++++++++++++++++ .claude/python_script/indexer.py | 204 +++++++ .claude/python_script/install.sh | 189 +++++++ .claude/python_script/requirements.txt | 15 + .claude/python_script/setup.py | 92 ++++ .claude/python_script/tools/__init__.py | 13 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 480 bytes .../module_analyzer.cpython-313.pyc | Bin 0 -> 18823 bytes .../__pycache__/tech_stack.cpython-313.pyc | Bin 0 -> 9792 bytes .../workflow_updater.cpython-313.pyc | Bin 0 -> 12239 bytes .../python_script/tools/module_analyzer.py | 369 +++++++++++++ .claude/python_script/tools/tech_stack.py | 202 +++++++ .../python_script/tools/workflow_updater.py | 241 +++++++++ .claude/python_script/utils/__init__.py | 16 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 517 bytes .../utils/__pycache__/cache.cpython-313.pyc | Bin 0 -> 16056 bytes .../utils/__pycache__/colors.cpython-313.pyc | Bin 0 -> 10696 bytes .../__pycache__/io_helpers.cpython-313.pyc | Bin 0 -> 18575 bytes .claude/python_script/utils/cache.py | 350 ++++++++++++ .claude/python_script/utils/colors.py | 248 +++++++++ .claude/python_script/utils/io_helpers.py | 378 +++++++++++++ .claude/workflows/ANALYSIS_RESULTS.md | 143 +++++ .../workflows/agent-orchestration-patterns.md | 160 ------ .claude/workflows/workflow-architecture.md | 2 + 49 files changed, 5865 insertions(+), 174 deletions(-) create mode 100644 .claude/python_script/__init__.py create mode 100644 .claude/python_script/__pycache__/context_analyzer.cpython-313.pyc create mode 100644 .claude/python_script/__pycache__/embedding_manager.cpython-313.pyc create mode 100644 .claude/python_script/__pycache__/file_indexer.cpython-313.pyc create mode 100644 .claude/python_script/__pycache__/path_matcher.cpython-313.pyc create mode 100644 .claude/python_script/analyzer.py create mode 100644 .claude/python_script/cache/embedding_index.json create mode 100644 .claude/python_script/cache/embeddings.pkl create mode 100644 .claude/python_script/cache/file_index.json create mode 100644 .claude/python_script/cli.py create mode 100644 .claude/python_script/config.yaml create mode 100644 .claude/python_script/core/__init__.py create mode 100644 .claude/python_script/core/__pycache__/__init__.cpython-313.pyc create mode 100644 .claude/python_script/core/__pycache__/config.cpython-313.pyc create mode 100644 .claude/python_script/core/__pycache__/context_analyzer.cpython-313.pyc create mode 100644 .claude/python_script/core/__pycache__/embedding_manager.cpython-313.pyc create mode 100644 .claude/python_script/core/__pycache__/file_indexer.cpython-313.pyc create mode 100644 .claude/python_script/core/__pycache__/gitignore_parser.cpython-313.pyc create mode 100644 .claude/python_script/core/__pycache__/path_matcher.cpython-313.pyc create mode 100644 .claude/python_script/core/config.py create mode 100644 .claude/python_script/core/context_analyzer.py create mode 100644 .claude/python_script/core/embedding_manager.py create mode 100644 .claude/python_script/core/file_indexer.py create mode 100644 .claude/python_script/core/gitignore_parser.py create mode 100644 .claude/python_script/core/path_matcher.py create mode 100644 .claude/python_script/indexer.py create mode 100644 .claude/python_script/install.sh create mode 100644 .claude/python_script/requirements.txt create mode 100644 .claude/python_script/setup.py create mode 100644 .claude/python_script/tools/__init__.py create mode 100644 .claude/python_script/tools/__pycache__/__init__.cpython-313.pyc create mode 100644 .claude/python_script/tools/__pycache__/module_analyzer.cpython-313.pyc create mode 100644 .claude/python_script/tools/__pycache__/tech_stack.cpython-313.pyc create mode 100644 .claude/python_script/tools/__pycache__/workflow_updater.cpython-313.pyc create mode 100644 .claude/python_script/tools/module_analyzer.py create mode 100644 .claude/python_script/tools/tech_stack.py create mode 100644 .claude/python_script/tools/workflow_updater.py create mode 100644 .claude/python_script/utils/__init__.py create mode 100644 .claude/python_script/utils/__pycache__/__init__.cpython-313.pyc create mode 100644 .claude/python_script/utils/__pycache__/cache.cpython-313.pyc create mode 100644 .claude/python_script/utils/__pycache__/colors.cpython-313.pyc create mode 100644 .claude/python_script/utils/__pycache__/io_helpers.cpython-313.pyc create mode 100644 .claude/python_script/utils/cache.py create mode 100644 .claude/python_script/utils/colors.py create mode 100644 .claude/python_script/utils/io_helpers.py create mode 100644 .claude/workflows/ANALYSIS_RESULTS.md delete mode 100644 .claude/workflows/agent-orchestration-patterns.md diff --git a/.claude/commands/workflow/plan.md b/.claude/commands/workflow/plan.md index 913b9d48..1c0ef5a7 100644 --- a/.claude/commands/workflow/plan.md +++ b/.claude/commands/workflow/plan.md @@ -13,7 +13,7 @@ examples: ## Usage ```bash -/workflow:plan [--AM gemini|codex] [--analyze|--deep] +/workflow:plan ``` ## Input Detection @@ -21,20 +21,37 @@ examples: - **Issues**: `ISS-*`, `ISSUE-*`, `*-request-*` β†’ Loads issue data and acceptance criteria - **Text**: Everything else β†’ Parses natural language requirements -## Analysis Levels -- **Quick** (default): Structure only (5s) -- **--analyze**: Structure + context analysis (30s) -- **--deep**: Structure + comprehensive parallel analysis (1-2m) +## Default Analysis Workflow + +### Automatic Intelligence Selection +The command automatically performs comprehensive analysis by: +1. **Context Gathering**: Reading relevant CLAUDE.md documentation based on task requirements +2. **Task Assignment**: Automatically assigning Task agents based on complexity: + - **Simple tasks** (≀3 modules): Direct CLI tools (`~/.claude/scripts/gemini-wrapper` or `codex --full-auto exec`) + - **Complex tasks** (>3 modules): Task agents with integrated CLI tool access +3. **Process Documentation**: Generates analysis artifacts in `.workflow/WFS-[session]/.process/ANALYSIS_RESULTS.md` +4. **Flow Control Integration**: Automatic tool selection managed by flow_control system + +### Analysis Artifacts Generated +- **ANALYSIS_RESULTS.md**: Documents context analysis, codebase structure, pattern identification, and task decomposition results +- **Context mapping**: Project structure, dependencies, and cohesion groups +- **Implementation strategy**: Tool selection and execution approach ## Core Rules -### Dependency Context Resolution (CRITICAL) -⚠️ **For tasks with dependencies or documentation associations**: MUST include bash content retrieval as first step in flow_control.pre_analysis +### Agent Execution Context (CRITICAL) +⚠️ **For agent execution phase**: Agents will automatically load context from plan-generated documents -**Required Pattern**: +**Agent Context Loading Pattern**: ```json "flow_control": { "pre_analysis": [ + { + "step": "load_planning_context", + "action": "Load plan-generated analysis and context", + "command": "bash(cat .workflow/WFS-[session]/.process/ANALYSIS_RESULTS.md 2>/dev/null || echo 'planning analysis not found')", + "output_to": "planning_context" + }, // ε―ι€‰οΌšTaskδ»»εŠ‘θΎƒδΈΊε€ζ‚ζ—Ά { "step": "load_dependencies", "action": "Retrieve dependency task summaries", @@ -43,7 +60,7 @@ examples: }, { "step": "load_documentation", - "action": "Retrieve project documentation", + "action": "Retrieve project documentation based on task requirements", "command": "bash(cat CLAUDE.md README.md 2>/dev/null || echo 'documentation not found')", "output_to": "doc_context" } @@ -59,13 +76,15 @@ examples: **Content Sources**: - Task summaries: `.workflow/WFS-[session]/.summaries/IMPL-[task-id]-summary.md` (主任劑) / `IMPL-[task-id].[subtask-id]-summary.md` (子任劑) -- Documentation: `CLAUDE.md`, `README.md`, config files +- Documentation: `CLAUDE.md`, `README.md`, config files (loaded based on task context requirements) - Schema definitions: `.json`, `.yaml`, `.sql` files - Dependency contexts: `.workflow/WFS-[session]/.task/IMPL-*.json` +- Analysis artifacts: `.workflow/WFS-[session]/.process/ANALYSIS_RESULTS.md` ### File Structure Reference **Architecture**: @~/.claude/workflows/workflow-architecture.md + ### Task Limits & Decomposition - **Maximum 10 tasks**: Hard enforced limit - projects exceeding must be re-scoped - **Function-based decomposition**: By complete functional units, not files/steps @@ -100,10 +119,11 @@ examples: ### Project Structure Analysis & Engineering Enhancement **⚠️ CRITICAL PRE-PLANNING STEP**: Must complete comprehensive project analysis before any task planning begins **Analysis Process**: Context Gathering β†’ Codebase Exploration β†’ Pattern Recognition β†’ Implementation Strategy -**Tool Selection Protocol**: -- **Simple patterns** (≀3 modules): Use `~/.claude/scripts/gemini-wrapper` or `codex --full-auto exec` -- **Complex analysis** (>3 modules): Launch Task agents with integrated CLI tool access +**Tool Selection Protocol** (Managed by flow_control): +- **Simple patterns** (≀3 modules): Direct CLI tools (`~/.claude/scripts/gemini-wrapper` or `codex --full-auto exec`) +- **Complex analysis** (>3 modules): Task agents with integrated CLI tool access and built-in tool capabilities **Tool Priority**: Task(complex) > Direct CLI(simple) > Hybrid(mixed complexity) +**Automatic Selection**: flow_control system determines optimal tool based on task complexity and context requirements **Core Principles**: - **Complexity-Driven Selection**: Simple patterns use direct CLI, complex analysis uses Task agents with CLI integration @@ -140,14 +160,17 @@ examples: ### Auto-Created (complexity > simple) - **TODO_LIST.md**: Hierarchical progress tracking - **.task/*.json**: Individual task definitions with flow_control +- **.process/ANALYSIS_RESULTS.md**: Analysis results and planning artifacts.Template:@~/.claude/workflows/ANALYSIS_RESULTS.md ### Document Structure ``` .workflow/WFS-[topic]/ β”œβ”€β”€ IMPL_PLAN.md # Main planning document β”œβ”€β”€ TODO_LIST.md # Progress tracking (if complex) +β”œβ”€β”€ .process/ +β”‚ └── ANALYSIS_RESULTS.md # Analysis results and planning artifacts └── .task/ - β”œβ”€β”€ IMPL-001.json # Task definitions + β”œβ”€β”€ IMPL-001.json # Task definitions with flow_control └── IMPL-002.json ``` diff --git a/.claude/python_script/__init__.py b/.claude/python_script/__init__.py new file mode 100644 index 00000000..e02aa58c --- /dev/null +++ b/.claude/python_script/__init__.py @@ -0,0 +1,35 @@ +""" +Refactored Python Script Analyzer +Modular, reusable architecture for intelligent file analysis and workflow automation. +""" + +__version__ = "2.0.0" +__author__ = "Claude Development Team" +__email__ = "dev@example.com" + +from .analyzer import Analyzer +from .indexer import ProjectIndexer +from .cli import AnalysisCLI +from .core import ( + Config, FileIndexer, FileInfo, IndexStats, + ContextAnalyzer, AnalysisResult, + PathMatcher, MatchResult, PathMatchingResult, + EmbeddingManager, GitignoreParser +) +from .tools import ModuleAnalyzer, ModuleInfo, TechStackLoader +from .utils import Colors, CacheManager, IOHelpers + +__all__ = [ + 'Analyzer', 'ProjectIndexer', 'AnalysisCLI', + # Core modules + 'Config', + 'FileIndexer', 'FileInfo', 'IndexStats', + 'ContextAnalyzer', 'AnalysisResult', + 'PathMatcher', 'MatchResult', 'PathMatchingResult', + 'EmbeddingManager', 'GitignoreParser', + # Tools + 'ModuleAnalyzer', 'ModuleInfo', + 'TechStackLoader', + # Utils + 'Colors', 'CacheManager', 'IOHelpers' +] \ No newline at end of file diff --git a/.claude/python_script/__pycache__/context_analyzer.cpython-313.pyc b/.claude/python_script/__pycache__/context_analyzer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbc49d2d3d8b99957b60cca230fa076a45059b34 GIT binary patch literal 15922 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 literal 0 HcmV?d00001 diff --git a/.claude/python_script/__pycache__/embedding_manager.cpython-313.pyc b/.claude/python_script/__pycache__/embedding_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4835d0cab354826082948ddb8bdf7df4d589c7db GIT binary patch literal 22854 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 literal 0 HcmV?d00001 diff --git a/.claude/python_script/__pycache__/file_indexer.cpython-313.pyc b/.claude/python_script/__pycache__/file_indexer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c6852bd02d17299e8419d98cec8432397d3bbf8 GIT binary patch literal 19571 zcmcJ1dvF`andb~Jc!LB;g2aQbAqk0uM1rJ9$)cVnMZHBzCInj!8N(0&l8{J%o&h5f z@k19^HdHPt({a3_)~<+7QW2C(t?AwDnek;WlCoFPZtm(Zf?~iEZKAEXuFiK`TU4Z! zW9Ogyz8(w!La^j))wRT)o_Ykcl%LGIM5fuRdIHsDi5?X~p46u5!#|qnas?#zA?sn-$WSHfM*% zRb@+cP^y2R)IwSspv)+jan+tO2gTKlSv}?3PEeFzOHpGr&6M0DOHC*y6fRSIPQNca z6=DxYf}yh^jva~yXW@1{%CX+580SAe8IGK0kNIPh9sOtgT!`(D_^0OLAx=Lq8=eaC ztUnTDr~Tnb4F2$r4P#Gibe!cvGf_Sqi*j@9I6Q)9&xB)>>~tvR5Bg(%xBer1xnUPA zGT6OKB{GBIKulzgg!!1LJvtK$N8wqb(i4h_s^haWQz4f^)M4Ael%MBCwVw|{1L!~t znW0#CIuyvx7rErt_1kd!IyFYYdQ!NSoN^uIQE<$dYE11>wn8aMF(jpdJQYc4A*Ifh zYH}%UuAVNJr^l4XFsAEOc#IC}6vOGq44$$voyXvzdKphS=E-x<89gShY{KlS5Vbh@ z2P5Ork1C;q_$Ewp(|&w))_*cO8)N+}AC64mlHf9hBW!FkR4}z}k(q(n6U(^J6g2x% z$VYA&J{%8;Wz*4Mcsv{m`fzeYUFd8q6yc$7Q5%36n}DSe%^@BN;SMTAPeZY23`8SP zFXo%{^OK^Q3&m!+NPvd%$w3JT{JUp^=yhr|SLUTQXVXsV3O#&<=8)~Kz!LMJsc|05 zSh06-+t{8d|7?6 ztANiRi*ezTv#}5~Cqt74i7&~O$q`Ny?}O-T)W0x0GWxQY4`!-rQcO)YX-hG-jH&YF z;Y@kO%R`wOdy27VDyvgW^(sTF>J}~cDY&evDRsrFf>zZo9$$rw+ND$X@N!QH`S&ni zdNw{b0DrlFONHTH5Teh`!^qbX4{OG>W2#;Su6mA}b&w+5?9q;CfL}qTBENd{oMFP? zGKzX43q3GN{737cVVuP@2&HfU1RP;X7NY`+G;(d2 z5Y(1FWzE=bH{Ii2uWu{roxh9ED~vC z%vCQR&6q6)=FWDE*VHOaGinmjc?@fA>lbCB&5>p*RLcEoCr6>Gzo z98ll(IqKW9^=qNN1Nu>u9v}6T+H9*jPqU|P%=pMr)o1J1dm6ZgiAqc~fFle&R#w2c04ccxzJR$11Xf#HYXFwJ@NM~yJ!~1*xO33Ymc?xq$ z7|qr5DrB`cg0oN*WVP?5rgAL`+R`Jsq6Tn_I!T6dCCe2arCcsQ8~V>Z-Aj!Sz`M5R z5tOe*!Nb5mRiA6y`V=4J*)g{qc>�|^#60Tqa2k3u$-PZbF2gVYrsI77%yr=XF*T+qNn2~)CCfLai8J=ahXGB15=230H4J|9N;X& zOo$_BFA_kg=08jHhYvMHn4*>_mzjGU#={}eh#D@$N2k6N;`;GUIT4C+gP2lHpN27s zdV-Wl2cl*wIx#`U47H9xI>ZtAE~>F0#EBN)NyP5H931pb`)5Q;{x+|sxcxYSgAlot zTpgw##|Q_(BH)`oOV>&=?hA8AuH-H>Mh5VFpdcJ36MJ5$cx z^UN)66J#~DUe>>*Pj1|~!ll>&%&mvq2Ir+OT>L_!Y?({dZ%fzj66$xYyp%k4B31wN zJo76V&pn{EbJF4m@T#vHteovw%rwTD(fQ ztEQ+3bE3)1jO2Lq!B(x1H3S1TP%JeBpcb!Qf_UTIM1X=7H!}ld!y=ZI&`pjacs3Sg z^Hkv$CGyUfyBLWu4#QJKRX7%!=0(+1^h}6zsR-SO8mNqP#-Vf&_4%fv64E@rFA!6f zoVX{j61#?yO3*A4CqD$yIqFWOWnPnUwp~8_*5S)X-#VJ?-FuZwIS-|s#{}oG_njkx zk)1#AYirYjGSk|*sJx`RsC&bZGCKi++M4ewsdXM2*4=EKFDJkQU_LT18dCv8|8hY1 zfopf~7L6-hVEWLd))ZEsk(=hPvRss}5e3vk+9?XKwm*ioRgTJ_hZ@PH@>QFu6D_hX zXv<~?*HP4?S{0669*H-nRpg%h@b=&=puLKboD9!Ddlc<15=L^8Sk9qx#$bdQt&np^ ztK^iIk)JAG;eEHl`>H&O5uoYf;AK2YFF-rQpiI7R^`qYNMBsthfp;2u4BrdijBvz% zmN;TUB5e{iT9}2QyqszkFeO#nFhd%3u$R;il64Vhm8fwP z`r&^F(K+fvTkYb(bX~Vl*S+j~*Zqz=Ro9=g4a}F_vDT)o?YFG$^ZP$E)x7f3*I!zE zA#K_sn6~`1^=F;$b^iC38{uENlV9+sx1AKWo&3OB3k+7>oVK(ImewCB-d0~xr!1ZG zdv8}e-t1YNePi4E)$14NOk?w|HS4u5w)1{jJ?gJJMBs zsj5D)w(*{Zs%gDzqHGb*cg6-3YEwQIO^8NFkqR>y+l- zZQllm8W=l&et#ai1D@ifMsaK;c)``^Q4$QsC@7Ewic)w_9wW$t92WwwX5R)d8uZG8 zS%d#%YG>g&O;kH2b8--Bk1JTJks@@j*{N)##_1+1&%>iR9`03WC_f9d^zVL0UeO58 zAE%_JxU})Dd$W!v&?Vq!Vgu1xK<4OZ^hJ^?vQat4`wvinT- zp4F{UT1BOPXw1I>(K%|B(doJ~Cfh47o_{fAYD}A21ygInnclEN*svpI+L5uc7hZhz z#gw%*ZS54Soy*;VwKr+*{jj2XA-q(TIJ0v0X2pRU{TWB?rS%urztOg!xoveUKK9!4 zOV0_`wvSa*ec z(gJBt6R)hA5HWaXCqrk0;R&D^8EO-`j~%Rq+T}jL^^&bXJhYBA6t|`V zYV*u(5Z7P+`j;2Oi=zoT;e5OON_(PV`MDL#&+30tzhX+(9!S?76>5*BYM)4%M$)D+ z!8DdKJ(JWv^PrGLeo(^SJ$x2fX1ErIQqV}AUIKowm!eP#%zD9lFN-&_EG>H^KQ=JA zv?e*toCBC{Q56M$9p_?1H9re3fwLU!hfo~Cat`t8n(0P#vUa-7J{kDNLz02Dmc@%o zdF%kaW-#TJ`n1Iqu*Rd+mn`P}HE6DvnsF~JlY6c%s04s^^uyd1z?uchs+4kqrY*k(32cGJP5Sp5TJ87r5v&VvC_C| zOaVDjamyZpfnmde<*|1n|Kf^mtgBMggn7aj+%#54c}4IqhssXEy}0f~>~UYk)Hg69 zJorsay@b&`M$bbenvR9I=`fGmR74YV$1qm|n~xFD^e%N?FH6rNz*w@@!mxWL(jgvI z1nOv6OY31%$sduJw`PsB%;lp{=KmEU)XX(HTc)ZmUF8(2oaw6dLe+Xy%zK0ld#)Z# zZx|Lf3}>vhc`Z6Wc)PA~Ntb3f3+(3Q;JcA`A}Mxns&3!>(1$kmcC~HM{@OE{hK-qO z`-QPr$1)Agm!>aHXB;hQ#|FW%A?@fA99=2Lri{HlZFdWHcVaGO@5?l;OE+y1nzk&@ zrJDLP^-byeE}_0_*`BK3ez!trFP}fST0>dtlg7F~erUG7*_brBP`%qSM)NC&&mT@3 z8wF!yvKiVhpT1gowf|~QlHGsHc;I%G^-rkkZJ+XhVX3cd+*3pS^*}4cKd&(lRw#eI zzG={?{0E~7(qb6^v_Qzmg^2lGTERa{@th--MdyjaOUsff z2qG0)l~DerB$CYYa%(>j3AD~Z=lm$;nh^y&Pw7zr2bUaMDS!YjSdgC>z;Ffi2b`@M zL5?a4@-yVo1Yy30VAlqolrxL-O}r|(*NEDy#u*>#g1YKb@-Ct|!EJ}8ekVnpXhk1N zMC;X#po^s_K>HH84}%UWJhc$1nGd326LO~0M3mtLo-=-(dS=b)o4!E=FJq4 zWNI%1v8E^Yjx5eXDY=`QTd1J~U9XKdN^UAJ7^Gb@vJ*}O5pjOH<2F)(xUdkOkz9h! zIG990@f)~j;TqSHT`{oV<60uec(yHG#zsPCIwcGjbh*kTp~Ij+kwn-K_Mj&2U0&3~ zE^AP7?t;}mF%><@EkHT(g~TLS4`NP{vf3GPyIgE}?eof;LlCo33gTs@f6<)9nL7`#`E{ z;7;dbi_?Oo{R0bbF}HOuObbS5#%N!BLNGRbedum0<>*=6KI)Hkn^QJ}JEp0Qv<)qdofwC-kqT2mNoz1-SdUS)!B> z=mHD5ff_}=C82b0c-Y9J>{EhwfF8{Wz|x+;JRFe(8i1z~LeIk#?24drE(-EkL979g znFa2d*_-zbJX~m<=xv4Gh5+^u53>@y5-R*B!}dpbB~&@DME*@n0KNPk6MWrO|-k1C?>S39%U0#K!i*B$|umtap;uArBk>;SpB=ir$f2N4{2 ziNOWVVSoVQhvPMQ*n}cO5=j7>$~crK9C`w{Ic&g?Jt77x5u1d23X5=Qq*EKFL!P6< zg?0B( z;wnUFM^?S^()pLtrgeg8o#eI1R8<2O0e?o#ZE#KmFGOFBF7eAx-K^Mowc@t5_QKq^ z=9Vg6dwzk**c=P0OzXzWXWu%ztXZi{weCo_4hXFS@3#)VdURp%cJqdGbFa|cyZm&j zdH2HJ*A9bYzHKjk$6EKXfvVh#2Z1wv?#zH z#FN0~>OLhLXe{zVPH1=q3r00QcbxiM9?6^;2wq4)O%V;uDeBdtFoLE^QSfNJnzg(Q ze65LS`ZW_UF1!}-wJvV=fO;00!w4(03qbEl=ms$S$OTuH?FW8Atfb#~N6#1Qm(lSDx6*zCA0!XipgZnPa94xY0_KY3w z3E&AkqP^T}j>X|(^}ye~tq`?PEP-Gk20=JPD@L-DUU>Nu^}33sh5$k!9GRdVQ;yRd z+8ZvKxHqmSM1{MHLv~tw%5%p=C=xn5!|f6~3t@CIJG*yGMS;WlUG7|i-(p{AY<&c* zm2@0=@0sP}>DsRMYrEci=97C26nw?zp#T597V+e~SM);MPa%W*dx-8}3CxEyJln1x zzPJ>-WdDx+hcAA@p{t%4OTY&4zdh zxZsPE<9{N9?rj;Apx!Lm8<}|xloGKe3$7ny=f!^3HFESe)ZiB&f*r~l`{KISPR$!& z14M8fS@HgC>?dPCeJ15NlCmC|H-ZC3u!JwqMRh5^mgEhe%YYRF$W|@AJGc@b2+n8?jJ^;+f&N*1$t)a0F)?8c^C5@I?ybm zvY`tKjgGhM1ErPqDv?tj=#BET=ptgXXE)p+R^Nn&OD5h)Od+C!&sk<*e>x~TxN*`6 z=o8E3jPR_|E4aKHHax7WpQ+OC3fCxy_U_QSyW3gR0`s&e? zW!?PVJNDZ7@{FVL(#DG$zteI45O}+@r#)h6Q-@&c__6if#=mL2YWaozn*GN9bocR8 z_wkImA#GkKnAa@@-#2f(tDl%}b_gkgYV<#W;0qv+{La6fcP zh7!02qCKT($xEXbl=x&pcX^e^6hk&?O?gEGJdeb9fz&vz@1}zFJvk>IiJN2F@B~3S z3m<6k?QpUIPZYPap;*A}a)a0)%Y6+O6t$y#v;fl*tS34UNf&jXbg-ai?K)*Q8Rx|2 z&t6NpjvAPfMH0vEmF;h6itA%cF@rI`I;Pnp(?qOK;vtLX);2FK5}Ue$vrevP02R0M*egrCi; zmQgd$0Txa>C~23JVRAkT4E#C&gXE z2(%19PM8qx2Bc)m_oefvc&X*_cTkjO$PTcg{DA+fq?cIXC zJ7w>gKXltvd8=~&Ps&r3`_rcVDbxNUajF3*x~iAN=_kK8QI0-xe)bLfKn(-OZo|{z zSZPN63mEI?Wz)Qw`fzrWNgTAhvXr^$0V5X=xRFl`!13gz8E|{MRPiPRaMMvf2A?wq z0IyBW!JYKIqNG9H!)uHrm~7Z+1KZCby+ujayOWZwJ2Y{?$V0ToW7Di))$W+`PoE6> zcZu#o+C`Km4MWnO1$JGwelspMlFvD6sp4|ucN>W!!{h%-pa(pl52y3_5J-2VT9KIo zYnG5L97c#ICN8j{62)wtGu|~{Ms{c%6VRu z(U!fUKd%?8T`SIcy`bHlFD>ParU*nnhN*)X^+N=% zFWT**_eoQ&_y}2f*wy&iUy(7`vI8RzMo(ab!b)ULMx#^G!GTW9%%27LCf*`glf2-s zW9oYt5t)FFEeWIl7*qcZBSfX#KVkGgFv?)`JB&7AX$x$~X_}{=p{sr{` zv+pibq51;77+agT-OX9@x zq|mYZKBn*XwyNw2%PIwz73T_n^}xyt!nUD5;Qi_dU9PHG99*henp~N@XJaKFl$ zR6CZKB~PMl>5Br}vr57J3UePXtIcMWp1g}Tu~S&zhcBRCIfXBvUv=tLw#BVWhlE<_ z2-0_LdR0ThvkK{k728$kRsP0-t1k!xUZL+vl9@a?o}7pZCujbERqonUs$n|8uTpqj z*?M*GM%9hU8}-8AQ^NKWQf@LhnG8QC1i3$uN_Wf4;0dwiJy+&dDM((m-EiLElgGzy zydWIJvZ8}eo=VbBRqDTWG0^t zCPUN0vyqRn3dxd|9#=g8f4P8;2PDLM`1u-q`%lAn|5QY}((v&{NjDpjW;8jfi|va7 zy1~H(u0>ZkNX}9<;G-k>F5LK#8f8$U{Gl~W9k}xprm=tc@;fSatniJ zerkay3@?0xuZ4ky9~rv zJRGk@B$fLK5v-N}?GV5UhG0ngQ3p8E7uR-lNcM5uAYI*a{^_Z>bq_I`XE}1{UwR5! z9ssX2!O*yWb_#wOWHK~06W4WgWKW}sdLF*43Hf5&Y$#r}4-cvVlq3gTB;Ox2R{P0m zZ0`TUe8x2$9mGKwul7tv&q#F&T!`3p)=7h}^q?arTL`EUyDRc5#vSY0rT_zyA8>H- z+t_t&(;=SsPlQ}N3`9lBL^aF~_;ccA$MF{fB%HgAjh9Qe(r*TAV@0|>Zj`d{`vs_= zW>3z+FEzxCd$I?fL-2%n`4RXLf$VXpAl&y1N3(A=4qsOU-R#J0g#7FZ{*p-cl$KipbxOyKpVdHjbQ1Ce*e58c#tL$5|t#L)`Htv66T?Na=%MOwsdcm$@@MxXX ztet%^u93d*;9ac9;Npr}KQ{rNDe)n(yq}wxod&Rf3~xEn=nn>cP)hR9)`VQV=OuUu zUzH-g!B?efD1v4ZIGuwrh&tXs9s>IgPD~8ry#bDXhXWEBiOj*_eMcpbw+w{@Ai_se z9ME#fT?o)_6r6R0Gaa~T0Ym~jvFJh}kQ1>znK9xCl_2H>7Md|a0O~SwcQO5Yh{#a! z-57ii_yrC^*yQspX|MKitfT?*!XMm`ywSvZy^KA)1`%(Bh?uC^NHSCz{Fxn20m5M& z{PFw1M|qC=9b@`6Q+21TI$h=z%ACpW;UvUmN6+ocF#0rO6&UNHJ~5Ph{OKg#d?BA} zNE}J-K2CDI`CMJ1J-PFNTtjlhep2(m9YaOhP$wAbQig_egRmb`W&QTq*WBsKR-qC; zUwM1um5oB>=5vQ{>&oU&FZLv?H+9|1Ph={pFEqT`utcXSn&;K92dv+8UrVWsFusat63UT#XV zz03QOmD_I_cdRK%vYV2XJ-3X#q@V9^O}F&jZ0TEBm)>?n*mfkp8wkOy__HHFIkK|rMrE@6$Sv(q#@?{VFDc$|%pb%-4J^#1 zt!}~UPS$iLE4$t|cK=>UIh^q93R~LTCYak2)hTnQ)Nk2hDBaL0G;}86$goa+jT=uS z`;Oi?lPrJgSK1RF?}lY~AHVoPdb?g%d9YjgkKOu1lm>sG28qq*bJ;kA3S1DQmoZ9U zbR8qYb@)Xhe%d1Ot2#D^pAd5lmJ@qSjVZ*yoE9Qb1@g}~!G0n7+Bozya>#z%NsP)c zLVO@GFH*Z?R-*39+i#+FhxEHSyEqM0f&Uc$Rfu4dgQo8?N}9Q+r)d4Jsq$Y_2J*lB zpDEjKsh(d^J-?+~|4h|G(H&aQ;gq#yhFe~Kc_pa1{> literal 0 HcmV?d00001 diff --git a/.claude/python_script/__pycache__/path_matcher.cpython-313.pyc b/.claude/python_script/__pycache__/path_matcher.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..408580480ce27386e7c9de4d24fac446124e923d GIT binary patch literal 22406 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 literal 0 HcmV?d00001 diff --git a/.claude/python_script/analyzer.py b/.claude/python_script/analyzer.py new file mode 100644 index 00000000..46d7bc19 --- /dev/null +++ b/.claude/python_script/analyzer.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +""" +Unified Path-Aware Analyzer +Main entry point for the refactored analyzer system. +Provides a clean, simple API for intelligent file analysis. +""" + +import os +import sys +import argparse +import logging +import json +import time +from pathlib import Path +from typing import Dict, List, Optional, Any + +# Add current directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from core.config import get_config +from core.file_indexer import FileIndexer, IndexStats +from core.context_analyzer import ContextAnalyzer, AnalysisResult +from core.path_matcher import PathMatcher, PathMatchingResult +from core.embedding_manager import EmbeddingManager +from utils.colors import Colors + + +class Analyzer: + """Main analyzer class with simplified API.""" + + def __init__(self, config_path: Optional[str] = None, root_path: str = "."): + self.root_path = Path(root_path).resolve() + self.config = get_config(config_path) + + # Setup logging + logging.basicConfig( + level=getattr(logging, self.config.get('logging.level', 'INFO')), + format=self.config.get('logging.format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ) + self.logger = logging.getLogger(__name__) + + # Initialize core components + self.indexer = FileIndexer(self.config, str(self.root_path)) + self.context_analyzer = ContextAnalyzer(self.config) + self.path_matcher = PathMatcher(self.config) + + # Initialize embedding manager if enabled + self.embedding_manager = None + if self.config.is_embedding_enabled(): + try: + self.embedding_manager = EmbeddingManager(self.config) + except ImportError: + self.logger.warning("Embedding dependencies not available. Install sentence-transformers for enhanced functionality.") + + def build_index(self) -> IndexStats: + """Build or update the file index.""" + print(Colors.yellow("Building file index...")) + start_time = time.time() + + self.indexer.build_index() + stats = self.indexer.get_stats() + + elapsed = time.time() - start_time + if stats: + print(Colors.green(f"Index built: {stats.total_files} files, ~{stats.total_tokens:,} tokens ({elapsed:.2f}s)")) + else: + print(Colors.green(f"Index built successfully ({elapsed:.2f}s)")) + + return stats + + def analyze(self, prompt: str, mode: str = "auto", patterns: Optional[List[str]] = None, + token_limit: Optional[int] = None, use_embeddings: Optional[bool] = None) -> Dict[str, Any]: + """Analyze and return relevant file paths for a given prompt.""" + + print(Colors.yellow("Analyzing project and prompt...")) + start_time = time.time() + + # Load or build index + index = self.indexer.load_index() + if not index: + self.build_index() + index = self.indexer.load_index() + + stats = self.indexer.get_stats() + print(Colors.cyan(f"Project stats: ~{stats.total_tokens:,} tokens across {stats.total_files} files")) + print(Colors.cyan(f"Categories: {', '.join(f'{k}: {v}' for k, v in stats.categories.items())}")) + + # Determine project size + project_size = self._classify_project_size(stats.total_tokens) + print(Colors.cyan(f"Project size: {project_size}")) + + # Analyze prompt context + print(Colors.yellow("Analyzing prompt context...")) + context_result = self.context_analyzer.analyze(prompt) + + print(Colors.cyan(f"Identified: {len(context_result.domains)} domains, {len(context_result.languages)} languages")) + if context_result.domains: + print(Colors.cyan(f"Top domains: {', '.join(context_result.domains[:3])}")) + + # Determine if we should use embeddings + should_use_embeddings = use_embeddings + if should_use_embeddings is None: + should_use_embeddings = ( + self.embedding_manager is not None and + self.config.is_embedding_enabled() and + len(context_result.keywords) < 5 # Use embeddings for vague queries + ) + + similar_files = [] + if should_use_embeddings and self.embedding_manager: + print(Colors.yellow("Using semantic similarity search...")) + # Update embeddings if needed + if not self.embedding_manager.embeddings_exist(): + print(Colors.yellow("Building embeddings (first run)...")) + self.embedding_manager.update_embeddings(index) + + similar_files = self.embedding_manager.find_similar_files(prompt, index) + print(Colors.cyan(f"Found {len(similar_files)} semantically similar files")) + + # Match files to context + print(Colors.yellow("Matching files to context...")) + matching_result = self.path_matcher.match_files( + index, + context_result, + token_limit=token_limit, + explicit_patterns=patterns + ) + + elapsed = time.time() - start_time + + print(Colors.green(f"Analysis complete: {len(matching_result.matched_files)} files, ~{matching_result.total_tokens:,} tokens")) + print(Colors.cyan(f"Confidence: {matching_result.confidence_score:.2f}")) + print(Colors.cyan(f"Execution time: {elapsed:.2f}s")) + + return { + 'files': [match.file_info.relative_path for match in matching_result.matched_files], + 'total_tokens': matching_result.total_tokens, + 'confidence': matching_result.confidence_score, + 'context': { + 'domains': context_result.domains, + 'languages': context_result.languages, + 'keywords': context_result.keywords + }, + 'stats': { + 'project_size': project_size, + 'total_files': stats.total_files, + 'analysis_time': elapsed, + 'embeddings_used': should_use_embeddings + } + } + + def generate_command(self, prompt: str, tool: str = "gemini", **kwargs) -> str: + """Generate a command for external tools (gemini/codex).""" + analysis_result = self.analyze(prompt, **kwargs) + + # Format file patterns + file_patterns = " ".join(f"@{{{file}}}" for file in analysis_result['files']) + + if tool == "gemini": + if len(analysis_result['files']) > 50: # Too many files for individual patterns + return f'gemini --all-files -p "{prompt}"' + else: + return f'gemini -p "{file_patterns} {prompt}"' + + elif tool == "codex": + workspace_flag = "-s workspace-write" if analysis_result['total_tokens'] > 100000 else "-s danger-full-access" + return f'codex {workspace_flag} --full-auto exec "{file_patterns} {prompt}"' + + else: + raise ValueError(f"Unsupported tool: {tool}") + + def _classify_project_size(self, tokens: int) -> str: + """Classify project size based on token count.""" + small_limit = self.config.get('token_limits.small_project', 500000) + medium_limit = self.config.get('token_limits.medium_project', 2000000) + + if tokens < small_limit: + return "small" + elif tokens < medium_limit: + return "medium" + else: + return "large" + + def get_project_stats(self) -> Dict[str, Any]: + """Get comprehensive project statistics.""" + stats = self.indexer.get_stats() + embedding_stats = {} + + if self.embedding_manager: + embedding_stats = { + 'embeddings_exist': self.embedding_manager.embeddings_exist(), + 'embedding_count': len(self.embedding_manager.load_embeddings()) if self.embedding_manager.embeddings_exist() else 0 + } + + return { + 'files': stats.total_files, + 'tokens': stats.total_tokens, + 'size_bytes': stats.total_size, + 'categories': stats.categories, + 'project_size': self._classify_project_size(stats.total_tokens), + 'last_updated': stats.last_updated, + 'embeddings': embedding_stats, + 'config': { + 'cache_dir': self.config.get_cache_dir(), + 'embedding_enabled': self.config.is_embedding_enabled(), + 'exclude_patterns_count': len(self.config.get_exclude_patterns()) + } + } + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="Path-Aware Analyzer - Intelligent file pattern detection", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python analyzer.py "analyze authentication flow" + python analyzer.py "fix database connection" --patterns "src/**/*.py" + python analyzer.py "review API endpoints" --tool gemini + python analyzer.py --stats + """ + ) + + parser.add_argument('prompt', nargs='?', help='Analysis prompt or task description') + parser.add_argument('--patterns', nargs='*', help='Explicit file patterns to include') + parser.add_argument('--tool', choices=['gemini', 'codex'], help='Generate command for specific tool') + parser.add_argument('--output', choices=['patterns', 'json'], default='patterns', help='Output format') + parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') + parser.add_argument('--stats', action='store_true', help='Show project statistics and exit') + parser.add_argument('--build-index', action='store_true', help='Build file index and exit') + + args = parser.parse_args() + + # Create analyzer with default values + analyzer = Analyzer(config_path=None, root_path=".") + + # Handle special commands + if args.build_index: + analyzer.build_index() + return + + if args.stats: + stats = analyzer.get_project_stats() + if args.output == 'json': + print(json.dumps(stats, indent=2, default=str)) + else: + print(f"Total files: {stats['files']}") + print(f"Total tokens: {stats['tokens']:,}") + print(f"Categories: {stats['categories']}") + if 'embeddings' in stats: + print(f"Embeddings: {stats['embeddings']['embedding_count']}") + return + + # Require prompt for analysis + if not args.prompt: + parser.error("Analysis prompt is required unless using --build-index or --stats") + + # Perform analysis + try: + result = analyzer.analyze( + args.prompt, + patterns=args.patterns, + use_embeddings=False # Disable embeddings by default for simplicity + ) + + # Generate output + if args.tool: + # Generate command using already computed result + file_patterns = " ".join(f"@{{{file}}}" for file in result['files']) + if args.tool == "gemini": + if len(result['files']) > 50: + command = f'gemini --all-files -p "{args.prompt}"' + else: + command = f'gemini -p "{file_patterns} {args.prompt}"' + elif args.tool == "codex": + workspace_flag = "-s workspace-write" if result['total_tokens'] > 100000 else "-s danger-full-access" + command = f'codex {workspace_flag} --full-auto exec "{file_patterns} {args.prompt}"' + print(command) + elif args.output == 'json': + print(json.dumps(result, indent=2, default=str)) + else: # patterns output (default) + for file_path in result['files']: + print(f"@{{{file_path}}}") + + # Show verbose details + if args.verbose: + print(f"\n# Analysis Details:") + print(f"# Matched files: {len(result['files'])}") + print(f"# Total tokens: {result['total_tokens']:,}") + print(f"# Confidence: {result['confidence']:.2f}") + + except KeyboardInterrupt: + print(Colors.warning("\nAnalysis interrupted by user")) + sys.exit(1) + except Exception as e: + print(Colors.error(f"Analysis failed: {e}")) + if args.verbose: + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/cache/embedding_index.json b/.claude/python_script/cache/embedding_index.json new file mode 100644 index 00000000..094f0e5c --- /dev/null +++ b/.claude/python_script/cache/embedding_index.json @@ -0,0 +1,156 @@ +{ + "analyzer.py": { + "file_path": "analyzer.py", + "content_hash": "9a7665c34d5ac84634342f8b1425bb13", + "embedding_hash": "fb5b5a58ec8e070620747c7313b0b2b6", + "created_time": 1758175163.6748724, + "vector_size": 384 + }, + "config.yaml": { + "file_path": "config.yaml", + "content_hash": "fc0526eea28cf37d15425035d2dd17d9", + "embedding_hash": "4866d8bd2b14c16c448c34c0251d199e", + "created_time": 1758175163.6748896, + "vector_size": 384 + }, + "install.sh": { + "file_path": "install.sh", + "content_hash": "6649df913eadef34fa2f253aed541dfd", + "embedding_hash": "54af072da7c1139108c79b64bd1ee291", + "created_time": 1758175163.6748998, + "vector_size": 384 + }, + "requirements.txt": { + "file_path": "requirements.txt", + "content_hash": "e981a0aa103bdec4a99b75831967766d", + "embedding_hash": "37bc877ea041ad606234262423cf578a", + "created_time": 1758175163.6749053, + "vector_size": 384 + }, + "setup.py": { + "file_path": "setup.py", + "content_hash": "7b93af473bfe37284c6cf493458bc421", + "embedding_hash": "bdda9a6e8d3bd34465436b119a17e263", + "created_time": 1758175163.6749127, + "vector_size": 384 + }, + "__init__.py": { + "file_path": "__init__.py", + "content_hash": "c981c4ffc664bbd3c253d0dc82f48ac6", + "embedding_hash": "3ab1a0c5d0d4bd832108b7a6ade0ad9c", + "created_time": 1758175163.6749194, + "vector_size": 384 + }, + "cache\\file_index.json": { + "file_path": "cache\\file_index.json", + "content_hash": "6534fef14d12e39aff1dc0dcf5b91d1d", + "embedding_hash": "d76efa530f0d21e52f9d5b3a9ccc358c", + "created_time": 1758175163.6749268, + "vector_size": 384 + }, + "core\\config.py": { + "file_path": "core\\config.py", + "content_hash": "ee72a95cea7397db8dd25b10a4436eaa", + "embedding_hash": "65d1fca1cf59bcd36409c3b11f50aab1", + "created_time": 1758175163.6749349, + "vector_size": 384 + }, + "core\\context_analyzer.py": { + "file_path": "core\\context_analyzer.py", + "content_hash": "2e9ac2050e463c9d3f94bad23e65d4e5", + "embedding_hash": "dfb51c8eaafd96ac544b3d9c8dcd3f51", + "created_time": 1758175163.674943, + "vector_size": 384 + }, + "core\\embedding_manager.py": { + "file_path": "core\\embedding_manager.py", + "content_hash": "cafa24b0431c6463266dde8b37fc3ab7", + "embedding_hash": "531c3206f0caf9789873719cdd644e99", + "created_time": 1758175163.6749508, + "vector_size": 384 + }, + "core\\file_indexer.py": { + "file_path": "core\\file_indexer.py", + "content_hash": "0626c89c060d6022261ca094aed47093", + "embedding_hash": "93d5fc6e84334d3bd9be0f07f9823b20", + "created_time": 1758175163.6749592, + "vector_size": 384 + }, + "core\\gitignore_parser.py": { + "file_path": "core\\gitignore_parser.py", + "content_hash": "5f1d87fb03bc3b19833406be0fa5125f", + "embedding_hash": "784be673b6b428cce60ab5390bfc7f08", + "created_time": 1758175163.6749675, + "vector_size": 384 + }, + "core\\path_matcher.py": { + "file_path": "core\\path_matcher.py", + "content_hash": "89132273951a091610c1579ccc44f3a7", + "embedding_hash": "e01ca0180c2834a514ad6d8e62315ce0", + "created_time": 1758175163.6749754, + "vector_size": 384 + }, + "core\\__init__.py": { + "file_path": "core\\__init__.py", + "content_hash": "3a323be141f1ce6b9d9047aa444029b0", + "embedding_hash": "3fc5a5427067e59b054428083a5899ca", + "created_time": 1758175163.6749818, + "vector_size": 384 + }, + "tools\\module_analyzer.py": { + "file_path": "tools\\module_analyzer.py", + "content_hash": "926289c2fd8d681ed20c445d2ac34fa1", + "embedding_hash": "3378fcde062914859b765d8dfce1207f", + "created_time": 1758175163.67499, + "vector_size": 384 + }, + "tools\\tech_stack.py": { + "file_path": "tools\\tech_stack.py", + "content_hash": "eef6eabcbc8ba0ece0dfacb9314f3585", + "embedding_hash": "bc3aa5334ef17328490bc5a8162d776a", + "created_time": 1758175163.674997, + "vector_size": 384 + }, + "tools\\workflow_updater.py": { + "file_path": "tools\\workflow_updater.py", + "content_hash": "40d7d884e0db24eb45aa27739fef8210", + "embedding_hash": "00488f4acdb7fe1b5126da4da3bb9869", + "created_time": 1758175163.6750047, + "vector_size": 384 + }, + "tools\\__init__.py": { + "file_path": "tools\\__init__.py", + "content_hash": "41bf583571f4355e4af90842d0674b1f", + "embedding_hash": "fccd7745f9e1e242df3bace7cee9759c", + "created_time": 1758175163.6750097, + "vector_size": 384 + }, + "utils\\cache.py": { + "file_path": "utils\\cache.py", + "content_hash": "dc7c08bcd9af9ae465020997e4b9127e", + "embedding_hash": "68394bc0f57a0f66b83a57249b39957d", + "created_time": 1758175163.6750169, + "vector_size": 384 + }, + "utils\\colors.py": { + "file_path": "utils\\colors.py", + "content_hash": "8ce555a2dcf4057ee7adfb3286d47da2", + "embedding_hash": "1b18e22acb095e83ed291b6c5dc7a2ce", + "created_time": 1758175163.6750243, + "vector_size": 384 + }, + "utils\\io_helpers.py": { + "file_path": "utils\\io_helpers.py", + "content_hash": "fb276a0e46b28f80d5684368a8b15e57", + "embedding_hash": "f6ff8333b1afc5b98d4644f334c18cda", + "created_time": 1758175163.6750326, + "vector_size": 384 + }, + "utils\\__init__.py": { + "file_path": "utils\\__init__.py", + "content_hash": "f305ede9cbdec2f2e0189a4b89558b7e", + "embedding_hash": "7d3f10fe4210d40eafd3c065b8e0c8b7", + "created_time": 1758175163.6750393, + "vector_size": 384 + } +} \ No newline at end of file diff --git a/.claude/python_script/cache/embeddings.pkl b/.claude/python_script/cache/embeddings.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2d694ae254703c45c15c14d8077cd030106a98e6 GIT binary patch literal 35109 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ǰ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 z zYqenEwP?b#VlleU*aUxGE&!g`4oH}r3ESSi=T6IQ)J40MB;S6^3~5QAij)9+x66o? z(!EADbto~d`^Svtto}l7G_N4hSz*lk&Yj@?J{qOC72#p(yf9HZ50(d%z_?mAew=MZ zp07Jrt2XwYxFnX4%b&}s=~-(yajA+}N9}-v#vIHz?KUN<)5!rf0rJgaC&$INfqUW= zWD>Vc#B<+wTJk{|^j^!Ncl;uw&~QEw%i4r8$0O)DEM$6U=(BWtg zsxZ?Oz88t%+IbyNcZ*v|@Y(~a7EAws9i97AjByynn`M*Gtkozb*07^wvQ6t!@ADwz z7L}wX+Hzl)5_a7xnJpSBbiZE^(RQKTYDV>bpT?l6CR0Cb$fa0QQ%1$erD;aI<9|56 zoHL(u4%4;GMZ4$(_yz`W$Xd->)nD+QmyS1YzRaXmUZD0*B(dvl79XDvG2)x#u8n5& zUP4hZ_PkM6MDuqflD%OTX_BgeLJyI`e2g{?$=J-Xr&K67@BD%U$l3Z8{FEtdiAYIh zErHM}cA|){MOaeNN`BLyQdEzRm3DtJerlOTzh3FMH@9+ehf+cnPsUkxqlV=D0d&>e z1LK!Qz{N3`zE@eG;cP!x42nqd@*SuHOz2}<&zuGV;oWy%F80G>8t9NWXANh=ikJiF zSs4Mv)J?jY8`z)gNTLoASx3jQ#2zUP*)JnwSR=Q}(UWC9uc2!%hMKln)Ul-drIB#0JOXg1+r&8+;kpRCY3_%u;@rZ8ey?&D88`riQK=FzQ`P>7&V*yYL8k zdrVTN*Iw8ZC_GsCm*bVrP>TJnfuExv+1^K$oc>}Rzp~gG^E8%ZY;s{64A)UNXAf_s z%HXQkUqzc#H!#T~X*{Ax&}ce)>(YYZ0$@G0*QU9FrLp{&!#H8>i({r#Uhgg@;~$5* zzI_(FwxE#w1@Lp4Z58&gEu{N9j~%?H#i7Ij;7pWvh~$ z5loW+Th7p+^&OJFnGKOS!WfG>6hEwZ$R1apruA#@k|-n(PtpxC?#p9r*cDq}d2(`b c7n+?^sA*R=CM#D$`@~X|xk literal 0 HcmV?d00001 diff --git a/.claude/python_script/cache/file_index.json b/.claude/python_script/cache/file_index.json new file mode 100644 index 00000000..ab5c1ce2 --- /dev/null +++ b/.claude/python_script/cache/file_index.json @@ -0,0 +1,276 @@ +{ + "stats": { + "total_files": 26, + "total_tokens": 56126, + "total_size": 246519, + "categories": { + "code": 21, + "config": 3, + "docs": 1, + "other": 1 + }, + "last_updated": 1758177270.9103189 + }, + "files": { + "analyzer.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\analyzer.py", + "relative_path": "analyzer.py", + "size": 12595, + "modified_time": 1758175179.730658, + "extension": ".py", + "category": "code", + "estimated_tokens": 3072, + "content_hash": "3fb090745b5080e0731e7ef3fc94029d" + }, + "cli.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\cli.py", + "relative_path": "cli.py", + "size": 8329, + "modified_time": 1758177193.3710027, + "extension": ".py", + "category": "code", + "estimated_tokens": 2030, + "content_hash": "b9f0b5d6a154cf51c8665b2344c9faf8" + }, + "config.yaml": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\config.yaml", + "relative_path": "config.yaml", + "size": 4317, + "modified_time": 1758163450.6223683, + "extension": ".yaml", + "category": "config", + "estimated_tokens": 1040, + "content_hash": "b431b73dfa86ff83145468bbf4422a79" + }, + "indexer.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\indexer.py", + "relative_path": "indexer.py", + "size": 7776, + "modified_time": 1758177151.2160237, + "extension": ".py", + "category": "code", + "estimated_tokens": 1893, + "content_hash": "f88b5e5bffce26f3170974df2906aac3" + }, + "install.sh": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\install.sh", + "relative_path": "install.sh", + "size": 5236, + "modified_time": 1758161898.317552, + "extension": ".sh", + "category": "code", + "estimated_tokens": 1262, + "content_hash": "cc3a9121a0b8281457270f30ad76f5f6" + }, + "requirements.txt": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\requirements.txt", + "relative_path": "requirements.txt", + "size": 495, + "modified_time": 1758164967.7707567, + "extension": ".txt", + "category": "docs", + "estimated_tokens": 118, + "content_hash": "aea2ba14dfa7b37b1dde5518de87d956" + }, + "setup.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\setup.py", + "relative_path": "setup.py", + "size": 2860, + "modified_time": 1758177212.9095325, + "extension": ".py", + "category": "code", + "estimated_tokens": 692, + "content_hash": "609abf8b9c84a09f6a59d5815eb90bc5" + }, + "__init__.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\__init__.py", + "relative_path": "__init__.py", + "size": 1065, + "modified_time": 1758177224.8017242, + "extension": ".py", + "category": "code", + "estimated_tokens": 257, + "content_hash": "47368b235086fc0c75ba34a824c58506" + }, + "cache\\embeddings.pkl": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\cache\\embeddings.pkl", + "relative_path": "cache\\embeddings.pkl", + "size": 35109, + "modified_time": 1758175163.6754165, + "extension": ".pkl", + "category": "other", + "estimated_tokens": 4713, + "content_hash": "b8ed5c068acd5ed52ba10839701a5a24" + }, + "cache\\embedding_index.json": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\cache\\embedding_index.json", + "relative_path": "cache\\embedding_index.json", + "size": 5589, + "modified_time": 1758175163.6764157, + "extension": ".json", + "category": "config", + "estimated_tokens": 1358, + "content_hash": "5c2ba41b1b69ce19d2fc3b5854f6ee53" + }, + "cache\\file_index.json": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\cache\\file_index.json", + "relative_path": "cache\\file_index.json", + "size": 12164, + "modified_time": 1758165699.0883024, + "extension": ".json", + "category": "config", + "estimated_tokens": 2957, + "content_hash": "73563db28a2808aa28544c0275b97f94" + }, + "core\\config.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\core\\config.py", + "relative_path": "core\\config.py", + "size": 12266, + "modified_time": 1758164531.5934324, + "extension": ".py", + "category": "code", + "estimated_tokens": 2985, + "content_hash": "d85aedc01a528b486d41acbd823181d7" + }, + "core\\context_analyzer.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\core\\context_analyzer.py", + "relative_path": "core\\context_analyzer.py", + "size": 15002, + "modified_time": 1758164846.7665854, + "extension": ".py", + "category": "code", + "estimated_tokens": 3661, + "content_hash": "677903b5aaf3db13575ca1ca99ec7c16" + }, + "core\\embedding_manager.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\core\\embedding_manager.py", + "relative_path": "core\\embedding_manager.py", + "size": 17271, + "modified_time": 1758166063.1635072, + "extension": ".py", + "category": "code", + "estimated_tokens": 4204, + "content_hash": "d8f52cb93140a46fe3d22d465ec01b22" + }, + "core\\file_indexer.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\core\\file_indexer.py", + "relative_path": "core\\file_indexer.py", + "size": 14484, + "modified_time": 1758164612.5888917, + "extension": ".py", + "category": "code", + "estimated_tokens": 3525, + "content_hash": "1518d309108f3300417b65f6234241d1" + }, + "core\\gitignore_parser.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\core\\gitignore_parser.py", + "relative_path": "core\\gitignore_parser.py", + "size": 6757, + "modified_time": 1758164472.643646, + "extension": ".py", + "category": "code", + "estimated_tokens": 1644, + "content_hash": "9cd97725576727080aaafd329d9ce2c4" + }, + "core\\path_matcher.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\core\\path_matcher.py", + "relative_path": "core\\path_matcher.py", + "size": 19568, + "modified_time": 1758166045.8395746, + "extension": ".py", + "category": "code", + "estimated_tokens": 4767, + "content_hash": "f1dc44dc3ed67f100770aea40197623f" + }, + "core\\__init__.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\core\\__init__.py", + "relative_path": "core\\__init__.py", + "size": 712, + "modified_time": 1758164419.4437866, + "extension": ".py", + "category": "code", + "estimated_tokens": 172, + "content_hash": "b25991cb8d977021362f45e121e89de7" + }, + "tools\\module_analyzer.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\tools\\module_analyzer.py", + "relative_path": "tools\\module_analyzer.py", + "size": 14273, + "modified_time": 1758164687.488236, + "extension": ".py", + "category": "code", + "estimated_tokens": 3476, + "content_hash": "b958ec7ed264242f2bb30b1cca66b144" + }, + "tools\\tech_stack.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\tools\\tech_stack.py", + "relative_path": "tools\\tech_stack.py", + "size": 7576, + "modified_time": 1758164695.643722, + "extension": ".py", + "category": "code", + "estimated_tokens": 1843, + "content_hash": "f391a45d8254f0c4f4f789027dd69afc" + }, + "tools\\workflow_updater.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\tools\\workflow_updater.py", + "relative_path": "tools\\workflow_updater.py", + "size": 9577, + "modified_time": 1758164703.2230499, + "extension": ".py", + "category": "code", + "estimated_tokens": 2334, + "content_hash": "526edf0cfbe3c2041135eace9f89ef13" + }, + "tools\\__init__.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\tools\\__init__.py", + "relative_path": "tools\\__init__.py", + "size": 329, + "modified_time": 1758165927.9923615, + "extension": ".py", + "category": "code", + "estimated_tokens": 79, + "content_hash": "139aa450d7511347cc6799c471eac745" + }, + "utils\\cache.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\utils\\cache.py", + "relative_path": "utils\\cache.py", + "size": 12067, + "modified_time": 1758164781.2914226, + "extension": ".py", + "category": "code", + "estimated_tokens": 2929, + "content_hash": "39e49b731d601fafac74e96ed074e654" + }, + "utils\\colors.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\utils\\colors.py", + "relative_path": "utils\\colors.py", + "size": 6959, + "modified_time": 1758165650.9865932, + "extension": ".py", + "category": "code", + "estimated_tokens": 1678, + "content_hash": "8bb57134555d8fb07d2e351d4e100f0f" + }, + "utils\\io_helpers.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\utils\\io_helpers.py", + "relative_path": "utils\\io_helpers.py", + "size": 13773, + "modified_time": 1758164823.513003, + "extension": ".py", + "category": "code", + "estimated_tokens": 3349, + "content_hash": "aa54747c49319cc2c90c0544c668009a" + }, + "utils\\__init__.py": { + "path": "D:\\Claude_dms3\\.claude\\python_script\\utils\\__init__.py", + "relative_path": "utils\\__init__.py", + "size": 370, + "modified_time": 1758164433.7142198, + "extension": ".py", + "category": "code", + "estimated_tokens": 88, + "content_hash": "62ec4a34f1643a23c79207061bdb8d49" + } + } +} \ No newline at end of file diff --git a/.claude/python_script/cli.py b/.claude/python_script/cli.py new file mode 100644 index 00000000..eef6e2fb --- /dev/null +++ b/.claude/python_script/cli.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +""" +CLI Interface for Path-Aware Analysis +Provides command-line interface for intelligent file analysis and pattern matching. +""" + +import sys +import argparse +import logging +import json +import time +from pathlib import Path +from typing import Dict, List, Optional, Any + +# Add current directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from core.config import get_config +from core.file_indexer import FileIndexer +from core.context_analyzer import ContextAnalyzer +from core.path_matcher import PathMatcher +from utils.colors import Colors + + +class AnalysisCLI: + """Command-line interface for file analysis and pattern matching.""" + + def __init__(self, config_path: Optional[str] = None, root_path: str = "."): + self.root_path = Path(root_path).resolve() + self.config = get_config(config_path) + + # Setup logging + logging.basicConfig( + level=getattr(logging, self.config.get('logging.level', 'INFO')), + format=self.config.get('logging.format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ) + self.logger = logging.getLogger(__name__) + + # Initialize core components + self.indexer = FileIndexer(self.config, str(self.root_path)) + self.context_analyzer = ContextAnalyzer(self.config) + self.path_matcher = PathMatcher(self.config) + + def analyze(self, prompt: str, patterns: Optional[List[str]] = None) -> Dict[str, Any]: + """Analyze and return relevant file paths for a given prompt.""" + print(Colors.yellow("Analyzing project and prompt...")) + start_time = time.time() + + # Load index (build if not exists) + index = self.indexer.load_index() + if not index: + print(Colors.warning("No file index found. Run 'python indexer.py --build' first or use --auto-build")) + return {} + + stats = self.indexer.get_stats() + print(Colors.cyan(f"Project stats: ~{stats.total_tokens:,} tokens across {stats.total_files} files")) + print(Colors.cyan(f"Categories: {', '.join(f'{k}: {v}' for k, v in stats.categories.items())}")) + + # Determine project size + project_size = self._classify_project_size(stats.total_tokens) + print(Colors.cyan(f"Project size: {project_size}")) + + # Analyze prompt context + print(Colors.yellow("Analyzing prompt context...")) + context_result = self.context_analyzer.analyze(prompt) + + print(Colors.cyan(f"Identified: {len(context_result.domains)} domains, {len(context_result.languages)} languages")) + if context_result.domains: + print(Colors.cyan(f"Top domains: {', '.join(context_result.domains[:3])}")) + + # Match files to context + print(Colors.yellow("Matching files to context...")) + matching_result = self.path_matcher.match_files( + index, + context_result, + explicit_patterns=patterns + ) + + elapsed = time.time() - start_time + + print(Colors.green(f"Analysis complete: {len(matching_result.matched_files)} files, ~{matching_result.total_tokens:,} tokens")) + print(Colors.cyan(f"Confidence: {matching_result.confidence_score:.2f}")) + print(Colors.cyan(f"Execution time: {elapsed:.2f}s")) + + return { + 'files': [match.file_info.relative_path for match in matching_result.matched_files], + 'total_tokens': matching_result.total_tokens, + 'confidence': matching_result.confidence_score, + 'context': { + 'domains': context_result.domains, + 'languages': context_result.languages, + 'keywords': context_result.keywords + }, + 'stats': { + 'project_size': project_size, + 'total_files': stats.total_files, + 'analysis_time': elapsed + } + } + + def generate_command(self, prompt: str, tool: str, files: List[str]) -> str: + """Generate a command for external tools (gemini/codex).""" + file_patterns = " ".join(f"@{{{file}}}" for file in files) + + if tool == "gemini": + if len(files) > 50: + return f'gemini --all-files -p "{prompt}"' + else: + return f'gemini -p "{file_patterns} {prompt}"' + elif tool == "codex": + # Estimate tokens for workspace selection + total_tokens = sum(len(file) * 50 for file in files) # Rough estimate + workspace_flag = "-s workspace-write" if total_tokens > 100000 else "-s danger-full-access" + return f'codex {workspace_flag} --full-auto exec "{file_patterns} {prompt}"' + else: + raise ValueError(f"Unsupported tool: {tool}") + + def _classify_project_size(self, tokens: int) -> str: + """Classify project size based on token count.""" + small_limit = self.config.get('token_limits.small_project', 500000) + medium_limit = self.config.get('token_limits.medium_project', 2000000) + + if tokens < small_limit: + return "small" + elif tokens < medium_limit: + return "medium" + else: + return "large" + + def auto_build_index(self): + """Auto-build index if it doesn't exist.""" + from indexer import ProjectIndexer + indexer = ProjectIndexer(root_path=str(self.root_path)) + indexer.build_index() + + +def main(): + """CLI entry point for analysis.""" + parser = argparse.ArgumentParser( + description="Path-Aware Analysis CLI - Intelligent file pattern detection", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python cli.py "analyze authentication flow" + python cli.py "fix database connection" --patterns "src/**/*.py" + python cli.py "review API endpoints" --tool gemini + """ + ) + + parser.add_argument('prompt', help='Analysis prompt or task description') + parser.add_argument('--patterns', nargs='*', help='Explicit file patterns to include') + parser.add_argument('--tool', choices=['gemini', 'codex'], help='Generate command for specific tool') + parser.add_argument('--output', choices=['patterns', 'json'], default='patterns', help='Output format') + parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') + parser.add_argument('--auto-build', action='store_true', help='Auto-build index if missing') + parser.add_argument('--config', help='Configuration file path') + parser.add_argument('--root', default='.', help='Root directory to analyze') + + args = parser.parse_args() + + # Create CLI interface + cli = AnalysisCLI(args.config, args.root) + + try: + # Auto-build index if requested and missing + if args.auto_build: + index = cli.indexer.load_index() + if not index: + print(Colors.yellow("Auto-building missing index...")) + cli.auto_build_index() + + # Perform analysis + result = cli.analyze(args.prompt, patterns=args.patterns) + + if not result: + sys.exit(1) + + # Generate output + if args.tool: + command = cli.generate_command(args.prompt, args.tool, result['files']) + print(command) + elif args.output == 'json': + print(json.dumps(result, indent=2, default=str)) + else: # patterns output (default) + for file_path in result['files']: + print(f"@{{{file_path}}}") + + # Show verbose details + if args.verbose: + print(f"\n# Analysis Details:") + print(f"# Matched files: {len(result['files'])}") + print(f"# Total tokens: {result['total_tokens']:,}") + print(f"# Confidence: {result['confidence']:.2f}") + + except KeyboardInterrupt: + print(Colors.warning("\nAnalysis interrupted by user")) + sys.exit(1) + except Exception as e: + print(Colors.error(f"Analysis failed: {e}")) + if args.verbose: + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/config.yaml b/.claude/python_script/config.yaml new file mode 100644 index 00000000..85c6925d --- /dev/null +++ b/.claude/python_script/config.yaml @@ -0,0 +1,158 @@ +# Configuration for UltraThink Path-Aware Analyzer +# Based on gemini-wrapper patterns with intelligent enhancements + +# Token limits for project size classification +token_limits: + small_project: 500000 # <500K tokens - include most files + medium_project: 2000000 # 500K-2M tokens - smart selection + large_project: 10000000 # >2M tokens - precise targeting + max_files: 1000 # Maximum files to process + +# File patterns to exclude (performance and relevance) +exclude_patterns: + - "*/node_modules/*" + - "*/.git/*" + - "*/build/*" + - "*/dist/*" + - "*/.next/*" + - "*/.nuxt/*" + - "*/target/*" + - "*/vendor/*" + - "*/__pycache__/*" + - "*.pyc" + - "*.pyo" + - "*.log" + - "*.tmp" + - "*.temp" + - "*.history" + +# File extensions grouped by category +file_extensions: + code: + - ".py" + - ".js" + - ".ts" + - ".tsx" + - ".jsx" + - ".java" + - ".cpp" + - ".c" + - ".h" + - ".rs" + - ".go" + - ".php" + - ".rb" + - ".sh" + - ".bash" + docs: + - ".md" + - ".txt" + - ".rst" + - ".adoc" + config: + - ".json" + - ".yaml" + - ".yml" + - ".toml" + - ".ini" + - ".env" + web: + - ".html" + - ".css" + - ".scss" + - ".sass" + - ".xml" + +# Embedding/RAG configuration +embedding: + enabled: true # Set to true to enable RAG features + model: "all-MiniLM-L6-v2" # Lightweight sentence transformer + cache_dir: "cache" + similarity_threshold: 0.3 + max_context_length: 512 + batch_size: 32 + +# Context analysis settings +context_analysis: + # Keywords that indicate specific domains/modules + domain_keywords: + auth: ["auth", "login", "user", "password", "jwt", "token", "session"] + database: ["db", "database", "sql", "query", "model", "schema", "migration"] + api: ["api", "endpoint", "route", "controller", "service", "handler"] + frontend: ["ui", "component", "view", "template", "style", "css"] + backend: ["server", "service", "logic", "business", "core"] + test: ["test", "spec", "unit", "integration", "mock"] + config: ["config", "setting", "environment", "env"] + util: ["util", "helper", "common", "shared", "lib"] + + # Programming language indicators + language_indicators: + python: [".py", "python", "pip", "requirements.txt", "setup.py"] + javascript: [".js", ".ts", "npm", "package.json", "node"] + java: [".java", "maven", "gradle", "pom.xml"] + go: [".go", "go.mod", "go.sum"] + rust: [".rs", "cargo", "Cargo.toml"] + +# Path matching and ranking +path_matching: + # Scoring weights for relevance calculation + weights: + keyword_match: 0.4 # Direct keyword match in filename/path + extension_match: 0.2 # File extension relevance + directory_context: 0.2 # Directory name relevance + file_size_penalty: 0.1 # Penalty for very large files + recency_bonus: 0.1 # Bonus for recently modified files + + # Maximum files to return per category + max_files_per_category: 20 + + # Minimum relevance score to include file + min_relevance_score: 0.1 + +# Output formatting +output: + # How to format path patterns + pattern_format: "@{{{path}}}" # Results in @{path/to/file} + + # Include project documentation by default + always_include: + - "CLAUDE.md" + - "**/CLAUDE.md" + - "README.md" + - "docs/**/*.md" + + # Maximum total files in output + max_total_files: 50 + +# Analysis modes +modes: + auto: + description: "Fully automatic path detection" + enabled: true + guided: + description: "Suggest paths for user confirmation" + enabled: true + pattern: + description: "Use explicit patterns from user" + enabled: true + hybrid: + description: "Combine auto-detection with user patterns" + enabled: true + +# Performance settings +performance: + # Cache settings + cache_enabled: true + cache_ttl: 3600 # Cache TTL in seconds (1 hour) + + # File size limits + max_file_size: 10485760 # 10MB max file size to analyze + + # Parallel processing + max_workers: 4 # Number of parallel workers for file processing + +# Logging configuration +logging: + level: "INFO" # DEBUG, INFO, WARNING, ERROR + file: ".claude/scripts/ultrathink/ultrathink.log" + format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" \ No newline at end of file diff --git a/.claude/python_script/core/__init__.py b/.claude/python_script/core/__init__.py new file mode 100644 index 00000000..ac4eac25 --- /dev/null +++ b/.claude/python_script/core/__init__.py @@ -0,0 +1,25 @@ +""" +Core modules for the Python script analyzer. +Provides unified interfaces for file indexing, context analysis, and path matching. +""" + +from .config import Config +from .file_indexer import FileIndexer, FileInfo, IndexStats +from .context_analyzer import ContextAnalyzer, AnalysisResult +from .path_matcher import PathMatcher, MatchResult, PathMatchingResult +from .embedding_manager import EmbeddingManager +from .gitignore_parser import GitignoreParser + +__all__ = [ + 'Config', + 'FileIndexer', + 'FileInfo', + 'IndexStats', + 'ContextAnalyzer', + 'AnalysisResult', + 'PathMatcher', + 'MatchResult', + 'PathMatchingResult', + 'EmbeddingManager', + 'GitignoreParser' +] \ No newline at end of file diff --git a/.claude/python_script/core/__pycache__/__init__.cpython-313.pyc b/.claude/python_script/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29c8bdd560db281781a76381aa4764be307c33f3 GIT binary patch literal 816 zcmZ8fJ&)5s5M6)7act+390fu&+eRF@0?}}QgaSd>IuuA7X=Ut(lDX{If~OR}~x<#(HgY{Cpr2 z0%T%L%*Y5$Fsa&GkrmothYe_i4mdhzN6pX$H*7&m$BoDfeelBog0Ky3opYk2umc^v zZ${m)2R%wo$;5qSz;V{tr8H+n#M((uP-?a zyfKt-MT&*1Ld*nDsVHg=vpCuRI}BuIF?S*U21< z2HrOV$L<@_8J5LNWPMBeBcyquSU5;`7tBiO>C~i->#A?c4wftOCwp=7Nbj)AYhJDL zTJemr)46JdJigxS&gAi*6O^#8H)QwO;tfwzOr7J%83$C=c`j;^6=>2ph6+)9ZZ+{Ww71Lm?r4Ul%aM}Gd5VRrTcLAlpez^WKlO;-9h+n8HVx0v<&-~M~vGW fa&|?|HssNUjIPM&nw(yfyFcx~H{UhkCf5H3XQu5t literal 0 HcmV?d00001 diff --git a/.claude/python_script/core/__pycache__/config.cpython-313.pyc b/.claude/python_script/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f0e167be19268915ac6136a87482f74b3b1d98b GIT binary patch literal 14061 zcmc(GdvIIVnco3$@dgQq?>8?Wk_bu!C5x7A$&yWz)QhAn@rsTt(m_Gsl7s~U@V%fU zLUp6r+38T5bR@0on%d2b>6y%~)JYSi)7>WBY*&u+hdZ6l0s(EpH`=VW+1agjrZbeR zG+u8z+uwICE(p?;leC@bA^F|&etz$B_-R?0iy`d0@SkV@u9vZ&Q^E-LLgdpi6Ju{P zi78AnN#K+$=$$S7hX3hzR7Kp=fGLU zLLtT`%i7t~h3P33la=VL>}kxBg2;Hfe6nK9ujiCPqg6TEXWrDpJ6YY%(5k1jz8*JP z87;PG?L$lK;O1bo+}L8qJY)7(q-R_MiTHGMW?l)UqKUXT8j6Qz5=Rq}`IziF zr6evyBeE*a$D`9xIUSx(Y3xu;6GozH>H+N{YBtQssbDA;3l_T!CPS%|ti;uD=^Bej zl0w6;0!=0v(1Q_b04>alAXyZvBq%niRkBXnCmnROedcl2XOe6v=z$kon?4{#<1;Zi1$5d9R}_&W%4}3tLP~fx8V<$8SRxcbqrk}{np z&R{gIrb6+stXb9hq^u}zwAAduU|hZ!4EimaB^*L;w0(mI zCI@1n`G_2h%&Ge(1E_$+WO5-jn}`S1uo6wCCc_{Z0HVb}azUw}&dT*&4HO2dMxNRY z@YxlXcb8u~boEfyE#7Nt`__(I9oeS+chp?d!Nq}ZjOM)+*WSGPX2#L95|iOoE!{lHY!DqCrIcVEVQW&h9Z$Ic$iid3?-AXg`gb25KLSERuPxg ze8CGCITA|Ag${0;H9?hQ(|jf>ZM`Tev@RN%W{Q!$MMTjv8c?jt0E8&sRd*A9^PY

J;Y7k*u^7g%pf~h?~YumF(Ep z8H=08&12MVQ!r{8Xc zH|8DL%jZqUuqhZj_kTD)hp|f{o3YMV+t_Ya91|xxZ5CN8W`}wH6=R1it!&!V#wLh& z@nB~mXfko@6Ly4)<+)?!4S4$a3>8zrR^eVn~S- zXR73*6y{JE&2LjWsg-BL;D(^39nwtGS{ZRIxyZLnvPvi|^1vC6(n_6F8oZ{2$&r?; z{x5(l?0#+IYHd%pwkKEHyXg6O*^SAbW zzxF5f{dZ5_>)QR@@_$`^=Z##~b4z7EYwXH*2UfceWxEf3Kk{eie{lZ8L%Hr(mj=H% zoNp4}9lkld+O#{{v^&%L(%pl1don$zR+>)#(#aZLGG*)gAKF<{*IFI(Rb9J$?Q*`M zWwqg{Y{OHzhQ7tYZw%)>zH7r*hgUsKSx-}@`M^h>gKJjiYy4#;HUGH2{~sQ^SY6wv z%!EnitLokvzA?O7wKH3_Gt)hE_x#=0GGk{lo9jVCwWNEG$|*6SL}Udd90S#fiRmH<6OSN$ zQzhaK&&uKRfpmd90z{PQ%8dlYGyT?rasV+d$0G`z70G0xC(;IzAq8fX-=fo=mG`f? zbagKni7EvJQ5WV4>2+G2L0g+S#UYfcZvue8d=-n

EzR^5&G&3m?88@W32&8nry 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 literal 0 HcmV?d00001 diff --git a/.claude/python_script/core/__pycache__/embedding_manager.cpython-313.pyc b/.claude/python_script/core/__pycache__/embedding_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73e5c437665f4aaf042d0a6cfb8ffa135f32c549 GIT binary patch literal 23228 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 BhQI&- literal 0 HcmV?d00001 diff --git a/.claude/python_script/core/__pycache__/gitignore_parser.cpython-313.pyc b/.claude/python_script/core/__pycache__/gitignore_parser.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5417ba7120401abf5cbf558d6eaef727eae0abe7 GIT binary patch literal 7930 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 PFFmhcxf$_ZdOZIFwKBB^ literal 0 HcmV?d00001 diff --git a/.claude/python_script/core/__pycache__/path_matcher.cpython-313.pyc b/.claude/python_script/core/__pycache__/path_matcher.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ed1969cc1407373f0032d205f48edc8c07feb56 GIT binary patch literal 22421 zcmb_^dr(_hdgm2-zYrjSBqTsQY>*AYHuwQ{`wh0S!DFt~O*c$40tt{?7M?5Nhi0bZ z%2P8twd3BIsm7VwG`%ymO>d@lNGerJ9<}kL_76`|wWZslE%j|r*2&~iovoyB>|}Ol zYpeG6ovSMeTDUu@J&1G9J?A^G@0|0U?{Ut3X0@6)xTdfE*}{!W9QQMNQJl6Ac=k64 ze3#=nf#X$t)kT${V$Uj}iaphW8c+2_&4^aej_3s4h+fc-7zD$JQ84-_kLIFj#4MOu zSbNbjVil|`Ty?QpsOELN{-SN9MyNrYjyGJa9jO!QeB6}T#kFy~v616V2dm_^94s_R zuoqIzNVR+_)xmOFkyfpw3H7|KkrNuGoP5pT1jmI895>a_#wjKGxcRF2Dt~S2mgx(j z3N2)@z9@KP+|n_Coo!B|oVU0sOA-|!JgA>A>um~55?}JB7#ZIk?_q$ zW08a}x#$Z~N+O!@%`V21k=sc`gndFN{svMPBG-L0cYH!57P%RU&qe~KPpE5SeyyZA z8=Xx`noH3{Qqo>sx)F;=hRZjS(M1#|srg9Kuab=6P%<*+b>ZdNc?7=8O>r1hjs{dv@8b9>K{KVD((&p}q)zD( z(y)+W%1E;U@7iLDjzYX%FwGnMM#;?3AXoTF9a2&~@{4bA&Ub?_dz~;FjnDgTMe$a| zfG;t-C`1C1k?=PiMGqufF>#tDN>-M4T8M-ai}8e1JsV0!<`;!K(=&_lrNsL{pII_Y zPsc;oBh%B8X?prP8$EG0z0^t50`izOt7 z;_<~~h>)H@vUV=E7)lBnN>nG3f>b{}ouK5|=}n(oH`Q= zErlb~;p>UMsQ?BGFH<+}Bo`Lr=#CJ*k({C)PZ3h4S;rzm;Km)H8u^(=*h{p+`wi|d zG>7vxXIA6P*EeQ0jd{CsS^d^PR^!b(+>bPFp;^gOd5mX}ywe!7@zfVa+AyUrjI?pe zG-c?oDx}Jzt(`Iz-r1N>nE|b5qi+x_^G3f(s-vmMW&j4bFbNt_d1~*O#p{^K$p{9V z=HjPk!{UvIzz`lN409xmLBdj!EJ+MSY&yC4Mg)UyDhzrQDOTVv84)l>ONmHWs=-2= zi-sduPx6Fe12X2f3iX7a2J)QbxyW;qM?)%OoSG@Aqw%CbOGEII=OM3&JQ`UU%Pkb5 zQDGkCO7z2{F*-=&VixTkSxra2u0E@&&(}Ea@yqS^Cf=&fYMRT2t-#)(nRu)suVrp27ZK@U})uEOe7Hj8gK~{`YdbwqM zU0GW&^=NVd*3MuZ+hFy8l_4~|gF)2u^-3Hs*JPNgE0oc|AR72q-US#I%e$wXQ!YwJ zn@W(41bIy*xSKh?ZCjmGYAKfo`3rb1l+xI5;=P6Z9==H_wa^;EZZm6#w=9k7W<(U& zYhkHPyiaJJclz5Ub8-1ZgiqSh($rD5n6RJhysIoFh?w3C3DL!+gfA2WK?GsFo(M=f zwjSnZ3!5j(<2WP!X@Wod1)E$GH8F~1Zn6kBPRnAOYvU$$O4LE*B^TskMaZCPT$#9X zBJ+9GK2F(!E2dZRYF>jsE&g-?tpbaZi?#hLOV;y-veNWQqyCkp8Ts_86SMxT$hT*q1phZe?eC9cS-58^ug`Ed8NYdYm zMCTWhiNF2L`{X`4Q3(7*3H<&Ea3|I`B6n^r3SoK2l5F5>K$Wpi79wiHQ6Vy$q)oZN z_DHpCM^8jkk?9)|FfqwHk`*u@(}j&&YPcS{Efae{2-C{8{s~cC$$mW=$Ch7UlnR`H zZgDAjV<{=wD5tzXuod#j0m@Pxiros`Nq~!(jbYdQTfzp%Z7Lo4x0fTfVh}eoeloX5BfJ=^3YUnUTw~S;uwuym#&0YilhJ+OoTca=R~xyDwx$ zW-{Tq?C$xrX2Z~)Z}-19`0n89;<}J+Kb>nI7Tbq2=cY2RhqCQ61c3^6y_341TJ2gp zoozXsYdI>m9L*dXeJEsG#tFcS#sTEo{cEc(G_Ltju6a;w9?TrQ_;5Vi`~?E>Y#O+F zSH8iWcR2I*hJ3vXE$uL+H6I#4DA94DhM}o26BCk7h$NSU_$<*i#S!&jbj}CVewPbY zbrn*=pjf=Bm+PFdi z71sr;?A-Z(%W-jiQ2)XlhKXN2io!!+Q-V6as>g~vx}bKvNKZ;N^Xj0!n(M1lTIb_N z`#5gmSIY8hUYK9|sr)=@Rad0xI;EDlF=#|zI?yiD#5bs}Fh*k(6bkLtzpy^T_WID` zZ*rWO!}Ff}ERAUhJabStiT=wjSnOvHXx?Rrt8w$Bt(a=&HN~9)tW*c*H>M7q3B_iY zVh|`~LBux`0!!&zjQa}HOqN@)6G!80GiJg}pg>wUZoW|Z$zL!5h6Ig4;RGUop#_t= zFes^d3$Ra4qzpGgLNXeP4f+j43Q=qHCWs^8ByDW*7KjTCp`@WwBwgqR$Yxm5CT_%{ zNl6`x#3eOoB52bkK(WOjTqbxEl`zc;kgACsGI2+K*UJ({0>zNjA?XX%u_yJ-x%zIgzB^ZcK&(HIt?y5p9=n^Cqq*jTV)MbQ`%un(RCFKBxlf7i zQ(5k9-@0^fDDUvRJ@(ev%EapY>Z_Ud!yAqxlEeMX$hq5}wsS7uKZ8|x zY5Cv}`c`g#|D_K+`yRAs46etvx{SSjh0ogjtA{hT0~x~sM)@RTEEJz#3e86nTc{n4 z2%~lljH@fHj4EEyiJ!=WF6xA9uNmR(DX#+n9m|R0G^g zEnmmmc?Vw~&@y^hq`oMk;Ly7Yw3c@$YaDISVvRSX4(zzbX|ecc~&qe4+ize(vfny1snoCzVsaw)S{vMKosBqc!{wL(&4deT@% zPl^Q<*uUL7P?T+O*HCEUzrrJ$;sQ;1;%Le_Iz>lk&e0<}da{n*wDBJeoHHOb?$3LC zE4!C_^Yz|)7xH%3y-w&_?wynD$Ma6_z4^SO@!k}pM) zhkE8i+n&7LyJir510UK4^4^wvLmxTZPqEV5Hmf;H?Y%Sq=GYULC*RnVhwp66yS-p+ zTB_5RkzC&)H*D3I>lzfh2G`GI9VgR6Pi&5~MPB;)@Z$AQG`@A=)0W2;{%>KS|Nmoi z#w#o{zPPEzK^(BiRdKCM9*Vw(5ezN!b zHpK_BGa4u`7Z4yAlQCS8$bER=eSP@#hG+>AzUNrq$uT0ZblpgFN9;)Nl({d%v{bN}8O-lSN^18`;hrIs)PtsE@z_85qX)vOLzo_at?$-&g5DcT8lN5T5 zyeaa&2yY7;_ce+QllLX^7}2!J>z>#{rt6e+a)bmfvJj&$goFs?nuE6kk5*1oi&O41 zXzDh&?@;M~4UcGQ!wdMdT>YR}Ke*nPtv``A<%>KS$Zh>amDGGya$S||mnGL1BSKhNn^c=Wn__(F>-nox`dzLkM zPussgk@vQ)@b@n*4-uy~c7JU3>e}u#*LvOhx%Gj}-k}Zeu;gt8!_~U?nVR!;J=LSg ze{SIVk20?7*7uKn;OTv!egW6DMS-S4*BQnAj|hO`a-Ua(+@cDrAY$@Rg%w$rDlj!v z(>52Z8U+4Bp!M^bogjE^kCx+#aJqTD-;nAoQJ#VhhMBam6z}+AAz>aCF4DCJBr{eB zj9u5sC`F2RSuZn9+8gZdD~g=+h@+vm3^iQ0GA<_-YotqtFL{p+tUFRa?{$FtUML_M)Lu)TK6fJ0)H z2Rw`b%%Eff<>FPrs&Nd%c3099Oza=0oZDOvwzIe<2qpetU2!16T^HZv;p#387c*_K z!D2eIWbBR)1U)(zg>)*9nLzc7MeklwaRt3r_C)mVppg@>mvQpQK-rXxLYTBFd8g^s zuPqIa@J9faEVE030K(0ZLH;l)rb*hsjG_@!#;jb{+p5ajN!Hr}^$?{c2H@T0erB*g zaeCgq^VXejeof)@7|QqBJPOBpDPF>lR^% z_IM9M7vi_EU(i{C)+tlxE4mm4`8%ju>lt8X4T zizDd2#(@uC`|B7|{b-*`2>c2@~!pt%rfx4Tk*;^v9phGGlYB%H)8 z6X)2|3cl3HC^FQSLF+h~9=AE(gtEz+xXo=<+69fgnUzz!V>uROw)nV7GF>tkR85j< znK|&l#S;@jEBMsv0S&}sTdJo-bQZBOgkrIhAY9;A=@>+ak)415{b4~`?57YD8HxM} zhsa~3=P-p{hL~mK&~)|J2=f#s)t2xT@|bw} zI}~Dk(mI76kk?Bd?K;9m@@|pGxU8ELB7-_KW}(~u>N0ZoClp(>X2^;vo1(W;lx1b+ zjRM3rZb&jCqF}v;rMi4;5~%l5wQN;UbZ4w*l6fu)JBZA8u?{Dwjf}n)J$Z9<&ngnb zr%WKb%D2ZI!Ez`#u?zxumjgN76Uh08MgQ=FJGt>$aeOxG4~vG~X=}c*IoCKKHV&-& zb3?C)L$72TC(^cj=iXfBu-M7KXT}^?5uS{mW`}2*Rd5{08QCnl$Ua!*&-1vS|lLof1@DazbijWR)Zq% zrU!-%OvP{t7}lYOv^=-Dp#CUU6|-m5qh9?uQLk;Ta!#dZJ}zjeWIx%`+A{?;gaIU4 zmGZOW05ad_lMVlo+b|2vMw7nz*y0QsC76&Hz)>u16ykV7($B>iIU{v`a)DVEgjdl} zMj>&W35H7yGbm&z9QGS!3YnpFnukI=d28ftkVkY{G9@CBI5Ss(?r#SntvcOl%g65B=?i(AD`(p++IFvA`_OhI zU(-nDg5^88mP2C8p^ciu`5JGord_OQU%Bw8CIHd0u@iKt#+xz7r+caYM3#LHl$Z1$XOmuTCyaxHgYNIWoGwz(2@_H05eK#F~zksYf*jXdSj}S~z1v&d@00*E0Cv zY=(jtp4hN9y$k_kl7(7SJZETTCk&@!(d*G<<;nW1XdZ@5Rov6C>-2IRZ2CeSuGt3* zevqc|e=6)7=*f1+tXLKHU+g&AO5$eFz^jyrj|pBu^rQ(MqW$kBOz`44hY&=XiF1{9 zPZCKAiX_Sf#pGm3ZLL;zC)7n&#cht&g@r3mf9$rlZ2)*J-@kTfft4-u&+ISGG@!hR ziB9;VKP>EowCvQWGdxZN%_iw2HRxR;Sy+K|rWgi#Hj+Lzk~k6<5Bn5z6RFp6fERX@ zWFo*M5rLKltXQN9PoK(a+hYVkYeU&C|DGcLfpS#iNDrE& z%qgLRB1^_DrlC&=e@F1e3M=lKayiq~wZbvjExYEYk~?VSh$xrTve1{*r!PEld(zc; zrwjafqc?5)nYRmT$JLQ@1w>Z>%gX7?IsKy3pFR&hx27d$YZGm4D@RtZWNp1`{kgs| zv2QGE8-u0$pONla{%S68Uc|5E{KKQ!mX{I#9P##h$Fs;5&g~u)cMoQ5gRq*zo>kMF zF*I!&IjbjUX%#K4E8QEG-n_+?^BxlMvm8QOx(?^Mj*4AJ*Uvu)WxLKl+%I-rN?*=e zc0KLnn*5u4*ED{EfFMC{ju0-{y*~Ufi+Ob(Qidq!Q#*SCJ(* zC}V_Csm!!jY)@IP&p%T7vi=wA-QNF+mx;(tLWqzZ1Ujb@E@*i5n-2nj|GX zZo;UW*|DorfUpEDm9$Dh(-z^yG+xvcm1?Yh==GmjT_O69pq5Jg_<^JC0VhB32J z(sqnt2DX-JR6-NKTO51qzhI6U%WAZiYAio0htk@WT1wA}Y_Y!T9qJ>Bw9WCRvihn^ z^=)0fle?5`JZ?AztL?>mN=lBn4a`OjrJRRg9j^`6j_+3Tl%C_tGAc!J@w#Bm_};RJ zLNHiYI+V;?X?_K3f^}@`g{qGo&Edu=dYha0%OW1^lf6n}usUc%etpmm{URN|v8BAD z*ml`IUAhA_w-leHTQ-4I>!@-sh)D~~;-RSb<*P!eYUM1Ryr-uOzQU1+RLv<_@+pWt zgT9nbW(ZRDAw@oh^vgtJBs%ydoZ3xbn=T5>h^@Oy_e{Vl#KTco&m#%MIfuzC<_pb0 zBMM#v-0s3+EQ|!B%zvPSEgU{EcBB}6MuJv5*d*GEFh*eb7i%k0_zJAxP42r|NFrVt z>+{@U^_)uh3lsK1=iZ&dXS(<0kDGn#lG4ilvrPdX9cu5nZOK`fI zq1aL+A$&;rNmU^{B99m_;eV3%3?392OV>+0m3&I6zRa}9P71KQLG0p=-%*lFxrP$p6V$Ox@@0U$?3;UsztjZkKk+`zx5Mn5I5gt%YO z>n-vKdkHY_B}Gb6qoSDK%wT2voxT~m|jftYqWmL{B?8A;1Uh4jOW-N@ef?N zQ{&>P@$5@{=GfKD)tQaixy<}xZvK`ye=9qCJ2P_!(=4SrU4=*EnX2cwJ94%PoT9HI z*K$N`IkMiEZGlkb%^NIo(QeV;&Um|j$Y=HsWqXD*-RB@+^*=xfQ>Bb6c*tUu^DQdo6eB74g(7+2)D#y8Am1ce$jE{kNO{&awo5dC$D5r^4X(TMaR{&@o~M2 zDT;{2{@C62c@#y>xtd*K&90T`qndprpteI%q>PG;y1)+;Ysa&_r!#>w4-RD-M>Y%> z(Fm9K?O%H9msZ})I!}U=-->L)jV0SZl<6DJG@jcq5GPLx?q1Q|yJpF{M<4(wIcVgU z2Y$e>HfMJo$#e{?H)Px=Hw>r1#JfCVCs0YfGi~~~zCAsJqi0Iw6*S*$9c+=dNy25U{2=@-(l#daylN7@?D#5djgYn# z)<@YICyU!|$#hERz~i{44CPW)B_)xvPlmn+R!pzdpp;yY>aU>;(gqup^wJX~9+Zj6 z_C;B`l!}8Lg;p!|gh6S+epZ4lvf4#hlG3rs1KwIHQwh<%DP;rZ*~wa_NvOc)2a7By zHWz4s*)g}6{4B#+QKTq~tB5nDPYkzo9MA~d(*Cr~Dg6j7SHvw0pTOSy3vqk(1-R8K z?NI7r%0ADI_%f8?i(SO~oLDsnO_hYFV3AD)R$=R8ScRpMo@E#`DXlDvtB5THj$suC z9eER(lW}6iteBNy1hy#QxMh+Qip&Kqyj7VAV7RJNeI<=3&Q z*jCAS7SGrpytx+1dG|l^?*D@~3;c?!Q6=zHEv67BsRo(4NBC`ufkFv4-pL~qweW}J zVVTJ%qWva8jnd0F7sh^&qzd~-lI};4u1T1r42&OPoXr>Uiu=aI{mDE0pHK#C=;nOM zek;`|X69LDXP=MI&1I-yp*1R`Bn09I$Vg5%-Gv!?QO*GtGuf0(1uj_l&jd#2H4B%Z z6GSe`n1=%UAZXArsr54vewVOIC?=Is6VBaWr*L~k&)Dz32|aAqHMVi(Y9=_Fy%H8( zVHkaNoVNueKX10@%wGH+x3qn4DP!?|?C!)(i{|!R^Zt$I{r86R&ZeBROLTUv!is!Q zbRJx9$PJ8$10xR)=LY!f0H1H`dawK4?$yNFschS!wJ+m5(6T=7>dLvgMOXLgE00_U zpXs@l12Eb&c0D(7zV=LeU$&+H_Y&(zaz`(TM=xa$kBTj$s2h#)G{ay`ZSvsvxViOv zk@V%Ao79kV?#nv&&UV$@9NCC z_KL2(tHY06eOnsU(zeo@Z9V|L!j&7iAP!u}9vl&yM{wAijN{8^L`zHF;$2RNmNv3} zx5N0oJPSn`8N1uaSZ#l9A?@1Waw2QPosf2>2isy#0$q@IO5&&gWlFEI`>Dm<5``4p z)}x3;j-^ulbYrce5Gwme2>=H zEJ}dE!`<|b=f3BimisO2Y9!rk8Wi2^YUCHQ?nzv1f>zfc^JNVkxrV)B!``*}|I+e1 zExG+;;{LHr!`@88Wn6f&IKFlH>z9|$J+gGjd>Jf~uC$F!AZWUeQ~NKH$NElUntW@O z>9q&xmBe`AFW~vB#>V_DJAXfcCXX;m9_QLs{-=WYtc@N05 zkXKC}Gdq1mA)3juP)BGLXiI0}9h2Dv6QQ4|m*6Fjlvdln%&>>z$ZN-cLJji%>)c10 zp-qifYg$e|=kVA()}ifN>)Pb-co2Tr_v0uFX0E=Pd2Koqie%=ZV(41twQJ(lH=YyF z<|S2^wtnsWCWpsE$HUp5xLGjs%G7g;+PtdTg%XjD$AjdZkGq2Byug*WGl+8U=+Hl zYnwq8kBR}*i15RO%qy?`V6<3ag%~2eqb^@M_y;>lk5gs&>D zVkdp8;_M3r7u6%0xY~!?aJ=>+K5`V-_#k^!;s@z-MvTH~E2*D(18?L_%zRuq9oI&9 zD^zBv3--X;Vq1#=-ZoiOm(%^Y7eE$n&b(i zl%8lisiw9$#$y{M{Yo@!txbG0-!cHY0cnP-s3tWk^~6osC-JB7+`yo^jeDiY=|c^# zL7PkOmdT!CO{G{I!5k-gyUi8xULCAfa2)mk$HbyC$NQ9)pl0(psU5euvN9~=%1apx zuyUyEwGdQFjoWZk6}#|u$5gwzaoQ-hIh03x$Tr8f4rt;vW$kX8JfVO<6@5|(m!3)s zp!jbG#OYFs65=~-9KWmF#8y`RZsa+uK$f0ckJ6UGcLIL61gC`fF58wH^Q9aLDuH+F zIPIU?+>3h^uPv3t`<1?;tXj;bJsMIMW8KH=%AocJ>$bLj6jC2aSD&#aQ(NXscUfH( z@s9d?SpAS{;`S}|+s9!(-0sFn&e`TLW`Ur6d~91ZUin^(*}fgdY(IZs{K}{Epri^~ zbEO~Ka020rNu2nQTPxSi6;};-g*niS7U0Xrol}i%yrgd7=2ZT^R2M!=g{v;%9{Lg( z4o$&8Fc+GYKletQV&T%iARiSNlg|pMBr{nBF3@33+=Wv~wlnev-q`2X@F}W6Yu6zX89So z6S^KtInjW*=zQTCoNQvD=HrBqJ{*|~;e%U3JFt+l_VloW4CpToxuo1z>6=>eISFQP zk-2L$i@r`m_lo6grk)-YC*M9(?MZn*WmBP67i=%csg2`CQ9{mY?CF7IJF}RGq^f#u zlIVWK6AGVlXUDt6_5SLN>^z9Ta zOA|ckm823?UO!CPOf68(OHX-`B5;F{07|NMF0oEXO;J{qU2tHr7FbPC8Q&m3L zluTzM$q;5sxGWN; zD2>H2{+d1;fh3B=VbQ{13FpvANqctabnu*{xiB_-Su*1`X>?YeY4Z0e@Sz&kj%iF{ z+@!>5eiA4o4L;=-myC(fTm&DZ2!$ErC7CFQ58GhTNLqXb2bV_8Gc-blIK9&DHZ)u2 z^#1{+$!nZVp(nKOn4Sm*5ZtWVihXlmWFymfv^ttO`6`QjtrTnf;cVsz&tk8ZVlAs@GKa3R*kCEvymED|J2Mny z@h?9y*Fu>in!Q#`Lyt9PH0c90DU#_v z|AFQLbVYXOcW!?>kgMwy>pE9Eezf=fy<*+LyB8lDt?4(G`&OMF827JT$=BAu-TYSb ziYi;%me&1T&6y57HE_1Z<=xrp*1P9EvNbQ?%-VL}J^#4cU5G;y>fE{7cCofSTf6J- z$e)|*9~-Qh>fIX#+}Zc$>bu1Hu5A6Dv+FDjL@0s5t95sWpVx3&Q~LaO_~nM2yGL~QWa|SN zNAKz#Qd(US2QEFlk~wnu;q8oRVng%lV~sI=i6Hlg?mZdT-i)bxL(@}?-6y*Dt=TiK zgBjDI4b5T1|LVCXwg&lx@yg9y=TWiqXx4TNy+uKLH#E3k>sdatJpS#M(t7mq9zKb3 z&!3JzGBiQ=Ti6N^}l_?(2=kAagi{%Ypc*D`gJ8XC8(FA^li|2IS3#ZL8~b($_7($n{=Xkt%K!v^f8 z1z9CT7(EfP^rpkU0;(*K|ohI@!Cf# znJFFmDO6G>v{JcD4L~bPR$VwKjzkJsMPoQ7e;D+*a1|ixo{{}0l? BHqZb7 literal 0 HcmV?d00001 diff --git a/.claude/python_script/core/config.py b/.claude/python_script/core/config.py new file mode 100644 index 00000000..83f06528 --- /dev/null +++ b/.claude/python_script/core/config.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +""" +Configuration Management Module +Provides unified configuration management with gitignore integration. +""" + +import os +import yaml +import logging +from pathlib import Path +from typing import Dict, Any, Optional, List +from .gitignore_parser import get_all_gitignore_patterns + + +class Config: + """Singleton configuration manager with hierarchical loading.""" + + _instance = None + _initialized = False + + def __new__(cls, config_path: Optional[str] = None): + if cls._instance is None: + cls._instance = super(Config, cls).__new__(cls) + return cls._instance + + def __init__(self, config_path: Optional[str] = None): + if self._initialized: + return + + self.config_path = config_path + self.config = {} + self.logger = logging.getLogger(__name__) + + self._load_config() + self._add_gitignore_patterns() + self._apply_env_overrides() + self._validate_config() + + self._initialized = True + + def _load_config(self): + """Load configuration from file with fallback hierarchy.""" + config_paths = self._get_config_paths() + + for config_file in config_paths: + if config_file.exists(): + try: + with open(config_file, 'r', encoding='utf-8') as f: + loaded_config = yaml.safe_load(f) + if loaded_config: + self.config = self._merge_configs(self.config, loaded_config) + self.logger.info(f"Loaded config from {config_file}") + except Exception as e: + self.logger.warning(f"Failed to load config from {config_file}: {e}") + + # Apply default config if no config loaded + if not self.config: + self.config = self._get_default_config() + self.logger.info("Using default configuration") + + def _get_config_paths(self) -> List[Path]: + """Get ordered list of config file paths to check.""" + paths = [] + + # 1. Explicitly provided config path + if self.config_path: + paths.append(Path(self.config_path)) + + # 2. Current directory config.yaml + paths.append(Path('config.yaml')) + + # 3. Script directory config.yaml + script_dir = Path(__file__).parent.parent + paths.append(script_dir / 'config.yaml') + + # 4. Default config in script directory + paths.append(script_dir / 'default_config.yaml') + + return paths + + def _get_default_config(self) -> Dict[str, Any]: + """Get default configuration.""" + return { + 'token_limits': { + 'small_project': 500000, + 'medium_project': 2000000, + 'large_project': 10000000, + 'max_files': 1000 + }, + 'exclude_patterns': [ + "*/node_modules/*", + "*/.git/*", + "*/build/*", + "*/dist/*", + "*/.next/*", + "*/.nuxt/*", + "*/target/*", + "*/vendor/*", + "*/__pycache__/*", + "*.pyc", + "*.pyo", + "*.log", + "*.tmp", + "*.temp", + "*.history" + ], + 'file_extensions': { + 'code': ['.py', '.js', '.ts', '.tsx', '.jsx', '.java', '.cpp', '.c', '.h', '.rs', '.go', '.php', '.rb', '.sh', '.bash'], + 'docs': ['.md', '.txt', '.rst', '.adoc'], + 'config': ['.json', '.yaml', '.yml', '.toml', '.ini', '.env'], + 'web': ['.html', '.css', '.scss', '.sass', '.xml'] + }, + 'embedding': { + 'enabled': True, + 'model': 'all-MiniLM-L6-v2', + 'cache_dir': 'cache', + 'similarity_threshold': 0.3, + 'max_context_length': 512, + 'batch_size': 32 + }, + 'context_analysis': { + 'domain_keywords': { + 'auth': ['auth', 'login', 'user', 'password', 'jwt', 'token', 'session'], + 'database': ['db', 'database', 'sql', 'query', 'model', 'schema', 'migration'], + 'api': ['api', 'endpoint', 'route', 'controller', 'service', 'handler'], + 'frontend': ['ui', 'component', 'view', 'template', 'style', 'css'], + 'backend': ['server', 'service', 'logic', 'business', 'core'], + 'test': ['test', 'spec', 'unit', 'integration', 'mock'], + 'config': ['config', 'setting', 'environment', 'env'], + 'util': ['util', 'helper', 'common', 'shared', 'lib'] + }, + 'language_indicators': { + 'python': ['.py', 'python', 'pip', 'requirements.txt', 'setup.py'], + 'javascript': ['.js', '.ts', 'npm', 'package.json', 'node'], + 'java': ['.java', 'maven', 'gradle', 'pom.xml'], + 'go': ['.go', 'go.mod', 'go.sum'], + 'rust': ['.rs', 'cargo', 'Cargo.toml'] + } + }, + 'path_matching': { + 'weights': { + 'keyword_match': 0.4, + 'extension_match': 0.2, + 'directory_context': 0.2, + 'file_size_penalty': 0.1, + 'recency_bonus': 0.1 + }, + 'max_files_per_category': 20, + 'min_relevance_score': 0.1 + }, + 'output': { + 'pattern_format': '@{{{path}}}', + 'always_include': [ + 'CLAUDE.md', + '**/CLAUDE.md', + 'README.md', + 'docs/**/*.md' + ], + 'max_total_files': 50 + }, + 'performance': { + 'cache_enabled': True, + 'cache_ttl': 3600, + 'max_file_size': 10485760, + 'max_workers': 4 + }, + 'logging': { + 'level': 'INFO', + 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + } + } + + def _merge_configs(self, base: Dict, override: Dict) -> Dict: + """Recursively merge configuration dictionaries.""" + result = base.copy() + + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = self._merge_configs(result[key], value) + else: + result[key] = value + + return result + + def _add_gitignore_patterns(self): + """Add patterns from .gitignore files to exclude_patterns.""" + try: + # Find root directory (current working directory or script parent) + root_dir = Path.cwd() + + gitignore_patterns = get_all_gitignore_patterns(str(root_dir)) + + if gitignore_patterns: + # Ensure exclude_patterns exists + if 'exclude_patterns' not in self.config: + self.config['exclude_patterns'] = [] + + # Add gitignore patterns, avoiding duplicates + existing_patterns = set(self.config['exclude_patterns']) + new_patterns = [p for p in gitignore_patterns if p not in existing_patterns] + + self.config['exclude_patterns'].extend(new_patterns) + + self.logger.info(f"Added {len(new_patterns)} patterns from .gitignore files") + + except Exception as e: + self.logger.warning(f"Failed to load .gitignore patterns: {e}") + + def _apply_env_overrides(self): + """Apply environment variable overrides.""" + env_mappings = { + 'ANALYZER_CACHE_DIR': ('embedding', 'cache_dir'), + 'ANALYZER_LOG_LEVEL': ('logging', 'level'), + 'ANALYZER_MAX_FILES': ('token_limits', 'max_files'), + 'ANALYZER_EMBEDDING_MODEL': ('embedding', 'model') + } + + for env_var, config_path in env_mappings.items(): + env_value = os.getenv(env_var) + if env_value: + self._set_nested_value(config_path, env_value) + self.logger.info(f"Applied environment override: {env_var} = {env_value}") + + def _set_nested_value(self, path: tuple, value: str): + """Set a nested configuration value.""" + current = self.config + for key in path[:-1]: + if key not in current: + current[key] = {} + current = current[key] + + # Try to convert value to appropriate type + if isinstance(current.get(path[-1]), int): + try: + value = int(value) + except ValueError: + pass + elif isinstance(current.get(path[-1]), bool): + value = value.lower() in ('true', '1', 'yes', 'on') + + current[path[-1]] = value + + def _validate_config(self): + """Validate configuration values.""" + required_sections = ['exclude_patterns', 'file_extensions', 'token_limits'] + + for section in required_sections: + if section not in self.config: + self.logger.warning(f"Missing required config section: {section}") + + # Validate token limits + if 'token_limits' in self.config: + limits = self.config['token_limits'] + if limits.get('small_project', 0) >= limits.get('medium_project', 0): + self.logger.warning("Token limit configuration may be incorrect") + + def get(self, path: str, default: Any = None) -> Any: + """Get configuration value using dot notation.""" + keys = path.split('.') + current = self.config + + try: + for key in keys: + current = current[key] + return current + except (KeyError, TypeError): + return default + + def set(self, path: str, value: Any): + """Set configuration value using dot notation.""" + keys = path.split('.') + current = self.config + + for key in keys[:-1]: + if key not in current: + current[key] = {} + current = current[key] + + current[keys[-1]] = value + + def get_exclude_patterns(self) -> List[str]: + """Get all exclude patterns including gitignore patterns.""" + return self.config.get('exclude_patterns', []) + + def get_file_extensions(self) -> Dict[str, List[str]]: + """Get file extension mappings.""" + return self.config.get('file_extensions', {}) + + def is_embedding_enabled(self) -> bool: + """Check if embedding functionality is enabled.""" + return self.config.get('embedding', {}).get('enabled', False) + + def get_cache_dir(self) -> str: + """Get cache directory path.""" + return self.config.get('embedding', {}).get('cache_dir', 'cache') + + def to_dict(self) -> Dict[str, Any]: + """Return configuration as dictionary.""" + return self.config.copy() + + def reload(self, config_path: Optional[str] = None): + """Reload configuration from file.""" + self._initialized = False + if config_path: + self.config_path = config_path + self.__init__(self.config_path) + + +# Global configuration instance +_global_config = None + + +def get_config(config_path: Optional[str] = None) -> Config: + """Get global configuration instance.""" + global _global_config + if _global_config is None: + _global_config = Config(config_path) + return _global_config + + +if __name__ == "__main__": + # Test configuration loading + config = Config() + print("Configuration loaded successfully!") + print(f"Cache dir: {config.get_cache_dir()}") + print(f"Exclude patterns: {len(config.get_exclude_patterns())}") + print(f"Embedding enabled: {config.is_embedding_enabled()}") \ No newline at end of file diff --git a/.claude/python_script/core/context_analyzer.py b/.claude/python_script/core/context_analyzer.py new file mode 100644 index 00000000..bf3ca0d3 --- /dev/null +++ b/.claude/python_script/core/context_analyzer.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +""" +Context Analyzer Module for UltraThink Path-Aware Analyzer +Analyzes user prompts to extract relevant context and keywords. +""" + +import re +import logging +from typing import Dict, List, Set, Tuple, Optional +from dataclasses import dataclass +from collections import Counter +import string + +@dataclass +class AnalysisResult: + """Results of context analysis.""" + keywords: List[str] + domains: List[str] + languages: List[str] + file_patterns: List[str] + confidence_scores: Dict[str, float] + extracted_entities: Dict[str, List[str]] + +class ContextAnalyzer: + """Analyzes user prompts to understand context and intent.""" + + def __init__(self, config: Dict): + self.config = config + self.logger = logging.getLogger(__name__) + + # Load domain and language mappings from config + self.domain_keywords = config.get('context_analysis', {}).get('domain_keywords', {}) + self.language_indicators = config.get('context_analysis', {}).get('language_indicators', {}) + + # Common programming terms and patterns + self.technical_terms = self._build_technical_terms() + self.file_pattern_indicators = self._build_pattern_indicators() + + # Stop words to filter out + self.stop_words = { + 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', + 'by', 'from', 'up', 'about', 'into', 'through', 'during', 'before', 'after', + 'above', 'below', 'between', 'among', 'as', 'is', 'are', 'was', 'were', 'be', + 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', + 'could', 'should', 'may', 'might', 'must', 'can', 'this', 'that', 'these', + 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', + 'us', 'them', 'my', 'your', 'his', 'its', 'our', 'their' + } + + def _build_technical_terms(self) -> Dict[str, List[str]]: + """Build comprehensive list of technical terms grouped by category.""" + return { + 'authentication': [ + 'auth', 'authentication', 'login', 'logout', 'signin', 'signout', + 'user', 'password', 'token', 'jwt', 'oauth', 'session', 'cookie', + 'credential', 'authorize', 'permission', 'role', 'access' + ], + 'database': [ + 'database', 'db', 'sql', 'query', 'table', 'schema', 'migration', + 'model', 'orm', 'entity', 'relation', 'index', 'transaction', + 'crud', 'select', 'insert', 'update', 'delete', 'join' + ], + 'api': [ + 'api', 'rest', 'graphql', 'endpoint', 'route', 'controller', + 'handler', 'middleware', 'service', 'request', 'response', + 'http', 'get', 'post', 'put', 'delete', 'patch' + ], + 'frontend': [ + 'ui', 'component', 'view', 'template', 'page', 'layout', + 'style', 'css', 'html', 'javascript', 'react', 'vue', + 'angular', 'dom', 'event', 'state', 'props' + ], + 'backend': [ + 'server', 'service', 'business', 'logic', 'core', 'engine', + 'worker', 'job', 'queue', 'cache', 'redis', 'memcache' + ], + 'testing': [ + 'test', 'testing', 'spec', 'unit', 'integration', 'e2e', + 'mock', 'stub', 'fixture', 'assert', 'expect', 'should' + ], + 'configuration': [ + 'config', 'configuration', 'setting', 'environment', 'env', + 'variable', 'constant', 'parameter', 'option' + ], + 'utility': [ + 'util', 'utility', 'helper', 'common', 'shared', 'lib', + 'library', 'tool', 'function', 'method' + ] + } + + def _build_pattern_indicators(self) -> Dict[str, List[str]]: + """Build indicators that suggest specific file patterns.""" + return { + 'source_code': ['implement', 'code', 'function', 'class', 'method'], + 'tests': ['test', 'testing', 'spec', 'unittest', 'pytest'], + 'documentation': ['doc', 'readme', 'guide', 'documentation', 'manual'], + 'configuration': ['config', 'setting', 'env', 'environment'], + 'build': ['build', 'compile', 'package', 'deploy', 'release'], + 'scripts': ['script', 'automation', 'tool', 'utility'] + } + + def extract_keywords(self, text: str) -> List[str]: + """Extract meaningful keywords from text.""" + # Clean and normalize text + text = text.lower() + text = re.sub(r'[^\w\s-]', ' ', text) # Remove punctuation except hyphens + words = text.split() + + # Filter stop words and short words + keywords = [] + for word in words: + word = word.strip('-') # Remove leading/trailing hyphens + if (len(word) >= 2 and + word not in self.stop_words and + not word.isdigit()): + keywords.append(word) + + # Count frequency and return top keywords + word_counts = Counter(keywords) + return [word for word, count in word_counts.most_common(20)] + + def identify_domains(self, keywords: List[str]) -> List[Tuple[str, float]]: + """Identify relevant domains based on keywords.""" + domain_scores = {} + + for domain, domain_keywords in self.domain_keywords.items(): + score = 0.0 + matched_keywords = [] + + for keyword in keywords: + for domain_keyword in domain_keywords: + if keyword in domain_keyword or domain_keyword in keyword: + score += 1.0 + matched_keywords.append(keyword) + break + + if score > 0: + # Normalize score by number of domain keywords + normalized_score = score / len(domain_keywords) + domain_scores[domain] = normalized_score + + # Also check technical terms + for category, terms in self.technical_terms.items(): + score = 0.0 + for keyword in keywords: + for term in terms: + if keyword in term or term in keyword: + score += 1.0 + break + + if score > 0: + normalized_score = score / len(terms) + if category not in domain_scores: + domain_scores[category] = normalized_score + else: + domain_scores[category] = max(domain_scores[category], normalized_score) + + # Sort by score and return top domains + sorted_domains = sorted(domain_scores.items(), key=lambda x: x[1], reverse=True) + return sorted_domains[:5] + + def identify_languages(self, keywords: List[str]) -> List[Tuple[str, float]]: + """Identify programming languages based on keywords.""" + language_scores = {} + + for language, indicators in self.language_indicators.items(): + score = 0.0 + for keyword in keywords: + for indicator in indicators: + if keyword in indicator or indicator in keyword: + score += 1.0 + break + + if score > 0: + normalized_score = score / len(indicators) + language_scores[language] = normalized_score + + sorted_languages = sorted(language_scores.items(), key=lambda x: x[1], reverse=True) + return sorted_languages[:3] + + def extract_file_patterns(self, text: str) -> List[str]: + """Extract explicit file patterns from text.""" + patterns = [] + + # Look for @{pattern} syntax + at_patterns = re.findall(r'@\{([^}]+)\}', text) + patterns.extend(at_patterns) + + # Look for file extensions + extensions = re.findall(r'\*\.(\w+)', text) + for ext in extensions: + patterns.append(f"*.{ext}") + + # Look for directory patterns + dir_patterns = re.findall(r'(\w+)/\*\*?', text) + for dir_pattern in dir_patterns: + patterns.append(f"{dir_pattern}/**/*") + + # Look for specific file names + file_patterns = re.findall(r'\b(\w+\.\w+)\b', text) + for file_pattern in file_patterns: + if '.' in file_pattern: + patterns.append(file_pattern) + + return list(set(patterns)) # Remove duplicates + + def suggest_patterns_from_domains(self, domains: List[str]) -> List[str]: + """Suggest file patterns based on identified domains.""" + patterns = [] + + domain_to_patterns = { + 'auth': ['**/auth/**/*', '**/login/**/*', '**/user/**/*'], + 'authentication': ['**/auth/**/*', '**/login/**/*', '**/user/**/*'], + 'database': ['**/db/**/*', '**/model/**/*', '**/migration/**/*', '**/*model*'], + 'api': ['**/api/**/*', '**/route/**/*', '**/controller/**/*', '**/handler/**/*'], + 'frontend': ['**/ui/**/*', '**/component/**/*', '**/view/**/*', '**/template/**/*'], + 'backend': ['**/service/**/*', '**/core/**/*', '**/server/**/*'], + 'test': ['**/test/**/*', '**/spec/**/*', '**/*test*', '**/*spec*'], + 'testing': ['**/test/**/*', '**/spec/**/*', '**/*test*', '**/*spec*'], + 'config': ['**/config/**/*', '**/*.config.*', '**/env/**/*'], + 'configuration': ['**/config/**/*', '**/*.config.*', '**/env/**/*'], + 'util': ['**/util/**/*', '**/helper/**/*', '**/common/**/*'], + 'utility': ['**/util/**/*', '**/helper/**/*', '**/common/**/*'] + } + + for domain in domains: + if domain in domain_to_patterns: + patterns.extend(domain_to_patterns[domain]) + + return list(set(patterns)) # Remove duplicates + + def extract_entities(self, text: str) -> Dict[str, List[str]]: + """Extract named entities from text.""" + entities = { + 'files': [], + 'functions': [], + 'classes': [], + 'variables': [], + 'technologies': [] + } + + # File patterns + file_patterns = re.findall(r'\b(\w+\.\w+)\b', text) + entities['files'] = list(set(file_patterns)) + + # Function patterns (camelCase or snake_case followed by parentheses) + function_patterns = re.findall(r'\b([a-z][a-zA-Z0-9_]*)\s*\(', text) + entities['functions'] = list(set(function_patterns)) + + # Class patterns (PascalCase) + class_patterns = re.findall(r'\b([A-Z][a-zA-Z0-9]*)\b', text) + entities['classes'] = list(set(class_patterns)) + + # Technology mentions + tech_keywords = [ + 'react', 'vue', 'angular', 'node', 'express', 'django', 'flask', + 'spring', 'rails', 'laravel', 'docker', 'kubernetes', 'aws', + 'azure', 'gcp', 'postgresql', 'mysql', 'mongodb', 'redis' + ] + text_lower = text.lower() + for tech in tech_keywords: + if tech in text_lower: + entities['technologies'].append(tech) + + return entities + + def analyze(self, prompt: str) -> AnalysisResult: + """Perform comprehensive analysis of the user prompt.""" + self.logger.debug(f"Analyzing prompt: {prompt[:100]}...") + + # Extract keywords + keywords = self.extract_keywords(prompt) + + # Identify domains and languages + domains_with_scores = self.identify_domains(keywords) + languages_with_scores = self.identify_languages(keywords) + + # Extract patterns and entities + explicit_patterns = self.extract_file_patterns(prompt) + entities = self.extract_entities(prompt) + + # Get top domains and languages + domains = [domain for domain, score in domains_with_scores] + languages = [lang for lang, score in languages_with_scores] + + # Suggest additional patterns based on domains + suggested_patterns = self.suggest_patterns_from_domains(domains) + + # Combine explicit and suggested patterns + all_patterns = list(set(explicit_patterns + suggested_patterns)) + + # Build confidence scores + confidence_scores = { + 'keywords': len(keywords) / 20, # Normalize to 0-1 + 'domain_match': max([score for _, score in domains_with_scores[:1]], default=0), + 'language_match': max([score for _, score in languages_with_scores[:1]], default=0), + 'pattern_extraction': len(explicit_patterns) / 5, # Normalize to 0-1 + } + + result = AnalysisResult( + keywords=keywords, + domains=domains, + languages=languages, + file_patterns=all_patterns, + confidence_scores=confidence_scores, + extracted_entities=entities + ) + + self.logger.info(f"Analysis complete: {len(domains)} domains, {len(languages)} languages, {len(all_patterns)} patterns") + return result + +def main(): + """Command-line interface for context analyzer.""" + import yaml + import argparse + import json + + parser = argparse.ArgumentParser(description="Context Analyzer for UltraThink") + parser.add_argument("prompt", help="Prompt to analyze") + parser.add_argument("--config", default="config.yaml", help="Configuration file path") + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + + args = parser.parse_args() + + # Setup logging + level = logging.DEBUG if args.verbose else logging.INFO + logging.basicConfig(level=level, format='%(levelname)s: %(message)s') + + # Load configuration + from pathlib import Path + config_path = Path(__file__).parent / args.config + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + # Create analyzer + analyzer = ContextAnalyzer(config) + + # Analyze prompt + result = analyzer.analyze(args.prompt) + + # Output results + print(f"Keywords: {', '.join(result.keywords[:10])}") + print(f"Domains: {', '.join(result.domains[:5])}") + print(f"Languages: {', '.join(result.languages[:3])}") + print(f"Patterns: {', '.join(result.file_patterns[:10])}") + + if args.verbose: + print("\nDetailed Results:") + print(json.dumps({ + 'keywords': result.keywords, + 'domains': result.domains, + 'languages': result.languages, + 'file_patterns': result.file_patterns, + 'confidence_scores': result.confidence_scores, + 'extracted_entities': result.extracted_entities + }, indent=2)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/core/embedding_manager.py b/.claude/python_script/core/embedding_manager.py new file mode 100644 index 00000000..217ddead --- /dev/null +++ b/.claude/python_script/core/embedding_manager.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 +""" +Embedding Manager Module for UltraThink Path-Aware Analyzer +Manages embeddings for semantic similarity search (RAG functionality). +""" + +import os +import json +import hashlib +import logging +import pickle +from pathlib import Path +from typing import Dict, List, Tuple, Optional, Any +from dataclasses import dataclass +import time + +# Optional imports for embedding functionality +try: + import numpy as np + NUMPY_AVAILABLE = True +except ImportError: + NUMPY_AVAILABLE = False + +try: + from sentence_transformers import SentenceTransformer + SENTENCE_TRANSFORMERS_AVAILABLE = True +except ImportError: + SENTENCE_TRANSFORMERS_AVAILABLE = False + +from .file_indexer import FileInfo + +@dataclass +class EmbeddingInfo: + """Information about a file's embedding.""" + file_path: str + content_hash: str + embedding_hash: str + created_time: float + vector_size: int + +@dataclass +class SimilarityResult: + """Result of similarity search.""" + file_info: FileInfo + similarity_score: float + matching_content: str + +class EmbeddingManager: + """Manages embeddings for semantic file matching.""" + + def __init__(self, config: Dict): + self.config = config + self.logger = logging.getLogger(__name__) + + # Check if embeddings are enabled + self.enabled = config.get('embedding', {}).get('enabled', False) + if not self.enabled: + self.logger.info("Embeddings disabled in configuration") + return + + # Check dependencies + if not NUMPY_AVAILABLE: + self.logger.warning("NumPy not available, disabling embeddings") + self.enabled = False + return + + if not SENTENCE_TRANSFORMERS_AVAILABLE: + self.logger.warning("sentence-transformers not available, disabling embeddings") + self.enabled = False + return + + # Load configuration + self.model_name = config.get('embedding', {}).get('model', 'all-MiniLM-L6-v2') + self.cache_dir = Path(config.get('embedding', {}).get('cache_dir', '.claude/cache/embeddings')) + self.similarity_threshold = config.get('embedding', {}).get('similarity_threshold', 0.6) + self.max_context_length = config.get('embedding', {}).get('max_context_length', 512) + self.batch_size = config.get('embedding', {}).get('batch_size', 32) + + # Setup cache directories + self.cache_dir.mkdir(parents=True, exist_ok=True) + self.embeddings_file = self.cache_dir / "embeddings.pkl" + self.index_file = self.cache_dir / "embedding_index.json" + + # Initialize model lazily + self._model = None + self._embeddings_cache = None + self._embedding_index = None + + @property + def model(self): + """Lazy load the embedding model.""" + if not self.enabled: + return None + + if self._model is None: + try: + self.logger.info(f"Loading embedding model: {self.model_name}") + self._model = SentenceTransformer(self.model_name) + self.logger.info(f"Model loaded successfully") + except Exception as e: + self.logger.error(f"Failed to load embedding model: {e}") + self.enabled = False + return None + + return self._model + + def embeddings_exist(self) -> bool: + """Check if embeddings cache exists.""" + return self.embeddings_file.exists() and self.index_file.exists() + + def _load_embedding_cache(self) -> Dict[str, np.ndarray]: + """Load embeddings from cache.""" + if self._embeddings_cache is not None: + return self._embeddings_cache + + if not self.embeddings_file.exists(): + self._embeddings_cache = {} + return self._embeddings_cache + + try: + with open(self.embeddings_file, 'rb') as f: + self._embeddings_cache = pickle.load(f) + self.logger.debug(f"Loaded {len(self._embeddings_cache)} embeddings from cache") + except Exception as e: + self.logger.warning(f"Failed to load embeddings cache: {e}") + self._embeddings_cache = {} + + return self._embeddings_cache + + def _save_embedding_cache(self): + """Save embeddings to cache.""" + if self._embeddings_cache is None: + return + + try: + with open(self.embeddings_file, 'wb') as f: + pickle.dump(self._embeddings_cache, f) + self.logger.debug(f"Saved {len(self._embeddings_cache)} embeddings to cache") + except Exception as e: + self.logger.error(f"Failed to save embeddings cache: {e}") + + def _load_embedding_index(self) -> Dict[str, EmbeddingInfo]: + """Load embedding index.""" + if self._embedding_index is not None: + return self._embedding_index + + if not self.index_file.exists(): + self._embedding_index = {} + return self._embedding_index + + try: + with open(self.index_file, 'r', encoding='utf-8') as f: + data = json.load(f) + self._embedding_index = {} + for path, info_dict in data.items(): + self._embedding_index[path] = EmbeddingInfo(**info_dict) + self.logger.debug(f"Loaded embedding index with {len(self._embedding_index)} entries") + except Exception as e: + self.logger.warning(f"Failed to load embedding index: {e}") + self._embedding_index = {} + + return self._embedding_index + + def _save_embedding_index(self): + """Save embedding index.""" + if self._embedding_index is None: + return + + try: + data = {} + for path, info in self._embedding_index.items(): + data[path] = { + 'file_path': info.file_path, + 'content_hash': info.content_hash, + 'embedding_hash': info.embedding_hash, + 'created_time': info.created_time, + 'vector_size': info.vector_size + } + + with open(self.index_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2) + self.logger.debug(f"Saved embedding index with {len(self._embedding_index)} entries") + except Exception as e: + self.logger.error(f"Failed to save embedding index: {e}") + + def _extract_text_content(self, file_info: FileInfo) -> Optional[str]: + """Extract text content from a file for embedding.""" + try: + file_path = Path(file_info.path) + + # Skip binary files and very large files + if file_info.size > self.config.get('performance', {}).get('max_file_size', 10485760): + return None + + # Only process text-based files + text_extensions = {'.py', '.js', '.ts', '.tsx', '.jsx', '.java', '.cpp', '.c', '.h', + '.rs', '.go', '.php', '.rb', '.sh', '.bash', '.md', '.txt', '.json', + '.yaml', '.yml', '.xml', '.html', '.css', '.scss', '.sass'} + + if file_info.extension.lower() not in text_extensions: + return None + + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + # Truncate content if too long + if len(content) > self.max_context_length * 4: # Approximate token limit + content = content[:self.max_context_length * 4] + + return content + + except Exception as e: + self.logger.debug(f"Could not extract content from {file_info.path}: {e}") + return None + + def _create_embedding(self, text: str) -> Optional[np.ndarray]: + """Create embedding for text content.""" + if not self.enabled or self.model is None: + return None + + try: + # Truncate text if needed + if len(text) > self.max_context_length * 4: + text = text[:self.max_context_length * 4] + + embedding = self.model.encode([text])[0] + return embedding + + except Exception as e: + self.logger.warning(f"Failed to create embedding: {e}") + return None + + def _get_content_hash(self, content: str) -> str: + """Get hash of content for caching.""" + return hashlib.md5(content.encode('utf-8')).hexdigest() + + def _get_embedding_hash(self, embedding: np.ndarray) -> str: + """Get hash of embedding for verification.""" + return hashlib.md5(embedding.tobytes()).hexdigest() + + def update_embeddings(self, file_index: Dict[str, FileInfo], force_rebuild: bool = False) -> int: + """Update embeddings for files in the index.""" + if not self.enabled: + self.logger.info("Embeddings disabled, skipping update") + return 0 + + self.logger.info("Updating embeddings...") + + # Load caches + embeddings_cache = self._load_embedding_cache() + embedding_index = self._load_embedding_index() + + new_embeddings = 0 + batch_texts = [] + batch_paths = [] + + for file_path, file_info in file_index.items(): + # Check if embedding exists and is current + if not force_rebuild and file_path in embedding_index: + cached_info = embedding_index[file_path] + if cached_info.content_hash == file_info.content_hash: + continue # Embedding is current + + # Extract content + content = self._extract_text_content(file_info) + if content is None: + continue + + # Prepare for batch processing + batch_texts.append(content) + batch_paths.append(file_path) + + # Process batch when full + if len(batch_texts) >= self.batch_size: + self._process_batch(batch_texts, batch_paths, file_index, embeddings_cache, embedding_index) + new_embeddings += len(batch_texts) + batch_texts = [] + batch_paths = [] + + # Process remaining batch + if batch_texts: + self._process_batch(batch_texts, batch_paths, file_index, embeddings_cache, embedding_index) + new_embeddings += len(batch_texts) + + # Save caches + self._save_embedding_cache() + self._save_embedding_index() + + self.logger.info(f"Updated {new_embeddings} embeddings") + return new_embeddings + + def _process_batch(self, texts: List[str], paths: List[str], file_index: Dict[str, FileInfo], + embeddings_cache: Dict[str, np.ndarray], embedding_index: Dict[str, EmbeddingInfo]): + """Process a batch of texts for embedding.""" + try: + # Create embeddings for batch + embeddings = self.model.encode(texts) + + for i, (text, path) in enumerate(zip(texts, paths)): + embedding = embeddings[i] + file_info = file_index[path] + + # Store embedding + content_hash = self._get_content_hash(text) + embedding_hash = self._get_embedding_hash(embedding) + + embeddings_cache[path] = embedding + embedding_index[path] = EmbeddingInfo( + file_path=path, + content_hash=content_hash, + embedding_hash=embedding_hash, + created_time=time.time(), + vector_size=len(embedding) + ) + + except Exception as e: + self.logger.error(f"Failed to process embedding batch: {e}") + + def find_similar_files(self, query: str, file_index: Dict[str, FileInfo], + top_k: int = 20) -> List[SimilarityResult]: + """Find files similar to the query using embeddings.""" + if not self.enabled: + return [] + + # Create query embedding + query_embedding = self._create_embedding(query) + if query_embedding is None: + return [] + + # Load embeddings + embeddings_cache = self._load_embedding_cache() + if not embeddings_cache: + self.logger.warning("No embeddings available for similarity search") + return [] + + # Calculate similarities + similarities = [] + for file_path, file_embedding in embeddings_cache.items(): + if file_path not in file_index: + continue + + try: + # Calculate cosine similarity + similarity = np.dot(query_embedding, file_embedding) / ( + np.linalg.norm(query_embedding) * np.linalg.norm(file_embedding) + ) + + if similarity >= self.similarity_threshold: + similarities.append((file_path, similarity)) + + except Exception as e: + self.logger.debug(f"Failed to calculate similarity for {file_path}: {e}") + continue + + # Sort by similarity + similarities.sort(key=lambda x: x[1], reverse=True) + + # Create results + results = [] + for file_path, similarity in similarities[:top_k]: + file_info = file_index[file_path] + + # Extract a snippet of matching content + content = self._extract_text_content(file_info) + snippet = content[:200] + "..." if content and len(content) > 200 else content or "" + + result = SimilarityResult( + file_info=file_info, + similarity_score=similarity, + matching_content=snippet + ) + results.append(result) + + self.logger.info(f"Found {len(results)} similar files for query") + return results + + def get_stats(self) -> Dict[str, Any]: + """Get statistics about the embedding cache.""" + if not self.enabled: + return {'enabled': False} + + embedding_index = self._load_embedding_index() + embeddings_cache = self._load_embedding_cache() + + return { + 'enabled': True, + 'model_name': self.model_name, + 'total_embeddings': len(embedding_index), + 'cache_size_mb': os.path.getsize(self.embeddings_file) / 1024 / 1024 if self.embeddings_file.exists() else 0, + 'similarity_threshold': self.similarity_threshold, + 'vector_size': list(embedding_index.values())[0].vector_size if embedding_index else 0 + } + +def main(): + """Command-line interface for embedding manager.""" + import yaml + import argparse + from .file_indexer import FileIndexer + + parser = argparse.ArgumentParser(description="Embedding Manager for UltraThink") + parser.add_argument("--config", default="config.yaml", help="Configuration file path") + parser.add_argument("--update", action="store_true", help="Update embeddings") + parser.add_argument("--rebuild", action="store_true", help="Force rebuild all embeddings") + parser.add_argument("--query", help="Search for similar files") + parser.add_argument("--stats", action="store_true", help="Show embedding statistics") + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + + args = parser.parse_args() + + # Setup logging + level = logging.DEBUG if args.verbose else logging.INFO + logging.basicConfig(level=level, format='%(levelname)s: %(message)s') + + # Load configuration + config_path = Path(__file__).parent / args.config + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + # Create components + indexer = FileIndexer(config) + embedding_manager = EmbeddingManager(config) + + if not embedding_manager.enabled: + print("Embeddings are disabled. Enable in config.yaml or install required dependencies.") + return + + # Load file index + file_index = indexer.load_index() + if not file_index: + print("Building file index...") + file_index = indexer.build_index() + + if args.stats: + stats = embedding_manager.get_stats() + print("Embedding Statistics:") + for key, value in stats.items(): + print(f" {key}: {value}") + return + + if args.update or args.rebuild: + count = embedding_manager.update_embeddings(file_index, force_rebuild=args.rebuild) + print(f"Updated {count} embeddings") + + if args.query: + results = embedding_manager.find_similar_files(args.query, file_index) + print(f"Found {len(results)} similar files:") + for result in results: + print(f" {result.file_info.relative_path} (similarity: {result.similarity_score:.3f})") + if args.verbose and result.matching_content: + print(f" Content: {result.matching_content[:100]}...") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/core/file_indexer.py b/.claude/python_script/core/file_indexer.py new file mode 100644 index 00000000..83dcd290 --- /dev/null +++ b/.claude/python_script/core/file_indexer.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +""" +File Indexer Module for UltraThink Path-Aware Analyzer +Builds and maintains an index of repository files with metadata. +Enhanced with gitignore support and unified configuration. +""" + +import os +import hashlib +import json +import time +import logging +from pathlib import Path +from typing import Dict, List, Optional, Set, Tuple, Union +from dataclasses import dataclass, asdict +from datetime import datetime +import fnmatch + +from .gitignore_parser import GitignoreParser + +@dataclass +class FileInfo: + """Information about a single file in the repository.""" + path: str + relative_path: str + size: int + modified_time: float + extension: str + category: str # code, docs, config, web + estimated_tokens: int + content_hash: str + + def to_dict(self) -> Dict: + return asdict(self) + + @classmethod + def from_dict(cls, data: Dict) -> 'FileInfo': + return cls(**data) + +@dataclass +class IndexStats: + """Statistics about the file index.""" + total_files: int + total_tokens: int + total_size: int + categories: Dict[str, int] + last_updated: float + + def to_dict(self) -> Dict: + return asdict(self) + +class FileIndexer: + """Builds and maintains an efficient index of repository files.""" + + def __init__(self, config: Union['Config', Dict], root_path: str = "."): + # Support both Config object and Dict for backward compatibility + if hasattr(config, 'to_dict'): + self.config_obj = config + self.config = config.to_dict() + else: + self.config_obj = None + self.config = config + + self.root_path = Path(root_path).resolve() + self.cache_dir = Path(self.config.get('embedding', {}).get('cache_dir', '.claude/cache')) + self.cache_dir.mkdir(parents=True, exist_ok=True) + self.index_file = self.cache_dir / "file_index.json" + + # Setup logging + self.logger = logging.getLogger(__name__) + + # File extension mappings + self.extension_categories = self._build_extension_map() + + # Exclude patterns from config + self.exclude_patterns = list(self.config.get('exclude_patterns', [])) + + # Initialize gitignore parser and add patterns + self.gitignore_parser = GitignoreParser(str(self.root_path)) + self._load_gitignore_patterns() + + # Performance settings + self.max_file_size = self.config.get('performance', {}).get('max_file_size', 10485760) + + def _build_extension_map(self) -> Dict[str, str]: + """Build mapping from file extensions to categories.""" + ext_map = {} + for category, extensions in self.config.get('file_extensions', {}).items(): + for ext in extensions: + ext_map[ext.lower()] = category + return ext_map + + def _load_gitignore_patterns(self): + """Load patterns from .gitignore files and add to exclude_patterns.""" + try: + gitignore_patterns = self.gitignore_parser.parse_all_gitignores() + + if gitignore_patterns: + # Avoid duplicates + existing_patterns = set(self.exclude_patterns) + new_patterns = [p for p in gitignore_patterns if p not in existing_patterns] + + self.exclude_patterns.extend(new_patterns) + self.logger.info(f"Added {len(new_patterns)} patterns from .gitignore files") + + except Exception as e: + self.logger.warning(f"Failed to load .gitignore patterns: {e}") + + def _should_exclude_file(self, file_path: Path) -> bool: + """Check if file should be excluded based on patterns and gitignore rules.""" + relative_path = str(file_path.relative_to(self.root_path)) + + # Check against exclude patterns from config + for pattern in self.exclude_patterns: + # Convert pattern to work with fnmatch + if fnmatch.fnmatch(relative_path, pattern) or fnmatch.fnmatch(str(file_path), pattern): + return True + + # Check if any parent directory matches + parts = relative_path.split(os.sep) + for i in range(len(parts)): + partial_path = "/".join(parts[:i+1]) + if fnmatch.fnmatch(partial_path, pattern): + return True + + # Also check gitignore rules using dedicated parser + # Note: gitignore patterns are already included in self.exclude_patterns + # but we can add additional gitignore-specific checking here if needed + try: + # The gitignore patterns are already loaded into exclude_patterns, + # but we can do additional gitignore-specific checks if needed + pass + except Exception as e: + self.logger.debug(f"Error in gitignore checking for {file_path}: {e}") + + return False + + def _estimate_tokens(self, file_path: Path) -> int: + """Estimate token count for a file (chars/4 approximation).""" + try: + if file_path.stat().st_size > self.max_file_size: + return file_path.stat().st_size // 8 # Penalty for large files + + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + return len(content) // 4 # Rough approximation + except (UnicodeDecodeError, OSError): + # Binary files or unreadable files + return file_path.stat().st_size // 8 + + def _get_file_hash(self, file_path: Path) -> str: + """Get a hash of file metadata for change detection.""" + stat = file_path.stat() + return hashlib.md5(f"{file_path}:{stat.st_size}:{stat.st_mtime}".encode()).hexdigest() + + def _categorize_file(self, file_path: Path) -> str: + """Categorize file based on extension.""" + extension = file_path.suffix.lower() + return self.extension_categories.get(extension, 'other') + + def _scan_file(self, file_path: Path) -> Optional[FileInfo]: + """Scan a single file and create FileInfo.""" + try: + if not file_path.is_file() or self._should_exclude_file(file_path): + return None + + stat = file_path.stat() + relative_path = str(file_path.relative_to(self.root_path)) + + file_info = FileInfo( + path=str(file_path), + relative_path=relative_path, + size=stat.st_size, + modified_time=stat.st_mtime, + extension=file_path.suffix.lower(), + category=self._categorize_file(file_path), + estimated_tokens=self._estimate_tokens(file_path), + content_hash=self._get_file_hash(file_path) + ) + + return file_info + + except (OSError, PermissionError) as e: + self.logger.warning(f"Could not scan file {file_path}: {e}") + return None + + def build_index(self, force_rebuild: bool = False) -> Dict[str, FileInfo]: + """Build or update the file index.""" + self.logger.info(f"Building file index for {self.root_path}") + + # Load existing index if available + existing_index = {} + if not force_rebuild and self.index_file.exists(): + existing_index = self.load_index() + + new_index = {} + changed_files = 0 + + # Walk through all files + for file_path in self.root_path.rglob('*'): + if not file_path.is_file(): + continue + + file_info = self._scan_file(file_path) + if file_info is None: + continue + + # Check if file has changed + relative_path = file_info.relative_path + if relative_path in existing_index: + old_info = existing_index[relative_path] + if old_info.content_hash == file_info.content_hash: + # File unchanged, keep old info + new_index[relative_path] = old_info + continue + + # File is new or changed + new_index[relative_path] = file_info + changed_files += 1 + + self.logger.info(f"Indexed {len(new_index)} files ({changed_files} new/changed)") + + # Save index + self.save_index(new_index) + + return new_index + + def load_index(self) -> Dict[str, FileInfo]: + """Load file index from cache.""" + if not self.index_file.exists(): + return {} + + try: + with open(self.index_file, 'r', encoding='utf-8') as f: + data = json.load(f) + index = {} + for path, info_dict in data.get('files', {}).items(): + index[path] = FileInfo.from_dict(info_dict) + return index + except (json.JSONDecodeError, KeyError) as e: + self.logger.warning(f"Could not load index: {e}") + return {} + + def save_index(self, index: Dict[str, FileInfo]) -> None: + """Save file index to cache.""" + try: + # Calculate stats + stats = self._calculate_stats(index) + + data = { + 'stats': stats.to_dict(), + 'files': {path: info.to_dict() for path, info in index.items()} + } + + with open(self.index_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2) + + except OSError as e: + self.logger.error(f"Could not save index: {e}") + + def _calculate_stats(self, index: Dict[str, FileInfo]) -> IndexStats: + """Calculate statistics for the index.""" + total_files = len(index) + total_tokens = sum(info.estimated_tokens for info in index.values()) + total_size = sum(info.size for info in index.values()) + + categories = {} + for info in index.values(): + categories[info.category] = categories.get(info.category, 0) + 1 + + return IndexStats( + total_files=total_files, + total_tokens=total_tokens, + total_size=total_size, + categories=categories, + last_updated=time.time() + ) + + def get_stats(self) -> Optional[IndexStats]: + """Get statistics about the current index.""" + if not self.index_file.exists(): + return None + + try: + with open(self.index_file, 'r', encoding='utf-8') as f: + data = json.load(f) + return IndexStats(**data.get('stats', {})) + except (json.JSONDecodeError, KeyError): + return None + + def find_files_by_pattern(self, pattern: str, index: Optional[Dict[str, FileInfo]] = None) -> List[FileInfo]: + """Find files matching a glob pattern.""" + if index is None: + index = self.load_index() + + matching_files = [] + for path, info in index.items(): + if fnmatch.fnmatch(path, pattern) or fnmatch.fnmatch(info.path, pattern): + matching_files.append(info) + + return matching_files + + def find_files_by_category(self, category: str, index: Optional[Dict[str, FileInfo]] = None) -> List[FileInfo]: + """Find files by category (code, docs, config, etc.).""" + if index is None: + index = self.load_index() + + return [info for info in index.values() if info.category == category] + + def find_files_by_keywords(self, keywords: List[str], index: Optional[Dict[str, FileInfo]] = None) -> List[FileInfo]: + """Find files whose paths contain any of the specified keywords.""" + if index is None: + index = self.load_index() + + matching_files = [] + keywords_lower = [kw.lower() for kw in keywords] + + for info in index.values(): + path_lower = info.relative_path.lower() + if any(keyword in path_lower for keyword in keywords_lower): + matching_files.append(info) + + return matching_files + + def get_recent_files(self, limit: int = 20, index: Optional[Dict[str, FileInfo]] = None) -> List[FileInfo]: + """Get most recently modified files.""" + if index is None: + index = self.load_index() + + files = list(index.values()) + files.sort(key=lambda f: f.modified_time, reverse=True) + return files[:limit] + +def main(): + """Command-line interface for file indexer.""" + import yaml + import argparse + + parser = argparse.ArgumentParser(description="File Indexer for UltraThink") + parser.add_argument("--config", default="config.yaml", help="Configuration file path") + parser.add_argument("--rebuild", action="store_true", help="Force rebuild index") + parser.add_argument("--stats", action="store_true", help="Show index statistics") + parser.add_argument("--pattern", help="Find files matching pattern") + + args = parser.parse_args() + + # Load configuration + config_path = Path(__file__).parent / args.config + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + # Setup logging + logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') + + # Create indexer + indexer = FileIndexer(config) + + if args.stats: + stats = indexer.get_stats() + if stats: + print(f"Total files: {stats.total_files}") + print(f"Total tokens: {stats.total_tokens:,}") + print(f"Total size: {stats.total_size:,} bytes") + print(f"Categories: {stats.categories}") + print(f"Last updated: {datetime.fromtimestamp(stats.last_updated)}") + else: + print("No index found. Run without --stats to build index.") + return + + # Build index + index = indexer.build_index(force_rebuild=args.rebuild) + + if args.pattern: + files = indexer.find_files_by_pattern(args.pattern, index) + print(f"Found {len(files)} files matching pattern '{args.pattern}':") + for file_info in files[:20]: # Limit output + print(f" {file_info.relative_path}") + else: + stats = indexer._calculate_stats(index) + print(f"Index built: {stats.total_files} files, ~{stats.total_tokens:,} tokens") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/core/gitignore_parser.py b/.claude/python_script/core/gitignore_parser.py new file mode 100644 index 00000000..549e0014 --- /dev/null +++ b/.claude/python_script/core/gitignore_parser.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +""" +GitIgnore Parser Module +Parses .gitignore files and converts rules to fnmatch patterns for file exclusion. +""" + +import os +import fnmatch +from pathlib import Path +from typing import List, Set, Optional + + +class GitignoreParser: + """Parser for .gitignore files that converts rules to fnmatch patterns.""" + + def __init__(self, root_path: str = "."): + self.root_path = Path(root_path).resolve() + self.patterns: List[str] = [] + self.negation_patterns: List[str] = [] + + def parse_file(self, gitignore_path: str) -> List[str]: + """Parse a .gitignore file and return exclude patterns.""" + gitignore_file = Path(gitignore_path) + if not gitignore_file.exists(): + return [] + + patterns = [] + try: + with open(gitignore_file, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + pattern = self._parse_line(line.strip()) + if pattern: + patterns.append(pattern) + except (UnicodeDecodeError, IOError): + # Fallback to system encoding if UTF-8 fails + try: + with open(gitignore_file, 'r') as f: + for line_num, line in enumerate(f, 1): + pattern = self._parse_line(line.strip()) + if pattern: + patterns.append(pattern) + except IOError: + # If file can't be read, return empty list + return [] + + return patterns + + def _parse_line(self, line: str) -> Optional[str]: + """Parse a single line from .gitignore file.""" + # Skip empty lines and comments + if not line or line.startswith('#'): + return None + + # Handle negation patterns (starting with !) + if line.startswith('!'): + # For now, we'll skip negation patterns as they require + # more complex logic to implement correctly + return None + + # Convert gitignore pattern to fnmatch pattern + return self._convert_to_fnmatch(line) + + def _convert_to_fnmatch(self, pattern: str) -> str: + """Convert gitignore pattern to fnmatch pattern.""" + # Remove trailing slash (directory indicator) + if pattern.endswith('/'): + pattern = pattern[:-1] + + # Handle absolute paths (starting with /) + if pattern.startswith('/'): + pattern = pattern[1:] + # Make it match from root + return pattern + + # Handle patterns that should match anywhere in the tree + # If pattern doesn't contain '/', it matches files/dirs at any level + if '/' not in pattern: + return f"*/{pattern}" + + # Pattern contains '/', so it's relative to the gitignore location + return pattern + + def parse_all_gitignores(self, root_path: Optional[str] = None) -> List[str]: + """Parse all .gitignore files in the repository hierarchy.""" + if root_path: + self.root_path = Path(root_path).resolve() + + all_patterns = [] + + # Find all .gitignore files in the repository + gitignore_files = self._find_gitignore_files() + + for gitignore_file in gitignore_files: + patterns = self.parse_file(gitignore_file) + all_patterns.extend(patterns) + + return all_patterns + + def _find_gitignore_files(self) -> List[Path]: + """Find all .gitignore files in the repository.""" + gitignore_files = [] + + # Start with root .gitignore + root_gitignore = self.root_path / '.gitignore' + if root_gitignore.exists(): + gitignore_files.append(root_gitignore) + + # Find .gitignore files in subdirectories + try: + for gitignore_file in self.root_path.rglob('.gitignore'): + if gitignore_file != root_gitignore: + gitignore_files.append(gitignore_file) + except (PermissionError, OSError): + # Skip directories we can't access + pass + + return gitignore_files + + def should_exclude(self, file_path: str, gitignore_patterns: List[str]) -> bool: + """Check if a file should be excluded based on gitignore patterns.""" + # Convert to relative path from root + try: + rel_path = str(Path(file_path).relative_to(self.root_path)) + except ValueError: + # File is not under root path + return False + + # Normalize path separators for consistent matching + rel_path = rel_path.replace(os.sep, '/') + + for pattern in gitignore_patterns: + if self._matches_pattern(rel_path, pattern): + return True + + return False + + def _matches_pattern(self, file_path: str, pattern: str) -> bool: + """Check if a file path matches a gitignore pattern.""" + # Normalize pattern separators + pattern = pattern.replace(os.sep, '/') + + # Handle different pattern types + if pattern.startswith('*/'): + # Pattern like */pattern - matches at any level + sub_pattern = pattern[2:] + return fnmatch.fnmatch(file_path, f"*/{sub_pattern}") or fnmatch.fnmatch(file_path, sub_pattern) + elif '/' in pattern: + # Pattern contains slash - match exact path + return fnmatch.fnmatch(file_path, pattern) + else: + # Simple pattern - match filename or directory at any level + parts = file_path.split('/') + return any(fnmatch.fnmatch(part, pattern) for part in parts) + + +def parse_gitignore(gitignore_path: str) -> List[str]: + """Convenience function to parse a single .gitignore file.""" + parser = GitignoreParser() + return parser.parse_file(gitignore_path) + + +def get_all_gitignore_patterns(root_path: str = ".") -> List[str]: + """Convenience function to get all gitignore patterns in a repository.""" + parser = GitignoreParser(root_path) + return parser.parse_all_gitignores() + + +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + gitignore_path = sys.argv[1] + patterns = parse_gitignore(gitignore_path) + print(f"Parsed {len(patterns)} patterns from {gitignore_path}:") + for pattern in patterns: + print(f" {pattern}") + else: + # Parse all .gitignore files in current directory + patterns = get_all_gitignore_patterns() + print(f"Found {len(patterns)} gitignore patterns:") + for pattern in patterns: + print(f" {pattern}") \ No newline at end of file diff --git a/.claude/python_script/core/path_matcher.py b/.claude/python_script/core/path_matcher.py new file mode 100644 index 00000000..c410ef77 --- /dev/null +++ b/.claude/python_script/core/path_matcher.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 +""" +Path Matcher Module for UltraThink Path-Aware Analyzer +Matches files to analysis context and ranks them by relevance. +""" + +import re +import logging +import fnmatch +from typing import Dict, List, Tuple, Optional, Set +from dataclasses import dataclass +from pathlib import Path +import math + +from .file_indexer import FileInfo +from .context_analyzer import AnalysisResult + +@dataclass +class MatchResult: + """Result of path matching with relevance score.""" + file_info: FileInfo + relevance_score: float + match_reasons: List[str] + category_bonus: float + +@dataclass +class PathMatchingResult: + """Complete result of path matching operation.""" + matched_files: List[MatchResult] + total_tokens: int + categories: Dict[str, int] + patterns_used: List[str] + confidence_score: float + +class PathMatcher: + """Matches files to analysis context using various algorithms.""" + + def __init__(self, config: Dict): + self.config = config + self.logger = logging.getLogger(__name__) + + # Load scoring weights + self.weights = config.get('path_matching', {}).get('weights', { + 'keyword_match': 0.4, + 'extension_match': 0.2, + 'directory_context': 0.2, + 'file_size_penalty': 0.1, + 'recency_bonus': 0.1 + }) + + # Load limits + self.max_files_per_category = config.get('path_matching', {}).get('max_files_per_category', 20) + self.min_relevance_score = config.get('path_matching', {}).get('min_relevance_score', 0.1) + self.max_total_files = config.get('output', {}).get('max_total_files', 50) + + # Load always include patterns + self.always_include = config.get('output', {}).get('always_include', []) + + # Category priorities + self.category_priorities = { + 'code': 1.0, + 'config': 0.8, + 'docs': 0.6, + 'web': 0.4, + 'other': 0.2 + } + + def _calculate_keyword_score(self, file_info: FileInfo, keywords: List[str]) -> Tuple[float, List[str]]: + """Calculate score based on keyword matches in file path.""" + if not keywords: + return 0.0, [] + + path_lower = file_info.relative_path.lower() + filename_lower = Path(file_info.relative_path).name.lower() + + matches = [] + score = 0.0 + + for keyword in keywords: + keyword_lower = keyword.lower() + + # Exact filename match (highest weight) + if keyword_lower in filename_lower: + score += 2.0 + matches.append(f"filename:{keyword}") + continue + + # Directory name match + if keyword_lower in path_lower: + score += 1.0 + matches.append(f"path:{keyword}") + continue + + # Partial match in path components + path_parts = path_lower.split('/') + for part in path_parts: + if keyword_lower in part: + score += 0.5 + matches.append(f"partial:{keyword}") + break + + # Normalize by number of keywords + normalized_score = score / len(keywords) if keywords else 0.0 + return min(normalized_score, 1.0), matches + + def _calculate_extension_score(self, file_info: FileInfo, languages: List[str]) -> float: + """Calculate score based on file extension relevance.""" + if not languages: + return 0.5 # Neutral score + + extension = file_info.extension.lower() + + # Language-specific extension mapping + lang_extensions = { + 'python': ['.py', '.pyx', '.pyi'], + 'javascript': ['.js', '.jsx', '.mjs'], + 'typescript': ['.ts', '.tsx'], + 'java': ['.java'], + 'go': ['.go'], + 'rust': ['.rs'], + 'cpp': ['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp'], + 'csharp': ['.cs'], + 'php': ['.php'], + 'ruby': ['.rb'], + 'shell': ['.sh', '.bash', '.zsh'] + } + + score = 0.0 + for language in languages: + if language in lang_extensions: + if extension in lang_extensions[language]: + score = 1.0 + break + + # Fallback to category-based scoring + if score == 0.0: + category_scores = { + 'code': 1.0, + 'config': 0.8, + 'docs': 0.6, + 'web': 0.4, + 'other': 0.2 + } + score = category_scores.get(file_info.category, 0.2) + + return score + + def _calculate_directory_score(self, file_info: FileInfo, domains: List[str]) -> Tuple[float, List[str]]: + """Calculate score based on directory context.""" + if not domains: + return 0.0, [] + + path_parts = file_info.relative_path.lower().split('/') + matches = [] + score = 0.0 + + # Domain-specific directory patterns + domain_patterns = { + 'auth': ['auth', 'authentication', 'login', 'user', 'account'], + 'authentication': ['auth', 'authentication', 'login', 'user', 'account'], + 'database': ['db', 'database', 'model', 'entity', 'migration', 'schema'], + 'api': ['api', 'rest', 'graphql', 'route', 'controller', 'handler'], + 'frontend': ['ui', 'component', 'view', 'template', 'client', 'web'], + 'backend': ['service', 'server', 'core', 'business', 'logic'], + 'test': ['test', 'spec', 'tests', '__tests__', 'testing'], + 'testing': ['test', 'spec', 'tests', '__tests__', 'testing'], + 'config': ['config', 'configuration', 'env', 'settings'], + 'configuration': ['config', 'configuration', 'env', 'settings'], + 'util': ['util', 'utils', 'helper', 'common', 'shared', 'lib'], + 'utility': ['util', 'utils', 'helper', 'common', 'shared', 'lib'] + } + + for domain in domains: + if domain in domain_patterns: + patterns = domain_patterns[domain] + for pattern in patterns: + for part in path_parts: + if pattern in part: + score += 1.0 + matches.append(f"dir:{domain}->{pattern}") + break + + # Normalize by number of domains + normalized_score = score / len(domains) if domains else 0.0 + return min(normalized_score, 1.0), matches + + def _calculate_size_penalty(self, file_info: FileInfo) -> float: + """Calculate penalty for very large files.""" + max_size = self.config.get('performance', {}).get('max_file_size', 10485760) # 10MB + + if file_info.size > max_size: + # Heavy penalty for oversized files + return -0.5 + elif file_info.size > max_size * 0.5: + # Light penalty for large files + return -0.2 + else: + return 0.0 + + def _calculate_recency_bonus(self, file_info: FileInfo) -> float: + """Calculate bonus for recently modified files.""" + import time + + current_time = time.time() + file_age = current_time - file_info.modified_time + + # Files modified in last day get bonus + if file_age < 86400: # 1 day + return 0.3 + elif file_age < 604800: # 1 week + return 0.1 + else: + return 0.0 + + def calculate_relevance_score(self, file_info: FileInfo, analysis: AnalysisResult) -> MatchResult: + """Calculate overall relevance score for a file.""" + # Calculate individual scores + keyword_score, keyword_matches = self._calculate_keyword_score(file_info, analysis.keywords) + extension_score = self._calculate_extension_score(file_info, analysis.languages) + directory_score, dir_matches = self._calculate_directory_score(file_info, analysis.domains) + size_penalty = self._calculate_size_penalty(file_info) + recency_bonus = self._calculate_recency_bonus(file_info) + + # Apply weights + weighted_score = ( + keyword_score * self.weights.get('keyword_match', 0.4) + + extension_score * self.weights.get('extension_match', 0.2) + + directory_score * self.weights.get('directory_context', 0.2) + + size_penalty * self.weights.get('file_size_penalty', 0.1) + + recency_bonus * self.weights.get('recency_bonus', 0.1) + ) + + # Category bonus + category_bonus = self.category_priorities.get(file_info.category, 0.2) + + # Final score with category bonus + final_score = weighted_score + (category_bonus * 0.1) + + # Collect match reasons + match_reasons = keyword_matches + dir_matches + if extension_score > 0.5: + match_reasons.append(f"extension:{file_info.extension}") + if recency_bonus > 0: + match_reasons.append("recent") + + return MatchResult( + file_info=file_info, + relevance_score=max(0.0, final_score), + match_reasons=match_reasons, + category_bonus=category_bonus + ) + + def match_by_patterns(self, file_index: Dict[str, FileInfo], patterns: List[str]) -> List[FileInfo]: + """Match files using explicit glob patterns.""" + matched_files = [] + + for pattern in patterns: + for path, file_info in file_index.items(): + # Try matching both relative path and full path + if (fnmatch.fnmatch(path, pattern) or + fnmatch.fnmatch(file_info.path, pattern) or + fnmatch.fnmatch(Path(path).name, pattern)): + matched_files.append(file_info) + + # Remove duplicates based on path + seen_paths = set() + unique_files = [] + for file_info in matched_files: + if file_info.relative_path not in seen_paths: + seen_paths.add(file_info.relative_path) + unique_files.append(file_info) + return unique_files + + def match_always_include(self, file_index: Dict[str, FileInfo]) -> List[FileInfo]: + """Match files that should always be included.""" + return self.match_by_patterns(file_index, self.always_include) + + def rank_files(self, files: List[FileInfo], analysis: AnalysisResult) -> List[MatchResult]: + """Rank files by relevance score.""" + match_results = [] + + for file_info in files: + match_result = self.calculate_relevance_score(file_info, analysis) + if match_result.relevance_score >= self.min_relevance_score: + match_results.append(match_result) + + # Sort by relevance score (descending) + match_results.sort(key=lambda x: x.relevance_score, reverse=True) + + return match_results + + def select_best_files(self, ranked_files: List[MatchResult], token_limit: Optional[int] = None) -> List[MatchResult]: + """Select the best files within token limits and category constraints.""" + if not ranked_files: + return [] + + selected_files = [] + total_tokens = 0 + category_counts = {} + + for match_result in ranked_files: + file_info = match_result.file_info + category = file_info.category + + # Check category limit + if category_counts.get(category, 0) >= self.max_files_per_category: + continue + + # Check token limit + if token_limit and total_tokens + file_info.estimated_tokens > token_limit: + continue + + # Check total file limit + if len(selected_files) >= self.max_total_files: + break + + # Add file + selected_files.append(match_result) + total_tokens += file_info.estimated_tokens + category_counts[category] = category_counts.get(category, 0) + 1 + + return selected_files + + def match_files(self, file_index: Dict[str, FileInfo], analysis: AnalysisResult, + token_limit: Optional[int] = None, explicit_patterns: Optional[List[str]] = None) -> PathMatchingResult: + """Main file matching function.""" + self.logger.info(f"Matching files for analysis with {len(analysis.keywords)} keywords and {len(analysis.domains)} domains") + + # Start with always-include files + always_include_files = self.match_always_include(file_index) + self.logger.debug(f"Always include: {len(always_include_files)} files") + + # Add explicit pattern matches + pattern_files = [] + patterns_used = [] + if explicit_patterns: + pattern_files = self.match_by_patterns(file_index, explicit_patterns) + patterns_used.extend(explicit_patterns) + self.logger.debug(f"Explicit patterns: {len(pattern_files)} files") + + # Add suggested pattern matches + if analysis.file_patterns: + suggested_files = self.match_by_patterns(file_index, analysis.file_patterns) + pattern_files.extend(suggested_files) + patterns_used.extend(analysis.file_patterns) + self.logger.debug(f"Suggested patterns: {len(suggested_files)} files") + + # Combine all candidate files and remove duplicates + all_files = always_include_files + pattern_files + list(file_index.values()) + seen_paths = set() + all_candidates = [] + for file_info in all_files: + if file_info.relative_path not in seen_paths: + seen_paths.add(file_info.relative_path) + all_candidates.append(file_info) + self.logger.debug(f"Total candidates: {len(all_candidates)} files") + + # Rank all candidates + ranked_files = self.rank_files(all_candidates, analysis) + self.logger.debug(f"Files above threshold: {len(ranked_files)}") + + # Select best files within limits + selected_files = self.select_best_files(ranked_files, token_limit) + self.logger.info(f"Selected {len(selected_files)} files") + + # Calculate statistics + total_tokens = sum(match.file_info.estimated_tokens for match in selected_files) + categories = {} + for match in selected_files: + category = match.file_info.category + categories[category] = categories.get(category, 0) + 1 + + # Calculate confidence score + confidence_score = self._calculate_confidence(selected_files, analysis) + + return PathMatchingResult( + matched_files=selected_files, + total_tokens=total_tokens, + categories=categories, + patterns_used=patterns_used, + confidence_score=confidence_score + ) + + def _calculate_confidence(self, selected_files: List[MatchResult], analysis: AnalysisResult) -> float: + """Calculate confidence score for the matching result.""" + if not selected_files: + return 0.0 + + # Average relevance score + avg_relevance = sum(match.relevance_score for match in selected_files) / len(selected_files) + + # Keyword coverage (how many keywords are represented) + keyword_coverage = 0.0 + if analysis.keywords: + covered_keywords = set() + for match in selected_files: + for reason in match.match_reasons: + if reason.startswith('filename:') or reason.startswith('path:'): + keyword = reason.split(':', 1)[1] + covered_keywords.add(keyword) + keyword_coverage = len(covered_keywords) / len(analysis.keywords) + + # Domain coverage + domain_coverage = 0.0 + if analysis.domains: + covered_domains = set() + for match in selected_files: + for reason in match.match_reasons: + if reason.startswith('dir:'): + domain = reason.split('->', 1)[0].split(':', 1)[1] + covered_domains.add(domain) + domain_coverage = len(covered_domains) / len(analysis.domains) + + # Weighted confidence score + confidence = ( + avg_relevance * 0.5 + + keyword_coverage * 0.3 + + domain_coverage * 0.2 + ) + + return min(confidence, 1.0) + + def format_patterns(self, selected_files: List[MatchResult]) -> List[str]: + """Format selected files as @{pattern} strings.""" + pattern_format = self.config.get('output', {}).get('pattern_format', '@{{{path}}}') + + patterns = [] + for match in selected_files: + pattern = pattern_format.format(path=match.file_info.relative_path) + patterns.append(pattern) + + return patterns + +def main(): + """Command-line interface for path matcher.""" + import yaml + import argparse + import json + from .file_indexer import FileIndexer + from .context_analyzer import ContextAnalyzer + + parser = argparse.ArgumentParser(description="Path Matcher for UltraThink") + parser.add_argument("prompt", help="Prompt to analyze and match") + parser.add_argument("--config", default="config.yaml", help="Configuration file path") + parser.add_argument("--token-limit", type=int, help="Token limit for selection") + parser.add_argument("--patterns", nargs="*", help="Explicit patterns to include") + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + + args = parser.parse_args() + + # Setup logging + level = logging.DEBUG if args.verbose else logging.INFO + logging.basicConfig(level=level, format='%(levelname)s: %(message)s') + + # Load configuration + config_path = Path(__file__).parent / args.config + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + # Create components + indexer = FileIndexer(config) + analyzer = ContextAnalyzer(config) + matcher = PathMatcher(config) + + # Build file index + file_index = indexer.load_index() + if not file_index: + print("Building file index...") + file_index = indexer.build_index() + + # Analyze prompt + analysis = analyzer.analyze(args.prompt) + + # Match files + result = matcher.match_files( + file_index=file_index, + analysis=analysis, + token_limit=args.token_limit, + explicit_patterns=args.patterns + ) + + # Output results + print(f"Matched {len(result.matched_files)} files (~{result.total_tokens:,} tokens)") + print(f"Categories: {result.categories}") + print(f"Confidence: {result.confidence_score:.2f}") + print() + + patterns = matcher.format_patterns(result.matched_files) + print("Patterns:") + for pattern in patterns[:20]: # Limit output + print(f" {pattern}") + + if args.verbose: + print("\nDetailed matches:") + for match in result.matched_files[:10]: + print(f" {match.file_info.relative_path} (score: {match.relevance_score:.3f})") + print(f" Reasons: {', '.join(match.match_reasons)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/indexer.py b/.claude/python_script/indexer.py new file mode 100644 index 00000000..978951a8 --- /dev/null +++ b/.claude/python_script/indexer.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +File Structure Indexer +Builds and maintains file indices for intelligent analysis. +""" + +import sys +import argparse +import logging +import json +import time +from pathlib import Path +from typing import Dict, List, Optional, Any + +# Add current directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from core.config import get_config +from core.file_indexer import FileIndexer, IndexStats +from core.embedding_manager import EmbeddingManager +from utils.colors import Colors + + +class ProjectIndexer: + """Manages file indexing and project statistics.""" + + def __init__(self, config_path: Optional[str] = None, root_path: str = "."): + self.root_path = Path(root_path).resolve() + self.config = get_config(config_path) + + # Setup logging + logging.basicConfig( + level=getattr(logging, self.config.get('logging.level', 'INFO')), + format=self.config.get('logging.format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ) + self.logger = logging.getLogger(__name__) + + # Initialize core components + self.indexer = FileIndexer(self.config, str(self.root_path)) + + # Initialize embedding manager if enabled + self.embedding_manager = None + if self.config.is_embedding_enabled(): + try: + self.embedding_manager = EmbeddingManager(self.config) + except ImportError: + self.logger.warning("Embedding dependencies not available. Install sentence-transformers for enhanced functionality.") + + def build_index(self) -> IndexStats: + """Build or update the file index.""" + print(Colors.yellow("Building file index...")) + start_time = time.time() + + self.indexer.build_index() + stats = self.indexer.get_stats() + + elapsed = time.time() - start_time + if stats: + print(Colors.green(f"Index built: {stats.total_files} files, ~{stats.total_tokens:,} tokens ({elapsed:.2f}s)")) + else: + print(Colors.green(f"Index built successfully ({elapsed:.2f}s)")) + + return stats + + def update_embeddings(self) -> bool: + """Update embeddings for semantic similarity.""" + if not self.embedding_manager: + print(Colors.error("Embedding functionality not available")) + return False + + print(Colors.yellow("Updating embeddings...")) + start_time = time.time() + + # Load file index + index = self.indexer.load_index() + if not index: + print(Colors.warning("No file index found. Building index first...")) + self.build_index() + index = self.indexer.load_index() + + try: + count = self.embedding_manager.update_embeddings(index) + elapsed = time.time() - start_time + print(Colors.green(f"Updated {count} embeddings ({elapsed:.2f}s)")) + return True + except Exception as e: + print(Colors.error(f"Failed to update embeddings: {e}")) + return False + + def get_project_stats(self) -> Dict[str, Any]: + """Get comprehensive project statistics.""" + stats = self.indexer.get_stats() + embedding_stats = {} + + if self.embedding_manager: + embedding_stats = { + 'embeddings_exist': self.embedding_manager.embeddings_exist(), + 'embedding_count': len(self.embedding_manager._load_embedding_cache()) if self.embedding_manager.embeddings_exist() else 0 + } + + project_size = self._classify_project_size(stats.total_tokens if stats else 0) + + return { + 'files': stats.total_files if stats else 0, + 'tokens': stats.total_tokens if stats else 0, + 'size_bytes': stats.total_size if stats else 0, + 'categories': stats.categories if stats else {}, + 'project_size': project_size, + 'last_updated': stats.last_updated if stats else 0, + 'embeddings': embedding_stats, + 'config': { + 'cache_dir': self.config.get_cache_dir(), + 'embedding_enabled': self.config.is_embedding_enabled(), + 'exclude_patterns_count': len(self.config.get_exclude_patterns()) + } + } + + def _classify_project_size(self, tokens: int) -> str: + """Classify project size based on token count.""" + small_limit = self.config.get('token_limits.small_project', 500000) + medium_limit = self.config.get('token_limits.medium_project', 2000000) + + if tokens < small_limit: + return "small" + elif tokens < medium_limit: + return "medium" + else: + return "large" + + def cleanup_cache(self): + """Clean up old cache files.""" + cache_dir = Path(self.config.get_cache_dir()) + if cache_dir.exists(): + print(Colors.yellow("Cleaning up cache...")) + for file in cache_dir.glob("*"): + if file.is_file(): + file.unlink() + print(f"Removed: {file}") + print(Colors.green("Cache cleaned")) + + +def main(): + """CLI entry point for indexer.""" + parser = argparse.ArgumentParser( + description="Project File Indexer - Build and manage file indices", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python indexer.py --build # Build file index + python indexer.py --stats # Show project statistics + python indexer.py --embeddings # Update embeddings + python indexer.py --cleanup # Clean cache + """ + ) + + parser.add_argument('--build', action='store_true', help='Build file index') + parser.add_argument('--stats', action='store_true', help='Show project statistics') + parser.add_argument('--embeddings', action='store_true', help='Update embeddings') + parser.add_argument('--cleanup', action='store_true', help='Clean up cache files') + parser.add_argument('--output', choices=['json', 'text'], default='text', help='Output format') + parser.add_argument('--config', help='Configuration file path') + parser.add_argument('--root', default='.', help='Root directory to analyze') + + args = parser.parse_args() + + # Require at least one action + if not any([args.build, args.stats, args.embeddings, args.cleanup]): + parser.error("At least one action is required: --build, --stats, --embeddings, or --cleanup") + + # Create indexer + indexer = ProjectIndexer(args.config, args.root) + + try: + if args.cleanup: + indexer.cleanup_cache() + + if args.build: + indexer.build_index() + + if args.embeddings: + indexer.update_embeddings() + + if args.stats: + stats = indexer.get_project_stats() + if args.output == 'json': + print(json.dumps(stats, indent=2, default=str)) + else: + print(f"Total files: {stats['files']}") + print(f"Total tokens: {stats['tokens']:,}") + print(f"Project size: {stats['project_size']}") + print(f"Categories: {stats['categories']}") + if 'embeddings' in stats: + print(f"Embeddings: {stats['embeddings']['embedding_count']}") + + except KeyboardInterrupt: + print(Colors.warning("\nOperation interrupted by user")) + sys.exit(1) + except Exception as e: + print(Colors.error(f"Operation failed: {e}")) + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/install.sh b/.claude/python_script/install.sh new file mode 100644 index 00000000..d855fa13 --- /dev/null +++ b/.claude/python_script/install.sh @@ -0,0 +1,189 @@ +#!/bin/bash +# Installation script for UltraThink Path-Aware Analyzer + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Functions +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check Python version +check_python() { + if command -v python3 &> /dev/null; then + PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + PYTHON_CMD="python3" + elif command -v python &> /dev/null; then + PYTHON_VERSION=$(python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + PYTHON_CMD="python" + else + print_error "Python not found. Please install Python 3.8 or later." + exit 1 + fi + + # Check version + if [[ $(echo "$PYTHON_VERSION >= 3.8" | bc -l) -eq 1 ]]; then + print_success "Python $PYTHON_VERSION found" + else + print_error "Python 3.8 or later required. Found Python $PYTHON_VERSION" + exit 1 + fi +} + +# Install dependencies +install_dependencies() { + print_status "Installing core dependencies..." + + # Install core requirements + $PYTHON_CMD -m pip install --user -r requirements.txt + + if [ $? -eq 0 ]; then + print_success "Core dependencies installed" + else + print_error "Failed to install core dependencies" + exit 1 + fi +} + +# Install optional dependencies +install_optional() { + read -p "Install RAG/embedding features? (requires ~200MB download) [y/N]: " install_rag + if [[ $install_rag =~ ^[Yy]$ ]]; then + print_status "Installing RAG dependencies..." + $PYTHON_CMD -m pip install --user sentence-transformers numpy + if [ $? -eq 0 ]; then + print_success "RAG dependencies installed" + else + print_warning "Failed to install RAG dependencies (optional)" + fi + fi + + read -p "Install development tools? [y/N]: " install_dev + if [[ $install_dev =~ ^[Yy]$ ]]; then + print_status "Installing development dependencies..." + $PYTHON_CMD -m pip install --user pytest pytest-cov black flake8 + if [ $? -eq 0 ]; then + print_success "Development dependencies installed" + else + print_warning "Failed to install development dependencies (optional)" + fi + fi +} + +# Create wrapper script +create_wrapper() { + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" + WRAPPER_PATH="$HOME/.local/bin/ultrathink" + + # Create .local/bin if it doesn't exist + mkdir -p "$HOME/.local/bin" + + # Create wrapper script + cat > "$WRAPPER_PATH" << EOF +#!/bin/bash +# UltraThink Path-Aware Analyzer Wrapper +# Auto-generated by install.sh + +SCRIPT_DIR="$SCRIPT_DIR" +export PYTHONPATH="\$SCRIPT_DIR:\$PYTHONPATH" + +exec $PYTHON_CMD "\$SCRIPT_DIR/path_aware_analyzer.py" "\$@" +EOF + + chmod +x "$WRAPPER_PATH" + + if [ -f "$WRAPPER_PATH" ]; then + print_success "Wrapper script created at $WRAPPER_PATH" + else + print_error "Failed to create wrapper script" + exit 1 + fi +} + +# Update configuration +setup_config() { + print_status "Setting up configuration..." + + # Create cache directory + mkdir -p .claude/cache/embeddings + + # Check if config needs updating + if [ ! -f config.yaml ]; then + print_error "Configuration file config.yaml not found" + exit 1 + fi + + print_success "Configuration ready" +} + +# Test installation +test_installation() { + print_status "Testing installation..." + + # Test basic functionality + if $PYTHON_CMD path_aware_analyzer.py --stats &> /dev/null; then + print_success "Installation test passed" + else + print_warning "Installation test failed - but this might be normal for first run" + fi +} + +# Add to PATH instructions +show_path_instructions() { + if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + print_warning "Add $HOME/.local/bin to your PATH to use 'ultrathink' command globally" + echo "" + echo "Add this line to your ~/.bashrc or ~/.zshrc:" + echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" + echo "" + echo "Or run: echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc" + echo "Then: source ~/.bashrc" + fi +} + +# Main installation +main() { + print_status "Installing UltraThink Path-Aware Analyzer..." + echo "" + + check_python + install_dependencies + install_optional + create_wrapper + setup_config + test_installation + + echo "" + print_success "Installation complete!" + echo "" + + print_status "Usage examples:" + echo " ./path_aware_analyzer.py \"analyze authentication flow\"" + echo " ultrathink \"implement user login feature\"" + echo " ultrathink --tool gemini \"review API endpoints\"" + echo "" + + show_path_instructions +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/.claude/python_script/requirements.txt b/.claude/python_script/requirements.txt new file mode 100644 index 00000000..53640467 --- /dev/null +++ b/.claude/python_script/requirements.txt @@ -0,0 +1,15 @@ +# Core dependencies for embedding tests +numpy>=1.21.0 +scikit-learn>=1.0.0 + +# Sentence Transformers for advanced embeddings +sentence-transformers>=2.2.0 + +# Optional: For better performance and additional models +torch>=1.9.0 + +# Development and testing +pytest>=6.0.0 + +# Data handling +pandas>=1.3.0 diff --git a/.claude/python_script/setup.py b/.claude/python_script/setup.py new file mode 100644 index 00000000..ab8d01f0 --- /dev/null +++ b/.claude/python_script/setup.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +""" +Setup script for UltraThink Path-Aware Analyzer +""" + +from setuptools import setup, find_packages +from pathlib import Path + +# Read README +readme_path = Path(__file__).parent / "README.md" +long_description = readme_path.read_text(encoding='utf-8') if readme_path.exists() else "" + +# Read requirements +requirements_path = Path(__file__).parent / "requirements.txt" +requirements = [] +if requirements_path.exists(): + with open(requirements_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + requirements.append(line) + +setup( + name="ultrathink-path-analyzer", + version="1.0.0", + description="Lightweight path-aware program for intelligent file pattern detection and analysis", + long_description=long_description, + long_description_content_type="text/markdown", + author="UltraThink Development Team", + author_email="dev@ultrathink.ai", + url="https://github.com/ultrathink/path-analyzer", + + packages=find_packages(), + py_modules=[ + 'analyzer', # Main entry point + ], + + install_requires=requirements, + + extras_require={ + 'rag': [ + 'sentence-transformers>=2.2.0', + 'numpy>=1.21.0' + ], + 'nlp': [ + 'nltk>=3.8', + 'spacy>=3.4.0' + ], + 'performance': [ + 'numba>=0.56.0' + ], + 'dev': [ + 'pytest>=7.0.0', + 'pytest-cov>=4.0.0', + 'black>=22.0.0', + 'flake8>=5.0.0' + ] + }, + + entry_points={ + 'console_scripts': [ + 'path-analyzer=cli:main', + 'path-indexer=indexer:main', + 'analyzer=analyzer:main', # Legacy compatibility + 'module-analyzer=tools.module_analyzer:main', + 'tech-stack=tools.tech_stack:main', + ], + }, + + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Tools", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Operating System :: OS Independent", + ], + + python_requires=">=3.8", + + keywords="ai, analysis, path-detection, code-analysis, file-matching, rag, nlp", + + project_urls={ + "Bug Reports": "https://github.com/ultrathink/path-analyzer/issues", + "Source": "https://github.com/ultrathink/path-analyzer", + "Documentation": "https://github.com/ultrathink/path-analyzer/docs", + }, +) \ No newline at end of file diff --git a/.claude/python_script/tools/__init__.py b/.claude/python_script/tools/__init__.py new file mode 100644 index 00000000..205b99d4 --- /dev/null +++ b/.claude/python_script/tools/__init__.py @@ -0,0 +1,13 @@ +""" +Independent tool scripts for specialized analysis tasks. +Provides module analysis, tech stack detection, and workflow management tools. +""" + +from .module_analyzer import ModuleAnalyzer, ModuleInfo +from .tech_stack import TechStackLoader + +__all__ = [ + 'ModuleAnalyzer', + 'ModuleInfo', + 'TechStackLoader' +] \ No newline at end of file diff --git a/.claude/python_script/tools/__pycache__/__init__.cpython-313.pyc b/.claude/python_script/tools/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42acf8b8ceebc0bdf898175734743ddecab4bc89 GIT binary patch literal 480 zcmYL_!AiqG5QcY?w3U?Fv$wrTpwxpPA|i-h+Nvn=7+6Af+lFqkVRx&vo;>;hK7;ry zzCpl~CvR#GUY$*>&S99J+4<(5U3EG>f_S=nnGP^QZ&s|s`7KsGC>~LW6bfG?cwkTyX zp|tGQ)@WrNVE=DR-C@o%AD4} z6xkos5v6IK8cO@cqTD#FmmaAOVC+$P3(|*+G5&O1?0x$P@4uq$FVAZ`_bt_i?hmMp Bj(`9F literal 0 HcmV?d00001 diff --git a/.claude/python_script/tools/__pycache__/module_analyzer.cpython-313.pyc b/.claude/python_script/tools/__pycache__/module_analyzer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4705dee71a679dad2b208e167ae002f0ff9dd010 GIT binary patch literal 18823 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%0DBxBZHKQ9ul3&^ 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 R4Q;<*EWbAm Dict: + return asdict(self) + +class ModuleAnalyzer: + """Unified module analysis tool with change detection and depth analysis.""" + + def __init__(self, root_path: str = ".", config_path: Optional[str] = None): + self.root_path = Path(root_path).resolve() + self.config = get_config(config_path) + + # Source file extensions for analysis + self.source_extensions = { + '.md', '.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', + '.java', '.cpp', '.c', '.h', '.sh', '.ps1', '.json', '.yaml', '.yml', + '.php', '.rb', '.swift', '.kt', '.scala', '.dart' + } + + # Initialize gitignore parser for exclusions + self.gitignore_parser = GitignoreParser(str(self.root_path)) + self.exclude_patterns = self._build_exclusion_patterns() + + def _build_exclusion_patterns(self) -> Set[str]: + """Build exclusion patterns from config and gitignore.""" + exclusions = { + '.git', '.history', '.vscode', '__pycache__', '.pytest_cache', + 'node_modules', 'dist', 'build', '.egg-info', '.env', + '.cache', '.tmp', '.temp', '.DS_Store', 'Thumbs.db' + } + + # Add patterns from config + config_patterns = self.config.get('exclude_patterns', []) + for pattern in config_patterns: + # Extract directory names from patterns + if '/' in pattern: + parts = pattern.replace('*/', '').replace('/*', '').split('/') + exclusions.update(part for part in parts if part and not part.startswith('*')) + + return exclusions + + def _should_exclude_directory(self, dir_path: Path) -> bool: + """Check if directory should be excluded from analysis.""" + dir_name = dir_path.name + + # Check against exclusion patterns + if dir_name in self.exclude_patterns: + return True + + # Check if directory starts with . (hidden directories) + if dir_name.startswith('.') and dir_name not in {'.github', '.vscode'}: + return True + + return False + + def get_git_changed_files(self, since: str = "HEAD") -> Set[str]: + """Get files changed in git.""" + changed_files = set() + + try: + # Check if we're in a git repository + subprocess.run(['git', 'rev-parse', '--git-dir'], + check=True, capture_output=True, cwd=self.root_path) + + # Get changes since specified reference + commands = [ + ['git', 'diff', '--name-only', since], # Changes since reference + ['git', 'diff', '--name-only', '--staged'], # Staged changes + ['git', 'ls-files', '--others', '--exclude-standard'] # Untracked files + ] + + for cmd in commands: + try: + result = subprocess.run(cmd, capture_output=True, text=True, + cwd=self.root_path, check=True) + if result.stdout.strip(): + files = result.stdout.strip().split('\n') + changed_files.update(f for f in files if f) + except subprocess.CalledProcessError: + continue + + except subprocess.CalledProcessError: + # Not a git repository or git not available + pass + + return changed_files + + def get_recently_modified_files(self, hours: int = 24) -> Set[str]: + """Get files modified within the specified hours.""" + cutoff_time = time.time() - (hours * 3600) + recent_files = set() + + try: + for file_path in self.root_path.rglob('*'): + if file_path.is_file(): + try: + if file_path.stat().st_mtime > cutoff_time: + rel_path = file_path.relative_to(self.root_path) + recent_files.add(str(rel_path)) + except (OSError, ValueError): + continue + except Exception: + pass + + return recent_files + + def analyze_directory(self, dir_path: Path) -> Optional[ModuleInfo]: + """Analyze a single directory and return module information.""" + if self._should_exclude_directory(dir_path): + return None + + try: + # Count files by type + file_types = set() + file_count = 0 + has_claude = False + last_modified = 0 + + for item in dir_path.iterdir(): + if item.is_file(): + file_count += 1 + + # Track file types + if item.suffix.lower() in self.source_extensions: + file_types.add(item.suffix.lower()) + + # Check for CLAUDE.md + if item.name.upper() == 'CLAUDE.MD': + has_claude = True + + # Track latest modification + try: + mtime = item.stat().st_mtime + last_modified = max(last_modified, mtime) + except OSError: + continue + + # Calculate depth relative to root + try: + relative_path = dir_path.relative_to(self.root_path) + depth = len(relative_path.parts) + except ValueError: + depth = 0 + + return ModuleInfo( + depth=depth, + path=str(relative_path) if depth > 0 else ".", + files=file_count, + types=sorted(list(file_types)), + has_claude=has_claude, + last_modified=last_modified if last_modified > 0 else None + ) + + except (PermissionError, OSError): + return None + + def detect_changed_modules(self, since: str = "HEAD") -> List[ModuleInfo]: + """Detect modules affected by changes.""" + changed_files = self.get_git_changed_files(since) + + # If no git changes, fall back to recently modified files + if not changed_files: + changed_files = self.get_recently_modified_files(24) + + # Get affected directories + affected_dirs = set() + for file_path in changed_files: + full_path = self.root_path / file_path + if full_path.exists(): + # Add the file's directory and parent directories + current_dir = full_path.parent + while current_dir != self.root_path and current_dir.parent != current_dir: + affected_dirs.add(current_dir) + current_dir = current_dir.parent + + # Analyze affected directories + modules = [] + for dir_path in affected_dirs: + module_info = self.analyze_directory(dir_path) + if module_info: + module_info.status = "changed" + modules.append(module_info) + + return sorted(modules, key=lambda m: (m.depth, m.path)) + + def analyze_by_depth(self, max_depth: Optional[int] = None) -> List[ModuleInfo]: + """Analyze all modules organized by depth (deepest first).""" + modules = [] + + def scan_directory(dir_path: Path, current_depth: int = 0): + """Recursively scan directories.""" + if max_depth and current_depth > max_depth: + return + + module_info = self.analyze_directory(dir_path) + if module_info and module_info.files > 0: + modules.append(module_info) + + # Recurse into subdirectories + try: + for item in dir_path.iterdir(): + if item.is_dir() and not self._should_exclude_directory(item): + scan_directory(item, current_depth + 1) + except (PermissionError, OSError): + pass + + scan_directory(self.root_path) + + # Sort by depth (deepest first), then by path + return sorted(modules, key=lambda m: (-m.depth, m.path)) + + def get_dependencies(self, module_path: str) -> List[str]: + """Get module dependencies (basic implementation).""" + dependencies = [] + module_dir = self.root_path / module_path + + if not module_dir.exists() or not module_dir.is_dir(): + return dependencies + + # Look for common dependency files + dependency_files = [ + 'package.json', # Node.js + 'requirements.txt', # Python + 'Cargo.toml', # Rust + 'go.mod', # Go + 'pom.xml', # Java Maven + 'build.gradle', # Java Gradle + ] + + for dep_file in dependency_files: + dep_path = module_dir / dep_file + if dep_path.exists(): + dependencies.append(str(dep_path.relative_to(self.root_path))) + + return dependencies + + def find_modules_with_pattern(self, pattern: str) -> List[ModuleInfo]: + """Find modules matching a specific pattern in their path or files.""" + modules = self.analyze_by_depth() + matching_modules = [] + + for module in modules: + # Check if pattern matches path + if pattern.lower() in module.path.lower(): + matching_modules.append(module) + continue + + # Check if pattern matches file types + if any(pattern.lower() in ext.lower() for ext in module.types): + matching_modules.append(module) + + return matching_modules + + def export_analysis(self, modules: List[ModuleInfo], format: str = "json") -> str: + """Export module analysis in specified format.""" + if format == "json": + return json.dumps([module.to_dict() for module in modules], indent=2) + + elif format == "list": + lines = [] + for module in modules: + status = f"[{module.status}]" if module.status != "normal" else "" + claude_marker = "[CLAUDE]" if module.has_claude else "" + lines.append(f"{module.path} (depth:{module.depth}, files:{module.files}) {status} {claude_marker}") + return "\n".join(lines) + + elif format == "grouped": + grouped = {} + for module in modules: + depth = module.depth + if depth not in grouped: + grouped[depth] = [] + grouped[depth].append(module) + + lines = [] + for depth in sorted(grouped.keys()): + lines.append(f"\n=== Depth {depth} ===") + for module in grouped[depth]: + status = f"[{module.status}]" if module.status != "normal" else "" + claude_marker = "[CLAUDE]" if module.has_claude else "" + lines.append(f" {module.path} (files:{module.files}) {status} {claude_marker}") + return "\n".join(lines) + + elif format == "paths": + return "\n".join(module.path for module in modules) + + else: + raise ValueError(f"Unsupported format: {format}") + + +def main(): + """Main CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser(description="Module Analysis Tool") + parser.add_argument("command", choices=["changed", "depth", "dependencies", "find"], + help="Analysis command to run") + parser.add_argument("--format", choices=["json", "list", "grouped", "paths"], + default="list", help="Output format") + parser.add_argument("--since", default="HEAD~1", + help="Git reference for change detection (default: HEAD~1)") + parser.add_argument("--max-depth", type=int, + help="Maximum directory depth to analyze") + parser.add_argument("--pattern", help="Pattern to search for (for find command)") + parser.add_argument("--module", help="Module path for dependency analysis") + parser.add_argument("--config", help="Configuration file path") + + args = parser.parse_args() + + analyzer = ModuleAnalyzer(config_path=args.config) + + if args.command == "changed": + modules = analyzer.detect_changed_modules(args.since) + print(analyzer.export_analysis(modules, args.format)) + + elif args.command == "depth": + modules = analyzer.analyze_by_depth(args.max_depth) + print(analyzer.export_analysis(modules, args.format)) + + elif args.command == "dependencies": + if not args.module: + print("Error: --module required for dependencies command", file=sys.stderr) + sys.exit(1) + deps = analyzer.get_dependencies(args.module) + if args.format == "json": + print(json.dumps(deps, indent=2)) + else: + print("\n".join(deps)) + + elif args.command == "find": + if not args.pattern: + print("Error: --pattern required for find command", file=sys.stderr) + sys.exit(1) + modules = analyzer.find_modules_with_pattern(args.pattern) + print(analyzer.export_analysis(modules, args.format)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/tools/tech_stack.py b/.claude/python_script/tools/tech_stack.py new file mode 100644 index 00000000..2d434f5c --- /dev/null +++ b/.claude/python_script/tools/tech_stack.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +Python equivalent of tech-stack-loader.sh +DMSFlow Tech Stack Guidelines Loader +Returns tech stack specific coding guidelines and best practices for Claude processing + +Usage: python tech_stack_loader.py [command] [tech_stack] +""" + +import sys +import argparse +import re +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +class TechStackLoader: + """Load tech stack specific development guidelines.""" + + def __init__(self, script_dir: Optional[str] = None): + if script_dir: + self.script_dir = Path(script_dir) + else: + self.script_dir = Path(__file__).parent + + # Look for template directory in multiple locations + possible_template_dirs = [ + self.script_dir / "../tech-stack-templates", + self.script_dir / "../workflows/cli-templates/tech-stacks", + self.script_dir / "tech-stack-templates", + self.script_dir / "templates", + ] + + self.template_dir = None + for template_dir in possible_template_dirs: + if template_dir.exists(): + self.template_dir = template_dir.resolve() + break + + if not self.template_dir: + # Create a default template directory + self.template_dir = self.script_dir / "tech-stack-templates" + self.template_dir.mkdir(exist_ok=True) + + def parse_yaml_frontmatter(self, content: str) -> Tuple[Dict[str, str], str]: + """Parse YAML frontmatter from markdown content.""" + frontmatter = {} + content_start = 0 + + lines = content.split('\n') + if lines and lines[0].strip() == '---': + # Find the closing --- + for i, line in enumerate(lines[1:], 1): + if line.strip() == '---': + content_start = i + 1 + break + elif ':' in line: + key, value = line.split(':', 1) + frontmatter[key.strip()] = value.strip() + + # Return frontmatter and content without YAML + remaining_content = '\n'.join(lines[content_start:]) + return frontmatter, remaining_content + + def list_available_guidelines(self) -> str: + """List all available development guidelines.""" + output = ["Available Development Guidelines:", "=" * 33] + + if not self.template_dir.exists(): + output.append("No template directory found.") + return '\n'.join(output) + + for file_path in self.template_dir.glob("*.md"): + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + frontmatter, _ = self.parse_yaml_frontmatter(content) + name = frontmatter.get('name', file_path.stem) + description = frontmatter.get('description', 'No description available') + + output.append(f"{name:<20} - {description}") + + except Exception as e: + output.append(f"{file_path.stem:<20} - Error reading file: {e}") + + return '\n'.join(output) + + def load_guidelines(self, tech_stack: str) -> str: + """Load specific development guidelines.""" + template_path = self.template_dir / f"{tech_stack}.md" + + if not template_path.exists(): + # Try with different naming conventions + alternatives = [ + f"{tech_stack}-dev.md", + f"{tech_stack}_dev.md", + f"{tech_stack.replace('-', '_')}.md", + f"{tech_stack.replace('_', '-')}.md" + ] + + for alt in alternatives: + alt_path = self.template_dir / alt + if alt_path.exists(): + template_path = alt_path + break + else: + raise FileNotFoundError( + f"Error: Development guidelines '{tech_stack}' not found\n" + f"Use --list to see available guidelines" + ) + + try: + with open(template_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Parse and return content without YAML frontmatter + _, content_without_yaml = self.parse_yaml_frontmatter(content) + return content_without_yaml.strip() + + except Exception as e: + raise RuntimeError(f"Error reading guidelines file: {e}") + + def get_version(self) -> str: + """Get version information.""" + return "DMSFlow tech-stack-loader v2.0 (Python)\nSemantic-based development guidelines system" + + def get_help(self) -> str: + """Get help message.""" + return """Usage: + tech_stack_loader.py --list List all available guidelines with descriptions + tech_stack_loader.py --load Load specific development guidelines + tech_stack_loader.py Load specific guidelines (legacy format) + tech_stack_loader.py --help Show this help message + tech_stack_loader.py --version Show version information + +Examples: + tech_stack_loader.py --list + tech_stack_loader.py --load javascript-dev + tech_stack_loader.py python-dev""" + +def main(): + """Command-line interface.""" + parser = argparse.ArgumentParser( + description="DMSFlow Tech Stack Guidelines Loader", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""Examples: + python tech_stack_loader.py --list + python tech_stack_loader.py --load javascript-dev + python tech_stack_loader.py python-dev""" + ) + + parser.add_argument("command", nargs="?", help="Command or tech stack name") + parser.add_argument("tech_stack", nargs="?", help="Tech stack name (when using --load)") + parser.add_argument("--list", action="store_true", help="List all available guidelines") + parser.add_argument("--load", metavar="TECH_STACK", help="Load specific development guidelines") + parser.add_argument("--version", "-v", action="store_true", help="Show version information") + parser.add_argument("--template-dir", help="Override template directory path") + + args = parser.parse_args() + + try: + loader = TechStackLoader(args.template_dir) + + # Handle version check + if args.version or args.command == "--version": + print(loader.get_version()) + return + + # Handle list command + if args.list or args.command == "--list": + print(loader.list_available_guidelines()) + return + + # Handle load command + if args.load: + result = loader.load_guidelines(args.load) + print(result) + return + + if args.command == "--load" and args.tech_stack: + result = loader.load_guidelines(args.tech_stack) + print(result) + return + + # Handle legacy usage (direct tech stack name) + if args.command and args.command not in ["--help", "--list", "--load"]: + result = loader.load_guidelines(args.command) + print(result) + return + + # Show help + print(loader.get_help()) + + except (FileNotFoundError, RuntimeError) as e: + print(str(e), file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/tools/workflow_updater.py b/.claude/python_script/tools/workflow_updater.py new file mode 100644 index 00000000..14822286 --- /dev/null +++ b/.claude/python_script/tools/workflow_updater.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +""" +Python equivalent of update_module_claude.sh +Update CLAUDE.md for a specific module with automatic layer detection + +Usage: python update_module_claude.py [update_type] + module_path: Path to the module directory + update_type: full|related (default: full) + Script automatically detects layer depth and selects appropriate template +""" + +import os +import sys +import subprocess +import time +import argparse +from pathlib import Path +from typing import Optional, Tuple, Dict +from dataclasses import dataclass + +@dataclass +class LayerInfo: + """Information about a documentation layer.""" + name: str + template_path: str + analysis_strategy: str + +class ModuleClaudeUpdater: + """Update CLAUDE.md documentation for modules with layer detection.""" + + def __init__(self, home_dir: Optional[str] = None): + self.home_dir = Path(home_dir) if home_dir else Path.home() + self.template_base = self.home_dir / ".claude/workflows/cli-templates/prompts/dms" + + def detect_layer(self, module_path: str) -> LayerInfo: + """Determine documentation layer based on path patterns.""" + clean_path = module_path.replace('./', '') if module_path.startswith('./') else module_path + + if module_path == ".": + # Root directory + return LayerInfo( + name="Layer 1 (Root)", + template_path=str(self.template_base / "claude-layer1-root.txt"), + analysis_strategy="--all-files" + ) + elif '/' not in clean_path: + # Top-level directories (e.g., .claude, src, tests) + return LayerInfo( + name="Layer 2 (Domain)", + template_path=str(self.template_base / "claude-layer2-domain.txt"), + analysis_strategy="@{*/CLAUDE.md}" + ) + elif clean_path.count('/') == 1: + # Second-level directories (e.g., .claude/scripts, src/components) + return LayerInfo( + name="Layer 3 (Module)", + template_path=str(self.template_base / "claude-layer3-module.txt"), + analysis_strategy="@{*/CLAUDE.md}" + ) + else: + # Deeper directories (e.g., .claude/workflows/cli-templates/prompts) + return LayerInfo( + name="Layer 4 (Sub-Module)", + template_path=str(self.template_base / "claude-layer4-submodule.txt"), + analysis_strategy="--all-files" + ) + + def load_template(self, template_path: str) -> str: + """Load template content from file.""" + try: + with open(template_path, 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + print(f" [WARN] Template not found: {template_path}, using fallback") + return "Update CLAUDE.md documentation for this module following hierarchy standards." + except Exception as e: + print(f" [WARN] Error reading template: {e}, using fallback") + return "Update CLAUDE.md documentation for this module following hierarchy standards." + + def build_prompt(self, layer_info: LayerInfo, module_path: str, update_type: str) -> str: + """Build the prompt for gemini.""" + template_content = self.load_template(layer_info.template_path) + module_name = os.path.basename(module_path) + + if update_type == "full": + update_context = """ + Update Mode: Complete refresh + - Perform comprehensive analysis of all content + - Document patterns, architecture, and purpose + - Consider existing documentation hierarchy + - Follow template guidelines strictly""" + else: + update_context = """ + Update Mode: Context-aware update + - Focus on recent changes and affected areas + - Maintain consistency with existing documentation + - Update only relevant sections + - Follow template guidelines for updated content""" + + base_prompt = f""" + [CRITICAL] RULES - MUST FOLLOW: + 1. ONLY modify CLAUDE.md files at any hierarchy level + 2. NEVER modify source code files + 3. Focus exclusively on updating documentation + 4. Follow the template guidelines exactly + + {template_content} + + {update_context} + + Module Information: + - Name: {module_name} + - Path: {module_path} + - Layer: {layer_info.name} + - Analysis Strategy: {layer_info.analysis_strategy}""" + + return base_prompt + + def execute_gemini_command(self, prompt: str, analysis_strategy: str, module_path: str) -> bool: + """Execute gemini command with the appropriate strategy.""" + original_dir = os.getcwd() + + try: + os.chdir(module_path) + + if analysis_strategy == "--all-files": + cmd = ["gemini", "--all-files", "--yolo", "-p", prompt] + else: + cmd = ["gemini", "--yolo", "-p", f"{analysis_strategy} {prompt}"] + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + return True + else: + print(f" [ERROR] Gemini command failed: {result.stderr}") + return False + + except subprocess.CalledProcessError as e: + print(f" [ERROR] Error executing gemini: {e}") + return False + except FileNotFoundError: + print(f" [ERROR] Gemini command not found. Make sure gemini is installed and in PATH.") + return False + finally: + os.chdir(original_dir) + + def update_module_claude(self, module_path: str, update_type: str = "full") -> bool: + """Main function to update CLAUDE.md for a module.""" + # Validate parameters + if not module_path: + print("[ERROR] Module path is required") + print("Usage: update_module_claude.py [update_type]") + return False + + path_obj = Path(module_path) + if not path_obj.exists() or not path_obj.is_dir(): + print(f"[ERROR] Directory '{module_path}' does not exist") + return False + + # Check if directory has files + files = list(path_obj.glob('*')) + file_count = len([f for f in files if f.is_file()]) + if file_count == 0: + print(f"[SKIP] Skipping '{module_path}' - no files found") + return True + + # Detect layer and get configuration + layer_info = self.detect_layer(module_path) + + print(f"[UPDATE] Updating: {module_path}") + print(f" Layer: {layer_info.name} | Type: {update_type} | Files: {file_count}") + print(f" Template: {os.path.basename(layer_info.template_path)} | Strategy: {layer_info.analysis_strategy}") + + # Build prompt + prompt = self.build_prompt(layer_info, module_path, update_type) + + # Execute update + start_time = time.time() + print(" [PROGRESS] Starting update...") + + success = self.execute_gemini_command(prompt, layer_info.analysis_strategy, module_path) + + if success: + duration = int(time.time() - start_time) + print(f" [OK] Completed in {duration}s") + return True + else: + print(f" [ERROR] Update failed for {module_path}") + return False + +def main(): + """Command-line interface.""" + parser = argparse.ArgumentParser( + description="Update CLAUDE.md for a specific module with automatic layer detection", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""Examples: + python update_module_claude.py . + python update_module_claude.py src/components full + python update_module_claude.py .claude/scripts related""" + ) + + parser.add_argument("module_path", help="Path to the module directory") + parser.add_argument("update_type", nargs="?", choices=["full", "related"], + default="full", help="Update type (default: full)") + parser.add_argument("--home", help="Override home directory path") + parser.add_argument("--dry-run", action="store_true", + help="Show what would be done without executing") + + args = parser.parse_args() + + try: + updater = ModuleClaudeUpdater(args.home) + + if args.dry_run: + layer_info = updater.detect_layer(args.module_path) + prompt = updater.build_prompt(layer_info, args.module_path, args.update_type) + + print("[DRY-RUN] Dry run mode - showing configuration:") + print(f"Module Path: {args.module_path}") + print(f"Update Type: {args.update_type}") + print(f"Layer: {layer_info.name}") + print(f"Template: {layer_info.template_path}") + print(f"Strategy: {layer_info.analysis_strategy}") + print("\nPrompt preview:") + print("-" * 50) + print(prompt[:500] + "..." if len(prompt) > 500 else prompt) + return + + success = updater.update_module_claude(args.module_path, args.update_type) + sys.exit(0 if success else 1) + + except KeyboardInterrupt: + print("\n[ERROR] Operation cancelled by user") + sys.exit(1) + except Exception as e: + print(f"[ERROR] Unexpected error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/python_script/utils/__init__.py b/.claude/python_script/utils/__init__.py new file mode 100644 index 00000000..a7a6fdae --- /dev/null +++ b/.claude/python_script/utils/__init__.py @@ -0,0 +1,16 @@ +""" +Shared utility functions and helpers. +Provides common functionality for colors, caching, and I/O operations. +""" + +from .colors import Colors +from .cache import CacheManager +from .io_helpers import IOHelpers, ensure_directory, safe_read_file + +__all__ = [ + 'Colors', + 'CacheManager', + 'IOHelpers', + 'ensure_directory', + 'safe_read_file' +] \ No newline at end of file diff --git a/.claude/python_script/utils/__pycache__/__init__.cpython-313.pyc b/.claude/python_script/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e1a09431be27250bfae75212d03badde06b406a GIT binary patch literal 517 zcmZ8dJ#X7E5GD2LC~=UzgSiR_oS{I5z{sKuv5eF$vq2m44JyMHM*CS(+<7i;T`ziyF2d3;{@4y?!PsF5qfjY-}2tcerCxNDiELo7v2Kr z9(crYf8pl=1OpEiVID!0#}MZu7!h=Vitr1Dqji+V4>Bnl2D~PHCtA_%$f}oBCzRBL z$(l60-Ez>=~7PsR==1@@s;pwCIgsz%85`QV>lEQzuSY>1pD-|a0Z z#U{!IwPh!n)$=`hMbnerINFOX*j%w{gf^-dvSg%lg{x zv(X>RX*D$EcGopZQeA=AcBT78T~aEf=qR0TcV;x(sJd_YHJsTPcfOuj_~B!We|kO+ ZekTZjctMj_ls%*DR}dY0k1?ED%^yczlKuby literal 0 HcmV?d00001 diff --git a/.claude/python_script/utils/__pycache__/cache.cpython-313.pyc b/.claude/python_script/utils/__pycache__/cache.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30d289044f3454ae95d0784b238d9c512b439d8a GIT binary patch literal 16056 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

?Mit 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 LUW#cX;{SgDELGMa literal 0 HcmV?d00001 diff --git a/.claude/python_script/utils/cache.py b/.claude/python_script/utils/cache.py new file mode 100644 index 00000000..01b9f19a --- /dev/null +++ b/.claude/python_script/utils/cache.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +""" +Cache Management Utility +Provides unified caching functionality for the analyzer system. +""" + +import os +import json +import time +import hashlib +import pickle +import logging +from pathlib import Path +from typing import Any, Optional, Dict, Union +from dataclasses import dataclass, asdict + + +@dataclass +class CacheEntry: + """Cache entry with metadata.""" + value: Any + timestamp: float + ttl: Optional[float] = None + key_hash: Optional[str] = None + + def is_expired(self) -> bool: + """Check if cache entry is expired.""" + if self.ttl is None: + return False + return time.time() - self.timestamp > self.ttl + + def to_dict(self) -> Dict: + """Convert to dictionary for JSON serialization.""" + return { + 'value': self.value, + 'timestamp': self.timestamp, + 'ttl': self.ttl, + 'key_hash': self.key_hash + } + + @classmethod + def from_dict(cls, data: Dict) -> 'CacheEntry': + """Create from dictionary.""" + return cls(**data) + + +class CacheManager: + """Unified cache manager with multiple storage backends.""" + + def __init__(self, cache_dir: str = "cache", default_ttl: int = 3600): + self.cache_dir = Path(cache_dir) + self.cache_dir.mkdir(parents=True, exist_ok=True) + self.default_ttl = default_ttl + self.logger = logging.getLogger(__name__) + + # In-memory cache for fast access + self._memory_cache: Dict[str, CacheEntry] = {} + + # Cache subdirectories + self.json_cache_dir = self.cache_dir / "json" + self.pickle_cache_dir = self.cache_dir / "pickle" + self.temp_cache_dir = self.cache_dir / "temp" + + for cache_subdir in [self.json_cache_dir, self.pickle_cache_dir, self.temp_cache_dir]: + cache_subdir.mkdir(exist_ok=True) + + def _generate_key_hash(self, key: str) -> str: + """Generate a hash for the cache key.""" + return hashlib.md5(key.encode('utf-8')).hexdigest() + + def _get_cache_path(self, key: str, cache_type: str = "json") -> Path: + """Get cache file path for a key.""" + key_hash = self._generate_key_hash(key) + + if cache_type == "json": + return self.json_cache_dir / f"{key_hash}.json" + elif cache_type == "pickle": + return self.pickle_cache_dir / f"{key_hash}.pkl" + elif cache_type == "temp": + return self.temp_cache_dir / f"{key_hash}.tmp" + else: + raise ValueError(f"Unsupported cache type: {cache_type}") + + def set(self, key: str, value: Any, ttl: Optional[int] = None, + storage: str = "memory") -> bool: + """Set a cache value.""" + if ttl is None: + ttl = self.default_ttl + + entry = CacheEntry( + value=value, + timestamp=time.time(), + ttl=ttl, + key_hash=self._generate_key_hash(key) + ) + + try: + if storage == "memory": + self._memory_cache[key] = entry + return True + + elif storage == "json": + cache_path = self._get_cache_path(key, "json") + with open(cache_path, 'w', encoding='utf-8') as f: + json.dump(entry.to_dict(), f, indent=2, default=str) + return True + + elif storage == "pickle": + cache_path = self._get_cache_path(key, "pickle") + with open(cache_path, 'wb') as f: + pickle.dump(entry, f) + return True + + else: + self.logger.warning(f"Unsupported storage type: {storage}") + return False + + except Exception as e: + self.logger.error(f"Failed to set cache for key '{key}': {e}") + return False + + def get(self, key: str, storage: str = "memory", + default: Any = None) -> Any: + """Get a cache value.""" + try: + entry = None + + if storage == "memory": + entry = self._memory_cache.get(key) + + elif storage == "json": + cache_path = self._get_cache_path(key, "json") + if cache_path.exists(): + with open(cache_path, 'r', encoding='utf-8') as f: + data = json.load(f) + entry = CacheEntry.from_dict(data) + + elif storage == "pickle": + cache_path = self._get_cache_path(key, "pickle") + if cache_path.exists(): + with open(cache_path, 'rb') as f: + entry = pickle.load(f) + + else: + self.logger.warning(f"Unsupported storage type: {storage}") + return default + + if entry is None: + return default + + # Check if entry is expired + if entry.is_expired(): + self.delete(key, storage) + return default + + return entry.value + + except Exception as e: + self.logger.error(f"Failed to get cache for key '{key}': {e}") + return default + + def delete(self, key: str, storage: str = "memory") -> bool: + """Delete a cache entry.""" + try: + if storage == "memory": + if key in self._memory_cache: + del self._memory_cache[key] + return True + + elif storage in ["json", "pickle", "temp"]: + cache_path = self._get_cache_path(key, storage) + if cache_path.exists(): + cache_path.unlink() + return True + + else: + self.logger.warning(f"Unsupported storage type: {storage}") + return False + + except Exception as e: + self.logger.error(f"Failed to delete cache for key '{key}': {e}") + return False + + def exists(self, key: str, storage: str = "memory") -> bool: + """Check if a cache entry exists and is not expired.""" + return self.get(key, storage) is not None + + def clear(self, storage: Optional[str] = None) -> bool: + """Clear cache entries.""" + try: + if storage is None or storage == "memory": + self._memory_cache.clear() + + if storage is None or storage == "json": + for cache_file in self.json_cache_dir.glob("*.json"): + cache_file.unlink() + + if storage is None or storage == "pickle": + for cache_file in self.pickle_cache_dir.glob("*.pkl"): + cache_file.unlink() + + if storage is None or storage == "temp": + for cache_file in self.temp_cache_dir.glob("*.tmp"): + cache_file.unlink() + + return True + + except Exception as e: + self.logger.error(f"Failed to clear cache: {e}") + return False + + def cleanup_expired(self) -> int: + """Clean up expired cache entries.""" + cleaned_count = 0 + + try: + # Clean memory cache + expired_keys = [] + for key, entry in self._memory_cache.items(): + if entry.is_expired(): + expired_keys.append(key) + + for key in expired_keys: + del self._memory_cache[key] + cleaned_count += 1 + + # Clean file caches + for cache_type in ["json", "pickle"]: + cache_dir = self.json_cache_dir if cache_type == "json" else self.pickle_cache_dir + extension = f".{cache_type}" if cache_type == "json" else ".pkl" + + for cache_file in cache_dir.glob(f"*{extension}"): + try: + if cache_type == "json": + with open(cache_file, 'r', encoding='utf-8') as f: + data = json.load(f) + entry = CacheEntry.from_dict(data) + else: + with open(cache_file, 'rb') as f: + entry = pickle.load(f) + + if entry.is_expired(): + cache_file.unlink() + cleaned_count += 1 + + except Exception: + # If we can't read the cache file, delete it + cache_file.unlink() + cleaned_count += 1 + + self.logger.info(f"Cleaned up {cleaned_count} expired cache entries") + return cleaned_count + + except Exception as e: + self.logger.error(f"Failed to cleanup expired cache entries: {e}") + return 0 + + def get_stats(self) -> Dict[str, Any]: + """Get cache statistics.""" + stats = { + 'memory_entries': len(self._memory_cache), + 'json_files': len(list(self.json_cache_dir.glob("*.json"))), + 'pickle_files': len(list(self.pickle_cache_dir.glob("*.pkl"))), + 'temp_files': len(list(self.temp_cache_dir.glob("*.tmp"))), + 'cache_dir_size': 0 + } + + # Calculate total cache directory size + try: + for cache_file in self.cache_dir.rglob("*"): + if cache_file.is_file(): + stats['cache_dir_size'] += cache_file.stat().st_size + except Exception: + pass + + return stats + + def set_file_cache(self, key: str, file_path: Union[str, Path], + ttl: Optional[int] = None) -> bool: + """Cache a file by copying it to the cache directory.""" + try: + source_path = Path(file_path) + if not source_path.exists(): + return False + + cache_path = self.temp_cache_dir / f"{self._generate_key_hash(key)}.cached" + + # Copy file to cache + import shutil + shutil.copy2(source_path, cache_path) + + # Store metadata + metadata = { + 'original_path': str(source_path), + 'cached_path': str(cache_path), + 'size': source_path.stat().st_size, + 'timestamp': time.time(), + 'ttl': ttl or self.default_ttl + } + + return self.set(f"{key}_metadata", metadata, ttl, "json") + + except Exception as e: + self.logger.error(f"Failed to cache file '{file_path}': {e}") + return False + + def get_file_cache(self, key: str) -> Optional[Path]: + """Get cached file path.""" + metadata = self.get(f"{key}_metadata", "json") + if metadata is None: + return None + + cached_path = Path(metadata['cached_path']) + if not cached_path.exists(): + # Cache file missing, clean up metadata + self.delete(f"{key}_metadata", "json") + return None + + return cached_path + + +# Global cache manager instance +_global_cache = None + + +def get_cache_manager(cache_dir: str = "cache", default_ttl: int = 3600) -> CacheManager: + """Get global cache manager instance.""" + global _global_cache + if _global_cache is None: + _global_cache = CacheManager(cache_dir, default_ttl) + return _global_cache + + +if __name__ == "__main__": + # Test cache functionality + cache = CacheManager("test_cache") + + # Test memory cache + cache.set("test_key", {"data": "test_value"}, ttl=60) + print(f"Memory cache: {cache.get('test_key')}") + + # Test JSON cache + cache.set("json_key", {"complex": {"data": [1, 2, 3]}}, ttl=60, storage="json") + print(f"JSON cache: {cache.get('json_key', storage='json')}") + + # Test stats + print(f"Cache stats: {cache.get_stats()}") + + # Clean up + cache.clear() \ No newline at end of file diff --git a/.claude/python_script/utils/colors.py b/.claude/python_script/utils/colors.py new file mode 100644 index 00000000..0c0f0d67 --- /dev/null +++ b/.claude/python_script/utils/colors.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +""" +Terminal Colors Utility +Provides ANSI color codes for terminal output formatting. +""" + +import os +import sys +from typing import Optional + + +class Colors: + """ANSI color codes for terminal output.""" + + # Basic colors + RED = '\033[0;31m' + GREEN = '\033[0;32m' + YELLOW = '\033[1;33m' + BLUE = '\033[0;34m' + PURPLE = '\033[0;35m' + CYAN = '\033[0;36m' + WHITE = '\033[0;37m' + BLACK = '\033[0;30m' + + # Bright colors + BRIGHT_RED = '\033[1;31m' + BRIGHT_GREEN = '\033[1;32m' + BRIGHT_YELLOW = '\033[1;33m' + BRIGHT_BLUE = '\033[1;34m' + BRIGHT_PURPLE = '\033[1;35m' + BRIGHT_CYAN = '\033[1;36m' + BRIGHT_WHITE = '\033[1;37m' + + # Background colors + BG_RED = '\033[41m' + BG_GREEN = '\033[42m' + BG_YELLOW = '\033[43m' + BG_BLUE = '\033[44m' + BG_PURPLE = '\033[45m' + BG_CYAN = '\033[46m' + BG_WHITE = '\033[47m' + + # Text formatting + BOLD = '\033[1m' + DIM = '\033[2m' + UNDERLINE = '\033[4m' + BLINK = '\033[5m' + REVERSE = '\033[7m' + STRIKETHROUGH = '\033[9m' + + # Reset + NC = '\033[0m' # No Color / Reset + RESET = '\033[0m' + + @classmethod + def is_tty(cls) -> bool: + """Check if output is a TTY (supports colors).""" + return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() + + @classmethod + def supports_color(cls) -> bool: + """Check if the terminal supports color output.""" + # Check environment variables + if os.getenv('NO_COLOR'): + return False + + if os.getenv('FORCE_COLOR'): + return True + + # Check if output is a TTY + if not cls.is_tty(): + return False + + # Check TERM environment variable + term = os.getenv('TERM', '').lower() + if 'color' in term or term in ('xterm', 'xterm-256color', 'screen', 'tmux'): + return True + + # Windows Terminal detection + if os.name == 'nt': + # Windows 10 version 1511 and later support ANSI colors + try: + import subprocess + result = subprocess.run(['ver'], capture_output=True, text=True, shell=True) + if result.returncode == 0: + version_info = result.stdout + # Extract Windows version (simplified check) + if 'Windows' in version_info: + return True + except Exception: + pass + + return False + + @classmethod + def colorize(cls, text: str, color: str, bold: bool = False) -> str: + """Apply color to text if colors are supported.""" + if not cls.supports_color(): + return text + + prefix = color + if bold: + prefix = cls.BOLD + prefix + + return f"{prefix}{text}{cls.RESET}" + + @classmethod + def red(cls, text: str, bold: bool = False) -> str: + """Color text red.""" + return cls.colorize(text, cls.RED, bold) + + @classmethod + def green(cls, text: str, bold: bool = False) -> str: + """Color text green.""" + return cls.colorize(text, cls.GREEN, bold) + + @classmethod + def yellow(cls, text: str, bold: bool = False) -> str: + """Color text yellow.""" + return cls.colorize(text, cls.YELLOW, bold) + + @classmethod + def blue(cls, text: str, bold: bool = False) -> str: + """Color text blue.""" + return cls.colorize(text, cls.BLUE, bold) + + @classmethod + def purple(cls, text: str, bold: bool = False) -> str: + """Color text purple.""" + return cls.colorize(text, cls.PURPLE, bold) + + @classmethod + def cyan(cls, text: str, bold: bool = False) -> str: + """Color text cyan.""" + return cls.colorize(text, cls.CYAN, bold) + + @classmethod + def bold(cls, text: str) -> str: + """Make text bold.""" + return cls.colorize(text, '', True) + + @classmethod + def dim(cls, text: str) -> str: + """Make text dim.""" + return cls.colorize(text, cls.DIM) + + @classmethod + def underline(cls, text: str) -> str: + """Underline text.""" + return cls.colorize(text, cls.UNDERLINE) + + @classmethod + def success(cls, text: str) -> str: + """Format success message (green).""" + return cls.green(f"[SUCCESS] {text}", bold=True) + + @classmethod + def error(cls, text: str) -> str: + """Format error message (red).""" + return cls.red(f"[ERROR] {text}", bold=True) + + @classmethod + def warning(cls, text: str) -> str: + """Format warning message (yellow).""" + return cls.yellow(f"[WARNING] {text}", bold=True) + + @classmethod + def info(cls, text: str) -> str: + """Format info message (blue).""" + return cls.blue(f"[INFO] {text}") + + @classmethod + def highlight(cls, text: str) -> str: + """Highlight text (cyan background).""" + if not cls.supports_color(): + return f"[{text}]" + return f"{cls.BG_CYAN}{cls.BLACK}{text}{cls.RESET}" + + @classmethod + def strip_colors(cls, text: str) -> str: + """Remove ANSI color codes from text.""" + import re + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + return ansi_escape.sub('', text) + + +# Convenience functions for common usage +def colorize(text: str, color: str) -> str: + """Convenience function to colorize text.""" + return Colors.colorize(text, color) + + +def red(text: str) -> str: + """Red text.""" + return Colors.red(text) + + +def green(text: str) -> str: + """Green text.""" + return Colors.green(text) + + +def yellow(text: str) -> str: + """Yellow text.""" + return Colors.yellow(text) + + +def blue(text: str) -> str: + """Blue text.""" + return Colors.blue(text) + + +def success(text: str) -> str: + """Success message.""" + return Colors.success(text) + + +def error(text: str) -> str: + """Error message.""" + return Colors.error(text) + + +def warning(text: str) -> str: + """Warning message.""" + return Colors.warning(text) + + +def info(text: str) -> str: + """Info message.""" + return Colors.info(text) + + +if __name__ == "__main__": + # Test color output + print(Colors.red("Red text")) + print(Colors.green("Green text")) + print(Colors.yellow("Yellow text")) + print(Colors.blue("Blue text")) + print(Colors.purple("Purple text")) + print(Colors.cyan("Cyan text")) + print(Colors.bold("Bold text")) + print(Colors.success("Success message")) + print(Colors.error("Error message")) + print(Colors.warning("Warning message")) + print(Colors.info("Info message")) + print(Colors.highlight("Highlighted text")) + print(f"Color support: {Colors.supports_color()}") + print(f"TTY: {Colors.is_tty()}") \ No newline at end of file diff --git a/.claude/python_script/utils/io_helpers.py b/.claude/python_script/utils/io_helpers.py new file mode 100644 index 00000000..8a86a887 --- /dev/null +++ b/.claude/python_script/utils/io_helpers.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +""" +I/O Helper Functions +Provides common file and directory operations with error handling. +""" + +import os +import json +import yaml +import logging +from pathlib import Path +from typing import Any, Optional, Union, List, Dict +import shutil +import tempfile + + +class IOHelpers: + """Collection of I/O helper methods.""" + + @staticmethod + def ensure_directory(path: Union[str, Path], mode: int = 0o755) -> bool: + """Ensure directory exists, create if necessary.""" + try: + dir_path = Path(path) + dir_path.mkdir(parents=True, exist_ok=True, mode=mode) + return True + except (PermissionError, OSError) as e: + logging.error(f"Failed to create directory '{path}': {e}") + return False + + @staticmethod + def safe_read_file(file_path: Union[str, Path], encoding: str = 'utf-8', + fallback_encoding: str = 'latin-1') -> Optional[str]: + """Safely read file content with encoding fallback.""" + path = Path(file_path) + if not path.exists(): + return None + + encodings = [encoding, fallback_encoding] if encoding != fallback_encoding else [encoding] + + for enc in encodings: + try: + with open(path, 'r', encoding=enc) as f: + return f.read() + except UnicodeDecodeError: + continue + except (IOError, OSError) as e: + logging.error(f"Failed to read file '{file_path}': {e}") + return None + + logging.warning(f"Failed to decode file '{file_path}' with any encoding") + return None + + @staticmethod + def safe_write_file(file_path: Union[str, Path], content: str, + encoding: str = 'utf-8', backup: bool = False) -> bool: + """Safely write content to file with optional backup.""" + path = Path(file_path) + + try: + # Create backup if requested and file exists + if backup and path.exists(): + backup_path = path.with_suffix(path.suffix + '.bak') + shutil.copy2(path, backup_path) + + # Ensure parent directory exists + if not IOHelpers.ensure_directory(path.parent): + return False + + # Write to temporary file first, then move to final location + with tempfile.NamedTemporaryFile(mode='w', encoding=encoding, + dir=path.parent, delete=False) as tmp_file: + tmp_file.write(content) + tmp_path = Path(tmp_file.name) + + # Atomic move + shutil.move(str(tmp_path), str(path)) + return True + + except (IOError, OSError) as e: + logging.error(f"Failed to write file '{file_path}': {e}") + return False + + @staticmethod + def read_json(file_path: Union[str, Path], default: Any = None) -> Any: + """Read JSON file with error handling.""" + content = IOHelpers.safe_read_file(file_path) + if content is None: + return default + + try: + return json.loads(content) + except json.JSONDecodeError as e: + logging.error(f"Failed to parse JSON from '{file_path}': {e}") + return default + + @staticmethod + def write_json(file_path: Union[str, Path], data: Any, + indent: int = 2, backup: bool = False) -> bool: + """Write data to JSON file.""" + try: + content = json.dumps(data, indent=indent, ensure_ascii=False, default=str) + return IOHelpers.safe_write_file(file_path, content, backup=backup) + except (TypeError, ValueError) as e: + logging.error(f"Failed to serialize data to JSON for '{file_path}': {e}") + return False + + @staticmethod + def read_yaml(file_path: Union[str, Path], default: Any = None) -> Any: + """Read YAML file with error handling.""" + content = IOHelpers.safe_read_file(file_path) + if content is None: + return default + + try: + return yaml.safe_load(content) + except yaml.YAMLError as e: + logging.error(f"Failed to parse YAML from '{file_path}': {e}") + return default + + @staticmethod + def write_yaml(file_path: Union[str, Path], data: Any, backup: bool = False) -> bool: + """Write data to YAML file.""" + try: + content = yaml.dump(data, default_flow_style=False, allow_unicode=True) + return IOHelpers.safe_write_file(file_path, content, backup=backup) + except yaml.YAMLError as e: + logging.error(f"Failed to serialize data to YAML for '{file_path}': {e}") + return False + + @staticmethod + def find_files(directory: Union[str, Path], pattern: str = "*", + recursive: bool = True, max_depth: Optional[int] = None) -> List[Path]: + """Find files matching pattern in directory.""" + dir_path = Path(directory) + if not dir_path.exists() or not dir_path.is_dir(): + return [] + + files = [] + try: + if recursive: + if max_depth is not None: + # Implement depth-limited search + def search_with_depth(path: Path, current_depth: int = 0): + if current_depth > max_depth: + return + + for item in path.iterdir(): + if item.is_file() and item.match(pattern): + files.append(item) + elif item.is_dir() and current_depth < max_depth: + search_with_depth(item, current_depth + 1) + + search_with_depth(dir_path) + else: + files = list(dir_path.rglob(pattern)) + else: + files = list(dir_path.glob(pattern)) + + return sorted(files) + + except (PermissionError, OSError) as e: + logging.error(f"Failed to search files in '{directory}': {e}") + return [] + + @staticmethod + def get_file_stats(file_path: Union[str, Path]) -> Optional[Dict[str, Any]]: + """Get file statistics.""" + path = Path(file_path) + if not path.exists(): + return None + + try: + stat = path.stat() + return { + 'size': stat.st_size, + 'modified_time': stat.st_mtime, + 'created_time': stat.st_ctime, + 'is_file': path.is_file(), + 'is_dir': path.is_dir(), + 'permissions': oct(stat.st_mode)[-3:], + 'extension': path.suffix.lower(), + 'name': path.name, + 'parent': str(path.parent) + } + except (OSError, PermissionError) as e: + logging.error(f"Failed to get stats for '{file_path}': {e}") + return None + + @staticmethod + def copy_with_backup(source: Union[str, Path], dest: Union[str, Path]) -> bool: + """Copy file with automatic backup if destination exists.""" + source_path = Path(source) + dest_path = Path(dest) + + if not source_path.exists(): + logging.error(f"Source file '{source}' does not exist") + return False + + try: + # Create backup if destination exists + if dest_path.exists(): + backup_path = dest_path.with_suffix(dest_path.suffix + '.bak') + shutil.copy2(dest_path, backup_path) + logging.info(f"Created backup: {backup_path}") + + # Ensure destination directory exists + if not IOHelpers.ensure_directory(dest_path.parent): + return False + + # Copy file + shutil.copy2(source_path, dest_path) + return True + + except (IOError, OSError) as e: + logging.error(f"Failed to copy '{source}' to '{dest}': {e}") + return False + + @staticmethod + def move_with_backup(source: Union[str, Path], dest: Union[str, Path]) -> bool: + """Move file with automatic backup if destination exists.""" + source_path = Path(source) + dest_path = Path(dest) + + if not source_path.exists(): + logging.error(f"Source file '{source}' does not exist") + return False + + try: + # Create backup if destination exists + if dest_path.exists(): + backup_path = dest_path.with_suffix(dest_path.suffix + '.bak') + shutil.move(str(dest_path), str(backup_path)) + logging.info(f"Created backup: {backup_path}") + + # Ensure destination directory exists + if not IOHelpers.ensure_directory(dest_path.parent): + return False + + # Move file + shutil.move(str(source_path), str(dest_path)) + return True + + except (IOError, OSError) as e: + logging.error(f"Failed to move '{source}' to '{dest}': {e}") + return False + + @staticmethod + def clean_temp_files(directory: Union[str, Path], extensions: List[str] = None, + max_age_hours: int = 24) -> int: + """Clean temporary files older than specified age.""" + if extensions is None: + extensions = ['.tmp', '.temp', '.bak', '.swp', '.~'] + + dir_path = Path(directory) + if not dir_path.exists(): + return 0 + + import time + cutoff_time = time.time() - (max_age_hours * 3600) + cleaned_count = 0 + + try: + for file_path in dir_path.rglob('*'): + if file_path.is_file(): + # Check extension + if file_path.suffix.lower() in extensions: + # Check age + if file_path.stat().st_mtime < cutoff_time: + try: + file_path.unlink() + cleaned_count += 1 + except OSError: + continue + + logging.info(f"Cleaned {cleaned_count} temporary files from '{directory}'") + return cleaned_count + + except (PermissionError, OSError) as e: + logging.error(f"Failed to clean temp files in '{directory}': {e}") + return 0 + + @staticmethod + def get_directory_size(directory: Union[str, Path]) -> int: + """Get total size of directory in bytes.""" + dir_path = Path(directory) + if not dir_path.exists() or not dir_path.is_dir(): + return 0 + + total_size = 0 + try: + for file_path in dir_path.rglob('*'): + if file_path.is_file(): + total_size += file_path.stat().st_size + except (PermissionError, OSError): + pass + + return total_size + + @staticmethod + def make_executable(file_path: Union[str, Path]) -> bool: + """Make file executable (Unix/Linux/Mac).""" + if os.name == 'nt': # Windows + return True # Windows doesn't use Unix permissions + + try: + path = Path(file_path) + current_mode = path.stat().st_mode + path.chmod(current_mode | 0o111) # Add execute permission + return True + except (OSError, PermissionError) as e: + logging.error(f"Failed to make '{file_path}' executable: {e}") + return False + + +# Convenience functions +def ensure_directory(path: Union[str, Path]) -> bool: + """Ensure directory exists.""" + return IOHelpers.ensure_directory(path) + + +def safe_read_file(file_path: Union[str, Path]) -> Optional[str]: + """Safely read file content.""" + return IOHelpers.safe_read_file(file_path) + + +def safe_write_file(file_path: Union[str, Path], content: str) -> bool: + """Safely write content to file.""" + return IOHelpers.safe_write_file(file_path, content) + + +def read_json(file_path: Union[str, Path], default: Any = None) -> Any: + """Read JSON file.""" + return IOHelpers.read_json(file_path, default) + + +def write_json(file_path: Union[str, Path], data: Any) -> bool: + """Write data to JSON file.""" + return IOHelpers.write_json(file_path, data) + + +def read_yaml(file_path: Union[str, Path], default: Any = None) -> Any: + """Read YAML file.""" + return IOHelpers.read_yaml(file_path, default) + + +def write_yaml(file_path: Union[str, Path], data: Any) -> bool: + """Write data to YAML file.""" + return IOHelpers.write_yaml(file_path, data) + + +if __name__ == "__main__": + # Test I/O operations + test_dir = Path("test_io") + + # Test directory creation + print(f"Create directory: {ensure_directory(test_dir)}") + + # Test file operations + test_file = test_dir / "test.txt" + content = "Hello, World!\nThis is a test file." + + print(f"Write file: {safe_write_file(test_file, content)}") + print(f"Read file: {safe_read_file(test_file)}") + + # Test JSON operations + json_file = test_dir / "test.json" + json_data = {"name": "test", "numbers": [1, 2, 3], "nested": {"key": "value"}} + + print(f"Write JSON: {write_json(json_file, json_data)}") + print(f"Read JSON: {read_json(json_file)}") + + # Test file stats + stats = IOHelpers.get_file_stats(test_file) + print(f"File stats: {stats}") + + # Cleanup + shutil.rmtree(test_dir, ignore_errors=True) \ No newline at end of file diff --git a/.claude/workflows/ANALYSIS_RESULTS.md b/.claude/workflows/ANALYSIS_RESULTS.md new file mode 100644 index 00000000..a04ae3db --- /dev/null +++ b/.claude/workflows/ANALYSIS_RESULTS.md @@ -0,0 +1,143 @@ +# Analysis Results Documentation + +## Metadata +- **Generated by**: `/workflow:plan` command +- **Session**: `WFS-[session-id]` +- **Task Context**: `[task-description]` +- **Analysis Date**: `[timestamp]` + +## 1. Verified Project Assets + +### Confirmed Documentation Files +```bash +# Verified file existence with full paths: +βœ“ [/absolute/path/to/CLAUDE.md] - [file size] - Contains: [key sections found] +βœ“ [/absolute/path/to/README.md] - [file size] - Contains: [technical info] +βœ“ [/absolute/path/to/package.json] - [file size] - Dependencies: [list] +``` + +### Confirmed Technical Stack +- **Package Manager**: [npm/yarn/pnpm] (confirmed via `[specific file path]`) +- **Framework**: [React/Vue/Angular/etc] (version: [x.x.x]) +- **Build Tool**: [webpack/vite/etc] (config: `[config file path]`) +- **Test Framework**: [jest/vitest/etc] (config: `[config file path]`) + +## 2. Verified Code Structure + +### Confirmed Directory Structure +``` +[project-root]/ +β”œβ”€β”€ [actual-folder-name]/ # [purpose - verified] +β”‚ β”œβ”€β”€ [actual-file.ext] # [size] [last-modified] +β”‚ └── [actual-file.ext] # [size] [last-modified] +└── [actual-folder-name]/ # [purpose - verified] + β”œβ”€β”€ [actual-file.ext] # [size] [last-modified] + └── [actual-file.ext] # [size] [last-modified] +``` + +### Confirmed Key Modules +- **Module 1**: `[/absolute/path/to/module]` + - **Entry Point**: `[actual-file.js]` (exports: [verified-exports]) + - **Key Methods**: `[method1()]`, `[method2()]` (line numbers: [X-Y]) + - **Dependencies**: `[import statements verified]` + +- **Module 2**: `[/absolute/path/to/module]` + - **Entry Point**: `[actual-file.js]` (exports: [verified-exports]) + - **Key Methods**: `[method1()]`, `[method2()]` (line numbers: [X-Y]) + - **Dependencies**: `[import statements verified]` + +## 3. Confirmed Implementation Standards + +### Verified Coding Patterns +- **Naming Convention**: [verified pattern from actual files] + - Files: `[example1.js]`, `[example2.js]` (pattern: [pattern]) + - Functions: `[actualFunction()]` from `[file:line]` + - Classes: `[ActualClass]` from `[file:line]` + +### Confirmed Build Commands +```bash +# Verified commands (tested successfully): +βœ“ [npm run build] - Output: [build result] +βœ“ [npm run test] - Framework: [test framework found] +βœ“ [npm run lint] - Tool: [linter found] +``` + +## 4. Task Decomposition Results + +### Task Count Determination +- **Identified Tasks**: [exact number] (based on functional boundaries) +- **Structure**: [Flat ≀5 | Hierarchical 6-10 | Re-scope >10] +- **Merge Rationale**: [specific reasons for combining related files] + +### Confirmed Task Breakdown +- **IMPL-001**: `[Specific functional description]` + - **Target Files**: `[/absolute/path/file1.js]`, `[/absolute/path/file2.js]` (verified) + - **Key Methods to Implement**: `[method1()]`, `[method2()]` (signatures defined) + - **Size**: [X files, ~Y lines] (measured from similar existing code) + - **Dependencies**: Uses `[existingModule.method()]` from `[verified-path]` + +- **IMPL-002**: `[Specific functional description]` + - **Target Files**: `[/absolute/path/file3.js]`, `[/absolute/path/file4.js]` (verified) + - **Key Methods to Implement**: `[method3()]`, `[method4()]` (signatures defined) + - **Size**: [X files, ~Y lines] (measured from similar existing code) + - **Dependencies**: Uses `[existingModule.method()]` from `[verified-path]` + +### Verified Dependency Chain +```bash +# Confirmed execution order (based on actual imports): +IMPL-001 β†’ Uses: [existing-file:method] +IMPL-002 β†’ Depends: IMPL-001.[method] β†’ Uses: [existing-file:method] +``` + +## 5. Implementation Execution Plan + +### Confirmed Integration Points +- **Existing Entry Points**: + - `[actual-file.js:line]` exports `[verified-method]` + - `[actual-config.json]` contains `[verified-setting]` +- **Integration Methods**: + - Hook into `[existing-method()]` at `[file:line]` + - Extend `[ExistingClass]` from `[file:line]` + +### Validated Commands +```bash +# Commands verified to work in current environment: +βœ“ [exact build command] - Tested: [timestamp] +βœ“ [exact test command] - Tested: [timestamp] +βœ“ [exact lint command] - Tested: [timestamp] +``` + +## 6. Success Validation Criteria + +### Testable Outcomes +- **IMPL-001 Success**: + - `[specific test command]` passes + - `[integration point]` correctly calls `[new method]` + - No regression in `[existing test suite]` + +- **IMPL-002 Success**: + - `[specific test command]` passes + - Feature accessible via `[verified UI path]` + - Performance: `[measurable criteria]` + +### Quality Gates +- **Code Standards**: Must pass `[verified lint command]` +- **Test Coverage**: Maintain `[current coverage %]` (measured by `[tool]`) +- **Build**: Must complete `[verified build command]` without errors + +--- + +## Template Instructions + +**CRITICAL**: Every bracketed item MUST be filled with verified, existing information: +- File paths must be confirmed with `ls` or `find` +- Method names must be found in actual source code +- Commands must be tested and work +- Line numbers should reference actual code locations +- Dependencies must trace to real imports/requires + +**Verification Required Before Use**: +1. All file paths exist and are readable +2. All referenced methods/classes exist in specified locations +3. All commands execute successfully +4. All integration points are actual, not assumed \ No newline at end of file diff --git a/.claude/workflows/agent-orchestration-patterns.md b/.claude/workflows/agent-orchestration-patterns.md deleted file mode 100644 index cc13603b..00000000 --- a/.claude/workflows/agent-orchestration-patterns.md +++ /dev/null @@ -1,160 +0,0 @@ -# Agent Orchestration Patterns - -## Core Agent Coordination Features - -- **Gemini Context Analysis**: MANDATORY context gathering before any agent execution -- **Context-Driven Coordination**: Agents work with comprehensive codebase understanding -- **Dynamic Agent Selection**: Choose agents based on discovered context and patterns -- **Continuous Context Updates**: Refine understanding throughout agent execution -- **Cross-Agent Context Sharing**: Maintain shared context state across all agents -- **Pattern-Aware Execution**: Leverage discovered patterns for optimal implementation -- **Quality Gates**: Each Agent validates input and ensures output standards -- **Error Recovery**: Graceful handling of Agent coordination failures - -## Workflow Implementation Patterns - -### Simple Workflow Pattern -```pseudocode -Flow: Gemini Context Analysis β†’ TodoWrite Creation β†’ Context-Aware Implementation β†’ Review - -1. MANDATORY Gemini Context Analysis: - - Analyze target files and immediate dependencies - - Discover existing patterns and conventions - - Identify utilities and libraries to use - - Generate context package for agents - -2. TodoWrite Creation (Context-informed): - - "Execute Gemini context analysis" - - "Implement solution following discovered patterns" - - "Review against codebase standards" - - "Complete task with context validation" - -3. Context-Aware Implementation: - Task(code-developer): Implementation with Gemini context package - Input: CONTEXT_PACKAGE, PATTERNS_DISCOVERED, CONVENTIONS_IDENTIFIED - Output: SUMMARY, FILES_MODIFIED, TESTS, VERIFICATION - -4. Context-Aware Review: - Task(code-review-agent): Review with codebase standards context - Input: CONTEXT_PACKAGE, IMPLEMENTATION_RESULTS - Output: STATUS, SCORE, ISSUES, RECOMMENDATIONS - -Resume Support: Load todos + full context state from checkpoint -``` - -### Medium Workflow Pattern -```pseudocode -Flow: Comprehensive Gemini Analysis β†’ TodoWrite β†’ Multi-Context Implementation β†’ Review - -1. MANDATORY Comprehensive Gemini Context Analysis: - - Analyze feature area and related components - - Discover cross-file patterns and architectural decisions - - Identify integration points and dependencies - - Generate comprehensive context packages for multiple agents - -2. TodoWrite Creation (Context-driven, 5-7 todos): - - "Execute comprehensive Gemini context analysis" - - "Coordinate multi-agent implementation with shared context" - - "Implement following discovered architectural patterns" - - "Validate against existing system patterns", "Review", "Complete" - -3. Multi-Context Implementation: - Task(code-developer): Implementation with comprehensive context - Input: CONTEXT_PACKAGES, ARCHITECTURAL_PATTERNS, INTEGRATION_POINTS - Update context as new patterns discovered - -4. Context-Aware Review: - Task(code-review-agent): Comprehensive review with system context - Input: FULL_CONTEXT_STATE, IMPLEMENTATION_RESULTS, PATTERN_COMPLIANCE - Verify against discovered architectural patterns - -Resume Support: Full context state + pattern discovery restoration -``` - -### Complex Workflow Pattern -```pseudocode -Flow: Deep Gemini Analysis β†’ TodoWrite β†’ Orchestrated Multi-Agent β†’ Review β†’ Iterate (max 2) - -1. MANDATORY Deep Gemini Context Analysis: - - System-wide architectural understanding - - Deep pattern analysis across entire codebase - - Integration complexity assessment - - Multi-agent coordination requirements discovery - - Risk pattern identification - -2. TodoWrite Creation (Context-orchestrated, 7-10 todos): - - "Execute deep system-wide Gemini analysis" - - "Orchestrate multi-agent coordination with shared context" - - "Implement with continuous context refinement" - - "Validate against system architectural patterns", "Review", "Iterate", "Complete" - -3. Orchestrated Multi-Agent Implementation: - Multiple specialized agents with shared deep context - Input: SYSTEM_CONTEXT, ARCHITECTURAL_PATTERNS, RISK_ASSESSMENT - Continuous Gemini context updates throughout execution - Cross-agent context synchronization - -4. Deep Context Review & Iteration Loop (max 2 iterations): - Task(code-review-agent): Production-ready review with full system context - If CRITICAL_ISSUES found: Re-analyze context and coordinate fixes - Continue until no critical issues or max iterations reached - -Context Validation: Verify deep context analysis maintained throughout -Resume Support: Full context state + iteration tracking + cross-agent coordination -``` - -## Workflow Characteristics by Pattern - -| Pattern | Context Analysis | Agent Coordination | Iteration Strategy | -|---------|------------------|--------------------|--------------------| -| **Complex** | Deep system-wide Gemini analysis | Multi-agent orchestration with shared context | Multiple rounds with context refinement | -| **Medium** | Comprehensive multi-file analysis | Context-driven coordination | Single thorough pass with pattern validation | -| **Simple** | Focused file-level analysis | Direct context-aware execution | Quick context validation | - -## Context-Driven Task Invocation Examples - -```bash -# Gemini Context Analysis (Always First) -gemini "Analyze authentication patterns in codebase - identify existing implementations, - conventions, utilities, and integration patterns" - -# Context-Aware Research Task -Task(subagent_type="general-purpose", - prompt="Research authentication patterns in codebase", - context="[GEMINI_CONTEXT_PACKAGE]") - -# Context-Informed Implementation Task -Task(subagent_type="code-developer", - prompt="Implement email validation function following discovered patterns", - context="PATTERNS: [pattern_list], UTILITIES: [util_list], CONVENTIONS: [conv_list]") - -# Context-Driven Review Task -Task(subagent_type="code-review-agent", - prompt="Review authentication service against codebase standards and patterns", - context="STANDARDS: [discovered_standards], PATTERNS: [existing_patterns]") - -# Cross-Agent Context Sharing -Task(subagent_type="code-developer", - prompt="Coordinate with previous agent results using shared context", - context="PREVIOUS_CONTEXT: [agent_context], SHARED_STATE: [context_state]") -``` - -## Gemini Context Integration Points - -### Pre-Agent Context Gathering -```bash -# Always execute before agent coordination -gemini "Comprehensive analysis for [task] - discover patterns, conventions, and optimal approach" -``` - -### During-Agent Context Updates -```bash -# Continuous context refinement -gemini "Update context understanding based on agent discoveries in [area]" -``` - -### Cross-Agent Context Synchronization -```bash -# Ensure context consistency across agents -gemini "Synchronize context between [agent1] and [agent2] work on [feature]" -``` \ No newline at end of file diff --git a/.claude/workflows/workflow-architecture.md b/.claude/workflows/workflow-architecture.md index 1a48f3ed..bb5777ce 100644 --- a/.claude/workflows/workflow-architecture.md +++ b/.claude/workflows/workflow-architecture.md @@ -245,6 +245,8 @@ All workflows use the same file structure definition regardless of complexity. * β”œβ”€β”€ [.chat/] # CLI interaction sessions (created when analysis is run) β”‚ β”œβ”€β”€ chat-*.md # Saved chat sessions β”‚ └── analysis-*.md # Analysis results +β”œβ”€β”€ [.process/] # Planning analysis results (created by /workflow:plan) +β”‚ └── ANALYSIS_RESULTS.md # Analysis results and planning artifacts β”œβ”€β”€ IMPL_PLAN.md # Planning document (REQUIRED) β”œβ”€β”€ TODO_LIST.md # Progress tracking (REQUIRED) β”œβ”€β”€ [.summaries/] # Task completion summaries (created when tasks complete)