adelie-ai 0.3.2 → 0.3.5
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.
package/adelie/__init__.py
CHANGED
|
@@ -44,6 +44,41 @@ Output a single valid JSON array of files to create/update."""
|
|
|
44
44
|
SYSTEM_PROMPT = load_prompt("coder", _FALLBACK_PROMPT)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _get_framework_guidelines(framework: str) -> str:
|
|
48
|
+
"""Return framework-specific coding rules to inject into coder prompt."""
|
|
49
|
+
guidelines = {
|
|
50
|
+
"nextjs": (
|
|
51
|
+
"\n## ⚠️ Next.js App Router — MANDATORY Rules\n"
|
|
52
|
+
"- Any component using useState, useEffect, onClick, or other client hooks "
|
|
53
|
+
"MUST start with `'use client';` as the FIRST line.\n"
|
|
54
|
+
"- `layout.tsx` is a SERVER component — NEVER use client-side hooks, "
|
|
55
|
+
"context providers, or event handlers in it without `'use client'`.\n"
|
|
56
|
+
"- Dynamic routes use `[param]`, `[...catch]`, `[[...optional]]` syntax — "
|
|
57
|
+
"these are valid directory names.\n"
|
|
58
|
+
"- API routes go in `src/app/api/` as `route.ts` files.\n"
|
|
59
|
+
"- Use `next/navigation` (not `next/router`) for App Router.\n"
|
|
60
|
+
"- The package name for lucide icons is `lucide-react` (NOT `@lucide/react`).\n\n"
|
|
61
|
+
),
|
|
62
|
+
"nuxt": (
|
|
63
|
+
"\n## ⚠️ Nuxt 3 — MANDATORY Rules\n"
|
|
64
|
+
"- Use `<script setup lang='ts'>` for composition API.\n"
|
|
65
|
+
"- Auto-imports: `ref`, `computed`, `useRoute` etc. are auto-imported.\n"
|
|
66
|
+
"- Pages go in `pages/`, layouts in `layouts/`.\n\n"
|
|
67
|
+
),
|
|
68
|
+
"sveltekit": (
|
|
69
|
+
"\n## ⚠️ SvelteKit — MANDATORY Rules\n"
|
|
70
|
+
"- Pages go in `src/routes/` as `+page.svelte`.\n"
|
|
71
|
+
"- Server-only code uses `+page.server.ts` or `+server.ts`.\n"
|
|
72
|
+
"- Layouts use `+layout.svelte`.\n\n"
|
|
73
|
+
),
|
|
74
|
+
"vite": (
|
|
75
|
+
"\n## ⚠️ Vite + React — MANDATORY Rules\n"
|
|
76
|
+
"- Entry point is `index.html` referencing `src/main.tsx`.\n"
|
|
77
|
+
"- Use `vite.config.ts` with `@vitejs/plugin-react`.\n\n"
|
|
78
|
+
),
|
|
79
|
+
}
|
|
80
|
+
return guidelines.get(framework, "")
|
|
81
|
+
|
|
47
82
|
def _get_coder_log_dir(layer: int, coder_name: str) -> Path:
|
|
48
83
|
"""Get or create the log directory for a coder."""
|
|
49
84
|
log_dir = CODER_ROOT / "layer" / str(layer) / coder_name
|
|
@@ -174,6 +209,16 @@ def run_coder(
|
|
|
174
209
|
f"## Lower Layer Coder Logs\n{lower_logs}\n\n"
|
|
175
210
|
)
|
|
176
211
|
|
|
212
|
+
# Bug #7: Inject framework-specific coding rules to prevent common mistakes
|
|
213
|
+
try:
|
|
214
|
+
from adelie.agents.expert_ai import _detect_framework
|
|
215
|
+
fw = _detect_framework(workspace_root)
|
|
216
|
+
fw_guidelines = _get_framework_guidelines(fw)
|
|
217
|
+
if fw_guidelines:
|
|
218
|
+
user_prompt += fw_guidelines
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
177
222
|
if feedback:
|
|
178
223
|
user_prompt += (
|
|
179
224
|
f"## ⚠️ REVIEWER FEEDBACK (FIX THESE ISSUES)\n{feedback}\n\n"
|
|
@@ -36,7 +36,13 @@ _STOP_WORDS = {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
_DEDUP_THRESHOLD = 0.6 # Jaccard similarity >= 60% → duplicate
|
|
39
|
-
MAX_CODERS_PER_FILE =
|
|
39
|
+
MAX_CODERS_PER_FILE = 5
|
|
40
|
+
|
|
41
|
+
# Keywords that indicate a "fix/patch" task (bypass per-file limit)
|
|
42
|
+
_FIX_KEYWORDS = {
|
|
43
|
+
"fix", "patch", "hotfix", "repair", "resolve", "debug",
|
|
44
|
+
"error", "bug", "broken", "typo", "incorrect", "wrong",
|
|
45
|
+
}
|
|
40
46
|
|
|
41
47
|
|
|
42
48
|
def _tokenize(text: str) -> set[str]:
|
|
@@ -89,17 +95,20 @@ def _find_duplicate_coder(
|
|
|
89
95
|
def _count_file_modifications(registry: dict, files: list[str]) -> int:
|
|
90
96
|
"""
|
|
91
97
|
주어진 파일 목록과 겹치는 기존 코더 수를 반환.
|
|
92
|
-
coder task description 내 파일
|
|
98
|
+
coder task description 내 **전체 파일 경로** 매칭 기반.
|
|
99
|
+
(basename 매칭은 다른 경로의 동일 이름 파일을 오탐하므로 fullpath 사용)
|
|
93
100
|
"""
|
|
94
101
|
if not files:
|
|
95
102
|
return 0
|
|
96
103
|
|
|
97
|
-
|
|
104
|
+
# Use full path for matching to avoid collisions
|
|
105
|
+
# e.g. "quests/create/page.tsx" and "login/page.tsx" are different files
|
|
106
|
+
file_fullpaths = {f.lower() for f in files}
|
|
98
107
|
count = 0
|
|
99
108
|
|
|
100
109
|
for coder in registry.get("coders", []):
|
|
101
110
|
task_lower = coder.get("last_task", "").lower()
|
|
102
|
-
if any(
|
|
111
|
+
if any(fp in task_lower for fp in file_fullpaths):
|
|
103
112
|
count += 1
|
|
104
113
|
|
|
105
114
|
return count
|
|
@@ -270,7 +279,10 @@ def run_coders(
|
|
|
270
279
|
name = existing_name
|
|
271
280
|
|
|
272
281
|
# ── Per-file limit ─────────────────────────────────────
|
|
273
|
-
|
|
282
|
+
# Fix/patch tasks bypass per-file limit — they MUST be able
|
|
283
|
+
# to modify files that need correction (Bug #8b)
|
|
284
|
+
is_fix_task = bool(_FIX_KEYWORDS & _tokenize(task_desc))
|
|
285
|
+
if relevant and not is_fix_task and _count_file_modifications(registry, relevant) >= MAX_CODERS_PER_FILE:
|
|
274
286
|
console.print(
|
|
275
287
|
f" [yellow]⏭ Skipped '{name}': target files modified "
|
|
276
288
|
f"{MAX_CODERS_PER_FILE}+ times already[/yellow]"
|
|
@@ -444,8 +444,9 @@ def _get_scaffolding_need() -> str:
|
|
|
444
444
|
return ""
|
|
445
445
|
|
|
446
446
|
lines = [
|
|
447
|
-
"
|
|
448
|
-
"
|
|
447
|
+
"ℹ️ SCAFFOLDING NOTE: The following entry files are missing.",
|
|
448
|
+
"If no scaffolding coder has been created yet, create ONE scaffolding task (layer 0).",
|
|
449
|
+
"If scaffolding was ALREADY attempted in a previous cycle, SKIP this and focus on feature tasks:",
|
|
449
450
|
]
|
|
450
451
|
for c in checks:
|
|
451
452
|
lines.append(f" - {c['file']}: {c['desc']}")
|
package/adelie/orchestrator.py
CHANGED
|
@@ -12,6 +12,7 @@ State machine:
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import copy
|
|
15
16
|
import json
|
|
16
17
|
from typing import Callable, Optional
|
|
17
18
|
import signal
|
|
@@ -112,6 +113,12 @@ class Orchestrator:
|
|
|
112
113
|
self._last_assembled_contexts: list | None = None
|
|
113
114
|
self._last_build_errors: list[dict] = []
|
|
114
115
|
|
|
116
|
+
# Track coder/reviewer results for next cycle's Expert AI context (Bug #4)
|
|
117
|
+
self._last_coder_result: dict | None = None
|
|
118
|
+
|
|
119
|
+
# Track consecutive zero-file coder cycles for emergency reset (Bug #10)
|
|
120
|
+
self._zero_file_streak: int = 0
|
|
121
|
+
|
|
115
122
|
# Auto-scan: if KB is empty and project has existing code, scan first
|
|
116
123
|
self._auto_scan_done = False
|
|
117
124
|
|
|
@@ -181,6 +188,10 @@ class Orchestrator:
|
|
|
181
188
|
# Include recent build errors for Expert AI context
|
|
182
189
|
if self._last_build_errors:
|
|
183
190
|
state["build_errors"] = self._last_build_errors[:3] # 최대 3개
|
|
191
|
+
# Include last cycle's coder/reviewer result so Expert AI can avoid
|
|
192
|
+
# re-issuing tasks that previously failed (Bug #4)
|
|
193
|
+
if self._last_coder_result:
|
|
194
|
+
state["last_coder_result"] = self._last_coder_result
|
|
184
195
|
return state
|
|
185
196
|
|
|
186
197
|
def get_agent_context(self, agent_type: AgentType | str) -> dict:
|
|
@@ -656,6 +667,7 @@ class Orchestrator:
|
|
|
656
667
|
# Force state transition on critical loops
|
|
657
668
|
if self.state.value in ("new_logic", "error"):
|
|
658
669
|
console.print("[bold yellow]🔧 Forcing transition to NORMAL state[/bold yellow]")
|
|
670
|
+
self._archive_errors() # Clear outstanding errors to prevent stale context loop
|
|
659
671
|
self.state = LoopState.NORMAL
|
|
660
672
|
system_state["situation"] = "normal"
|
|
661
673
|
else:
|
|
@@ -976,7 +988,10 @@ class Orchestrator:
|
|
|
976
988
|
)] or all_written_files
|
|
977
989
|
|
|
978
990
|
for retry in range(MAX_REVIEW_RETRIES + 1):
|
|
979
|
-
|
|
991
|
+
# Bug #6: pass STAGING_ROOT so reviewer reads files from
|
|
992
|
+
# staging (where Coder wrote them), not PROJECT_ROOT.
|
|
993
|
+
from adelie.agents.coder_ai import STAGING_ROOT as _REVIEW_STAGING
|
|
994
|
+
review = run_review(coder_name=name, written_files=task_files, workspace_root=_REVIEW_STAGING)
|
|
980
995
|
score = review.get("overall_score", 5)
|
|
981
996
|
self._review_score_history.append(score)
|
|
982
997
|
|
|
@@ -1005,6 +1020,44 @@ class Orchestrator:
|
|
|
1005
1020
|
self._emit_agent_end("Reviewer", f"error: {e}")
|
|
1006
1021
|
else:
|
|
1007
1022
|
self._emit_agent_end("Reviewer", "approved" if reviewer_approved else "rejected")
|
|
1023
|
+
|
|
1024
|
+
# Bug #3: Log review failures to KB so Expert AI avoids re-issuing
|
|
1025
|
+
# the exact same task that was rejected.
|
|
1026
|
+
if not reviewer_approved and all_written_files:
|
|
1027
|
+
failed_files = [f.get("filepath", "") for f in all_written_files]
|
|
1028
|
+
review_summary = review.get("summary", "N/A") if review else "N/A"
|
|
1029
|
+
review_issues = ""
|
|
1030
|
+
if review:
|
|
1031
|
+
for issue in review.get("issues", [])[:5]:
|
|
1032
|
+
review_issues += f"- [{issue.get('severity')}] {issue.get('title')}: {issue.get('suggestion', '')}\n"
|
|
1033
|
+
failure_note = (
|
|
1034
|
+
f"# Review Failure Log (Cycle #{self.loop_iteration})\n\n"
|
|
1035
|
+
f"The following files were rejected by Reviewer AI after "
|
|
1036
|
+
f"{MAX_REVIEW_RETRIES + 1} attempts:\n"
|
|
1037
|
+
+ "\n".join(f"- `{f}`" for f in failed_files) + "\n\n"
|
|
1038
|
+
f"**Review Summary**: {review_summary}\n\n"
|
|
1039
|
+
f"**Issues**:\n{review_issues}\n"
|
|
1040
|
+
f"Expert AI should NOT re-assign the same task until "
|
|
1041
|
+
f"the underlying issue is resolved.\n"
|
|
1042
|
+
)
|
|
1043
|
+
failure_path = WORKSPACE_PATH / "errors" / f"review_failure_{self.loop_iteration}.md"
|
|
1044
|
+
failure_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1045
|
+
failure_path.write_text(failure_note, encoding="utf-8")
|
|
1046
|
+
retriever.update_index(
|
|
1047
|
+
f"errors/{failure_path.name}",
|
|
1048
|
+
tags=["error", "review-failure"],
|
|
1049
|
+
summary=f"Review rejected cycle #{self.loop_iteration}: {', '.join(failed_files[:3])}",
|
|
1050
|
+
)
|
|
1051
|
+
console.print(f"[yellow]📝 Review failure logged to KB for Expert AI awareness[/yellow]")
|
|
1052
|
+
|
|
1053
|
+
# Bug #4: Record coder/reviewer results for next cycle context
|
|
1054
|
+
self._last_coder_result = {
|
|
1055
|
+
"cycle": self.loop_iteration,
|
|
1056
|
+
"files_written": len(all_written_files),
|
|
1057
|
+
"reviewer_approved": reviewer_approved,
|
|
1058
|
+
"review_score": review.get("overall_score", 0) if review else 0,
|
|
1059
|
+
"review_summary": review.get("summary", "")[:300] if review else "",
|
|
1060
|
+
}
|
|
1008
1061
|
elif all_written_files and self.phase != "initial":
|
|
1009
1062
|
# Reviewer not scheduled this cycle — auto-approve staged files
|
|
1010
1063
|
reviewer_approved = True
|
|
@@ -1099,10 +1152,56 @@ class Orchestrator:
|
|
|
1099
1152
|
except Exception as e:
|
|
1100
1153
|
console.print(f"[dim]⚠️ PolicyGate error: {e}[/dim]")
|
|
1101
1154
|
|
|
1102
|
-
# ──
|
|
1155
|
+
# ── Syntax validation with Coder Retry Loop (before promotion) ───
|
|
1156
|
+
syntax_passed = True
|
|
1103
1157
|
if all_written_files and reviewer_approved and policy_passed:
|
|
1158
|
+
passed_files, failed_files = self._verify_staged_files(all_written_files)
|
|
1159
|
+
if failed_files:
|
|
1160
|
+
syntax_passed = False
|
|
1161
|
+
console.print(
|
|
1162
|
+
f"[yellow]⚠️ Syntax validation failed for {len(failed_files)} file(s) — "
|
|
1163
|
+
f"initiating retry[/yellow]"
|
|
1164
|
+
)
|
|
1165
|
+
if coder_tasks and self.phase != "initial":
|
|
1166
|
+
from adelie.agents.coder_manager import run_coders
|
|
1167
|
+
from adelie.phases import PHASE_INFO
|
|
1168
|
+
phase_info = PHASE_INFO.get(self.phase, {})
|
|
1169
|
+
max_layer = phase_info.get("max_coder_layer", 0)
|
|
1170
|
+
|
|
1171
|
+
# Build compiler failure feedback
|
|
1172
|
+
error_feedback = "## ⚠️ SYNTAX/COMPILATION FAILURE — FIX THESE ERRORS\n"
|
|
1173
|
+
for f in failed_files:
|
|
1174
|
+
error_feedback += f"### File: `{f.get('filepath')}`\n"
|
|
1175
|
+
error_feedback += f"Error: {f.get('error', 'Syntax error')}\n\n"
|
|
1176
|
+
error_feedback += "Fix the code to resolve all syntax, compile, and parse errors."
|
|
1177
|
+
|
|
1178
|
+
for task in coder_tasks:
|
|
1179
|
+
task["feedback"] = error_feedback
|
|
1180
|
+
|
|
1181
|
+
try:
|
|
1182
|
+
fix_start_time = time.time()
|
|
1183
|
+
run_coders(coder_tasks, max_active_layer=max_layer)
|
|
1184
|
+
new_files = self._collect_staged_files(fix_start_time)
|
|
1185
|
+
if new_files:
|
|
1186
|
+
all_written_files = new_files
|
|
1187
|
+
# Re-verify after retry
|
|
1188
|
+
passed_files, failed_files = self._verify_staged_files(all_written_files)
|
|
1189
|
+
if not failed_files:
|
|
1190
|
+
syntax_passed = True
|
|
1191
|
+
console.print("[bold green]✅ Syntax validation passed after retry[/bold green]")
|
|
1192
|
+
else:
|
|
1193
|
+
console.print(
|
|
1194
|
+
f"[red]❌ Syntax validation still failed for {len(failed_files)} file(s) "
|
|
1195
|
+
f"after retry[/red]"
|
|
1196
|
+
)
|
|
1197
|
+
except Exception as se:
|
|
1198
|
+
console.print(f"[dim]⚠️ Syntax retry exception: {se}[/dim]")
|
|
1199
|
+
|
|
1200
|
+
# ── Promote staged files to project (after review + policy + syntax check) ───────
|
|
1201
|
+
promoted_count = 0
|
|
1202
|
+
if all_written_files and reviewer_approved and policy_passed and syntax_passed:
|
|
1104
1203
|
with self._staging_lock:
|
|
1105
|
-
self._promote_staged_files(all_written_files)
|
|
1204
|
+
promoted_count = self._promote_staged_files(all_written_files)
|
|
1106
1205
|
self._cleanup_staging()
|
|
1107
1206
|
|
|
1108
1207
|
# ── Git auto-commit (MID_1+) ──────────────────────────────────────
|
|
@@ -1117,6 +1216,36 @@ class Orchestrator:
|
|
|
1117
1216
|
except Exception as e:
|
|
1118
1217
|
console.print(f"[dim]⚠️ Git commit error: {e}[/dim]")
|
|
1119
1218
|
|
|
1219
|
+
# ── Bug #10: Zero-file streak detection & emergency registry reset ────
|
|
1220
|
+
# Zero-file streak triggers when coder tasks exist but promoted_count is 0
|
|
1221
|
+
# (covers coder writing 0 files OR files being written but blocked/rejected before promotion)
|
|
1222
|
+
if promoted_count == 0 and coder_tasks:
|
|
1223
|
+
self._zero_file_streak += 1
|
|
1224
|
+
if self._zero_file_streak >= 3:
|
|
1225
|
+
console.print(
|
|
1226
|
+
f"[bold yellow]🔓 Emergency: {self._zero_file_streak} consecutive"
|
|
1227
|
+
f" zero-promoted cycles — resetting coder registry[/bold yellow]"
|
|
1228
|
+
)
|
|
1229
|
+
try:
|
|
1230
|
+
from adelie.agents.coder_manager import REGISTRY_PATH
|
|
1231
|
+
if REGISTRY_PATH.exists():
|
|
1232
|
+
import json as _json
|
|
1233
|
+
reg = _json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
|
1234
|
+
old_count = len(reg.get("coders", []))
|
|
1235
|
+
reg["coders"] = []
|
|
1236
|
+
REGISTRY_PATH.write_text(
|
|
1237
|
+
_json.dumps(reg, indent=2, ensure_ascii=False),
|
|
1238
|
+
encoding="utf-8",
|
|
1239
|
+
)
|
|
1240
|
+
console.print(
|
|
1241
|
+
f" [yellow]Cleared {old_count} coder(s) from registry[/yellow]"
|
|
1242
|
+
)
|
|
1243
|
+
except Exception as e:
|
|
1244
|
+
console.print(f" [dim]⚠️ Registry reset error: {e}[/dim]")
|
|
1245
|
+
self._zero_file_streak = 0
|
|
1246
|
+
else:
|
|
1247
|
+
self._zero_file_streak = 0
|
|
1248
|
+
|
|
1120
1249
|
# ══════════════════════════════════════════════════════════════════════
|
|
1121
1250
|
# PHASE 3: Tester AI + Runner AI (PARALLEL)
|
|
1122
1251
|
# After code is promoted, testing and building are independent.
|
|
@@ -1143,13 +1272,14 @@ class Orchestrator:
|
|
|
1143
1272
|
parallel_names.append("Runner")
|
|
1144
1273
|
console.print(f"[dim] ⚡ Phase 3: parallel execution [{', '.join(parallel_names)}][/dim]")
|
|
1145
1274
|
|
|
1275
|
+
import copy
|
|
1146
1276
|
with ThreadPoolExecutor(max_workers=len(parallel_names), thread_name_prefix="adelie-p3") as pool:
|
|
1147
1277
|
futures = {}
|
|
1148
1278
|
|
|
1149
1279
|
if run_tester:
|
|
1150
1280
|
_test_pass_results = []
|
|
1151
1281
|
_test_metrics = {}
|
|
1152
|
-
_p3_coder_tasks =
|
|
1282
|
+
_p3_coder_tasks = copy.deepcopy(coder_tasks) # Thread-safe deep copy to prevent shared mutations
|
|
1153
1283
|
_p3_all_files = list(all_written_files)
|
|
1154
1284
|
|
|
1155
1285
|
def _run_tester():
|
|
@@ -1193,7 +1323,11 @@ class Orchestrator:
|
|
|
1193
1323
|
if coder_result.get("total_files", 0) > 0:
|
|
1194
1324
|
new_files = self._collect_staged_files(fix_start_time)
|
|
1195
1325
|
if new_files:
|
|
1196
|
-
_files
|
|
1326
|
+
# Merge new_files into _files by filepath to keep all original targets in validation
|
|
1327
|
+
existing_paths = {f["filepath"] for f in _files}
|
|
1328
|
+
for nf in new_files:
|
|
1329
|
+
if nf["filepath"] not in existing_paths:
|
|
1330
|
+
_files.append(nf)
|
|
1197
1331
|
with self._staging_lock:
|
|
1198
1332
|
self._promote_staged_files(_files)
|
|
1199
1333
|
self._cleanup_staging()
|
|
@@ -1479,6 +1613,19 @@ class Orchestrator:
|
|
|
1479
1613
|
if self._recover_count >= self.MAX_RECOVER_RETRIES:
|
|
1480
1614
|
console.print(f"[yellow]⚠️ Max recovery retries ({self.MAX_RECOVER_RETRIES}) reached — entering maintenance.[/yellow]")
|
|
1481
1615
|
self._archive_errors()
|
|
1616
|
+
|
|
1617
|
+
# Checkpoint Rollback: Revert to the latest safe state
|
|
1618
|
+
try:
|
|
1619
|
+
from adelie.checkpoint import CheckpointManager
|
|
1620
|
+
cp_mgr = CheckpointManager()
|
|
1621
|
+
cps = cp_mgr.list_checkpoints()
|
|
1622
|
+
if cps:
|
|
1623
|
+
latest_id = cps[0].checkpoint_id
|
|
1624
|
+
console.print(f"[bold yellow]🔄 Self-Healing: Rolling back project files to checkpoint {latest_id}...[/bold yellow]")
|
|
1625
|
+
cp_mgr.restore(latest_id)
|
|
1626
|
+
except Exception as cpe:
|
|
1627
|
+
console.print(f"[dim]⚠️ Rollback failed during recovery limit: {cpe}[/dim]")
|
|
1628
|
+
|
|
1482
1629
|
self.state = LoopState.MAINTENANCE
|
|
1483
1630
|
else:
|
|
1484
1631
|
console.print(f"[yellow]🔄 Recovery attempt {self._recover_count}/{self.MAX_RECOVER_RETRIES} — clearing errors and returning to normal.[/yellow]")
|
|
@@ -1501,9 +1648,8 @@ class Orchestrator:
|
|
|
1501
1648
|
f"[yellow]⚠️ Max new_logic cycles ({self.MAX_NEW_LOGIC_CYCLES}) reached — "
|
|
1502
1649
|
f"transitioning to normal.[/yellow]"
|
|
1503
1650
|
)
|
|
1504
|
-
|
|
1651
|
+
next_situation = "normal"
|
|
1505
1652
|
self._new_logic_count = 0
|
|
1506
|
-
return
|
|
1507
1653
|
else:
|
|
1508
1654
|
self._new_logic_count = 0
|
|
1509
1655
|
|
package/adelie/utils/dep_sync.py
CHANGED
|
@@ -174,7 +174,25 @@ def sync_package_json(missing: list[str], project_root: Path) -> int:
|
|
|
174
174
|
dev_deps = pkg.get("devDependencies", {})
|
|
175
175
|
added = 0
|
|
176
176
|
|
|
177
|
+
# Known-invalid package name patterns — common LLM hallucinations
|
|
178
|
+
_INVALID_PKG_NAMES = {
|
|
179
|
+
"requested", "response", "result", "data", "config",
|
|
180
|
+
"utils", "helpers", "types", "models", "services",
|
|
181
|
+
"app", "main", "index", "test", "error", "handler",
|
|
182
|
+
"component", "module", "function", "class", "object",
|
|
183
|
+
"input", "output", "value", "item", "list", "array",
|
|
184
|
+
"string", "number", "boolean", "null", "undefined",
|
|
185
|
+
"client", "server", "api", "route", "page", "layout",
|
|
186
|
+
}
|
|
187
|
+
|
|
177
188
|
for name in missing:
|
|
189
|
+
# Skip hallucinated/generic package names
|
|
190
|
+
name_lower = name.lower()
|
|
191
|
+
if name_lower in _INVALID_PKG_NAMES:
|
|
192
|
+
continue
|
|
193
|
+
# npm packages must be lowercase, start with letter/@, no spaces
|
|
194
|
+
if not re.match(r'^(@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9][a-z0-9._-]*$', name):
|
|
195
|
+
continue
|
|
178
196
|
if name not in deps and name not in dev_deps:
|
|
179
197
|
# Heuristic: testing/dev tools go to devDependencies
|
|
180
198
|
if any(kw in name.lower() for kw in ["test", "jest", "vitest", "eslint", "prettier", "lint"]):
|