sync: 192 file da Baida98/AI@6bb6a205 (2026-08-26 18:57 UTC) [deploy-all]

#122
by Baida07 - opened
agents/html_fast_path.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Classificazione locale del fast path per mini-app HTML a file singolo.
2
+
3
+ Il classificatore è deliberatamente conservativo: in caso di dubbio restituisce
4
+ False. Non usa LLM, rete o stato globale e quindi non aggiunge latenza misurabile.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ import re
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class HtmlFastPathDecision:
14
+ eligible: bool
15
+ reason: str
16
+ path: str = "index.html"
17
+
18
+
19
+ _HTML_RE = re.compile(r"\b(?:html5?|html|pagina\s+web|single[- ]page|landing\s+page)\b", re.I)
20
+ _CREATE_RE = re.compile(r"\b(?:crea|genera|scrivi|realizza|implementa|build|create|generate|make)\b", re.I)
21
+ _SINGLE_FILE_RE = re.compile(
22
+ r"\b(?:un\s+solo\s+file|singolo\s+file|one\s+file|single\s+file|file\s+unico)\b", re.I
23
+ )
24
+ _PATH_RE = re.compile(r"(?<![\w./-])([\w./-]+\.html)(?![\w.-])", re.I)
25
+ _FORBIDDEN_RE = re.compile(
26
+ r"\b(?:deploy|pubblica|publish|rilascia|release|github|git|npm|pnpm|yarn|install|"
27
+ r"api|backend|server|database|db|auth|login|pagamento|payment|webhook|secret|token|"
28
+ r"shell|bash|terminal|esegui\s+comandi|execute\s+commands|multi[- ]file|pi[uù]\s+file|"
29
+ r"react|vue|angular|next(?:\.js)?|vite|typescript|python|sql)\b",
30
+ re.I,
31
+ )
32
+ _EXTERNAL_RE = re.compile(r"\b(?:fetch|axios|websocket|stripe|supabase|firebase|oauth)\b|https?://", re.I)
33
+
34
+
35
+ def classify_html_fast_path(goal: str) -> HtmlFastPathDecision:
36
+ """Return an eligible decision only for a safe, self-contained HTML request."""
37
+ text = " ".join(str(goal or "").split())
38
+ if not text:
39
+ return HtmlFastPathDecision(False, "empty_goal")
40
+ if len(text) > 500:
41
+ return HtmlFastPathDecision(False, "goal_too_long")
42
+ if not _HTML_RE.search(text):
43
+ return HtmlFastPathDecision(False, "not_html_goal")
44
+ if not _CREATE_RE.search(text):
45
+ return HtmlFastPathDecision(False, "not_creation_goal")
46
+ if not _SINGLE_FILE_RE.search(text):
47
+ return HtmlFastPathDecision(False, "single_file_not_explicit")
48
+ if _FORBIDDEN_RE.search(text):
49
+ return HtmlFastPathDecision(False, "contains_project_or_sensitive_operation")
50
+ if _EXTERNAL_RE.search(text):
51
+ return HtmlFastPathDecision(False, "external_dependency_or_network")
52
+
53
+ paths = _PATH_RE.findall(text)
54
+ path = paths[0] if paths else "index.html"
55
+ if "/" in path or path.startswith("."):
56
+ return HtmlFastPathDecision(False, "nested_path_not_allowed", path)
57
+ return HtmlFastPathDecision(True, "self_contained_single_html", path)
58
+
59
+
60
+ __all__ = ["HtmlFastPathDecision", "classify_html_fast_path"]
agents/unified_loop.py CHANGED
@@ -550,9 +550,29 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
550
  # F17+B7: planner per task di progettazione/implementazione — soglia ridotta a 10 chars
551
  # Bug: "crea app react" (14 chars) non attivava mai il planner (soglia era 50).
552
  # _NEEDS_PLAN_RE filtra già query semplici — len guard serve solo per 1-8 char input.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  _should_plan = (
554
  self.planner
555
  and not tool_results
 
556
  and bool(self._NEEDS_PLAN_RE.search(state.goal[:200]))
557
  and len(state.goal) > 10
558
  )
@@ -569,7 +589,7 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
569
  }
570
  _logger.info("S-FMT-ORCH fast-fix: piano sintetico iniettato, skip ARCHITECT")
571
  _t0_plan = asyncio.get_running_loop().time() # Sprint 5 ITEM 13: plan_ms timing
572
- if _should_plan:
573
  if on_step:
574
  await _maybe_await(on_step({
575
  "loop": 0, "action": "plan", "status": "started",
@@ -578,7 +598,10 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
578
  }))
579
  # S640: timeout planner + S-FMT-ORCH fast-fix bypass
580
  # Se _fast_fix_plan disponibile, salta ARCHITECT (~15s risparmiati)
581
- if _fast_fix_plan is not None:
 
 
 
582
  plan = _fast_fix_plan
583
  _logger.info("S-FMT-ORCH fast-fix: ARCHITECT bypassato")
584
  else:
@@ -3777,29 +3800,6 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
3777
  except Exception:
3778
  pass # decision_memory non disponibile — continua normalmente
3779
 
3780
- # B5-ORDER: una spiegazione completa e non operativa è già un intento
3781
- # sufficiente. Deve bypassare le guardie di ambiguità, che sono riservate a
3782
- # comandi realmente vaghi; le guardie in _is_pure_explanation() proteggono
3783
- # file, mutazioni, dati realtime e richieste troppo lunghe.
3784
- # Sprint 5 ITEM 13: classify_ms — tempo routing/classificazione goal (sync, <1ms)
3785
- _t0_classify = _time.monotonic()
3786
- if self._is_pure_explanation(goal):
3787
- try:
3788
- from api.state import record_timing as _rtcB5
3789
- _rtcB5("classify_ms", (_time.monotonic() - _t0_classify) * 1000)
3790
- except Exception:
3791
- pass
3792
- await self._transition_state(state, AgentState.THINKING, on_step)
3793
- _r = await _finish(await self._run_fallback(state, on_step))
3794
- _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
3795
- _r["effective_max_steps"] = state.max_steps
3796
- if _sid_token is not None:
3797
- try: _sid_var.reset(_sid_token)
3798
- except Exception: pass
3799
- if self._session_files:
3800
- asyncio.ensure_future(self._vfs_git_backup())
3801
- return _r
3802
-
3803
  # P29-B1: gate ambiguità strutturale — _is_goal_ambiguous() era P28-B2 dead code (mai chiamata).
3804
  # Zero LLM, <0.1ms. Lingua-aware via self._run_lang (P28-B1). Fires dopo blacklist e prima del routing.
3805
  if _is_goal_ambiguous(goal):
@@ -3963,6 +3963,9 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
3963
  return _r_bl
3964
 
3965
 
 
 
 
3966
  # S402: Fast Path — greeting/ack/identità semplice → bypass tutto l'overhead
3967
  if self._is_simple_query(goal):
3968
  try:
@@ -4012,6 +4015,27 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
4012
  # già — "direct tools + fallback" — ma il codice faceva solo _run_fallback senza tool).
4013
  # Bug: query meteo/news/cerca non chiamavano mai i tool reali → LLM allucinava i dati
4014
  # → ResponseVerifier girava su risposta inventata → retry → 20-60s inutili.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4015
  # P36: Hybrid Execution Router — Python code analysis fast path.
4016
  # Se goal contiene keyword analisi + codice Python nel context/goal,
4017
  # chiama python_analyze direttamente (<5ms) saltando planner+LLM (5-15s).
 
550
  # F17+B7: planner per task di progettazione/implementazione — soglia ridotta a 10 chars
551
  # Bug: "crea app react" (14 chars) non attivava mai il planner (soglia era 50).
552
  # _NEEDS_PLAN_RE filtra già query semplici — len guard serve solo per 1-8 char input.
553
+ try:
554
+ from agents.html_fast_path import classify_html_fast_path
555
+ _html_fast_decision = classify_html_fast_path(state.goal)
556
+ except Exception as _html_cls_exc:
557
+ _logger.debug("[html-fast-path] classifier unavailable: %s", type(_html_cls_exc).__name__)
558
+ _html_fast_decision = None
559
+ _html_fast_plan = None
560
+ if _html_fast_decision is not None and _html_fast_decision.eligible and not tool_results:
561
+ _html_fast_plan = {
562
+ "summary": "Piano locale mini-app HTML a file singolo",
563
+ "goal": state.goal,
564
+ "subtasks": [
565
+ {"id": 1, "description": f"Scrivi {_html_fast_decision.path}: {state.goal}", "tool": "write_file", "requires": []},
566
+ {"id": 2, "description": f"Rileggi {_html_fast_decision.path} e verifica la scrittura", "tool": "read_file", "requires": [1]},
567
+ ],
568
+ "complexity": "low",
569
+ "source": "local_html_fast_path",
570
+ }
571
+ _logger.info("[html-fast-path] planner bypass: %s", _html_fast_decision.path)
572
  _should_plan = (
573
  self.planner
574
  and not tool_results
575
+ and _html_fast_plan is None
576
  and bool(self._NEEDS_PLAN_RE.search(state.goal[:200]))
577
  and len(state.goal) > 10
578
  )
 
589
  }
590
  _logger.info("S-FMT-ORCH fast-fix: piano sintetico iniettato, skip ARCHITECT")
591
  _t0_plan = asyncio.get_running_loop().time() # Sprint 5 ITEM 13: plan_ms timing
592
+ if _should_plan or _html_fast_plan is not None:
593
  if on_step:
594
  await _maybe_await(on_step({
595
  "loop": 0, "action": "plan", "status": "started",
 
598
  }))
599
  # S640: timeout planner + S-FMT-ORCH fast-fix bypass
600
  # Se _fast_fix_plan disponibile, salta ARCHITECT (~15s risparmiati)
601
+ if _html_fast_plan is not None:
602
+ plan = _html_fast_plan
603
+ _logger.info("[html-fast-path] ARCHITECT bypassato")
604
+ elif _fast_fix_plan is not None:
605
  plan = _fast_fix_plan
606
  _logger.info("S-FMT-ORCH fast-fix: ARCHITECT bypassato")
607
  else:
 
3800
  except Exception:
3801
  pass # decision_memory non disponibile — continua normalmente
3802
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3803
  # P29-B1: gate ambiguità strutturale — _is_goal_ambiguous() era P28-B2 dead code (mai chiamata).
3804
  # Zero LLM, <0.1ms. Lingua-aware via self._run_lang (P28-B1). Fires dopo blacklist e prima del routing.
3805
  if _is_goal_ambiguous(goal):
 
3963
  return _r_bl
3964
 
3965
 
3966
+ # Sprint 5 ITEM 13: classify_ms — tempo routing/classificazione goal (sync, <1ms)
3967
+ _t0_classify = _time.monotonic()
3968
+
3969
  # S402: Fast Path — greeting/ack/identità semplice → bypass tutto l'overhead
3970
  if self._is_simple_query(goal):
3971
  try:
 
4015
  # già — "direct tools + fallback" — ma il codice faceva solo _run_fallback senza tool).
4016
  # Bug: query meteo/news/cerca non chiamavano mai i tool reali → LLM allucinava i dati
4017
  # → ResponseVerifier girava su risposta inventata → retry → 20-60s inutili.
4018
+ # B5: query spiegazione pura → _run_fallback diretta (-20-30s risparmio)
4019
+ # Scenari: "cos'è X", "spiegami Y", "how does Z work?", "explain W"
4020
+ # Fail-open: se regex troppo larga → path normale (nessuna perdita)
4021
+ if self._is_pure_explanation(goal):
4022
+ try:
4023
+ from api.state import record_timing as _rtcB5
4024
+ _rtcB5("classify_ms", (_time.monotonic() - _t0_classify) * 1000)
4025
+ except Exception:
4026
+ pass
4027
+ await self._transition_state(state, AgentState.THINKING, on_step)
4028
+ _r = await _finish(await self._run_fallback(state, on_step))
4029
+ _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
4030
+ _r["effective_max_steps"] = state.max_steps
4031
+ if _sid_token is not None:
4032
+ try: _sid_var.reset(_sid_token)
4033
+ except Exception: pass
4034
+ if self._session_files:
4035
+ asyncio.ensure_future(self._vfs_git_backup())
4036
+ return _r
4037
+
4038
+
4039
  # P36: Hybrid Execution Router — Python code analysis fast path.
4040
  # Se goal contiene keyword analisi + codice Python nel context/goal,
4041
  # chiama python_analyze direttamente (<5ms) saltando planner+LLM (5-15s).
api/agent.py CHANGED
@@ -849,8 +849,8 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
849
  # tool. È il backstop per client SSE che non applicano il fast path UI.
850
  _literal_response = task.get('literal_response')
851
  if isinstance(_literal_response, str) and _literal_response:
852
- _agent_tasks[task_id]['status'] = 'SUCCESS'
853
- asyncio.create_task(sb_update_status(task_id, 'SUCCESS')).add_done_callback(_log_task_exc)
854
  literal_event = json.dumps(_sanitize_for_json({
855
  'event': 'task_done', 'taskId': task_id, 'result': _literal_response,
856
  }))
@@ -899,7 +899,7 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
899
  sb_events = await sb_get_events(task_id)
900
  if sb_events:
901
  task_status = task.get('status', 'UNKNOWN')
902
- terminal = task_status in ('SUCCESS', 'ERROR', 'CANCELLED')
903
  # Replay buffer from resume point
904
  for evt_str in sb_events[_resume_from:]:
905
  yield evt_str
@@ -1005,8 +1005,8 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
1005
  # È emesso direttamente nello stream affinché i client SSE non possano bypassarlo.
1006
  _literal_response = task.get('literal_response')
1007
  if isinstance(_literal_response, str) and _literal_response:
1008
- _agent_tasks[task_id]['status'] = 'SUCCESS'
1009
- asyncio.create_task(sb_update_status(task_id, 'SUCCESS')).add_done_callback(_log_task_exc)
1010
  _sse('task_done', {'taskId': task_id, 'result': _literal_response})
1011
  return
1012
 
@@ -1453,8 +1453,13 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
1453
  except Exception as _artifact_exc:
1454
  _logger.warning('[agent] artifact fallback failed: %s', type(_artifact_exc).__name__)
1455
 
1456
- _agent_tasks[task_id]['status'] = 'SUCCESS'
1457
- asyncio.create_task(sb_update_status(task_id, 'SUCCESS')).add_done_callback(_log_task_exc)
 
 
 
 
 
1458
  # ARCH-K2.2: pubblica lifecycle event via Kernel
1459
  if _KERNEL_AVAILABLE and _kernel is not None:
1460
  asyncio.create_task(_kernel.publish_event(
@@ -1470,17 +1475,46 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
1470
  # confermato il task. Su errore/cancellazione lo staging rimane
1471
  # intenzionalmente non committato.
1472
  _sse('vfs_sync_complete', _vfs_commit)
1473
- _sse('task_done', {'taskId': task_id, 'result': _result_text[:8000]})
 
 
 
 
 
 
 
 
 
 
 
1474
  asyncio.create_task(_tg_done(task_id, task.get('goal', ''), _result_text[:500], _task_started_ms)).add_done_callback(_log_task_exc)
1475
 
1476
- # S363: fire-and-forget quality check when code detected in output
 
1477
  if _run_quality_check:
1478
  _qg_result = str(result.get('output', result) if isinstance(result, dict) else result)
1479
- if len(_qg_result) > 500 and _qg_result.count('```') >= 2: # S373: threshold raised — evita QG su snippet brevi
1480
- asyncio.create_task(_run_quality_check(
1481
- task_id, task['goal'], _qg_result,
1482
- on_event=lambda ev: _sse(ev.get('type', 'test_result'), ev),
1483
- )).add_done_callback(_log_task_exc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1484
 
1485
 
1486
  except asyncio.CancelledError:
@@ -1495,8 +1529,8 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
1495
  _sse('task_cancelled', {'taskId': task_id})
1496
 
1497
  except (ImportError, ModuleNotFoundError):
1498
- _agent_tasks[task_id]['status'] = 'SUCCESS'
1499
- asyncio.create_task(sb_update_status(task_id, 'SUCCESS')).add_done_callback(_log_task_exc)
1500
  _sse('step_done', {'taskId': task_id, 'step': {'name': 'Ragionamento', 'index': 0}})
1501
  _sse('task_done', {'taskId': task_id, 'result': (
1502
  f'Goal ricevuto: {task["goal"]}\n\n'
 
849
  # tool. È il backstop per client SSE che non applicano il fast path UI.
850
  _literal_response = task.get('literal_response')
851
  if isinstance(_literal_response, str) and _literal_response:
852
+ _agent_tasks[task_id]['status'] = 'COMPLETED'
853
+ asyncio.create_task(sb_update_status(task_id, 'COMPLETED')).add_done_callback(_log_task_exc)
854
  literal_event = json.dumps(_sanitize_for_json({
855
  'event': 'task_done', 'taskId': task_id, 'result': _literal_response,
856
  }))
 
899
  sb_events = await sb_get_events(task_id)
900
  if sb_events:
901
  task_status = task.get('status', 'UNKNOWN')
902
+ terminal = task_status in ('COMPLETED', 'SUCCESS', 'ERROR', 'CANCELLED')
903
  # Replay buffer from resume point
904
  for evt_str in sb_events[_resume_from:]:
905
  yield evt_str
 
1005
  # È emesso direttamente nello stream affinché i client SSE non possano bypassarlo.
1006
  _literal_response = task.get('literal_response')
1007
  if isinstance(_literal_response, str) and _literal_response:
1008
+ _agent_tasks[task_id]['status'] = 'COMPLETED'
1009
+ asyncio.create_task(sb_update_status(task_id, 'COMPLETED')).add_done_callback(_log_task_exc)
1010
  _sse('task_done', {'taskId': task_id, 'result': _literal_response})
1011
  return
1012
 
 
1453
  except Exception as _artifact_exc:
1454
  _logger.warning('[agent] artifact fallback failed: %s', type(_artifact_exc).__name__)
1455
 
1456
+ # Lifecycle separato: l'esecuzione primaria è completa quando VFS e risultato
1457
+ # sono confermati; la verifica qualità successiva non deve tenere il task RUNNING.
1458
+ _agent_tasks[task_id]['status'] = 'COMPLETED'
1459
+ _agent_tasks[task_id]['quality_status'] = (
1460
+ 'QUALITY_CHECK_PENDING' if _run_quality_check else 'NOT_REQUIRED'
1461
+ )
1462
+ asyncio.create_task(sb_update_status(task_id, 'COMPLETED')).add_done_callback(_log_task_exc)
1463
  # ARCH-K2.2: pubblica lifecycle event via Kernel
1464
  if _KERNEL_AVAILABLE and _kernel is not None:
1465
  asyncio.create_task(_kernel.publish_event(
 
1475
  # confermato il task. Su errore/cancellazione lo staging rimane
1476
  # intenzionalmente non committato.
1477
  _sse('vfs_sync_complete', _vfs_commit)
1478
+ if _run_quality_check:
1479
+ _sse('quality_check_pending', {
1480
+ 'taskId': task_id,
1481
+ 'status': 'QUALITY_CHECK_PENDING',
1482
+ 'primaryStatus': 'COMPLETED',
1483
+ })
1484
+ _sse('task_done', {
1485
+ 'taskId': task_id,
1486
+ 'result': _result_text[:8000],
1487
+ 'status': 'COMPLETED',
1488
+ 'qualityStatus': _agent_tasks[task_id].get('quality_status', 'NOT_REQUIRED'),
1489
+ })
1490
  asyncio.create_task(_tg_done(task_id, task.get('goal', ''), _result_text[:500], _task_started_ms)).add_done_callback(_log_task_exc)
1491
 
1492
+ # Quality check non bloccante: aggiorna solo quality_status, mai lo stato
1493
+ # primario del task e mai il commit VFS già confermato.
1494
  if _run_quality_check:
1495
  _qg_result = str(result.get('output', result) if isinstance(result, dict) else result)
1496
+ if len(_qg_result) > 500 and _qg_result.count('```') >= 2:
1497
+ async def _run_quality_background() -> None:
1498
+ try:
1499
+ await _run_quality_check(
1500
+ task_id, task['goal'], _qg_result,
1501
+ on_event=lambda ev: _sse(ev.get('type', 'test_result'), ev),
1502
+ )
1503
+ _agent_tasks.get(task_id, {})['quality_status'] = 'QUALITY_CHECK_DONE'
1504
+ _sse('quality_check_done', {
1505
+ 'taskId': task_id,
1506
+ 'status': 'QUALITY_CHECK_DONE',
1507
+ 'primaryStatus': 'COMPLETED',
1508
+ })
1509
+ except Exception as _q_exc:
1510
+ _agent_tasks.get(task_id, {})['quality_status'] = 'QUALITY_CHECK_ERROR'
1511
+ _sse('quality_check_done', {
1512
+ 'taskId': task_id,
1513
+ 'status': 'QUALITY_CHECK_ERROR',
1514
+ 'primaryStatus': 'COMPLETED',
1515
+ 'error': type(_q_exc).__name__,
1516
+ })
1517
+ asyncio.create_task(_run_quality_background()).add_done_callback(_log_task_exc)
1518
 
1519
 
1520
  except asyncio.CancelledError:
 
1529
  _sse('task_cancelled', {'taskId': task_id})
1530
 
1531
  except (ImportError, ModuleNotFoundError):
1532
+ _agent_tasks[task_id]['status'] = 'COMPLETED'
1533
+ asyncio.create_task(sb_update_status(task_id, 'COMPLETED')).add_done_callback(_log_task_exc)
1534
  _sse('step_done', {'taskId': task_id, 'step': {'name': 'Ragionamento', 'index': 0}})
1535
  _sse('task_done', {'taskId': task_id, 'result': (
1536
  f'Goal ricevuto: {task["goal"]}\n\n'
tests/test_html_fast_path.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agents.html_fast_path import classify_html_fast_path
2
+
3
+
4
+ def test_accepts_self_contained_single_html():
5
+ decision = classify_html_fast_path(
6
+ "Crea una mini-app lista spesa in un solo file index.html con aggiunta e filtro"
7
+ )
8
+ assert decision.eligible is True
9
+ assert decision.path == "index.html"
10
+ assert decision.reason == "self_contained_single_html"
11
+
12
+
13
+ def test_accepts_explicit_html_file_without_network():
14
+ decision = classify_html_fast_path(
15
+ "Genera una pagina web HTML in un solo file todo.html con CSS e JavaScript inline"
16
+ )
17
+ assert decision.eligible is True
18
+ assert decision.path == "todo.html"
19
+
20
+
21
+ def test_rejects_deploy_and_external_dependencies():
22
+ assert not classify_html_fast_path(
23
+ "Crea una mini-app HTML in un solo file index.html e pubblicala su GitHub"
24
+ ).eligible
25
+ assert not classify_html_fast_path(
26
+ "Crea una pagina HTML single-file che usa fetch https://api.example.com"
27
+ ).eligible
28
+
29
+
30
+ def test_rejects_multi_file_or_sensitive_tasks():
31
+ assert not classify_html_fast_path(
32
+ "Crea una app React multi-file con backend API"
33
+ ).eligible
34
+ assert not classify_html_fast_path(
35
+ "Crea una pagina HTML in un solo file con login e database"
36
+ ).eligible