moai-adk 0.3.9__py3-none-any.whl → 0.3.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of moai-adk might be problematic. Click here for more details.

@@ -134,45 +134,20 @@ def init(
134
134
 
135
135
  console.print("\n[cyan]🚀 Starting installation...[/cyan]\n")
136
136
 
137
- # 4. Check for reinitialization (SPEC-INIT-003 v0.3.0)
137
+ # 4. Check for reinitialization (SPEC-INIT-003 v0.3.0) - DEFAULT TO FORCE MODE
138
138
  initializer = ProjectInitializer(project_path)
139
139
 
140
140
  if initializer.is_initialized():
141
- if non_interactive and not force:
142
- # Non-interactive mode (without force): Reject reinitialization
143
- console.print("\n[yellow] Project already initialized[/yellow]")
144
- console.print(f"[dim] Location: {project_path}/.moai/[/dim]")
145
- console.print("[dim] Use --force to reinitialize or interactive mode[/dim]\n")
146
- raise click.Abort()
147
-
148
- if force:
149
- # Force mode: Reinitialize without confirmation
150
- console.print("\n[green]🔄 Force reinitializing project...[/green]\n")
141
+ # Always reinitialize without confirmation (force mode by default)
142
+ if non_interactive:
143
+ console.print("\n[green]🔄 Reinitializing project (force mode)...[/green]\n")
151
144
  else:
152
- # Interactive mode: Reinitialization prompt
153
- console.print("\n⚠️ [yellow]Project already initialized[/yellow]")
154
- console.print(f" Location: {project_path}/.moai/\n")
155
-
156
- console.print("[cyan]This will:[/cyan]")
157
- console.print(" ✓ Backup existing files to .moai-backups/{timestamp}/")
158
- console.print(" • CLAUDE.md")
159
- console.print(" • .claude/ (settings, commands, hooks)")
160
- console.print(" • .moai/ (all configurations and specs)")
161
- console.print(" ✓ Update template files from moai-adk v0.3.0")
162
- console.print(" • .claude/ → Latest Alfred commands")
163
- console.print(" • .moai/memory/ → Latest development guides")
164
- console.print(" • CLAUDE.md → Latest project documentation")
165
- console.print(" ✓ Preserve your content")
166
- console.print(" • .moai/project/ (product/structure/tech.md)")
167
- console.print(" • .moai/specs/ (all SPEC documents)\n")
168
-
169
- if not Confirm.ask("Would you like to update the project templates?", default=True):
170
- console.print("\n[yellow]Reinit cancelled.[/yellow]\n")
171
- raise click.Abort()
172
-
173
- console.print("\n[green]🔄 Starting reinit process...[/green]\n")
145
+ # Interactive mode: Simple notification
146
+ console.print("\n[cyan]🔄 Reinitializing project...[/cyan]")
147
+ console.print(" Backup will be created at .moai-backups/{timestamp}/\n")
174
148
 
175
149
  # 5. Initialize project (Progress Bar with 5 phases)
150
+ # Always allow reinit (force mode by default)
176
151
  is_reinit = initializer.is_initialized()
177
152
 
178
153
  # Reinit mode: set config.json optimized to false (v0.3.1+)
@@ -220,7 +195,7 @@ def init(
220
195
  language=language,
221
196
  backup_enabled=True,
222
197
  progress_callback=callback,
223
- reinit=is_reinit, # SPEC-INIT-003 v0.3.0
198
+ reinit=True, # Always allow reinit (force mode by default)
224
199
  )
225
200
 
226
201
  # 6. Output results
@@ -241,7 +216,25 @@ def init(
241
216
  f" [dim]📄 Files:[/dim] {len(result.created_files)} created"
242
217
  )
243
218
  console.print(f" [dim]⏱️ Duration:[/dim] {result.duration}ms")
219
+
220
+ # Show backup info if reinitialized
221
+ if is_reinit:
222
+ backup_dir = project_path / ".moai-backups"
223
+ if backup_dir.exists():
224
+ latest_backup = max(backup_dir.iterdir(), key=lambda p: p.stat().st_mtime)
225
+ console.print(f" [dim]💾 Backup:[/dim] {latest_backup.name}/")
226
+
244
227
  console.print(f"\n{separator}")
228
+
229
+ # Show config merge notice if reinitialized
230
+ if is_reinit:
231
+ console.print("\n[yellow]⚠️ Configuration Notice:[/yellow]")
232
+ console.print(" All template files have been [bold]force overwritten[/bold]")
233
+ console.print(" Previous files are backed up in [cyan].moai-backups/{timestamp}/[/cyan]")
234
+ console.print("\n [cyan]To merge your previous config:[/cyan]")
235
+ console.print(" Run [bold]/alfred:0-project[/bold] command in Claude Code")
236
+ console.print(" It will merge backup config when [dim]optimized=false[/dim]\n")
237
+
245
238
  console.print("\n[cyan]🚀 Next Steps:[/cyan]")
246
239
  if not is_current_dir:
247
240
  console.print(
@@ -23,7 +23,8 @@ class TemplateProcessor:
23
23
  ".moai/specs/", # User SPEC documents
24
24
  ".moai/reports/", # User reports
25
25
  ".moai/project/", # User project documents (product/structure/tech.md)
26
- ".moai/config.json", # User configuration (merged via /alfred:9-update flow)
26
+ # config.json is now FORCE OVERWRITTEN (backup in .moai-backups/)
27
+ # Merge via /alfred:0-project when optimized=false
27
28
  ]
28
29
 
29
30
  # Paths excluded from backups
@@ -277,7 +278,7 @@ class TemplateProcessor:
277
278
  if not silent:
278
279
  console.print(f" ✅ .claude/{folder}/ overwritten")
279
280
 
280
- # 2. Copy other files/folders individually (preserve existing, with substitution)
281
+ # 2. Copy other files/folders individually (FORCE OVERWRITE all files)
281
282
  all_warnings = []
282
283
  for item in src.iterdir():
283
284
  rel_path = item.relative_to(src)
@@ -288,16 +289,12 @@ class TemplateProcessor:
288
289
  continue
289
290
 
290
291
  if item.is_file():
291
- # Copy file, skip if exists (preserve user modifications)
292
- if not dst_item.exists():
293
- # Copy with variable substitution for text files
294
- warnings = self._copy_file_with_substitution(item, dst_item)
295
- all_warnings.extend(warnings)
292
+ # FORCE OVERWRITE: Always copy files (no skip)
293
+ warnings = self._copy_file_with_substitution(item, dst_item)
294
+ all_warnings.extend(warnings)
296
295
  elif item.is_dir():
297
- # Copy directory recursively (preserve existing files)
298
- if not dst_item.exists():
299
- # Recursively copy directory with substitution
300
- self._copy_dir_with_substitution(item, dst_item)
296
+ # FORCE OVERWRITE: Always copy directories (no skip)
297
+ self._copy_dir_with_substitution(item, dst_item)
301
298
 
302
299
  # Print warnings if any
303
300
  if all_warnings and not silent:
@@ -362,9 +359,7 @@ class TemplateProcessor:
362
359
 
363
360
  dst_item = dst / rel_path
364
361
  if item.is_file():
365
- # Skip existing files to preserve user content (v0.3.0)
366
- if dst_item.exists():
367
- continue
362
+ # FORCE OVERWRITE: Always copy files (no skip)
368
363
  dst_item.parent.mkdir(parents=True, exist_ok=True)
369
364
  # Copy with variable substitution
370
365
  warnings = self._copy_file_with_substitution(item, dst_item)
@@ -382,7 +377,7 @@ class TemplateProcessor:
382
377
  console.print(" ✅ .moai/ copy complete (variables substituted)")
383
378
 
384
379
  def _copy_claude_md(self, silent: bool = False) -> None:
385
- """Copy CLAUDE.md with smart merging and variable substitution."""
380
+ """Copy CLAUDE.md with FORCE OVERWRITE."""
386
381
  src = self.template_root / "CLAUDE.md"
387
382
  dst = self.target_path / "CLAUDE.md"
388
383
 
@@ -391,16 +386,10 @@ class TemplateProcessor:
391
386
  console.print("⚠️ CLAUDE.md template not found")
392
387
  return
393
388
 
394
- # Preserve project information when the file exists
395
- if dst.exists():
396
- self._merge_claude_md(src, dst)
397
- if not silent:
398
- console.print(" 🔄 CLAUDE.md merged (project information preserved)")
399
- else:
400
- # Copy with variable substitution
401
- self._copy_file_with_substitution(src, dst)
402
- if not silent:
403
- console.print(" ✅ CLAUDE.md copy complete")
389
+ # FORCE OVERWRITE: Always copy template (backup already created in Phase 1)
390
+ self._copy_file_with_substitution(src, dst)
391
+ if not silent:
392
+ console.print(" ✅ CLAUDE.md overwritten (backup available in .moai-backups/)")
404
393
 
405
394
  def _merge_claude_md(self, src: Path, dst: Path) -> None:
406
395
  """Delegate the smart merge for CLAUDE.md.
@@ -77,3 +77,9 @@ class HookResult:
77
77
 
78
78
 
79
79
  __all__ = ["HookPayload", "HookResult"]
80
+
81
+ # Note: core module exports:
82
+ # - HookPayload, HookResult (type definitions)
83
+ # - project.py: detect_language, get_git_info, count_specs, get_project_language
84
+ # - context.py: get_jit_context
85
+ # - checkpoint.py: detect_risky_operation, create_checkpoint, log_checkpoint, list_checkpoints
@@ -1,15 +1,10 @@
1
1
  #!/usr/bin/env python3
2
2
  """Context Engineering utilities
3
3
 
4
- JIT (Just-in-Time) Retrieval 및 워크플로우 컨텍스트 관리
4
+ JIT (Just-in-Time) Retrieval
5
5
  """
6
6
 
7
- import time
8
7
  from pathlib import Path
9
- from typing import Any
10
-
11
- # Workflow context shared across phases
12
- _workflow_context: dict[str, Any] = {}
13
8
 
14
9
 
15
10
  def get_jit_context(prompt: str, cwd: str) -> list[str]:
@@ -69,42 +64,4 @@ def get_jit_context(prompt: str, cwd: str) -> list[str]:
69
64
  return context_files
70
65
 
71
66
 
72
- def save_phase_context(phase: str, data: dict):
73
- """Store per-phase workflow context data.
74
-
75
- Args:
76
- phase: ``"analysis"``, ``"implementation"`` or ``"verification"``.
77
- data: Payload to share with later phases.
78
-
79
- Notes:
80
- - Enables reuse of expensive analysis across phases.
81
- - Entries expire after 10 minutes to avoid stale data.
82
- """
83
- _workflow_context[phase] = {"data": data, "timestamp": time.time()}
84
-
85
-
86
- def load_phase_context(phase: str) -> dict | None:
87
- """Load previously stored workflow context for a phase.
88
-
89
- Returns:
90
- Stored context data or ``None`` when expired or missing.
91
- """
92
- if phase in _workflow_context:
93
- ctx = _workflow_context[phase]
94
- # Only accept entries that are younger than 10 minutes
95
- if time.time() - ctx["timestamp"] < 600:
96
- return ctx["data"]
97
- return None
98
-
99
-
100
- def clear_workflow_context():
101
- """Clear all cached workflow context (call at workflow end)."""
102
- _workflow_context.clear()
103
-
104
-
105
- __all__ = [
106
- "get_jit_context",
107
- "save_phase_context",
108
- "load_phase_context",
109
- "clear_workflow_context",
110
- ]
67
+ __all__ = ["get_jit_context"]
@@ -86,7 +86,9 @@
86
86
  "Bash(uv remove:*)",
87
87
  "Bash(pip install:*)",
88
88
  "Bash(pip3 install:*)",
89
- "Bash(rm:*)"
89
+ "Bash(rm:*)",
90
+ "Bash(sudo:*)",
91
+ "Bash(rm -rf:*)"
90
92
  ],
91
93
  "deny": [
92
94
  "Read(./.env)",
@@ -95,8 +97,13 @@
95
97
  "Read(~/.ssh/**)",
96
98
  "Read(~/.aws/**)",
97
99
  "Read(~/.config/gcloud/**)",
98
- "Bash(sudo:*)",
99
- "Bash(rm -rf:*)",
100
+ "Bash(rm -rf /:*)",
101
+ "Bash(rm -rf /*:*)",
102
+ "Bash(rm -rf C\\:/:*)",
103
+ "Bash(rm -rf C\\:/*:*)",
104
+ "Bash(del /S /Q C\\:/:*)",
105
+ "Bash(rmdir /S /Q C\\:/:*)",
106
+ "Bash(format:*)",
100
107
  "Bash(chmod -R 777:*)",
101
108
  "Bash(dd:*)",
102
109
  "Bash(mkfs:*)",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moai-adk
3
- Version: 0.3.9
3
+ Version: 0.3.10
4
4
  Summary: MoAI Agentic Development Kit - SPEC-First TDD with Alfred SuperAgent
5
5
  Project-URL: Homepage, https://github.com/modu-ai/moai-adk
6
6
  Project-URL: Repository, https://github.com/modu-ai/moai-adk
@@ -5,7 +5,7 @@ moai_adk/cli/main.py,sha256=mHACHi27AP2N7fg7zhzOo1tlF1Jrlw1_AcbxMfpGLVE,306
5
5
  moai_adk/cli/commands/__init__.py,sha256=Gq_obgREC-eFpiYB3IEpYywWj0dy4N0eEbw6q7NxrKs,490
6
6
  moai_adk/cli/commands/backup.py,sha256=Hzf3zc7NhfChAJu2OTN_0xARtUbNaB32fmPczchbrC4,1652
7
7
  moai_adk/cli/commands/doctor.py,sha256=R4Cf9Jqwww1ng36u2SHTh6x0VdmnvaT7G9KVKJRX7N4,6623
8
- moai_adk/cli/commands/init.py,sha256=LhmmrpQh2Wofe0QfMpuZI-DJvtTNzOM5n11NwO9A-cM,10907
8
+ moai_adk/cli/commands/init.py,sha256=dVdBNuCBuwWu-8JBXW8XLX2Acr_6BVa4VsI2I3PP7nA,10409
9
9
  moai_adk/cli/commands/restore.py,sha256=nIhFR9PUQ-XuxZApONVyN77x2lWCjNhn0orL94YD0xs,2691
10
10
  moai_adk/cli/commands/status.py,sha256=YJhh7RoKnumxY0_ho7FWTWm5Nxh2jTnH9aSsapdLhyo,2277
11
11
  moai_adk/cli/commands/update.py,sha256=aoYSR6Nz70sI-KNnkeD_9nZg4CHo5DJkmstnVfoTbrk,5979
@@ -35,11 +35,11 @@ moai_adk/core/template/backup.py,sha256=R187b3ZGQU5f-vQ-iBSFx0aIqbNHJAHODC4ODc3S
35
35
  moai_adk/core/template/config.py,sha256=UbDxqJ-yohwLW3Yzx_6-0RmjCxNRFqzw6bRIC2bS5-w,2712
36
36
  moai_adk/core/template/languages.py,sha256=waeyA_MFy217bV9IiILc2gofG9RM9JhD-kdVGRyJzFk,1648
37
37
  moai_adk/core/template/merger.py,sha256=dvKobOW4vXz-7GWKJpZFYxCMtR-LszcJZYbYFTL3XY0,4049
38
- moai_adk/core/template/processor.py,sha256=uhSEBvAbZtdWyeLJ7I2IuDpla0MUNI0eM04yTV1ktd0,16687
38
+ moai_adk/core/template/processor.py,sha256=_EklBt3jFay235vREsYPNMoDHEM5xAgW3JIGM52jPc8,16179
39
39
  moai_adk/templates/.gitignore,sha256=6VNKResdDpyaii3cmJA4pOLwK2PhYARIWkUODYtKyxg,310
40
40
  moai_adk/templates/CLAUDE.md,sha256=xANecTF739Bh2aJX7nExWQcS2Yc_G8jk2HEVZKlmJOo,27286
41
41
  moai_adk/templates/__init__.py,sha256=9YY0tDkKbDFCdUB7rJFtGq0CZbF2ftVSKF573iw0KJc,99
42
- moai_adk/templates/.claude/settings.json,sha256=7Cujh2ZtqZmNfNTwndBAbNpTA4RZh1qYSpDEeu-LM7s,2312
42
+ moai_adk/templates/.claude/settings.json,sha256=-wa_uzvkUc2nE91VJhCAMv5Xrw-gwEW4-f61yhC-olk,2518
43
43
  moai_adk/templates/.claude/agents/alfred/cc-manager.md,sha256=xP3V7QAI-08ga6hhF3J4fXbgiHUM08bJbT2nfE52Bq4,13883
44
44
  moai_adk/templates/.claude/agents/alfred/debug-helper.md,sha256=1Sxz6tBHa-oCoTmwjEZ8ix2OMbjTsOpWUOekQKftlF8,5249
45
45
  moai_adk/templates/.claude/agents/alfred/doc-syncer.md,sha256=kLwLvI-5oRjEG6fSlq8CwwsH3TbqrAY-OWZDcS_3IMo,6223
@@ -57,11 +57,10 @@ moai_adk/templates/.claude/commands/alfred/2-build.md,sha256=cEvXk9gadL2twrkDf3p
57
57
  moai_adk/templates/.claude/commands/alfred/3-sync.md,sha256=5rYopDYzJHKEG400CuHoSWP5xBppcZNTFCu2GlFNVs0,19610
58
58
  moai_adk/templates/.claude/hooks/alfred/README.md,sha256=1E_nUaFj_50UTXIfFnkNLTy4qZHCs_R3gzhBoekf4Pk,6823
59
59
  moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py,sha256=vffNW0u2RXZ_jG3EzIggxEv23fDuh2qtU7oF7PZIW6A,6943
60
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py,sha256=No6VZOruX83zHq48SGcOJiza6HIut9VtsVkevaaFT88,2490
60
+ moai_adk/templates/.claude/hooks/alfred/core/__init__.py,sha256=5sHy-OPaGxBXizSRaEBeydm8U3MW1ANWxSB_36m0hz4,2775
61
61
  moai_adk/templates/.claude/hooks/alfred/core/checkpoint.py,sha256=vAm5hEqXUT2frkXFSrJf2W6uOd24Bi7O0Qr51ll51Jw,8543
62
- moai_adk/templates/.claude/hooks/alfred/core/context.py,sha256=0FBagGq9cvYFqmjl8eZvDL9g5KAh1KvTvHpQMvtR6v4,3410
62
+ moai_adk/templates/.claude/hooks/alfred/core/context.py,sha256=-PHNNktkULYL9jpIjDqpBgTNYqFo7O1QdrPbgbe52rk,2155
63
63
  moai_adk/templates/.claude/hooks/alfred/core/project.py,sha256=h7s2D0e5zn0BKu3GReMVH93ojKOTkF6yyLWNnoJI6-0,9103
64
- moai_adk/templates/.claude/hooks/alfred/core/tags.py,sha256=QAT7Oh8eupawGiZnaYDEcPhdlm1LGyzVF-Hv6SKZ_5g,7711
65
64
  moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py,sha256=mk1FQFj5V3rxyV1BQbd8bZE9R-IK4nanj61oO7jhbdw,577
66
65
  moai_adk/templates/.claude/hooks/alfred/handlers/notification.py,sha256=MjwaCRSbK4f-YvRC9CG4Rg8dhQir1gzPqF02x9O_4Gc,655
67
66
  moai_adk/templates/.claude/hooks/alfred/handlers/session.py,sha256=6zz0uJ3cYtnY8gXtoP0cRf6LmmQUfggLWIJduokd-Jw,2783
@@ -83,8 +82,8 @@ moai_adk/templates/.moai/project/tech.md,sha256=1BgkApeFqQCq6TPriGiev02G9niVx23c
83
82
  moai_adk/utils/__init__.py,sha256=fv-UwHv8r4-eedwRnDA9hFjo5QSZYXjctKDyE7XF10g,220
84
83
  moai_adk/utils/banner.py,sha256=TmZyJKXOnJpdbdn6NZDJC6a4hm051QudEvOfiKQhvI8,1873
85
84
  moai_adk/utils/logger.py,sha256=jYCWKvcN-tX17hZ-e2IPuHATwkQBFc_I1dd5fUTWxmY,5059
86
- moai_adk-0.3.9.dist-info/METADATA,sha256=-ayTp2OE0jlXLrIf6WFN-a6O339alF9LdaOqEre1l10,49932
87
- moai_adk-0.3.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
88
- moai_adk-0.3.9.dist-info/entry_points.txt,sha256=P9no1794UipqH72LP-ltdyfVd_MeB1WKJY_6-JQgV3U,52
89
- moai_adk-0.3.9.dist-info/licenses/LICENSE,sha256=M1M2b07fWcSWRM6_P3wbZKndZvyfHyYk_Wu9bS8F7o8,1069
90
- moai_adk-0.3.9.dist-info/RECORD,,
85
+ moai_adk-0.3.10.dist-info/METADATA,sha256=xJtnrnXZna5oSyjS0kZa4SRTbf-H0APs7cSYXvhoHdk,49933
86
+ moai_adk-0.3.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
87
+ moai_adk-0.3.10.dist-info/entry_points.txt,sha256=P9no1794UipqH72LP-ltdyfVd_MeB1WKJY_6-JQgV3U,52
88
+ moai_adk-0.3.10.dist-info/licenses/LICENSE,sha256=M1M2b07fWcSWRM6_P3wbZKndZvyfHyYk_Wu9bS8F7o8,1069
89
+ moai_adk-0.3.10.dist-info/RECORD,,
@@ -1,244 +0,0 @@
1
- #!/usr/bin/env python3
2
- """TAG search and verification system
3
-
4
- TAG 검색, 체인 검증, 라이브러리 버전 캐싱
5
- """
6
-
7
- import json
8
- import subprocess
9
- import time
10
- from pathlib import Path
11
-
12
- # TAG search cache: {pattern: (results, mtime_hash, cached_at)}
13
- _tag_cache: dict[str, tuple[list[dict], float, float]] = {}
14
-
15
- # Library version cache: {lib_name: (version, timestamp)}
16
- _lib_version_cache: dict[str, tuple[str, float]] = {}
17
-
18
-
19
- def _get_dir_mtime_hash(paths: list[str]) -> float:
20
- """Calculate a directory-wide mtime hash used for cache invalidation.
21
-
22
- Args:
23
- paths: List of directory paths to inspect.
24
-
25
- Returns:
26
- Highest modification timestamp (float) across all files.
27
-
28
- Notes:
29
- - Any file change bumps the hash and invalidates the cache.
30
- - Missing or inaccessible directories are ignored.
31
- """
32
- max_mtime = 0.0
33
- for path in paths:
34
- path_obj = Path(path)
35
- if not path_obj.exists():
36
- continue
37
-
38
- try:
39
- for file_path in path_obj.rglob("*"):
40
- if file_path.is_file():
41
- max_mtime = max(max_mtime, file_path.stat().st_mtime)
42
- except (OSError, PermissionError):
43
- # Skip directories we cannot read
44
- continue
45
-
46
- return max_mtime
47
-
48
-
49
- def search_tags(pattern: str, scope: list[str] | None = None, cache_ttl: int = 60) -> list[dict]:
50
- """Search TAG markers using an in-memory cache.
51
-
52
- Args:
53
- pattern: Regex pattern (for example ``'@SPEC:AUTH-.*'``).
54
- scope: List of directories to scan. Defaults to specs/src/tests.
55
- cache_ttl: Cache time-to-live in seconds (default 60).
56
-
57
- Returns:
58
- List of matches such as ``{"file": "path", "line": 10, "tag": "...", "content": "..."}``.
59
-
60
- Notes:
61
- - Cache integrity relies on directory mtimes plus TTL.
62
- - Cache hits avoid spawning ``rg`` while misses shell out (≈13ms).
63
- - Uses ``rg --json`` output for structured parsing.
64
- """
65
- if scope is None:
66
- scope = [".moai/specs/", "src/", "tests/"]
67
-
68
- cache_key = f"{pattern}:{':'.join(scope)}"
69
-
70
- current_mtime = _get_dir_mtime_hash(scope)
71
- cache_entry = _tag_cache.get(cache_key)
72
-
73
- # Serve cached results when still valid
74
- if cache_entry:
75
- cached_results, cached_mtime, cached_at = cache_entry
76
- ttl_valid = time.time() - cached_at < cache_ttl
77
-
78
- # Matching mtime and a fresh TTL means we can reuse the cache
79
- if current_mtime == cached_mtime and ttl_valid:
80
- return cached_results
81
-
82
- # Cache miss → invoke ripgrep
83
- cmd = ["rg", pattern, "--json"] + scope
84
-
85
- try:
86
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=5, check=False)
87
- except (subprocess.TimeoutExpired, FileNotFoundError):
88
- # Missing rg or a timeout returns an empty list
89
- return []
90
-
91
- matches = []
92
- for line in result.stdout.strip().split("\n"):
93
- if not line:
94
- continue
95
- try:
96
- data = json.loads(line)
97
- if data.get("type") == "match":
98
- matches.append(
99
- {
100
- "file": data["data"]["path"]["text"],
101
- "line": data["data"]["line_number"],
102
- "tag": data["data"]["lines"]["text"].strip(),
103
- "content": data["data"]["lines"]["text"],
104
- }
105
- )
106
- except (json.JSONDecodeError, KeyError):
107
- # Ignore malformed JSON lines
108
- continue
109
-
110
- # Persist results with the current mtime snapshot
111
- _tag_cache[cache_key] = (matches, _get_dir_mtime_hash(scope), time.time())
112
-
113
- return matches
114
-
115
-
116
- def verify_tag_chain(spec_id: str) -> dict:
117
- """Verify a TAG chain across ``@SPEC`` → ``@TEST`` → ``@CODE``.
118
-
119
- Args:
120
- spec_id: SPEC identifier (for example ``"AUTH-001"``).
121
-
122
- Returns:
123
- Dictionary with keys ``complete``, ``spec``, ``test``, ``code`` and ``orphans``.
124
-
125
- Notes:
126
- - Orphans capture TAGs found in code/tests without a SPEC.
127
- - A chain is complete only when all three categories contain matches.
128
- - Relies on ``search_tags`` so cached scans remain inexpensive.
129
- """
130
- chain = {
131
- "spec": search_tags(f"@SPEC:{spec_id}", [".moai/specs/"]),
132
- "test": search_tags(f"@TEST:{spec_id}", ["tests/"]),
133
- "code": search_tags(f"@CODE:{spec_id}", ["src/"]),
134
- }
135
-
136
- orphans = []
137
- if chain["code"] and not chain["spec"]:
138
- orphans.extend(chain["code"])
139
- if chain["test"] and not chain["spec"]:
140
- orphans.extend(chain["test"])
141
-
142
- return {
143
- "complete": bool(chain["spec"] and chain["test"] and chain["code"]),
144
- **chain,
145
- "orphans": orphans,
146
- }
147
-
148
-
149
- def find_all_tags_by_type(tag_type: str = "SPEC") -> dict:
150
- """Return TAG IDs grouped by domain for the requested type.
151
-
152
- Args:
153
- tag_type: TAG category (``SPEC``, ``TEST``, ``CODE`` or ``DOC``).
154
-
155
- Returns:
156
- Dictionary like ``{"AUTH": ["AUTH-001", "AUTH-002"], ...}``.
157
-
158
- Notes:
159
- - Domain is derived from the ``FOO-123`` prefix.
160
- - Deduplicates repeated matches within the same domain.
161
- - Fetches data via ``search_tags`` so caching still applies.
162
- """
163
- tags = search_tags(f"@{tag_type}:([A-Z]+-[0-9]{{3}})")
164
-
165
- by_domain = {}
166
- for tag in tags:
167
- # @SPEC:AUTH-001 → AUTH
168
- try:
169
- tag_id = tag["tag"].split(":")[1]
170
- domain = "-".join(tag_id.split("-")[:-1])
171
-
172
- if domain not in by_domain:
173
- by_domain[domain] = []
174
- if tag_id not in by_domain[domain]:
175
- by_domain[domain].append(tag_id)
176
- except IndexError:
177
- # 파싱 실패 무시
178
- continue
179
-
180
- return by_domain
181
-
182
-
183
- def suggest_tag_reuse(keyword: str, tag_type: str = "SPEC") -> list[str]:
184
- """Suggest existing TAG IDs that match the supplied keyword.
185
-
186
- Args:
187
- keyword: Keyword used to match domain names (case-insensitive).
188
- tag_type: TAG category (defaults to ``SPEC``).
189
-
190
- Returns:
191
- A list of up to five suggested TAG IDs.
192
-
193
- Notes:
194
- - Encourages reuse to avoid creating duplicate TAGs.
195
- - Performs a simple substring match against domain names.
196
- """
197
- all_tags = find_all_tags_by_type(tag_type)
198
- suggestions = []
199
-
200
- keyword_lower = keyword.lower()
201
- for domain, tag_ids in all_tags.items():
202
- if keyword_lower in domain.lower():
203
- suggestions.extend(tag_ids)
204
-
205
- return suggestions[:5] # Cap results at five
206
-
207
-
208
- def get_library_version(lib_name: str, cache_ttl: int = 86400) -> str | None:
209
- """Get the cached latest stable version for a library.
210
-
211
- Args:
212
- lib_name: Package name (for example ``"fastapi"``).
213
- cache_ttl: Cache TTL in seconds (defaults to 24 hours).
214
-
215
- Returns:
216
- Cached version string or ``None`` when the cache is cold.
217
-
218
- Notes:
219
- - Cache hits skip costly web searches (saves 3–5 seconds).
220
- - Agents should call ``set_library_version`` after fetching live data.
221
- """
222
- # Serve cached value when still within TTL
223
- if lib_name in _lib_version_cache:
224
- cached_version, cached_time = _lib_version_cache[lib_name]
225
- if time.time() - cached_time < cache_ttl:
226
- return cached_version
227
-
228
- # Cache miss → agent needs to perform the web search
229
- return None
230
-
231
-
232
- def set_library_version(lib_name: str, version: str):
233
- """Persist a library version in the cache."""
234
- _lib_version_cache[lib_name] = (version, time.time())
235
-
236
-
237
- __all__ = [
238
- "search_tags",
239
- "verify_tag_chain",
240
- "find_all_tags_by_type",
241
- "suggest_tag_reuse",
242
- "get_library_version",
243
- "set_library_version",
244
- ]