From 7f8ce66c6ae561c1a1036747d48ff60829922e52 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Tue, 27 Jan 2026 12:24:16 -0800 Subject: [PATCH 001/184] multi dimensional scoring and structured json parsing for ranker --- django/aiservice/ranker/ranker.py | 667 +++++++++++++++++++++++++----- 1 file changed, 573 insertions(+), 94 deletions(-) diff --git a/django/aiservice/ranker/ranker.py b/django/aiservice/ranker/ranker.py index b737e5c0b..f03af3a0f 100644 --- a/django/aiservice/ranker/ranker.py +++ b/django/aiservice/ranker/ranker.py @@ -1,8 +1,12 @@ from __future__ import annotations import asyncio +import json import logging +import random import re +from collections import defaultdict +from dataclasses import dataclass from typing import TYPE_CHECKING import sentry_sdk @@ -22,44 +26,97 @@ if TYPE_CHECKING: ranker_api = NinjaAPI(urls_namespace="ranker") + +# Regex patterns for fallback parsing (legacy format) rank_regex_pattern = re.compile(r"(.*)<\/rank>", re.DOTALL | re.IGNORECASE) explain_regex_pattern = re.compile(r"(.*)<\/explain>", re.DOTALL | re.IGNORECASE) +scores_regex_pattern = re.compile(r"(.*)<\/scores>", re.DOTALL | re.IGNORECASE) + +# Pattern to extract JSON from markdown code blocks or raw JSON +json_pattern = re.compile(r"```(?:json)?\s*(\{.*?\})\s*```|(\{[^`]*\"ranking\"[^`]*\})", re.DOTALL) + +# Scoring dimensions and their weights (higher weight = more important) +# Weights should sum to 1.0 for normalized scoring +SCORING_DIMENSIONS = { + "speedup_quality": 0.30, # Quality of the speedup (not just magnitude, but how it's achieved) + "readability": 0.25, # Code readability and maintainability + "diff_precision": 0.20, # How focused/minimal is the change + "correctness_confidence": 0.25, # Confidence the optimization is correct/safe +} SYSTEM_PROMPT = """You are an expert code reviewer who understands why programs run fast. -You are provided with a list of optimization candidates with their code diff with respect to the baseline code and speedup ratio information. Your task is to rank the candidates in decreasing order of their viability as a pull request. Your goal is to improve the probability of acceptance of the optimization by an expert engineer. +You are provided with a list of optimization candidates with their code diff with respect to the baseline code and speedup ratio information. Your task is to evaluate each candidate on multiple dimensions and provide scores. You are also provided with the following information. - python_version - The version of python the code would be executed on. - function_references - Python markdown blocks with filename and references of some functions which call the function being optimized. The filenames and/or references could indicate if the function being optimized is in a hot path. The reference could have the function being called from a place that is important, for example in a loop, which means the effect of optimization might be important. -Rules to follow while ranking optimization candidates - -- DISREGARD the fact that new dependencies could be introduced in the diff, these are already installed in the system. -- Prefer optimizations with higher speedup ratios. If the higher speedups happen due to strange hacks or micro-optimizations or something an expert won't write then prefer it less. -- Prefer optimizations which contain precise diffs unless the speedup provided is very high. Larger pull requests are typically harder to accept then more precise smaller pull requests. -- Introduction of the `global` and `nonlocal` keywords in optimizations is **HIGHLY DISCOURAGED** as it reduces code clarity and maintainability, introduces hidden dependencies, can cause subtle bugs and breaks modularity. **DO NOT** prefer such optimizations. -- Replacement of `isinstance()` checks with `type()` checks is **HIGHLY DISCOURAGED** as `isinstance()` correctly handles inheritance and subclasses, while `type()` checks are incorrect for subclass instances and represent a micro-optimization that should be avoided. Do not prefer such optimizations. -- If the only optimizations are micro-optimizations like inlining a function call, or localizing variables or methods (attribute lookup optimizations), do not prefer the optimizations. The performance improvements are minimal and come at a substantial cost to readability. -- The optimization candidate should not impact the code readability unless the speedup provided is very high. +## Scoring Dimensions (score each from 1-10, where 10 is best): -Sometimes, these criteria maybe in conflict with each other. In such cases you have to remember that the goal is acceptance of the pull request, so make a judgement on what optimization candidate would be most likely to be accepted. +1. **speedup_quality** - Quality of the performance improvement: + - 10: Significant speedup achieved through clean, algorithmic improvements + - 7-9: Good speedup with reasonable implementation + - 4-6: Moderate speedup or achieved through acceptable micro-optimizations + - 1-3: Minimal speedup or achieved through hacky/unacceptable means -Please provide your response in the following format (you MUST include the and tags): +2. **readability** - Code readability and maintainability: + - 10: Code is cleaner or equally readable as original + - 7-9: Minor readability trade-offs that are acceptable + - 4-6: Noticeable readability impact but still maintainable + - 1-3: Significantly harder to read/maintain + - **IMPORTANT**: Use of `global`/`nonlocal` keywords should score 1-3 + - **IMPORTANT**: Replacing `isinstance()` with `type()` should score 1-3 - -Comma separated list of candidate indices in decreasing order of their viability as a pull request candidate. - - -A brief explanation of why the particular ranking was made. - +3. **diff_precision** - How focused and minimal is the change: + - 10: Minimal, surgical change that only touches what's necessary + - 7-9: Focused change with minor extra modifications + - 4-6: Moderate scope, some unnecessary changes + - 1-3: Large, sprawling diff that touches many things -Example output for 3 candidates where candidate 2 is best, then 1, then 3: - -2,1,3 - - -Candidate 2 provides the best balance of speedup and readability... - +4. **correctness_confidence** - Confidence the optimization preserves correctness: + - 10: Obviously correct, no edge cases affected + - 7-9: Very likely correct, minimal risk + - 4-6: Probably correct but has some risk + - 1-3: Risky, may break edge cases or change behavior + +## Rules: +- DISREGARD new dependencies - they are already installed +- Micro-optimizations (inlining, localizing variables, attribute lookup optimizations) should score LOW on speedup_quality unless speedup is very high +- The goal is acceptance of the pull request by an expert engineer + +## Response Format + +You MUST respond with a valid JSON object. Do not include any text before or after the JSON. + +```json +{ + "scores": { + "1": {"speedup_quality": 7, "readability": 8, "diff_precision": 9, "correctness_confidence": 8}, + "2": {"speedup_quality": 9, "readability": 6, "diff_precision": 7, "correctness_confidence": 7} + }, + "ranking": [2, 1], + "explanation": "Candidate 2 has the best speedup quality despite slightly lower readability..." +} +``` + +The JSON schema: +- "scores": Object mapping candidate number (as string "1", "2", etc.) to dimension scores (integers 1-10) +- "ranking": Array of candidate numbers in decreasing order of viability (best first) +- "explanation": Brief explanation of the scoring and ranking decisions + +Example for 3 candidates where candidate 2 is best: +```json +{ + "scores": { + "1": {"speedup_quality": 7, "readability": 8, "diff_precision": 9, "correctness_confidence": 8}, + "2": {"speedup_quality": 9, "readability": 6, "diff_precision": 7, "correctness_confidence": 7}, + "3": {"speedup_quality": 5, "readability": 4, "diff_precision": 6, "correctness_confidence": 9} + }, + "ranking": [2, 1, 3], + "explanation": "Candidate 2 has the best speedup quality despite slightly lower readability. Candidate 1 offers good balance. Candidate 3 has poor readability." +} +``` """ USER_PROMPT = """Here is a numbered list of optimization candidates' code diffs and their speedup ratios. @@ -74,10 +131,389 @@ Here are the function references """ +NUM_RANKING_PASSES = 3 + +# Type alias for candidate scores: {candidate_idx: {dimension: score}} +CandidateScores = dict[int, dict[str, float]] + + +@dataclass +class ParsedRankingResponse: + """Structured response from ranking LLM.""" + + ranking: list[int] # 1-indexed candidate order (best first) + scores: CandidateScores | None # {candidate_idx: {dimension: score}} + explanation: str + + +def _extract_json_from_response(content: str) -> dict | None: + """Extract JSON object from LLM response. + + Handles: + - Raw JSON + - JSON in markdown code blocks + - JSON with surrounding text + """ + # Try direct JSON parse first + try: + return json.loads(content.strip()) + except json.JSONDecodeError: + pass + + # Try to extract from markdown code block or find JSON object + match = json_pattern.search(content) + if match: + json_str = match.group(1) or match.group(2) + if json_str: + try: + return json.loads(json_str.strip()) + except json.JSONDecodeError: + pass + + # Try to find any JSON-like structure with "ranking" key + # Look for the outermost braces containing "ranking" + try: + start = content.find("{") + if start != -1: + # Find matching closing brace + depth = 0 + for i, char in enumerate(content[start:], start): + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + json_str = content[start : i + 1] + return json.loads(json_str) + except json.JSONDecodeError: + pass + + return None + + +def _parse_json_response(content: str, num_candidates: int) -> ParsedRankingResponse | None: + """Parse structured JSON response from LLM. + + Expected format: + { + "scores": {"1": {"speedup_quality": 7, ...}, "2": {...}}, + "ranking": [2, 1, 3], + "explanation": "..." + } + """ + data = _extract_json_from_response(content) + if data is None: + return None + + try: + # Parse ranking + ranking_data = data.get("ranking") + if not isinstance(ranking_data, list): + return None + ranking = [int(x) for x in ranking_data] + + # Validate ranking + if sorted(ranking) != list(range(1, num_candidates + 1)): + logging.warning(f"Invalid ranking in JSON response: {ranking}") + return None + + # Parse scores + scores: CandidateScores | None = None + scores_data = data.get("scores") + if isinstance(scores_data, dict): + scores = {} + for candidate_key, dim_scores in scores_data.items(): + try: + candidate_idx = int(candidate_key) + if isinstance(dim_scores, dict): + parsed_scores: dict[str, float] = {} + for dim in SCORING_DIMENSIONS: + if dim in dim_scores: + score = float(dim_scores[dim]) + # Clamp to valid range + parsed_scores[dim] = max(1.0, min(10.0, score)) + if all(dim in parsed_scores for dim in SCORING_DIMENSIONS): + scores[candidate_idx] = parsed_scores + except (ValueError, TypeError): + continue + + # Validate we have scores for all candidates + if len(scores) != num_candidates or not all(i in scores for i in range(1, num_candidates + 1)): + scores = None # Invalid, fall back to no scores + + # Parse explanation + explanation = str(data.get("explanation", "")) + + return ParsedRankingResponse(ranking=ranking, scores=scores, explanation=explanation) + + except (KeyError, TypeError, ValueError) as e: + logging.warning(f"Failed to parse JSON response: {e}") + return None + + +def _parse_scores(scores_text: str, num_candidates: int) -> CandidateScores | None: + """Parse the scores block from LLM output. + + Expected format: + 1: speedup_quality=7, readability=8, diff_precision=9, correctness_confidence=8 + 2: speedup_quality=9, readability=6, diff_precision=7, correctness_confidence=7 + ... + + Returns dict mapping candidate index (1-indexed) to dimension scores, or None if parsing fails. + """ + scores: CandidateScores = {} + lines = scores_text.strip().split("\n") + + for line in lines: + line = line.strip() + if not line: + continue + + # Parse "N: dimension=score, dimension=score, ..." + try: + idx_part, scores_part = line.split(":", 1) + candidate_idx = int(idx_part.strip()) + + candidate_scores: dict[str, float] = {} + for pair in scores_part.split(","): + pair = pair.strip() + if "=" not in pair: + continue + dimension, score_str = pair.split("=", 1) + dimension = dimension.strip() + score = float(score_str.strip()) + # Clamp to valid range + score = max(1.0, min(10.0, score)) + candidate_scores[dimension] = score + + # Verify all dimensions are present + if all(dim in candidate_scores for dim in SCORING_DIMENSIONS): + scores[candidate_idx] = candidate_scores + except (ValueError, IndexError): + continue + + # Verify we got scores for all candidates + if len(scores) == num_candidates and all(i in scores for i in range(1, num_candidates + 1)): + return scores + return None + + +def _compute_weighted_score(candidate_scores: dict[str, float]) -> float: + """Compute weighted score from dimension scores.""" + total = 0.0 + for dimension, weight in SCORING_DIMENSIONS.items(): + total += candidate_scores.get(dimension, 5.0) * weight + return total + + +def _scores_to_ranking(scores: CandidateScores) -> list[int]: + """Convert candidate scores to a ranking (best first, 1-indexed).""" + # Calculate weighted score for each candidate + weighted_scores = { + candidate_idx: _compute_weighted_score(dim_scores) + for candidate_idx, dim_scores in scores.items() + } + # Sort by weighted score descending (higher is better) + sorted_candidates = sorted(weighted_scores.keys(), key=lambda c: weighted_scores[c], reverse=True) + return sorted_candidates + + +def _aggregate_scores( + all_scores: list[CandidateScores], num_candidates: int +) -> tuple[CandidateScores, list[int]]: + """Aggregate scores from multiple passes by averaging. + + Returns: + Tuple of (averaged_scores, final_ranking) + """ + # Initialize accumulators + score_sums: dict[int, dict[str, float]] = { + i: {dim: 0.0 for dim in SCORING_DIMENSIONS} for i in range(1, num_candidates + 1) + } + counts: dict[int, int] = {i: 0 for i in range(1, num_candidates + 1)} + + # Sum all scores + for scores in all_scores: + for candidate_idx, dim_scores in scores.items(): + for dim, score in dim_scores.items(): + if dim in SCORING_DIMENSIONS: + score_sums[candidate_idx][dim] += score + counts[candidate_idx] += 1 + + # Average the scores + averaged_scores: CandidateScores = {} + for candidate_idx in range(1, num_candidates + 1): + if counts[candidate_idx] > 0: + averaged_scores[candidate_idx] = { + dim: score_sums[candidate_idx][dim] / counts[candidate_idx] + for dim in SCORING_DIMENSIONS + } + else: + # Fallback to neutral scores if no data + averaged_scores[candidate_idx] = {dim: 5.0 for dim in SCORING_DIMENSIONS} + + # Derive ranking from averaged scores + final_ranking = _scores_to_ranking(averaged_scores) + return averaged_scores, final_ranking + + +async def _single_ranking_pass( + user_id: str, + trace_id: str, + diffs: list[str], + speedups: list[float], + python_version: str | None, + function_references: str | None, + rank_model: LLM, + pass_number: int, +) -> tuple[list[int], str, CandidateScores | None] | None: + """Execute a single ranking pass with the given candidate order. + + Returns a tuple of (ranking, explanation, scores) or None if the ranking failed. + The ranking is 1-indexed based on the order candidates were presented. + Scores may be None if parsing failed but ranking succeeded. + """ + ranking_context = "" + for i, (diff, speedup) in enumerate(zip(diffs, speedups, strict=False)): + ranking_context += f"{i + 1}. Diff:\n```diff\n{diff}\n```\nSpeedup: {speedup:.3f}\n" + + user_prompt = USER_PROMPT.format( + ranking_context=ranking_context, + python_version=python_version or "Not available", + function_references=function_references or "Not available", + ) + system_message = ChatCompletionSystemMessageParam(role="system", content=SYSTEM_PROMPT) + user_message = ChatCompletionUserMessageParam(role="user", content=user_prompt) + debug_log_sensitive_data(f"Pass {pass_number}: {SYSTEM_PROMPT}{user_prompt}") + messages: list[ChatCompletionMessageParam] = [system_message, user_message] + + try: + output = await call_llm( + llm=rank_model, + messages=messages, + call_type="ranking", + trace_id=trace_id, + user_id=user_id, + context={ + "num_candidates": len(diffs), + "speedups": speedups, + "python_version": python_version, + "pass_number": pass_number, + }, + ) + await update_optimization_cost( + trace_id=trace_id, cost=calculate_llm_cost(output.raw_response, rank_model), user_id=user_id + ) + except Exception as e: + logging.exception(f"Failed to generate ranking in pass {pass_number}") + sentry_sdk.capture_exception(e) + return None + + debug_log_sensitive_data(f"Pass {pass_number} AIClient optimization response:\n{output}") + if output.raw_response.usage is not None: + await asyncio.to_thread( + ph, + user_id, + "aiservice-optimize-openai-usage", + properties={ + "model": rank_model.name, + "n": 1, + "usage": output.raw_response.usage.model_dump_json(), + "pass_number": pass_number, + }, + ) + + num_candidates = len(diffs) + + # Try JSON parsing first (preferred structured format) + json_response = _parse_json_response(output.content, num_candidates) + if json_response is not None: + logging.info(f"Pass {pass_number}: Successfully parsed JSON response") + return json_response.ranking, json_response.explanation, json_response.scores + + # Fall back to regex parsing (legacy XML-tag format) + logging.info(f"Pass {pass_number}: JSON parsing failed, falling back to regex") + + # Parse scores (optional - we can fall back to ranking if scores fail) + scores: CandidateScores | None = None + try: + scores_match = re.search(scores_regex_pattern, output.content) + if scores_match is not None: + scores = _parse_scores(scores_match.group(1), num_candidates) + if scores is None: + logging.warning(f"Failed to parse scores in pass {pass_number}") + except Exception: # noqa: BLE001 + logging.warning(f"Error parsing scores in pass {pass_number}") + + # Parse explanation + explanation = "" + try: + explanation_match = re.search(explain_regex_pattern, output.content) + if explanation_match is not None: + explanation = explanation_match.group(1).strip() + except Exception: # noqa: BLE001 + pass + + # Parse ranking + ranking: list[int] | None = None + try: + ranking_match = re.search(rank_regex_pattern, output.content) + if ranking_match is not None: + ranking = list(map(int, ranking_match.group(1).strip().split(","))) + except Exception: # noqa: BLE001 + pass + + # Validate or derive ranking + if ranking is None or sorted(ranking) != list(range(1, num_candidates + 1)): + # If ranking is invalid but we have scores, derive from scores + if scores is not None: + ranking = _scores_to_ranking(scores) + logging.info(f"Pass {pass_number}: Derived ranking from scores") + else: + logging.warning(f"Pass {pass_number}: No valid ranking found") + return None + + return ranking, explanation, scores + + +def _aggregate_rankings( + rankings: list[list[int]], num_candidates: int +) -> list[int]: + """Aggregate multiple rankings using Borda count method. + + Each ranking assigns points based on position: last place gets 1 point, + second-to-last gets 2, etc. Lower total score = better rank. + + Args: + rankings: List of rankings, each is a list of 1-indexed candidate positions + in decreasing order of preference (first is best). + num_candidates: Total number of candidates. + + Returns: + Aggregated ranking as 1-indexed list in decreasing order of preference. + """ + # Score accumulator for each original candidate (1-indexed) + scores: dict[int, int] = defaultdict(int) + + for ranking in rankings: + # ranking[0] is the best candidate (should get lowest score) + # ranking[-1] is the worst candidate (should get highest score) + for position, candidate in enumerate(ranking): + # Position 0 (best) gets score 1, position 1 gets 2, etc. + scores[candidate] += position + 1 + + # Sort candidates by their total score (lower is better) + sorted_candidates = sorted(range(1, num_candidates + 1), key=lambda c: scores[c]) + return sorted_candidates + + async def rank_optimizations( # noqa: D417 user_id: str, data: RankInputSchema, rank_model: LLM = RANKING_MODEL ) -> RankResponseSchema | RankErrorResponseSchema: - """Optimize the given python code for performance using the Claude 4 model. + """Rank optimization candidates using multiple shuffled passes to reduce position bias. + + Runs the ranking model multiple times with different candidate orderings, + then aggregates results using Borda count to produce a final ranking. Parameters ---------- @@ -90,79 +526,112 @@ async def rank_optimizations( # noqa: D417 :param optimization_ids: """ - debug_log_sensitive_data(f"Generating a ranking for {user_id}") - # TODO add logging instead of print(optimization_ids) - ranking_context = "" - for i, (diff, speedup) in enumerate(zip(data.diffs, data.speedups, strict=False)): - ranking_context += f"{i + 1}. Diff:\n```diff\n{diff}\n```\nSpeedup: {speedup:.3f}\n" + debug_log_sensitive_data(f"Generating a ranking for {user_id} with {NUM_RANKING_PASSES} passes") + num_candidates = len(data.diffs) - user_prompt = USER_PROMPT.format( - ranking_context=ranking_context, - python_version=data.python_version or "Not available", - function_references=data.function_references or "Not available", - ) - system_message = ChatCompletionSystemMessageParam(role="system", content=SYSTEM_PROMPT) - user_message = ChatCompletionUserMessageParam(role="user", content=user_prompt) - debug_log_sensitive_data(f"{SYSTEM_PROMPT}{user_prompt}") - messages: list[ChatCompletionMessageParam] = [system_message, user_message] + # Generate shuffled orderings for each pass + # First pass uses original order, subsequent passes use random shuffles + orderings: list[list[int]] = [] + original_indices = list(range(num_candidates)) + orderings.append(original_indices.copy()) # Pass 1: original order - try: - output = await call_llm( - llm=rank_model, - messages=messages, - call_type="ranking", - trace_id=data.trace_id, + for _ in range(NUM_RANKING_PASSES - 1): + shuffled = original_indices.copy() + random.shuffle(shuffled) + orderings.append(shuffled) + + # Run all ranking passes concurrently + async def run_pass(pass_idx: int) -> tuple[list[int], str, CandidateScores | None] | None: + ordering = orderings[pass_idx] + # Reorder diffs and speedups according to the shuffled ordering + shuffled_diffs = [data.diffs[i] for i in ordering] + shuffled_speedups = [data.speedups[i] for i in ordering] + + result = await _single_ranking_pass( user_id=user_id, - context={ - "num_candidates": len(data.diffs), - "speedups": data.speedups, - "python_version": data.python_version, - }, + trace_id=data.trace_id, + diffs=shuffled_diffs, + speedups=shuffled_speedups, + python_version=data.python_version, + function_references=data.function_references, + rank_model=rank_model, + pass_number=pass_idx + 1, ) - await update_optimization_cost( - trace_id=data.trace_id, cost=calculate_llm_cost(output.raw_response, rank_model), user_id=user_id - ) - except Exception as e: - logging.exception("Failed to generate ranking") - sentry_sdk.capture_exception(e) - return RankErrorResponseSchema(error=str(e)) - debug_log_sensitive_data(f"AIClient optimization response:\n{output}") - if output.raw_response.usage is not None: - await asyncio.to_thread( - ph, - user_id, - "aiservice-optimize-openai-usage", - properties={"model": rank_model.name, "n": 1, "usage": output.raw_response.usage.model_dump_json()}, - ) - # parse xml tag for explanation, ranking - try: - explanation_match = re.search(explain_regex_pattern, output.content) - if explanation_match is None: - raise ValueError("No explanation match found") - explanation = explanation_match.group(1) - except: # noqa: E722 - # TODO add logging instead of print("No explanation found") - explanation = "" - # still doing stuff instead of returning coz ranking is important - if explanation == "": - # TODO add logging instead of print("No explanation found") - pass - # still doing stuff instead of returning coz ranking is important - try: - ranking_match = re.search(rank_regex_pattern, output.content) - if ranking_match is None: - raise ValueError("No ranking match found") - # TODO better parsing, could be only comma separated, need to handle all edge cases - ranking = list(map(int, ranking_match.group(1).strip().split(","))) - except: # noqa: E722 - # TODO add logging instead of print("No ranking found") - return RankErrorResponseSchema(error="No ranking found") - if sorted(ranking) != list(range(1, len(data.diffs) + 1)): - # TODO need to handle all edge cases - # TODO add logging instead of print("Invalid ranking") - return RankErrorResponseSchema(error="No ranking found") - return RankResponseSchema(ranking=ranking, explanation=explanation) + if result is None: + return None + + ranking, explanation, scores = result + # Map the ranking back to original indices + # ranking is 1-indexed positions in decreasing preference order + # e.g., [2, 1, 3] means shuffled candidate 2 is best, then 1, then 3 + # We need to convert to original indices + original_ranking = [ordering[r - 1] + 1 for r in ranking] # Convert back to 1-indexed original + + # Map scores back to original indices if present + original_scores: CandidateScores | None = None + if scores is not None: + original_scores = { + ordering[shuffled_idx - 1] + 1: dim_scores + for shuffled_idx, dim_scores in scores.items() + } + + return original_ranking, explanation, original_scores + + tasks = [run_pass(i) for i in range(NUM_RANKING_PASSES)] + results = await asyncio.gather(*tasks) + + # Filter out failed passes + valid_results = [r for r in results if r is not None] + + if not valid_results: + return RankErrorResponseSchema(error="All ranking passes failed") + + # Extract rankings, explanations, and scores + valid_rankings = [r[0] for r in valid_results] + explanations = [r[1] for r in valid_results] + valid_scores = [r[2] for r in valid_results if r[2] is not None] + + # Determine final ranking: prefer score-based aggregation if we have scores + final_scores: dict[int, dict[str, float]] | None = None + if valid_scores: + # Aggregate scores and derive ranking from them + final_scores, final_ranking = _aggregate_scores(valid_scores, num_candidates) + debug_log_sensitive_data( + f"Aggregated {len(valid_scores)} score sets -> final scores: {final_scores}" + ) + else: + # Fall back to Borda count on rankings + final_ranking = _aggregate_rankings(valid_rankings, num_candidates) + debug_log_sensitive_data( + f"No scores available, using Borda count on {len(valid_rankings)} rankings" + ) + + # Combine explanations from all passes + combined_explanation = explanations[0] if explanations else "" + if len(valid_results) > 1: + score_info = "" + if final_scores: + # Add score summary to explanation + score_summaries = [] + for candidate_idx in final_ranking[:3]: # Top 3 + weighted = _compute_weighted_score(final_scores[candidate_idx]) + score_summaries.append(f"#{candidate_idx}: {weighted:.2f}") + score_info = f" Top scores: {', '.join(score_summaries)}." + combined_explanation = ( + f"Aggregated from {len(valid_results)} ranking passes.{score_info} " + f"Primary explanation: {explanations[0]}" + ) + + debug_log_sensitive_data( + f"Aggregated {len(valid_results)} rankings: {valid_rankings} -> final: {final_ranking}" + ) + + return RankResponseSchema( + ranking=final_ranking, + explanation=combined_explanation, + scores=final_scores, + ) class RankInputSchema(Schema): @@ -177,6 +646,7 @@ class RankInputSchema(Schema): class RankResponseSchema(Schema): ranking: list[int] explanation: str + scores: dict[int, dict[str, float]] | None = None # {candidate_idx: {dimension: score}} class RankErrorResponseSchema(Schema): @@ -204,5 +674,14 @@ async def rank( user_id=request.user, ranking={"ranking": ranked_opt_ids, "explanation": ranking_response.explanation}, ) - response = RankResponseSchema(explanation=ranking_response.explanation, ranking=ranking_with_0_idx) + # Convert scores to 0-indexed if present + scores_0_idx: dict[int, dict[str, float]] | None = None + if ranking_response.scores: + scores_0_idx = {k - 1: v for k, v in ranking_response.scores.items()} + + response = RankResponseSchema( + explanation=ranking_response.explanation, + ranking=ranking_with_0_idx, + scores=scores_0_idx, + ) return 200, response From 72cb58994851ad832cad3bd554cfe4effc02b452 Mon Sep 17 00:00:00 2001 From: ali Date: Tue, 27 Jan 2026 23:30:03 +0200 Subject: [PATCH 002/184] some instrumentation fixes --- .../javascript/instrument_javascript.py | 54 +++++++++++++++++-- .../javascript/execute_async_system_prompt.md | 7 +++ .../javascript/execute_system_prompt.md | 7 +++ .../aiservice/testgen/testgen_javascript.py | 19 ++++++- 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py b/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py index 5ae823a49..0c10aae49 100644 --- a/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py +++ b/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py @@ -64,7 +64,13 @@ def _fix_import_path(test_source: str, function_name: str, module_path: str) -> def instrument_javascript_tests( - test_source: str, function_name: str, module_path: str, mode: Literal["behavior", "performance"] = "behavior" + test_source: str, + function_name: str, + module_path: str, + is_ts: bool = False, + mode: Literal["behavior", "performance"] = "behavior", + # TODO: let the client decide whether to inject jest or not + inject_jest: bool = False, ) -> str: """Instrument JavaScript tests with codeflash helper. @@ -78,6 +84,7 @@ def instrument_javascript_tests( test_source: The JavaScript test source code function_name: The name of the function being tested module_path: The path to the module containing the function + is_ts: Whether the test is written in TypeScript mode: Testing mode - 'behavior' uses capture() for SQLite, 'performance' uses capturePerf() for stdout timing @@ -88,8 +95,15 @@ def instrument_javascript_tests( # First, fix any incorrect import paths generated by the LLM test_source = _fix_import_path(test_source, function_name, module_path) + if inject_jest: + test_source = "import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from '@jest/globals'\n" + test_source + + if is_ts: + # Disable TypeScript checks + test_source = "// @ts-nocheck\n" + test_source + # Check if already instrumented - if "codeflash-jest-helper" in test_source: + if "codeflash" in test_source: # If already instrumented, convert between modes if needed if mode == "performance": # Convert to performance mode @@ -105,8 +119,7 @@ def instrument_javascript_tests( result_lines = [] # Add helper import at the top, after any existing imports - # Use relative path - helper is in same directory as test files (tests/) - helper_import = "const codeflash = require('./codeflash-jest-helper');" + helper_import = "const codeflash = require('codeflash');" import_inserted = False in_import_block = False @@ -187,6 +200,16 @@ def _comment_out_expects(source: str) -> str: while i < length: # Check for expect( pattern if source[i : i + 7] == "expect(": + # Check if there's an 'await' keyword before expect (with optional whitespace) + await_start = None + if len(result) >= 6: + # Look back in the result to find 'await' followed by whitespace + result_str = "".join(result) + # Check for 'await' followed by whitespace at the end + await_match = re.search(r"await\s*$", result_str) + if await_match: + await_start = await_match.start() + # Check if this is a codeflash capture call (capture or capturePerf) capture_match = re.match(r"expect\((codeflash\.capture(?:Perf)?)\(", source[i:]) if capture_match: @@ -194,6 +217,12 @@ def _comment_out_expects(source: str) -> str: result_str = _extract_and_transform_capture_expect(source, i) if result_str: transformed, end_pos = result_str + # If there was an await, keep it before the capture call + # e.g., await codeflash.capturePerf(...); // [codeflash-disabled] .resolves.toBe(...) + if await_start is not None: + result_joined = "".join(result) + result = [result_joined[:await_start]] + transformed = "await " + transformed result.append(transformed) i = end_pos continue @@ -202,6 +231,12 @@ def _comment_out_expects(source: str) -> str: statement_end = _find_statement_end(source, i) statement = source[i:statement_end] + # If there was an await, include it in the statement and remove from result + if await_start is not None: + result_joined = "".join(result) + result = [result_joined[:await_start]] + statement = "await " + statement + # Check if it spans multiple lines if "\n" in statement: # Use block comment for multi-line statements @@ -223,7 +258,11 @@ def _find_statement_end(source: str, start: int) -> int: """Find the end of a JavaScript statement starting at the given position. Tracks parentheses to handle multi-line statements properly. - The statement ends at the semicolon or closing brace after all parens are matched. + The statement ends at: + - A semicolon when all parens are closed + - A newline when all parens are closed (and not followed by .to* matcher) + - When paren_count goes negative (we hit a closing paren belonging to outer context, + like an arrow function callback: `arr.forEach((x) => expect(x).toBe(1))`) """ i = start length = len(source) @@ -255,6 +294,11 @@ def _find_statement_end(source: str, start: int) -> int: paren_count += 1 elif char == ")": paren_count -= 1 + # If paren_count goes negative, we've hit a closing paren that belongs + # to an outer context (e.g., the closing paren of a forEach callback). + # The statement ends just before this character. + if paren_count < 0: + return i # Statement ends at semicolon or newline when all parens are closed if paren_count == 0: diff --git a/django/aiservice/testgen/prompts/javascript/execute_async_system_prompt.md b/django/aiservice/testgen/prompts/javascript/execute_async_system_prompt.md index b96d863fb..42ec28680 100644 --- a/django/aiservice/testgen/prompts/javascript/execute_async_system_prompt.md +++ b/django/aiservice/testgen/prompts/javascript/execute_async_system_prompt.md @@ -30,6 +30,13 @@ - **CRITICAL: DO NOT MOCK THE FUNCTION UNDER TEST** - Never mock, stub, or spy on the {function_name} function itself. - **CRITICAL: TEST REJECTION CASES** - Use `expect(...).rejects.toThrow()` for testing async errors. +**CRITICAL: MOCKING RULES FOR JEST**: +- **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. +- **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. +- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics.js')` ✓ +- **ALWAYS include the `.js` extension** in mock paths when the project uses ESM imports. + + **Output Format Requirements**: - Your response MUST be a single markdown code block containing valid JavaScript/TypeScript code. - Do NOT nest code blocks inside each other. diff --git a/django/aiservice/testgen/prompts/javascript/execute_system_prompt.md b/django/aiservice/testgen/prompts/javascript/execute_system_prompt.md index 77c859b65..e1d42c1dd 100644 --- a/django/aiservice/testgen/prompts/javascript/execute_system_prompt.md +++ b/django/aiservice/testgen/prompts/javascript/execute_system_prompt.md @@ -21,6 +21,13 @@ - **CRITICAL: IMPORT FROM REAL MODULES** - Import the function and any related classes/utilities from their actual module paths as shown in the context. - **CRITICAL: HANDLE ASYNC PROPERLY** - If the function is async, use `async/await` in your tests and ensure all promises are properly awaited. +**CRITICAL: MOCKING RULES FOR JEST**: +- **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. +- **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. +- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics.js')` ✓ +- **ALWAYS include the `.js` extension** in mock paths when the project uses ESM imports. + + **Output Format Requirements**: - Your response MUST be a single markdown code block containing valid JavaScript/TypeScript code. - Do NOT nest code blocks inside each other. diff --git a/django/aiservice/testgen/testgen_javascript.py b/django/aiservice/testgen/testgen_javascript.py index e9728f118..9cba60139 100644 --- a/django/aiservice/testgen/testgen_javascript.py +++ b/django/aiservice/testgen/testgen_javascript.py @@ -207,6 +207,7 @@ async def generate_javascript_tests_from_function( is_async: bool = False, trace_id: str = "", call_sequence: int | None = None, + language: str = "javascript", ) -> tuple[str, str, str]: """Generate JavaScript tests for a function. @@ -220,6 +221,7 @@ async def generate_javascript_tests_from_function( is_async: Whether function is async trace_id: Trace ID for logging call_sequence: Call sequence number + language: Language of the function to test (javascript, typescript) Returns: Tuple of (generated_tests, instrumented_behavior_tests, instrumented_perf_tests) @@ -252,14 +254,26 @@ async def generate_javascript_tests_from_function( total_llm_cost = sum(cost_tracker) await update_optimization_cost(trace_id=trace_id, cost=total_llm_cost, user_id=user_id) + is_ts = language == "typescript" + # Apply behavior instrumentation - uses codeflash.capture() to write to SQLite behavior_instrumented = instrument_javascript_tests( - test_source=validated_code, function_name=function_name, module_path=module_path, mode="behavior" + test_source=validated_code, + function_name=function_name, + module_path=module_path, + is_ts=is_ts, + mode="behavior", + inject_jest=True, ) # Apply performance instrumentation - uses codeflash.capturePerf() for stdout timing perf_instrumented = instrument_javascript_tests( - test_source=validated_code, function_name=function_name, module_path=module_path, mode="performance" + test_source=validated_code, + function_name=function_name, + module_path=module_path, + is_ts=is_ts, + mode="performance", + inject_jest=True, # TODO: let the client decide if it wants jest to be injected or not ) return validated_code, behavior_instrumented, perf_instrumented @@ -354,6 +368,7 @@ async def testgen_javascript( trace_id=data.trace_id, call_sequence=data.call_sequence, execute_model=execute_model, + language=data.language, ) ph(request.user, "aiservice-testgen-tests-generated", properties={"language": language}) From b0a1d6c09f0cfab241f8aeae688bf00cb8ded42b Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Tue, 27 Jan 2026 16:10:35 -0800 Subject: [PATCH 003/184] Remove instrumentation of js tests from aiservice and into client --- .../instrumentation/javascript/__init__.py | 9 - .../javascript/codeflash-comparator.js | 406 --------- .../javascript/codeflash-compare-results.js | 313 ------- .../javascript/codeflash-jest-helper.js | 823 ----------------- .../javascript/codeflash-serializer.js | 851 ------------------ .../javascript/instrument_javascript.py | 661 -------------- .../aiservice/testgen/testgen_javascript.py | 20 +- .../test_instrument_javascript.py | 287 ------ .../test_jest_helper.py | 150 --- 9 files changed, 5 insertions(+), 3515 deletions(-) delete mode 100644 django/aiservice/testgen/instrumentation/javascript/__init__.py delete mode 100644 django/aiservice/testgen/instrumentation/javascript/codeflash-comparator.js delete mode 100644 django/aiservice/testgen/instrumentation/javascript/codeflash-compare-results.js delete mode 100644 django/aiservice/testgen/instrumentation/javascript/codeflash-jest-helper.js delete mode 100644 django/aiservice/testgen/instrumentation/javascript/codeflash-serializer.js delete mode 100644 django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py delete mode 100644 django/aiservice/tests/testgen_instrumentation/test_instrument_javascript.py delete mode 100644 django/aiservice/tests/testgen_instrumentation/test_jest_helper.py diff --git a/django/aiservice/testgen/instrumentation/javascript/__init__.py b/django/aiservice/testgen/instrumentation/javascript/__init__.py deleted file mode 100644 index 616d13ac9..000000000 --- a/django/aiservice/testgen/instrumentation/javascript/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""JavaScript test instrumentation module. - -This module provides unified instrumentation for JavaScript tests, -working identically for both generated and existing tests. -""" - -from testgen.instrumentation.javascript.instrument_javascript import get_jest_helper_path, instrument_javascript_tests - -__all__ = ["get_jest_helper_path", "instrument_javascript_tests"] diff --git a/django/aiservice/testgen/instrumentation/javascript/codeflash-comparator.js b/django/aiservice/testgen/instrumentation/javascript/codeflash-comparator.js deleted file mode 100644 index 298c535b6..000000000 --- a/django/aiservice/testgen/instrumentation/javascript/codeflash-comparator.js +++ /dev/null @@ -1,406 +0,0 @@ -/** - * Codeflash Comparator - Deep equality comparison for JavaScript values - * - * This module provides a robust comparator function for comparing JavaScript - * values to determine behavioral equivalence between original and optimized code. - * - * Features: - * - Handles all JavaScript primitive types - * - Floating point comparison with relative tolerance (like Python's math.isclose) - * - Deep comparison of objects, arrays, Maps, Sets - * - Handles special values: NaN, Infinity, -Infinity, undefined, null - * - Handles TypedArrays, Date, RegExp, Error objects - * - Circular reference detection - * - Superset mode: allows new object to have additional keys - * - * Usage: - * const { comparator } = require('./codeflash-comparator'); - * comparator(original, optimized); // Exact comparison - * comparator(original, optimized, { supersetObj: true }); // Allow extra keys - */ - -'use strict'; - -/** - * Default options for the comparator. - */ -const DEFAULT_OPTIONS = { - // Relative tolerance for floating point comparison (like Python's rtol) - rtol: 1e-9, - // Absolute tolerance for floating point comparison (like Python's atol) - atol: 0, - // If true, the new object is allowed to have more keys than the original - supersetObj: false, - // Maximum recursion depth to prevent stack overflow - maxDepth: 1000, -}; - -/** - * Check if two floating point numbers are close within tolerance. - * Equivalent to Python's math.isclose(a, b, rel_tol, abs_tol). - * - * @param {number} a - First number - * @param {number} b - Second number - * @param {number} rtol - Relative tolerance (default: 1e-9) - * @param {number} atol - Absolute tolerance (default: 0) - * @returns {boolean} - True if numbers are close - */ -function isClose(a, b, rtol = 1e-9, atol = 0) { - // Handle identical values (including both being 0) - if (a === b) return true; - - // Handle NaN - if (Number.isNaN(a) && Number.isNaN(b)) return true; - if (Number.isNaN(a) || Number.isNaN(b)) return false; - - // Handle Infinity - if (!Number.isFinite(a) || !Number.isFinite(b)) { - return a === b; // Both must be same infinity - } - - // Use the same formula as Python's math.isclose - // abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) - const diff = Math.abs(a - b); - const maxAbs = Math.max(Math.abs(a), Math.abs(b)); - return diff <= Math.max(rtol * maxAbs, atol); -} - -/** - * Get the precise type of a value for comparison. - * - * @param {any} value - The value to get the type of - * @returns {string} - The type name - */ -function getType(value) { - if (value === null) return 'null'; - if (value === undefined) return 'undefined'; - - const type = typeof value; - if (type !== 'object') return type; - - // Get the constructor name for objects - const constructorName = value.constructor?.name; - if (constructorName) return constructorName; - - // Fallback to Object.prototype.toString - return Object.prototype.toString.call(value).slice(8, -1); -} - -/** - * Check if a value is a TypedArray. - * - * @param {any} value - The value to check - * @returns {boolean} - True if TypedArray - */ -function isTypedArray(value) { - return ArrayBuffer.isView(value) && !(value instanceof DataView); -} - -/** - * Compare two values for deep equality. - * - * @param {any} orig - Original value - * @param {any} newVal - New value to compare - * @param {Object} options - Comparison options - * @param {number} options.rtol - Relative tolerance for floats - * @param {number} options.atol - Absolute tolerance for floats - * @param {boolean} options.supersetObj - Allow new object to have extra keys - * @param {number} options.maxDepth - Maximum recursion depth - * @returns {boolean} - True if values are equivalent - */ -function comparator(orig, newVal, options = {}) { - const opts = { ...DEFAULT_OPTIONS, ...options }; - - // Track visited objects to handle circular references - const visited = new WeakMap(); - - function compare(a, b, depth) { - // Check recursion depth - if (depth > opts.maxDepth) { - console.warn('[comparator] Maximum recursion depth exceeded'); - return false; - } - - // === Identical references === - if (a === b) return true; - - // === Handle null and undefined === - if (a === null || a === undefined || b === null || b === undefined) { - return a === b; - } - - // === Type checking === - const typeA = typeof a; - const typeB = typeof b; - - if (typeA !== typeB) { - // Special case: comparing number with BigInt - // In JavaScript, 1n !== 1, but we might want to consider them equal - // For strict behavioral comparison, we'll say they're different - return false; - } - - // === Primitives === - - // Numbers (including NaN and Infinity) - if (typeA === 'number') { - return isClose(a, b, opts.rtol, opts.atol); - } - - // Strings, booleans - if (typeA === 'string' || typeA === 'boolean') { - return a === b; - } - - // BigInt - if (typeA === 'bigint') { - return a === b; - } - - // Symbols - compare by description since Symbol() always creates unique - if (typeA === 'symbol') { - return a.description === b.description; - } - - // Functions - compare by reference (same function) - if (typeA === 'function') { - // Functions are equal if they're the same reference - // or if they have the same name and source code - if (a === b) return true; - // For bound functions or native functions, we can only compare by reference - try { - return a.name === b.name && a.toString() === b.toString(); - } catch (e) { - return false; - } - } - - // === Objects (typeA === 'object') === - - // Check for circular references - if (visited.has(a)) { - // If we've seen 'a' before, check if 'b' was the corresponding value - return visited.get(a) === b; - } - - // Get constructor names for type comparison - const constructorA = a.constructor?.name || 'Object'; - const constructorB = b.constructor?.name || 'Object'; - - // Different constructors means different types - // Exception: plain objects might have different constructors due to different realms - if (constructorA !== constructorB) { - // Allow comparison between plain objects from different realms - if (!(constructorA === 'Object' && constructorB === 'Object')) { - return false; - } - } - - // Mark as visited before recursing - visited.set(a, b); - - try { - // === Arrays === - if (Array.isArray(a)) { - if (!Array.isArray(b)) return false; - if (a.length !== b.length) return false; - return a.every((elem, i) => compare(elem, b[i], depth + 1)); - } - - // === TypedArrays (Int8Array, Uint8Array, Float32Array, etc.) === - if (isTypedArray(a)) { - if (!isTypedArray(b)) return false; - if (a.constructor !== b.constructor) return false; - if (a.length !== b.length) return false; - - // For float arrays, use tolerance comparison - if (a instanceof Float32Array || a instanceof Float64Array) { - for (let i = 0; i < a.length; i++) { - if (!isClose(a[i], b[i], opts.rtol, opts.atol)) return false; - } - return true; - } - - // For integer arrays, use exact comparison - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; - } - - // === ArrayBuffer === - if (a instanceof ArrayBuffer) { - if (!(b instanceof ArrayBuffer)) return false; - if (a.byteLength !== b.byteLength) return false; - const viewA = new Uint8Array(a); - const viewB = new Uint8Array(b); - for (let i = 0; i < viewA.length; i++) { - if (viewA[i] !== viewB[i]) return false; - } - return true; - } - - // === DataView === - if (a instanceof DataView) { - if (!(b instanceof DataView)) return false; - if (a.byteLength !== b.byteLength) return false; - for (let i = 0; i < a.byteLength; i++) { - if (a.getUint8(i) !== b.getUint8(i)) return false; - } - return true; - } - - // === Date === - if (a instanceof Date) { - if (!(b instanceof Date)) return false; - // Handle Invalid Date (NaN time) - const timeA = a.getTime(); - const timeB = b.getTime(); - if (Number.isNaN(timeA) && Number.isNaN(timeB)) return true; - return timeA === timeB; - } - - // === RegExp === - if (a instanceof RegExp) { - if (!(b instanceof RegExp)) return false; - return a.source === b.source && a.flags === b.flags; - } - - // === Error === - if (a instanceof Error) { - if (!(b instanceof Error)) return false; - // Compare error name and message - if (a.name !== b.name) return false; - if (a.message !== b.message) return false; - // Optionally compare stack traces (usually not, as they differ) - return true; - } - - // === Map === - if (a instanceof Map) { - if (!(b instanceof Map)) return false; - if (a.size !== b.size) return false; - for (const [key, val] of a) { - if (!b.has(key)) return false; - if (!compare(val, b.get(key), depth + 1)) return false; - } - return true; - } - - // === Set === - if (a instanceof Set) { - if (!(b instanceof Set)) return false; - if (a.size !== b.size) return false; - // For Sets, we need to find matching elements - // This is O(n^2) but necessary for deep comparison - const bArray = Array.from(b); - for (const valA of a) { - let found = false; - for (let i = 0; i < bArray.length; i++) { - if (compare(valA, bArray[i], depth + 1)) { - found = true; - bArray.splice(i, 1); // Remove matched element - break; - } - } - if (!found) return false; - } - return true; - } - - // === WeakMap / WeakSet === - // Cannot iterate over these, so we can only compare by reference - if (a instanceof WeakMap || a instanceof WeakSet) { - return a === b; - } - - // === Promise === - // Promises can only be compared by reference - if (a instanceof Promise) { - return a === b; - } - - // === URL === - if (typeof URL !== 'undefined' && a instanceof URL) { - if (!(b instanceof URL)) return false; - return a.href === b.href; - } - - // === URLSearchParams === - if (typeof URLSearchParams !== 'undefined' && a instanceof URLSearchParams) { - if (!(b instanceof URLSearchParams)) return false; - return a.toString() === b.toString(); - } - - // === Plain Objects === - // This includes class instances - - const keysA = Object.keys(a); - const keysB = Object.keys(b); - - if (opts.supersetObj) { - // In superset mode, all keys from original must exist in new - // but new can have additional keys - for (const key of keysA) { - if (!(key in b)) return false; - if (!compare(a[key], b[key], depth + 1)) return false; - } - return true; - } else { - // Exact key matching - if (keysA.length !== keysB.length) return false; - - for (const key of keysA) { - if (!(key in b)) return false; - if (!compare(a[key], b[key], depth + 1)) return false; - } - return true; - } - } finally { - // Clean up visited tracking - // Note: We don't delete from visited because the same object - // might appear multiple times in the structure - } - } - - try { - return compare(orig, newVal, 0); - } catch (e) { - console.error('[comparator] Error during comparison:', e); - return false; - } -} - -/** - * Create a comparator with custom default options. - * - * @param {Object} defaultOptions - Default options for all comparisons - * @returns {Function} - Comparator function with bound defaults - */ -function createComparator(defaultOptions = {}) { - const opts = { ...DEFAULT_OPTIONS, ...defaultOptions }; - return (orig, newVal, overrideOptions = {}) => { - return comparator(orig, newVal, { ...opts, ...overrideOptions }); - }; -} - -/** - * Strict comparator that requires exact equality (no tolerance). - */ -const strictComparator = createComparator({ rtol: 0, atol: 0 }); - -/** - * Loose comparator with larger tolerance for floating point. - */ -const looseComparator = createComparator({ rtol: 1e-6, atol: 1e-9 }); - -// Export public API -module.exports = { - comparator, - createComparator, - strictComparator, - looseComparator, - isClose, - getType, - DEFAULT_OPTIONS, -}; diff --git a/django/aiservice/testgen/instrumentation/javascript/codeflash-compare-results.js b/django/aiservice/testgen/instrumentation/javascript/codeflash-compare-results.js deleted file mode 100644 index fc1fe667b..000000000 --- a/django/aiservice/testgen/instrumentation/javascript/codeflash-compare-results.js +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/env node -/** - * Codeflash Result Comparator - * - * This script compares test results between original and optimized code runs. - * It reads serialized behavior data from SQLite databases and compares them - * using the codeflash-comparator in JavaScript land. - * - * Usage: - * node codeflash-compare-results.js - * node codeflash-compare-results.js --json - * - * Output (JSON): - * { - * "equivalent": true/false, - * "diffs": [ - * { - * "invocation_id": "...", - * "scope": "return_value|stdout|did_pass", - * "original": "...", - * "candidate": "..." - * } - * ], - * "error": null | "error message" - * } - */ - -const fs = require('fs'); -const path = require('path'); - -// Import our modules -const { deserialize } = require('./codeflash-serializer'); -const { comparator } = require('./codeflash-comparator'); - -// Try to load better-sqlite3 -let Database; -try { - Database = require('better-sqlite3'); -} catch (e) { - console.error(JSON.stringify({ - equivalent: false, - diffs: [], - error: 'better-sqlite3 not installed' - })); - process.exit(1); -} - -/** - * Read test results from a SQLite database. - * - * @param {string} dbPath - Path to SQLite database - * @returns {Map} Map of invocation_id -> result object - */ -function readTestResults(dbPath) { - const results = new Map(); - - if (!fs.existsSync(dbPath)) { - throw new Error(`Database not found: ${dbPath}`); - } - - const db = new Database(dbPath, { readonly: true }); - - try { - const stmt = db.prepare(` - SELECT - test_module_path, - test_class_name, - test_function_name, - function_getting_tested, - loop_index, - iteration_id, - runtime, - return_value, - verification_type - FROM test_results - WHERE loop_index = 1 - `); - - for (const row of stmt.iterate()) { - // Build unique invocation ID (matches Python's format) - const invocationId = `${row.loop_index}:${row.test_module_path}:${row.test_class_name || ''}:${row.test_function_name}:${row.function_getting_tested}:${row.iteration_id}`; - - // Deserialize the return value - let returnValue = null; - if (row.return_value) { - try { - returnValue = deserialize(row.return_value); - } catch (e) { - console.error(`Failed to deserialize result for ${invocationId}: ${e.message}`); - } - } - - results.set(invocationId, { - testModulePath: row.test_module_path, - testClassName: row.test_class_name, - testFunctionName: row.test_function_name, - functionGettingTested: row.function_getting_tested, - loopIndex: row.loop_index, - iterationId: row.iteration_id, - runtime: row.runtime, - returnValue, - verificationType: row.verification_type, - }); - } - } finally { - db.close(); - } - - return results; -} - -/** - * Compare two sets of test results. - * - * @param {Map} originalResults - Results from original code - * @param {Map} candidateResults - Results from optimized code - * @returns {object} Comparison result - */ -function compareResults(originalResults, candidateResults) { - const diffs = []; - let allEquivalent = true; - - // Get all unique invocation IDs - const allIds = new Set([...originalResults.keys(), ...candidateResults.keys()]); - - for (const invocationId of allIds) { - const original = originalResults.get(invocationId); - const candidate = candidateResults.get(invocationId); - - // If candidate has extra results not in original, that's OK - if (candidate && !original) { - continue; - } - - // If original has results not in candidate, that's a diff - if (original && !candidate) { - allEquivalent = false; - diffs.push({ - invocation_id: invocationId, - scope: 'missing', - original: summarizeValue(original.returnValue), - candidate: null, - test_info: { - test_module_path: original.testModulePath, - test_function_name: original.testFunctionName, - function_getting_tested: original.functionGettingTested, - } - }); - continue; - } - - // Compare return values using the JavaScript comparator - // The return value format is [args, kwargs, returnValue] (behavior tuple) - const originalValue = original.returnValue; - const candidateValue = candidate.returnValue; - - const isEqual = comparator(originalValue, candidateValue); - - if (!isEqual) { - allEquivalent = false; - diffs.push({ - invocation_id: invocationId, - scope: 'return_value', - original: summarizeValue(originalValue), - candidate: summarizeValue(candidateValue), - test_info: { - test_module_path: original.testModulePath, - test_function_name: original.testFunctionName, - function_getting_tested: original.functionGettingTested, - } - }); - } - } - - return { - equivalent: allEquivalent, - diffs, - total_invocations: allIds.size, - original_count: originalResults.size, - candidate_count: candidateResults.size, - }; -} - -/** - * Create a summary of a value for diff reporting. - * Truncates long values to avoid huge output. - * - * @param {any} value - Value to summarize - * @returns {string} String representation - */ -function summarizeValue(value, maxLength = 500) { - try { - let str; - if (value === undefined) { - str = 'undefined'; - } else if (value === null) { - str = 'null'; - } else if (typeof value === 'function') { - str = `[Function: ${value.name || 'anonymous'}]`; - } else if (value instanceof Map) { - str = `Map(${value.size}) { ${[...value.entries()].slice(0, 3).map(([k, v]) => `${summarizeValue(k, 50)} => ${summarizeValue(v, 50)}`).join(', ')}${value.size > 3 ? ', ...' : ''} }`; - } else if (value instanceof Set) { - str = `Set(${value.size}) { ${[...value].slice(0, 3).map(v => summarizeValue(v, 50)).join(', ')}${value.size > 3 ? ', ...' : ''} }`; - } else if (value instanceof Date) { - str = value.toISOString(); - } else if (Array.isArray(value)) { - if (value.length <= 5) { - str = JSON.stringify(value); - } else { - str = `[${value.slice(0, 3).map(v => summarizeValue(v, 50)).join(', ')}, ... (${value.length} items)]`; - } - } else if (typeof value === 'object') { - str = JSON.stringify(value); - } else { - str = String(value); - } - - if (str.length > maxLength) { - return str.slice(0, maxLength - 3) + '...'; - } - return str; - } catch (e) { - return `[Unable to stringify: ${e.message}]`; - } -} - -/** - * Compare results from serialized buffers directly (for stdin input). - * - * @param {Buffer} originalBuffer - Serialized original result - * @param {Buffer} candidateBuffer - Serialized candidate result - * @returns {boolean} True if equivalent - */ -function compareBuffers(originalBuffer, candidateBuffer) { - try { - const original = deserialize(originalBuffer); - const candidate = deserialize(candidateBuffer); - return comparator(original, candidate); - } catch (e) { - console.error(`Comparison error: ${e.message}`); - return false; - } -} - -/** - * Main entry point. - */ -function main() { - const args = process.argv.slice(2); - - if (args.length === 0) { - console.error('Usage: node codeflash-compare-results.js '); - console.error(' node codeflash-compare-results.js --stdin (reads JSON from stdin)'); - process.exit(1); - } - - // Handle stdin mode for programmatic use - if (args[0] === '--stdin') { - let input = ''; - process.stdin.setEncoding('utf8'); - process.stdin.on('data', chunk => input += chunk); - process.stdin.on('end', () => { - try { - const data = JSON.parse(input); - const originalBuffer = Buffer.from(data.original, 'base64'); - const candidateBuffer = Buffer.from(data.candidate, 'base64'); - const isEqual = compareBuffers(originalBuffer, candidateBuffer); - console.log(JSON.stringify({ equivalent: isEqual, error: null })); - } catch (e) { - console.log(JSON.stringify({ equivalent: false, error: e.message })); - } - }); - return; - } - - // Standard mode: compare two SQLite databases - if (args.length < 2) { - console.error('Usage: node codeflash-compare-results.js '); - process.exit(1); - } - - const [originalDb, candidateDb] = args; - - try { - const originalResults = readTestResults(originalDb); - const candidateResults = readTestResults(candidateDb); - - const comparison = compareResults(originalResults, candidateResults); - - console.log(JSON.stringify(comparison, null, 2)); - process.exit(comparison.equivalent ? 0 : 1); - } catch (e) { - console.log(JSON.stringify({ - equivalent: false, - diffs: [], - error: e.message - })); - process.exit(1); - } -} - -// Export for programmatic use -module.exports = { - readTestResults, - compareResults, - compareBuffers, - summarizeValue, -}; - -// Run if called directly -if (require.main === module) { - main(); -} diff --git a/django/aiservice/testgen/instrumentation/javascript/codeflash-jest-helper.js b/django/aiservice/testgen/instrumentation/javascript/codeflash-jest-helper.js deleted file mode 100644 index 639700fe7..000000000 --- a/django/aiservice/testgen/instrumentation/javascript/codeflash-jest-helper.js +++ /dev/null @@ -1,823 +0,0 @@ -/** - * Codeflash Jest Helper - Unified Test Instrumentation - * - * This module provides a unified approach to instrumenting JavaScript tests - * for both behavior verification and performance measurement. - * - * The instrumentation mirrors Python's codeflash implementation: - * - Static identifiers (testModule, testFunction, lineId) are passed at instrumentation time - * - Dynamic invocation counter increments only when same call site is seen again (e.g., in loops) - * - Uses hrtime for nanosecond precision timing - * - SQLite for consistent data format with Python implementation - * - * Usage: - * const codeflash = require('./codeflash-jest-helper'); - * - * // For behavior verification (writes to SQLite): - * const result = codeflash.capture('functionName', lineId, targetFunction, arg1, arg2); - * - * // For performance benchmarking (stdout only): - * const result = codeflash.capturePerf('functionName', lineId, targetFunction, arg1, arg2); - * - * Environment Variables: - * CODEFLASH_OUTPUT_FILE - Path to write results SQLite file - * CODEFLASH_LOOP_INDEX - Current benchmark loop iteration (default: 1) - * CODEFLASH_TEST_ITERATION - Test iteration number (default: 0) - * CODEFLASH_TEST_MODULE - Test module path - */ - -const fs = require("fs") -const path = require("path") - -// Load the codeflash serializer for robust value serialization -const serializer = require("./codeflash-serializer") - -// Try to load better-sqlite3, fall back to JSON if not available -let Database -let useSqlite = false -try { - Database = require("better-sqlite3") - useSqlite = true -} catch (e) { - // better-sqlite3 not available, will use JSON fallback - console.warn("[codeflash] better-sqlite3 not found, using JSON fallback") -} - -// Configuration from environment -const OUTPUT_FILE = process.env.CODEFLASH_OUTPUT_FILE || "/tmp/codeflash_results.sqlite" -const LOOP_INDEX = parseInt(process.env.CODEFLASH_LOOP_INDEX || "1", 10) -const TEST_ITERATION = process.env.CODEFLASH_TEST_ITERATION || "0" -const TEST_MODULE = process.env.CODEFLASH_TEST_MODULE || "" - -// Looping configuration for performance benchmarking -const MIN_LOOPS = parseInt(process.env.CODEFLASH_MIN_LOOPS || "5", 10) -const MAX_LOOPS = parseInt(process.env.CODEFLASH_MAX_LOOPS || "100000", 10) -const TARGET_DURATION_MS = parseInt(process.env.CODEFLASH_TARGET_DURATION_MS || "10000", 10) -const STABILITY_CHECK = process.env.CODEFLASH_STABILITY_CHECK !== "false" - -// Stability checking constants (matching Python's pytest_plugin.py) -const STABILITY_WINDOW_SIZE = 0.35 // 35% of estimated total loops -const STABILITY_CENTER_TOLERANCE = 0.0025 // ±0.25% around median -const STABILITY_SPREAD_TOLERANCE = 0.0025 // 0.25% window spread - -// Current test context (set by Jest hooks) -let currentTestName = null -let currentTestPath = null // Test file path from Jest - -// Invocation counter map: tracks how many times each testId has been seen -// Key: testId (testModule:testClass:testFunction:lineId:loopIndex) -// Value: count (starts at 0, increments each time same key is seen) -const invocationCounterMap = new Map() - -// Results buffer (for JSON fallback) -const results = [] - -// SQLite database (lazy initialized) -let db = null - -/** - * Get high-resolution time in nanoseconds. - * Prefers process.hrtime.bigint() for nanosecond precision, - * falls back to performance.now() * 1e6 for non-Node environments. - * - * @returns {bigint|number} - Time in nanoseconds - */ -function getTimeNs() { - if (typeof process !== "undefined" && process.hrtime && process.hrtime.bigint) { - return process.hrtime.bigint() - } - // Fallback to performance.now() in milliseconds, converted to nanoseconds - const { performance } = require("perf_hooks") - return BigInt(Math.floor(performance.now() * 1_000_000)) -} - -/** - * Calculate duration in nanoseconds. - * - * @param {bigint} start - Start time in nanoseconds - * @param {bigint} end - End time in nanoseconds - * @returns {number} - Duration in nanoseconds (as Number for SQLite compatibility) - */ -function getDurationNs(start, end) { - const duration = end - start - // Convert to Number for SQLite storage (SQLite INTEGER is 64-bit) - return Number(duration) -} - -/** - * Sanitize a string for use in test IDs. - * Replaces special characters that could conflict with regex extraction - * during stdout parsing. - * - * Characters replaced with '_': ! # : (space) ( ) [ ] { } | \ / * ? ^ $ . + - - * - * @param {string} str - String to sanitize - * @returns {string} - Sanitized string safe for test IDs - */ -function sanitizeTestId(str) { - if (!str) return str - // Replace characters that could conflict with our delimiter pattern (######) - // or the colon-separated format, or general regex metacharacters - return str.replace(/[!#: ()\[\]{}|\\/*?^$.+\-]/g, "_") -} - -/** - * Get or create invocation index for a testId. - * This mirrors Python's index tracking per wrapper function. - * - * @param {string} testId - Unique test identifier - * @returns {number} - Current invocation index (0-based) - */ -function getInvocationIndex(testId) { - const currentIndex = invocationCounterMap.get(testId) - if (currentIndex === undefined) { - invocationCounterMap.set(testId, 0) - return 0 - } - invocationCounterMap.set(testId, currentIndex + 1) - return currentIndex + 1 -} - -/** - * Reset invocation counter for a test. - * Called at the start of each test to ensure consistent indexing. - */ -function resetInvocationCounters() { - invocationCounterMap.clear() -} - -/** - * Initialize the SQLite database. - */ -function initDatabase() { - if (!useSqlite || db) return - - try { - db = new Database(OUTPUT_FILE) - db.exec(` - CREATE TABLE IF NOT EXISTS test_results ( - test_module_path TEXT, - test_class_name TEXT, - test_function_name TEXT, - function_getting_tested TEXT, - loop_index INTEGER, - iteration_id TEXT, - runtime INTEGER, - return_value BLOB, - verification_type TEXT - ) - `) - } catch (e) { - console.error("[codeflash] Failed to initialize SQLite:", e.message) - useSqlite = false - } -} - -/** - * Safely serialize a value for storage. - * - * @param {any} value - Value to serialize - * @returns {Buffer} - Serialized value as Buffer - */ -function safeSerialize(value) { - try { - return serializer.serialize(value) - } catch (e) { - console.warn("[codeflash] Serialization failed:", e.message) - return Buffer.from(JSON.stringify({ __type: "SerializationError", error: e.message })) - } -} - -/** - * Safely deserialize a buffer back to a value. - * - * @param {Buffer|Uint8Array} buffer - Serialized buffer - * @returns {any} - Deserialized value - */ -function safeDeserialize(buffer) { - try { - return serializer.deserialize(buffer) - } catch (e) { - console.warn("[codeflash] Deserialization failed:", e.message) - return { __type: "DeserializationError", error: e.message } - } -} - -/** - * Record a test result to SQLite or JSON buffer. - * - * @param {string} testModulePath - Test module path - * @param {string|null} testClassName - Test class name (null for Jest) - * @param {string} testFunctionName - Test function name - * @param {string} funcName - Name of the function being tested - * @param {string} invocationId - Unique invocation identifier (lineId_index) - * @param {Array} args - Arguments passed to the function - * @param {any} returnValue - Return value from the function - * @param {Error|null} error - Error thrown by the function (if any) - * @param {number} durationNs - Execution time in nanoseconds - */ -function recordResult( - testModulePath, - testClassName, - testFunctionName, - funcName, - invocationId, - args, - returnValue, - error, - durationNs, -) { - // Serialize the return value (args, kwargs (empty for JS), return_value) like Python does - const serializedValue = error ? safeSerialize(error) : safeSerialize([args, {}, returnValue]) - - if (useSqlite && db) { - try { - const stmt = db.prepare(` - INSERT INTO test_results VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `) - stmt.run( - testModulePath, // test_module_path - testClassName, // test_class_name - testFunctionName, // test_function_name - funcName, // function_getting_tested - LOOP_INDEX, // loop_index - invocationId, // iteration_id - durationNs, // runtime (nanoseconds) - no rounding - serializedValue, // return_value (serialized) - "function_call", // verification_type - ) - } catch (e) { - console.error("[codeflash] Failed to write to SQLite:", e.message) - // Fall back to JSON - results.push({ - testModulePath, - testClassName, - testFunctionName, - funcName, - loopIndex: LOOP_INDEX, - iterationId: invocationId, - durationNs, - returnValue: error ? null : returnValue, - error: error ? { name: error.name, message: error.message } : null, - verificationType: "function_call", - }) - } - } else { - // JSON fallback - results.push({ - testModulePath, - testClassName, - testFunctionName, - funcName, - loopIndex: LOOP_INDEX, - iterationId: invocationId, - durationNs, - returnValue: error ? null : returnValue, - error: error ? { name: error.name, message: error.message } : null, - verificationType: "function_call", - }) - } -} - -/** - * Capture a function call with full behavior tracking. - * - * This is the main API for instrumenting function calls for BEHAVIOR verification. - * It captures inputs, outputs, errors, and timing. - * Results are written to SQLite for comparison between original and optimized code. - * - * Static parameters (funcName, lineId) are determined at instrumentation time. - * The lineId enables tracking when the same call site is invoked multiple times (e.g., in loops). - * - * @param {string} funcName - Name of the function being tested (static) - * @param {string} lineId - Line number identifier in test file (static) - * @param {Function} fn - The function to call - * @param {...any} args - Arguments to pass to the function - * @returns {any} - The function's return value - * @throws {Error} - Re-throws any error from the function - */ -function capture(funcName, lineId, fn, ...args) { - // Initialize database on first capture - initDatabase() - - // Get test context (raw values for SQLite storage) - // Use TEST_MODULE env var if set, otherwise derive from test file path - let testModulePath - if (TEST_MODULE) { - testModulePath = TEST_MODULE - } else if (currentTestPath) { - // Get relative path from cwd and convert to module-style path - const path = require("path") - const relativePath = path.relative(process.cwd(), currentTestPath) - // Convert to Python module-style path (e.g., "tests/test_foo.test.js" -> "tests.test_foo.test") - // This matches what Jest's junit XML produces - testModulePath = relativePath - .replace(/\\/g, "/") // Handle Windows paths - .replace(/\.js$/, "") // Remove .js extension - .replace(/\.test$/, ".test") // Keep .test suffix - .replace(/\//g, ".") // Convert path separators to dots - } else { - testModulePath = currentTestName || "unknown" - } - const testClassName = null // Jest doesn't use classes like Python - const testFunctionName = currentTestName || "unknown" - - // Sanitized versions for stdout tags (avoid regex conflicts) - const safeModulePath = sanitizeTestId(testModulePath) - const safeTestFunctionName = sanitizeTestId(testFunctionName) - - // Create testId for invocation tracking (matches Python format) - const testId = `${safeModulePath}:${testClassName}:${safeTestFunctionName}:${lineId}:${LOOP_INDEX}` - - // Get invocation index (increments if same testId seen again) - const invocationIndex = getInvocationIndex(testId) - const invocationId = `${lineId}_${invocationIndex}` - - // Format stdout tag (matches Python format, uses sanitized names) - const testStdoutTag = `${safeModulePath}:${testClassName ? testClassName + "." : ""}${safeTestFunctionName}:${funcName}:${LOOP_INDEX}:${invocationId}` - - // Print start tag - console.log(`!$######${testStdoutTag}######$!`) - - // Timing with nanosecond precision - const startTime = getTimeNs() - let returnValue - let error = null - - try { - returnValue = fn(...args) - - // Handle promises (async functions) - if (returnValue instanceof Promise) { - return returnValue.then( - (resolved) => { - const endTime = getTimeNs() - const durationNs = getDurationNs(startTime, endTime) - recordResult( - testModulePath, - testClassName, - testFunctionName, - funcName, - invocationId, - args, - resolved, - null, - durationNs, - ) - // Print end tag (no duration for behavior mode) - console.log(`!######${testStdoutTag}######!`) - return resolved - }, - (err) => { - const endTime = getTimeNs() - const durationNs = getDurationNs(startTime, endTime) - recordResult( - testModulePath, - testClassName, - testFunctionName, - funcName, - invocationId, - args, - null, - err, - durationNs, - ) - console.log(`!######${testStdoutTag}######!`) - throw err - }, - ) - } - } catch (e) { - error = e - } - - const endTime = getTimeNs() - const durationNs = getDurationNs(startTime, endTime) - recordResult( - testModulePath, - testClassName, - testFunctionName, - funcName, - invocationId, - args, - returnValue, - error, - durationNs, - ) - - // Print end tag (no duration for behavior mode, matching Python) - console.log(`!######${testStdoutTag}######!`) - - if (error) throw error - return returnValue -} - -/** - * Capture a function call for PERFORMANCE benchmarking only. - * - * This is a lightweight instrumentation that only measures timing. - * It prints start/end tags to stdout (no SQLite writes, no serialization overhead). - * Used when we've already verified behavior and just need accurate timing. - * - * The timing measurement is done exactly around the function call for accuracy. - * - * Output format matches Python's codeflash_performance wrapper: - * Start: !$######test_module:test_class.test_name:func_name:loop_index:invocation_id######$! - * End: !######test_module:test_class.test_name:func_name:loop_index:invocation_id:duration_ns######! - * - * @param {string} funcName - Name of the function being tested (static) - * @param {string} lineId - Line number identifier in test file (static) - * @param {Function} fn - The function to call - * @param {...any} args - Arguments to pass to the function - * @returns {any} - The function's return value - * @throws {Error} - Re-throws any error from the function - */ -function capturePerf(funcName, lineId, fn, ...args) { - // Get test context - // Use TEST_MODULE env var if set, otherwise derive from test file path - let testModulePath - if (TEST_MODULE) { - testModulePath = TEST_MODULE - } else if (currentTestPath) { - // Get relative path from cwd and convert to module-style path - const path = require("path") - const relativePath = path.relative(process.cwd(), currentTestPath) - // Convert to Python module-style path (e.g., "tests/test_foo.test.js" -> "tests.test_foo.test") - testModulePath = relativePath - .replace(/\\/g, "/") - .replace(/\.js$/, "") - .replace(/\.test$/, ".test") - .replace(/\//g, ".") - } else { - testModulePath = currentTestName || "unknown" - } - const testClassName = null // Jest doesn't use classes like Python - const testFunctionName = currentTestName || "unknown" - - // Sanitized versions for stdout tags (avoid regex conflicts) - const safeModulePath = sanitizeTestId(testModulePath) - const safeTestFunctionName = sanitizeTestId(testFunctionName) - - // Create testId for invocation tracking (matches Python format) - const testId = `${safeModulePath}:${testClassName}:${safeTestFunctionName}:${lineId}:${LOOP_INDEX}` - - // Get invocation index (increments if same testId seen again) - const invocationIndex = getInvocationIndex(testId) - const invocationId = `${lineId}_${invocationIndex}` - - // Format stdout tag (matches Python format, uses sanitized names) - const testStdoutTag = `${safeModulePath}:${testClassName ? testClassName + "." : ""}${safeTestFunctionName}:${funcName}:${LOOP_INDEX}:${invocationId}` - - // Print start tag - console.log(`!$######${testStdoutTag}######$!`) - - // Timing with nanosecond precision - exactly around the function call - let returnValue - let error = null - let durationNs - - try { - const startTime = getTimeNs() - returnValue = fn(...args) - const endTime = getTimeNs() - durationNs = getDurationNs(startTime, endTime) - - // Handle promises (async functions) - if (returnValue instanceof Promise) { - return returnValue.then( - (resolved) => { - // For async, we measure until resolution - const asyncEndTime = getTimeNs() - const asyncDurationNs = getDurationNs(startTime, asyncEndTime) - // Print end tag with timing - console.log(`!######${testStdoutTag}:${asyncDurationNs}######!`) - return resolved - }, - (err) => { - const asyncEndTime = getTimeNs() - const asyncDurationNs = getDurationNs(startTime, asyncEndTime) - // Print end tag with timing even on error - console.log(`!######${testStdoutTag}:${asyncDurationNs}######!`) - throw err - }, - ) - } - } catch (e) { - const endTime = getTimeNs() - // For sync errors, we still need to calculate duration - // Use a fallback if we didn't capture startTime yet - durationNs = 0 - error = e - } - - // Print end tag with timing (no rounding) - console.log(`!######${testStdoutTag}:${durationNs}######!`) - - if (error) throw error - return returnValue -} - -/** - * Check if performance measurements have stabilized. - * Implements the same stability criteria as Python's pytest_plugin.py. - * - * @param {number[]} runtimes - Array of runtime measurements - * @param {number} windowSize - Size of the window to check - * @returns {boolean} - True if performance has stabilized - */ -function checkStability(runtimes, windowSize) { - if (runtimes.length < windowSize || windowSize < 3) { - return false - } - - // Get recent window - const window = runtimes.slice(-windowSize) - - // Check center tolerance (all values within ±0.25% of median) - const sorted = [...window].sort((a, b) => a - b) - const medianIndex = Math.floor(sorted.length / 2) - const median = sorted[medianIndex] - const centerTolerance = median * STABILITY_CENTER_TOLERANCE - - const withinCenter = window.every((v) => Math.abs(v - median) <= centerTolerance) - if (!withinCenter) return false - - // Check spread tolerance (max-min ≤ 0.25% of min) - const minVal = Math.min(...window) - const maxVal = Math.max(...window) - const spreadTolerance = minVal * STABILITY_SPREAD_TOLERANCE - - return maxVal - minVal <= spreadTolerance -} - -/** - * Capture a function call with internal looping for stable performance measurement. - * - * This function runs the target function multiple times within a single test execution, - * similar to Python's pytest_plugin behavior. It provides stable timing by: - * - Running multiple iterations to warm up JIT - * - Continuing until timing stabilizes or time limit is reached - * - Outputting timing data for each iteration - * - * Environment Variables: - * CODEFLASH_MIN_LOOPS - Minimum number of loops (default: 5) - * CODEFLASH_MAX_LOOPS - Maximum number of loops (default: 100000) - * CODEFLASH_TARGET_DURATION_MS - Target duration in ms (default: 10000) - * CODEFLASH_STABILITY_CHECK - Enable stability checking (default: true) - * - * @param {string} funcName - Name of the function being tested (static) - * @param {string} lineId - Line number identifier in test file (static) - * @param {Function} fn - The function to call - * @param {...any} args - Arguments to pass to the function - * @returns {any} - The function's return value from the last iteration - * @throws {Error} - Re-throws any error from the function - */ -function capturePerfLooped(funcName, lineId, fn, ...args) { - // Get test context - // Use TEST_MODULE env var if set, otherwise derive from test file path - let testModulePath - if (TEST_MODULE) { - testModulePath = TEST_MODULE - } else if (currentTestPath) { - // Get relative path from cwd and convert to module-style path - const path = require("path") - const relativePath = path.relative(process.cwd(), currentTestPath) - // Convert to Python module-style path (e.g., "tests/test_foo.test.js" -> "tests.test_foo.test") - testModulePath = relativePath - .replace(/\\/g, "/") - .replace(/\.js$/, "") - .replace(/\.test$/, ".test") - .replace(/\//g, ".") - } else { - testModulePath = currentTestName || "unknown" - } - const testClassName = null // Jest doesn't use classes like Python - const testFunctionName = currentTestName || "unknown" - - // Sanitized versions for stdout tags (avoid regex conflicts) - const safeModulePath = sanitizeTestId(testModulePath) - const safeTestFunctionName = sanitizeTestId(testFunctionName) - - // Create base testId for invocation tracking - const baseTestId = `${safeModulePath}:${testClassName}:${safeTestFunctionName}:${lineId}` - - // Get invocation index (same call site in loops within test) - const invocationIndex = getInvocationIndex(baseTestId + ":base") - const invocationId = `${lineId}_${invocationIndex}` - - // Track runtimes for stability checking - const runtimes = [] - let returnValue - let error = null - - const loopStartTime = Date.now() - let loopCount = 0 - - while (true) { - loopCount++ - - // Create per-loop stdout tag (uses sanitized names) - const testStdoutTag = `${safeModulePath}:${testClassName ? testClassName + "." : ""}${safeTestFunctionName}:${funcName}:${loopCount}:${invocationId}` - - // Print start tag - console.log(`!$######${testStdoutTag}######$!`) - - // Timing with nanosecond precision - let durationNs - try { - const startTime = getTimeNs() - returnValue = fn(...args) - const endTime = getTimeNs() - durationNs = getDurationNs(startTime, endTime) - - // Handle promises - for async, we can't easily loop internally - // Fall back to single execution for async functions - if (returnValue instanceof Promise) { - return returnValue.then( - (resolved) => { - const asyncEndTime = getTimeNs() - const asyncDurationNs = getDurationNs(startTime, asyncEndTime) - console.log(`!######${testStdoutTag}:${asyncDurationNs}######!`) - return resolved - }, - (err) => { - const asyncEndTime = getTimeNs() - const asyncDurationNs = getDurationNs(startTime, asyncEndTime) - console.log(`!######${testStdoutTag}:${asyncDurationNs}######!`) - throw err - }, - ) - } - } catch (e) { - durationNs = 0 - error = e - // Print end tag even on error - console.log(`!######${testStdoutTag}:${durationNs}######!`) - throw error - } - - // Print end tag with timing - console.log(`!######${testStdoutTag}:${durationNs}######!`) - - // Track runtime for stability - runtimes.push(durationNs) - - // Check stopping conditions - const elapsedMs = Date.now() - loopStartTime - - // Stop if we've reached max loops - if (loopCount >= MAX_LOOPS) { - break - } - - // Stop if we've reached min loops AND exceeded a reasonable portion of time - if (loopCount >= MIN_LOOPS && elapsedMs >= TARGET_DURATION_MS * 0.8) { - break - } - - // Stability check - if (STABILITY_CHECK && loopCount >= MIN_LOOPS) { - // Estimate total loops based on current rate - const rate = loopCount / elapsedMs - const estimatedTotalLoops = Math.floor(rate * TARGET_DURATION_MS) - const windowSize = Math.max(3, Math.floor(STABILITY_WINDOW_SIZE * estimatedTotalLoops)) - - if (checkStability(runtimes, windowSize)) { - // Performance has stabilized - break - } - } - } - - return returnValue -} - -/** - * Capture multiple invocations for benchmarking. - * - * @param {string} funcName - Name of the function being tested - * @param {string} lineId - Line number identifier - * @param {Function} fn - The function to call - * @param {Array} argsList - List of argument arrays to test - * @returns {Array} - Array of return values - */ -function captureMultiple(funcName, lineId, fn, argsList) { - return argsList.map((args) => capture(funcName, lineId, fn, ...args)) -} - -/** - * Write remaining JSON results to file (fallback mode). - * Called automatically via Jest afterAll hook. - */ -function writeResults() { - // Close SQLite connection if open - if (db) { - try { - db.close() - } catch (e) { - // Ignore close errors - } - db = null - return - } - - // Write JSON fallback if SQLite wasn't used - if (results.length === 0) return - - try { - // Write as JSON for fallback parsing - const jsonPath = OUTPUT_FILE.replace(".sqlite", ".json") - const output = { - version: "1.0.0", - loopIndex: LOOP_INDEX, - timestamp: Date.now(), - results, - } - fs.writeFileSync(jsonPath, JSON.stringify(output, null, 2)) - } catch (e) { - console.error("[codeflash] Error writing JSON results:", e.message) - } -} - -/** - * Clear all recorded results. - * Useful for resetting between test files. - */ -function clearResults() { - results.length = 0 - resetInvocationCounters() -} - -/** - * Get the current results buffer. - * Useful for debugging or custom result handling. - * - * @returns {Array} - Current results buffer - */ -function getResults() { - return results -} - -/** - * Set the current test name. - * Called automatically via Jest beforeEach hook. - * - * @param {string} name - Test name - */ -function setTestName(name) { - currentTestName = name - resetInvocationCounters() -} - -// Jest lifecycle hooks - these run automatically when this module is imported -if (typeof beforeEach !== "undefined") { - beforeEach(() => { - // Get current test name and path from Jest's expect state - try { - const state = expect.getState() - currentTestName = state.currentTestName || "unknown" - // testPath is the absolute path to the test file - currentTestPath = state.testPath || null - } catch (e) { - currentTestName = "unknown" - currentTestPath = null - } - // Reset invocation counters for each test - resetInvocationCounters() - }) -} - -if (typeof afterAll !== "undefined") { - afterAll(() => { - writeResults() - }) -} - -// Export public API -module.exports = { - capture, // Behavior verification (writes to SQLite) - capturePerf, // Performance benchmarking (prints to stdout only, single run) - capturePerfLooped, // Performance benchmarking with internal looping - captureMultiple, - writeResults, - clearResults, - getResults, - setTestName, - safeSerialize, - safeDeserialize, - initDatabase, - resetInvocationCounters, - getInvocationIndex, - checkStability, - sanitizeTestId, // Sanitize test names for stdout tags - // Serializer info - getSerializerType: serializer.getSerializerType, - // Constants - LOOP_INDEX, - OUTPUT_FILE, - TEST_ITERATION, - MIN_LOOPS, - MAX_LOOPS, - TARGET_DURATION_MS, - STABILITY_CHECK, -} diff --git a/django/aiservice/testgen/instrumentation/javascript/codeflash-serializer.js b/django/aiservice/testgen/instrumentation/javascript/codeflash-serializer.js deleted file mode 100644 index 131445203..000000000 --- a/django/aiservice/testgen/instrumentation/javascript/codeflash-serializer.js +++ /dev/null @@ -1,851 +0,0 @@ -/** - * Codeflash Universal Serializer - * - * A robust serialization system for JavaScript values that: - * 1. Prefers V8 serialization (Node.js native) - fastest, handles all JS types - * 2. Falls back to msgpack with custom extensions (for Bun/browser environments) - * - * Supports: - * - All primitive types (null, undefined, boolean, number, string, bigint, symbol) - * - Special numbers (NaN, Infinity, -Infinity) - * - Objects, Arrays (including sparse arrays) - * - Map, Set, WeakMap references, WeakSet references - * - Date, RegExp, Error (and subclasses) - * - TypedArrays (Int8Array, Uint8Array, Float32Array, etc.) - * - ArrayBuffer, SharedArrayBuffer, DataView - * - Circular references - * - Functions (by reference/name only) - * - * Usage: - * const { serialize, deserialize, getSerializerType } = require('./codeflash-serializer'); - * - * const buffer = serialize(value); - * const restored = deserialize(buffer); - */ - -'use strict'; - -// ============================================================================ -// SERIALIZER DETECTION -// ============================================================================ - -let useV8 = false; -let v8Module = null; - -// Try to load V8 module (available in Node.js) -try { - v8Module = require('v8'); - // Verify serialize/deserialize are available - if (typeof v8Module.serialize === 'function' && typeof v8Module.deserialize === 'function') { - // Perform a self-test to verify V8 serialization works correctly - // This catches cases like Jest's VM context where V8 serialization - // produces data that deserializes incorrectly (Maps become plain objects) - const testMap = new Map([['__test__', 1]]); - const testBuffer = v8Module.serialize(testMap); - const testRestored = v8Module.deserialize(testBuffer); - - if (testRestored instanceof Map && testRestored.get('__test__') === 1) { - useV8 = true; - } else { - // V8 serialization is broken in this environment (e.g., Jest) - useV8 = false; - } - } -} catch (e) { - // V8 not available (Bun, browser, etc.) -} - -// Load msgpack as fallback -let msgpack = null; -try { - msgpack = require('@msgpack/msgpack'); -} catch (e) { - // msgpack not installed -} - -/** - * Get the serializer type being used. - * @returns {string} - 'v8' or 'msgpack' - */ -function getSerializerType() { - return useV8 ? 'v8' : 'msgpack'; -} - -// ============================================================================ -// V8 SERIALIZATION (PRIMARY) -// ============================================================================ - -/** - * Serialize a value using V8's native serialization. - * This handles all JavaScript types including: - * - Primitives, Objects, Arrays - * - Map, Set, Date, RegExp, Error - * - TypedArrays, ArrayBuffer - * - Circular references - * - * @param {any} value - Value to serialize - * @returns {Buffer} - Serialized buffer - */ -function serializeV8(value) { - try { - return v8Module.serialize(value); - } catch (e) { - // V8 can't serialize some things (functions, symbols in some contexts) - // Fall back to wrapped serialization - return v8Module.serialize(wrapForV8(value)); - } -} - -/** - * Deserialize a V8-serialized buffer. - * - * @param {Buffer} buffer - Serialized buffer - * @returns {any} - Deserialized value - */ -function deserializeV8(buffer) { - const value = v8Module.deserialize(buffer); - return unwrapFromV8(value); -} - -/** - * Wrap values that V8 can't serialize natively. - * V8 can't serialize: functions, symbols (in some cases) - */ -function wrapForV8(value, seen = new WeakMap()) { - if (value === null || value === undefined) return value; - - const type = typeof value; - - // Primitives that V8 handles - if (type === 'number' || type === 'string' || type === 'boolean' || type === 'bigint') { - return value; - } - - // Symbols - wrap with marker - if (type === 'symbol') { - return { __codeflash_type__: 'Symbol', description: value.description }; - } - - // Functions - wrap with marker - if (type === 'function') { - return { - __codeflash_type__: 'Function', - name: value.name || 'anonymous', - // Can't serialize function body reliably - }; - } - - // Objects - if (type === 'object') { - // Check for circular reference - if (seen.has(value)) { - return seen.get(value); - } - - // V8 handles most objects natively - // Just need to recurse into arrays and plain objects to wrap nested functions/symbols - - if (Array.isArray(value)) { - const wrapped = []; - seen.set(value, wrapped); - for (let i = 0; i < value.length; i++) { - if (i in value) { - wrapped[i] = wrapForV8(value[i], seen); - } - } - return wrapped; - } - - // V8 handles these natively - if (value instanceof Date || value instanceof RegExp || value instanceof Error || - value instanceof Map || value instanceof Set || - ArrayBuffer.isView(value) || value instanceof ArrayBuffer) { - return value; - } - - // Plain objects - recurse - const wrapped = {}; - seen.set(value, wrapped); - for (const key of Object.keys(value)) { - wrapped[key] = wrapForV8(value[key], seen); - } - return wrapped; - } - - return value; -} - -/** - * Unwrap values that were wrapped for V8 serialization. - */ -function unwrapFromV8(value, seen = new WeakMap()) { - if (value === null || value === undefined) return value; - - const type = typeof value; - - if (type !== 'object') return value; - - // Check for circular reference - if (seen.has(value)) { - return seen.get(value); - } - - // Check for wrapped types - if (value.__codeflash_type__) { - switch (value.__codeflash_type__) { - case 'Symbol': - return Symbol(value.description); - case 'Function': - // Can't restore function body, return a placeholder - const fn = function() { throw new Error(`Deserialized function placeholder: ${value.name}`); }; - Object.defineProperty(fn, 'name', { value: value.name }); - return fn; - default: - // Unknown wrapped type, return as-is - return value; - } - } - - // Arrays - if (Array.isArray(value)) { - const unwrapped = []; - seen.set(value, unwrapped); - for (let i = 0; i < value.length; i++) { - if (i in value) { - unwrapped[i] = unwrapFromV8(value[i], seen); - } - } - return unwrapped; - } - - // V8 restores these natively - if (value instanceof Date || value instanceof RegExp || value instanceof Error || - value instanceof Map || value instanceof Set || - ArrayBuffer.isView(value) || value instanceof ArrayBuffer) { - return value; - } - - // Plain objects - recurse - const unwrapped = {}; - seen.set(value, unwrapped); - for (const key of Object.keys(value)) { - unwrapped[key] = unwrapFromV8(value[key], seen); - } - return unwrapped; -} - -// ============================================================================ -// MSGPACK SERIALIZATION (FALLBACK) -// ============================================================================ - -/** - * Extension type IDs for msgpack. - * Using negative IDs to avoid conflicts with user-defined extensions. - */ -const EXT_TYPES = { - UNDEFINED: 0x01, - NAN: 0x02, - INFINITY_POS: 0x03, - INFINITY_NEG: 0x04, - BIGINT: 0x05, - SYMBOL: 0x06, - DATE: 0x07, - REGEXP: 0x08, - ERROR: 0x09, - MAP: 0x0A, - SET: 0x0B, - INT8ARRAY: 0x10, - UINT8ARRAY: 0x11, - UINT8CLAMPEDARRAY: 0x12, - INT16ARRAY: 0x13, - UINT16ARRAY: 0x14, - INT32ARRAY: 0x15, - UINT32ARRAY: 0x16, - FLOAT32ARRAY: 0x17, - FLOAT64ARRAY: 0x18, - BIGINT64ARRAY: 0x19, - BIGUINT64ARRAY: 0x1A, - ARRAYBUFFER: 0x1B, - DATAVIEW: 0x1C, - FUNCTION: 0x1D, - CIRCULAR_REF: 0x1E, - SPARSE_ARRAY: 0x1F, -}; - -/** - * Create msgpack extension codec for JavaScript types. - */ -function createMsgpackCodec() { - const extensionCodec = new msgpack.ExtensionCodec(); - - // Undefined - extensionCodec.register({ - type: EXT_TYPES.UNDEFINED, - encode: (value) => { - if (value === undefined) return new Uint8Array(0); - return null; - }, - decode: () => undefined, - }); - - // NaN - extensionCodec.register({ - type: EXT_TYPES.NAN, - encode: (value) => { - if (typeof value === 'number' && Number.isNaN(value)) return new Uint8Array(0); - return null; - }, - decode: () => NaN, - }); - - // Positive Infinity - extensionCodec.register({ - type: EXT_TYPES.INFINITY_POS, - encode: (value) => { - if (value === Infinity) return new Uint8Array(0); - return null; - }, - decode: () => Infinity, - }); - - // Negative Infinity - extensionCodec.register({ - type: EXT_TYPES.INFINITY_NEG, - encode: (value) => { - if (value === -Infinity) return new Uint8Array(0); - return null; - }, - decode: () => -Infinity, - }); - - // BigInt - extensionCodec.register({ - type: EXT_TYPES.BIGINT, - encode: (value) => { - if (typeof value === 'bigint') { - const str = value.toString(); - return new TextEncoder().encode(str); - } - return null; - }, - decode: (data) => { - const str = new TextDecoder().decode(data); - return BigInt(str); - }, - }); - - // Symbol - extensionCodec.register({ - type: EXT_TYPES.SYMBOL, - encode: (value) => { - if (typeof value === 'symbol') { - // Distinguish between undefined description and empty string - // Use a special marker for undefined description - const desc = value.description; - if (desc === undefined) { - return new TextEncoder().encode('\x00__UNDEF__'); - } - return new TextEncoder().encode(desc); - } - return null; - }, - decode: (data) => { - const description = new TextDecoder().decode(data); - // Check for undefined marker - if (description === '\x00__UNDEF__') { - return Symbol(); - } - return Symbol(description); - }, - }); - - // Note: Date is handled via marker objects in prepareForMsgpack/restoreFromMsgpack - // because msgpack's built-in timestamp extension doesn't properly handle NaN (Invalid Date) - - // RegExp - use Object.prototype.toString for cross-context detection - extensionCodec.register({ - type: EXT_TYPES.REGEXP, - encode: (value) => { - if (Object.prototype.toString.call(value) === '[object RegExp]') { - const obj = { source: value.source, flags: value.flags }; - return msgpack.encode(obj); - } - return null; - }, - decode: (data) => { - const obj = msgpack.decode(data); - return new RegExp(obj.source, obj.flags); - }, - }); - - // Error - use Object.prototype.toString for cross-context detection - extensionCodec.register({ - type: EXT_TYPES.ERROR, - encode: (value) => { - // Check for Error-like objects (cross-VM-context compatible) - if (Object.prototype.toString.call(value) === '[object Error]' || - (value && value.name && value.message !== undefined && value.stack !== undefined)) { - const obj = { - name: value.name, - message: value.message, - stack: value.stack, - // Include custom properties - ...Object.fromEntries( - Object.entries(value).filter(([k]) => !['name', 'message', 'stack'].includes(k)) - ), - }; - return msgpack.encode(obj); - } - return null; - }, - decode: (data) => { - const obj = msgpack.decode(data); - let ErrorClass = Error; - // Try to use the appropriate error class - const errorClasses = { - TypeError, RangeError, SyntaxError, ReferenceError, - URIError, EvalError, Error - }; - if (obj.name in errorClasses) { - ErrorClass = errorClasses[obj.name]; - } - const error = new ErrorClass(obj.message); - error.stack = obj.stack; - // Restore custom properties - for (const [key, val] of Object.entries(obj)) { - if (!['name', 'message', 'stack'].includes(key)) { - error[key] = val; - } - } - return error; - }, - }); - - // Function (limited - can't serialize body) - extensionCodec.register({ - type: EXT_TYPES.FUNCTION, - encode: (value) => { - if (typeof value === 'function') { - return new TextEncoder().encode(value.name || 'anonymous'); - } - return null; - }, - decode: (data) => { - const name = new TextDecoder().decode(data); - const fn = function() { throw new Error(`Deserialized function placeholder: ${name}`); }; - Object.defineProperty(fn, 'name', { value: name }); - return fn; - }, - }); - - return extensionCodec; -} - -// Singleton codec instance -let msgpackCodec = null; - -function getMsgpackCodec() { - if (!msgpackCodec && msgpack) { - msgpackCodec = createMsgpackCodec(); - } - return msgpackCodec; -} - -/** - * Prepare a value for msgpack serialization. - * Handles types that need special treatment beyond extensions. - */ -function prepareForMsgpack(value, seen = new Map(), refId = { current: 0 }) { - if (value === null) return null; - // undefined needs special handling because msgpack converts it to null - if (value === undefined) return { __codeflash_undefined__: true }; - - const type = typeof value; - - // Special number values that msgpack doesn't handle correctly - if (type === 'number') { - if (Number.isNaN(value)) return { __codeflash_nan__: true }; - if (value === Infinity) return { __codeflash_infinity__: true }; - if (value === -Infinity) return { __codeflash_neg_infinity__: true }; - return value; - } - - // Primitives that msgpack handles or our extensions handle - if (type === 'string' || type === 'boolean' || - type === 'bigint' || type === 'symbol' || type === 'function') { - return value; - } - - if (type !== 'object') return value; - - // Check for circular reference - if (seen.has(value)) { - return { __codeflash_circular__: seen.get(value) }; - } - - // Assign reference ID for potential circular refs - const id = refId.current++; - seen.set(value, id); - - // Use toString for cross-VM-context type detection - const tag = Object.prototype.toString.call(value); - - // Date - handle specially because msgpack's built-in timestamp doesn't handle NaN - if (tag === '[object Date]') { - const time = value.getTime(); - // Store as marker object with the timestamp - // We use a string representation to preserve NaN - return { - __codeflash_date__: Number.isNaN(time) ? '__NAN__' : time, - __id__: id, - }; - } - - // RegExp, Error - handled by extensions - if (tag === '[object RegExp]' || tag === '[object Error]') { - return value; - } - - // Map (use toString for cross-VM-context) - if (tag === '[object Map]') { - const entries = []; - for (const [k, v] of value) { - entries.push([prepareForMsgpack(k, seen, refId), prepareForMsgpack(v, seen, refId)]); - } - return { __codeflash_map__: entries, __id__: id }; - } - - // Set (use toString for cross-VM-context) - if (tag === '[object Set]') { - const values = []; - for (const v of value) { - values.push(prepareForMsgpack(v, seen, refId)); - } - return { __codeflash_set__: values, __id__: id }; - } - - // TypedArrays (use ArrayBuffer.isView which works cross-context) - if (ArrayBuffer.isView(value) && tag !== '[object DataView]') { - return { - __codeflash_typedarray__: value.constructor.name, - data: Array.from(value), - __id__: id, - }; - } - - // DataView (use toString for cross-VM-context) - if (tag === '[object DataView]') { - return { - __codeflash_dataview__: true, - data: Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)), - __id__: id, - }; - } - - // ArrayBuffer (use toString for cross-VM-context) - if (tag === '[object ArrayBuffer]') { - return { - __codeflash_arraybuffer__: true, - data: Array.from(new Uint8Array(value)), - __id__: id, - }; - } - - // Arrays - always wrap in marker to preserve __id__ for circular references - // (msgpack doesn't preserve non-numeric properties on arrays) - if (Array.isArray(value)) { - const isSparse = value.length > 0 && Object.keys(value).length !== value.length; - if (isSparse) { - // Sparse array - store as object with indices - const sparse = { __codeflash_sparse_array__: true, length: value.length, elements: {}, __id__: id }; - for (const key of Object.keys(value)) { - sparse.elements[key] = prepareForMsgpack(value[key], seen, refId); - } - return sparse; - } - // Dense array - wrap in marker object to preserve __id__ - const elements = []; - for (let i = 0; i < value.length; i++) { - elements[i] = prepareForMsgpack(value[i], seen, refId); - } - return { __codeflash_array__: elements, __id__: id }; - } - - // Plain objects - const obj = { __id__: id }; - for (const key of Object.keys(value)) { - obj[key] = prepareForMsgpack(value[key], seen, refId); - } - return obj; -} - -/** - * Restore a value after msgpack deserialization. - */ -function restoreFromMsgpack(value, refs = new Map()) { - if (value === null || value === undefined) return value; - - const type = typeof value; - if (type !== 'object') return value; - - // Built-in types that msgpack handles via extensions - return as-is - // These should NOT be treated as plain objects (use toString for cross-VM-context) - // Note: Date is handled via marker objects, so not included here - const tag = Object.prototype.toString.call(value); - if (tag === '[object RegExp]' || tag === '[object Error]') { - return value; - } - - // Special value markers - if (value.__codeflash_undefined__) return undefined; - if (value.__codeflash_nan__) return NaN; - if (value.__codeflash_infinity__) return Infinity; - if (value.__codeflash_neg_infinity__) return -Infinity; - - // Date marker - if (value.__codeflash_date__ !== undefined) { - const time = value.__codeflash_date__ === '__NAN__' ? NaN : value.__codeflash_date__; - const date = new Date(time); - const id = value.__id__; - if (id !== undefined) refs.set(id, date); - return date; - } - - // Check for circular reference marker - if (value.__codeflash_circular__ !== undefined) { - return refs.get(value.__codeflash_circular__); - } - - // Store reference if this object has an ID - const id = value.__id__; - - // Map - if (value.__codeflash_map__) { - const map = new Map(); - if (id !== undefined) refs.set(id, map); - for (const [k, v] of value.__codeflash_map__) { - map.set(restoreFromMsgpack(k, refs), restoreFromMsgpack(v, refs)); - } - return map; - } - - // Set - if (value.__codeflash_set__) { - const set = new Set(); - if (id !== undefined) refs.set(id, set); - for (const v of value.__codeflash_set__) { - set.add(restoreFromMsgpack(v, refs)); - } - return set; - } - - // TypedArrays - if (value.__codeflash_typedarray__) { - const TypedArrayClass = globalThis[value.__codeflash_typedarray__]; - if (TypedArrayClass) { - const arr = new TypedArrayClass(value.data); - if (id !== undefined) refs.set(id, arr); - return arr; - } - } - - // DataView - if (value.__codeflash_dataview__) { - const buffer = new ArrayBuffer(value.data.length); - new Uint8Array(buffer).set(value.data); - const view = new DataView(buffer); - if (id !== undefined) refs.set(id, view); - return view; - } - - // ArrayBuffer - if (value.__codeflash_arraybuffer__) { - const buffer = new ArrayBuffer(value.data.length); - new Uint8Array(buffer).set(value.data); - if (id !== undefined) refs.set(id, buffer); - return buffer; - } - - // Dense array marker - if (value.__codeflash_array__) { - const arr = []; - if (id !== undefined) refs.set(id, arr); - const elements = value.__codeflash_array__; - for (let i = 0; i < elements.length; i++) { - arr[i] = restoreFromMsgpack(elements[i], refs); - } - return arr; - } - - // Sparse array - if (value.__codeflash_sparse_array__) { - const arr = new Array(value.length); - if (id !== undefined) refs.set(id, arr); - for (const [key, val] of Object.entries(value.elements)) { - arr[parseInt(key, 10)] = restoreFromMsgpack(val, refs); - } - return arr; - } - - // Arrays (legacy - shouldn't happen with new format, but keep for safety) - if (Array.isArray(value)) { - const arr = []; - if (id !== undefined) refs.set(id, arr); - for (let i = 0; i < value.length; i++) { - if (i in value) { - arr[i] = restoreFromMsgpack(value[i], refs); - } - } - return arr; - } - - // Plain objects - remove __id__ from result - const obj = {}; - if (id !== undefined) refs.set(id, obj); - for (const [key, val] of Object.entries(value)) { - if (key !== '__id__') { - obj[key] = restoreFromMsgpack(val, refs); - } - } - return obj; -} - -/** - * Serialize a value using msgpack with extensions. - * - * @param {any} value - Value to serialize - * @returns {Buffer} - Serialized buffer - */ -function serializeMsgpack(value) { - if (!msgpack) { - throw new Error('msgpack not available and V8 serialization not available'); - } - - const codec = getMsgpackCodec(); - const prepared = prepareForMsgpack(value); - const encoded = msgpack.encode(prepared, { extensionCodec: codec }); - return Buffer.from(encoded); -} - -/** - * Deserialize a msgpack-serialized buffer. - * - * @param {Buffer|Uint8Array} buffer - Serialized buffer - * @returns {any} - Deserialized value - */ -function deserializeMsgpack(buffer) { - if (!msgpack) { - throw new Error('msgpack not available'); - } - - const codec = getMsgpackCodec(); - const decoded = msgpack.decode(buffer, { extensionCodec: codec }); - return restoreFromMsgpack(decoded); -} - -// ============================================================================ -// PUBLIC API -// ============================================================================ - -/** - * Serialize a value using the best available method. - * Prefers V8 serialization, falls back to msgpack. - * - * @param {any} value - Value to serialize - * @returns {Buffer} - Serialized buffer with format marker - */ -function serialize(value) { - // Add a format marker byte at the start - // 0x01 = V8, 0x02 = msgpack - if (useV8) { - const serialized = serializeV8(value); - const result = Buffer.allocUnsafe(serialized.length + 1); - result[0] = 0x01; - serialized.copy(result, 1); - return result; - } else { - const serialized = serializeMsgpack(value); - const result = Buffer.allocUnsafe(serialized.length + 1); - result[0] = 0x02; - serialized.copy(result, 1); - return result; - } -} - -/** - * Deserialize a buffer that was serialized with serialize(). - * Automatically detects the format from the marker byte. - * - * @param {Buffer|Uint8Array} buffer - Serialized buffer - * @returns {any} - Deserialized value - */ -function deserialize(buffer) { - if (!buffer || buffer.length === 0) { - throw new Error('Empty buffer cannot be deserialized'); - } - - const format = buffer[0]; - const data = buffer.slice(1); - - if (format === 0x01) { - // V8 format - if (!useV8) { - throw new Error('Buffer was serialized with V8 but V8 is not available'); - } - return deserializeV8(data); - } else if (format === 0x02) { - // msgpack format - return deserializeMsgpack(data); - } else { - throw new Error(`Unknown serialization format: ${format}`); - } -} - -/** - * Force serialization using a specific method. - * Useful for testing or cross-environment compatibility. - */ -const serializeWith = { - v8: useV8 ? (value) => { - const serialized = serializeV8(value); - const result = Buffer.allocUnsafe(serialized.length + 1); - result[0] = 0x01; - serialized.copy(result, 1); - return result; - } : null, - - msgpack: msgpack ? (value) => { - const serialized = serializeMsgpack(value); - const result = Buffer.allocUnsafe(serialized.length + 1); - result[0] = 0x02; - serialized.copy(result, 1); - return result; - } : null, -}; - -// ============================================================================ -// EXPORTS -// ============================================================================ - -module.exports = { - // Main API - serialize, - deserialize, - getSerializerType, - - // Force specific serializer - serializeWith, - - // Low-level (for testing) - serializeV8: useV8 ? serializeV8 : null, - deserializeV8: useV8 ? deserializeV8 : null, - serializeMsgpack: msgpack ? serializeMsgpack : null, - deserializeMsgpack: msgpack ? deserializeMsgpack : null, - - // Feature detection - hasV8: useV8, - hasMsgpack: !!msgpack, - - // Extension types (for reference) - EXT_TYPES, -}; diff --git a/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py b/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py deleted file mode 100644 index 6fdc860c8..000000000 --- a/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py +++ /dev/null @@ -1,661 +0,0 @@ -"""JavaScript test instrumentation module. - -This module instruments JavaScript tests by injecting the codeflash-jest-helper -to capture function call behavior and performance data. - -Unlike Python which has separate instrumentation for generated vs existing tests, -JavaScript uses a UNIFIED approach - the same instrumentation works for all tests. - -The key difference between behavior and performance modes: -- behavior: Uses codeflash.capture() which writes to SQLite with full args/return values -- performance: Uses codeflash.capturePerf() which only prints timing to stdout -""" - -from __future__ import annotations - -import re -from collections.abc import Callable -from pathlib import Path -from typing import Literal - - -def get_jest_helper_path() -> Path: - """Get the path to the codeflash-jest-helper.js file. - - Returns: - Path to the helper JavaScript file - - """ - return Path(__file__).parent / "codeflash-jest-helper.js" - - -def _fix_import_path(test_source: str, function_name: str, module_path: str) -> str: - """Fix the import path for the function being tested. - - LLMs sometimes generate incorrect import paths. This function finds and fixes - any require() statements that import the function_name to use the correct module_path. - - Args: - test_source: The JavaScript test source code - function_name: The name of the function being tested - module_path: The correct path to the module - - Returns: - Test source with corrected import path - - """ - # Pattern to match: const { functionName } = require('...'); - # or: const { functionName, otherFunc } = require('...'); - import_pattern = ( - rf"(const\s*\{{\s*[^}}]*\b{re.escape(function_name)}\b[^}}]*\}}\s*=\s*require\s*\(\s*['\"])([^'\"]+)(['\"])" - ) - - def replace_import(match): - prefix = match.group(1) # const { functionName } = require(' - old_path = match.group(2) # the old path - suffix = match.group(3) # ' or " - - # Only replace if the path is different - if old_path != module_path: - return f"{prefix}{module_path}{suffix}" - return match.group(0) - - return re.sub(import_pattern, replace_import, test_source) - - -def instrument_javascript_tests( - test_source: str, function_name: str, module_path: str, mode: Literal["behavior", "performance"] = "behavior" -) -> str: - """Instrument JavaScript tests with codeflash helper. - - This is a UNIFIED approach - works for both generated and existing tests. - The instrumentation wraps function calls to capture inputs, outputs, and timing. - - Static identifiers (funcName, lineId) are determined at instrumentation time. - The lineId enables tracking when the same call site is invoked multiple times (e.g., in loops). - - Args: - test_source: The JavaScript test source code - function_name: The name of the function being tested - module_path: The path to the module containing the function - mode: Testing mode - 'behavior' uses capture() for SQLite, - 'performance' uses capturePerf() for stdout timing - - Returns: - Instrumented test source code - - """ - # First, fix any incorrect import paths generated by the LLM - test_source = _fix_import_path(test_source, function_name, module_path) - - # Check if already instrumented - if "codeflash-jest-helper" in test_source: - # If already instrumented, convert between modes if needed - if mode == "performance": - # Convert to performance mode - if "codeflash.capture(" in test_source and "codeflash.capturePerf(" not in test_source: - test_source = test_source.replace("codeflash.capture(", "codeflash.capturePerf(") - elif mode == "behavior": - # Convert to behavior mode - if "codeflash.capturePerf(" in test_source: - test_source = test_source.replace("codeflash.capturePerf(", "codeflash.capture(") - return test_source - - lines = test_source.split("\n") - result_lines = [] - - # Add helper import at the top, after any existing imports - # Use relative path - helper is in same directory as test files (tests/) - helper_import = "const codeflash = require('./codeflash-jest-helper');" - import_inserted = False - in_import_block = False - - for i, line in enumerate(lines): - stripped = line.strip() - - # Track if we're in the import block - if stripped.startswith("import ") or (stripped.startswith("const ") and "require" in stripped): - in_import_block = True - elif ( - in_import_block - and stripped - and not stripped.startswith("import ") - and not (stripped.startswith("const ") and "require" in stripped) - ): - # End of import block - insert helper import - if not import_inserted: - result_lines.append(helper_import) - result_lines.append("") - import_inserted = True - in_import_block = False - - result_lines.append(line) - - # If no imports found, add at the beginning - if not import_inserted: - result_lines = [helper_import, ""] + result_lines - - instrumented_source = "\n".join(result_lines) - - # Choose the capture function based on mode - # - behavior: capture() writes to SQLite with args/return values - # - performance: capturePerf() prints timing markers to stdout - capture_func = "capturePerf" if mode == "performance" else "capture" - - # Wrap function calls with codeflash.capture or codeflash.capturePerf - # Pattern matches: functionName(args) but not inside strings or comments - # This is a simple approach - a more robust solution would use an AST parser - - # Pattern for standalone function calls (not method calls) - pattern = rf"(? str: - """Comment out expect() assertions in the test code. - - During behavior capture, we want to execute the function calls but not - fail on hardcoded assertions. The captured outputs will be used for - verification instead. - - IMPORTANT: If an expect() contains a codeflash.capture/capturePerf call, - we need to preserve that call while disabling the assertion. - e.g., expect(codeflash.capturePerf('fn', fn, arg)).toBe(x) - becomes: codeflash.capturePerf('fn', fn, arg); // [codeflash-disabled] .toBe(x) - - This function handles MULTI-LINE expect() statements by tracking parentheses - and using block comments for statements that span multiple lines. - """ - import re - - # Process the source character by character to handle multi-line statements - result = [] - i = 0 - length = len(source) - - while i < length: - # Check for expect( pattern - if source[i : i + 7] == "expect(": - # Check if this is a codeflash capture call (capture or capturePerf) - capture_match = re.match(r"expect\((codeflash\.capture(?:Perf)?)\(", source[i:]) - if capture_match: - # Has codeflash capture - extract it and disable the assertion - result_str = _extract_and_transform_capture_expect(source, i) - if result_str: - transformed, end_pos = result_str - result.append(transformed) - i = end_pos - continue - - # Regular expect() without codeflash - find the full statement and comment it out - statement_end = _find_statement_end(source, i) - statement = source[i:statement_end] - - # Check if it spans multiple lines - if "\n" in statement: - # Use block comment for multi-line statements - result.append("/* [codeflash-disabled] " + statement + " */") - else: - # Single line - use line comment - result.append("// [codeflash-disabled] " + statement) - - i = statement_end - continue - - result.append(source[i]) - i += 1 - - return "".join(result) - - -def _find_statement_end(source: str, start: int) -> int: - """Find the end of a JavaScript statement starting at the given position. - - Tracks parentheses to handle multi-line statements properly. - The statement ends at the semicolon or closing brace after all parens are matched. - """ - i = start - length = len(source) - paren_count = 0 - in_string = False - string_char = None - - while i < length: - char = source[i] - - # Handle string literals - if char in "'\"`" and not in_string: - in_string = True - string_char = char - i += 1 - continue - if in_string: - if char == "\\" and i + 1 < length: - i += 2 # Skip escaped character - continue - if char == string_char: - in_string = False - string_char = None - i += 1 - continue - - # Track parentheses - if char == "(": - paren_count += 1 - elif char == ")": - paren_count -= 1 - - # Statement ends at semicolon or newline when all parens are closed - if paren_count == 0: - if char == ";": - return i + 1 # Include the semicolon - if char == "\n": - # Check if this is a complete statement (look ahead for .to) - rest = source[i:].lstrip() - if not rest.startswith(".to"): - return i # End before newline - - i += 1 - - return i - - -def _extract_and_transform_capture_expect(source: str, start: int) -> tuple[str, int] | None: - """Transform expect(codeflash.capture(...)).toBe(...) into - codeflash.capture(...); // [codeflash-disabled] .toBe(...) - - Handles multi-line statements properly. - - Returns: - Tuple of (transformed_code, end_position) or None if parsing fails - - """ - import re - - # Match the expect(codeflash.capture pattern (capture or capturePerf) - match = re.match(r"expect\((codeflash\.capture(?:Perf)?)\(", source[start:]) - if not match: - return None - - capture_func = match.group(1) # e.g., "codeflash.capturePerf" - - # Find the matching closing paren for the capture call - capture_start = start + match.end() - 1 # Position of opening paren of capture call - i = capture_start + 1 - paren_count = 1 - length = len(source) - - while i < length and paren_count > 0: - char = source[i] - - # Handle string literals - if char in "'\"`": - string_char = char - i += 1 - while i < length: - if source[i] == "\\" and i + 1 < length: - i += 2 - elif source[i] == string_char: - i += 1 - break - else: - i += 1 - continue - - if char == "(": - paren_count += 1 - elif char == ")": - paren_count -= 1 - i += 1 - - # i now points just after the closing paren of capture call - capture_args = source[capture_start + 1 : i - 1] # Args inside capture(...) - - # Now find the rest: ).toBe(...) or ).toEqual(...) etc. - # Skip the ) that closes expect( - if i >= length or source[i] != ")": - return None - - expect_close = i - i += 1 - - # Find the assertion matcher (if any) - assertion_start = i - # Skip whitespace/newlines - while i < length and source[i] in " \t\n": - i += 1 - - if i < length and source[i] == ".": - # Find the full assertion chain: .toBe(x).toEqual(y) etc. - while i < length: - if source[i] == ".": - i += 1 - # Read matcher name - while i < length and (source[i].isalnum() or source[i] == "_"): - i += 1 - # Skip whitespace - while i < length and source[i] in " \t\n": - i += 1 - # Handle matcher arguments - if i < length and source[i] == "(": - paren_count = 1 - i += 1 - while i < length and paren_count > 0: - if source[i] in "'\"`": - string_char = source[i] - i += 1 - while i < length: - if source[i] == "\\" and i + 1 < length: - i += 2 - elif source[i] == string_char: - i += 1 - break - else: - i += 1 - continue - if source[i] == "(": - paren_count += 1 - elif source[i] == ")": - paren_count -= 1 - i += 1 - elif source[i] == ";": - i += 1 - break - elif source[i] in " \t": - i += 1 - else: - break - - assertion = source[assertion_start:i].strip() - # Remove leading ) if present (it's the expect closing paren) - if assertion.startswith(")"): - assertion = assertion[1:].strip() - - # Build the transformed code - transformed = f"{capture_func}({capture_args}); // [codeflash-disabled] {assertion}" - return (transformed, i) - # No assertion, just the capture call - # Skip to semicolon if present - while i < length and source[i] in " \t\n": - i += 1 - if i < length and source[i] == ";": - i += 1 - - transformed = f"{capture_func}({capture_args});" - return (transformed, i) - - -def _safe_replace_function_calls_with_lineid(source: str, function_name: str, capture_func: str, pattern: str) -> str: - """Replace function calls while tracking line numbers. - - Each function call is wrapped with a static lineId that enables tracking - when the same call site is invoked multiple times (e.g., in loops). - - Args: - source: The JavaScript source code - function_name: Name of the function to wrap - capture_func: The capture function to use ('capture' or 'capturePerf') - pattern: Regex pattern to match function calls - - """ - lines = source.split("\n") - result_lines = [] - - for line_num, line in enumerate(lines, start=1): - # Process each line independently to track line numbers - # Use the line number as the lineId - - # Skip lines that are in strings or comments (simplified check) - if line.strip().startswith("//") or line.strip().startswith("/*"): - result_lines.append(line) - continue - - # Track position within the line - processed_line = _replace_calls_in_line(line, function_name, capture_func, pattern, line_num) - result_lines.append(processed_line) - - return "\n".join(result_lines) - - -def _replace_calls_in_line(line: str, function_name: str, capture_func: str, pattern: str, line_num: int) -> str: - """Replace function calls in a single line with capture wrapper. - - Handles string literals and avoids replacing inside them. - """ - result = [] - i = 0 - length = len(line) - call_index = 0 # Track multiple calls on the same line - - while i < length: - char = line[i] - - # Skip string literals - if char in "'\"`": - quote_char = char - result.append(char) - i += 1 - - # Handle template literals with ${} expressions - if quote_char == "`": - while i < length: - if line[i] == "\\": - result.append(line[i : i + 2] if i + 1 < length else line[i:]) - i += min(2, length - i) - elif line[i] == "$" and i + 1 < length and line[i + 1] == "{": - # Template expression - need to handle nested braces - result.append(line[i : i + 2]) - i += 2 - brace_count = 1 - while i < length and brace_count > 0: - if line[i] == "{": - brace_count += 1 - elif line[i] == "}": - brace_count -= 1 - result.append(line[i]) - i += 1 - elif line[i] == quote_char: - result.append(line[i]) - i += 1 - break - else: - result.append(line[i]) - i += 1 - else: - # Regular string - while i < length: - if line[i] == "\\": - result.append(line[i : i + 2] if i + 1 < length else line[i:]) - i += min(2, length - i) - elif line[i] == quote_char: - result.append(line[i]) - i += 1 - break - else: - result.append(line[i]) - i += 1 - continue - - # Check for function call pattern - remaining = line[i:] - match = re.match(pattern, remaining) - if match: - # Check that we're not preceded by a dot (method call) or already wrapped - if i > 0 and line[i - 1] == ".": - result.append(char) - i += 1 - continue - - # Check we haven't already wrapped this - check_start = max(0, i - 25) - preceding = line[check_start:i] - if "codeflash.capture" in preceding or "codeflash.capturePerf" in preceding: - result.append(char) - i += 1 - continue - - # Generate lineId: line_number_call_index - # For multiple calls on same line, we add call_index to distinguish them - line_id = f"{line_num}_{call_index}" if call_index > 0 else str(line_num) - call_index += 1 - - # Build replacement: codeflash.capture('funcName', 'lineId', func, args) - args = match.group(1).strip() - if args: - replacement = f"codeflash.{capture_func}('{function_name}', '{line_id}', {function_name}, {args})" - else: - replacement = f"codeflash.{capture_func}('{function_name}', '{line_id}', {function_name})" - - result.append(replacement) - i += match.end() - continue - - result.append(char) - i += 1 - - return "".join(result) - - -def _safe_replace_function_calls( - source: str, function_name: str, replace_func: Callable[[re.Match[str]], str], pattern: str -) -> str: - """Replace function calls while avoiding string literals and comments. - - This is a simplified approach that handles common cases. - A more robust solution would use a proper JavaScript parser. - """ - result = [] - i = 0 - length = len(source) - - pattern_re = re.compile(pattern) - - while i < length: - char = source[i] - - # Skip string literals - if char in "'\"`": - quote_char = char - result.append(char) - i += 1 - - # Handle template literals with ${} expressions - if quote_char == "`": - while i < length: - if source[i] == "\\": - result.append(source[i : i + 2]) - i += 2 - elif source[i] == "$" and i + 1 < length and source[i + 1] == "{": - # Template expression - need to handle nested braces - result.append(source[i : i + 2]) - i += 2 - brace_count = 1 - while i < length and brace_count > 0: - if source[i] == "{": - brace_count += 1 - elif source[i] == "}": - brace_count -= 1 - result.append(source[i]) - i += 1 - elif source[i] == quote_char: - result.append(source[i]) - i += 1 - break - else: - result.append(source[i]) - i += 1 - else: - # Regular string - while i < length: - if source[i] == "\\": - result.append(source[i : i + 2]) - i += 2 - elif source[i] == quote_char: - result.append(source[i]) - i += 1 - break - else: - result.append(source[i]) - i += 1 - continue - - # Skip single-line comments - if char == "/" and i + 1 < length and source[i + 1] == "/": - j = source.find("\n", i) - if j == -1: - result.append(source[i:]) - i = length - else: - result.append(source[i:j]) - i = j - continue - - # Skip multi-line comments - if char == "/" and i + 1 < length and source[i + 1] == "*": - result.append(source[i : i + 2]) - i += 2 - j = source.find("*/", i) - if j == -1: - result.append(source[i:]) - i = length - else: - result.append(source[i : j + 2]) - i = j + 2 - continue - - # Check for function call pattern - match = pattern_re.match(source, i) - if match: - # Check that we're not preceded by a dot (method call) - if i > 0 and source[i - 1] == ".": - result.append(char) - i += 1 - continue - - # Check that we haven't already wrapped this - if i >= len("codeflash.capture") and source[i - len("codeflash.capture") : i] == "codeflash.capture": - result.append(char) - i += 1 - continue - - # Apply replacement - replacement = replace_func(match) - result.append(replacement) - i = match.end() - continue - - result.append(char) - i += 1 - - return "".join(result) - - -def get_jest_setup_code(output_file: str, mode: str = "behavior", loop_index: int = 0) -> str: - """Generate Jest setup code for setting environment variables. - - Args: - output_file: Path where results should be written - mode: Testing mode ('behavior' or 'performance') - loop_index: Current benchmark loop iteration - - Returns: - JavaScript code to set up the testing environment - - """ - return f""" -// Codeflash test setup -process.env.CODEFLASH_OUTPUT_FILE = '{output_file}'; -process.env.CODEFLASH_MODE = '{mode}'; -process.env.CODEFLASH_LOOP_INDEX = '{loop_index}'; -""" diff --git a/django/aiservice/testgen/testgen_javascript.py b/django/aiservice/testgen/testgen_javascript.py index 61d2a495f..f75b39201 100644 --- a/django/aiservice/testgen/testgen_javascript.py +++ b/django/aiservice/testgen/testgen_javascript.py @@ -1,8 +1,7 @@ """JavaScript/TypeScript test generation module. -This module generates Jest tests for JavaScript/TypeScript functions -and applies unified instrumentation for behavior verification and -performance measurement. +This module generates Jest tests for JavaScript/TypeScript functions. +Instrumentation is handled by the codeflash CLI client, not here. """ from __future__ import annotations @@ -26,7 +25,6 @@ from aiservice.validators.javascript_validator import validate_javascript_syntax from authapp.auth import AuthenticatedRequest from log_features.log_event import update_optimization_cost from log_features.log_features import log_features -from testgen.instrumentation.javascript import instrument_javascript_tests from testgen.models import TestGenerationFailedError, TestGenErrorResponseSchema, TestGenResponseSchema, TestGenSchema if TYPE_CHECKING: @@ -253,17 +251,9 @@ async def generate_javascript_tests_from_function( total_llm_cost = sum(cost_tracker) await update_optimization_cost(trace_id=trace_id, cost=total_llm_cost, user_id=user_id) - # Apply behavior instrumentation - uses codeflash.capture() to write to SQLite - behavior_instrumented = instrument_javascript_tests( - test_source=validated_code, function_name=function_name, module_path=module_path, mode="behavior" - ) - - # Apply performance instrumentation - uses codeflash.capturePerf() for stdout timing - perf_instrumented = instrument_javascript_tests( - test_source=validated_code, function_name=function_name, module_path=module_path, mode="performance" - ) - - return validated_code, behavior_instrumented, perf_instrumented + # Return uninstrumented code - instrumentation is handled by the codeflash CLI client + # This ensures consistent instrumentation using the codeflash npm package + return validated_code, validated_code, validated_code except (SyntaxError, ValueError) as e: total_llm_cost = sum(cost_tracker) diff --git a/django/aiservice/tests/testgen_instrumentation/test_instrument_javascript.py b/django/aiservice/tests/testgen_instrumentation/test_instrument_javascript.py deleted file mode 100644 index e96f184d0..000000000 --- a/django/aiservice/tests/testgen_instrumentation/test_instrument_javascript.py +++ /dev/null @@ -1,287 +0,0 @@ -"""Tests for JavaScript test instrumentation.""" - -from testgen.instrumentation.javascript.instrument_javascript import ( - get_jest_helper_path, - get_jest_setup_code, - instrument_javascript_tests, -) - - -class TestJestHelperPath: - """Tests for Jest helper path resolution.""" - - def test_helper_path_exists(self) -> None: - """Test that the Jest helper file exists.""" - helper_path = get_jest_helper_path() - - assert helper_path.exists() - assert helper_path.is_file() - assert helper_path.name == "codeflash-jest-helper.js" - - def test_helper_path_is_javascript(self) -> None: - """Test that the helper file has .js extension.""" - helper_path = get_jest_helper_path() - - assert helper_path.suffix == ".js" - - def test_helper_contains_capture_function(self) -> None: - """Test that the helper contains the capture function.""" - helper_path = get_jest_helper_path() - content = helper_path.read_text() - - assert "function capture" in content - assert "module.exports" in content - - -class TestJestSetupCode: - """Tests for Jest setup code generation.""" - - def test_generates_environment_variables(self) -> None: - """Test that setup code sets environment variables.""" - setup = get_jest_setup_code("/tmp/results.bin", "behavior", 0) - - assert "CODEFLASH_OUTPUT_FILE" in setup - assert "/tmp/results.bin" in setup - assert "CODEFLASH_MODE" in setup - assert "behavior" in setup - assert "CODEFLASH_LOOP_INDEX" in setup - - def test_performance_mode(self) -> None: - """Test setup code for performance mode.""" - setup = get_jest_setup_code("/tmp/results.bin", "performance", 5) - - assert "performance" in setup - assert "'5'" in setup or "5" in setup - - -class TestInstrumentJavaScriptTests: - """Tests for the main instrumentation function.""" - - def test_adds_helper_import(self) -> None: - """Test that the codeflash helper import is added.""" - test_source = """ -const { fibonacci } = require('./fibonacci'); - -describe('fibonacci', () => { - test('should return 0 for n=0', () => { - expect(fibonacci(0)).toBe(0); - }); -}); -""" - result = instrument_javascript_tests( - test_source=test_source, function_name="fibonacci", module_path="./fibonacci" - ) - - assert "codeflash-jest-helper" in result - assert "const codeflash = require" in result - - def test_does_not_duplicate_import(self) -> None: - """Test that import is not duplicated if already present.""" - test_source = """ -const codeflash = require('codeflash-jest-helper'); -const { fibonacci } = require('./fibonacci'); - -describe('fibonacci', () => { - test('should return 0 for n=0', () => { - expect(codeflash.capture('fibonacci', fibonacci, 0)).toBe(0); - }); -}); -""" - result = instrument_javascript_tests( - test_source=test_source, function_name="fibonacci", module_path="./fibonacci" - ) - - # Should only have one import - assert result.count("codeflash-jest-helper") == 1 - - def test_wraps_function_calls(self) -> None: - """Test that function calls are wrapped with codeflash.capture.""" - test_source = """ -const { add } = require('./math'); - -describe('add', () => { - test('should add two numbers', () => { - const result = add(1, 2); - expect(result).toBe(3); - }); -}); -""" - result = instrument_javascript_tests(test_source=test_source, function_name="add", module_path="./math") - - # New signature includes line_id: codeflash.capture('funcName', 'lineId', func, args) - assert "codeflash.capture('add'," in result - assert ", add, 1, 2)" in result - - def test_wraps_function_without_args(self) -> None: - """Test wrapping function calls without arguments.""" - test_source = """ -const { getVersion } = require('./utils'); - -test('should return version', () => { - expect(getVersion()).toBe('1.0.0'); -}); -""" - result = instrument_javascript_tests(test_source=test_source, function_name="getVersion", module_path="./utils") - - # New signature includes line_id: codeflash.capture('funcName', 'lineId', func) - assert "codeflash.capture('getVersion'," in result - assert ", getVersion)" in result - - def test_preserves_test_structure(self) -> None: - """Test that the overall test structure is preserved.""" - test_source = """ -const { sort } = require('./sort'); - -describe('sort', () => { - describe('basic cases', () => { - test('should sort empty array', () => { - expect(sort([])).toEqual([]); - }); - - test('should sort single element', () => { - expect(sort([1])).toEqual([1]); - }); - }); -}); -""" - result = instrument_javascript_tests(test_source=test_source, function_name="sort", module_path="./sort") - - # Test structure should be preserved - assert "describe('sort'" in result - assert "describe('basic cases'" in result - assert "test('should sort empty array'" in result - assert "test('should sort single element'" in result - - def test_handles_es6_imports(self) -> None: - """Test that ES6 imports are handled correctly.""" - test_source = """ -import { fibonacci } from './fibonacci'; - -describe('fibonacci', () => { - test('base case', () => { - expect(fibonacci(0)).toBe(0); - }); -}); -""" - result = instrument_javascript_tests( - test_source=test_source, function_name="fibonacci", module_path="./fibonacci" - ) - - # Helper should be added - assert "codeflash-jest-helper" in result - # Original import should be preserved - assert "import { fibonacci }" in result - - def test_does_not_wrap_method_calls(self) -> None: - """Test that method calls on objects are not wrapped.""" - test_source = """ -const { Calculator } = require('./calc'); - -test('should add', () => { - const calc = new Calculator(); - expect(calc.add(1, 2)).toBe(3); -}); -""" - result = instrument_javascript_tests(test_source=test_source, function_name="add", module_path="./calc") - - # Method calls should NOT be wrapped (they have a dot before them) - # Only standalone function calls should be wrapped - assert "calc.add" in result # Method call preserved - # But not wrapped (no codeflash.capture around method call) - - def test_handles_multiple_calls_same_test(self) -> None: - """Test handling multiple function calls in the same test.""" - test_source = """ -const { process } = require('./processor'); - -test('should process multiple inputs', () => { - const result1 = process('a'); - const result2 = process('b'); - const result3 = process('c'); - expect([result1, result2, result3]).toEqual(['A', 'B', 'C']); -}); -""" - result = instrument_javascript_tests( - test_source=test_source, function_name="process", module_path="./processor" - ) - - # All three calls should be wrapped - assert result.count("codeflash.capture('process'") == 3 - - def test_handles_async_functions(self) -> None: - """Test instrumentation of async function calls.""" - test_source = """ -const { fetchData } = require('./api'); - -test('should fetch data', async () => { - const data = await fetchData('http://example.com'); - expect(data).toBeDefined(); -}); -""" - result = instrument_javascript_tests(test_source=test_source, function_name="fetchData", module_path="./api") - - # Should wrap the async function call - assert "codeflash.capture('fetchData'" in result - - -class TestInstrumentationEdgeCases: - """Tests for edge cases in instrumentation.""" - - def test_function_name_in_string_not_wrapped(self) -> None: - """Test that function names in strings are not wrapped.""" - test_source = """ -const { add } = require('./math'); - -test('should call add function', () => { - console.log('Calling add function'); - expect(add(1, 2)).toBe(3); -}); -""" - result = instrument_javascript_tests(test_source=test_source, function_name="add", module_path="./math") - - # The string should remain unchanged - assert "'Calling add function'" in result - # But the actual call should be wrapped (with line_id) - assert "codeflash.capture('add'," in result - assert ", add, 1, 2)" in result - - def test_function_name_in_comment_not_wrapped(self) -> None: - """Test that function names in comments are not wrapped.""" - test_source = """ -const { multiply } = require('./math'); - -test('should multiply', () => { - // multiply is the function we're testing - const result = multiply(2, 3); - expect(result).toBe(6); -}); -""" - result = instrument_javascript_tests(test_source=test_source, function_name="multiply", module_path="./math") - - # The actual call should be wrapped (with line_id) - assert "codeflash.capture('multiply'," in result - assert ", multiply, 2, 3)" in result - - def test_empty_test_file(self) -> None: - """Test handling of empty test file.""" - result = instrument_javascript_tests(test_source="", function_name="test", module_path="./test") - - # Should add the helper import even to empty file - assert "codeflash-jest-helper" in result - - def test_complex_arguments(self) -> None: - """Test handling of complex function arguments.""" - test_source = """ -const { process } = require('./processor'); - -test('should process', () => { - const result = process({ key: 'value' }, [1, 2, 3], () => true); - expect(result).toBeDefined(); -}); -""" - result = instrument_javascript_tests( - test_source=test_source, function_name="process", module_path="./processor" - ) - - # Should handle complex arguments - assert "codeflash.capture('process'" in result diff --git a/django/aiservice/tests/testgen_instrumentation/test_jest_helper.py b/django/aiservice/tests/testgen_instrumentation/test_jest_helper.py deleted file mode 100644 index f94732983..000000000 --- a/django/aiservice/tests/testgen_instrumentation/test_jest_helper.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Tests for the codeflash-jest-helper.js module structure.""" - -import pytest - -from testgen.instrumentation.javascript.instrument_javascript import get_jest_helper_path - - -class TestJestHelperStructure: - """Tests for the structure and content of the Jest helper module.""" - - @pytest.fixture - def helper_content(self) -> str: - """Load the Jest helper content.""" - helper_path = get_jest_helper_path() - return helper_path.read_text() - - def test_exports_capture_function(self, helper_content: str) -> None: - """Test that the helper exports a capture function.""" - assert "function capture" in helper_content - assert "module.exports" in helper_content - # Check that capture is exported - assert "capture" in helper_content.split("module.exports")[1] - - def test_has_safe_serialize(self, helper_content: str) -> None: - """Test that the helper has a safeSerialize function for handling complex objects.""" - assert "safeSerialize" in helper_content or "safe" in helper_content.lower() - - def test_handles_promises(self, helper_content: str) -> None: - """Test that the helper handles Promise returns for async functions.""" - assert "Promise" in helper_content - assert ".then" in helper_content or "instanceof Promise" in helper_content - - def test_records_timing(self, helper_content: str) -> None: - """Test that the helper records timing information.""" - assert "performance" in helper_content.lower() - assert "duration" in helper_content.lower() or "time" in helper_content.lower() - - def test_captures_errors(self, helper_content: str) -> None: - """Test that the helper captures and re-throws errors.""" - assert "error" in helper_content.lower() - assert "catch" in helper_content - assert "throw" in helper_content - - def test_uses_environment_variables(self, helper_content: str) -> None: - """Test that the helper reads from environment variables.""" - assert "process.env" in helper_content - assert "CODEFLASH" in helper_content - - def test_has_write_results_function(self, helper_content: str) -> None: - """Test that there's a function to write results to file.""" - assert "writeResults" in helper_content or "write" in helper_content.lower() - assert "fs" in helper_content # Uses fs module - - def test_has_jest_hooks(self, helper_content: str) -> None: - """Test that the helper includes Jest lifecycle hooks.""" - # Should reference beforeEach and/or afterAll hooks - assert "beforeEach" in helper_content or "afterAll" in helper_content - - def test_has_invocation_tracking(self, helper_content: str) -> None: - """Test that the helper tracks invocation order.""" - assert "invocation" in helper_content.lower() - - def test_handles_special_types(self, helper_content: str) -> None: - """Test that the helper handles special JavaScript types.""" - # Should handle at least some special types - special_types = ["Date", "Map", "Set", "Symbol", "BigInt", "Error"] - handled = sum(1 for t in special_types if t in helper_content) - assert handled >= 3 # Should handle at least 3 special types - - def test_handles_circular_references(self, helper_content: str) -> None: - """Test that the helper handles circular references.""" - assert "Circular" in helper_content or "seen" in helper_content.lower() - - def test_valid_javascript_syntax(self, helper_content: str) -> None: - """Test that the helper has valid JavaScript syntax (basic check).""" - # Basic bracket matching - brackets = {"(": ")", "[": "]", "{": "}"} - stack = [] - in_string = False - string_char = None - - for char in helper_content: - if char in "'\"`" and not in_string: - in_string = True - string_char = char - elif char == string_char and in_string: - in_string = False - string_char = None - elif not in_string: - if char in brackets: - stack.append(brackets[char]) - elif char in brackets.values(): - if stack and stack[-1] == char: - stack.pop() - - # Should have balanced brackets (allowing for some slack due to strings) - assert len(stack) < 5 # Some tolerance for complex string handling - - -class TestJestHelperExports: - """Tests for what the Jest helper exports.""" - - @pytest.fixture - def helper_content(self) -> str: - """Load the Jest helper content.""" - helper_path = get_jest_helper_path() - return helper_path.read_text() - - def test_exports_section_exists(self, helper_content: str) -> None: - """Test that there's a module.exports section.""" - assert "module.exports" in helper_content - - def test_exports_capture(self, helper_content: str) -> None: - """Test that capture is exported.""" - exports_section = helper_content.split("module.exports")[-1] - assert "capture" in exports_section - - def test_exports_multiple_functions(self, helper_content: str) -> None: - """Test that multiple utility functions are exported.""" - exports_section = helper_content.split("module.exports")[-1] - # Should export multiple things - export_count = exports_section.count(",") - assert export_count >= 2 # At least 3 exports - - -class TestJestHelperDocumentation: - """Tests for documentation in the Jest helper.""" - - @pytest.fixture - def helper_content(self) -> str: - """Load the Jest helper content.""" - helper_path = get_jest_helper_path() - return helper_path.read_text() - - def test_has_jsdoc_comments(self, helper_content: str) -> None: - """Test that the helper has JSDoc comments.""" - # Should have at least some JSDoc-style comments - assert "/**" in helper_content - assert "@param" in helper_content or "@returns" in helper_content - - def test_has_usage_instructions(self, helper_content: str) -> None: - """Test that the helper has usage instructions.""" - # Should have some usage guidance - assert "Usage" in helper_content or "require" in helper_content - - def test_documents_environment_variables(self, helper_content: str) -> None: - """Test that environment variables are documented.""" - assert "CODEFLASH_OUTPUT_FILE" in helper_content - # Mode is now determined by function choice (capture vs capturePerf), not env var - assert "CODEFLASH_LOOP_INDEX" in helper_content From dd4510b254cbf728904e6b18bd862110426d83ec Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Wed, 28 Jan 2026 16:47:43 +0530 Subject: [PATCH 004/184] new endpoint for close pr --- js/cf-api/constants/index.ts | 1 + js/cf-api/endpoints/close-pr.ts | 192 ++++++++++++++++ .../endpoints/tests/close-pr.unit.test.ts | 217 ++++++++++++++++++ js/cf-api/routes/github.routes.ts | 2 + 4 files changed, 412 insertions(+) create mode 100644 js/cf-api/endpoints/close-pr.ts create mode 100644 js/cf-api/endpoints/tests/close-pr.unit.test.ts diff --git a/js/cf-api/constants/index.ts b/js/cf-api/constants/index.ts index 8804bc43f..382576d96 100644 --- a/js/cf-api/constants/index.ts +++ b/js/cf-api/constants/index.ts @@ -39,6 +39,7 @@ export const ROUTES = { // GitHub IS_GITHUB_APP_INSTALLED: "/is-github-app-installed", SETUP_GITHUB_ACTIONS: "/setup-github-actions", + CLOSE_PR: "/close-pr", // Subscription SUBSCRIPTION: "/subscription", diff --git a/js/cf-api/endpoints/close-pr.ts b/js/cf-api/endpoints/close-pr.ts new file mode 100644 index 000000000..a520bedb2 --- /dev/null +++ b/js/cf-api/endpoints/close-pr.ts @@ -0,0 +1,192 @@ +import { Request, Response } from "express" +import { AuthorizedUserReq } from "types.js" +import { userNickname } from "../auth0-mgmt.js" +import { getInstallationOctokitByOwner, isUserCollaborator } from "../github/github-utils.js" +import { githubApp } from "../github/github-app.js" +import { logger } from "../utils/logger.js" +import { + missingRequiredFields, + validationFailure, + unauthorized, + githubInstallationError, + githubNotCollaborator, + githubPrNotFound, + forbidden, + internalServerError, +} from "../exceptions/index.js" + +// Dependencies interface for easier testing +export interface ClosePrDependencies { + userNickname: typeof userNickname + getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner + isUserCollaborator: typeof isUserCollaborator + githubApp: typeof githubApp +} + +// Default dependencies +let dependencies: ClosePrDependencies = { + userNickname, + getInstallationOctokitByOwner, + isUserCollaborator, + githubApp, +} + +// For testing - allow dependency injection +export function setClosePrDependencies(deps: Partial) { + dependencies = { ...dependencies, ...deps } +} + +export function resetClosePrDependencies() { + dependencies = { + userNickname, + getInstallationOctokitByOwner, + isUserCollaborator, + githubApp, + } +} + +/** + * Close a PR that was created by Codeflash + * + * This endpoint allows closing PRs that were raised by mistake on client repositories. + * It verifies that: + * 1. The user is authenticated and is a collaborator on the repo + * 2. The PR was created by the Codeflash bot (safety check) + * 3. The PR exists and is open + */ +export async function closePr(req: Request, res: Response): Promise { + const { owner, repo, pr_number, comment } = req.body + + // Validate required fields + if (!owner || !repo || !pr_number) { + throw missingRequiredFields("owner, repo, pr_number") + } + + const ownerStr = String(owner).trim() + const repoStr = String(repo).trim() + const prNumber = Number(pr_number) + + if (ownerStr === "" || repoStr === "") { + throw validationFailure("owner and repo cannot be empty") + } + + if (isNaN(prNumber) || prNumber <= 0) { + throw validationFailure("pr_number must be a positive integer") + } + + try { + // Verify user is authenticated + const nickname = await dependencies.userNickname((req as AuthorizedUserReq).userId) + if (nickname == null) { + throw unauthorized("") + } + + // Get installation octokit + const installationOctokit = await dependencies.getInstallationOctokitByOwner( + dependencies.githubApp, + ownerStr, + repoStr, + ) + + if (installationOctokit instanceof Error) { + throw githubInstallationError(installationOctokit.message) + } + + //Verify user is a collaborator + const isCollaborator = await dependencies.isUserCollaborator( + installationOctokit, + ownerStr, + repoStr, + nickname, + ) + + if (!isCollaborator) { + throw githubNotCollaborator(`${ownerStr}/${repoStr}`) + } + + // Get the PR to verify it exists and was created by Codeflash + let pr + try { + const prResponse = await installationOctokit.rest.pulls.get({ + owner: ownerStr, + repo: repoStr, + pull_number: prNumber, + }) + pr = prResponse.data + } catch (error: any) { + if (error.status === 404) { + throw githubPrNotFound(`${ownerStr}/${repoStr}#${prNumber}`) + } + throw error + } + + // Safety check: Only allow closing PRs created by the Codeflash bot + const isCodeflashBot = pr.user?.type === "Bot" && pr.user?.login?.includes("codeflash") + if (!isCodeflashBot) { + throw forbidden( + `Cannot close PR #${prNumber} - it was not created by Codeflash. ` + + `PR was created by ${pr.user?.login} (${pr.user?.type})`, + ) + } + + // Check if PR is already closed + if (pr.state === "closed") { + logger.info("PR is already closed", req, { + repo: `${ownerStr}/${repoStr}`, + prNumber, + }) + res.json({ + success: true, + message: `PR #${prNumber} is already closed`, + pr_url: pr.html_url, + state: pr.state, + }) + return + } + + // Add a comment if provided + const closeComment = + comment || `This PR has been manually closed by ${nickname} via Codeflash admin API.` + + await installationOctokit.rest.issues.createComment({ + owner: ownerStr, + repo: repoStr, + issue_number: prNumber, + body: closeComment, + }) + + // Close the PR + await installationOctokit.rest.pulls.update({ + owner: ownerStr, + repo: repoStr, + pull_number: prNumber, + state: "closed", + }) + + logger.info("Successfully closed PR", req, { + repo: `${ownerStr}/${repoStr}`, + prNumber, + closedBy: nickname, + }) + + res.json({ + success: true, + message: `Successfully closed PR #${prNumber}`, + pr_url: pr.html_url, + closed_by: nickname, + }) + } catch (error: any) { + // Re-throw AppExceptions + if (error && typeof error === "object" && "getHttpStatus" in error) { + throw error + } + + logger.errorWithSentry( + "Error closing PR", + req, + { repo: `${ownerStr}/${repoStr}`, prNumber }, + error, + ) + throw internalServerError("Error closing PR") + } +} diff --git a/js/cf-api/endpoints/tests/close-pr.unit.test.ts b/js/cf-api/endpoints/tests/close-pr.unit.test.ts new file mode 100644 index 000000000..917c25ffe --- /dev/null +++ b/js/cf-api/endpoints/tests/close-pr.unit.test.ts @@ -0,0 +1,217 @@ +import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from "@jest/globals" +import { Request, Response } from "express" + +let closePr: typeof import("../close-pr").closePr +let setClosePrDependencies: typeof import("../close-pr").setClosePrDependencies +let resetClosePrDependencies: typeof import("../close-pr").resetClosePrDependencies + +interface TestRequest extends Request { + userId: string +} + +describe("closePr", () => { + beforeAll(async () => { + process.env.KEY_VAULT_NAME = "mocked-keyvault-name" + const mod = await import("../close-pr") + closePr = mod.closePr + setClosePrDependencies = mod.setClosePrDependencies + resetClosePrDependencies = mod.resetClosePrDependencies + }) + + let mockReq: Partial + let mockRes: Partial + let mockDependencies: any + + beforeEach(() => { + mockRes = { + status: jest.fn().mockReturnThis() as any, + send: jest.fn() as any, + json: jest.fn() as any, + } + + mockDependencies = { + userNickname: jest.fn(), + getInstallationOctokitByOwner: jest.fn(), + isUserCollaborator: jest.fn(), + githubApp: {}, + } + + setClosePrDependencies(mockDependencies) + }) + + afterEach(() => { + resetClosePrDependencies() + jest.clearAllMocks() + }) + + describe("input validation", () => { + it("should throw missingRequiredFields when owner is missing", async () => { + mockReq = { + body: { repo: "test-repo", pr_number: 123 }, + userId: "test-user-id", + } + + await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow( + "Missing required fields", + ) + }) + + it("should throw validationFailure when owner is empty", async () => { + mockReq = { + body: { owner: " ", repo: "test-repo", pr_number: 123 }, + userId: "test-user-id", + } + + await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow( + "owner and repo cannot be empty", + ) + }) + + it("should throw validationFailure when pr_number is invalid", async () => { + mockReq = { + body: { owner: "test-owner", repo: "test-repo", pr_number: "invalid" }, + userId: "test-user-id", + } + + await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow( + "pr_number must be a positive integer", + ) + }) + }) + + describe("authorization", () => { + it("should throw unauthorized when user nickname is null", async () => { + mockReq = { + body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, + userId: "test-user-id", + } + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue(null) + + await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow() + }) + + it("should throw githubNotCollaborator when user is not a collaborator", async () => { + mockReq = { + body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, + userId: "test-user-id", + } + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") + ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ + rest: { pulls: { get: jest.fn() } }, + }) + ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(false) + + await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow( + "not a collaborator", + ) + }) + }) + + describe("PR validation", () => { + it("should throw forbidden when PR was not created by Codeflash", async () => { + mockReq = { + body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, + userId: "test-user-id", + } + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") + ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ + rest: { + pulls: { + get: jest.fn().mockResolvedValue({ + data: { + number: 123, + state: "open", + html_url: "https://github.com/test-owner/test-repo/pull/123", + user: { login: "some-other-user", type: "User" }, + }, + }), + }, + }, + }) + ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) + + await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow( + "not created by Codeflash", + ) + }) + }) + + describe("successful operations", () => { + it("should return success when PR is already closed", async () => { + mockReq = { + body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, + userId: "test-user-id", + } + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") + ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ + rest: { + pulls: { + get: jest.fn().mockResolvedValue({ + data: { + number: 123, + state: "closed", + html_url: "https://github.com/test-owner/test-repo/pull/123", + user: { login: "codeflash-ai[bot]", type: "Bot" }, + }, + }), + }, + }, + }) + ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) + + await closePr(mockReq as Request, mockRes as Response) + + expect(mockRes.json).toHaveBeenCalledWith({ + success: true, + message: "PR #123 is already closed", + pr_url: "https://github.com/test-owner/test-repo/pull/123", + state: "closed", + }) + }) + + it("should successfully close a PR created by Codeflash", async () => { + const mockUpdate = jest.fn().mockResolvedValue({}) + const mockCreateComment = jest.fn().mockResolvedValue({}) + + mockReq = { + body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, + userId: "test-user-id", + } + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") + ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ + rest: { + pulls: { + get: jest.fn().mockResolvedValue({ + data: { + number: 123, + state: "open", + html_url: "https://github.com/test-owner/test-repo/pull/123", + user: { login: "codeflash-ai[bot]", type: "Bot" }, + }, + }), + update: mockUpdate, + }, + issues: { + createComment: mockCreateComment, + }, + }, + }) + ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) + + await closePr(mockReq as Request, mockRes as Response) + + expect(mockUpdate).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + pull_number: 123, + state: "closed", + }) + expect(mockRes.json).toHaveBeenCalledWith({ + success: true, + message: "Successfully closed PR #123", + pr_url: "https://github.com/test-owner/test-repo/pull/123", + closed_by: "test-user", + }) + }) + }) +}) \ No newline at end of file diff --git a/js/cf-api/routes/github.routes.ts b/js/cf-api/routes/github.routes.ts index 1c3b29a18..148a7c264 100644 --- a/js/cf-api/routes/github.routes.ts +++ b/js/cf-api/routes/github.routes.ts @@ -2,6 +2,7 @@ import { Router } from "express" import { addAsync } from "@awaitjs/express" import { isGitHubAppInstalled } from "../endpoints/is-github-app-installed.js" import { setupGithubActions } from "../endpoints/setup-github-actions.js" +import { closePr } from "../endpoints/close-pr.js" import { ROUTES } from "../constants/index.js" const router = addAsync(Router()) as any @@ -9,5 +10,6 @@ const router = addAsync(Router()) as any // GitHub integration endpoints router.getAsync(ROUTES.IS_GITHUB_APP_INSTALLED, isGitHubAppInstalled) router.postAsync(ROUTES.SETUP_GITHUB_ACTIONS, setupGithubActions) +router.postAsync(ROUTES.CLOSE_PR, closePr) export default router From 557fb119399c77e3a1e9b0e8fc1038b4f2c8fb91 Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 28 Jan 2026 13:41:06 +0200 Subject: [PATCH 005/184] remove jest globals check (client handles it now) --- .../instrumentation/javascript/instrument_javascript.py | 5 ----- django/aiservice/testgen/testgen_javascript.py | 2 -- 2 files changed, 7 deletions(-) diff --git a/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py b/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py index 7d416e60f..76b37d1a1 100644 --- a/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py +++ b/django/aiservice/testgen/instrumentation/javascript/instrument_javascript.py @@ -69,8 +69,6 @@ def instrument_javascript_tests( module_path: str, is_ts: bool = False, mode: Literal["behavior", "performance"] = "behavior", - # TODO: let the client decide whether to inject jest or not - inject_jest: bool = False, ) -> str: """Instrument JavaScript tests with codeflash helper. @@ -95,9 +93,6 @@ def instrument_javascript_tests( # First, fix any incorrect import paths generated by the LLM test_source = _fix_import_path(test_source, function_name, module_path) - if inject_jest: - test_source = "import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from '@jest/globals'\n" + test_source - if is_ts: # Disable TypeScript checks test_source = "// @ts-nocheck\n" + test_source diff --git a/django/aiservice/testgen/testgen_javascript.py b/django/aiservice/testgen/testgen_javascript.py index c3b9baf16..fdbcad444 100644 --- a/django/aiservice/testgen/testgen_javascript.py +++ b/django/aiservice/testgen/testgen_javascript.py @@ -264,7 +264,6 @@ async def generate_javascript_tests_from_function( module_path=module_path, is_ts=is_ts, mode="behavior", - inject_jest=True, ) # Apply performance instrumentation - uses codeflash.capturePerf() for stdout timing @@ -274,7 +273,6 @@ async def generate_javascript_tests_from_function( module_path=module_path, is_ts=is_ts, mode="performance", - inject_jest=True, # TODO: let the client decide if it wants jest to be injected or not ) return validated_code, behavior_instrumented, perf_instrumented From 8a66c78220b02484d5bfa0ccb5d91875a5bcf244 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:05:45 -0500 Subject: [PATCH 006/184] refactor: consolidate CST utilities and simplify add_missing_imports (#2324) ## Summary - Consolidate shared CST utilities into `aiservice/common/cst_utils.py` - Simplify `add_missing_imports` by removing redundant abstractions - Require CST modules instead of strings in postprocessing pipeline --- .../aiservice/aiservice/common/cst_utils.py | 226 ++++ .../context_utils/optimizer_context.py | 6 +- django/aiservice/optimizer/postprocess.py | 36 +- .../postprocessing/add_missing_imports.py | 1152 +++++++---------- .../postprocessing/postprocess_pipeline.py | 23 +- .../postprocessing/topdef_terminator.py | 111 +- .../optimizer/test_docstring_replacement.py | 9 +- .../tests/optimizer/test_optimizer.py | 63 +- .../test_add_missing_imports.py | 786 +++-------- django/aiservice/uv.lock | 465 ++++--- 10 files changed, 1204 insertions(+), 1673 deletions(-) diff --git a/django/aiservice/aiservice/common/cst_utils.py b/django/aiservice/aiservice/common/cst_utils.py index 3c45ac1ff..61bd2fede 100644 --- a/django/aiservice/aiservice/common/cst_utils.py +++ b/django/aiservice/aiservice/common/cst_utils.py @@ -54,6 +54,206 @@ class DepthTrackingMixin: return self._function_depth > 0 +# ============================================================================== +# Import Extraction Utilities +# ============================================================================== + + +def extract_import_info(alias: cst.ImportAlias, module_name: str = "") -> tuple[str, str, str]: + """Extract import information from a single import alias. + + Args: + alias: The ImportAlias node (e.g., `foo` or `foo as bar`) + module_name: The module being imported from (for ImportFrom statements) + + Returns: + Tuple of (available_name, module_path, original_name) where: + - available_name: The name available in the local namespace + - module_path: The full module path for the import + - original_name: The original name being imported + + Examples: + `from pkg import foo` -> ("foo", "pkg", "foo") + `from pkg import foo as bar` -> ("bar", "pkg", "foo") + `import pkg.sub` -> ("pkg", "pkg.sub", "pkg.sub") + `import pkg.sub as p` -> ("p", "pkg.sub", "pkg.sub") + + """ + original_name = alias.name.value if isinstance(alias.name, cst.Name) else get_dotted_name(alias.name) + + if alias.asname and isinstance(alias.asname.name, cst.Name): + available_name = alias.asname.name.value + elif module_name: + # ImportFrom: available name is the imported name + available_name = original_name + else: + # Import: available name is the first part of dotted name + available_name = original_name.split(".")[0] + + return available_name, module_name, original_name + + +def extract_imports_from_import(node: cst.Import) -> dict[str, tuple[str, str]]: + """Extract all imported names from an Import statement. + + Returns: + Dict mapping available_name -> (module_path, original_name) + + Example: + `import os, sys as system` -> {"os": ("os", "os"), "system": ("sys", "sys")} + + """ + if isinstance(node.names, cst.ImportStar): + return {} + + result: dict[str, tuple[str, str]] = {} + for alias in node.names: + available_name, _, original_name = extract_import_info(alias) + if available_name: + result[available_name] = (original_name, original_name) + return result + + +def extract_imports_from_import_from(node: cst.ImportFrom) -> dict[str, tuple[str, str]]: + """Extract all imported names from an ImportFrom statement. + + Returns: + Dict mapping available_name -> (module_path, original_name) + + Example: + `from os.path import join, exists as ex` -> {"join": ("os.path", "join"), "ex": ("os.path", "exists")} + + """ + if isinstance(node.names, cst.ImportStar): + return {} + + module_name = get_dotted_name(node.module) if node.module else "" + result: dict[str, tuple[str, str]] = {} + for alias in node.names: + available_name, _, original_name = extract_import_info(alias, module_name) + if available_name: + result[available_name] = (module_name, original_name) + return result + + +def collect_imported_names_from_import(node: cst.Import) -> set[str]: + """Collect just the available names from an Import statement.""" + if isinstance(node.names, cst.ImportStar): + return set() + return set(extract_imports_from_import(node).keys()) + + +def collect_imported_names_from_import_from(node: cst.ImportFrom) -> set[str]: + """Collect just the available names from an ImportFrom statement.""" + if isinstance(node.names, cst.ImportStar): + return set() + return set(extract_imports_from_import_from(node).keys()) + + +class DefinitionRemover(DepthTrackingMixin, cst.CSTTransformer): + """Remove top-level class and function definitions by name. + + Supports: + - Simple names: "MyClass", "my_function" + - Qualified names: "ClassName.method_name" (removes class if method matches) + - Protected names that should not be removed + - Tracking which names were actually removed + + Args: + names_to_remove: Names to remove (can be simple or qualified like "Class.method") + protected_names: Names that should never be removed (takes precedence) + + Attributes: + removed_names: Set of names that were actually removed (populated after visiting) + + """ + + def __init__(self, names_to_remove: set[str] | list[str], protected_names: set[str] | None = None) -> None: + DepthTrackingMixin.__init__(self) + cst.CSTTransformer.__init__(self) + + # Parse qualified names (e.g., "ClassName.method_name") + names_list = list(names_to_remove) + self._qualified_names: list[list[str]] = [s.split(".")[-2:] for s in names_list] + self._simple_names: set[str] = {pair[-1] for pair in self._qualified_names if pair} + + self.protected_names: set[str] = protected_names or set() + self.removed_names: set[str] = set() + + # State for qualified name matching (remove class if method matches) + self._is_killable_class: bool = False + self._potential_methods: set[str] = set() + + def _should_remove(self, name: str) -> bool: + """Check if a name should be removed (not protected and in removal set).""" + return name in self._simple_names and name not in self.protected_names + + def visit_ClassDef(self, node: cst.ClassDef) -> bool: + # Check if top-level BEFORE incrementing depth + is_top_level = self._is_top_level_class() + self._visit_class() + + if not is_top_level: + return True # Still visit children for nested classes + + class_name = node.name.value + + # Check if class itself should be removed + if self._should_remove(class_name): + self._is_killable_class = True + return False + + # Check if any methods in qualified names match this class + potential_methods = { + pair[-1] + for pair in self._qualified_names + if len(pair) > 1 and pair[-2] == class_name and pair[-1] not in self.protected_names + } + if potential_methods: + self._potential_methods = potential_methods + return True + + return True + + def leave_ClassDef( + self, original_node: cst.ClassDef, updated_node: cst.ClassDef + ) -> cst.ClassDef | cst.RemovalSentinel: + self._leave_class() + + # Only remove if we're back at top level and class is marked for removal + if self._is_top_level() and self._is_killable_class: + self._is_killable_class = False + self._potential_methods = set() + self.removed_names.add(original_node.name.value) + return cst.RemovalSentinel.REMOVE + + self._potential_methods = set() + return updated_node + + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # noqa: ARG002 + self._visit_function() + return True # Visit children + + def leave_FunctionDef( + self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef + ) -> cst.FunctionDef | cst.RemovalSentinel: + self._leave_function() + + func_name = original_node.name.value + + # Top-level function removal (check after decrementing depth) + if self._is_top_level() and self._should_remove(func_name): + self.removed_names.add(func_name) + return cst.RemovalSentinel.REMOVE + + # Method matching for qualified names (marks parent class for removal) + if func_name in self._potential_methods: + self._is_killable_class = True + self._potential_methods = set() + + return updated_node + + class ImportTrackingVisitor(ast.NodeVisitor): """Base AST visitor that tracks imported names.""" @@ -81,6 +281,32 @@ def parse_module_to_cst(module_str: str) -> cst.Module: return cst.parse_module(module_str) +def file_path_to_module_path(file_path: str) -> str: + r"""Convert a file path to a module path. + + Examples: + "path/to/module.py" -> "path.to.module" + "path\\to\\module.py" -> "path.to.module" + + """ + file_path = file_path.removesuffix(".py") + return file_path.replace("/", ".").replace("\\", ".") + + +def get_dotted_name(node: cst.BaseExpression | None) -> str: + """Extract dotted module path from a CST node.""" + if node is None: + return "" + if isinstance(node, cst.Name): + return node.value + if isinstance(node, cst.Attribute): + base = get_dotted_name(node.value) + if base: + return f"{base}.{node.attr.value}" + return node.attr.value + return "" + + def get_base_class_name(base: cst.Arg) -> str | None: """Get the name of a base class from a CST Arg node. diff --git a/django/aiservice/optimizer/context_utils/optimizer_context.py b/django/aiservice/optimizer/context_utils/optimizer_context.py index 4ae4e5605..b65b78ca7 100644 --- a/django/aiservice/optimizer/context_utils/optimizer_context.py +++ b/django/aiservice/optimizer/context_utils/optimizer_context.py @@ -158,8 +158,9 @@ class SingleOptimizerContext(BaseOptimizerContext): self.code_and_explanation_before_post_processing[op_id] = CodeStrAndExplanation( code=cst_module.code, explanation=extracted.explanation ) + original_cst_module = parse_module_to_cst(self.source_code) postprocessed_list: list[CodeExplanationAndID] = optimizations_postprocessing_pipeline( - self.source_code, + original_cst_module, [CodeExplanationAndID(cst_module=cst_module, id=op_id, explanation=extracted.explanation)], ) @@ -307,12 +308,13 @@ class MultiOptimizerContext(BaseOptimizerContext): original_code = original_code_files[original_file] try: new_cst_module = parse_module_to_cst(new_code) + original_cst_module = parse_module_to_cst(original_code) code_and_explanation = CodeExplanationAndID( cst_module=new_cst_module, id=f"{original_file}:post-processing", explanation=extracted.explanation ) postprocessed_list: list[CodeExplanationAndID] = optimizations_postprocessing_pipeline( - original_code, [code_and_explanation] + original_cst_module, [code_and_explanation] ) if len(postprocessed_list) == 0: # rejected by postprocessing pipeline diff --git a/django/aiservice/optimizer/postprocess.py b/django/aiservice/optimizer/postprocess.py index fb9a90ed3..c46dcc785 100644 --- a/django/aiservice/optimizer/postprocess.py +++ b/django/aiservice/optimizer/postprocess.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: def deduplicate_optimizations( - _original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + _original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: """Remove optimizations that have equivalent code. @@ -34,7 +34,7 @@ def deduplicate_optimizations( Args: ---- - original_source_code (str): The original source code that was optimized. + _original_module: The original source code CST module (unused). optimized_code_and_explanations (List[CodeExplanationAndID]): A list of CodeExplanationAndID objects representing the optimized code and their explanations. @@ -55,8 +55,9 @@ def deduplicate_optimizations( def equality_check( - original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: + original_source_code = original_module.code try: original_source_ast = unparse_parse_source(original_source_code) except Exception: @@ -91,7 +92,7 @@ explanation_sub_patterns = [ def cleanup_explanations( - _original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + _original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: new_optimized_code_and_explanations = [] @@ -179,14 +180,13 @@ class DocstringTransformer(CSTTransformer): def fix_missing_docstring( - original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: visitor = DocstringVisitor() try: - original_tree = cst.parse_module(original_source_code) + original_module.visit(visitor) except Exception: return optimized_code_and_explanations - original_tree.visit(visitor) original_docstrings = visitor.original_docstrings transformer = DocstringTransformer(original_docstrings) returns = [] @@ -202,7 +202,7 @@ def fix_missing_docstring( def dedup_and_sort_imports( - _original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + _original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: new_optimized_code_and_explanations = [] for ce in optimized_code_and_explanations: @@ -228,12 +228,11 @@ class EllipsisContainingCodeVisitor(CSTVisitor): def filter_ellipsis_containing_code( - original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: new_optimized_code_and_explanations = [] original_visitor = EllipsisContainingCodeVisitor() - original_tree = cst.parse_module(original_source_code) - original_tree.visit(original_visitor) + original_module.visit(original_visitor) if original_visitor.ellipsis_containing_code: # don't check for ellipsis containing optimized code if the original code contains ellipsis return optimized_code_and_explanations @@ -522,14 +521,13 @@ def clean_extraneous_comments(original_module: cst.Module, optimized_module: cst def clean_extraneous_comments_pipeline( - original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: """Pipeline wrapper for comment cleaning. Cleans extraneous comments from all optimized code variants. """ try: - original_module = cst.parse_module(original_source_code) cleaned_results = [] for ce in optimized_code_and_explanations: @@ -552,7 +550,7 @@ def clean_extraneous_comments_pipeline( def fix_forward_references( - _original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + _original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: """Add 'from __future__ import annotations' to fix forward reference errors. @@ -563,10 +561,8 @@ def fix_forward_references( new_optimizations = [] for ce in optimized_code_and_explanations: try: - fixed_code = add_future_annotations_import(ce.cst_module.code) - if fixed_code != ce.cst_module.code: - # Code was modified, create a new module - new_module = parse_module_to_cst(fixed_code) + new_module = add_future_annotations_import(ce.cst_module) + if new_module is not ce.cst_module: new_optimizations.append( CodeExplanationAndID(cst_module=new_module, explanation=ce.explanation, id=ce.id) ) @@ -581,7 +577,7 @@ def fix_forward_references( def optimizations_postprocessing_pipeline( - original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID] + original_module: cst.Module, optimized_code_and_explanations: list[CodeExplanationAndID] ) -> list[CodeExplanationAndID]: pipeline = [ fix_missing_docstring, # We want to deduplicate with the fixed docstrings included @@ -595,5 +591,5 @@ def optimizations_postprocessing_pipeline( ] for pipeline_fn in pipeline: - optimized_code_and_explanations = pipeline_fn(original_source_code, optimized_code_and_explanations) + optimized_code_and_explanations = pipeline_fn(original_module, optimized_code_and_explanations) return optimized_code_and_explanations diff --git a/django/aiservice/testgen/postprocessing/add_missing_imports.py b/django/aiservice/testgen/postprocessing/add_missing_imports.py index 44f75f6e1..455ebba1b 100644 --- a/django/aiservice/testgen/postprocessing/add_missing_imports.py +++ b/django/aiservice/testgen/postprocessing/add_missing_imports.py @@ -1,722 +1,516 @@ -"""Remove local redefinitions and add imports for symbols from the source module.""" +"""Add missing imports to test code based on source module symbols.""" from __future__ import annotations -import ast import builtins import logging -from functools import lru_cache -from typing import TYPE_CHECKING +from dataclasses import dataclass, field import libcst as cst import sentry_sdk from libcst.codemod import CodemodContext from libcst.codemod.visitors import AddImportsVisitor -from aiservice.common.cst_utils import DepthTrackingMixin, ImportTrackingVisitor -from testgen.ast_utils.test_detection import is_test_function_name +from aiservice.common.cst_utils import ( + DefinitionRemover, + DepthTrackingMixin, + collect_imported_names_from_import, + collect_imported_names_from_import_from, + extract_imports_from_import, + extract_imports_from_import_from, + file_path_to_module_path, + get_dotted_name, +) -if TYPE_CHECKING: - from libcst import Module +logger = logging.getLogger(__name__) -_BUILTIN_NAMES = frozenset(dir(builtins)) +BUILTIN_NAMES = frozenset(dir(builtins)) -class LocalDefinitionRemover(DepthTrackingMixin, cst.CSTTransformer): - """Removes top-level class and function definitions that should be imported instead. +@dataclass +class SourceAnalysis: + """Analysis results for a source module.""" - Only removes definitions at module level - nested classes and functions are preserved. - """ + defined_symbols: set[str] = field(default_factory=set) + public_symbols: set[str] = field(default_factory=set) + imports: dict[str, tuple[str, str]] = field(default_factory=dict) + referenced_names: set[str] = field(default_factory=set) + symbol_to_module: dict[str, str] = field(default_factory=dict) - def __init__(self, names_to_remove: set[str]) -> None: - DepthTrackingMixin.__init__(self) - cst.CSTTransformer.__init__(self) - self.names_to_remove = names_to_remove - self.removed_names: set[str] = set() - def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # noqa: ARG002 - self._visit_function() - return True # Continue visiting children +@dataclass +class TestAnalysis: + """Analysis results for a test module.""" - def leave_FunctionDef( - self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef - ) -> cst.FunctionDef | cst.RemovalSentinel: - self._leave_function() - # Only remove top-level functions - if not self._is_top_level(): - return updated_node - # Don't remove test functions or functions starting with underscore - name = original_node.name.value - if is_test_function_name(name) or name.startswith("_"): - return updated_node - if name in self.names_to_remove: - self.removed_names.add(name) - return cst.RemovalSentinel.REMOVE - return updated_node + local_definitions: set[str] = field(default_factory=set) + imported_names: set[str] = field(default_factory=set) + used_names: set[str] = field(default_factory=set) - def visit_ClassDef(self, node: cst.ClassDef) -> bool: # noqa: ARG002 + @property + def undefined_names(self) -> set[str]: + return self.used_names - self.local_definitions - self.imported_names - BUILTIN_NAMES + + +class SourceModuleAnalyzer(DepthTrackingMixin, cst.CSTVisitor): + """Visitor to analyze a source module and collect symbol information.""" + + def __init__(self, module_path: str) -> None: + super().__init__() + self.module_path = module_path + self.defined_symbols: set[str] = set() + self.public_symbols: set[str] = set() + self.imports: dict[str, tuple[str, str]] = {} + self.referenced_names: set[str] = set() + self._in_assignment_target = False + + def visit_ClassDef(self, node: cst.ClassDef) -> bool | None: + if self._is_top_level(): + name = node.name.value + self.defined_symbols.add(name) + if not name.startswith("_"): + self.public_symbols.add(name) self._visit_class() - return True # Continue visiting children + return True - def leave_ClassDef( - self, original_node: cst.ClassDef, updated_node: cst.ClassDef - ) -> cst.ClassDef | cst.RemovalSentinel: + def leave_ClassDef(self, original_node: cst.ClassDef) -> None: # noqa: ARG002 self._leave_class() - # Only remove top-level classes - if not self._is_top_level(): - return updated_node - if original_node.name.value in self.names_to_remove: - self.removed_names.add(original_node.name.value) - return cst.RemovalSentinel.REMOVE - return updated_node + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool | None: + if self._is_top_level(): + name = node.name.value + self.defined_symbols.add(name) + if not name.startswith("_"): + self.public_symbols.add(name) + self._visit_function() + return True -class UndefinedNameCollector(ImportTrackingVisitor): - """Collects all names that are used but not defined in the module.""" + def leave_FunctionDef(self, original_node: cst.FunctionDef) -> None: # noqa: ARG002 + self._leave_function() - def __init__(self) -> None: - super().__init__() - self.used_names: set[str] = set() - self.defined_names: set[str] = set() - self._scope_stack: list[set[str]] = [set()] - - def _current_scope(self) -> set[str]: - return self._scope_stack[-1] - - def _is_defined(self, name: str) -> bool: - for scope in self._scope_stack: - if name in scope: - return True - return name in self.defined_names or name in self.imported_names - - def visit_FunctionDef(self, node: ast.FunctionDef) -> None: - self.defined_names.add(node.name) - self._scope_stack.append(set()) - for arg in node.args.args: - self._current_scope().add(arg.arg) - for arg in node.args.posonlyargs: - self._current_scope().add(arg.arg) - for arg in node.args.kwonlyargs: - self._current_scope().add(arg.arg) - if node.args.vararg: - self._current_scope().add(node.args.vararg.arg) - if node.args.kwarg: - self._current_scope().add(node.args.kwarg.arg) - self.generic_visit(node) - self._scope_stack.pop() - - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: - self.defined_names.add(node.name) - self._scope_stack.append(set()) - for arg in node.args.args: - self._current_scope().add(arg.arg) - for arg in node.args.posonlyargs: - self._current_scope().add(arg.arg) - for arg in node.args.kwonlyargs: - self._current_scope().add(arg.arg) - if node.args.vararg: - self._current_scope().add(node.args.vararg.arg) - if node.args.kwarg: - self._current_scope().add(node.args.kwarg.arg) - self.generic_visit(node) - self._scope_stack.pop() - - def visit_ClassDef(self, node: ast.ClassDef) -> None: - self.defined_names.add(node.name) - self._scope_stack.append(set()) - self.generic_visit(node) - self._scope_stack.pop() - - def visit_Assign(self, node: ast.Assign) -> None: - for target in node.targets: - self._collect_assigned_names(target) - self.generic_visit(node) - - def visit_AnnAssign(self, node: ast.AnnAssign) -> None: - if node.target: - self._collect_assigned_names(node.target) - self.generic_visit(node) - - def visit_NamedExpr(self, node: ast.NamedExpr) -> None: - self._collect_assigned_names(node.target) - self.generic_visit(node) - - def visit_For(self, node: ast.For) -> None: - self._collect_assigned_names(node.target) - self.generic_visit(node) - - def visit_comprehension(self, node: ast.comprehension) -> None: - self._collect_assigned_names(node.target) - self.generic_visit(node) - - def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None: - if node.name: - self._current_scope().add(node.name) - self.generic_visit(node) - - def _collect_assigned_names(self, target: ast.expr) -> None: - if isinstance(target, ast.Name): - self._current_scope().add(target.id) - elif isinstance(target, (ast.Tuple, ast.List)): - for elt in target.elts: - self._collect_assigned_names(elt) - - def visit_Name(self, node: ast.Name) -> None: - if isinstance(node.ctx, ast.Load) and not self._is_defined(node.id): - self.used_names.add(node.id) - self.generic_visit(node) - - def get_undefined_names(self) -> set[str]: - return self.used_names - self.defined_names - self.imported_names - _BUILTIN_NAMES - - -def get_symbols_from_source_code(source_code: str, *, include_private: bool = False) -> set[str]: - """Get symbols (classes, functions, constants) defined in source code. - - Args: - source_code: The source code to parse - include_private: If True, include private symbols (starting with _) - - Returns: - Set of symbol names defined in the source code - - """ - try: - tree = ast.parse(source_code) - except SyntaxError: - return set() - - # Check for __all__ first (only when not including private) - if not include_private: - for node in tree.body: - if ( - isinstance(node, ast.Assign) - and len(node.targets) == 1 - and isinstance(node.targets[0], ast.Name) - and node.targets[0].id == "__all__" - and isinstance(node.value, (ast.List, ast.Tuple)) - ): - all_names = [ - elt.value for elt in node.value.elts if isinstance(elt, ast.Constant) and isinstance(elt.value, str) - ] - return set(all_names) - - # Collect names based on include_private flag - names = set() - for node in tree.body: - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): - if include_private or not node.name.startswith("_"): - names.add(node.name) - elif isinstance(node, ast.Assign): - for target in node.targets: - if isinstance(target, ast.Name) and (include_private or not target.id.startswith("_")): - names.add(target.id) - elif ( - isinstance(node, ast.AnnAssign) - and isinstance(node.target, ast.Name) - and (include_private or not node.target.id.startswith("_")) - ): - names.add(node.target.id) - return names - - -def get_public_symbols_from_source_code(source_code: str) -> set[str]: - """Get all public symbols (classes, functions, constants) defined in source code.""" - return get_symbols_from_source_code(source_code, include_private=False) - - -def file_path_to_module_path(file_path: str) -> str: - """Convert file path to module path (e.g., 'a/b/c.py' -> 'a.b.c').""" - file_path = file_path.removesuffix(".py") - return file_path.replace("/", ".").replace("\\", ".") - - -def get_imports_from_source_code(source_code: str) -> dict[str, tuple[str, str]]: - """Extract imports from source code and map symbol names to their source modules. - - Args: - source_code: The source code to parse - - Returns: - Dictionary mapping available name to (module, original_name). - For example: - - `from X import foo` -> {"foo": ("X", "foo")} - - `from X import foo as bar` -> {"bar": ("X", "foo")} - - """ - try: - tree = ast.parse(source_code) - except SyntaxError: - return {} - - imports: dict[str, tuple[str, str]] = {} - - for node in ast.walk(tree): - if isinstance(node, ast.ImportFrom) and node.module: - for alias in node.names: - if alias.name == "*": - continue - # available_name is what's usable in code (alias if present, else original) - available_name = alias.asname if alias.asname else alias.name - # original_name is what the module actually exports - original_name = alias.name - imports[available_name] = (node.module, original_name) - elif isinstance(node, ast.Import): - for alias in node.names: - # For "import foo.bar", the symbol available is "foo" - # For "import foo.bar as baz", the symbol available is "baz" - available_name = alias.asname if alias.asname else alias.name.split(".")[0] - imports[available_name] = (alias.name, alias.name) - - return imports - - -def get_referenced_names_from_source(source_code: str) -> set[str]: - """Get names that are used but not locally defined in source code. - - This returns names that could potentially be module-level exports, - excluding locally-scoped names like loop variables, function parameters, - and comprehension variables. - - Args: - source_code: The source code to parse - - Returns: - Set of names that are used but not defined locally - - """ - try: - tree = ast.parse(source_code) - except SyntaxError: - return set() - - collector = UndefinedNameCollector() - collector.visit(tree) - # Return names that are used but not defined locally, excluding builtins - return collector.get_undefined_names() - - -def get_symbols_with_modules_from_multi_context( - source_code_blocks: dict[str, str], -) -> tuple[dict[str, str], dict[str, tuple[str, str]], set[str]]: - """Extract symbols and their source modules from multi-context code blocks. - - For multi-context source (multiple files), this tracks which file each symbol - is defined in, so we can import from the correct module. - - Args: - source_code_blocks: Dict mapping file path to source code - - Returns: - - symbols_to_module: symbol name -> defining module path - - source_imports: available name -> (module, original_name) for aliased imports - - referenced_names: names used but not defined in any file (union across all files) - - """ - symbols_to_module: dict[str, str] = {} - all_source_imports: dict[str, tuple[str, str]] = {} - all_referenced_names: set[str] = set() - - for file_path, source_code in source_code_blocks.items(): - module_path = file_path_to_module_path(file_path) - - # Get all symbols defined in this file (including private) - symbols = get_symbols_from_source_code(source_code, include_private=True) - for symbol in symbols: - # First definition wins (earlier file in dict order) - if symbol not in symbols_to_module: - symbols_to_module[symbol] = module_path - - # Get imports from this file (includes alias info) - imports = get_imports_from_source_code(source_code) - for available_name, import_info in imports.items(): - # First import wins - if available_name not in all_source_imports: - all_source_imports[available_name] = import_info - - # Get referenced but undefined names from this file - referenced = get_referenced_names_from_source(source_code) - all_referenced_names |= referenced - - return symbols_to_module, all_source_imports, all_referenced_names - - -def get_local_definitions(test_code: str) -> set[str]: - """Get names of classes and functions defined at module level in test code.""" - try: - tree = ast.parse(test_code) - except SyntaxError: - return set() - - definitions = set() - for node in tree.body: - if isinstance(node, ast.ClassDef) or ( - isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) - and not is_test_function_name(node.name) - and not node.name.startswith("_") - ): - definitions.add(node.name) - return definitions - - -def add_missing_imports_from_source(module: Module, source_code: str, module_path: str) -> Module: - """Remove local redefinitions and add imports for symbols from the source module. - - This handles cases where the LLM: - 1. Redefines classes/functions locally instead of importing them - 2. Forgets to import or define some symbols it uses - 3. Uses symbols that the source module imports from other modules - 4. Uses symbols that appear in the source module but whose definition is not visible - - The fix: - 1. Remove any local class/function definitions that exist in the source module - 2. Add imports for those removed definitions - 3. Add imports for any undefined names that exist in the source module - 4. Add imports for any undefined names that the source module imports from elsewhere - 5. As a fallback, import undefined names that appear in the source (even if not defined/imported) - """ - test_code = module.code - - try: - # Get public symbols from the source module (for removing local redefinitions) - public_symbols = get_public_symbols_from_source_code(source_code) - # Get all symbols including private (for resolving undefined names) - all_symbols = get_symbols_from_source_code(source_code, include_private=True) - # Get symbols that the source module imports from other modules - source_imports = get_imports_from_source_code(source_code) - # Get all names referenced in the source (fallback for incomplete source snippets) - referenced_names = get_referenced_names_from_source(source_code) - - if not all_symbols and not source_imports and not referenced_names: - return module - - # Find local definitions that should be imports instead (only public symbols) - local_definitions = get_local_definitions(test_code) - definitions_to_remove = local_definitions & public_symbols - - # Remove local redefinitions - if definitions_to_remove: - remover = LocalDefinitionRemover(definitions_to_remove) - module = module.visit(remover) - symbols_to_import = remover.removed_names - else: - symbols_to_import = set() - - # Also find undefined names that need imports (check all symbols including private) - collector = UndefinedNameCollector() - tree = ast.parse(module.code) - collector.visit(tree) - undefined_names = collector.get_undefined_names() - symbols_to_import |= undefined_names & all_symbols - - # Check for symbols that the source module imports from other modules - # These need to be imported from their original source, not from the module being tested - imported_symbols_needed = undefined_names & set(source_imports.keys()) - # Remove these from symbols_to_import since they'll be imported from different modules - symbols_to_import -= imported_symbols_needed - - # Fallback: check for symbols that are referenced in the source but not defined/imported - # This handles cases where the source snippet is incomplete (e.g., _ChunkerSpec used but - # definition not shown). We try to import these from the source module. - remaining_undefined = undefined_names - symbols_to_import - imported_symbols_needed - fallback_symbols = remaining_undefined & referenced_names - symbols_to_import |= fallback_symbols - - # Add imports for symbols defined in (or referenced by) the source module - # For symbols that are imported into the source from other modules, use the original source - context = CodemodContext() - for symbol in symbols_to_import: - if symbol in source_imports: - # Symbol is imported into the source module from another module - # source_imports maps available_name -> (module, original_name) - src_module, original_name = source_imports[symbol] - asname = symbol if symbol != original_name else None - AddImportsVisitor.add_needed_import(context, src_module, original_name, asname=asname) - else: - # Symbol is defined in the source module - AddImportsVisitor.add_needed_import(context, module_path, symbol) - - # Add imports for symbols that the source module imports from other modules - for symbol in imported_symbols_needed: - src_module, original_name = source_imports[symbol] - asname = symbol if symbol != original_name else None - AddImportsVisitor.add_needed_import(context, src_module, original_name, asname=asname) - - if not symbols_to_import and not imported_symbols_needed: - return module - - return AddImportsVisitor(context).transform_module(module) - - except Exception as e: # noqa: BLE001 - logging.warning("add_missing_imports_from_source failed for module %s: %s", module_path, e) - sentry_sdk.capture_exception(e) - return module - - -def add_missing_imports_from_multi_context_source( - module: Module, source_code_blocks: dict[str, str], default_module_path: str -) -> Module: - """Add imports with correct module paths for multi-context source. - - Unlike add_missing_imports_from_source (which uses a single module_path for all - fallback imports), this function tracks which file each symbol is defined in and - imports from the correct module. - - This fixes issues where symbols get imported from the wrong module. For example: - - SkyvernContext is defined in skyvern/forge/sdk/core/skyvern_context.py - - module_path is skyvern.core.script_generations.skyvern_page - - Old code: imports from skyvern_page (wrong!) - - New code: imports from skyvern.forge.sdk.core.skyvern_context (correct!) - - Args: - module: The CST module (test code) to process - source_code_blocks: Dict mapping file path to source code - default_module_path: Fallback module path if symbol's origin is unknown - - Returns: - Updated module with correct imports added - - """ - test_code = module.code - - try: - # Build symbol-to-module mapping from all source files - symbols_to_module, source_imports, referenced_names = get_symbols_with_modules_from_multi_context( - source_code_blocks - ) - - # Get public symbols (for removing local redefinitions) - # Combine public symbols from all files - public_symbols: set[str] = set() - for source_code in source_code_blocks.values(): - public_symbols |= get_public_symbols_from_source_code(source_code) - - # Get all symbols including private (for resolving undefined names) - all_symbols = set(symbols_to_module.keys()) - - if not all_symbols and not source_imports and not referenced_names: - return module - - # Find local definitions that should be imports instead (only public symbols) - local_definitions = get_local_definitions(test_code) - definitions_to_remove = local_definitions & public_symbols - - # Remove local redefinitions - if definitions_to_remove: - remover = LocalDefinitionRemover(definitions_to_remove) - module = module.visit(remover) - symbols_to_import = remover.removed_names - else: - symbols_to_import = set() - - # Find undefined names that need imports - collector = UndefinedNameCollector() - tree = ast.parse(module.code) - collector.visit(tree) - undefined_names = collector.get_undefined_names() - symbols_to_import |= undefined_names & all_symbols - - # Check for symbols that the source modules import from other modules - imported_symbols_needed = undefined_names & set(source_imports.keys()) - symbols_to_import -= imported_symbols_needed - - # Fallback: symbols referenced in source but not defined/imported - remaining_undefined = undefined_names - symbols_to_import - imported_symbols_needed - fallback_symbols = remaining_undefined & referenced_names - symbols_to_import |= fallback_symbols - - # Add imports with correct module paths - context = CodemodContext() - for symbol in symbols_to_import: - if symbol in source_imports: - # Symbol is imported by one of the source files from another module - # source_imports maps available_name -> (module, original_name) - src_module, original_name = source_imports[symbol] - # Use asname if symbol is aliased (symbol != original_name) - asname = symbol if symbol != original_name else None - AddImportsVisitor.add_needed_import(context, src_module, original_name, asname=asname) - elif symbol in symbols_to_module: - # Symbol is defined in one of the source files - use that file's module path - defining_module = symbols_to_module[symbol] - AddImportsVisitor.add_needed_import(context, defining_module, symbol) - else: - # Fallback to default module path - AddImportsVisitor.add_needed_import(context, default_module_path, symbol) - - # Add imports for symbols that source modules import from other modules - for symbol in imported_symbols_needed: - src_module, original_name = source_imports[symbol] - asname = symbol if symbol != original_name else None - AddImportsVisitor.add_needed_import(context, src_module, original_name, asname=asname) - - if not symbols_to_import and not imported_symbols_needed: - return module - - return AddImportsVisitor(context).transform_module(module) - - except Exception as e: # noqa: BLE001 - source_files = list(source_code_blocks.keys()) if source_code_blocks else [] - logging.warning( - "add_missing_imports_from_multi_context_source failed: %s, " - "default_module_path=%s, source_files=%s, module_is_none=%s", - e, - default_module_path, - source_files, - module is None, - ) - sentry_sdk.capture_exception(e) - return module - - -class AnnotationNameCollector(ImportTrackingVisitor): - """Collects all names used in type annotations.""" - - def __init__(self) -> None: - super().__init__() - self.annotation_names: set[str] = set() - self.defined_names: set[str] = set() - - def visit_FunctionDef(self, node: ast.FunctionDef) -> None: - self.defined_names.add(node.name) - self._collect_annotation_names(node) - self.generic_visit(node) - - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: - self.defined_names.add(node.name) - self._collect_annotation_names(node) - self.generic_visit(node) - - def visit_ClassDef(self, node: ast.ClassDef) -> None: - self.defined_names.add(node.name) - self.generic_visit(node) - - def visit_AnnAssign(self, node: ast.AnnAssign) -> None: - self._extract_names_from_annotation(node.annotation) - self.generic_visit(node) - - def _collect_annotation_names(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: - # Collect names from argument annotations - for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs: - if arg.annotation: - self._extract_names_from_annotation(arg.annotation) - if node.args.vararg and node.args.vararg.annotation: - self._extract_names_from_annotation(node.args.vararg.annotation) - if node.args.kwarg and node.args.kwarg.annotation: - self._extract_names_from_annotation(node.args.kwarg.annotation) - # Collect names from return annotation - if node.returns: - self._extract_names_from_annotation(node.returns) - - def _extract_names_from_annotation(self, annotation: ast.expr) -> None: - """Extract all Name nodes from a type annotation expression.""" - for node in ast.walk(annotation): - if isinstance(node, ast.Name): - self.annotation_names.add(node.id) - - def get_undefined_annotation_names(self) -> set[str]: - """Get names used in annotations that are not defined or imported.""" - return self.annotation_names - self.defined_names - self.imported_names - _BUILTIN_NAMES - - -def has_future_annotations(source_code: str) -> bool: - """Check if source code already has 'from __future__ import annotations'.""" - try: - tree = _parse_cached(source_code) - except SyntaxError: + def visit_ImportFrom(self, node: cst.ImportFrom) -> bool | None: + self.imports.update(extract_imports_from_import_from(node)) return False - for node in tree.body: - if isinstance(node, ast.ImportFrom) and node.module == "__future__": - for alias in node.names: - if alias.name == "annotations": - return True + def visit_Import(self, node: cst.Import) -> bool | None: + self.imports.update(extract_imports_from_import(node)) + return False + + def visit_AssignTarget(self, node: cst.AssignTarget) -> bool | None: # noqa: ARG002 + self._in_assignment_target = True + return True + + def leave_AssignTarget(self, original_node: cst.AssignTarget) -> None: # noqa: ARG002 + self._in_assignment_target = False + + def visit_AnnAssign(self, node: cst.AnnAssign) -> bool | None: + # For annotated assignments like x: int = 5, skip the target + # but visit the annotation and value + if node.annotation: + node.annotation.visit(self) + if node.value: + node.value.visit(self) + return False + + def visit_Attribute(self, node: cst.Attribute) -> bool | None: + # For ElementType.TEXT, we want to collect ElementType but not TEXT + # Manually visit the value (base) and skip the attr + node.value.visit(self) + return False + + def visit_Name(self, node: cst.Name) -> bool | None: + if self._in_assignment_target: + return False + + name = node.value + if self.is_importable_symbol(name): + self.referenced_names.add(name) + return False + + def is_importable_symbol(self, name: str) -> bool: + """Check if a name looks like an importable symbol. + + Includes: + - Capitalized names (classes like ElementType) + - Private classes (_ChunkerSpec) + - Constants (MAX_SIZE, but not single chars) + + Excludes: + - Local variables (lowercase) + - Magic methods (__init__) + - Single character names + """ + if not name: + return False + if name[0].isupper(): + return True + if len(name) > 1 and name[0] == "_" and name[1].isupper(): + return True + return len(name) > 1 and name.isupper() and name.isalpha() + + def get_analysis(self) -> SourceAnalysis: + return SourceAnalysis( + defined_symbols=self.defined_symbols, + public_symbols=self.public_symbols, + imports=self.imports, + referenced_names=self.referenced_names, + symbol_to_module=dict.fromkeys(self.defined_symbols, self.module_path), + ) + + +class TestModuleAnalyzer(DepthTrackingMixin, cst.CSTVisitor): + """Visitor to analyze a test module and collect symbol information.""" + + def __init__(self) -> None: + super().__init__() + self.local_definitions: set[str] = set() + self.imported_names: set[str] = set() + self.used_names: set[str] = set() + self._assigned_names: set[str] = set() + + def visit_ClassDef(self, node: cst.ClassDef) -> bool | None: + if self._is_top_level(): + self.local_definitions.add(node.name.value) + self._visit_class() + return True + + def leave_ClassDef(self, original_node: cst.ClassDef) -> None: # noqa: ARG002 + self._leave_class() + + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool | None: + if self._is_top_level(): + self.local_definitions.add(node.name.value) + self.collect_function_parameters(node.params) + self._visit_function() + return True + + def leave_FunctionDef(self, original_node: cst.FunctionDef) -> None: # noqa: ARG002 + self._leave_function() + + def collect_function_parameters(self, params: cst.Parameters) -> None: + """Collect all parameter names from a function signature.""" + for param in params.params: + self._assigned_names.add(param.name.value) + if params.star_arg and isinstance(params.star_arg, cst.Param): + self._assigned_names.add(params.star_arg.name.value) + if params.star_kwarg: + self._assigned_names.add(params.star_kwarg.name.value) + for param in params.kwonly_params: + self._assigned_names.add(param.name.value) + + def visit_ImportFrom(self, node: cst.ImportFrom) -> bool | None: + self.imported_names.update(collect_imported_names_from_import_from(node)) + return False + + def visit_Import(self, node: cst.Import) -> bool | None: + self.imported_names.update(collect_imported_names_from_import(node)) + return False + + def visit_Assign(self, node: cst.Assign) -> bool | None: + # Track assigned names + for target in node.targets: + self._collect_assigned_names(target.target) + return True + + def visit_AnnAssign(self, node: cst.AnnAssign) -> bool | None: + if node.target: + self._collect_assigned_names(node.target) + return True + + def visit_NamedExpr(self, node: cst.NamedExpr) -> bool | None: + self._collect_assigned_names(node.target) + return True + + def visit_For(self, node: cst.For) -> bool | None: + self._collect_assigned_names(node.target) + return True + + def visit_CompFor(self, node: cst.CompFor) -> bool | None: + self._collect_assigned_names(node.target) + return True + + def visit_AsName(self, node: cst.AsName) -> bool | None: + # Handles 'with X as y' and 'except E as e' + if isinstance(node.name, cst.Name): + self._assigned_names.add(node.name.value) + return True + + def _collect_assigned_names(self, target: cst.BaseExpression) -> None: + if isinstance(target, cst.Name): + self._assigned_names.add(target.value) + elif isinstance(target, (cst.Tuple, cst.List)): + for element in target.elements: + if isinstance(element, (cst.Element, cst.StarredElement)): + self._collect_assigned_names(element.value) + + def visit_Arg(self, node: cst.Arg) -> bool | None: + if node.value: + node.value.visit(self) + return False + + def visit_Name(self, node: cst.Name) -> bool | None: + self.used_names.add(node.value) + return False + + def get_analysis(self) -> TestAnalysis: + return TestAnalysis( + local_definitions=self.local_definitions | self._assigned_names, + imported_names=self.imported_names, + used_names=self.used_names, + ) + + +def analyze_source(source_cst: cst.Module, module_path: str) -> SourceAnalysis: + analyzer = SourceModuleAnalyzer(module_path) + source_cst.visit(analyzer) + return analyzer.get_analysis() + + +def analyze_test(test_module: cst.Module) -> TestAnalysis: + analyzer = TestModuleAnalyzer() + test_module.visit(analyzer) + return analyzer.get_analysis() + + +def merge_source_analyses(analyses: list[tuple[str, SourceAnalysis]]) -> SourceAnalysis: + merged = SourceAnalysis() + for module_path, analysis in analyses: + merged.defined_symbols.update(analysis.defined_symbols) + merged.public_symbols.update(analysis.public_symbols) + merged.referenced_names.update(analysis.referenced_names) + + for name, value in analysis.imports.items(): + if name not in merged.imports: + merged.imports[name] = value + + for symbol in analysis.symbol_to_module: + if symbol not in merged.symbol_to_module: + merged.symbol_to_module[symbol] = module_path + + return merged + + +def get_source_analysis(source_cst: cst.Module | dict[str, cst.Module], module_path: str) -> SourceAnalysis | None: + """Get analysis of source modules, handling both single and multi-context sources.""" + if not isinstance(source_cst, dict): + return analyze_source(source_cst, module_path) + + if not source_cst: + return None + + analyses: list[tuple[str, SourceAnalysis]] = [] + for file_path, module in source_cst.items(): + mod_path = file_path_to_module_path(str(file_path)) + analysis = analyze_source(module, mod_path) # type: ignore[arg-type] + analyses.append((mod_path, analysis)) + + return merge_source_analyses(analyses) + + +def collect_symbols_to_import( + test_analysis: TestAnalysis, source_analysis: SourceAnalysis, definitions_to_remove: set[str] +) -> set[str]: + symbols: set[str] = set() + symbols.update(definitions_to_remove) + symbols.update(test_analysis.undefined_names & source_analysis.defined_symbols) + symbols.update(test_analysis.undefined_names & set(source_analysis.imports.keys())) + + remaining_undefined = ( + test_analysis.undefined_names - source_analysis.defined_symbols - set(source_analysis.imports.keys()) + ) + symbols.update(remaining_undefined & source_analysis.referenced_names) + + return symbols + + +def apply_imports( + module: cst.Module, symbols: set[str], source_analysis: SourceAnalysis, module_path: str +) -> cst.Module: + context = CodemodContext() + + for symbol in symbols: + if symbol in source_analysis.imports: + import_module, original_name = source_analysis.imports[symbol] + if original_name == symbol: + AddImportsVisitor.add_needed_import(context, import_module, symbol) + else: + AddImportsVisitor.add_needed_import(context, import_module, original_name, asname=symbol) + elif symbol in source_analysis.symbol_to_module: + defining_module = source_analysis.symbol_to_module[symbol] + AddImportsVisitor.add_needed_import(context, defining_module, symbol) + else: + AddImportsVisitor.add_needed_import(context, module_path, symbol) + + return module.visit(AddImportsVisitor(context)) + + +def add_missing_imports( + test_module: cst.Module, source_cst: cst.Module | dict[str, cst.Module], module_path: str +) -> cst.Module: + """Add missing imports to test code based on source module symbols. + + Args: + test_module: Parsed libcst.Module of the test code + source_cst: Source module(s) - can be: + - cst.Module: a single parsed module + - dict[str, cst.Module]: file paths to parsed modules (multi-context) + module_path: Default import path (e.g., "mypackage.mymodule") + + Returns: + Modified test_module with imports added. + On any error, returns original test_module unchanged. + + """ + try: + source_analysis = get_source_analysis(source_cst, module_path) + if source_analysis is None: + return test_module + + if not source_analysis.defined_symbols and not source_analysis.imports and not source_analysis.referenced_names: + return test_module + + test_analysis = analyze_test(test_module) + definitions_to_remove = test_analysis.local_definitions & source_analysis.public_symbols + + modified_module = test_module + if definitions_to_remove: + remover = DefinitionRemover(definitions_to_remove) + modified_module = modified_module.visit(remover) + test_analysis = analyze_test(modified_module) + + symbols_to_import = collect_symbols_to_import(test_analysis, source_analysis, definitions_to_remove) + if not symbols_to_import: + return modified_module + + return apply_imports(modified_module, symbols_to_import, source_analysis, module_path) + + except Exception: # noqa: BLE001 + logger.warning("Error in add_missing_imports, returning original module", exc_info=True) + sentry_sdk.capture_exception() + return test_module + + +# ============================================================================= +# Forward reference handling +# ============================================================================= + + +def extract_names_from_cst_annotation(annotation: cst.BaseExpression, names: set[str]) -> None: + """Walk a CST annotation expression and collect all Name nodes. + + Handles: Name, Attribute, Subscript (generics), BinaryOperation (unions). + """ + if isinstance(annotation, cst.Name): + names.add(annotation.value) + elif isinstance(annotation, cst.Attribute): + # For X.Y.Z, we only care about the base name X + extract_names_from_cst_annotation(annotation.value, names) + elif isinstance(annotation, cst.Subscript): + # e.g., List[X] or Dict[K, V] - visit the base and slice + extract_names_from_cst_annotation(annotation.value, names) + for element in annotation.slice: + if isinstance(element, cst.SubscriptElement): + slice_node = element.slice + if isinstance(slice_node, cst.Index): + extract_names_from_cst_annotation(slice_node.value, names) + elif isinstance(annotation, cst.BinaryOperation): + # e.g., X | Y (union types) + extract_names_from_cst_annotation(annotation.left, names) + extract_names_from_cst_annotation(annotation.right, names) + elif isinstance(annotation, (cst.Tuple, cst.List)): + # e.g., tuple[X, Y] or list[X] + for element in annotation.elements: + if isinstance(element, (cst.Element, cst.StarredElement)): + extract_names_from_cst_annotation(element.value, names) + + +class CSTAnnotationNameCollector(cst.CSTVisitor): + """CST-based visitor to collect names used in type annotations.""" + + def __init__(self) -> None: + self.annotation_names: set[str] = set() + self.defined_names: set[str] = set() + self.imported_names: set[str] = set() + + def visit_Import(self, node: cst.Import) -> bool | None: + self.imported_names.update(collect_imported_names_from_import(node)) + return False + + def visit_ImportFrom(self, node: cst.ImportFrom) -> bool | None: + self.imported_names.update(collect_imported_names_from_import_from(node)) + return False + + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool | None: + self.defined_names.add(node.name.value) + self.collect_annotation_names_from_function(node) + return True + + def visit_ClassDef(self, node: cst.ClassDef) -> bool | None: + self.defined_names.add(node.name.value) + return True + + def visit_AnnAssign(self, node: cst.AnnAssign) -> bool | None: + extract_names_from_cst_annotation(node.annotation.annotation, self.annotation_names) + return True + + def collect_annotation_names_from_function(self, node: cst.FunctionDef) -> None: + # Collect from parameters + params = node.params + for param in [*params.params, *params.kwonly_params, *params.posonly_params]: + if param.annotation: + extract_names_from_cst_annotation(param.annotation.annotation, self.annotation_names) + if params.star_arg and isinstance(params.star_arg, cst.Param) and params.star_arg.annotation: + extract_names_from_cst_annotation(params.star_arg.annotation.annotation, self.annotation_names) + if params.star_kwarg and params.star_kwarg.annotation: + extract_names_from_cst_annotation(params.star_kwarg.annotation.annotation, self.annotation_names) + # Collect from return type + if node.returns: + extract_names_from_cst_annotation(node.returns.annotation, self.annotation_names) + + def get_undefined_annotation_names(self) -> set[str]: + return self.annotation_names - self.defined_names - self.imported_names - BUILTIN_NAMES + + +def has_future_annotations_import(module: cst.Module) -> bool: + """Check if module already has 'from __future__ import annotations'.""" + for stmt in module.body: + if isinstance(stmt, cst.SimpleStatementLine): + for small_stmt in stmt.body: + if ( + isinstance(small_stmt, cst.ImportFrom) + and small_stmt.module + and get_dotted_name(small_stmt.module) == "__future__" + ): + if isinstance(small_stmt.names, cst.ImportStar): + continue + for alias in small_stmt.names: + if alias.name.value == "annotations": + return True return False -def has_undefined_annotation_names(source_code: str) -> bool: - """Check if source code has type annotations with undefined names. - - This detects forward references that would cause NameError at runtime - without 'from __future__ import annotations'. - """ - try: - tree = ast.parse(source_code) - except SyntaxError: - return False - - collector = AnnotationNameCollector() - collector.visit(tree) - return len(collector.get_undefined_annotation_names()) > 0 - - -def add_future_annotations_import(source_code: str) -> str: - """Add 'from __future__ import annotations' to source code if needed. +def add_future_annotations_import(module: cst.Module) -> cst.Module: + """Add 'from __future__ import annotations' to module if needed. This fixes forward reference issues where type annotations reference names that aren't yet defined or imported. With this import, Python treats all annotations as strings (lazy evaluation), avoiding NameError. Args: - source_code: The source code to process + module: The CST module to process Returns: - Source code with 'from __future__ import annotations' added if needed, - otherwise returns the original source code unchanged. + The original module unchanged if import not needed, or a new + module with the import added. """ - # Parse once and reuse the AST for both checks to avoid redundant parsing - try: - tree = ast.parse(source_code) - except SyntaxError: - return source_code + if has_future_annotations_import(module): + return module - # Skip if already has the import - for node in tree.body: - if isinstance(node, ast.ImportFrom) and node.module == "__future__": - for alias in node.names: - if alias.name == "annotations": - return source_code + collector = CSTAnnotationNameCollector() + module.visit(collector) + if not collector.get_undefined_annotation_names(): + return module - # Skip if no undefined annotation names - collector = AnnotationNameCollector() - collector.visit(tree) - if len(collector.get_undefined_annotation_names()) == 0: - return source_code - - # Add the import at the very beginning - # Future imports must be at the top of the file (after docstrings/comments) - try: - module = cst.parse_module(source_code) - context = CodemodContext() - AddImportsVisitor.add_needed_import(context, "__future__", "annotations") - new_module = AddImportsVisitor(context).transform_module(module) - except Exception: # noqa: BLE001 - # If CST parsing fails, do a simple string prepend - return f"from __future__ import annotations\n\n{source_code}" - else: - return new_module.code - - -def fix_forward_references_in_source_blocks(source_code_blocks: dict[str, str]) -> dict[str, str]: - """Fix forward references in multiple source code blocks. - - For each source code block that has type annotations with undefined names, - adds 'from __future__ import annotations' to make the annotations lazy-evaluated. - - Args: - source_code_blocks: Dict mapping file path to source code - - Returns: - Dict with the same keys, but source code updated with future annotations - import where needed. - - """ - result = {} - for file_path, source_code in source_code_blocks.items(): - result[file_path] = add_future_annotations_import(source_code) - return result - - -@lru_cache(maxsize=2048) -def _parse_cached(source: str) -> ast.Module: - return ast.parse(source) + context = CodemodContext() + AddImportsVisitor.add_needed_import(context, "__future__", "annotations") + return module.visit(AddImportsVisitor(context)) diff --git a/django/aiservice/testgen/postprocessing/postprocess_pipeline.py b/django/aiservice/testgen/postprocessing/postprocess_pipeline.py index e131ab3e3..c1ad75d77 100644 --- a/django/aiservice/testgen/postprocessing/postprocess_pipeline.py +++ b/django/aiservice/testgen/postprocessing/postprocess_pipeline.py @@ -2,12 +2,11 @@ from __future__ import annotations from typing import TYPE_CHECKING +import libcst as cst + from optimizer.context_utils.context_helpers import is_multi_context, split_markdown_code from testgen.instrumentation.edit_generated_test import replace_definition_with_import -from testgen.postprocessing.add_missing_imports import ( - add_missing_imports_from_multi_context_source, - add_missing_imports_from_source, -) +from testgen.postprocessing.add_missing_imports import add_missing_imports from testgen.postprocessing.range_modifier import modify_large_loops from testgen.postprocessing.remove_unused_definitions import remove_unused_definitions_from_pytest_file from testgen.postprocessing.removeassert_transformer import remove_asserts_from_test @@ -39,14 +38,12 @@ def postprocessing_testgen_pipeline( 4. Add missing imports 5. Replace function definition with import """ - add_imports_func, add_imports_kwargs = ( - ( - add_missing_imports_from_multi_context_source, - {"source_code_blocks": split_markdown_code(source_code_being_tested), "default_module_path": module_path}, - ) - if is_multi_context(source_code_being_tested) - else (add_missing_imports_from_source, {"source_code": source_code_being_tested, "module_path": module_path}) - ) + if is_multi_context(source_code_being_tested): + source_cst = { + path: cst.parse_module(code) for path, code in split_markdown_code(source_code_being_tested).items() + } + else: + source_cst = cst.parse_module(source_code_being_tested) pipeline: list[tuple[Callable[..., Module], dict[str, Any]]] = [ (delete_top_def_nodes, {"deletable_list": helper_function_names}), @@ -61,7 +58,7 @@ def postprocessing_testgen_pipeline( "module_path": module_path, }, ), - (add_imports_func, add_imports_kwargs), + (add_missing_imports, {"source_cst": source_cst, "module_path": module_path}), (replace_definition_with_import, {"function": function_to_optimize, "module_path": module_path}), ] diff --git a/django/aiservice/testgen/postprocessing/topdef_terminator.py b/django/aiservice/testgen/postprocessing/topdef_terminator.py index 26bd15fee..d3b0f6844 100644 --- a/django/aiservice/testgen/postprocessing/topdef_terminator.py +++ b/django/aiservice/testgen/postprocessing/topdef_terminator.py @@ -1,21 +1,22 @@ from __future__ import annotations -from libcst import ( - Arg, - Attribute, - BaseCompoundStatement, - Call, - ClassDef, - CSTTransformer, - CSTVisitor, - EmptyLine, - FunctionDef, - Module, - Name, - RemovalSentinel, - RemoveFromParent, - SimpleStatementLine, -) +from typing import TYPE_CHECKING + +from libcst import Arg, Attribute, CSTTransformer, CSTVisitor, Name, RemoveFromParent + +from aiservice.common.cst_utils import DefinitionRemover + +if TYPE_CHECKING: + from libcst import ( + BaseCompoundStatement, + Call, + ClassDef, + EmptyLine, + FunctionDef, + Module, + RemovalSentinel, + SimpleStatementLine, + ) class UsedNameCollector(CSTVisitor): @@ -71,78 +72,48 @@ class UsedNameCollector(CSTVisitor): return True -class TopDefTerminator(CSTTransformer): - def __init__(self, hit_list: list[str], protected_names: set[str] | None = None) -> None: - super().__init__() - self.hit_list_parents_names: list[list[str]] = [s.split(".")[-2:] for s in hit_list] - self.hit_list_names: set[str] = {pair[-1] for pair in self.hit_list_parents_names if pair} - self.protected_names: set[str] = protected_names or set() - self.level: int = 0 - self.is_killable_class: bool = False - self.potential_methods: set[str] = set() - - def visit_ClassDef(self, node: ClassDef) -> bool: - self.level += 1 - if self.level > 1: - return False - # Don't mark as killable if this class is actually used somewhere in the code - if node.name.value in self.hit_list_names and node.name.value not in self.protected_names: - self.is_killable_class = True - return False - if potential_methods := { - pair[-1] for pair in self.hit_list_parents_names if len(pair) > 1 and pair[-2] == node.name.value - }: - self.potential_methods = potential_methods - return True - return False - - def leave_ClassDef(self, original_node: ClassDef, updated_node: ClassDef) -> ClassDef | RemovalSentinel: - self.level -= 1 - self.potential_methods = set() - if self.is_killable_class: - self.is_killable_class = False - return RemovalSentinel.REMOVE - return updated_node - - def visit_FunctionDef(self, node: FunctionDef) -> bool: - return False - - def leave_FunctionDef(self, original_node: FunctionDef, updated_node: FunctionDef) -> FunctionDef | RemovalSentinel: - if self.level == 0: - # Don't remove if the function is actually used somewhere in the code - if original_node.name.value in self.hit_list_names and original_node.name.value not in self.protected_names: - return RemovalSentinel.REMOVE - return updated_node - if original_node.name.value in self.potential_methods: - self.is_killable_class = True - self.potential_methods = set() - return updated_node - - class LeadingWhitespaceRemover(CSTTransformer): def __init__(self) -> None: self.found_first_statement = False - def leave_EmptyLine(self, original_node: EmptyLine, updated_node: EmptyLine) -> EmptyLine | RemovalSentinel: + def leave_EmptyLine( + self, + original_node: EmptyLine, # noqa: ARG002 + updated_node: EmptyLine, + ) -> EmptyLine | RemovalSentinel: if not self.found_first_statement: return RemoveFromParent() return updated_node - def visit_SimpleStatementLine(self, node: SimpleStatementLine) -> bool: + def visit_SimpleStatementLine(self, node: SimpleStatementLine) -> bool: # noqa: ARG002 self.found_first_statement = True return True - def visit_BaseCompoundStatement(self, node: BaseCompoundStatement) -> bool: + def visit_BaseCompoundStatement(self, node: BaseCompoundStatement) -> bool: # noqa: ARG002 self.found_first_statement = True return True def delete_top_def_nodes(module: Module, deletable_list: list[str]) -> Module: - # First collect all names that are actually used in the module + """Delete top-level definitions from a module. + + First collects all names that are actually used in the module to protect them, + then removes the specified definitions. + + Args: + module: The CST module to modify + deletable_list: List of names to delete (can include qualified names like "Class.method") + + Returns: + Modified module with definitions removed and leading whitespace cleaned up + + """ + # Collect all names that are actually used in the module # This protects classes/functions that are referenced in test code from being deleted used_name_collector = UsedNameCollector() module.visit(used_name_collector) - # Apply the TopDefTerminator, protecting names that are actually used - module = module.visit(TopDefTerminator(deletable_list, used_name_collector.used_names)) + # Remove definitions, protecting names that are actually used + remover = DefinitionRemover(deletable_list, protected_names=used_name_collector.used_names) + module = module.visit(remover) return module.visit(LeadingWhitespaceRemover()) diff --git a/django/aiservice/tests/optimizer/test_docstring_replacement.py b/django/aiservice/tests/optimizer/test_docstring_replacement.py index 82109f8aa..67d918c31 100644 --- a/django/aiservice/tests/optimizer/test_docstring_replacement.py +++ b/django/aiservice/tests/optimizer/test_docstring_replacement.py @@ -152,6 +152,7 @@ def example_function(): \"\"\"This is a docstring for the example function.\"\"\" return 42 """ + original_module = cst.parse_module(original_code) optimized_code = """ def example_function(): return 42 @@ -164,7 +165,7 @@ def example_function(): ] # Apply the pipeline function - result = fix_missing_docstring(original_code, code_explanations) + result = fix_missing_docstring(original_module, code_explanations) # Check if docstring was preserved assert "This is a docstring for the example function" in result[0].cst_module.code @@ -408,13 +409,13 @@ class Agent(Generic[AgentDepsT, OutputDataT]): \"\"\" return isinstance(node, End) """ - original_cst = cst.parse_module(original_code) + original_module = cst.parse_module(original_code) code_explanations = [ - CodeExplanationAndID(cst_module=original_cst, explanation="Removed unnecessary docstring", id="test-id") + CodeExplanationAndID(cst_module=original_module, explanation="Removed unnecessary docstring", id="test-id") ] # Apply the pipeline function - fix_missing_docstring(original_code, code_explanations) + fix_missing_docstring(original_module, code_explanations) # TODO : This test case fails diff --git a/django/aiservice/tests/optimizer/test_optimizer.py b/django/aiservice/tests/optimizer/test_optimizer.py index 3f3d2edd7..0bd1f83c3 100644 --- a/django/aiservice/tests/optimizer/test_optimizer.py +++ b/django/aiservice/tests/optimizer/test_optimizer.py @@ -14,7 +14,7 @@ from optimizer.postprocess import ( def test_cleanup_explanations_removes_code_markers() -> None: - original_code = "def example(): pass" + original_module = cst.parse_module("def example(): pass") empty_cst_module = cst.parse_module("") explanations = [ CodeExplanationAndID(empty_cst_module, "Hi! Here is the code:\n```python\ndef example(): pass\n```", "1"), @@ -24,12 +24,12 @@ def test_cleanup_explanations_removes_code_markers() -> None: CodeExplanationAndID(empty_cst_module, "Hi!\n", "1"), CodeExplanationAndID(empty_cst_module, "\nEnd of explanation.", "2"), ] - cleaned = cleanup_explanations(original_code, explanations) + cleaned = cleanup_explanations(original_module, explanations) assert cleaned == expected_explanations def test_cleanup_explanations_preserves_non_code_text() -> None: - original_code = "def example(): pass" + original_module = cst.parse_module("def example(): pass") empty_cst_module = cst.parse_module("") explanations = [ CodeExplanationAndID( @@ -45,29 +45,29 @@ def test_cleanup_explanations_preserves_non_code_text() -> None: CodeExplanationAndID(empty_cst_module, "This is a test.\n", "1"), CodeExplanationAndID(empty_cst_module, "Explanation before code.\n\nExplanation after code.", "2"), ] - cleaned = cleanup_explanations(original_code, explanations) + cleaned = cleanup_explanations(original_module, explanations) assert cleaned == expected_explanations def test_postprocess_optimizations_basic() -> None: - original_code = "print('hello')" + original_module = cst.parse_module("print('hello')") optimizations = [CodeExplanationAndID(libcst.parse_module("print('hi')"), "Simplified print statement", "1")] expected = [CodeExplanationAndID(libcst.parse_module("print('hi')"), "Simplified print statement", "1")] - actual = optimizations_postprocessing_pipeline(original_code, optimizations) + actual = optimizations_postprocessing_pipeline(original_module, optimizations) assert actual[0].cst_module.deep_equals(expected[0].cst_module) def test_postprocess_deduplicates() -> None: - original_code = "print('hello')" + original_module = cst.parse_module("print('hello')") optimizations = [ CodeExplanationAndID(libcst.parse_module("print('hi')"), "Simplified print", "1"), CodeExplanationAndID(libcst.parse_module("print('hi')"), "Simplified print again", "2"), ] expected = [CodeExplanationAndID(libcst.parse_module("print('hi')"), "Simplified print again", "2")] - actual = deduplicate_optimizations(original_code, optimizations) + actual = deduplicate_optimizations(original_module, optimizations) assert actual[0].cst_module.deep_equals(expected[0].cst_module) @@ -92,6 +92,7 @@ class NuitkaPluginBase(getMetaClassBase("Plugin", require_slots=False)): # Virtual method, pylint: disable=no-self-use,unused-argument return True ''' + original_module = cst.parse_module(original_code) optimizations = [ CodeExplanationAndID( libcst.parse_module( @@ -119,7 +120,7 @@ class NuitkaPluginBase(getMetaClassBase("Plugin", require_slots=False)): "1", ) ] - actual = equality_check(original_code, optimizations) + actual = equality_check(original_module, optimizations) assert len(actual) == 0 @@ -134,6 +135,7 @@ def sorter(arr): arr[j + 1] = temp return arr """ + original_module = cst.parse_module(original_code) optimizations = [ CodeExplanationAndID( @@ -151,7 +153,7 @@ def sorter(arr): ) ] - actual = optimizations_postprocessing_pipeline(original_code, optimizations) + actual = optimizations_postprocessing_pipeline(original_module, optimizations) assert actual[0].cst_module.deep_equals(expected[0].cst_module) @@ -167,6 +169,7 @@ def sorter(arr): arr[j + 1] = temp return arr """ + original_module = cst.parse_module(original_code) optimizations = [ CodeExplanationAndID( @@ -190,7 +193,7 @@ def sorter(arr): ) ] - actual = optimizations_postprocessing_pipeline(original_code, optimizations) + actual = optimizations_postprocessing_pipeline(original_module, optimizations) assert actual[0].cst_module.deep_equals(expected[0].cst_module) @@ -301,7 +304,7 @@ Most changes in the code are associated with avoiding repetitive calls to certai expected_code_and_explanations = [ CodeExplanationAndID(empty_cst_module, exp, str(i)) for i, exp in enumerate(expected) ] - filtered = cleanup_explanations("", code_and_explanations) + filtered = cleanup_explanations(cst.parse_module(""), code_and_explanations) for filtered_elem, expected_elem in zip(filtered, expected_code_and_explanations, strict=False): assert filtered_elem.explanation == expected_elem.explanation @@ -311,6 +314,7 @@ def test_docstring_fix() -> None: """useful docstring""" pass ''' + original_module = cst.parse_module(original_code) optimized_code = libcst.parse_module( """def test_1(): @@ -323,7 +327,7 @@ def test_docstring_fix() -> None: pass ''' ) - actual = fix_missing_docstring(original_code, [CodeExplanationAndID(optimized_code, "Explanation", "1")]) + actual = fix_missing_docstring(original_module, [CodeExplanationAndID(optimized_code, "Explanation", "1")]) assert actual[0].cst_module.deep_equals(expected) @@ -335,6 +339,7 @@ def test_2(): """useful docstring""" pass ''' + original_module = cst.parse_module(original_code) optimized_code = libcst.parse_module( '''def test_1(): pass @@ -352,7 +357,7 @@ def test_2(): """useful docstring""" pass''' ) - actual = fix_missing_docstring(original_code, [CodeExplanationAndID(optimized_code, "Explanation", "2")]) + actual = fix_missing_docstring(original_module, [CodeExplanationAndID(optimized_code, "Explanation", "2")]) assert actual[0].cst_module.deep_equals(expected) original_code = '''class TestClass: @@ -360,6 +365,7 @@ def test_2(): """useful docstring""" pass ''' + original_module = cst.parse_module(original_code) optimized_code = libcst.parse_module( """class TestClass: def test_1(self): @@ -373,7 +379,7 @@ def test_2(): pass ''' ) - actual = fix_missing_docstring(original_code, [CodeExplanationAndID(optimized_code, "Explanation", "3")]) + actual = fix_missing_docstring(original_module, [CodeExplanationAndID(optimized_code, "Explanation", "3")]) assert actual[0].cst_module.deep_equals(expected) original_code = '''def test_1(): @@ -384,6 +390,7 @@ class TestClass: """useful docstring""" pass ''' + original_module = cst.parse_module(original_code) optimized_code = libcst.parse_module( """def test_1(): pass @@ -402,7 +409,7 @@ class TestClass: pass ''' ) - actual = fix_missing_docstring(original_code, [CodeExplanationAndID(optimized_code, "Explanation", "4")]) + actual = fix_missing_docstring(original_module, [CodeExplanationAndID(optimized_code, "Explanation", "4")]) assert actual[0].cst_module.deep_equals(expected) original_code = '''@deprecated("0.1.0", alternative="create_react_agent", removal="0.2.0") @@ -430,6 +437,7 @@ class ZeroShotAgent(Agent): """ pass ''' + original_module = cst.parse_module(original_code) optimized_code = libcst.parse_module( """@deprecated("0.1.0", alternative="create_react_agent", removal="0.2.0") class ZeroShotAgent(Agent): @@ -445,7 +453,7 @@ class ZeroShotAgent(Agent): pass """ ) - actual = fix_missing_docstring(original_code, [CodeExplanationAndID(optimized_code, "Explanation", "5")]) + actual = fix_missing_docstring(original_module, [CodeExplanationAndID(optimized_code, "Explanation", "5")]) assert actual[0].cst_module.deep_equals(libcst.parse_module(original_code)) original_code = '''def test_1(): @@ -456,6 +464,7 @@ def test_2(): def test_3(): """both have\n original""" pass''' + original_module = cst.parse_module(original_code) optimized_code = libcst.parse_module( '''def test_1(): pass @@ -477,7 +486,7 @@ def test_3(): """both have\n original""" pass''' ) - actual = fix_missing_docstring(original_code, [CodeExplanationAndID(optimized_code, "Explanation", "2")]) + actual = fix_missing_docstring(original_module, [CodeExplanationAndID(optimized_code, "Explanation", "2")]) assert actual[0].cst_module.deep_equals(expected) original_code = '''class test_1(): @@ -488,6 +497,7 @@ def test_3(): def test_3(self): """both have\n original""" pass''' + original_module = cst.parse_module(original_code) optimized_code = libcst.parse_module( '''class test_1(): pass @@ -509,7 +519,7 @@ def test_3(): """both have\n original""" pass''' ) - actual = fix_missing_docstring(original_code, [CodeExplanationAndID(optimized_code, "Explanation", "2")]) + actual = fix_missing_docstring(original_module, [CodeExplanationAndID(optimized_code, "Explanation", "2")]) assert actual[0].cst_module.deep_equals(expected) original_code = '''class test_1: @@ -520,6 +530,7 @@ def test_3(): def test_3(self): """both have\n original""" pass''' + original_module = cst.parse_module(original_code) optimized_code = libcst.parse_module( '''class test_1: """both have it\n multi line v2""" @@ -542,7 +553,7 @@ def test_3(): """both have\n original""" pass''' ) - actual = fix_missing_docstring(original_code, [CodeExplanationAndID(optimized_code, "Explanation", "2")]) + actual = fix_missing_docstring(original_module, [CodeExplanationAndID(optimized_code, "Explanation", "2")]) assert actual[0].cst_module.deep_equals(expected) @@ -555,6 +566,7 @@ import sys def foo(): return os.path.join(sys.path[0], 'bar') """ + original_module = cst.parse_module(original_code) optimizations = CodeExplanationAndID( libcst.parse_module( """ @@ -586,7 +598,7 @@ def foo(): "1", ) - actual = dedup_and_sort_imports(original_code, [optimizations]) + actual = dedup_and_sort_imports(original_module, [optimizations]) assert actual[0].cst_module.deep_equals(expected.cst_module) @@ -602,6 +614,7 @@ import os def foo(): return os.path.join(sys.path[0], 'bar') """ + original_module = cst.parse_module(original_code) optimizations = CodeExplanationAndID( libcst.parse_module( """ @@ -635,7 +648,7 @@ def foo(): "1", ) - actual = dedup_and_sort_imports(original_code, [optimizations]) + actual = dedup_and_sort_imports(original_module, [optimizations]) assert actual[0].cst_module.deep_equals(expected.cst_module) @@ -643,6 +656,7 @@ def foo(): def test_ellipsis_filter_removes_optimizations_with_ellipsis() -> None: original_code = """def foo(): return True""" + original_module = cst.parse_module(original_code) optimizations = [ CodeExplanationAndID( @@ -673,13 +687,14 @@ def test_ellipsis_filter_removes_optimizations_with_ellipsis() -> None: "2", ) ] - actual = filter_ellipsis_containing_code(original_code, optimizations) + actual = filter_ellipsis_containing_code(original_module, optimizations) assert len(actual) == 1 assert actual[0].cst_module.deep_equals(expected[0].cst_module) original_code2 = """def foo(): return ...""" - actual = filter_ellipsis_containing_code(original_code2, optimizations) + original_module2 = cst.parse_module(original_code2) + actual = filter_ellipsis_containing_code(original_module2, optimizations) assert len(actual) == 2 assert actual[0].cst_module.deep_equals(optimizations[0].cst_module) assert actual[1].cst_module.deep_equals(optimizations[1].cst_module) diff --git a/django/aiservice/tests/testgen_postprocessing/test_add_missing_imports.py b/django/aiservice/tests/testgen_postprocessing/test_add_missing_imports.py index 223a1a737..4ef516b36 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_add_missing_imports.py +++ b/django/aiservice/tests/testgen_postprocessing/test_add_missing_imports.py @@ -2,123 +2,16 @@ import libcst as cst -from testgen.postprocessing.add_missing_imports import ( - AnnotationNameCollector, - LocalDefinitionRemover, - UndefinedNameCollector, - add_future_annotations_import, - add_missing_imports_from_multi_context_source, - add_missing_imports_from_source, - file_path_to_module_path, - fix_forward_references_in_source_blocks, - get_imports_from_source_code, - get_local_definitions, - get_public_symbols_from_source_code, - get_referenced_names_from_source, - get_symbols_from_source_code, - get_symbols_with_modules_from_multi_context, - has_future_annotations, - has_undefined_annotation_names, -) +from aiservice.common.cst_utils import DefinitionRemover, file_path_to_module_path +from testgen.postprocessing.add_missing_imports import add_future_annotations_import, add_missing_imports + +# ============================================================================= +# Tests for add_missing_imports +# ============================================================================= -def test_get_public_symbols_from_source_code() -> None: - source = """ -class PreChunk: - pass - -class _PrivateClass: - pass - -def helper_func(): - pass - -def _private_func(): - pass - -CONSTANT = 42 -_PRIVATE_CONSTANT = 99 -""" - symbols = get_public_symbols_from_source_code(source) - assert "PreChunk" in symbols - assert "helper_func" in symbols - assert "CONSTANT" in symbols - assert "_PrivateClass" not in symbols - assert "_private_func" not in symbols - assert "_PRIVATE_CONSTANT" not in symbols - - -def test_get_public_symbols_with_all() -> None: - source = """ -__all__ = ["ExportedClass", "exported_func"] - -class ExportedClass: - pass - -class NotExportedClass: - pass - -def exported_func(): - pass - -def not_exported_func(): - pass -""" - symbols = get_public_symbols_from_source_code(source) - assert symbols == {"ExportedClass", "exported_func"} - - -def test_undefined_name_collector() -> None: - code = """ -from mymodule import ClassA - -def test_something(): - a = ClassA() - b = ClassB() # Not imported - c = ClassC() # Not imported -""" - import ast - - collector = UndefinedNameCollector() - tree = ast.parse(code) - collector.visit(tree) - undefined = collector.get_undefined_names() - - assert "ClassB" in undefined - assert "ClassC" in undefined - assert "ClassA" not in undefined - - -def test_undefined_name_collector_handles_local_definitions() -> None: - code = """ -from mymodule import Something - -class LocalClass: - pass - -def local_func(): - pass - -def test_something(): - obj = LocalClass() # Defined locally - func = local_func() # Defined locally - other = Something() # Imported - missing = UndefinedClass() # Not defined or imported -""" - import ast - - collector = UndefinedNameCollector() - tree = ast.parse(code) - collector.visit(tree) - undefined = collector.get_undefined_names() - - assert "UndefinedClass" in undefined - assert "LocalClass" not in undefined - assert "local_func" not in undefined - assert "Something" not in undefined - - -def test_add_missing_imports_from_source_basic() -> None: +def test_add_missing_imports_basic() -> None: + """Test basic import addition from source CST module.""" source_code = """ class PreChunk: pass @@ -138,13 +31,14 @@ def test_something(): opts = ChunkingOptions() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "mymodule") + result = add_missing_imports(module, cst.parse_module(source_code), "mymodule") assert "PreChunk" in result.code assert "ChunkingOptions" in result.code def test_add_missing_imports_preserves_existing() -> None: + """Test that existing imports are preserved.""" source_code = """ class ClassA: pass @@ -161,14 +55,14 @@ def test_something(): b = ClassB() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "mymodule") + result = add_missing_imports(module, cst.parse_module(source_code), "mymodule") - # Both should be in the result assert "ClassA" in result.code assert "ClassB" in result.code def test_add_missing_imports_no_changes_needed() -> None: + """Test when all imports are already present.""" source_code = """ class ClassA: pass @@ -181,14 +75,13 @@ def test_something(): a = ClassA() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "mymodule") + result = add_missing_imports(module, cst.parse_module(source_code), "mymodule") - # Should be unchanged (or only formatting changes) assert "ClassA" in result.code def test_add_missing_imports_handles_redefined_classes() -> None: - """Test the actual scenario: LLM redefines some classes locally but forgets others.""" + """Test removal of locally redefined classes and import addition.""" source_code = """ class PreChunk: def __init__(self, elements): @@ -203,7 +96,6 @@ class Element: self.text = text """ - # LLM redefined Element and ChunkingOptions locally, but forgot PreChunk test_code = """ from unstructured.chunking.base import _PreChunkAccumulator @@ -217,10 +109,10 @@ class ChunkingOptions: def test_will_fit(): opts = ChunkingOptions() acc = _PreChunkAccumulator(opts) - chunk = PreChunk([Element("test")]) # PreChunk not imported! + chunk = PreChunk([Element("test")]) """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "unstructured.chunking.base") + result = add_missing_imports(module, cst.parse_module(source_code), "unstructured.chunking.base") # Local class definitions should be removed assert "class Element" not in result.code @@ -233,158 +125,8 @@ def test_will_fit(): assert "ChunkingOptions" in result.code -def test_get_local_definitions() -> None: - """Test that get_local_definitions finds classes and non-test functions.""" - code = """ -class MyClass: - pass - -class AnotherClass: - pass - -def helper_func(): - pass - -def _private_func(): - pass - -def test_something(): - pass -""" - definitions = get_local_definitions(code) - assert "MyClass" in definitions - assert "AnotherClass" in definitions - assert "helper_func" in definitions - assert "_private_func" not in definitions # private functions excluded - assert "test_something" not in definitions # test functions excluded - - -def test_local_definition_remover() -> None: - """Test that LocalDefinitionRemover removes specified classes and functions.""" - code = """ -class Element: - pass - -class ChunkingOptions: - pass - -def helper(): - pass - -def test_something(): - pass -""" - module = cst.parse_module(code) - remover = LocalDefinitionRemover({"Element", "helper"}) - result = module.visit(remover) - - assert "class Element" not in result.code - assert "def helper" not in result.code - assert "class ChunkingOptions" in result.code # Not in removal set - assert "def test_something" in result.code # Test functions never removed - assert remover.removed_names == {"Element", "helper"} - - -def test_local_definition_remover_preserves_nested_functions() -> None: - """Test that LocalDefinitionRemover only removes top-level definitions, not nested ones. - - This is critical for test code that defines helper functions inside test functions, - such as thread workers or callbacks. - """ - code = """ -def worker(): - # Top-level worker function - should be removed if in names_to_remove - pass - -def test_concurrent_calls(): - results = [] - - def worker(idx): - # Nested worker function - should NOT be removed even if 'worker' is in names_to_remove - results.append(idx) - - threads = [worker(i) for i in range(10)] -""" - module = cst.parse_module(code) - # Even though 'worker' is in names_to_remove, only the top-level one should be removed - remover = LocalDefinitionRemover({"worker"}) - result = module.visit(remover) - - # Top-level worker should be removed - assert result.code.count("def worker") == 1 # Only the nested one remains - - # The nested worker inside test_concurrent_calls should still be there - assert "def worker(idx):" in result.code - assert "results.append(idx)" in result.code - - # Only the top-level worker should be in removed_names - assert remover.removed_names == {"worker"} - - -def test_local_definition_remover_preserves_nested_classes() -> None: - """Test that LocalDefinitionRemover preserves nested class definitions.""" - code = """ -class Helper: - # Top-level Helper class - should be removed if in names_to_remove - pass - -def test_something(): - class Helper: - # Nested Helper class - should NOT be removed - value = 42 - - h = Helper() -""" - module = cst.parse_module(code) - remover = LocalDefinitionRemover({"Helper"}) - result = module.visit(remover) - - # Top-level Helper should be removed - # The nested Helper inside test_something should still be there - assert result.code.count("class Helper") == 1 - assert "value = 42" in result.code - assert remover.removed_names == {"Helper"} - - -def test_get_symbols_from_source_code_with_private() -> None: - """Test that get_symbols_from_source_code can include private symbols.""" - source = """ -class PreChunk: - pass - -class _PreElementAccumulator: - pass - -def helper_func(): - pass - -def _private_func(): - pass - -CONSTANT = 42 -_PRIVATE_CONSTANT = 99 -""" - # Without include_private (default) - public_symbols = get_symbols_from_source_code(source) - assert "PreChunk" in public_symbols - assert "helper_func" in public_symbols - assert "CONSTANT" in public_symbols - assert "_PreElementAccumulator" not in public_symbols - assert "_private_func" not in public_symbols - assert "_PRIVATE_CONSTANT" not in public_symbols - - # With include_private=True - all_symbols = get_symbols_from_source_code(source, include_private=True) - assert "PreChunk" in all_symbols - assert "helper_func" in all_symbols - assert "CONSTANT" in all_symbols - assert "_PreElementAccumulator" in all_symbols - assert "_private_func" in all_symbols - assert "_PRIVATE_CONSTANT" in all_symbols - - def test_add_missing_imports_handles_private_symbols() -> None: - """Test that private symbols referenced in test code get imported.""" + """Test that private symbols get imported when referenced.""" source_code = """ class Pre: pass @@ -398,61 +140,22 @@ class _PreElementAccumulator: pass """ - # LLM generated test that references private _PreElementAccumulator test_code = """ from mymodule import Pre def test_element_accum(): p = Pre() - # This references _PreElementAccumulator which is private but exists in source _PreElementAccumulator.reset_counter() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "mymodule") + result = add_missing_imports(module, cst.parse_module(source_code), "mymodule") - # _PreElementAccumulator should now be imported assert "_PreElementAccumulator" in result.code assert "from mymodule import" in result.code -def test_get_imports_from_source_code() -> None: - """Test that get_imports_from_source_code extracts imports correctly.""" - source = """ -from typing import List -from unstructured.documents.elements import ElementMetadata -from some.module import ClassA, ClassB as AliasB -import os -import pathlib.Path as Path -""" - imports = get_imports_from_source_code(source) - - # from X import Y -> (module, original_name) - assert imports["List"] == ("typing", "List") - assert imports["ElementMetadata"] == ("unstructured.documents.elements", "ElementMetadata") - assert imports["ClassA"] == ("some.module", "ClassA") - # aliased imports: available_name -> (module, original_name) - assert imports["AliasB"] == ("some.module", "ClassB") - # import X - assert imports["os"] == ("os", "os") - # import X as Y - assert imports["Path"] == ("pathlib.Path", "pathlib.Path") - - -def test_get_imports_from_source_code_handles_star_import() -> None: - """Test that star imports are ignored.""" - source = """ -from typing import * -from foo import Bar -""" - imports = get_imports_from_source_code(source) - # Star import should be skipped - assert "Bar" in imports - assert imports["Bar"] == ("foo", "Bar") - - def test_add_missing_imports_handles_source_module_imports() -> None: - """Test that symbols imported by the source module get imported from their original source.""" - # Source module imports ElementMetadata from another module + """Test that symbols imported by source are imported from their original source.""" source_code = """ from typing import List from unstructured.documents.elements import ElementMetadata @@ -464,23 +167,20 @@ def create_unstructured_weaviate_class(class_name: str = "UnstructuredDocument") pass """ - # LLM generated test that references ElementMetadata but doesn't import it test_code = """ def test_large_scale(): annotations = {} - # Uses ElementMetadata which is imported by source, not defined in it ElementMetadata.__annotations__ = annotations """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "unstructured.staging.weaviate") + result = add_missing_imports(module, cst.parse_module(source_code), "unstructured.staging.weaviate") - # ElementMetadata should be imported from its original source assert "ElementMetadata" in result.code assert "from unstructured.documents.elements import ElementMetadata" in result.code def test_add_missing_imports_handles_both_defined_and_imported_symbols() -> None: - """Test handling of both symbols defined in source and symbols imported by source.""" + """Test handling of both local definitions and imported symbols.""" source_code = """ from external.module import ExternalClass @@ -491,55 +191,22 @@ def local_func(): pass """ - # LLM generated test that uses both local and external symbols test_code = """ def test_something(): local = LocalClass() external = ExternalClass() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "mymodule") + result = add_missing_imports(module, cst.parse_module(source_code), "mymodule") - # LocalClass should be imported from mymodule assert "LocalClass" in result.code - # ExternalClass should be imported from its original source assert "ExternalClass" in result.code assert "from external.module import ExternalClass" in result.code assert "from mymodule import" in result.code -def test_get_referenced_names_from_source() -> None: - """Test that get_referenced_names_from_source extracts referenced but undefined names.""" - source = """ -_chunker_registry: dict[str, _ChunkerSpec] = { - "basic": _ChunkerSpec(chunk_elements), - "by_title": _ChunkerSpec(chunk_by_title), -} - -def func(): - x = SomeClass() - return x -""" - names = get_referenced_names_from_source(source) - - # Should find referenced names that are not defined locally - assert "_ChunkerSpec" in names - assert "chunk_elements" in names - assert "chunk_by_title" in names - assert "SomeClass" in names - assert "dict" not in names - assert "str" not in names - assert "x" not in names - assert "_chunker_registry" not in names - - def test_add_missing_imports_handles_referenced_but_not_defined_symbols() -> None: - """Test fallback for symbols referenced in source but not defined/imported there. - - This handles cases where the source snippet is incomplete - a symbol is used - but its definition is not visible in the snippet. - """ - # Source uses _ChunkerSpec but doesn't define or import it (incomplete snippet) + """Test fallback for symbols referenced but not defined in incomplete source snippets.""" source_code = """ _chunker_registry: dict[str, _ChunkerSpec] = { "basic": _ChunkerSpec(chunk_elements), @@ -551,31 +218,19 @@ def chunk(elements, strategy): return spec.func(elements) """ - # LLM generated test that also uses _ChunkerSpec test_code = """ def test_chunker(): registry = {"basic": _ChunkerSpec(lambda x: x)} """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "unstructured.chunking.dispatch") + result = add_missing_imports(module, cst.parse_module(source_code), "unstructured.chunking.dispatch") - # _ChunkerSpec should be imported from the source module as a fallback assert "_ChunkerSpec" in result.code assert "from unstructured.chunking.dispatch import" in result.code def test_add_missing_imports_handles_local_redefinition_of_imported_symbol() -> None: - """Test that locally redefined symbols are imported from their original source, not from module_path. - - This is a critical case: when combined source code contains multiple files, and file A imports - a class from file B, if the LLM redefines that class locally in the test, it should be imported - from file B (its original source), not from file A (the module being tested). - - Real-world example: textlocindex.py imports EmbeddingIndex from fuzzyindex.py. If the LLM - redefines EmbeddingIndex locally in tests for textlocindex, the import should come from - fuzzyindex, not textlocindex. - """ - # Combined source code from multiple files (fuzzyindex.py + textlocindex.py) + """Test that redefined symbols are imported from their original source.""" source_code = """ # file: typeagent/knowpro/fuzzyindex.py from typeagent.aitools.vectorbase import VectorBase @@ -598,7 +253,6 @@ class TextToTextLocationIndex: return await self._embedding_index.size() """ - # LLM generated test that redefines EmbeddingIndex locally test_code = """ import pytest from typeagent.knowpro.textlocindex import TextToTextLocationIndex @@ -613,84 +267,31 @@ class EmbeddingIndex: @pytest.mark.asyncio async def test_size(): - # Uses the local EmbeddingIndex emb_index = EmbeddingIndex(settings=None, embeddings=[[1, 2, 3]]) """ module = cst.parse_module(test_code) - result = add_missing_imports_from_source(module, source_code, "typeagent.knowpro.textlocindex") + result = add_missing_imports(module, cst.parse_module(source_code), "typeagent.knowpro.textlocindex") - # Local EmbeddingIndex class should be removed assert "class EmbeddingIndex" not in result.code assert "Locally redefined by LLM" not in result.code - - # EmbeddingIndex should be imported from its ORIGINAL source (fuzzyindex), NOT from textlocindex assert "from typeagent.knowpro.fuzzyindex import EmbeddingIndex" in result.code - - # The original TextToTextLocationIndex import should still be there assert "from typeagent.knowpro.textlocindex import TextToTextLocationIndex" in result.code -# ============================================================================ -# Multi-context tests -# ============================================================================ - - -def test_file_path_to_module_path() -> None: - """Test file path to module path conversion.""" - assert file_path_to_module_path("a/b/c.py") == "a.b.c" - assert file_path_to_module_path("a/b/c") == "a.b.c" - assert file_path_to_module_path("single.py") == "single" - assert file_path_to_module_path("a\\b\\c.py") == "a.b.c" # Windows paths - assert ( - file_path_to_module_path("skyvern/forge/sdk/core/skyvern_context.py") - == "skyvern.forge.sdk.core.skyvern_context" - ) - - -def test_get_symbols_with_modules_from_multi_context() -> None: - """Test extracting symbols with their defining modules from multi-context source.""" - source_code_blocks = { - "skyvern/forge/sdk/core/skyvern_context.py": """ -class SkyvernContext: - def __init__(self): - pass -""", - "skyvern/core/script_generations/skyvern_page.py": """ -from skyvern.forge.sdk.core.skyvern_context import SkyvernContext - -class SkyvernPage: - def __init__(self): - self.context = SkyvernContext() -""", - } - - symbols_to_module, source_imports, _referenced_names = get_symbols_with_modules_from_multi_context( - source_code_blocks - ) - - # SkyvernContext is defined in skyvern_context.py - assert "SkyvernContext" in symbols_to_module - assert symbols_to_module["SkyvernContext"] == "skyvern.forge.sdk.core.skyvern_context" - - # SkyvernPage is defined in skyvern_page.py - assert "SkyvernPage" in symbols_to_module - assert symbols_to_module["SkyvernPage"] == "skyvern.core.script_generations.skyvern_page" - - # SkyvernContext is imported by skyvern_page.py from its original source - # source_imports maps available_name -> (module, original_name) - assert "SkyvernContext" in source_imports - assert source_imports["SkyvernContext"] == ("skyvern.forge.sdk.core.skyvern_context", "SkyvernContext") +# ============================================================================= +# Tests for add_missing_imports (multi-context dict[str, cst.Module]) +# ============================================================================= def test_add_missing_imports_multi_context_correct_module() -> None: - """Test the Skyvern scenario: symbol imported from correct module, not from module_path.""" + """Test that symbols are imported from their defining module in multi-context.""" source_code_blocks = { - "skyvern/forge/sdk/core/skyvern_context.py": """ + "skyvern/forge/sdk/core/skyvern_context.py": cst.parse_module(""" class SkyvernContext: def __init__(self): self.data = {} -""", - "skyvern/core/script_generations/skyvern_page.py": """ +"""), + "skyvern/core/script_generations/skyvern_page.py": cst.parse_module(""" from skyvern.forge.sdk.core.skyvern_context import SkyvernContext class SkyvernPage: @@ -699,47 +300,41 @@ class SkyvernPage: def get_context(self): return self.context -""", +"""), } - # LLM test code uses SkyvernContext but doesn't import it test_code = """ import pytest from skyvern.core.script_generations.skyvern_page import SkyvernPage def test_get_context(): page = SkyvernPage() - ctx = SkyvernContext() # Referenced but not imported + ctx = SkyvernContext() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_multi_context_source( - module, source_code_blocks, "skyvern.core.script_generations.skyvern_page" - ) + result = add_missing_imports(module, source_code_blocks, "skyvern.core.script_generations.skyvern_page") - # SkyvernContext should be imported from its defining module, NOT from module_path assert "from skyvern.forge.sdk.core.skyvern_context import SkyvernContext" in result.code - # The original SkyvernPage import should still be there assert "from skyvern.core.script_generations.skyvern_page import SkyvernPage" in result.code def test_add_missing_imports_multi_context_local_redefinition() -> None: - """Test that LLM-redefined classes are removed and imported from correct module.""" + """Test that redefined classes are removed and imported correctly in multi-context.""" source_code_blocks = { - "module_a/core.py": """ + "module_a/core.py": cst.parse_module(""" class CoreClass: def __init__(self, value): self.value = value -""", - "module_b/wrapper.py": """ +"""), + "module_b/wrapper.py": cst.parse_module(""" from module_a.core import CoreClass class Wrapper: def __init__(self): self.core = CoreClass(42) -""", +"""), } - # LLM test code redefines CoreClass locally test_code = """ import pytest from module_b.wrapper import Wrapper @@ -754,67 +349,60 @@ def test_wrapper(): c = CoreClass(10) """ module = cst.parse_module(test_code) - result = add_missing_imports_from_multi_context_source(module, source_code_blocks, "module_b.wrapper") + result = add_missing_imports(module, source_code_blocks, "module_b.wrapper") - # Local CoreClass definition should be removed assert "class CoreClass" not in result.code assert "Redefined by LLM" not in result.code - - # CoreClass should be imported from module_a.core (where it's defined) assert "from module_a.core import CoreClass" in result.code def test_add_missing_imports_multi_context_fallback_symbols() -> None: - """Test fallback for symbols referenced but not defined in incomplete snippets.""" + """Test fallback for referenced but not defined symbols in multi-context.""" source_code_blocks = { - "mymodule/handlers.py": """ -# _SpecialHandler is referenced but not defined here (incomplete snippet) + "mymodule/handlers.py": cst.parse_module(""" _handlers: dict[str, _SpecialHandler] = {} def get_handler(name): return _handlers.get(name) -""" +""") } - # LLM test code uses _SpecialHandler test_code = """ def test_handlers(): handler = _SpecialHandler() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_multi_context_source(module, source_code_blocks, "mymodule.handlers") + result = add_missing_imports(module, source_code_blocks, "mymodule.handlers") - # _SpecialHandler should be imported as a fallback from the default module assert "_SpecialHandler" in result.code assert "from mymodule.handlers import" in result.code def test_add_missing_imports_multi_context_empty_blocks() -> None: """Test handling of empty source code blocks.""" - source_code_blocks: dict[str, str] = {} + source_code_blocks: dict[str, cst.Module] = {} test_code = """ def test_something(): x = SomeClass() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_multi_context_source(module, source_code_blocks, "default.module") + result = add_missing_imports(module, source_code_blocks, "default.module") - # Should return unchanged module when no source blocks assert result.code == test_code def test_add_missing_imports_multi_context_multiple_files_same_symbol() -> None: - """Test that first definition wins when same symbol is defined in multiple files.""" + """Test that first definition wins when symbol is defined in multiple files.""" source_code_blocks = { - "package/v1/model.py": """ + "package/v1/model.py": cst.parse_module(""" class Model: version = 1 -""", - "package/v2/model.py": """ +"""), + "package/v2/model.py": cst.parse_module(""" class Model: version = 2 -""", +"""), } test_code = """ @@ -822,175 +410,144 @@ def test_model(): m = Model() """ module = cst.parse_module(test_code) - result = add_missing_imports_from_multi_context_source(module, source_code_blocks, "default.module") + result = add_missing_imports(module, source_code_blocks, "default.module") - # First definition (v1) should win assert "from package.v1.model import Model" in result.code def test_add_missing_imports_multi_context_aliased_imports() -> None: - """Test that aliased imports are handled correctly. - - When source code has `from X import foo as bar`, and test code uses `bar`, - we should generate `from X import foo as bar`, not `from X import bar`. - """ + """Test that aliased imports are handled correctly.""" source_code_blocks = { - "skyvern/forge/sdk/core/skyvern_context.py": """ + "skyvern/forge/sdk/core/skyvern_context.py": cst.parse_module(""" class SkyvernContext: current = None def __init__(self): self.data = {} -""", - "skyvern/core/script_generations/skyvern_page.py": """ +"""), + "skyvern/core/script_generations/skyvern_page.py": cst.parse_module(""" from skyvern.forge.sdk.core import skyvern_context as skyvern_context_mod class SkyvernPage: def type(self, text: str): ctx = skyvern_context_mod.current() return ctx -""", +"""), } - # LLM test code uses skyvern_context_mod (the alias) test_code = """ import pytest from skyvern.core.script_generations.skyvern_page import SkyvernPage def test_type(): - # Uses the aliased name from source skyvern_context_mod.current = lambda: "mock" page = SkyvernPage() result = page.type("hello") """ module = cst.parse_module(test_code) - result = add_missing_imports_from_multi_context_source( - module, source_code_blocks, "skyvern.core.script_generations.skyvern_page" + result = add_missing_imports(module, source_code_blocks, "skyvern.core.script_generations.skyvern_page") + + assert "from skyvern.forge.sdk.core import skyvern_context as skyvern_context_mod" in result.code + + +def test_definition_remover_preserves_nested_functions() -> None: + """Test that DefinitionRemover only removes top-level definitions, not nested ones. + + This is critical for test code that defines helper functions inside test functions, + such as thread workers or callbacks. + """ + code = """ +def worker(): + # Top-level worker function - should be removed if in names_to_remove + pass + +def test_concurrent_calls(): + results = [] + + def worker(idx): + # Nested worker function - should NOT be removed even if 'worker' is in names_to_remove + results.append(idx) + + threads = [worker(i) for i in range(10)] +""" + module = cst.parse_module(code) + # Even though 'worker' is in names_to_remove, only the top-level one should be removed + remover = DefinitionRemover({"worker"}) + result = module.visit(remover) + + # Top-level worker should be removed + assert result.code.count("def worker") == 1 # Only the nested one remains + + # The nested worker inside test_concurrent_calls should still be there + assert "def worker(idx):" in result.code + assert "results.append(idx)" in result.code + + # Only the top-level worker should be in removed_names + assert remover.removed_names == {"worker"} + + +def test_definition_remover_preserves_nested_classes() -> None: + """Test that DefinitionRemover preserves nested class definitions.""" + code = """ +class Helper: + # Top-level Helper class - should be removed if in names_to_remove + pass + +def test_something(): + class Helper: + # Nested Helper class - should NOT be removed + value = 42 + + h = Helper() +""" + module = cst.parse_module(code) + remover = DefinitionRemover({"Helper"}) + result = module.visit(remover) + + # Top-level Helper should be removed + # The nested Helper inside test_something should still be there + assert result.code.count("class Helper") == 1 + assert "value = 42" in result.code + assert remover.removed_names == {"Helper"} + + +# ============================================================================= +# Tests for file_path_to_module_path +# ============================================================================= + + +def test_file_path_to_module_path() -> None: + """Test file path to module path conversion.""" + assert file_path_to_module_path("a/b/c.py") == "a.b.c" + assert file_path_to_module_path("a/b/c") == "a.b.c" + assert file_path_to_module_path("single.py") == "single" + assert file_path_to_module_path("a\\b\\c.py") == "a.b.c" + assert ( + file_path_to_module_path("skyvern/forge/sdk/core/skyvern_context.py") + == "skyvern.forge.sdk.core.skyvern_context" ) - # Should generate import with alias: from X import skyvern_context as skyvern_context_mod - assert "from skyvern.forge.sdk.core import skyvern_context as skyvern_context_mod" in result.code - # Should NOT generate the incorrect import without alias - assert "import skyvern_context_mod" not in result.code or "as skyvern_context_mod" in result.code - # ============================================================================= -# Tests for forward reference detection and fixing (from __future__ import annotations) +# Tests for add_future_annotations_import # ============================================================================= -def test_has_future_annotations_present() -> None: - """Test detection of existing future annotations import.""" - code_with_import = """ -from __future__ import annotations - -def foo() -> SomeType: - pass -""" - assert has_future_annotations(code_with_import) is True - - -def test_has_future_annotations_absent() -> None: - """Test detection when future annotations import is missing.""" - code_without_import = """ -def foo() -> SomeType: - pass -""" - assert has_future_annotations(code_without_import) is False - - -def test_has_future_annotations_other_future_import() -> None: - """Test that other __future__ imports don't trigger false positive.""" - code_with_other_future = """ -from __future__ import division - -def foo() -> SomeType: - pass -""" - assert has_future_annotations(code_with_other_future) is False - - -def test_annotation_name_collector_basic() -> None: - """Test collecting names from basic type annotations.""" - code = """ -from contextvars import ContextVar - -_context: ContextVar[SkyvernContext | None] = ContextVar("context") - -def current() -> SkyvernContext | None: - return _context.get() -""" - import ast - - tree = ast.parse(code) - collector = AnnotationNameCollector() - collector.visit(tree) - - # SkyvernContext is used in annotations but not defined/imported - undefined = collector.get_undefined_annotation_names() - assert "SkyvernContext" in undefined - # ContextVar is imported, so should not be in undefined - assert "ContextVar" not in undefined - - -def test_annotation_name_collector_with_class() -> None: - """Test that class definitions are tracked.""" - code = """ -class MyClass: - pass - -def foo() -> MyClass: - pass -""" - import ast - - tree = ast.parse(code) - collector = AnnotationNameCollector() - collector.visit(tree) - - # MyClass is defined, so should not be in undefined - undefined = collector.get_undefined_annotation_names() - assert "MyClass" not in undefined - - -def test_has_undefined_annotation_names_true() -> None: - """Test detection of undefined names in annotations.""" - code = """ -from contextvars import ContextVar - -_context: ContextVar[SkyvernContext | None] = ContextVar("context") -""" - assert has_undefined_annotation_names(code) is True - - -def test_has_undefined_annotation_names_false() -> None: - """Test when all annotation names are defined/imported.""" - code = """ -from contextvars import ContextVar -from typing import Optional - -class MyContext: - pass - -_context: ContextVar[Optional[MyContext]] = ContextVar("context") -""" - assert has_undefined_annotation_names(code) is False - - def test_add_future_annotations_import_adds_when_needed() -> None: """Test that import is added when undefined annotation names exist.""" code = """from contextvars import ContextVar _context: ContextVar[SkyvernContext | None] = ContextVar("context") """ - result = add_future_annotations_import(code) + module = cst.parse_module(code) + result = add_future_annotations_import(module) expected = """from __future__ import annotations from contextvars import ContextVar _context: ContextVar[SkyvernContext | None] = ContextVar("context") """ - assert result == expected + assert result.code == expected def test_add_future_annotations_import_skips_when_present() -> None: @@ -1000,10 +557,12 @@ from contextvars import ContextVar _context: ContextVar[SkyvernContext | None] = ContextVar("context") """ - result = add_future_annotations_import(code) + module = cst.parse_module(code) + result = add_future_annotations_import(module) - # Code should be unchanged since import already exists - assert result == code + # Should return the same module object (identity check) + assert result is module + assert result.code == code def test_add_future_annotations_import_skips_when_not_needed() -> None: @@ -1015,44 +574,16 @@ class MyContext: _context: ContextVar[MyContext] = ContextVar("context") """ - result = add_future_annotations_import(code) + module = cst.parse_module(code) + result = add_future_annotations_import(module) - # Code should be unchanged since all names are defined - assert result == code - - -def test_fix_forward_references_in_source_blocks() -> None: - """Test fixing forward references in multiple source blocks.""" - source_blocks = { - "module_a.py": """from contextvars import ContextVar - -_context: ContextVar[UndefinedType] = ContextVar("context") -""", - "module_b.py": """class DefinedClass: - pass - -def foo() -> DefinedClass: - pass -""", - } - - result = fix_forward_references_in_source_blocks(source_blocks) - - # module_a.py should have the import added - expected_a = """from __future__ import annotations -from contextvars import ContextVar - -_context: ContextVar[UndefinedType] = ContextVar("context") -""" - assert result["module_a.py"] == expected_a - - # module_b.py should be unchanged since all names are defined - assert result["module_b.py"] == source_blocks["module_b.py"] + # Should return the same module object (identity check) + assert result is module + assert result.code == code def test_add_future_annotations_import_skyvern_context_case() -> None: """Test the specific SkyvernContext case that was causing NameError.""" - # This is the exact pattern that was failing code = """from contextvars import ContextVar _context: ContextVar[SkyvernContext | None] = ContextVar( @@ -1064,7 +595,8 @@ _context: ContextVar[SkyvernContext | None] = ContextVar( def current() -> SkyvernContext | None: return _context.get() """ - result = add_future_annotations_import(code) + module = cst.parse_module(code) + result = add_future_annotations_import(module) expected = """from __future__ import annotations from contextvars import ContextVar @@ -1078,8 +610,6 @@ _context: ContextVar[SkyvernContext | None] = ContextVar( def current() -> SkyvernContext | None: return _context.get() """ - assert result == expected - # The code should still be parseable - import ast - - ast.parse(result) # Should not raise + assert result.code == expected + # Verify the result is valid Python + cst.parse_module(result.code) diff --git a/django/aiservice/uv.lock b/django/aiservice/uv.lock index 7b2680b4e..0223053f2 100644 --- a/django/aiservice/uv.lock +++ b/django/aiservice/uv.lock @@ -363,76 +363,76 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" +version = "7.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [[package]] @@ -940,101 +940,101 @@ wheels = [ [[package]] name = "multidict" -version = "6.7.0" +version = "6.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] @@ -1106,11 +1106,11 @@ aiohttp = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1124,11 +1124,11 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -1171,26 +1171,26 @@ wheels = [ [[package]] name = "prek" -version = "0.2.29" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/95/d89e32fc02bbb5d20a562062165bfe309d382798dd2e4e76edcfbcd0434a/prek-0.2.29.tar.gz", hash = "sha256:9788d0503a6e13ed84f864beaf12e87eee6140d799e6a379c77c06c801656e75", size = 288357, upload-time = "2026-01-16T11:39:30.905Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/1e/6c23d3470145be1d6ff29d93f2a521864788827d22e509e2b978eb5bb4cb/prek-0.3.0.tar.gz", hash = "sha256:e70f16bbaf2803e490b866cfa997ea5cc46e7ada55d61f0cdd84bc90b8d5ca7f", size = 316063, upload-time = "2026-01-22T04:00:01.648Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/0f/5bcc388fb779c30dcf3b3c512b07520b39c27c31ce9616ea5fc0c34b76aa/prek-0.2.29-py3-none-linux_armv6l.whl", hash = "sha256:ec0c7b67f3fdbfab447ff3cb37284bc5ec26816f19641393a522a107be6a428a", size = 5227889, upload-time = "2026-01-16T11:39:35.244Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d0/c10e88e39dfb914981291c4e6929ddb1de6033e8df5b7f7949bf3864eff4/prek-0.2.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:642ddee15c18d91f79095fd9a57ede8f522997a2ac131dadbe7eb8a770909f62", size = 5645793, upload-time = "2026-01-16T11:39:26.39Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9c/d0783455cd28905d63326e33ec91527d2df5d65c66c2f10092ee56fac49a/prek-0.2.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0a1e9673d939811d7cf692df4fa313f6d821ca3512e601c0b7ec037f46877ccf", size = 5419376, upload-time = "2026-01-16T11:39:22.087Z" }, - { url = "https://files.pythonhosted.org/packages/1e/87/ee193357f149aec65fb597ff85f67465ea8c36ebf450b996186603b8b78c/prek-0.2.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8e47c5beda1f4916f3f8607bb232de93dba8bf61b76aadff3e842a95a26046c0", size = 5497984, upload-time = "2026-01-16T11:39:38.173Z" }, - { url = "https://files.pythonhosted.org/packages/1b/68/2a3dd25749387d925632811abab62b97d4894b58652c14eb6c5a70a4b3ff/prek-0.2.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a60c29bec6c75702e24b85b55819a3dce50c19980995429e79b4c2fc78d38eb", size = 5176032, upload-time = "2026-01-16T11:39:42.436Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cd/a827b85677971ccc37842de5e5dd9452721ad73cf4e6e2d95c2be4326b00/prek-0.2.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6fd36d2f507d1a5e466cd1bd2cdafbffab244e2667482aba5a25476d077e435", size = 6188687, upload-time = "2026-01-16T11:39:33.995Z" }, - { url = "https://files.pythonhosted.org/packages/db/b1/6e6d79bb77523b0b9331524f9edb454f4c60c70a40543074337946e0c1ba/prek-0.2.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a874c875f41735648e07da88a9ced3ef16b08b5626ad386d8f21c071d39b0a0", size = 5805684, upload-time = "2026-01-16T11:39:39.876Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d7/891129cde04c88e58b1443f9f1aff620d7d4e8ccb22cbd728a209e59895a/prek-0.2.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4724a9e6e15b2791d2bcc312d6702bf2548e7864c8e956451310c1ec81afb47d", size = 5849611, upload-time = "2026-01-16T11:39:32.368Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d8/28fd18a063e9c980b3a496e27d0dd0823849549912d797d797f83bddd30f/prek-0.2.29-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a71ede67340208759a52ca0c144476e4e2220f60a08cc55035d0b50d80fee79c", size = 5547601, upload-time = "2026-01-16T11:39:29.344Z" }, - { url = "https://files.pythonhosted.org/packages/51/2a/683b32b3f0bce0fd8a91b3bb6e9a5a89709d6132e2cbbe48113a05da2384/prek-0.2.29-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8cb4cf35ac26bf7a28f3d8ccda9c4a95da0904aa636ca38c3ae6ae94d0215bea", size = 5552655, upload-time = "2026-01-16T11:39:19.362Z" }, - { url = "https://files.pythonhosted.org/packages/82/60/c8b90dc109a9a0c0127550ece2c02a8e5c61f7e5e2889cc7d271fedf3dc7/prek-0.2.29-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:c11e680fa6c3b3e0e33533729b7bf11a20313c1a860dafd415bc371b7ae4bcdd", size = 5143709, upload-time = "2026-01-16T11:39:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5f/00d81f9ecd1abf0085557e553f7c41eae01714f4c02859ba36ebcf3f7c03/prek-0.2.29-py3-none-musllinux_1_1_i686.whl", hash = "sha256:12cd792070eb47b01b9bd8a2632a74425e8dae016a6396b83683728e1a9ba0aa", size = 5817372, upload-time = "2026-01-16T11:39:25.091Z" }, - { url = "https://files.pythonhosted.org/packages/de/e5/6434356f1e1162b4c4130c18ea14657e264947ae76d0b9794012d927a032/prek-0.2.29-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e6643bb734a4d7f49555eaceed71e4afde022c33e16134acf45dbbd93dbde690", size = 5958617, upload-time = "2026-01-16T11:39:41.109Z" }, - { url = "https://files.pythonhosted.org/packages/54/3c/443fdb087d045ae27ed053ef65f4d40bb624c9ac65299ae90f680b36dbd1/prek-0.2.29-py3-none-win32.whl", hash = "sha256:4f5c7dc6452c0342adf07711561a2912d20050cf378f4df6a6bc825b647f9e4b", size = 5063876, upload-time = "2026-01-16T11:39:36.58Z" }, - { url = "https://files.pythonhosted.org/packages/3b/bb/d6698aaab0f04d6743435eeecdf026bda2ddfc96845a3c6f9a044d5d5005/prek-0.2.29-py3-none-win_amd64.whl", hash = "sha256:4297090a24685fc0998699a89b3ab7eb6447ff8d4eca0e836a697b51657930a8", size = 5783120, upload-time = "2026-01-16T11:39:23.359Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d4/60a5356219640df96343039e58723a63ca67915f933bffa1c4779a596523/prek-0.2.29-py3-none-win_arm64.whl", hash = "sha256:95844eeae5bdeb4e3ba91f4afb46115853eaf0d7a6654238320ecbdfb7f33e67", size = 5491654, upload-time = "2026-01-16T11:39:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/84/49/469219c19bb00db678806f79fc084ac1ce9952004a183a798db26f6df22b/prek-0.3.0-py3-none-linux_armv6l.whl", hash = "sha256:7e5d40b22deff23e36f7ad91e24b8e62edf32f30f6dad420459f7ec7188233c3", size = 4317493, upload-time = "2026-01-22T03:59:51.769Z" }, + { url = "https://files.pythonhosted.org/packages/87/9f/f7afc49cc0fd92d1ba492929dc1573cb7004d09b61341aa6ee32a5288657/prek-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6712b58cbb5a7db0aaef180c489ce9f3462e0293d54e54baeedd75fc0d9d8c28", size = 4323961, upload-time = "2026-01-22T03:59:56.92Z" }, + { url = "https://files.pythonhosted.org/packages/42/94/ba36dc29e71d476bf71c3bac2b0c89cfcfc4b8973a0a6b20728f429f4560/prek-0.3.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f2c446fd9012a98c5690b4badf3f7dfb8d424cf0c6798a2d08ee56511f0a670", size = 3970121, upload-time = "2026-01-22T03:59:55.722Z" }, + { url = "https://files.pythonhosted.org/packages/b5/93/6131dd9f6cde3d72815b978b766de21b2ac9cc15fc38f5c22267cc7e574d/prek-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:10f3da7cda2397f7d2f3ff7f2be0d7486c15d4941f7568095b7168e57a9c88c5", size = 4307430, upload-time = "2026-01-22T03:59:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/7c55a765d96028d38dc984e66a096a969d80e56f66a47801acc86dede856/prek-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f747bb4a4322fea35d548cd2c1bd24477f56ed009f3d62a2b97ecbfc88096ac", size = 4238032, upload-time = "2026-01-22T04:00:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a7/59d9bf902b749c8a0cef9e8ac073cc5c886634cd09404c00af4a76470b3b/prek-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40bd61f11d8caabc0e2a5d4c326639d6ff558b580ef4388aabec293ddb5afd35", size = 4493295, upload-time = "2026-01-22T03:59:45.964Z" }, + { url = "https://files.pythonhosted.org/packages/08/dc/902b2e4ddff59ad001ddc2cda3b47e457ab1ee811698a4002b3e4f84faf1/prek-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d096b5e273d17a1300b20a7101a9e5a624a8104825eb59659776177f7fccea1", size = 5033370, upload-time = "2026-01-22T03:59:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/15/cd/277a3d2768b80bb1ff3c2ea8378687bb4c527d88a8b543bf6f364f8a0dc9/prek-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df39face5f1298851fbae495267ddf60f1694ea594ed5c6cdb88bdd6de14f6a4", size = 4549792, upload-time = "2026-01-22T03:59:41.518Z" }, + { url = "https://files.pythonhosted.org/packages/26/21/53aeabd3822ef7fa350aac66d099d4d97b05e8383a2df35499229389a642/prek-0.3.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9462f80a576d661490aa058d4493a991a34c7532dea76b7b004a17c8bc6b80f2", size = 4323158, upload-time = "2026-01-22T03:59:54.284Z" }, + { url = "https://files.pythonhosted.org/packages/27/c2/3a7392b0e7fd07e339d89701b49b12a89d85256a57279877195028215957/prek-0.3.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:33d3fa40eecf996ed14bab2d006c39d21ae344677d62599963efd9b27936558e", size = 4344632, upload-time = "2026-01-22T04:00:03.71Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/8254ac981d75d0ce2826bcac74fed901540d629cb2d9f4d73ce62f8ce843/prek-0.3.0-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:d8c6abfd53a23718afdf4e6107418db1d74c5d904e9b7ec7900e285f8da90723", size = 4216608, upload-time = "2026-01-22T03:59:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/20/f5/854d57d89376fac577ee647a1dba1b87e27b2baeca7edc3d40295adeb7c8/prek-0.3.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:eb4c80c3e7c0e16bf307947809112bfef3715a1b83c2b03f5937707934635617", size = 4371174, upload-time = "2026-01-22T03:59:53.088Z" }, + { url = "https://files.pythonhosted.org/packages/03/38/8927619411da8d3f189415c452ec7a463f09dea69e272888723f37b4b18f/prek-0.3.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:602bcce070c50900167acd89dcdf95d27894412f8a7b549c8eb66de612a99653", size = 4659113, upload-time = "2026-01-22T03:59:43.166Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4d/16baeef633b8b230dde878b858c0e955149c860feef518b5eb5aac640eec/prek-0.3.0-py3-none-win32.whl", hash = "sha256:a69229365ce33c68c05db7ae73ad1ef8bc7f0914ab3bc484ab7781256bcdfb7a", size = 3937103, upload-time = "2026-01-22T03:59:48.719Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f2/c7395b4afd1bba32cad2b24c30fd7781e94c1e41137348cd150bbef001d6/prek-0.3.0-py3-none-win_amd64.whl", hash = "sha256:a0379afd8d31bd5da6ee8977820fdb3c30601bed836b39761e6f605451dbccaa", size = 4290763, upload-time = "2026-01-22T03:59:59.938Z" }, + { url = "https://files.pythonhosted.org/packages/df/83/97ed76ab5470025992cd50cb1ebdeb21fcf6c25459f9ffc49ac7bf040cf4/prek-0.3.0-py3-none-win_arm64.whl", hash = "sha256:82e2c64f75dc1ea6f2023f4322500eb8da5d0557baf06c88677bddf163e1542a", size = 4041580, upload-time = "2026-01-22T03:59:50.082Z" }, ] [[package]] @@ -1592,41 +1592,41 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.13" +version = "0.14.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] name = "sentry-sdk" -version = "2.49.0" +version = "2.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/94/23ac26616a883f492428d9ee9ad6eee391612125326b784dbfc30e1e7bab/sentry_sdk-2.49.0.tar.gz", hash = "sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30", size = 387228, upload-time = "2026-01-08T09:56:25.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/8a/3c4f53d32c21012e9870913544e56bfa9e931aede080779a0f177513f534/sentry_sdk-2.50.0.tar.gz", hash = "sha256:873437a989ee1b8b25579847bae8384515bf18cfed231b06c591b735c1781fe3", size = 401233, upload-time = "2026-01-20T12:53:16.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5b/cbc2bb9569f03c8e15d928357e7e6179e5cfab45544a3bbac8aec4caf9be/sentry_sdk-2.50.0-py2.py3-none-any.whl", hash = "sha256:0ef0ed7168657ceb5a0be081f4102d92042a125462d1d1a29277992e344e749e", size = 424961, upload-time = "2026-01-20T12:53:14.826Z" }, ] [package.optional-dependencies] @@ -1728,27 +1728,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.12" +version = "0.0.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/78/ba1a4ad403c748fbba8be63b7e774a90e80b67192f6443d624c64fe4aaab/ty-0.0.12.tar.gz", hash = "sha256:cd01810e106c3b652a01b8f784dd21741de9fdc47bd595d02c122a7d5cefeee7", size = 4981303, upload-time = "2026-01-14T22:30:48.537Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/57/22c3d6bf95c2229120c49ffc2f0da8d9e8823755a1c3194da56e51f1cc31/ty-0.0.14.tar.gz", hash = "sha256:a691010565f59dd7f15cf324cdcd1d9065e010c77a04f887e1ea070ba34a7de2", size = 5036573, upload-time = "2026-01-27T00:57:31.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/8f/c21314d074dda5fb13d3300fa6733fd0d8ff23ea83a721818740665b6314/ty-0.0.12-py3-none-linux_armv6l.whl", hash = "sha256:eb9da1e2c68bd754e090eab39ed65edf95168d36cbeb43ff2bd9f86b4edd56d1", size = 9614164, upload-time = "2026-01-14T22:30:44.016Z" }, - { url = "https://files.pythonhosted.org/packages/09/28/f8a4d944d13519d70c486e8f96d6fa95647ac2aa94432e97d5cfec1f42f6/ty-0.0.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c181f42aa19b0ed7f1b0c2d559980b1f1d77cc09419f51c8321c7ddf67758853", size = 9542337, upload-time = "2026-01-14T22:30:05.687Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9c/f576e360441de7a8201daa6dc4ebc362853bc5305e059cceeb02ebdd9a48/ty-0.0.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1f829e1eecd39c3e1b032149db7ae6a3284f72fc36b42436e65243a9ed1173db", size = 8909582, upload-time = "2026-01-14T22:30:46.089Z" }, - { url = "https://files.pythonhosted.org/packages/d6/13/0898e494032a5d8af3060733d12929e3e7716db6c75eac63fa125730a3e7/ty-0.0.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45162e7826e1789cf3374627883cdeb0d56b82473a0771923e4572928e90be3", size = 9384932, upload-time = "2026-01-14T22:30:13.769Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1a/b35b6c697008a11d4cedfd34d9672db2f0a0621ec80ece109e13fca4dfef/ty-0.0.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d11fec40b269bec01e751b2337d1c7ffa959a2c2090a950d7e21c2792442cccd", size = 9453140, upload-time = "2026-01-14T22:30:11.131Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/71c9edbc79a3c88a0711324458f29c7dbf6c23452c6e760dc25725483064/ty-0.0.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09d99e37e761a4d2651ad9d5a610d11235fbcbf35dc6d4bc04abf54e7cf894f1", size = 9960680, upload-time = "2026-01-14T22:30:33.621Z" }, - { url = "https://files.pythonhosted.org/packages/0e/75/39375129f62dd22f6ad5a99cd2a42fd27d8b91b235ce2db86875cdad397d/ty-0.0.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d9ca0cdb17bd37397da7b16a7cd23423fc65c3f9691e453ad46c723d121225a1", size = 10904518, upload-time = "2026-01-14T22:30:08.464Z" }, - { url = "https://files.pythonhosted.org/packages/32/5e/26c6d88fafa11a9d31ca9f4d12989f57782ec61e7291d4802d685b5be118/ty-0.0.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcf2757b905e7eddb7e456140066335b18eb68b634a9f72d6f54a427ab042c64", size = 10525001, upload-time = "2026-01-14T22:30:16.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a5/2f0b91894af13187110f9ad7ee926d86e4e6efa755c9c88a820ed7f84c85/ty-0.0.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00cf34c1ebe1147efeda3021a1064baa222c18cdac114b7b050bbe42deb4ca80", size = 10307103, upload-time = "2026-01-14T22:30:41.221Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/13d0410827e4bc713ebb7fdaf6b3590b37dcb1b82e0a81717b65548f2442/ty-0.0.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3a655bd869352e9a22938d707631ac9fbca1016242b1f6d132d78f347c851", size = 10072737, upload-time = "2026-01-14T22:30:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/e1/dd/fc36d8bac806c74cf04b4ca735bca14d19967ca84d88f31e121767880df1/ty-0.0.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4658e282c7cb82be304052f8f64f9925f23c3c4f90eeeb32663c74c4b095d7ba", size = 9368726, upload-time = "2026-01-14T22:30:18.683Z" }, - { url = "https://files.pythonhosted.org/packages/54/70/9e8e461647550f83e2fe54bc632ccbdc17a4909644783cdbdd17f7296059/ty-0.0.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c167d838eaaa06e03bb66a517f75296b643d950fbd93c1d1686a187e5a8dbd1f", size = 9454704, upload-time = "2026-01-14T22:30:22.759Z" }, - { url = "https://files.pythonhosted.org/packages/04/9b/6292cf7c14a0efeca0539cf7d78f453beff0475cb039fbea0eb5d07d343d/ty-0.0.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2956e0c9ab7023533b461d8a0e6b2ea7b78e01a8dde0688e8234d0fce10c4c1c", size = 9649829, upload-time = "2026-01-14T22:30:31.234Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/472a5d2013371e4870886cff791c94abdf0b92d43d305dd0f8e06b6ff719/ty-0.0.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c6a3fd7479580009f21002f3828320621d8a82d53b7ba36993234e3ccad58c8", size = 10162814, upload-time = "2026-01-14T22:30:36.174Z" }, - { url = "https://files.pythonhosted.org/packages/31/e9/2ecbe56826759845a7c21d80aa28187865ea62bc9757b056f6cbc06f78ed/ty-0.0.12-py3-none-win32.whl", hash = "sha256:a91c24fd75c0f1796d8ede9083e2c0ec96f106dbda73a09fe3135e075d31f742", size = 9140115, upload-time = "2026-01-14T22:30:38.903Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/d9531eff35a5c0ec9dbc10231fac21f9dd6504814048e81d6ce1c84dc566/ty-0.0.12-py3-none-win_amd64.whl", hash = "sha256:df151894be55c22d47068b0f3b484aff9e638761e2267e115d515fcc9c5b4a4b", size = 9884532, upload-time = "2026-01-14T22:30:25.112Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f3/20b49e75967023b123a221134548ad7000f9429f13fdcdda115b4c26305f/ty-0.0.12-py3-none-win_arm64.whl", hash = "sha256:cea99d334b05629de937ce52f43278acf155d3a316ad6a35356635f886be20ea", size = 9313974, upload-time = "2026-01-14T22:30:27.44Z" }, + { url = "https://files.pythonhosted.org/packages/99/cb/cc6d1d8de59beb17a41f9a614585f884ec2d95450306c173b3b7cc090d2e/ty-0.0.14-py3-none-linux_armv6l.whl", hash = "sha256:32cf2a7596e693094621d3ae568d7ee16707dce28c34d1762947874060fdddaa", size = 10034228, upload-time = "2026-01-27T00:57:53.133Z" }, + { url = "https://files.pythonhosted.org/packages/f3/96/dd42816a2075a8f31542296ae687483a8d047f86a6538dfba573223eaf9a/ty-0.0.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f971bf9805f49ce8c0968ad53e29624d80b970b9eb597b7cbaba25d8a18ce9a2", size = 9939162, upload-time = "2026-01-27T00:57:43.857Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b4/73c4859004e0f0a9eead9ecb67021438b2e8e5fdd8d03e7f5aca77623992/ty-0.0.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:45448b9e4806423523268bc15e9208c4f3f2ead7c344f615549d2e2354d6e924", size = 9418661, upload-time = "2026-01-27T00:58:03.411Z" }, + { url = "https://files.pythonhosted.org/packages/58/35/839c4551b94613db4afa20ee555dd4f33bfa7352d5da74c5fa416ffa0fd2/ty-0.0.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94a9b747ff40114085206bdb3205a631ef19a4d3fb89e302a88754cbbae54c", size = 9837872, upload-time = "2026-01-27T00:57:23.718Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/bbecf7e2faa20c04bebd35fc478668953ca50ee5847ce23e08acf20ea119/ty-0.0.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6756715a3c33182e9ab8ffca2bb314d3c99b9c410b171736e145773ee0ae41c3", size = 9848819, upload-time = "2026-01-27T00:57:58.501Z" }, + { url = "https://files.pythonhosted.org/packages/be/60/3c0ba0f19c0f647ad9d2b5b5ac68c0f0b4dc899001bd53b3a7537fb247a2/ty-0.0.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89d0038a2f698ba8b6fec5cf216a4e44e2f95e4a5095a8c0f57fe549f87087c2", size = 10324371, upload-time = "2026-01-27T00:57:29.291Z" }, + { url = "https://files.pythonhosted.org/packages/24/32/99d0a0b37d0397b0a989ffc2682493286aa3bc252b24004a6714368c2c3d/ty-0.0.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c64a83a2d669b77f50a4957039ca1450626fb474619f18f6f8a3eb885bf7544", size = 10865898, upload-time = "2026-01-27T00:57:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/1a/88/30b583a9e0311bb474269cfa91db53350557ebec09002bfc3fb3fc364e8c/ty-0.0.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:242488bfb547ef080199f6fd81369ab9cb638a778bb161511d091ffd49c12129", size = 10555777, upload-time = "2026-01-27T00:58:05.853Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a2/cb53fb6325dcf3d40f2b1d0457a25d55bfbae633c8e337bde8ec01a190eb/ty-0.0.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4790c3866f6c83a4f424fc7d09ebdb225c1f1131647ba8bdc6fcdc28f09ed0ff", size = 10412913, upload-time = "2026-01-27T00:57:38.834Z" }, + { url = "https://files.pythonhosted.org/packages/42/8f/f2f5202d725ed1e6a4e5ffaa32b190a1fe70c0b1a2503d38515da4130b4c/ty-0.0.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:950f320437f96d4ea9a2332bbfb5b68f1c1acd269ebfa4c09b6970cc1565bd9d", size = 9837608, upload-time = "2026-01-27T00:57:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/59a2a0521640c489dafa2c546ae1f8465f92956fede18660653cce73b4c5/ty-0.0.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a0ec3ee70d83887f86925bbc1c56f4628bd58a0f47f6f32ddfe04e1f05466df", size = 9884324, upload-time = "2026-01-27T00:57:46.786Z" }, + { url = "https://files.pythonhosted.org/packages/03/95/8d2a49880f47b638743212f011088552ecc454dd7a665ddcbdabea25772a/ty-0.0.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1a4e6b6da0c58b34415955279eff754d6206b35af56a18bb70eb519d8d139ef", size = 10033537, upload-time = "2026-01-27T00:58:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/4523b36f2ce69f92ccf783855a9e0ebbbd0f0bb5cdce6211ee1737159ed3/ty-0.0.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dc04384e874c5de4c5d743369c277c8aa73d1edea3c7fc646b2064b637db4db3", size = 10495910, upload-time = "2026-01-27T00:57:26.691Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/655beb51224d1bfd4f9ddc0bb209659bfe71ff141bcf05c418ab670698f0/ty-0.0.14-py3-none-win32.whl", hash = "sha256:b20e22cf54c66b3e37e87377635da412d9a552c9bf4ad9fc449fed8b2e19dad2", size = 9507626, upload-time = "2026-01-27T00:57:41.43Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/c569c9961760e20e0a4bc008eeb1415754564304fd53997a371b7cf3f864/ty-0.0.14-py3-none-win_amd64.whl", hash = "sha256:e312ff9475522d1a33186657fe74d1ec98e4a13e016d66f5758a452c90ff6409", size = 10437980, upload-time = "2026-01-27T00:57:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/186829654f5bfd9a028f6648e9caeb11271960a61de97484627d24443f91/ty-0.0.14-py3-none-win_arm64.whl", hash = "sha256:b6facdbe9b740cb2c15293a1d178e22ffc600653646452632541d01c36d5e378", size = 9885831, upload-time = "2026-01-27T00:57:49.747Z" }, ] [[package]] @@ -1793,11 +1792,11 @@ wheels = [ [[package]] name = "types-pexpect" -version = "4.9.0.20250916" +version = "4.9.0.20260127" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/e6/cc43e306dc7de14ec7861c24ac4957f688741ae39ae685049695d796b587/types_pexpect-4.9.0.20250916.tar.gz", hash = "sha256:69e5fed6199687a730a572de780a5749248a4c5df2ff1521e194563475c9928d", size = 13322, upload-time = "2025-09-16T02:49:25.61Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/32/7e03a07e16f79a404d6200ed6bdfcc320d0fb833436a5c6895a1403dedb7/types_pexpect-4.9.0.20260127.tar.gz", hash = "sha256:f8d43efc24251a8e533c71ea9be03d19bb5d08af096d561611697af9720cba7f", size = 13461, upload-time = "2026-01-27T03:28:30.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/6d/7740e235a9fb2570968da7d386d7feb511ce68cd23472402ff8cdf7fc78f/types_pexpect-4.9.0.20250916-py3-none-any.whl", hash = "sha256:7fa43cb96042ac58bc74f7c28e5d85782be0ee01344149886849e9d90936fe8a", size = 17057, upload-time = "2025-09-16T02:49:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/7ac5c9aa5a89a1a64cd835ae348227f4939406d826e461b85b690a8ba1c2/types_pexpect-4.9.0.20260127-py3-none-any.whl", hash = "sha256:69216c0ebf0fe45ad2900823133959b027e9471e24fc3f2e4c7b00605555da5f", size = 17078, upload-time = "2026-01-27T03:28:29.848Z" }, ] [[package]] @@ -1823,11 +1822,11 @@ wheels = [ [[package]] name = "types-setuptools" -version = "80.9.0.20251223" +version = "80.10.0.20260124" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/07/d1b605230730990de20477150191d6dccf6aecc037da94c9960a5d563bc8/types_setuptools-80.9.0.20251223.tar.gz", hash = "sha256:d3411059ae2f5f03985217d86ac6084efea2c9e9cacd5f0869ef950f308169b2", size = 42420, upload-time = "2025-12-23T03:18:26.752Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/7e/116539b9610585e34771611e33c88a4c706491fa3565500f5a63139f8731/types_setuptools-80.10.0.20260124.tar.gz", hash = "sha256:1b86d9f0368858663276a0cbe5fe5a9722caf94b5acde8aba0399a6e90680f20", size = 43299, upload-time = "2026-01-24T03:18:39.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/5c/b8877da94012dbc6643e4eeca22bca9b99b295be05d161f8a403ae9387c0/types_setuptools-80.9.0.20251223-py3-none-any.whl", hash = "sha256:1b36db79d724c2287d83dc052cf887b47c0da6a2fff044378be0b019545f56e6", size = 64318, upload-time = "2025-12-23T03:18:25.868Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7f/016dc5cc718ec6ccaa84fb73ed409ef1c261793fd5e637cdfaa18beb40a9/types_setuptools-80.10.0.20260124-py3-none-any.whl", hash = "sha256:efed7e044f01adb9c2806c7a8e1b6aa3656b8e382379b53d5f26ee3db24d4c01", size = 64333, upload-time = "2026-01-24T03:18:38.344Z" }, ] [[package]] From f909642ce1f1d3ef9e105cb238d40c8b47ed6619 Mon Sep 17 00:00:00 2001 From: HeshamHM28 Date: Wed, 28 Jan 2026 08:36:54 -0800 Subject: [PATCH 007/184] feat: Add Line Profiler visualization to webapp (#2268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds a line-by-line performance profiler visualization to the webapp, allowing users to compare execution times between original and optimized code. ## Changes ### New Line Profiler View - **`LineProfilerView.tsx`**: Side-by-side comparison component showing: - Line-by-line execution times with heat map visualization - Syntax highlighting using `prism-react-renderer` - Collapsible function blocks - Light/dark mode support - Heat legend (cold → hot based on % time) - **`lineProfilerParser.ts`**: Parser utilities for line profiler data: - `parseLineProfilerResults()` - parses markdown table output from Python's line_profiler - `formatTime()` - converts timer units to human-readable format (ns, µs, ms, s) - `getHeatLevel()` - determines heat coloring based on % time - **`/review-optimizations/[traceId]/profiler/page.tsx`**: New route for the profiler view ### API Changes - **`create-pr.ts`**: Adds "📊 Performance Profile" link to PR description when profiler data exists - **`github-app.ts`**: Removes line profiler data from metadata when PR is closed/merged - **`create-staging.ts`**, **`suggest-pr-changes.ts`**: Handle line profiler data in staging - **`staging-storage-strategy.ts`**: Interface updates for line profiler fields ### Webapp Integration - **`page.tsx`**: Added "Performance Profile" button (only visible when profiler data exists) - **`action.ts`**: Sends line profiler data when creating PR from webapp Fixes CF-1018 https://codeflash-ai.slack.com/files/U08MSR1UN6L/F0A9YVDJY75/screen_recording_2026-01-21_at_10.03.18___pm.mov https://github.com/HeshamHM28/my-best-repo/pull/21 linked to https://github.com/codeflash-ai/codeflash/pull/1139 --------- Co-authored-by: Aseem Saxena --- .../src/providers/GitPatchProvider.ts | 5 +- js/cf-api/endpoints/create-pr.ts | 44 +- js/cf-api/endpoints/create-staging.ts | 16 +- js/cf-api/endpoints/suggest-pr-changes.ts | 50 +- .../endpoints/tests/create-pr.unit.test.ts | 9 + js/cf-api/github/github-app.ts | 24 +- .../staging/git-branch-staging-strategy.ts | 27 +- .../staging/plain-text-staging-strategy.ts | 24 +- js/cf-api/staging/staging-storage-strategy.ts | 15 + js/cf-webapp/package-lock.json | 14 + js/cf-webapp/package.json | 1 + .../review-optimizations/[traceId]/action.ts | 6 + .../review-optimizations/[traceId]/page.tsx | 28 +- .../[traceId]/profiler/page.tsx | 239 ++++++++++ .../LineProfiler/LineProfilerView.tsx | 430 ++++++++++++++++++ .../src/components/LineProfiler/index.ts | 1 + js/cf-webapp/src/lib/lineProfilerParser.ts | 247 ++++++++++ 17 files changed, 1153 insertions(+), 27 deletions(-) create mode 100644 js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx create mode 100644 js/cf-webapp/src/components/LineProfiler/LineProfilerView.tsx create mode 100644 js/cf-webapp/src/components/LineProfiler/index.ts create mode 100644 js/cf-webapp/src/lib/lineProfilerParser.ts diff --git a/js/VSC-Extension/src/providers/GitPatchProvider.ts b/js/VSC-Extension/src/providers/GitPatchProvider.ts index 585b4f873..4c4d8d8f8 100644 --- a/js/VSC-Extension/src/providers/GitPatchProvider.ts +++ b/js/VSC-Extension/src/providers/GitPatchProvider.ts @@ -1217,8 +1217,6 @@ export class GitPatchProvider { original_runtime: this.formattedOriginalRuntime, loop_count: 1, // Default loop count report_table: {}, // Empty report table - original_line_profiler: this.originalLineProfiler, - optimized_line_profiler: this.optimizedLineProfiler, }, existingTests: "", generatedTests: "", @@ -1227,6 +1225,9 @@ export class GitPatchProvider { replayTests: "", concolicTests: "", optimizationReview: this.optimizationReview, + // Line profiler data at root level (not inside prCommentFields) + originalLineProfiler: this.originalLineProfiler, + optimizedLineProfiler: this.optimizedLineProfiler, }; this.logger.info( diff --git a/js/cf-api/endpoints/create-pr.ts b/js/cf-api/endpoints/create-pr.ts index f9252b6e9..8688b9baf 100644 --- a/js/cf-api/endpoints/create-pr.ts +++ b/js/cf-api/endpoints/create-pr.ts @@ -61,6 +61,20 @@ function parseSpeedupValue(value: unknown, suffix: "x" | "%"): number | null { return isNaN(parsed) ? null : parsed } +// Helper function to add line profiler data to metadata (avoids duplication) +function addLineProfilerToMetadata( + metadata: Record, + originalLineProfiler?: string, + optimizedLineProfiler?: string, +): void { + if (originalLineProfiler && !metadata.originalLineProfiler) { + metadata.originalLineProfiler = originalLineProfiler + } + if (optimizedLineProfiler && !metadata.optimizedLineProfiler) { + metadata.optimizedLineProfiler = optimizedLineProfiler + } +} + // Define a comprehensive interface for PR title and body generation export interface PrContentBuilder { buildResultHeader: typeof buildResultHeader @@ -83,6 +97,7 @@ export function createStandalonePRTitleAndBody( concolicTests: string = "", optimizationReview: string = "", trace_id: string, + hasLineProfilerData: boolean = false, ): { title: string; body: string } { const prCommentHeader = builder.buildResultHeader(prCommentFields) @@ -112,9 +127,16 @@ export function createStandalonePRTitleAndBody( if (optReviewBadge) { optReviewBadge = ` ${optReviewBadge}\n` } + + // Add line profiler link if profiler data exists + let lineProfilerSection = "" + if (hasLineProfilerData && trace_id) { + lineProfilerSection = `\n### 📊 Performance Profile\n[View detailed line-by-line performance analysis](https://app.codeflash.ai/review-optimizations/${trace_id}/profiler)\n` + } + const body: string = benchmarkInfo - ? `${metadata}\n${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}${optReviewBadge}` - : `${metadata}\n${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}${optReviewBadge}` + ? `${metadata}\n${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${lineProfilerSection}${prCommentFooter}${optReviewBadge}` + : `${metadata}\n${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${lineProfilerSection}${prCommentFooter}${optReviewBadge}` return { title, body } } @@ -261,6 +283,8 @@ export async function createPr(req: Request, res: Response) { concolicTests, optimizationReview, stagingBranch, + originalLineProfiler, + optimizedLineProfiler, } = req.body // Get source from headers instead of body @@ -377,6 +401,8 @@ export async function createPr(req: Request, res: Response) { stagingBranch, userTier, organizationId, + originalLineProfiler, + optimizedLineProfiler, ) if (prNumber > 0) { @@ -436,6 +462,8 @@ export async function createPr(req: Request, res: Response) { stagingBranch, userTier, organizationId, + originalLineProfiler, + optimizedLineProfiler, ) if (prNumber > 0) { @@ -527,6 +555,8 @@ export async function triggerCreatePr( stagingBranch = "", userTier = "free", organizationId: string | null = null, + originalLineProfiler: string | undefined = undefined, + optimizedLineProfiler: string | undefined = undefined, ): Promise { const isPaidUser = userTier.toLowerCase() !== "free" logger.info( @@ -592,6 +622,7 @@ export async function triggerCreatePr( } // Use the injectable function instead of the hardcoded one + const hasLineProfilerData = !!(originalLineProfiler || optimizedLineProfiler) const { title, body } = triggerCreatePrDeps.createStandalonePRTitleAndBody( prCommentFields, existingTests, @@ -603,6 +634,7 @@ export async function triggerCreatePr( concolicTests, optimizationReview, traceId, + hasLineProfilerData, ) logger.info(`Creating PR with title: ${title}`, { @@ -679,6 +711,9 @@ export async function triggerCreatePr( currentMetadata.staging_branch_name = newBranchName currentMetadata.storageType = "git_branch" + // Add line profiler data if provided and not already present + addLineProfilerToMetadata(currentMetadata, originalLineProfiler, optimizedLineProfiler) + updateData.staging_storage_type = "git_branch" updateData.metadata = currentMetadata updateData.is_staging = true @@ -686,6 +721,11 @@ export async function triggerCreatePr( `[triggerCreatePr] Paid user/subscribed org: Converting storage to git_branch for traceId: ${traceId}`, ) } + } else if (traceId && (originalLineProfiler || optimizedLineProfiler)) { + // For non-paid users, still add line profiler data if provided + const currentMetadata = (existing?.metadata ?? {}) as Record + addLineProfilerToMetadata(currentMetadata, originalLineProfiler, optimizedLineProfiler) + updateData.metadata = currentMetadata } // Only add if missing (preserve staging data) if (prCommentFields) { diff --git a/js/cf-api/endpoints/create-staging.ts b/js/cf-api/endpoints/create-staging.ts index 4525eb515..5bf8e3b1e 100644 --- a/js/cf-api/endpoints/create-staging.ts +++ b/js/cf-api/endpoints/create-staging.ts @@ -15,6 +15,8 @@ export interface CreateStagingRequestBody { owner?: string repo?: string diffContents?: Record + originalLineProfiler?: string + optimizedLineProfiler?: string } // Dependencies for testing @@ -47,8 +49,16 @@ export async function saveStagingReview( organizationId?: string | null, subscriptionInfo?: SubscriptionInfo, ): Promise<{ status: number; data: Record }> { - const { prCommentFields, traceId, baseBranch, owner, repo, diffContents } = - body as unknown as CreateStagingRequestBody + const { + prCommentFields, + traceId, + baseBranch, + owner, + repo, + diffContents, + originalLineProfiler, + optimizedLineProfiler, + } = body as unknown as CreateStagingRequestBody // Validate required fields const validationError = validateRequiredFields(prCommentFields, traceId) @@ -93,6 +103,8 @@ export async function saveStagingReview( diffContents: diffContents ? new Map(Object.entries(diffContents)) : undefined, prCommentFields, metadata: body, + originalLineProfiler, + optimizedLineProfiler, } // Execute the storage strategy diff --git a/js/cf-api/endpoints/suggest-pr-changes.ts b/js/cf-api/endpoints/suggest-pr-changes.ts index e88795525..4485cbfc1 100644 --- a/js/cf-api/endpoints/suggest-pr-changes.ts +++ b/js/cf-api/endpoints/suggest-pr-changes.ts @@ -140,13 +140,21 @@ export async function updateOptimizationEvent( traceId: string, prId?: string, prCommentFields?: any, + originalLineProfiler?: string, + optimizedLineProfiler?: string, ) { if (traceId !== "") { try { // Check existing data first (preserve staging data) const existing = await dependencies.prisma.optimization_events.findUnique({ where: { trace_id: traceId }, - select: { function_name: true, speedup_x: true, file_path: true, speedup_pct: true }, + select: { + function_name: true, + speedup_x: true, + file_path: true, + speedup_pct: true, + metadata: true, + }, }) // Extract data from prCommentFields if provided @@ -172,6 +180,18 @@ export async function updateOptimizationEvent( } } + // Add line profiler data to metadata if provided + if (originalLineProfiler || optimizedLineProfiler) { + const currentMetadata = (existing?.metadata ?? {}) as Record + if (originalLineProfiler && !currentMetadata.originalLineProfiler) { + currentMetadata.originalLineProfiler = originalLineProfiler + } + if (optimizedLineProfiler && !currentMetadata.optimizedLineProfiler) { + currentMetadata.optimizedLineProfiler = optimizedLineProfiler + } + updateData.metadata = currentMetadata + } + await dependencies.prisma.optimization_events.update({ where: { trace_id: traceId }, data: updateData, @@ -204,6 +224,8 @@ export async function suggestPrChanges( replayTests, concolicTests, optimizationReview, + originalLineProfiler, + optimizedLineProfiler, } = req.body const userId = req.userId @@ -289,6 +311,8 @@ export async function suggestPrChanges( traceId, optimizationReview, res, + originalLineProfiler, + optimizedLineProfiler, ) if (result && typeof result === "object" && "status" in result) { @@ -308,6 +332,8 @@ export async function suggestPrChanges( coverage_message, userId, optimizationReview, + originalLineProfiler, + optimizedLineProfiler, } await dependencies.requestApproval( @@ -345,6 +371,8 @@ export async function suggestPrChanges( traceId, optimizationReview, res, + originalLineProfiler, + optimizedLineProfiler, ) // Check if this is a quality monitoring repo and send notification @@ -363,6 +391,8 @@ export async function suggestPrChanges( replayTests, concolicTests, optimizationReview, + originalLineProfiler, + optimizedLineProfiler, } // Send quality monitoring notification (non-blocking) @@ -430,6 +460,8 @@ export async function triggerSuggestPrChanges( traceId: string = "", optimizationReview: string = "", res?: Response, + originalLineProfiler: string = "", + optimizedLineProfiler: string = "", ): Promise { try { const diffContentsMap: Map = dependencies.fileDiffsToMap(diffContents) @@ -643,7 +675,13 @@ export async function triggerSuggestPrChanges( } } - await dependencies.updateOptimizationEvent(traceId, newPrData.data.id, prCommentFields) + await dependencies.updateOptimizationEvent( + traceId, + newPrData.data.id, + prCommentFields, + originalLineProfiler, + optimizedLineProfiler, + ) if (res) { res.json(newPrData.data.number) @@ -805,7 +843,13 @@ export async function triggerSuggestPrChanges( } } - await dependencies.updateOptimizationEvent(traceId, undefined, prCommentFields) + await dependencies.updateOptimizationEvent( + traceId, + undefined, + prCommentFields, + originalLineProfiler, + optimizedLineProfiler, + ) if (res) { res.json(review.data.id) diff --git a/js/cf-api/endpoints/tests/create-pr.unit.test.ts b/js/cf-api/endpoints/tests/create-pr.unit.test.ts index d3a286642..c230310c3 100644 --- a/js/cf-api/endpoints/tests/create-pr.unit.test.ts +++ b/js/cf-api/endpoints/tests/create-pr.unit.test.ts @@ -160,6 +160,8 @@ describe("createPr", () => { undefined, // stagingBranch "free", // userTier defaults to free null, // organizationId + undefined, // originalLineProfiler + undefined, // optimizedLineProfiler ) }) @@ -194,6 +196,8 @@ describe("createPr", () => { "codeflash-staging/test-branch", "free", null, // organizationId + undefined, // originalLineProfiler + undefined, // optimizedLineProfiler ) expect(mockRes.json).toHaveBeenCalledWith(456) }) @@ -227,6 +231,8 @@ describe("createPr", () => { undefined, "pro", null, // organizationId + undefined, // originalLineProfiler + undefined, // optimizedLineProfiler ) }) }) @@ -478,6 +484,8 @@ describe("createPr", () => { undefined, // stagingBranch "free", // userTier null, // organizationId + undefined, // originalLineProfiler + undefined, // optimizedLineProfiler ) expect(mockRes.json).toHaveBeenCalledWith(456) }) @@ -713,6 +721,7 @@ describe("triggerCreatePr", () => { "concolic tests", "medium", "trace123", + false, // hasLineProfilerData ) expect(mockDeps.createStandalonePullRequest).toHaveBeenCalledWith( mockInstallationOctokit, diff --git a/js/cf-api/github/github-app.ts b/js/cf-api/github/github-app.ts index 4b8893b26..16dfe65b1 100644 --- a/js/cf-api/github/github-app.ts +++ b/js/cf-api/github/github-app.ts @@ -147,14 +147,28 @@ export const githubApp = await (async () => { if (payload.pull_request) { const prId = String(payload.pull_request.id) try { - await prisma.optimization_events.updateMany({ + const optimizationEvent = await prisma.optimization_events.findUnique({ where: { pr_id: prId }, - data: { - event_type: payload.pull_request.merged ? "pr_merged" : "pr_closed", - }, + select: { id: true, metadata: true }, }) + + if (optimizationEvent) { + const currentMetadata = (optimizationEvent.metadata ?? {}) as Record + // Remove line profiler data from metadata + delete currentMetadata.originalLineProfiler + delete currentMetadata.optimizedLineProfiler + + await prisma.optimization_events.update({ + where: { id: optimizationEvent.id }, + data: { + event_type: payload.pull_request.merged ? "pr_merged" : "pr_closed", + metadata: currentMetadata as object, + }, + }) + } + console.log( - `Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"}`, + `Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"} and removed line profiler data`, ) } catch (err) { console.error(`Failed to update optimization_event for PR ID ${prId}:`, err) diff --git a/js/cf-api/staging/git-branch-staging-strategy.ts b/js/cf-api/staging/git-branch-staging-strategy.ts index 34e9f5ab1..a9c9678a7 100644 --- a/js/cf-api/staging/git-branch-staging-strategy.ts +++ b/js/cf-api/staging/git-branch-staging-strategy.ts @@ -82,8 +82,18 @@ export class GitBranchStagingStrategy extends StagingStorageStrategy { } async save(context: StagingStorageContext): Promise { - const { userId, traceId, baseBranch, owner, repo, diffContents, prCommentFields, metadata } = - context + const { + userId, + traceId, + baseBranch, + owner, + repo, + diffContents, + prCommentFields, + metadata, + originalLineProfiler, + optimizedLineProfiler, + } = context // Validate required git fields - fall back to plain text if missing if (!owner || !repo) { @@ -189,10 +199,15 @@ export class GitBranchStagingStrategy extends StagingStorageStrategy { } // Build metadata without diffContents (stored in git branch) - const mergedMetadata = buildMergedMetadata(existingEvent.metadata, metadata, { - storageType: "git_branch", - stagingBranchName, - }) + const mergedMetadata = buildMergedMetadata( + existingEvent.metadata, + metadata, + { + storageType: "git_branch", + stagingBranchName, + }, + { originalLineProfiler, optimizedLineProfiler }, + ) await dependencies.prisma.optimization_events.update({ where: { trace_id: traceId }, diff --git a/js/cf-api/staging/plain-text-staging-strategy.ts b/js/cf-api/staging/plain-text-staging-strategy.ts index 7c08445a9..31760fee6 100644 --- a/js/cf-api/staging/plain-text-staging-strategy.ts +++ b/js/cf-api/staging/plain-text-staging-strategy.ts @@ -47,7 +47,16 @@ export class PlainTextStagingStrategy extends StagingStorageStrategy { } async save(context: StagingStorageContext): Promise { - const { userId, traceId, baseBranch, prCommentFields, metadata, diffContents } = context + const { + userId, + traceId, + baseBranch, + prCommentFields, + metadata, + diffContents, + originalLineProfiler, + optimizedLineProfiler, + } = context const { function_name, @@ -80,10 +89,15 @@ export class PlainTextStagingStrategy extends StagingStorageStrategy { } } - const mergedMetadata = buildMergedMetadata(existingEvent.metadata, metadata, { - storageType: "plain_text", - diffContents, - }) + const mergedMetadata = buildMergedMetadata( + existingEvent.metadata, + metadata, + { + storageType: "plain_text", + diffContents, + }, + { originalLineProfiler, optimizedLineProfiler }, + ) await dependencies.prisma.optimization_events.update({ where: { trace_id: traceId }, diff --git a/js/cf-api/staging/staging-storage-strategy.ts b/js/cf-api/staging/staging-storage-strategy.ts index de570feda..94333c745 100644 --- a/js/cf-api/staging/staging-storage-strategy.ts +++ b/js/cf-api/staging/staging-storage-strategy.ts @@ -9,6 +9,8 @@ export interface StagingStorageContext { diffContents?: Map prCommentFields: Record metadata: Record + originalLineProfiler?: string + optimizedLineProfiler?: string } export interface StagingStorageResult { @@ -42,10 +44,16 @@ export type BuildMergedMetadataOptions = | { storageType: "git_branch"; stagingBranchName: string } | { storageType: "plain_text"; diffContents?: Map } +export interface LineProfilerData { + originalLineProfiler?: string + optimizedLineProfiler?: string +} + export function buildMergedMetadata( existingMetadata: unknown, requestMetadata: Record, options: BuildMergedMetadataOptions, + lineProfilerData?: LineProfilerData, ) { const existing = ( existingMetadata && typeof existingMetadata === "object" ? existingMetadata : {} @@ -74,6 +82,13 @@ export function buildMergedMetadata( ...((existing.prCommentFields as Record) ?? {}), ...sanitizedPrFields, }, + // Include line profiler data if provided + ...(lineProfilerData?.originalLineProfiler && { + originalLineProfiler: lineProfilerData.originalLineProfiler, + }), + ...(lineProfilerData?.optimizedLineProfiler && { + optimizedLineProfiler: lineProfilerData.optimizedLineProfiler, + }), } if (options.storageType === "git_branch") { diff --git a/js/cf-webapp/package-lock.json b/js/cf-webapp/package-lock.json index 65f09db75..4f7668962 100644 --- a/js/cf-webapp/package-lock.json +++ b/js/cf-webapp/package-lock.json @@ -49,6 +49,7 @@ "postcss": "^8", "posthog-js": "1.127.0", "posthog-node": "^4.0.1", + "prism-react-renderer": "^2.4.1", "react": "^18", "react-chartjs-2": "^5.3.0", "react-dom": "^18", @@ -13416,6 +13417,19 @@ "dev": true, "license": "MIT" }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/prisma": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz", diff --git a/js/cf-webapp/package.json b/js/cf-webapp/package.json index b2a3bcde0..9fa2c2fdd 100644 --- a/js/cf-webapp/package.json +++ b/js/cf-webapp/package.json @@ -59,6 +59,7 @@ "postcss": "^8", "posthog-js": "1.127.0", "posthog-node": "^4.0.1", + "prism-react-renderer": "^2.4.1", "react": "^18", "react-chartjs-2": "^5.3.0", "react-dom": "^18", diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts index 879336559..03e07421b 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts @@ -257,6 +257,8 @@ export async function createPullRequest({ baseBranch, full_repo_name, coverage_message, + originalLineProfiler, + optimizedLineProfiler, }: { traceId: string diffContents: Record @@ -270,6 +272,8 @@ export async function createPullRequest({ baseBranch?: string full_repo_name?: string coverage_message?: string + originalLineProfiler?: string + optimizedLineProfiler?: string }): Promise { const cfapiUrl = process.env.CODEFLASH_CFAPI_URL const session = await getAccessToken({ refresh: true }) @@ -307,6 +311,8 @@ export async function createPullRequest({ baseBranch, owner, repo: repoName, + originalLineProfiler, + optimizedLineProfiler, }), }) diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx index 6f0042753..90ed1d440 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx @@ -1,9 +1,9 @@ "use client" import { useEffect, useState, useCallback, useRef } from "react" -import { useParams } from "next/navigation" +import { useParams, useRouter } from "next/navigation" import Image from "next/image" -import { Zap, CheckCircle, XCircle, MessageSquare, Loader2, GitCommit } from "lucide-react" +import { Zap, CheckCircle, XCircle, MessageSquare, Loader2, GitCommit, BarChart3 } from "lucide-react" import { createPullRequest, getOptimizationEventById, @@ -63,6 +63,8 @@ interface EventMetadata { coverage_message?: string staging_storage_type?: "plain_text" | "git_branch" staging_branch_name?: string + originalLineProfiler?: string + optimizedLineProfiler?: string } interface Repository { @@ -136,6 +138,7 @@ interface SaveOptimizationResult { export default function OptimizationReviewPage() { const params = useParams() + const router = useRouter() const [event, setEvent] = useState(null) const [loading, setLoading] = useState(true) const [creatingPR, setCreatingPR] = useState(false) @@ -513,6 +516,8 @@ export default function OptimizationReviewPage() { baseBranch: customBaseBranch || event.baseBranch || undefined, full_repo_name: event.repository?.full_name, coverage_message: event.metadata.coverage_message, + originalLineProfiler: event.metadata.originalLineProfiler, + optimizedLineProfiler: event.metadata.optimizedLineProfiler, }) console.log("[handleCreatePR] Result from createPullRequest:", { @@ -614,6 +619,10 @@ export default function OptimizationReviewPage() { window.open(event.pr_url, "_blank") } + const handleViewProfiler = () => { + router.push(`/review-optimizations/${params.traceId}/profiler`) + } + const formatTimeAgo = (date: Date) => { const seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000) @@ -683,6 +692,21 @@ export default function OptimizationReviewPage() {
+ {/* Performance Profile Button - Only show if profiler data exists */} + {(metadata.originalLineProfiler || metadata.optimizedLineProfiler) && ( + + )} + {/* Comments Toggle Button with Count */} + )} +
+ + ) + } + return this.props.children + } +} + +interface EventMetadata { + originalLineProfiler?: string + optimizedLineProfiler?: string + prCommentFields?: { + original_runtime?: string + best_runtime?: string + } +} + +interface OptimizationEvent { + id: string + trace_id: string + function_name?: string | null + file_path?: string | null + speedup_x?: number | null + speedup_pct?: number | null + metadata: EventMetadata +} + +export default function LineProfilerPage() { + const params = useParams() + const router = useRouter() + const [event, setEvent] = useState(null) + const [loading, setLoading] = useState(true) + const { currentOrg } = useViewMode() + + useEffect(() => { + async function loadEvent() { + try { + const userSession = (await getUserIdAndUsername()) ?? { userId: "", username: "" } + + const data = await getOptimizationEventById({ + payload: currentOrg + ? { orgId: currentOrg.id } + : { userId: userSession.userId, username: userSession.username }, + trace_id: params.traceId as string, + }) + + if (data) { + const metadata = data.metadata as EventMetadata + setEvent({ + id: data.id, + trace_id: data.trace_id, + function_name: data.function_name, + file_path: data.file_path, + speedup_x: data.speedup_x, + speedup_pct: data.speedup_pct, + metadata, + }) + } else { + setEvent(null) + } + } catch (error) { + console.error("Failed to load optimization event:", error) + toast.error("Failed to load profiler data") + } finally { + setLoading(false) + } + } + + loadEvent() + }, [params.traceId, currentOrg?.id]) + + const handleBack = () => { + router.push(`/review-optimizations/${params.traceId}`) + } + + if (loading) { + return ( +
+ +
+ ) + } + + if (!event) { + return ( +
+
+

Event not found

+

+ The optimization event you're looking for doesn't exist. +

+ +
+
+ ) + } + + const metadata = event.metadata || {} + const hasProfilerData = metadata.originalLineProfiler || metadata.optimizedLineProfiler + + if (!hasProfilerData) { + return ( +
+
+

No Profiler Data

+

+ This optimization doesn't have line profiler data available. +

+ +
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+
+ +
+ +

+ Line Profiler Report + {event.function_name && ( + <> + {" - "} + {event.function_name}() + + )} +

+ {event.speedup_x && ( + + + + + {event.speedup_x.toFixed(2)}x faster + + )} +
+
+ + {event.file_path && ( + + {event.file_path} + + )} +
+
+ + {/* Line Profiler View */} +
+ + + +
+
+ ) +} diff --git a/js/cf-webapp/src/components/LineProfiler/LineProfilerView.tsx b/js/cf-webapp/src/components/LineProfiler/LineProfilerView.tsx new file mode 100644 index 000000000..e82054d37 --- /dev/null +++ b/js/cf-webapp/src/components/LineProfiler/LineProfilerView.tsx @@ -0,0 +1,430 @@ +"use client" + +import { useState, useEffect } from "react" +import { + parseLineProfilerResults, + getHeatLevel, + formatTime, + type LineProfilerReport, + type LineProfilerFunction, +} from "@/lib/lineProfilerParser" +import { ChevronDown, ChevronRight, Flame, Snowflake, Clock, Zap } from "lucide-react" +import { cn } from "@/lib/utils" +import { Highlight, themes } from "prism-react-renderer" + +// Sanitize line contents to prevent XSS (defense in depth - Prism should escape, but be safe) +const sanitizeCode = (code: string): string => + code.replace(/[<>&]/g, c => ({ "<": "<", ">": ">", "&": "&" })[c] || c) + +interface LineProfilerViewProps { + originalProfiler?: string + optimizedProfiler?: string + functionName?: string + originalRuntime?: string + optimizedRuntime?: string +} + +interface ProfilerPanelProps { + report: LineProfilerReport | null + variant: "original" | "optimized" + runtime?: string + isDarkMode: boolean +} + +function ProfilerPanel({ report, variant, runtime, isDarkMode }: ProfilerPanelProps) { + const [collapsedFunctions, setCollapsedFunctions] = useState>(new Set()) + + const toggleFunction = (funcName: string) => { + setCollapsedFunctions(prev => { + const next = new Set(prev) + if (next.has(funcName)) { + next.delete(funcName) + } else { + next.add(funcName) + } + return next + }) + } + + const isOriginal = variant === "original" + const headerBg = isOriginal + ? "bg-red-500/10 border-red-500/20" + : "bg-green-500/10 border-green-500/20" + const headerText = isOriginal + ? "text-red-700 dark:text-red-400" + : "text-green-700 dark:text-green-400" + const headerIcon = isOriginal ? : + + if (!report || report.functions.length === 0) { + return ( +
+
+
+ {headerIcon} + {isOriginal ? "Original" : "Optimized"} +
+ {runtime && {runtime}} +
+
+ No profiler data available +
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+ {headerIcon} + {isOriginal ? "Original" : "Optimized"} +
+
+ {runtime && ( + {runtime} + )} +
+
+ + {/* Scrollable content */} +
+ {report.functions.map((func, funcIndex) => ( + toggleFunction(func.functionName)} + variant={variant} + isDarkMode={isDarkMode} + /> + ))} +
+
+ ) +} + +interface FunctionBlockProps { + func: LineProfilerFunction + timerUnit: string + isCollapsed: boolean + onToggle: () => void + variant: "original" | "optimized" + isDarkMode: boolean +} + +function FunctionBlock({ + func, + timerUnit, + isCollapsed, + onToggle, + variant, + isDarkMode, +}: FunctionBlockProps) { + const isOriginal = variant === "original" + const accentColor = isOriginal ? "hover:bg-red-500/5" : "hover:bg-green-500/5" + + return ( +
+ {/* Function header */} + + + {/* Code content - Respects light/dark mode */} + {!isCollapsed && ( +
+ {/* Column headers */} +
+
+ Line +
+
+
Hits
+
Time
+
Per Hit
+
% Time
+
+
Code
+
+
+ {func.entries.map((entry, entryIndex) => { + const heatLevel = getHeatLevel(entry.percentTime) + const lineNum = entryIndex + 1 + + return ( +
+ {/* Line number */} +
+ {lineNum} +
+ + {/* Stats columns */} +
+
+ {entry.hits || ""} +
+
+ {entry.time ? formatTime(entry.time, timerUnit) : ""} +
+
+ {entry.perHit ? formatTime(entry.perHit, timerUnit) : ""} +
+
+ {entry.percentTime > 0 ? `${entry.percentTime.toFixed(1)}%` : ""} +
+
+ + {/* Code with syntax highlighting */} +
+ + {({ tokens, getTokenProps }) => ( + + {tokens.map((line, i) => ( + + {line.map((token, key) => ( + + ))} + + ))} + + )} + +
+
+ ) + })} +
+
+ )} +
+ ) +} + +export function LineProfilerView({ + originalProfiler, + optimizedProfiler, + functionName, + originalRuntime, + optimizedRuntime, +}: LineProfilerViewProps) { + const [isDarkMode, setIsDarkMode] = useState(false) + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + + const updateTheme = () => { + const isDark = document.documentElement.classList.contains("dark") + setIsDarkMode(isDark) + } + + updateTheme() + + // Use matchMedia for system preference changes (better performance than MutationObserver) + const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)") + darkModeQuery.addEventListener("change", updateTheme) + + // Also observe class changes for manual theme toggles (e.g., next-themes) + const observer = new MutationObserver(() => updateTheme()) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], // Only observe class changes + }) + + return () => { + darkModeQuery.removeEventListener("change", updateTheme) + observer.disconnect() + } + }, []) + + const originalReport = originalProfiler ? parseLineProfilerResults(originalProfiler) : null + const optimizedReport = optimizedProfiler ? parseLineProfilerResults(optimizedProfiler) : null + + const hasOriginal = originalReport && originalReport.functions.length > 0 + const hasOptimized = optimizedReport && optimizedReport.functions.length > 0 + + if (!mounted) { + return null + } + + if (!hasOriginal && !hasOptimized) { + return ( +
+
+ +

No line profiler data available

+

+ Run an optimization with line profiling enabled to see results here. +

+
+
+ ) + } + + return ( +
+ {/* Top header */} +
+
+ {functionName && ( +
+ + + Line Profile: {functionName}() + +
+ )} + {originalRuntime && optimizedRuntime && ( +
+ + {originalRuntime} + + + + {optimizedRuntime} + +
+ )} +
+ + {/* Heat legend */} +
+ Heat: +
+ + <5% +
+
+
+ 5-15% +
+
+
+ 15-30% +
+
+
+ 30-50% +
+
+ + >50% +
+
+
+ + {/* Side-by-side panels */} +
+ + +
+ + +
+
+ ) +} + +export default LineProfilerView diff --git a/js/cf-webapp/src/components/LineProfiler/index.ts b/js/cf-webapp/src/components/LineProfiler/index.ts new file mode 100644 index 000000000..63ff5fad6 --- /dev/null +++ b/js/cf-webapp/src/components/LineProfiler/index.ts @@ -0,0 +1 @@ +export { LineProfilerView, default } from "./LineProfilerView" diff --git a/js/cf-webapp/src/lib/lineProfilerParser.ts b/js/cf-webapp/src/lib/lineProfilerParser.ts new file mode 100644 index 000000000..1144c19b3 --- /dev/null +++ b/js/cf-webapp/src/lib/lineProfilerParser.ts @@ -0,0 +1,247 @@ +/** + * Line Profiler Parser Utility + * Parses the markdown table output from Python's line_profiler into structured data + */ + +export interface LineProfilerEntry { + hits: string + time: string + perHit: string + percentTime: number + lineContents: string +} + +export interface LineProfilerFunction { + functionName: string + totalTime: string + entries: LineProfilerEntry[] +} + +export interface LineProfilerReport { + timerUnit: string + functions: LineProfilerFunction[] +} + +/** + * Parse line profiler results from the markdown table format + * Expected format: + * ``` + * # Timer unit: 1e-09 s + * ## Function: function_name + * ## Total time: X s + * | Hits | Time | Per Hit | % Time | Line Contents | + * |------|------|---------|--------|---------------| + * | 123 | 456 | 3.7 | 50.0 | def foo(): | + * ``` + * + * TODO: Security & Robustness - Add input validation before processing: + * - Maximum input size limit (e.g., 10MB) to prevent DoS + * - Maximum line count limit (e.g., 100,000 lines) + * - Line length validation + * - Structure validation (expected headers, format) + * + * Example: + * ```typescript + * if (rawResults.length > 10_000_000) { + * throw new Error('Profiler output too large'); + * } + * const lines = rawResults.split("\n"); + * if (lines.length > 100_000) { + * throw new Error('Too many lines in profiler output'); + * } + * ``` + */ +export function parseLineProfilerResults(rawResults: string): LineProfilerReport { + const report: LineProfilerReport = { + timerUnit: "", + functions: [], + } + + if (!rawResults || rawResults.trim() === "") { + return report + } + + const lines = rawResults.split("\n") + let currentFunction: LineProfilerFunction | null = null + let inTable = false + let headerPassed = false + + for (const line of lines) { + const trimmedLine = line.trim() + + // Parse timer unit + if (trimmedLine.startsWith("# Timer unit:")) { + report.timerUnit = trimmedLine.replace("# Timer unit:", "").trim() + continue + } + + // Parse function name + if (trimmedLine.startsWith("## Function:")) { + if (currentFunction) { + report.functions.push(currentFunction) + } + currentFunction = { + functionName: trimmedLine.replace("## Function:", "").trim(), + totalTime: "", + entries: [], + } + inTable = false + headerPassed = false + continue + } + + // Parse total time + if (trimmedLine.startsWith("## Total time:")) { + if (currentFunction) { + currentFunction.totalTime = trimmedLine.replace("## Total time:", "").trim() + } + continue + } + + // Detect table header - handle variable whitespace in cells + // Tabulate may produce "| Hits |" with extra spaces + if (trimmedLine.includes("Hits") && trimmedLine.includes("Line Contents") && trimmedLine.startsWith("|")) { + inTable = true + headerPassed = false + continue + } + + // Skip separator line (|---|---|...) - also handles alignment colons like |-------:| + if (inTable && trimmedLine.match(/^\|[-\s|:]+\|$/)) { + headerPassed = true + continue + } + + // Parse table rows + if (inTable && headerPassed && trimmedLine.startsWith("|") && currentFunction) { + // Split by | but preserve the original line for extracting code with whitespace + const rawParts = trimmedLine.split("|") + // Trim stats columns but NOT the code column + const statParts = rawParts.slice(1, 5).map((p) => p.trim()) + // Keep code with original whitespace - join remaining parts (code may contain pipes) + // Use " " for empty lines to preserve blank lines in display + const codePart = rawParts.slice(5, -1).join("|") || " " + + if (statParts.length >= 4) { + const percentStr = statParts[3] || "0" + const percentTime = parseFloat(percentStr) || 0 + + currentFunction.entries.push({ + hits: statParts[0] || "", + time: statParts[1] || "", + perHit: statParts[2] || "", + percentTime, + lineContents: codePart, // Preserve whitespace in code + }) + } + } + } + + // Don't forget the last function + if (currentFunction) { + report.functions.push(currentFunction) + } + + return report +} + +/** + * Heat level thresholds for performance visualization (% of total time) + */ +export const HEAT_THRESHOLDS = { + HOT_4: 50, // >= 50% - Critical hotspot + HOT_3: 30, // >= 30% - High impact + HOT_2: 15, // >= 15% - Medium impact + HOT_1: 5, // >= 5% - Low impact +} as const + +/** + * Get the heat level for a given percent time + * Returns a class suffix for CSS styling + */ +export function getHeatLevel( + percentTime: number, +): "cold" | "hot-1" | "hot-2" | "hot-3" | "hot-4" { + if (percentTime >= HEAT_THRESHOLDS.HOT_4) return "hot-4" + if (percentTime >= HEAT_THRESHOLDS.HOT_3) return "hot-3" + if (percentTime >= HEAT_THRESHOLDS.HOT_2) return "hot-2" + if (percentTime >= HEAT_THRESHOLDS.HOT_1) return "hot-1" + return "cold" +} + +/** + * Calculate the maximum percent time across all entries for normalization + */ +export function getMaxPercentTime(report: LineProfilerReport): number { + let max = 0 + for (const func of report.functions) { + for (const entry of func.entries) { + if (entry.percentTime > max) { + max = entry.percentTime + } + } + } + return max || 100 +} + +/** + * Format timer unit to human-readable string + * e.g., "1e-09 s" → "1 ns", "1e-06 s" → "1 µs" + */ +export function formatTimerUnit(timerUnit: string): string { + if (!timerUnit || timerUnit.trim() === "") return "" + + const unitMatch = timerUnit.match(/([0-9.e+-]+)\s*s/) + if (!unitMatch) return timerUnit + + const scaleFactor = parseFloat(unitMatch[1]) + + if (scaleFactor >= 1) { + return `${scaleFactor} s` + } else if (scaleFactor >= 1e-3) { + return `${scaleFactor * 1000} ms` + } else if (scaleFactor >= 1e-6) { + return `${scaleFactor * 1e6} µs` + } else if (scaleFactor >= 1e-9) { + return `${scaleFactor * 1e9} ns` + } else { + return `${scaleFactor * 1e12} ps` + } +} + +/** + * Format time value to human-readable string + */ +export function formatTime(timeStr: string, timerUnit: string): string { + if (!timeStr || timeStr === "") return "" + + // Parse the timer unit to get the scale factor + // Common units: "1e-09 s" (nanoseconds), "1e-06 s" (microseconds), etc. + let scaleFactor = 1e-9 // Default to nanoseconds + const unitMatch = timerUnit.match(/([0-9.e+-]+)\s*s/) + if (unitMatch) { + scaleFactor = parseFloat(unitMatch[1]) + } + + // Parse the time value + const timeValue = parseFloat(timeStr.replace(/,/g, "")) + if (isNaN(timeValue)) return timeStr + + // Convert to seconds + const seconds = timeValue * scaleFactor + + // Format based on magnitude + if (seconds >= 3600) { + return `${(seconds / 3600).toFixed(2)}h` + } else if (seconds >= 60) { + return `${(seconds / 60).toFixed(2)}m` + } else if (seconds >= 1) { + return `${seconds.toFixed(2)}s` + } else if (seconds >= 0.001) { + return `${(seconds * 1000).toFixed(2)}ms` + } else if (seconds >= 0.000001) { + return `${(seconds * 1000000).toFixed(2)}µs` + } else { + return `${(seconds * 1000000000).toFixed(2)}ns` + } +} From d9a8aa88118a5d80ad9260d87fa7047555f1adbf Mon Sep 17 00:00:00 2001 From: mashraf-222 Date: Wed, 28 Jan 2026 20:20:00 +0200 Subject: [PATCH 008/184] enhance LLM response view & adding ranker details in observability (#2326) image image --------- Co-authored-by: Codeflash Bot Co-authored-by: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> --- .../app/observability/llm-call/[id]/page.tsx | 16 +- .../src/app/observability/llm-calls/page.tsx | 31 ++- .../observability/trace/[trace_id]/page.tsx | 75 +++++- .../observability/parsed-response-view.tsx | 223 ++++++++++++++++++ .../src/lib/observability-response-parse.ts | 68 ++++++ 5 files changed, 399 insertions(+), 14 deletions(-) create mode 100644 js/cf-webapp/src/components/observability/parsed-response-view.tsx create mode 100644 js/cf-webapp/src/lib/observability-response-parse.ts diff --git a/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx b/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx index 5af946059..ba0ad4d3e 100644 --- a/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx +++ b/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx @@ -13,6 +13,7 @@ import { prisma } from "@/lib/prisma" import { StatCard } from "@/components/observability/stat-card" import { InfoIcon } from "@/components/observability/info-icon" import { CopyButton } from "@/components/observability/copy-button" +import { ParsedResponseView } from "@/components/observability/parsed-response-view" interface LLMCallDetailPageProps { params: { @@ -398,25 +399,26 @@ export default async function LLMCallDetailPage({ params }: LLMCallDetailPagePro
- {/* Response */} + {/* Response — parsed by call type (ranking: rank/explain; optimization: code blocks + text), with View raw */} {llmCall.raw_response && (

LLM Response

- +
- {llmCall.raw_response?.length.toLocaleString() || 0} characters + {llmCall.raw_response.length.toLocaleString()} characters - +
-
-            {llmCall.raw_response}
-          
+
)} diff --git a/js/cf-webapp/src/app/observability/llm-calls/page.tsx b/js/cf-webapp/src/app/observability/llm-calls/page.tsx index 74e798cb8..966d7b5dd 100644 --- a/js/cf-webapp/src/app/observability/llm-calls/page.tsx +++ b/js/cf-webapp/src/app/observability/llm-calls/page.tsx @@ -1,7 +1,7 @@ import Link from "next/link" import { Metadata } from "next" import { unstable_cache } from "next/cache" -import { Database as DatabaseIcon, Github, Terminal } from "lucide-react" +import { Award, Database as DatabaseIcon, Github, Terminal } from "lucide-react" import { prisma } from "@/lib/prisma" import { getCallSource } from "@/lib/observability-utils" import { HelpButton } from "@/components/observability/help-button" @@ -329,6 +329,7 @@ export default async function LLMCallsPage({ searchParams }: { searchParams: Sea select: { trace_id: true, organization: true, + ranking: true, }, }) : [], @@ -359,6 +360,17 @@ export default async function LLMCallsPage({ searchParams }: { searchParams: Sea } }) + // Trace IDs that have a chosen best candidate (ranking.ranking[0] present) + const traceIdsWithBest = new Set( + optimizationFeatures + .filter( + f => + f.trace_id && + (f.ranking as { ranking?: string[] } | null)?.ranking?.[0], + ) + .map(f => f.trace_id.substring(0, 36)), + ) + // Get unique call types and models for filters const [callTypes, models] = await Promise.all([ prisma.llm_calls.findMany({ @@ -631,6 +643,8 @@ export default async function LLMCallsPage({ searchParams }: { searchParams: Sea const eventType = traceIdPrefix ? traceIdToEventType.get(traceIdPrefix) || null : null const organization = traceIdPrefix ? traceIdToOrganization.get(traceIdPrefix) : null const source = getCallSource(eventType, call.context as Record | null) + const isBestOptimizationCall = + call.call_type === "optimization" && traceIdsWithBest.has(traceIdPrefix) return ( @@ -660,8 +674,19 @@ export default async function LLMCallsPage({ searchParams }: { searchParams: Sea {organization || "N/A"} - - {call.call_type} + + + {call.call_type} + + {isBestOptimizationCall && ( + + + Best + + )} diff --git a/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx b/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx index aac691388..3f9ec4448 100644 --- a/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx +++ b/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx @@ -92,6 +92,27 @@ export default async function TracePage({ params }: TracePageProps) { const candidateExplanations = (optimizationFeatures?.explanations_post as Record) || {} + // Best candidate (first in ranking) and whether it was used for PR + const rankingData = optimizationFeatures?.ranking as + | { ranking?: string[]; explanation?: string } + | null + const bestCandidateId = rankingData?.ranking?.[0] ?? null + const pullRequestRaw = optimizationFeatures?.pull_request + const usedForPr = Boolean( + pullRequestRaw != null && + typeof pullRequestRaw === "object" && + !Array.isArray(pullRequestRaw) && + Object.keys(pullRequestRaw as Record).length > 0, + ) + + // Map candidate ID to rank position (1-based, 1 = best) + const candidateRankMap = new Map() + if (rankingData?.ranking) { + rankingData.ranking.forEach((id, index) => { + candidateRankMap.set(id, index + 1) + }) + } + // Sort by call_sequence from context if available, otherwise by created_at const llmCalls = rawLlmCalls.sort((a, b) => { const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity @@ -481,19 +502,43 @@ export default async function TracePage({ params }: TracePageProps) { />
- {candidatesForType.map(candidate => ( + {candidatesForType.map(candidate => { + const isBest = + bestCandidateId != null && candidate.id === bestCandidateId + const showUsedForPr = isBest && usedForPr + const rank = candidateRankMap.get(candidate.id) + return (
-
+
Candidate {candidate.index} {candidate.id.substring(0, 8)}... + {rank != null && ( + + Rank #{rank} + + )} + {isBest && ( + + Best + + )} + {showUsedForPr && ( + + Used for PR + + )} {candidate.model && ( {candidate.model} @@ -539,7 +584,7 @@ export default async function TracePage({ params }: TracePageProps) { )}
- ))} + )})}
)} @@ -549,6 +594,28 @@ export default async function TracePage({ params }: TracePageProps) {
+ {/* Ranking explanation — shown when ranker ran */} + {rankingData?.explanation && ( +
+
+
+

+ Ranking explanation +

+ +
+
+
+

+ {rankingData.explanation} +

+
+
+ )} + {/* Errors */}
diff --git a/js/cf-webapp/src/components/observability/parsed-response-view.tsx b/js/cf-webapp/src/components/observability/parsed-response-view.tsx new file mode 100644 index 000000000..2de97554f --- /dev/null +++ b/js/cf-webapp/src/components/observability/parsed-response-view.tsx @@ -0,0 +1,223 @@ +"use client" + +import { + extractExplainTag, + extractRankTag, + getResponseContentForParsing, + splitMarkdownCodeBlocks, + type ResponseSegment, +} from "@/lib/observability-response-parse" +import { CopyButton } from "@/components/observability/copy-button" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism" +import { useState } from "react" + +interface ParsedResponseViewProps { + rawResponse: string + callType: string | null +} + +/** Try to parse JSON and return pretty-printed version, or null if not JSON */ +function tryFormatJSON(content: string): string | null { + try { + const parsed = JSON.parse(content) + return JSON.stringify(parsed, null, 2) + } catch { + return null + } +} + +export function ParsedResponseView({ rawResponse, callType }: ParsedResponseViewProps) { + const [showRaw, setShowRaw] = useState(false) + const isRanking = callType === "ranking" + // Use inner message content when raw_response is API JSON (e.g. OpenAI) + const contentForParsing = getResponseContentForParsing(rawResponse) + const rankContent = isRanking ? extractRankTag(contentForParsing) : null + const explainContent = isRanking ? extractExplainTag(contentForParsing) : null + const hasRankingSections = isRanking && (rankContent != null || explainContent != null) + + const segments: ResponseSegment[] = hasRankingSections + ? [] + : splitMarkdownCodeBlocks(contentForParsing) + const hasSegments = segments.length > 0 + + // Check if raw response is JSON (for fallback display) + const formattedJSON = tryFormatJSON(rawResponse) + const isJSON = formattedJSON != null + + return ( +
+ {/* View raw toggle button - always visible in header */} +
+ + +
+ + {showRaw && ( +
+
+            {rawResponse}
+          
+
+ )} + + {!showRaw && ( +
+ {hasRankingSections && ( +
+ {rankContent != null && ( +
+
+

+ Ranking (best first) +

+ +
+
+
    + {rankContent + .split(/[\s,]+/) + .filter(Boolean) + .map((id, i) => { + const pos = i + 1 + const label = + pos === 1 + ? "1st" + : pos === 2 + ? "2nd" + : pos === 3 + ? "3rd" + : `${pos}th` + return ( +
  1. + + {label} + + + {id.trim()} + +
  2. + ) + })} +
+
+
+ )} + {explainContent != null && ( +
+
+

+ Explanation +

+
+
+
+

+ {explainContent} +

+ +
+
+
+ )} +
+ )} + + {!hasRankingSections && hasSegments && ( +
+ {segments.map((seg, i) => { + const textJSON = seg.kind === "text" ? tryFormatJSON(seg.content) : null + return seg.kind === "text" ? ( +
+ {textJSON ? ( + 10} + > + {textJSON} + + ) : ( +
+                        {seg.content}
+                      
+ )} +
+ ) : ( +
+
+ + {seg.language || "code"} + + +
+ 5} + > + {seg.content} + +
+ ) + })} +
+ )} + + {!hasRankingSections && !hasSegments && ( +
+ {isJSON ? ( + 10} + > + {formattedJSON} + + ) : ( +
+                  {rawResponse}
+                
+ )} +
+ )} +
+ )} +
+ ) +} diff --git a/js/cf-webapp/src/lib/observability-response-parse.ts b/js/cf-webapp/src/lib/observability-response-parse.ts new file mode 100644 index 000000000..3e497d77d --- /dev/null +++ b/js/cf-webapp/src/lib/observability-response-parse.ts @@ -0,0 +1,68 @@ +/** + * Helpers to parse LLM raw_response for observability display. + * Ranking responses use and tags; optimization uses markdown code blocks. + * raw_response is often the full API JSON (e.g. OpenAI); we extract message content when present. + */ + +/** Extract message content from OpenAI-style API response JSON, or return null */ +export function extractMessageContentFromApiResponse(raw: string): string | null { + try { + const parsed = JSON.parse(raw) as { + choices?: Array<{ message?: { content?: string } }> + } + const content = parsed?.choices?.[0]?.message?.content + return typeof content === "string" ? content : null + } catch { + return null + } +} + +/** Get the string to use for rank/explain and markdown parsing (inner content if API JSON, else raw) */ +export function getResponseContentForParsing(rawResponse: string): string { + return extractMessageContentFromApiResponse(rawResponse) ?? rawResponse +} + +/** Extract content inside ... */ +export function extractRankTag(content: string): string | null { + const m = content.match(/([\s\S]*?)<\/rank>/i) + return m ? m[1].trim() : null +} + +/** Extract content inside ... */ +export function extractExplainTag(content: string): string | null { + const m = content.match(/([\s\S]*?)<\/explain>/i) + return m ? m[1].trim() : null +} + +export type ResponseSegment = + | { kind: "text"; content: string } + | { kind: "code"; language: string; content: string } + +/** + * Split markdown-like content into text and code blocks (```lang ... ```). + * Language is taken from the first word after ```; default "text". + */ +export function splitMarkdownCodeBlocks(content: string): ResponseSegment[] { + const segments: ResponseSegment[] = [] + const re = /```(\w*)\n?([\s\S]*?)```/g + let lastEnd = 0 + let m: RegExpExecArray | null + while ((m = re.exec(content)) !== null) { + if (m.index > lastEnd) { + const text = content.slice(lastEnd, m.index) + if (text.trim()) { + segments.push({ kind: "text", content: text }) + } + } + const lang = m[1] || "text" + segments.push({ kind: "code", language: lang, content: m[2].trim() }) + lastEnd = re.lastIndex + } + if (lastEnd < content.length) { + const text = content.slice(lastEnd) + if (text.trim()) { + segments.push({ kind: "text", content: text }) + } + } + return segments +} From dcac02b3f2c87bada2721d3a112da13f32c1f525 Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 28 Jan 2026 21:53:51 +0200 Subject: [PATCH 009/184] abstraction --- django/aiservice/languages/__init__.py | 146 +++++++ django/aiservice/languages/base.py | 98 +++++ django/aiservice/languages/js_ts/__init__.py | 204 ++++++++++ .../js_ts/optimizer.py} | 0 .../aiservice/languages/js_ts/optimizer_lp.py | 378 ++++++++++++++++++ .../prompts/optimizer/async_system_prompt.md | 77 ++++ .../prompts/optimizer/async_user_prompt.md | 18 + .../js_ts/prompts/optimizer/system_prompt.md | 53 +++ .../js_ts/prompts/optimizer/user_prompt.md | 3 + .../testgen/execute_async_system_prompt.md | 44 ++ .../testgen/execute_async_user_prompt.md | 55 +++ .../prompts/testgen/execute_system_prompt.md | 35 ++ .../prompts/testgen/execute_user_prompt.md | 45 +++ .../js_ts/testgen.py} | 9 +- django/aiservice/languages/python/__init__.py | 80 ++++ .../aiservice/languages/python/validator.py | 35 ++ django/aiservice/optimizer/optimizer.py | 2 +- .../optimizer/optimizer_line_profiler.py | 340 +--------------- django/aiservice/testgen/testgen.py | 2 +- django/aiservice/tests/languages/__init__.py | 1 + .../tests/languages/test_registry.py | 249 ++++++++++++ 21 files changed, 1531 insertions(+), 343 deletions(-) create mode 100644 django/aiservice/languages/__init__.py create mode 100644 django/aiservice/languages/base.py create mode 100644 django/aiservice/languages/js_ts/__init__.py rename django/aiservice/{optimizer/optimizer_javascript.py => languages/js_ts/optimizer.py} (100%) create mode 100644 django/aiservice/languages/js_ts/optimizer_lp.py create mode 100644 django/aiservice/languages/js_ts/prompts/optimizer/async_system_prompt.md create mode 100644 django/aiservice/languages/js_ts/prompts/optimizer/async_user_prompt.md create mode 100644 django/aiservice/languages/js_ts/prompts/optimizer/system_prompt.md create mode 100644 django/aiservice/languages/js_ts/prompts/optimizer/user_prompt.md create mode 100644 django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md create mode 100644 django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md create mode 100644 django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md create mode 100644 django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md rename django/aiservice/{testgen/testgen_javascript.py => languages/js_ts/testgen.py} (96%) create mode 100644 django/aiservice/languages/python/__init__.py create mode 100644 django/aiservice/languages/python/validator.py create mode 100644 django/aiservice/tests/languages/__init__.py create mode 100644 django/aiservice/tests/languages/test_registry.py diff --git a/django/aiservice/languages/__init__.py b/django/aiservice/languages/__init__.py new file mode 100644 index 000000000..3d241cd9b --- /dev/null +++ b/django/aiservice/languages/__init__.py @@ -0,0 +1,146 @@ +"""Multi-language support registry. + +This module provides a registry for language implementations and factory +functions to retrieve language-specific functionality. + +Usage: + from languages import get_language, register_language + + # Get a language implementation + lang = get_language("python") + validator = lang.get_validator() + is_valid, error = validator.validate_syntax(code) + + # Register a new language (typically done in language module __init__) + @register_language("python") + class PythonLanguage: + ... +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from languages.base import LanguageSupport + +# Registry mapping language identifiers to their implementation classes +_LANGUAGE_REGISTRY: dict[str, type[LanguageSupport]] = {} + +# Aliases for language names (e.g., "js" -> "javascript") +_LANGUAGE_ALIASES: dict[str, str] = { + "js": "javascript", + "ts": "typescript", +} + +# Flag to track whether languages have been loaded +_languages_loaded = False + + +class UnsupportedLanguageError(ValueError): + """Raised when an unsupported language is requested.""" + + def __init__(self, language: str) -> None: + supported = ", ".join(sorted(_LANGUAGE_REGISTRY.keys())) + super().__init__(f"Unsupported language: {language}. Supported languages: {supported}") + self.language = language + + +def register_language(lang_id: str): + """Decorator to register a language implementation. + + Args: + lang_id: The language identifier (e.g., "python", "javascript"). + + Returns: + A decorator that registers the class in the language registry. + + Example: + @register_language("python") + class PythonLanguage: + language = "python" + ... + + """ + + def decorator(cls: type[LanguageSupport]) -> type[LanguageSupport]: + _LANGUAGE_REGISTRY[lang_id] = cls + return cls + + return decorator + + +def _load_languages() -> None: + """Load all language implementations. + + This is called lazily to avoid import issues during module initialization. + """ + # Import Python language support + try: + import languages.python # noqa: F401 + except ImportError: + pass + + # Import JavaScript/TypeScript language support + try: + import languages.js_ts # noqa: F401 + except ImportError: + pass + + +def _ensure_languages_loaded() -> None: + """Ensure language implementations are loaded.""" + global _languages_loaded + if not _languages_loaded: + _load_languages() + _languages_loaded = True + + +def get_language(language: str) -> LanguageSupport: + """Get language support implementation for the given language. + + Args: + language: The language identifier (e.g., "python", "javascript", "js"). + + Returns: + An instance of the language support class. + + Raises: + UnsupportedLanguageError: If the language is not registered. + + """ + _ensure_languages_loaded() + + # Normalize language identifier using aliases + normalized = _LANGUAGE_ALIASES.get(language.lower(), language.lower()) + + if normalized not in _LANGUAGE_REGISTRY: + raise UnsupportedLanguageError(language) + + return _LANGUAGE_REGISTRY[normalized]() + + +def get_supported_languages() -> list[str]: + """Get a list of supported language identifiers. + + Returns: + A sorted list of supported language names. + + """ + _ensure_languages_loaded() + return sorted(_LANGUAGE_REGISTRY.keys()) + + +def is_language_supported(language: str) -> bool: + """Check if a language is supported. + + Args: + language: The language identifier to check. + + Returns: + True if the language is supported, False otherwise. + + """ + _ensure_languages_loaded() + normalized = _LANGUAGE_ALIASES.get(language.lower(), language.lower()) + return normalized in _LANGUAGE_REGISTRY diff --git a/django/aiservice/languages/base.py b/django/aiservice/languages/base.py new file mode 100644 index 000000000..3d4607f85 --- /dev/null +++ b/django/aiservice/languages/base.py @@ -0,0 +1,98 @@ +"""Base protocols and interfaces for multi-language support. + +This module defines the contracts that language implementations must satisfy. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol, runtime_checkable + +if TYPE_CHECKING: + pass + + +@runtime_checkable +class CodeValidator(Protocol): + """Protocol for code syntax validation.""" + + def validate_syntax(self, code: str) -> tuple[bool, str | None]: + """Validate code syntax. + + Args: + code: The source code to validate. + + Returns: + A tuple of (is_valid, error_message). + - is_valid: True if the code is syntactically valid. + - error_message: None if valid, otherwise a description of the error. + + """ + ... + + +@runtime_checkable +class LanguageSupport(Protocol): + """Protocol for language-specific implementations. + + Each supported language should implement this protocol to provide + language-specific functionality like validation and code formatting. + """ + + @property + def language(self) -> str: + """The language identifier (e.g., 'python', 'javascript').""" + ... + + def get_validator(self) -> CodeValidator: + """Get the code validator for this language.""" + ... + + def is_multi_context(self, code: str) -> bool: + """Check if code is in multi-file markdown format. + + Multi-file format uses ```language:filepath blocks. + + Args: + code: The source code to check. + + Returns: + True if the code is in multi-file markdown format. + + """ + ... + + def get_code_block_tag(self) -> str: + """Get the markdown code block tag for this language. + + Returns: + The language tag used in markdown code blocks (e.g., 'python', 'javascript'). + + """ + ... + + def split_markdown_code(self, markdown: str) -> dict[str, str]: + """Split markdown into filepath -> code dict. + + Parses markdown with ```language:filepath blocks and returns + a dictionary mapping file paths to their code content. + + Args: + markdown: The markdown text containing code blocks. + + Returns: + A dictionary mapping file paths to code content. + + """ + ... + + def group_code(self, file_to_code: dict[str, str]) -> str: + """Combine code files into markdown format. + + Args: + file_to_code: A dictionary mapping file paths to code content. + + Returns: + Markdown-formatted string with code blocks for each file. + + """ + ... diff --git a/django/aiservice/languages/js_ts/__init__.py b/django/aiservice/languages/js_ts/__init__.py new file mode 100644 index 000000000..6dff6d52f --- /dev/null +++ b/django/aiservice/languages/js_ts/__init__.py @@ -0,0 +1,204 @@ +"""JavaScript/TypeScript language support for the aiservice. + +This module provides JS/TS-specific implementations for code validation, +multi-file context detection, and code formatting. + +Note: The full implementation will be moved here from optimizer_javascript.py +and testgen_javascript.py in Phase 3. +""" + +from __future__ import annotations + +from aiservice.common.markdown_utils import split_markdown_code as _split_markdown_code + +from languages import register_language + + +class JavaScriptValidator: + """Validator for JavaScript code syntax. + + Note: Currently uses a permissive fallback validation. + Full validation will be implemented in a future PR. + """ + + def validate_syntax(self, code: str) -> tuple[bool, str | None]: + """Validate JavaScript syntax. + + Args: + code: The JavaScript code to validate. + + Returns: + A tuple of (is_valid, error_message). + + Note: + Currently returns (True, None) as a permissive default. + The full validation logic from javascript_validator.py + will be integrated in Phase 3. + + """ + # TODO: Integrate full validation from aiservice/validators/javascript_validator.py + # For now, use permissive validation to match current behavior + return True, None + + +class TypeScriptValidator: + """Validator for TypeScript code syntax. + + Note: Currently uses a permissive fallback validation. + Full validation will be implemented in a future PR. + """ + + def validate_syntax(self, code: str) -> tuple[bool, str | None]: + """Validate TypeScript syntax. + + Args: + code: The TypeScript code to validate. + + Returns: + A tuple of (is_valid, error_message). + + Note: + Currently returns (True, None) as a permissive default. + The full validation logic will be integrated in Phase 3. + + """ + # TODO: Integrate full validation from aiservice/validators/javascript_validator.py + return True, None + + +@register_language("javascript") +class JavaScriptLanguage: + """JavaScript language support implementation.""" + + @property + def language(self) -> str: + """The language identifier.""" + return "javascript" + + def get_validator(self) -> JavaScriptValidator: + """Get the JavaScript code validator.""" + return JavaScriptValidator() + + def is_multi_context(self, code: str) -> bool: + """Check if code is in multi-file markdown format. + + Multi-file JavaScript code starts with ```javascript: or ```js: + followed by a filepath. + + Args: + code: The source code to check. + + Returns: + True if the code is in multi-file markdown format. + + """ + stripped = code.strip() + return stripped.startswith("```javascript:") or stripped.startswith("```js:") + + def get_code_block_tag(self) -> str: + """Get the markdown code block tag for JavaScript. + + Returns: + The string 'javascript' used in markdown code blocks. + + """ + return "javascript" + + def split_markdown_code(self, markdown: str) -> dict[str, str]: + """Split markdown into filepath -> code dict. + + Parses markdown with ```javascript:filepath or ```js:filepath blocks. + + Args: + markdown: The markdown text containing code blocks. + + Returns: + A dictionary mapping file paths to code content. + + """ + return _split_markdown_code(markdown, language="javascript") + + def group_code(self, file_to_code: dict[str, str]) -> str: + """Combine code files into markdown format. + + Args: + file_to_code: A dictionary mapping file paths to code content. + + Returns: + Markdown-formatted string with ```javascript:filepath blocks. + + """ + blocks = [] + for file_path, code in file_to_code.items(): + normalized = code if code.endswith("\n") else code + "\n" + blocks.append(f"```javascript:{file_path}\n{normalized}```") + return "\n".join(blocks) + + +@register_language("typescript") +class TypeScriptLanguage: + """TypeScript language support implementation.""" + + @property + def language(self) -> str: + """The language identifier.""" + return "typescript" + + def get_validator(self) -> TypeScriptValidator: + """Get the TypeScript code validator.""" + return TypeScriptValidator() + + def is_multi_context(self, code: str) -> bool: + """Check if code is in multi-file markdown format. + + Multi-file TypeScript code starts with ```typescript: or ```ts: + followed by a filepath. + + Args: + code: The source code to check. + + Returns: + True if the code is in multi-file markdown format. + + """ + stripped = code.strip() + return stripped.startswith("```typescript:") or stripped.startswith("```ts:") + + def get_code_block_tag(self) -> str: + """Get the markdown code block tag for TypeScript. + + Returns: + The string 'typescript' used in markdown code blocks. + + """ + return "typescript" + + def split_markdown_code(self, markdown: str) -> dict[str, str]: + """Split markdown into filepath -> code dict. + + Parses markdown with ```typescript:filepath or ```ts:filepath blocks. + + Args: + markdown: The markdown text containing code blocks. + + Returns: + A dictionary mapping file paths to code content. + + """ + return _split_markdown_code(markdown, language="typescript") + + def group_code(self, file_to_code: dict[str, str]) -> str: + """Combine code files into markdown format. + + Args: + file_to_code: A dictionary mapping file paths to code content. + + Returns: + Markdown-formatted string with ```typescript:filepath blocks. + + """ + blocks = [] + for file_path, code in file_to_code.items(): + normalized = code if code.endswith("\n") else code + "\n" + blocks.append(f"```typescript:{file_path}\n{normalized}```") + return "\n".join(blocks) diff --git a/django/aiservice/optimizer/optimizer_javascript.py b/django/aiservice/languages/js_ts/optimizer.py similarity index 100% rename from django/aiservice/optimizer/optimizer_javascript.py rename to django/aiservice/languages/js_ts/optimizer.py diff --git a/django/aiservice/languages/js_ts/optimizer_lp.py b/django/aiservice/languages/js_ts/optimizer_lp.py new file mode 100644 index 000000000..100cc60fe --- /dev/null +++ b/django/aiservice/languages/js_ts/optimizer_lp.py @@ -0,0 +1,378 @@ +"""JavaScript/TypeScript line profiler optimizer module. + +This module handles line profiler-guided optimization for JavaScript and TypeScript code. +""" + +from __future__ import annotations + +import asyncio +import logging +import re +import uuid +from pathlib import Path +from typing import TYPE_CHECKING + +import sentry_sdk +from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam + +from aiservice.analytics.posthog import ph +from aiservice.env_specific import debug_log_sensitive_data +from aiservice.llm import OPTIMIZE_MODEL, calculate_llm_cost, call_llm +from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax +from optimizer.config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution +from optimizer.context_utils.context_helpers import ( + group_code, + is_multi_context_js, + is_multi_context_ts, + split_markdown_code, +) +from optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema + +if TYPE_CHECKING: + from openai.types.chat import ChatCompletionMessageParam + + from aiservice.llm import LLM + + +# Get the prompts directory +current_dir = Path(__file__).parent +JS_PROMPTS_DIR = current_dir / "prompts" / "optimizer" + +# Fallback to original location if prompts haven't been moved +if not JS_PROMPTS_DIR.exists(): + JS_PROMPTS_DIR = Path(__file__).parent.parent.parent / "optimizer" / "prompts" / "javascript" + +# Load JavaScript system prompt +JS_SYSTEM_PROMPT = JS_PROMPTS_DIR / "system_prompt.md" +if JS_SYSTEM_PROMPT.exists(): + JS_SYSTEM_PROMPT_TEXT = JS_SYSTEM_PROMPT.read_text() +else: + # Fallback for backwards compatibility + JS_SYSTEM_PROMPT_TEXT = "" + +# Pattern to extract code blocks from JavaScript LLM response (single file, no file path) +JS_CODE_PATTERN = re.compile(r"```(?:javascript|js|typescript|ts)\s*\n(.*?)```", re.MULTILINE | re.DOTALL) + +# Pattern to extract code blocks with file paths (multi-file context) +JS_CODE_WITH_PATH_PATTERN = re.compile( + r"```(?:javascript|js|typescript|ts):([^\n]+)\n(.*?)```", re.MULTILINE | re.DOTALL +) + +# Line profiler context prompt for JavaScript +JS_LINE_PROF_CONTEXT = """ +Here are the results of the line profiling of the JavaScript/TypeScript code you will be optimizing. +The profiling data shows: +- Line numbers with execution counts (hits) +- Time spent on each line (in milliseconds) +- Percentage of total time per line + +Use this data to identify performance bottlenecks and focus your optimization on the hottest code paths. + +{line_profiler_results} +""" + + +def extract_js_code_and_explanation(content: str, is_multi_file: bool = False) -> tuple[str | dict[str, str], str]: + """Extract JavaScript code and explanation from LLM response. + + Args: + content: The raw LLM response content + is_multi_file: Whether to expect multi-file format + + Returns: + Tuple of (code, explanation) where code is a string for single file + or dict[str, str] for multi-file + + """ + if is_multi_file: + # Extract all code blocks with file paths + matches = JS_CODE_WITH_PATH_PATTERN.findall(content) + if matches: + file_to_code: dict[str, str] = {} + first_match_pos = content.find("```") + explanation = content[:first_match_pos].strip() if first_match_pos > 0 else "" + + for file_path, code in matches: + file_to_code[file_path.strip()] = code.strip() + + return file_to_code, explanation + + # Fall back to single file extraction + return extract_js_code_and_explanation(content, is_multi_file=False) + + # Single file extraction + match = JS_CODE_PATTERN.search(content) + if match: + code = match.group(1).strip() + explanation_end = match.start() + explanation = content[:explanation_end].strip() + return code, explanation + + return "", content + + +def normalize_js_code(code: str) -> str: + """Normalize JavaScript code for comparison.""" + # Remove single-line comments + code = re.sub(r"//.*$", "", code, flags=re.MULTILINE) + # Remove multi-line comments + code = re.sub(r"/\*.*?\*/", "", code, flags=re.DOTALL) + # Normalize whitespace + code = " ".join(code.split()) + return code + + +async def optimize_javascript_code_line_profiler_single( + user_id: str, + trace_id: str, + source_code: str, + line_profiler_results: str, + dependency_code: str | None = None, + optimize_model: LLM = OPTIMIZE_MODEL, + language_version: str = "ES2022", + language: str = "javascript", + call_sequence: int | None = None, +) -> tuple[OptimizeResponseItemSchema | None, float | None, str]: + """Optimize JavaScript/TypeScript code using LLMs with line profiler guidance.""" + lang_name = "TypeScript" if language == "typescript" else "JavaScript" + code_block_tag = "typescript" if language == "typescript" else "javascript" + logging.info(f"/optimize-line-profiler: Optimizing {lang_name} code.") + debug_log_sensitive_data(f"Optimizing {lang_name} code for user {user_id}:\n{source_code}") + + # Check if source code is multi-file format + is_multi_file = is_multi_context_ts(source_code) if language == "typescript" else is_multi_context_js(source_code) + original_file_to_code: dict[str, str] = {} + + if is_multi_file: + original_file_to_code = split_markdown_code(source_code, language) + logging.info( + f"Multi-file context detected with {len(original_file_to_code)} files: {list(original_file_to_code.keys())}" + ) + + # Format system prompt with language version + system_prompt = JS_SYSTEM_PROMPT_TEXT.format(language_version=language_version) + + # Build user prompt with line profiler results + if is_multi_file: + # For multi-file, identify the first file as the target and others as helper context + file_paths = list(original_file_to_code.keys()) + target_file = file_paths[0] if file_paths else "main file" + helper_files = file_paths[1:] if len(file_paths) > 1 else [] + + # Build multi-file instructions + helper_notice = "" + if helper_files: + helper_list = ", ".join(f"`{f}`" for f in helper_files) + helper_notice = f""" +HELPER FILES: {helper_list} +These files contain helper functions that the target function uses. You may optimize these as well if needed. +""" + + multi_file_instructions = f""" +The code is provided in a multi-file format. Each file is wrapped in a code block with its path. + +TARGET FILE: `{target_file}` +{helper_notice} +Output the optimized code for each file that you modify. Wrap each file's code in: +```{code_block_tag}: + +``` + +You MUST output the target file. You may also output helper files if you optimize them. +""" + system_prompt = system_prompt + "\n" + multi_file_instructions + + user_prompt = f"""Optimize the following {lang_name} code for better performance. + +{JS_LINE_PROF_CONTEXT.format(line_profiler_results=line_profiler_results)} + +Here is the code to optimize: +{source_code} +""" + else: + user_prompt = f"""Optimize the following {lang_name} code for better performance. + +{JS_LINE_PROF_CONTEXT.format(line_profiler_results=line_profiler_results)} + +Here is the code to optimize: +```{code_block_tag} +{source_code} +``` +""" + + if dependency_code: + user_prompt = f"Dependencies (read-only):\n```{code_block_tag}\n{dependency_code}\n```\n\n{user_prompt}" + + obs_context: dict = {} + if call_sequence is not None: + obs_context["call_sequence"] = call_sequence + + messages: list[ChatCompletionMessageParam] = [ + ChatCompletionSystemMessageParam(role="system", content=system_prompt), + ChatCompletionUserMessageParam(role="user", content=user_prompt), + ] + + try: + output = await call_llm( + llm=optimize_model, + messages=messages, + call_type="line_profiler", + trace_id=trace_id, + user_id=user_id, + python_version=language_version, # Reusing python_version field for language version + context=obs_context, + ) + except Exception as e: + logging.exception(f"LLM Code Generation error in {lang_name} line profiler optimizer") + sentry_sdk.capture_exception(e) + debug_log_sensitive_data(f"Failed to generate code for source:\n{source_code}") + return None, None, optimize_model.name + + llm_cost = calculate_llm_cost(output.raw_response, optimize_model) + + debug_log_sensitive_data(f"LLM optimization response:\n{output.raw_response.model_dump_json(indent=2)}") + + if output.raw_response.usage is not None: + ph( + user_id, + "aiservice-optimize-line-profiler-openai-usage", + properties={"model": optimize_model.name, "usage": output.raw_response.usage.json(), "language": language}, + ) + + # Extract code and explanation from response + extracted_code, explanation = extract_js_code_and_explanation(output.content, is_multi_file=is_multi_file) + + if not extracted_code: + sentry_sdk.capture_message(f"No code block found in {lang_name} line profiler optimization response") + debug_log_sensitive_data(f"No code found in response for source:\n{source_code}") + return None, llm_cost, optimize_model.name + + optimization_id = str(uuid.uuid4()) + + if is_multi_file and isinstance(extracted_code, dict): + # Handle multi-file response + # LLM can optimize both target and helper files + merged_file_to_code: dict[str, str] = {} + has_changes = False + + for file_path, original_code in original_file_to_code.items(): + if file_path in extracted_code: + new_code = extracted_code[file_path] + + # Validate the new code + if language == "typescript": + is_valid, error = validate_typescript_syntax(new_code) + else: + is_valid, error = validate_javascript_syntax(new_code) + + if not is_valid: + sentry_sdk.capture_message(f"Invalid {lang_name} generated for {file_path}: {error}") + debug_log_sensitive_data(f"Invalid code generated for {file_path}:\n{new_code}\nError: {error}") + # Keep original code for this file + merged_file_to_code[file_path] = original_code + else: + merged_file_to_code[file_path] = new_code + if normalize_js_code(new_code) != normalize_js_code(original_code): + has_changes = True + else: + # File not in response, keep original + merged_file_to_code[file_path] = original_code + + if not has_changes: + debug_log_sensitive_data("Generated code identical to original (multi-file)") + return None, llm_cost, optimize_model.name + + # Format as multi-file markdown + wrapped_code = group_code(merged_file_to_code, language=code_block_tag) + + result = OptimizeResponseItemSchema( + source_code=wrapped_code, explanation=explanation, optimization_id=optimization_id + ) + return result, llm_cost, optimize_model.name + + # Single file handling + optimized_code = extracted_code if isinstance(extracted_code, str) else "" + + if not optimized_code: + return None, llm_cost, optimize_model.name + + # Validate the generated code + if language == "typescript": + is_valid, error = validate_typescript_syntax(optimized_code) + else: + is_valid, error = validate_javascript_syntax(optimized_code) + + if not is_valid: + sentry_sdk.capture_message(f"Invalid {lang_name} generated: {error}") + debug_log_sensitive_data(f"Invalid code generated:\n{optimized_code}\nError: {error}") + return None, llm_cost, optimize_model.name + + # Check that the code is actually different from the original + if normalize_js_code(optimized_code) == normalize_js_code(source_code): + debug_log_sensitive_data("Generated code identical to original") + return None, llm_cost, optimize_model.name + + # Wrap code in markdown format for CLI parsing + wrapped_code = ( + f"```{code_block_tag}\n{optimized_code}\n```" + if not optimized_code.endswith("\n") + else f"```{code_block_tag}\n{optimized_code}```" + ) + result = OptimizeResponseItemSchema( + source_code=wrapped_code, explanation=explanation, optimization_id=optimization_id + ) + + return result, llm_cost, optimize_model.name + + +async def optimize_javascript_code_line_profiler( + user_id: str, + trace_id: str, + source_code: str, + line_profiler_results: str, + dependency_code: str | None = None, + language_version: str = "ES2022", + language: str = "javascript", + n_candidates: int = 0, +) -> tuple[list[OptimizeResponseItemSchema], float, dict[str, str]]: + """Run parallel JavaScript line profiler optimizations with multiple models.""" + if n_candidates == 0: + return [], 0.0, {} + + model_distribution = get_model_distribution(n_candidates, MAX_OPTIMIZER_LP_CALLS) + tasks: list[asyncio.Task[tuple[OptimizeResponseItemSchema | None, float | None, str]]] = [] + call_sequence = 1 + + async with asyncio.TaskGroup() as tg: + for model, num_calls in model_distribution: + for _ in range(num_calls): + task = tg.create_task( + optimize_javascript_code_line_profiler_single( + user_id=user_id, + trace_id=trace_id, + source_code=source_code, + line_profiler_results=line_profiler_results, + dependency_code=dependency_code, + optimize_model=model, + language_version=language_version, + language=language, + call_sequence=call_sequence, + ) + ) + tasks.append(task) + call_sequence += 1 + + # Collect results + optimization_results: list[OptimizeResponseItemSchema] = [] + total_cost = 0.0 + optimization_models: dict[str, str] = {} + + for task in tasks: + result, cost, model_name = task.result() + if cost: + total_cost += cost + if result is not None: + optimization_results.append(result) + optimization_models[result.optimization_id] = model_name + + return optimization_results, total_cost, optimization_models diff --git a/django/aiservice/languages/js_ts/prompts/optimizer/async_system_prompt.md b/django/aiservice/languages/js_ts/prompts/optimizer/async_system_prompt.md new file mode 100644 index 000000000..304796d73 --- /dev/null +++ b/django/aiservice/languages/js_ts/prompts/optimizer/async_system_prompt.md @@ -0,0 +1,77 @@ +You are a professional computer programmer who specializes in writing high-performance **asynchronous** JavaScript/TypeScript code. Your goal is to optimize the runtime and memory efficiency of the provided **async** code through safe and meaningful rewrites that would pass senior-level code review. + +**CRITICAL: ASYNC CODE REQUIREMENTS** +- The code contains **async functions** that must remain async +- ALL async functions must maintain their `async function` or `async () =>` signature +- ALL `await` expressions must be preserved where they exist +- Do NOT convert async functions to synchronous functions +- Do NOT remove `await` keywords unless replacing with functionally equivalent async operations +- Preserve Promise chains and async/await flow +- Maintain proper async error handling in async contexts + +**Behavioral Preservation (CRITICAL)** +- Do NOT rename functions or change their signatures. +- You MUST NOT change the behavior, return values, side effects, console output, or thrown errors - they MUST remain exactly the same. +- Do NOT mutate inputs in a different way than the original implementation. +- The same error types should be thrown in the same circumstances. +- Preserve existing type annotations (for TypeScript) - all function parameters, return types, and variable annotations must be preserved exactly as written. +- **Preserve the original code style**: Keep existing variable names unless the logic fundamentally changes +- Preserve ALL existing comments exactly as written, unless the corresponding code logic is changed or the comment becomes factually incorrect +- Avoid excessive inline comments - only add new comments for significant or non-obvious logic changes +- Preserve the export structure - exported functions/classes must remain exported + +**Async-Specific Optimization Focus** +- Use `Promise.all()` for concurrent execution of independent async operations +- Use `Promise.allSettled()` when you need results regardless of individual failures +- Use `Promise.race()` for timeout patterns or first-to-complete scenarios +- Batch async operations instead of executing them one-by-one in a loop +- Consider using `for await...of` for async iterables when appropriate +- Optimize async I/O operations and resource management +- Use streaming APIs instead of loading entire datasets into memory +- Consider worker threads for CPU-intensive tasks that would block the event loop + +**Code Style & Structure** +- Keep existing ES module syntax (`import`/`export`) or CommonJS (`require`/`module.exports`) as-is +- You may write new async helper functions that do not already exist in the codebase. +- Avoid purely stylistic changes unless they result in noticeable performance improvements +- Ensure all new async code follows proper async patterns and conventions +- Maintain consistent code formatting + +**Optimization Strategies** +- Replace sequential awaits with parallel execution when operations are independent: + ```javascript + // Before (sequential) + const a = await fetchA(); + const b = await fetchB(); + + // After (parallel) + const [a, b] = await Promise.all([fetchA(), fetchB()]); + ``` +- Use chunked/batched processing for large async operations +- Implement proper backpressure handling for streams +- Cache async results when appropriate using memoization + +**Optimization Focus** +- Create production-ready async code that professional programmers would merge without further edits +- Prioritize changes that provide measurable runtime or memory efficiency gains in async contexts +- Consider async-specific performance patterns like batching operations or reducing context switching + +**Code Quality Standards** +- Ensure all async optimizations are safe and would pass senior-level code review +- Maintain code readability and maintainability alongside performance improvements +- Verify that async operations are properly awaited and handled + +**Response Format (REQUIRED)** +- ALWAYS start your response with a brief explanation (2-4 sentences) of what optimization you made and why it improves performance +- Then provide the optimized code in a markdown code block +- Example format: + ``` + **Optimization Explanation:** + [Your explanation here describing the optimization technique and expected performance improvement] + + ```javascript:filename.js + [optimized code] + ``` + ``` + +The target JavaScript/TypeScript version is {language_version} diff --git a/django/aiservice/languages/js_ts/prompts/optimizer/async_user_prompt.md b/django/aiservice/languages/js_ts/prompts/optimizer/async_user_prompt.md new file mode 100644 index 000000000..31a307ccc --- /dev/null +++ b/django/aiservice/languages/js_ts/prompts/optimizer/async_user_prompt.md @@ -0,0 +1,18 @@ +Rewrite this **asynchronous** JavaScript/TypeScript program to run faster while preserving all async behavior. + +**CRITICAL ASYNC REQUIREMENTS:** +- The code contains **async functions** - you MUST keep them async +- ALL `async function` signatures must be preserved exactly +- ALL `await` expressions must be maintained (unless replaced with functionally equivalent async operations) +- Do NOT convert async functions to synchronous functions +- Preserve concurrent execution patterns and Promise handling +- Maintain proper async/await flow and exception handling + +**Async Optimization Guidelines:** +- Consider using `Promise.all()` for concurrent execution when beneficial +- Batch independent async operations instead of sequential awaits +- Optimize async I/O operations and use streaming where appropriate +- Use worker threads for CPU-intensive tasks to avoid blocking the event loop +- Implement proper error handling in async contexts + +{source_code} diff --git a/django/aiservice/languages/js_ts/prompts/optimizer/system_prompt.md b/django/aiservice/languages/js_ts/prompts/optimizer/system_prompt.md new file mode 100644 index 000000000..cee7890a5 --- /dev/null +++ b/django/aiservice/languages/js_ts/prompts/optimizer/system_prompt.md @@ -0,0 +1,53 @@ +You are a professional computer programmer who specializes in writing high-performance JavaScript/TypeScript code. Your goal is to optimize the runtime and memory efficiency of the provided code through safe and meaningful rewrites that would pass senior-level code review. + +**Behavioral Preservation (CRITICAL)** +- Do NOT rename functions or change their signatures. +- You MUST NOT change the behavior, return values, side effects, console output, or thrown errors - they MUST remain exactly the same. +- Do NOT mutate inputs in a different way than the original implementation. +- The same error types should be thrown in the same circumstances. +- Preserve existing type annotations (for TypeScript) - all function parameters, return types, and variable annotations must be preserved exactly as written. +- **Preserve the original code style**: Keep existing variable names unless the logic fundamentally changes +- Preserve ALL existing comments exactly as written, unless the corresponding code logic is changed or the comment becomes factually incorrect +- Avoid excessive inline comments - only add new comments for significant or non-obvious logic changes +- Preserve the export structure - exported functions/classes must remain exported + +**Code Style & Structure** +- Keep existing ES module syntax (`import`/`export`) or CommonJS (`require`/`module.exports`) as-is +- You may write new helper functions that do not already exist in the codebase. +- Avoid purely stylistic changes unless they result in noticeable performance improvements +- Maintain consistent code formatting + +**Optimization Strategies** +- Replace O(n^2) algorithms with O(n) or O(n log n) alternatives +- Use TypedArrays (Float64Array, Int32Array, etc.) instead of regular arrays for numeric-heavy operations +- Use Map/Set instead of Object for frequent lookups +- Minimize object allocations in hot paths (avoid creating temporary objects in loops) +- Use for loops instead of forEach/map/filter for performance-critical code when the functional style adds overhead +- Cache array lengths in tight loops: `for (let i = 0, len = arr.length; i < len; i++)` +- Leverage V8 optimization hints (keep functions monomorphic, avoid hidden class changes) +- Avoid try-catch in hot loops (move error handling outside the loop when possible) +- Use string concatenation with template literals or array join for building large strings +- Consider using WeakMap/WeakSet for caching to avoid memory leaks + +**Optimization Focus** +- Create production-ready code that professional programmers would merge without further edits +- Prioritize changes that provide measurable runtime or memory efficiency gains + +**Code Quality Standards** +- Ensure all optimizations are safe and would pass senior-level code review +- Maintain code readability and maintainability alongside performance improvements + +**Response Format (REQUIRED)** +- ALWAYS start your response with a brief explanation (2-4 sentences) of what optimization you made and why it improves performance +- Then provide the optimized code in a markdown code block +- Example format: + ``` + **Optimization Explanation:** + [Your explanation here describing the optimization technique and expected performance improvement] + + ```javascript:filename.js + [optimized code] + ``` + ``` + +The target JavaScript/TypeScript version is {language_version} diff --git a/django/aiservice/languages/js_ts/prompts/optimizer/user_prompt.md b/django/aiservice/languages/js_ts/prompts/optimizer/user_prompt.md new file mode 100644 index 000000000..497d60aff --- /dev/null +++ b/django/aiservice/languages/js_ts/prompts/optimizer/user_prompt.md @@ -0,0 +1,3 @@ +Rewrite this JavaScript/TypeScript program to run faster. + +{source_code} diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md new file mode 100644 index 000000000..42ec28680 --- /dev/null +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md @@ -0,0 +1,44 @@ +**Role**: You are Codeflash, a world-class JavaScript/TypeScript developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests for **asynchronous** code using Jest. When asked to reply only with code, you write all of your code in a single markdown code block. + +**Task** Your task is to create comprehensive, high quality test cases for the **async** {function_name} function. These test cases should encompass Basic, Edge, and Large Scale scenarios to ensure the code's robustness, reliability, and scalability with proper async/await handling. + +**CRITICAL: ASYNC TEST REQUIREMENTS** +- The function under test is **asynchronous** - all tests must handle async properly +- Use `async/await` syntax in test functions +- Ensure all promises are awaited +- Test both successful resolution and rejection scenarios +- Handle async timeouts appropriately + +**1. Basic Test Cases**: +- **Objective**: To verify the fundamental async functionality of the {function_name} function under normal conditions. + +**2. Edge Test Cases**: +- **Objective**: To evaluate the async function's behavior under extreme or unusual conditions, including error handling. + +**3. Large Scale Test Cases**: +- **Objective**: To assess the async function's performance and scalability with concurrent operations and large data samples. + +**Instructions**: +- Implement a comprehensive set of test cases following the guidelines above. +- Use Jest testing framework with `describe`, `test`, and `expect`. +- **ALL test functions must be async**: `test('...', async () => {{ ... }})` +- **ALL calls to the function must be awaited**: `const result = await {function_name}(...)` +- Ensure each test case is well-documented with comments explaining the scenario it covers. +- Pay special attention to edge cases including async error handling. +- For large-scale tests, consider concurrent execution with `Promise.all()`. +- Avoid loops exceeding 1000 iterations, and keep data structures under 1000 elements. +- **CRITICAL: DO NOT MOCK THE FUNCTION UNDER TEST** - Never mock, stub, or spy on the {function_name} function itself. +- **CRITICAL: TEST REJECTION CASES** - Use `expect(...).rejects.toThrow()` for testing async errors. + +**CRITICAL: MOCKING RULES FOR JEST**: +- **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. +- **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. +- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics.js')` ✓ +- **ALWAYS include the `.js` extension** in mock paths when the project uses ESM imports. + + +**Output Format Requirements**: +- Your response MUST be a single markdown code block containing valid JavaScript/TypeScript code. +- Do NOT nest code blocks inside each other. +- The code block MUST contain at least one async test using `test('...', async () => ...)`. +- Follow the exact template structure provided in the user message. diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md new file mode 100644 index 000000000..84cb42963 --- /dev/null +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md @@ -0,0 +1,55 @@ +Using the {test_framework} testing framework, write a test suite for the following **ASYNC** JavaScript function. + +**CRITICAL: This function is ASYNCHRONOUS** +- All test functions MUST be async: `test('...', async () => {{ ... }})` +- All calls to {function_name} MUST be awaited: `await {function_name}(...)` +- Test both successful and error cases for async operations + +**Function to Test:** +```javascript +{function_code} +``` + +**CRITICAL: Use this exact import statement (do not modify the path):** +```javascript +const {{ {function_name} }} = require('{module_path}'); +``` + +**Template to Follow:** +```javascript +// imports +const {{ {function_name} }} = require('{module_path}'); + +// unit tests +describe('{function_name}', () => {{ + // Basic Test Cases + describe('Basic async functionality', () => {{ + test('should resolve with correct value', async () => {{ + const result = await {function_name}(/* args */); + expect(result).toBe(/* expected */); + }}); + }}); + + // Edge Test Cases + describe('Async edge cases', () => {{ + test('should handle async error case', async () => {{ + await expect({function_name}(/* invalid args */)).rejects.toThrow(); + }}); + }}); + + // Large Scale Test Cases + describe('Concurrent execution tests', () => {{ + test('should handle multiple concurrent calls', async () => {{ + const results = await Promise.all([ + {function_name}(/* args1 */), + {function_name}(/* args2 */), + ]); + // assertions + }}); + }}); +}}); +``` + +{package_comment} + +Reply only with code, in a single markdown code block. diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md new file mode 100644 index 000000000..e1d42c1dd --- /dev/null +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md @@ -0,0 +1,35 @@ +**Role**: You are Codeflash, a world-class JavaScript/TypeScript developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests using Jest. When asked to reply only with code, you write all of your code in a single markdown code block. + +**Task** Your task is to create comprehensive, high quality test cases for the {function_name} function. These test cases should encompass Basic, Edge, and Large Scale scenarios to ensure the code's robustness, reliability, and scalability. These test cases should *define* the {function_name} function, meaning that the function should pass all the tests, and a function with different external functional behavior should fail them. + +**1. Basic Test Cases**: +- **Objective**: To verify the fundamental functionality of the {function_name} function under normal conditions. + +**2. Edge Test Cases**: +- **Objective**: To evaluate the function's behavior under extreme or unusual conditions. + +**3. Large Scale Test Cases**: +- **Objective**: To assess the function's performance and scalability with large data samples. + +**Instructions**: +- Implement a comprehensive set of test cases following the guidelines above. +- Use Jest testing framework with `describe`, `test`, and `expect`. +- Ensure each test case is well-documented with comments explaining the scenario it covers. +- Pay special attention to edge cases as they often reveal hidden bugs. +- For large-scale tests, focus on the function's efficiency and performance under heavy loads. Avoid loops exceeding 1000 iterations, and keep data structures under 1000 elements. +- **CRITICAL: DO NOT MOCK THE FUNCTION UNDER TEST** - Never mock, stub, or spy on the {function_name} function itself. You may mock external dependencies (APIs, databases, network calls, file I/O, etc.) if necessary, but the function being tested must execute with its real implementation. +- **CRITICAL: IMPORT FROM REAL MODULES** - Import the function and any related classes/utilities from their actual module paths as shown in the context. +- **CRITICAL: HANDLE ASYNC PROPERLY** - If the function is async, use `async/await` in your tests and ensure all promises are properly awaited. + +**CRITICAL: MOCKING RULES FOR JEST**: +- **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. +- **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. +- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics.js')` ✓ +- **ALWAYS include the `.js` extension** in mock paths when the project uses ESM imports. + + +**Output Format Requirements**: +- Your response MUST be a single markdown code block containing valid JavaScript/TypeScript code. +- Do NOT nest code blocks inside each other. +- The code block MUST contain at least one test using `test()` or `it()`. +- Follow the exact template structure provided in the user message. diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md new file mode 100644 index 000000000..c9ef67ad3 --- /dev/null +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md @@ -0,0 +1,45 @@ +Using the {test_framework} testing framework, write a test suite for the following JavaScript function. + +**Function to Test:** +```javascript +{function_code} +``` + +**CRITICAL: Use this exact import statement (do not modify the path):** +```javascript +const {{ {function_name} }} = require('{module_path}'); +``` + +**Template to Follow:** +```javascript +// imports +const {{ {function_name} }} = require('{module_path}'); + +// unit tests +describe('{function_name}', () => {{ + // Basic Test Cases + describe('Basic functionality', () => {{ + test('should handle normal input', () => {{ + // Test implementation + }}); + }}); + + // Edge Test Cases + describe('Edge cases', () => {{ + test('should handle edge case', () => {{ + // Test implementation + }}); + }}); + + // Large Scale Test Cases + describe('Performance tests', () => {{ + test('should handle large inputs efficiently', () => {{ + // Test implementation + }}); + }}); +}}); +``` + +{package_comment} + +Reply only with code, in a single markdown code block. diff --git a/django/aiservice/testgen/testgen_javascript.py b/django/aiservice/languages/js_ts/testgen.py similarity index 96% rename from django/aiservice/testgen/testgen_javascript.py rename to django/aiservice/languages/js_ts/testgen.py index 58e639c9c..201f298de 100644 --- a/django/aiservice/testgen/testgen_javascript.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -32,9 +32,14 @@ if TYPE_CHECKING: _TEST_FUNC_RE = re.compile(r"(?:test|it)\s*\(\s*['\"]") -# Get the directory of the current file +# Get the directory of the current file - prompts are now in languages/js_ts/prompts/testgen/ current_dir = Path(__file__).parent -JS_PROMPTS_DIR = current_dir / "prompts" / "javascript" +JS_PROMPTS_DIR = current_dir / "prompts" / "testgen" + +# Fallback to original location if prompts haven't been moved yet +if not JS_PROMPTS_DIR.exists(): + # Use original location for backward compatibility during migration + JS_PROMPTS_DIR = Path(__file__).parent.parent.parent / "testgen" / "prompts" / "javascript" # Load JavaScript prompts JS_EXECUTE_SYSTEM_PROMPT = (JS_PROMPTS_DIR / "execute_system_prompt.md").read_text() diff --git a/django/aiservice/languages/python/__init__.py b/django/aiservice/languages/python/__init__.py new file mode 100644 index 000000000..b76a449ea --- /dev/null +++ b/django/aiservice/languages/python/__init__.py @@ -0,0 +1,80 @@ +"""Python language support for the aiservice. + +This module provides Python-specific implementations for code validation, +multi-file context detection, and code formatting. +""" + +from __future__ import annotations + +from aiservice.common.markdown_utils import split_markdown_code as _split_markdown_code +from languages import register_language +from languages.python.validator import PythonValidator + + +@register_language("python") +class PythonLanguage: + """Python language support implementation.""" + + @property + def language(self) -> str: + """The language identifier.""" + return "python" + + def get_validator(self) -> PythonValidator: + """Get the Python code validator.""" + return PythonValidator() + + def is_multi_context(self, code: str) -> bool: + """Check if code is in multi-file markdown format. + + Multi-file Python code starts with ```python: followed by a filepath. + + Args: + code: The source code to check. + + Returns: + True if the code is in multi-file markdown format. + + """ + return code.strip().startswith("```python:") + + def get_code_block_tag(self) -> str: + """Get the markdown code block tag for Python. + + Returns: + The string 'python' used in markdown code blocks. + + """ + return "python" + + def split_markdown_code(self, markdown: str) -> dict[str, str]: + """Split markdown into filepath -> code dict. + + Parses markdown with ```python:filepath blocks and returns + a dictionary mapping file paths to their code content. + + Args: + markdown: The markdown text containing code blocks. + + Returns: + A dictionary mapping file paths to code content. + + """ + return _split_markdown_code(markdown, language="python") + + def group_code(self, file_to_code: dict[str, str]) -> str: + """Combine code files into markdown format. + + Args: + file_to_code: A dictionary mapping file paths to code content. + + Returns: + Markdown-formatted string with ```python:filepath blocks. + + """ + blocks = [] + for file_path, code in file_to_code.items(): + # Ensure code ends with newline before closing ``` + normalized = code if code.endswith("\n") else code + "\n" + blocks.append(f"```python:{file_path}\n{normalized}```") + return "\n".join(blocks) diff --git a/django/aiservice/languages/python/validator.py b/django/aiservice/languages/python/validator.py new file mode 100644 index 000000000..9837abadc --- /dev/null +++ b/django/aiservice/languages/python/validator.py @@ -0,0 +1,35 @@ +"""Python code syntax validation. + +This module provides a validator that uses libcst for Python syntax validation. +""" + +from __future__ import annotations + +import libcst as cst + +from aiservice.common.cst_utils import parse_module_to_cst + + +class PythonValidator: + """Validator for Python code syntax using libcst.""" + + def validate_syntax(self, code: str) -> tuple[bool, str | None]: + """Validate Python syntax using libcst. + + Args: + code: The Python code to validate. + + Returns: + A tuple of (is_valid, error_message). + - is_valid: True if the code is syntactically valid Python. + - error_message: None if valid, otherwise the parse error message. + + """ + try: + parse_module_to_cst(code) + return True, None + except cst.ParserSyntaxError as e: + return False, str(e) + except Exception as e: + # Catch any other parsing errors + return False, f"Parse error: {e}" diff --git a/django/aiservice/optimizer/optimizer.py b/django/aiservice/optimizer/optimizer.py index 0a9584a91..a75e9a296 100644 --- a/django/aiservice/optimizer/optimizer.py +++ b/django/aiservice/optimizer/optimizer.py @@ -31,7 +31,7 @@ from optimizer.context_utils.optimizer_context import ( ) from optimizer.diff_patches_utils.diff import DiffMethod from optimizer.models import OptimizedCandidateSource, OptimizeSchema -from optimizer.optimizer_javascript import optimize_javascript +from languages.js_ts.optimizer import optimize_javascript if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/optimizer/optimizer_line_profiler.py b/django/aiservice/optimizer/optimizer_line_profiler.py index 68675e164..492012c11 100644 --- a/django/aiservice/optimizer/optimizer_line_profiler.py +++ b/django/aiservice/optimizer/optimizer_line_profiler.py @@ -2,8 +2,6 @@ from __future__ import annotations import asyncio import logging -import re -import uuid from pathlib import Path from typing import TYPE_CHECKING @@ -16,11 +14,11 @@ from aiservice.common_utils import parse_python_version, validate_trace_id from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable from aiservice.llm import OPTIMIZE_MODEL, calculate_llm_cost, call_llm from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax +from languages.js_ts.optimizer_lp import optimize_javascript_code_line_profiler from log_features.log_event import update_optimization_cost from log_features.log_features import log_features from optimizer.config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution from optimizer.context_utils.context_helpers import ( - group_code, is_multi_context_js, is_multi_context_ts, split_markdown_code, @@ -49,81 +47,6 @@ SYSTEM_PROMPT = (current_dir / "system_prompt.md").read_text() USER_PROMPT = (current_dir / "user_prompt.md").read_text() JIT_INSTRUCTIONS = (current_dir / "jit_instructions.md").read_text() -# JavaScript/TypeScript prompts -JS_SYSTEM_PROMPT = (current_dir / "prompts" / "javascript" / "system_prompt.md").read_text() - -# Pattern to extract code blocks from JavaScript LLM response (single file, no file path) -JS_CODE_PATTERN = re.compile(r"```(?:javascript|js|typescript|ts)\s*\n(.*?)```", re.MULTILINE | re.DOTALL) - -# Pattern to extract code blocks with file paths (multi-file context) -JS_CODE_WITH_PATH_PATTERN = re.compile( - r"```(?:javascript|js|typescript|ts):([^\n]+)\n(.*?)```", re.MULTILINE | re.DOTALL -) - -# Line profiler context prompt for JavaScript -JS_LINE_PROF_CONTEXT = """ -Here are the results of the line profiling of the JavaScript/TypeScript code you will be optimizing. -The profiling data shows: -- Line numbers with execution counts (hits) -- Time spent on each line (in milliseconds) -- Percentage of total time per line - -Use this data to identify performance bottlenecks and focus your optimization on the hottest code paths. - -{line_profiler_results} -""" - - -def extract_js_code_and_explanation(content: str, is_multi_file: bool = False) -> tuple[str | dict[str, str], str]: - """Extract JavaScript code and explanation from LLM response. - - Args: - content: The raw LLM response content - is_multi_file: Whether to expect multi-file format - - Returns: - Tuple of (code, explanation) where code is a string for single file - or dict[str, str] for multi-file - - """ - if is_multi_file: - # Extract all code blocks with file paths - matches = JS_CODE_WITH_PATH_PATTERN.findall(content) - if matches: - file_to_code: dict[str, str] = {} - first_match_pos = content.find("```") - explanation = content[:first_match_pos].strip() if first_match_pos > 0 else "" - - for file_path, code in matches: - file_to_code[file_path.strip()] = code.strip() - - return file_to_code, explanation - - # Fall back to single file extraction - return extract_js_code_and_explanation(content, is_multi_file=False) - - # Single file extraction - match = JS_CODE_PATTERN.search(content) - if match: - code = match.group(1).strip() - explanation_end = match.start() - explanation = content[:explanation_end].strip() - return code, explanation - - return "", content - - -def normalize_js_code(code: str) -> str: - """Normalize JavaScript code for comparison.""" - # Remove single-line comments - code = re.sub(r"//.*$", "", code, flags=re.MULTILINE) - # Remove multi-line comments - code = re.sub(r"/\*.*?\*/", "", code, flags=re.DOTALL) - # Normalize whitespace - code = " ".join(code.split()) - return code - - async def optimize_python_code_line_profiler_single( user_id: str, trace_id: str, @@ -270,267 +193,6 @@ async def optimize_python_code_line_profiler( return optimization_results, total_cost, code_and_explanations, optimization_models -# ============================================================================ -# JavaScript/TypeScript Line Profiler Optimization -# ============================================================================ - - -async def optimize_javascript_code_line_profiler_single( - user_id: str, - trace_id: str, - source_code: str, - line_profiler_results: str, - dependency_code: str | None = None, - optimize_model: LLM = OPTIMIZE_MODEL, - language_version: str = "ES2022", - language: str = "javascript", - call_sequence: int | None = None, -) -> tuple[OptimizeResponseItemSchema | None, float | None, str]: - """Optimize JavaScript/TypeScript code using LLMs with line profiler guidance.""" - lang_name = "TypeScript" if language == "typescript" else "JavaScript" - code_block_tag = "typescript" if language == "typescript" else "javascript" - logging.info(f"/optimize-line-profiler: Optimizing {lang_name} code.") - debug_log_sensitive_data(f"Optimizing {lang_name} code for user {user_id}:\n{source_code}") - - # Check if source code is multi-file format - is_multi_file = is_multi_context_ts(source_code) if language == "typescript" else is_multi_context_js(source_code) - original_file_to_code: dict[str, str] = {} - - if is_multi_file: - original_file_to_code = split_markdown_code(source_code, language) - logging.info( - f"Multi-file context detected with {len(original_file_to_code)} files: {list(original_file_to_code.keys())}" - ) - - # Format system prompt with language version - system_prompt = JS_SYSTEM_PROMPT.format(language_version=language_version) - - # Build user prompt with line profiler results - if is_multi_file: - # For multi-file, identify the first file as the target and others as helper context - file_paths = list(original_file_to_code.keys()) - target_file = file_paths[0] if file_paths else "main file" - helper_files = file_paths[1:] if len(file_paths) > 1 else [] - - # Build multi-file instructions - helper_notice = "" - if helper_files: - helper_list = ", ".join(f"`{f}`" for f in helper_files) - helper_notice = f""" -HELPER FILES: {helper_list} -These files contain helper functions that the target function uses. You may optimize these as well if needed. -""" - - multi_file_instructions = f""" -The code is provided in a multi-file format. Each file is wrapped in a code block with its path. - -TARGET FILE: `{target_file}` -{helper_notice} -Output the optimized code for each file that you modify. Wrap each file's code in: -```{code_block_tag}: - -``` - -You MUST output the target file. You may also output helper files if you optimize them. -""" - system_prompt = system_prompt + "\n" + multi_file_instructions - - user_prompt = f"""Optimize the following {lang_name} code for better performance. - -{JS_LINE_PROF_CONTEXT.format(line_profiler_results=line_profiler_results)} - -Here is the code to optimize: -{source_code} -""" - else: - user_prompt = f"""Optimize the following {lang_name} code for better performance. - -{JS_LINE_PROF_CONTEXT.format(line_profiler_results=line_profiler_results)} - -Here is the code to optimize: -```{code_block_tag} -{source_code} -``` -""" - - if dependency_code: - user_prompt = f"Dependencies (read-only):\n```{code_block_tag}\n{dependency_code}\n```\n\n{user_prompt}" - - obs_context: dict = {} - if call_sequence is not None: - obs_context["call_sequence"] = call_sequence - - messages: list[ChatCompletionMessageParam] = [ - ChatCompletionSystemMessageParam(role="system", content=system_prompt), - ChatCompletionUserMessageParam(role="user", content=user_prompt), - ] - - try: - output = await call_llm( - llm=optimize_model, - messages=messages, - call_type="line_profiler", - trace_id=trace_id, - user_id=user_id, - python_version=language_version, # Reusing python_version field for language version - context=obs_context, - ) - except Exception as e: - logging.exception(f"LLM Code Generation error in {lang_name} line profiler optimizer") - sentry_sdk.capture_exception(e) - debug_log_sensitive_data(f"Failed to generate code for source:\n{source_code}") - return None, None, optimize_model.name - - llm_cost = calculate_llm_cost(output.raw_response, optimize_model) - - debug_log_sensitive_data(f"LLM optimization response:\n{output.raw_response.model_dump_json(indent=2)}") - - if output.raw_response.usage is not None: - ph( - user_id, - "aiservice-optimize-line-profiler-openai-usage", - properties={"model": optimize_model.name, "usage": output.raw_response.usage.json(), "language": language}, - ) - - # Extract code and explanation from response - extracted_code, explanation = extract_js_code_and_explanation(output.content, is_multi_file=is_multi_file) - - if not extracted_code: - sentry_sdk.capture_message(f"No code block found in {lang_name} line profiler optimization response") - debug_log_sensitive_data(f"No code found in response for source:\n{source_code}") - return None, llm_cost, optimize_model.name - - optimization_id = str(uuid.uuid4()) - - if is_multi_file and isinstance(extracted_code, dict): - # Handle multi-file response - # LLM can optimize both target and helper files - merged_file_to_code: dict[str, str] = {} - has_changes = False - - for file_path, original_code in original_file_to_code.items(): - if file_path in extracted_code: - new_code = extracted_code[file_path] - - # Validate the new code - if language == "typescript": - is_valid, error = validate_typescript_syntax(new_code) - else: - is_valid, error = validate_javascript_syntax(new_code) - - if not is_valid: - sentry_sdk.capture_message(f"Invalid {lang_name} generated for {file_path}: {error}") - debug_log_sensitive_data(f"Invalid code generated for {file_path}:\n{new_code}\nError: {error}") - # Keep original code for this file - merged_file_to_code[file_path] = original_code - else: - merged_file_to_code[file_path] = new_code - if normalize_js_code(new_code) != normalize_js_code(original_code): - has_changes = True - else: - # File not in response, keep original - merged_file_to_code[file_path] = original_code - - if not has_changes: - debug_log_sensitive_data("Generated code identical to original (multi-file)") - return None, llm_cost, optimize_model.name - - # Format as multi-file markdown - wrapped_code = group_code(merged_file_to_code, language=code_block_tag) - - result = OptimizeResponseItemSchema( - source_code=wrapped_code, explanation=explanation, optimization_id=optimization_id - ) - return result, llm_cost, optimize_model.name - - # Single file handling - optimized_code = extracted_code if isinstance(extracted_code, str) else "" - - if not optimized_code: - return None, llm_cost, optimize_model.name - - # Validate the generated code - if language == "typescript": - is_valid, error = validate_typescript_syntax(optimized_code) - else: - is_valid, error = validate_javascript_syntax(optimized_code) - - if not is_valid: - sentry_sdk.capture_message(f"Invalid {lang_name} generated: {error}") - debug_log_sensitive_data(f"Invalid code generated:\n{optimized_code}\nError: {error}") - return None, llm_cost, optimize_model.name - - # Check that the code is actually different from the original - if normalize_js_code(optimized_code) == normalize_js_code(source_code): - debug_log_sensitive_data("Generated code identical to original") - return None, llm_cost, optimize_model.name - - # Wrap code in markdown format for CLI parsing - wrapped_code = ( - f"```{code_block_tag}\n{optimized_code}\n```" - if not optimized_code.endswith("\n") - else f"```{code_block_tag}\n{optimized_code}```" - ) - result = OptimizeResponseItemSchema( - source_code=wrapped_code, explanation=explanation, optimization_id=optimization_id - ) - - return result, llm_cost, optimize_model.name - - -async def optimize_javascript_code_line_profiler( - user_id: str, - trace_id: str, - source_code: str, - line_profiler_results: str, - dependency_code: str | None = None, - language_version: str = "ES2022", - language: str = "javascript", - n_candidates: int = 0, -) -> tuple[list[OptimizeResponseItemSchema], float, dict[str, str]]: - """Run parallel JavaScript line profiler optimizations with multiple models.""" - if n_candidates == 0: - return [], 0.0, {} - - model_distribution = get_model_distribution(n_candidates, MAX_OPTIMIZER_LP_CALLS) - tasks: list[asyncio.Task[tuple[OptimizeResponseItemSchema | None, float | None, str]]] = [] - call_sequence = 1 - - async with asyncio.TaskGroup() as tg: - for model, num_calls in model_distribution: - for _ in range(num_calls): - task = tg.create_task( - optimize_javascript_code_line_profiler_single( - user_id=user_id, - trace_id=trace_id, - source_code=source_code, - line_profiler_results=line_profiler_results, - dependency_code=dependency_code, - optimize_model=model, - language_version=language_version, - language=language, - call_sequence=call_sequence, - ) - ) - tasks.append(task) - call_sequence += 1 - - # Collect results - optimization_results: list[OptimizeResponseItemSchema] = [] - total_cost = 0.0 - optimization_models: dict[str, str] = {} - - for task in tasks: - result, cost, model_name = task.result() - if cost: - total_cost += cost - if result is not None: - optimization_results.append(result) - optimization_models[result.optimization_id] = model_name - - return optimization_results, total_cost, optimization_models - - @optimize_line_profiler_api.post( "/", response={200: OptimizeResponseSchema, 400: OptimizeErrorResponseSchema, 500: OptimizeErrorResponseSchema} ) diff --git a/django/aiservice/testgen/testgen.py b/django/aiservice/testgen/testgen.py index 86db0824f..da20579f5 100644 --- a/django/aiservice/testgen/testgen.py +++ b/django/aiservice/testgen/testgen.py @@ -38,7 +38,7 @@ from testgen.models import ( from testgen.postprocessing.code_validator import CodeValidationError, has_test_functions, validate_testgen_code from testgen.postprocessing.postprocess_pipeline import postprocessing_testgen_pipeline from testgen.testgen_context import BaseTestGenContext, TestGenContextData -from testgen.testgen_javascript import testgen_javascript +from languages.js_ts.testgen import testgen_javascript if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/tests/languages/__init__.py b/django/aiservice/tests/languages/__init__.py new file mode 100644 index 000000000..d74f4d34d --- /dev/null +++ b/django/aiservice/tests/languages/__init__.py @@ -0,0 +1 @@ +"""Tests for the languages module.""" diff --git a/django/aiservice/tests/languages/test_registry.py b/django/aiservice/tests/languages/test_registry.py new file mode 100644 index 000000000..97ece0194 --- /dev/null +++ b/django/aiservice/tests/languages/test_registry.py @@ -0,0 +1,249 @@ +"""Tests for the language registry and language implementations.""" + +import pytest + +from languages import ( + UnsupportedLanguageError, + get_language, + get_supported_languages, + is_language_supported, +) +from languages.base import CodeValidator, LanguageSupport + + +class TestLanguageRegistry: + """Tests for the language registry.""" + + def test_get_supported_languages_returns_all_registered(self): + """Test that all registered languages are returned.""" + languages = get_supported_languages() + assert "python" in languages + assert "javascript" in languages + assert "typescript" in languages + + def test_is_language_supported_for_registered_languages(self): + """Test is_language_supported for registered languages.""" + assert is_language_supported("python") + assert is_language_supported("javascript") + assert is_language_supported("typescript") + + def test_is_language_supported_for_aliases(self): + """Test is_language_supported for language aliases.""" + assert is_language_supported("js") + assert is_language_supported("ts") + + def test_is_language_supported_false_for_unknown(self): + """Test is_language_supported returns False for unknown languages.""" + assert not is_language_supported("ruby") + assert not is_language_supported("go") + + def test_get_language_returns_instance(self): + """Test get_language returns an instance of the language class.""" + py = get_language("python") + assert py is not None + assert py.language == "python" + + def test_get_language_with_alias(self): + """Test get_language works with aliases.""" + js = get_language("js") + assert js.language == "javascript" + + ts = get_language("ts") + assert ts.language == "typescript" + + def test_get_language_case_insensitive(self): + """Test get_language is case insensitive.""" + py1 = get_language("Python") + py2 = get_language("PYTHON") + py3 = get_language("python") + assert py1.language == py2.language == py3.language == "python" + + def test_get_language_raises_for_unknown(self): + """Test get_language raises UnsupportedLanguageError for unknown languages.""" + with pytest.raises(UnsupportedLanguageError) as exc_info: + get_language("ruby") + assert "ruby" in str(exc_info.value) + assert "Unsupported language" in str(exc_info.value) + + +class TestPythonLanguage: + """Tests for the Python language implementation.""" + + @pytest.fixture + def python_lang(self): + """Get Python language instance.""" + return get_language("python") + + def test_language_property(self, python_lang): + """Test language property returns 'python'.""" + assert python_lang.language == "python" + + def test_get_code_block_tag(self, python_lang): + """Test get_code_block_tag returns 'python'.""" + assert python_lang.get_code_block_tag() == "python" + + def test_get_validator_returns_validator(self, python_lang): + """Test get_validator returns a CodeValidator.""" + validator = python_lang.get_validator() + assert isinstance(validator, CodeValidator) + + def test_validator_valid_python(self, python_lang): + """Test validator accepts valid Python code.""" + validator = python_lang.get_validator() + is_valid, error = validator.validate_syntax("def foo():\n return 42") + assert is_valid is True + assert error is None + + def test_validator_invalid_python(self, python_lang): + """Test validator rejects invalid Python code.""" + validator = python_lang.get_validator() + is_valid, error = validator.validate_syntax("def foo( return 42") + assert is_valid is False + assert error is not None + + def test_is_multi_context_false_for_plain_code(self, python_lang): + """Test is_multi_context returns False for plain Python code.""" + assert not python_lang.is_multi_context("def foo(): pass") + assert not python_lang.is_multi_context("```python\ndef foo(): pass\n```") + + def test_is_multi_context_true_for_markdown_with_path(self, python_lang): + """Test is_multi_context returns True for markdown with filepath.""" + code = "```python:test.py\ndef foo(): pass\n```" + assert python_lang.is_multi_context(code) + + def test_split_markdown_code(self, python_lang): + """Test split_markdown_code parses markdown correctly.""" + markdown = """```python:file1.py +def foo(): + pass +``` + +```python:file2.py +def bar(): + pass +```""" + result = python_lang.split_markdown_code(markdown) + assert "file1.py" in result + assert "file2.py" in result + assert "def foo():" in result["file1.py"] + assert "def bar():" in result["file2.py"] + + def test_group_code(self, python_lang): + """Test group_code formats code correctly.""" + file_to_code = { + "file1.py": "def foo():\n pass", + "file2.py": "def bar():\n pass", + } + result = python_lang.group_code(file_to_code) + assert "```python:file1.py" in result + assert "```python:file2.py" in result + assert "def foo():" in result + assert "def bar():" in result + + +class TestJavaScriptLanguage: + """Tests for the JavaScript language implementation.""" + + @pytest.fixture + def js_lang(self): + """Get JavaScript language instance.""" + return get_language("javascript") + + def test_language_property(self, js_lang): + """Test language property returns 'javascript'.""" + assert js_lang.language == "javascript" + + def test_get_code_block_tag(self, js_lang): + """Test get_code_block_tag returns 'javascript'.""" + assert js_lang.get_code_block_tag() == "javascript" + + def test_get_validator_returns_validator(self, js_lang): + """Test get_validator returns a CodeValidator.""" + validator = js_lang.get_validator() + assert isinstance(validator, CodeValidator) + + def test_is_multi_context_false_for_plain_code(self, js_lang): + """Test is_multi_context returns False for plain JavaScript code.""" + assert not js_lang.is_multi_context("function foo() {}") + assert not js_lang.is_multi_context("```javascript\nfunction foo() {}\n```") + + def test_is_multi_context_true_for_markdown_with_path(self, js_lang): + """Test is_multi_context returns True for markdown with filepath.""" + code = "```javascript:test.js\nfunction foo() {}\n```" + assert js_lang.is_multi_context(code) + + code_short = "```js:test.js\nfunction foo() {}\n```" + assert js_lang.is_multi_context(code_short) + + def test_group_code(self, js_lang): + """Test group_code formats code correctly.""" + file_to_code = { + "file1.js": "function foo() {}", + "file2.js": "function bar() {}", + } + result = js_lang.group_code(file_to_code) + assert "```javascript:file1.js" in result + assert "```javascript:file2.js" in result + + +class TestTypeScriptLanguage: + """Tests for the TypeScript language implementation.""" + + @pytest.fixture + def ts_lang(self): + """Get TypeScript language instance.""" + return get_language("typescript") + + def test_language_property(self, ts_lang): + """Test language property returns 'typescript'.""" + assert ts_lang.language == "typescript" + + def test_get_code_block_tag(self, ts_lang): + """Test get_code_block_tag returns 'typescript'.""" + assert ts_lang.get_code_block_tag() == "typescript" + + def test_is_multi_context_true_for_markdown_with_path(self, ts_lang): + """Test is_multi_context returns True for markdown with filepath.""" + code = "```typescript:test.ts\nfunction foo(): void {}\n```" + assert ts_lang.is_multi_context(code) + + code_short = "```ts:test.ts\nfunction foo(): void {}\n```" + assert ts_lang.is_multi_context(code_short) + + def test_group_code(self, ts_lang): + """Test group_code formats code correctly.""" + file_to_code = {"file1.ts": "function foo(): void {}"} + result = ts_lang.group_code(file_to_code) + assert "```typescript:file1.ts" in result + + +class TestLanguageProtocolCompliance: + """Tests to verify language implementations satisfy the protocols.""" + + @pytest.mark.parametrize("lang_id", ["python", "javascript", "typescript"]) + def test_language_satisfies_protocol(self, lang_id): + """Test that each language satisfies the LanguageSupport protocol.""" + lang = get_language(lang_id) + assert isinstance(lang, LanguageSupport) + + # Verify all protocol methods exist and return expected types + assert isinstance(lang.language, str) + assert isinstance(lang.get_code_block_tag(), str) + assert isinstance(lang.get_validator(), CodeValidator) + assert isinstance(lang.is_multi_context("test"), bool) + assert isinstance(lang.split_markdown_code("test"), dict) + assert isinstance(lang.group_code({"a.py": "code"}), str) + + @pytest.mark.parametrize("lang_id", ["python", "javascript", "typescript"]) + def test_validator_satisfies_protocol(self, lang_id): + """Test that each validator satisfies the CodeValidator protocol.""" + lang = get_language(lang_id) + validator = lang.get_validator() + assert isinstance(validator, CodeValidator) + + # Verify validate_syntax returns expected tuple + result = validator.validate_syntax("test code") + assert isinstance(result, tuple) + assert len(result) == 2 + assert isinstance(result[0], bool) + assert result[1] is None or isinstance(result[1], str) From 31091350c9585b3884070c33bf43c0e5fdf3ea4b Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 28 Jan 2026 22:19:40 +0200 Subject: [PATCH 010/184] cleanup --- .../aiservice/common/markdown_utils.py | 3 - .../validators/javascript_validator.py | 1 + .../code_repair/code_repair_context.py | 5 +- django/aiservice/languages/__init__.py | 148 +---------- django/aiservice/languages/base.py | 98 ------- django/aiservice/languages/js_ts/__init__.py | 213 +-------------- django/aiservice/languages/python/__init__.py | 80 ------ .../aiservice/languages/python/validator.py | 35 --- .../context_utils/optimizer_context.py | 2 +- .../context_utils/refiner_context.py | 21 +- django/aiservice/optimizer/optimizer.py | 5 - django/aiservice/tests/languages/__init__.py | 1 - .../tests/languages/test_registry.py | 249 ------------------ 13 files changed, 30 insertions(+), 831 deletions(-) delete mode 100644 django/aiservice/languages/base.py delete mode 100644 django/aiservice/languages/python/__init__.py delete mode 100644 django/aiservice/languages/python/validator.py delete mode 100644 django/aiservice/tests/languages/__init__.py delete mode 100644 django/aiservice/tests/languages/test_registry.py diff --git a/django/aiservice/aiservice/common/markdown_utils.py b/django/aiservice/aiservice/common/markdown_utils.py index 6f6b6359e..83d9933c3 100644 --- a/django/aiservice/aiservice/common/markdown_utils.py +++ b/django/aiservice/aiservice/common/markdown_utils.py @@ -10,9 +10,6 @@ import re from aiservice.common.llm_output_utils import truncate_pathological_output -# Matches ```python:filepath blocks, captures (filepath, content) -MARKDOWN_CODE_BLOCK_WITH_PATH_PATTERN = re.compile(r"```python:([^\n]+)\n(.*?)\n```", re.DOTALL) - # Matches both ```python and ```python:filepath blocks, captures content only MARKDOWN_CODE_BLOCK_PATTERN = re.compile(r"```python(?::[^\n]*)?\n(.*?)```", re.DOTALL) diff --git a/django/aiservice/aiservice/validators/javascript_validator.py b/django/aiservice/aiservice/validators/javascript_validator.py index a49d406f9..05a94e1c4 100644 --- a/django/aiservice/aiservice/validators/javascript_validator.py +++ b/django/aiservice/aiservice/validators/javascript_validator.py @@ -14,6 +14,7 @@ from functools import lru_cache @lru_cache(maxsize=100) def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: # TODO(claude): DON'T do this, use some pytohn lib for this instead of spawning a new subprocess + # Note: code can be a multi-file code (markdown with file paths) return True, None """Validate JavaScript syntax using Node.js. diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/code_repair/code_repair_context.py index b9e3a833d..1936f9b8c 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/code_repair/code_repair_context.py @@ -139,6 +139,7 @@ class CodeRepairContext: def validate_module(self) -> None: """Validate the module syntax based on language.""" # Skip validation for non-Python languages for now + # TODO: have some way to validate the syntax of the code for other languages like js & ts if self.data.language != "python": return for _code in split_markdown_code(self.data.modified_source_code).values(): @@ -147,7 +148,3 @@ class CodeRepairContext: CodeAndExplanation(cst_module, "") except (ValueError, ValidationError, cst.ParserSyntaxError): # noqa: TRY203 raise - - # Keep for backward compatibility - def validate_python_module(self) -> None: - self.validate_module() diff --git a/django/aiservice/languages/__init__.py b/django/aiservice/languages/__init__.py index 3d241cd9b..b5b2a30f1 100644 --- a/django/aiservice/languages/__init__.py +++ b/django/aiservice/languages/__init__.py @@ -1,146 +1,8 @@ -"""Multi-language support registry. +"""Multi-language support module. -This module provides a registry for language implementations and factory -functions to retrieve language-specific functionality. +This package contains language-specific implementations for code optimization +and test generation. -Usage: - from languages import get_language, register_language - - # Get a language implementation - lang = get_language("python") - validator = lang.get_validator() - is_valid, error = validator.validate_syntax(code) - - # Register a new language (typically done in language module __init__) - @register_language("python") - class PythonLanguage: - ... +Subpackages: + js_ts: JavaScript and TypeScript support """ - -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from languages.base import LanguageSupport - -# Registry mapping language identifiers to their implementation classes -_LANGUAGE_REGISTRY: dict[str, type[LanguageSupport]] = {} - -# Aliases for language names (e.g., "js" -> "javascript") -_LANGUAGE_ALIASES: dict[str, str] = { - "js": "javascript", - "ts": "typescript", -} - -# Flag to track whether languages have been loaded -_languages_loaded = False - - -class UnsupportedLanguageError(ValueError): - """Raised when an unsupported language is requested.""" - - def __init__(self, language: str) -> None: - supported = ", ".join(sorted(_LANGUAGE_REGISTRY.keys())) - super().__init__(f"Unsupported language: {language}. Supported languages: {supported}") - self.language = language - - -def register_language(lang_id: str): - """Decorator to register a language implementation. - - Args: - lang_id: The language identifier (e.g., "python", "javascript"). - - Returns: - A decorator that registers the class in the language registry. - - Example: - @register_language("python") - class PythonLanguage: - language = "python" - ... - - """ - - def decorator(cls: type[LanguageSupport]) -> type[LanguageSupport]: - _LANGUAGE_REGISTRY[lang_id] = cls - return cls - - return decorator - - -def _load_languages() -> None: - """Load all language implementations. - - This is called lazily to avoid import issues during module initialization. - """ - # Import Python language support - try: - import languages.python # noqa: F401 - except ImportError: - pass - - # Import JavaScript/TypeScript language support - try: - import languages.js_ts # noqa: F401 - except ImportError: - pass - - -def _ensure_languages_loaded() -> None: - """Ensure language implementations are loaded.""" - global _languages_loaded - if not _languages_loaded: - _load_languages() - _languages_loaded = True - - -def get_language(language: str) -> LanguageSupport: - """Get language support implementation for the given language. - - Args: - language: The language identifier (e.g., "python", "javascript", "js"). - - Returns: - An instance of the language support class. - - Raises: - UnsupportedLanguageError: If the language is not registered. - - """ - _ensure_languages_loaded() - - # Normalize language identifier using aliases - normalized = _LANGUAGE_ALIASES.get(language.lower(), language.lower()) - - if normalized not in _LANGUAGE_REGISTRY: - raise UnsupportedLanguageError(language) - - return _LANGUAGE_REGISTRY[normalized]() - - -def get_supported_languages() -> list[str]: - """Get a list of supported language identifiers. - - Returns: - A sorted list of supported language names. - - """ - _ensure_languages_loaded() - return sorted(_LANGUAGE_REGISTRY.keys()) - - -def is_language_supported(language: str) -> bool: - """Check if a language is supported. - - Args: - language: The language identifier to check. - - Returns: - True if the language is supported, False otherwise. - - """ - _ensure_languages_loaded() - normalized = _LANGUAGE_ALIASES.get(language.lower(), language.lower()) - return normalized in _LANGUAGE_REGISTRY diff --git a/django/aiservice/languages/base.py b/django/aiservice/languages/base.py deleted file mode 100644 index 3d4607f85..000000000 --- a/django/aiservice/languages/base.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Base protocols and interfaces for multi-language support. - -This module defines the contracts that language implementations must satisfy. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Protocol, runtime_checkable - -if TYPE_CHECKING: - pass - - -@runtime_checkable -class CodeValidator(Protocol): - """Protocol for code syntax validation.""" - - def validate_syntax(self, code: str) -> tuple[bool, str | None]: - """Validate code syntax. - - Args: - code: The source code to validate. - - Returns: - A tuple of (is_valid, error_message). - - is_valid: True if the code is syntactically valid. - - error_message: None if valid, otherwise a description of the error. - - """ - ... - - -@runtime_checkable -class LanguageSupport(Protocol): - """Protocol for language-specific implementations. - - Each supported language should implement this protocol to provide - language-specific functionality like validation and code formatting. - """ - - @property - def language(self) -> str: - """The language identifier (e.g., 'python', 'javascript').""" - ... - - def get_validator(self) -> CodeValidator: - """Get the code validator for this language.""" - ... - - def is_multi_context(self, code: str) -> bool: - """Check if code is in multi-file markdown format. - - Multi-file format uses ```language:filepath blocks. - - Args: - code: The source code to check. - - Returns: - True if the code is in multi-file markdown format. - - """ - ... - - def get_code_block_tag(self) -> str: - """Get the markdown code block tag for this language. - - Returns: - The language tag used in markdown code blocks (e.g., 'python', 'javascript'). - - """ - ... - - def split_markdown_code(self, markdown: str) -> dict[str, str]: - """Split markdown into filepath -> code dict. - - Parses markdown with ```language:filepath blocks and returns - a dictionary mapping file paths to their code content. - - Args: - markdown: The markdown text containing code blocks. - - Returns: - A dictionary mapping file paths to code content. - - """ - ... - - def group_code(self, file_to_code: dict[str, str]) -> str: - """Combine code files into markdown format. - - Args: - file_to_code: A dictionary mapping file paths to code content. - - Returns: - Markdown-formatted string with code blocks for each file. - - """ - ... diff --git a/django/aiservice/languages/js_ts/__init__.py b/django/aiservice/languages/js_ts/__init__.py index 6dff6d52f..7f328731e 100644 --- a/django/aiservice/languages/js_ts/__init__.py +++ b/django/aiservice/languages/js_ts/__init__.py @@ -1,204 +1,17 @@ -"""JavaScript/TypeScript language support for the aiservice. +"""JavaScript/TypeScript language support. -This module provides JS/TS-specific implementations for code validation, -multi-file context detection, and code formatting. - -Note: The full implementation will be moved here from optimizer_javascript.py -and testgen_javascript.py in Phase 3. +This package contains JS/TS-specific implementations for: +- Code optimization (optimizer.py) +- Line profiler optimization (optimizer_lp.py) +- Test generation (testgen.py) """ -from __future__ import annotations +from languages.js_ts.optimizer import optimize_javascript +from languages.js_ts.optimizer_lp import optimize_javascript_code_line_profiler +from languages.js_ts.testgen import testgen_javascript -from aiservice.common.markdown_utils import split_markdown_code as _split_markdown_code - -from languages import register_language - - -class JavaScriptValidator: - """Validator for JavaScript code syntax. - - Note: Currently uses a permissive fallback validation. - Full validation will be implemented in a future PR. - """ - - def validate_syntax(self, code: str) -> tuple[bool, str | None]: - """Validate JavaScript syntax. - - Args: - code: The JavaScript code to validate. - - Returns: - A tuple of (is_valid, error_message). - - Note: - Currently returns (True, None) as a permissive default. - The full validation logic from javascript_validator.py - will be integrated in Phase 3. - - """ - # TODO: Integrate full validation from aiservice/validators/javascript_validator.py - # For now, use permissive validation to match current behavior - return True, None - - -class TypeScriptValidator: - """Validator for TypeScript code syntax. - - Note: Currently uses a permissive fallback validation. - Full validation will be implemented in a future PR. - """ - - def validate_syntax(self, code: str) -> tuple[bool, str | None]: - """Validate TypeScript syntax. - - Args: - code: The TypeScript code to validate. - - Returns: - A tuple of (is_valid, error_message). - - Note: - Currently returns (True, None) as a permissive default. - The full validation logic will be integrated in Phase 3. - - """ - # TODO: Integrate full validation from aiservice/validators/javascript_validator.py - return True, None - - -@register_language("javascript") -class JavaScriptLanguage: - """JavaScript language support implementation.""" - - @property - def language(self) -> str: - """The language identifier.""" - return "javascript" - - def get_validator(self) -> JavaScriptValidator: - """Get the JavaScript code validator.""" - return JavaScriptValidator() - - def is_multi_context(self, code: str) -> bool: - """Check if code is in multi-file markdown format. - - Multi-file JavaScript code starts with ```javascript: or ```js: - followed by a filepath. - - Args: - code: The source code to check. - - Returns: - True if the code is in multi-file markdown format. - - """ - stripped = code.strip() - return stripped.startswith("```javascript:") or stripped.startswith("```js:") - - def get_code_block_tag(self) -> str: - """Get the markdown code block tag for JavaScript. - - Returns: - The string 'javascript' used in markdown code blocks. - - """ - return "javascript" - - def split_markdown_code(self, markdown: str) -> dict[str, str]: - """Split markdown into filepath -> code dict. - - Parses markdown with ```javascript:filepath or ```js:filepath blocks. - - Args: - markdown: The markdown text containing code blocks. - - Returns: - A dictionary mapping file paths to code content. - - """ - return _split_markdown_code(markdown, language="javascript") - - def group_code(self, file_to_code: dict[str, str]) -> str: - """Combine code files into markdown format. - - Args: - file_to_code: A dictionary mapping file paths to code content. - - Returns: - Markdown-formatted string with ```javascript:filepath blocks. - - """ - blocks = [] - for file_path, code in file_to_code.items(): - normalized = code if code.endswith("\n") else code + "\n" - blocks.append(f"```javascript:{file_path}\n{normalized}```") - return "\n".join(blocks) - - -@register_language("typescript") -class TypeScriptLanguage: - """TypeScript language support implementation.""" - - @property - def language(self) -> str: - """The language identifier.""" - return "typescript" - - def get_validator(self) -> TypeScriptValidator: - """Get the TypeScript code validator.""" - return TypeScriptValidator() - - def is_multi_context(self, code: str) -> bool: - """Check if code is in multi-file markdown format. - - Multi-file TypeScript code starts with ```typescript: or ```ts: - followed by a filepath. - - Args: - code: The source code to check. - - Returns: - True if the code is in multi-file markdown format. - - """ - stripped = code.strip() - return stripped.startswith("```typescript:") or stripped.startswith("```ts:") - - def get_code_block_tag(self) -> str: - """Get the markdown code block tag for TypeScript. - - Returns: - The string 'typescript' used in markdown code blocks. - - """ - return "typescript" - - def split_markdown_code(self, markdown: str) -> dict[str, str]: - """Split markdown into filepath -> code dict. - - Parses markdown with ```typescript:filepath or ```ts:filepath blocks. - - Args: - markdown: The markdown text containing code blocks. - - Returns: - A dictionary mapping file paths to code content. - - """ - return _split_markdown_code(markdown, language="typescript") - - def group_code(self, file_to_code: dict[str, str]) -> str: - """Combine code files into markdown format. - - Args: - file_to_code: A dictionary mapping file paths to code content. - - Returns: - Markdown-formatted string with ```typescript:filepath blocks. - - """ - blocks = [] - for file_path, code in file_to_code.items(): - normalized = code if code.endswith("\n") else code + "\n" - blocks.append(f"```typescript:{file_path}\n{normalized}```") - return "\n".join(blocks) +__all__ = [ + "optimize_javascript", + "optimize_javascript_code_line_profiler", + "testgen_javascript", +] diff --git a/django/aiservice/languages/python/__init__.py b/django/aiservice/languages/python/__init__.py deleted file mode 100644 index b76a449ea..000000000 --- a/django/aiservice/languages/python/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Python language support for the aiservice. - -This module provides Python-specific implementations for code validation, -multi-file context detection, and code formatting. -""" - -from __future__ import annotations - -from aiservice.common.markdown_utils import split_markdown_code as _split_markdown_code -from languages import register_language -from languages.python.validator import PythonValidator - - -@register_language("python") -class PythonLanguage: - """Python language support implementation.""" - - @property - def language(self) -> str: - """The language identifier.""" - return "python" - - def get_validator(self) -> PythonValidator: - """Get the Python code validator.""" - return PythonValidator() - - def is_multi_context(self, code: str) -> bool: - """Check if code is in multi-file markdown format. - - Multi-file Python code starts with ```python: followed by a filepath. - - Args: - code: The source code to check. - - Returns: - True if the code is in multi-file markdown format. - - """ - return code.strip().startswith("```python:") - - def get_code_block_tag(self) -> str: - """Get the markdown code block tag for Python. - - Returns: - The string 'python' used in markdown code blocks. - - """ - return "python" - - def split_markdown_code(self, markdown: str) -> dict[str, str]: - """Split markdown into filepath -> code dict. - - Parses markdown with ```python:filepath blocks and returns - a dictionary mapping file paths to their code content. - - Args: - markdown: The markdown text containing code blocks. - - Returns: - A dictionary mapping file paths to code content. - - """ - return _split_markdown_code(markdown, language="python") - - def group_code(self, file_to_code: dict[str, str]) -> str: - """Combine code files into markdown format. - - Args: - file_to_code: A dictionary mapping file paths to code content. - - Returns: - Markdown-formatted string with ```python:filepath blocks. - - """ - blocks = [] - for file_path, code in file_to_code.items(): - # Ensure code ends with newline before closing ``` - normalized = code if code.endswith("\n") else code + "\n" - blocks.append(f"```python:{file_path}\n{normalized}```") - return "\n".join(blocks) diff --git a/django/aiservice/languages/python/validator.py b/django/aiservice/languages/python/validator.py deleted file mode 100644 index 9837abadc..000000000 --- a/django/aiservice/languages/python/validator.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Python code syntax validation. - -This module provides a validator that uses libcst for Python syntax validation. -""" - -from __future__ import annotations - -import libcst as cst - -from aiservice.common.cst_utils import parse_module_to_cst - - -class PythonValidator: - """Validator for Python code syntax using libcst.""" - - def validate_syntax(self, code: str) -> tuple[bool, str | None]: - """Validate Python syntax using libcst. - - Args: - code: The Python code to validate. - - Returns: - A tuple of (is_valid, error_message). - - is_valid: True if the code is syntactically valid Python. - - error_message: None if valid, otherwise the parse error message. - - """ - try: - parse_module_to_cst(code) - return True, None - except cst.ParserSyntaxError as e: - return False, str(e) - except Exception as e: - # Catch any other parsing errors - return False, f"Parse error: {e}" diff --git a/django/aiservice/optimizer/context_utils/optimizer_context.py b/django/aiservice/optimizer/context_utils/optimizer_context.py index b65b78ca7..e44023f6d 100644 --- a/django/aiservice/optimizer/context_utils/optimizer_context.py +++ b/django/aiservice/optimizer/context_utils/optimizer_context.py @@ -53,7 +53,7 @@ class OptimizeErrorResponseSchema(Schema): ########################################################################################## -# BaseOptimizerContext # +# BaseOptimizerContext [PYTHON ONLY] # ########################################################################################## class BaseOptimizerContext: def __init__(self, base_system_prompt: str, base_user_prompt: str, source_code: str) -> None: diff --git a/django/aiservice/optimizer/context_utils/refiner_context.py b/django/aiservice/optimizer/context_utils/refiner_context.py index d2fb9a76b..cb09b332f 100644 --- a/django/aiservice/optimizer/context_utils/refiner_context.py +++ b/django/aiservice/optimizer/context_utils/refiner_context.py @@ -8,6 +8,7 @@ from pydantic import ValidationError from aiservice.common.cst_utils import parse_module_to_cst from aiservice.common.markdown_utils import wrap_code_in_markdown +from aiservice.validators.javascript_validator import validate_javascript_syntax from optimizer.context_utils.context_helpers import ( group_code, is_markdown_structure_changed, @@ -90,10 +91,9 @@ class BaseRefinerContext: if stripped_code == self.data.optimized_source_code.strip(): return False - # For JavaScript/TypeScript, skip Python-specific syntax validation if self.data.language in ("javascript", "typescript"): - # Basic validation: check it's not empty and has some code-like content - return len(stripped_code) > 10 and any(c in stripped_code for c in "{}();") + valid, _ = validate_javascript_syntax(stripped_code) + return bool(valid) try: parse_module_to_cst(new_refined_code) @@ -153,12 +153,10 @@ class SingleRefinerContext(BaseRefinerContext): def validate_code_syntax(self, code: str) -> None: """Validate code syntax based on language.""" - # For JavaScript/TypeScript, skip Python-specific validation if self.data.language in ("javascript", "typescript"): - # Basic validation: non-empty code - if not code.strip(): - msg = "Empty code" - raise ValueError(msg) + valid, _ = validate_javascript_syntax(code) + if not valid: + raise ValueError("Invalid JavaScript syntax") return # Python validation using libcst @@ -199,10 +197,9 @@ class MultiRefinerContext(BaseRefinerContext): """Validate code syntax based on language.""" # For JavaScript/TypeScript, skip Python-specific validation if self.data.language in ("javascript", "typescript"): - # Basic validation: non-empty code - if not code.strip(): - msg = "Empty code" - raise ValueError(msg) + valid, _ = validate_javascript_syntax(code) + if not valid: + raise ValueError("Invalid JavaScript syntax") return # Python validation using libcst diff --git a/django/aiservice/optimizer/optimizer.py b/django/aiservice/optimizer/optimizer.py index a75e9a296..29cf78681 100644 --- a/django/aiservice/optimizer/optimizer.py +++ b/django/aiservice/optimizer/optimizer.py @@ -287,13 +287,8 @@ async def optimize( request: AuthenticatedRequest, data: OptimizeSchema ) -> tuple[int, OptimizeResponseSchema | OptimizeErrorResponseSchema]: # Route based on language - logging.warning(f"[OPTIMIZE DEBUG] Received request with language='{data.language}' (type: {type(data.language)})") if data.language in ("javascript", "typescript"): - logging.warning("[OPTIMIZE DEBUG] Routing to optimize_javascript") return await optimize_javascript(request, data) - - # Default: Python optimization - logging.warning("[OPTIMIZE DEBUG] Routing to optimize_python") return await optimize_python(request, data) diff --git a/django/aiservice/tests/languages/__init__.py b/django/aiservice/tests/languages/__init__.py deleted file mode 100644 index d74f4d34d..000000000 --- a/django/aiservice/tests/languages/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the languages module.""" diff --git a/django/aiservice/tests/languages/test_registry.py b/django/aiservice/tests/languages/test_registry.py deleted file mode 100644 index 97ece0194..000000000 --- a/django/aiservice/tests/languages/test_registry.py +++ /dev/null @@ -1,249 +0,0 @@ -"""Tests for the language registry and language implementations.""" - -import pytest - -from languages import ( - UnsupportedLanguageError, - get_language, - get_supported_languages, - is_language_supported, -) -from languages.base import CodeValidator, LanguageSupport - - -class TestLanguageRegistry: - """Tests for the language registry.""" - - def test_get_supported_languages_returns_all_registered(self): - """Test that all registered languages are returned.""" - languages = get_supported_languages() - assert "python" in languages - assert "javascript" in languages - assert "typescript" in languages - - def test_is_language_supported_for_registered_languages(self): - """Test is_language_supported for registered languages.""" - assert is_language_supported("python") - assert is_language_supported("javascript") - assert is_language_supported("typescript") - - def test_is_language_supported_for_aliases(self): - """Test is_language_supported for language aliases.""" - assert is_language_supported("js") - assert is_language_supported("ts") - - def test_is_language_supported_false_for_unknown(self): - """Test is_language_supported returns False for unknown languages.""" - assert not is_language_supported("ruby") - assert not is_language_supported("go") - - def test_get_language_returns_instance(self): - """Test get_language returns an instance of the language class.""" - py = get_language("python") - assert py is not None - assert py.language == "python" - - def test_get_language_with_alias(self): - """Test get_language works with aliases.""" - js = get_language("js") - assert js.language == "javascript" - - ts = get_language("ts") - assert ts.language == "typescript" - - def test_get_language_case_insensitive(self): - """Test get_language is case insensitive.""" - py1 = get_language("Python") - py2 = get_language("PYTHON") - py3 = get_language("python") - assert py1.language == py2.language == py3.language == "python" - - def test_get_language_raises_for_unknown(self): - """Test get_language raises UnsupportedLanguageError for unknown languages.""" - with pytest.raises(UnsupportedLanguageError) as exc_info: - get_language("ruby") - assert "ruby" in str(exc_info.value) - assert "Unsupported language" in str(exc_info.value) - - -class TestPythonLanguage: - """Tests for the Python language implementation.""" - - @pytest.fixture - def python_lang(self): - """Get Python language instance.""" - return get_language("python") - - def test_language_property(self, python_lang): - """Test language property returns 'python'.""" - assert python_lang.language == "python" - - def test_get_code_block_tag(self, python_lang): - """Test get_code_block_tag returns 'python'.""" - assert python_lang.get_code_block_tag() == "python" - - def test_get_validator_returns_validator(self, python_lang): - """Test get_validator returns a CodeValidator.""" - validator = python_lang.get_validator() - assert isinstance(validator, CodeValidator) - - def test_validator_valid_python(self, python_lang): - """Test validator accepts valid Python code.""" - validator = python_lang.get_validator() - is_valid, error = validator.validate_syntax("def foo():\n return 42") - assert is_valid is True - assert error is None - - def test_validator_invalid_python(self, python_lang): - """Test validator rejects invalid Python code.""" - validator = python_lang.get_validator() - is_valid, error = validator.validate_syntax("def foo( return 42") - assert is_valid is False - assert error is not None - - def test_is_multi_context_false_for_plain_code(self, python_lang): - """Test is_multi_context returns False for plain Python code.""" - assert not python_lang.is_multi_context("def foo(): pass") - assert not python_lang.is_multi_context("```python\ndef foo(): pass\n```") - - def test_is_multi_context_true_for_markdown_with_path(self, python_lang): - """Test is_multi_context returns True for markdown with filepath.""" - code = "```python:test.py\ndef foo(): pass\n```" - assert python_lang.is_multi_context(code) - - def test_split_markdown_code(self, python_lang): - """Test split_markdown_code parses markdown correctly.""" - markdown = """```python:file1.py -def foo(): - pass -``` - -```python:file2.py -def bar(): - pass -```""" - result = python_lang.split_markdown_code(markdown) - assert "file1.py" in result - assert "file2.py" in result - assert "def foo():" in result["file1.py"] - assert "def bar():" in result["file2.py"] - - def test_group_code(self, python_lang): - """Test group_code formats code correctly.""" - file_to_code = { - "file1.py": "def foo():\n pass", - "file2.py": "def bar():\n pass", - } - result = python_lang.group_code(file_to_code) - assert "```python:file1.py" in result - assert "```python:file2.py" in result - assert "def foo():" in result - assert "def bar():" in result - - -class TestJavaScriptLanguage: - """Tests for the JavaScript language implementation.""" - - @pytest.fixture - def js_lang(self): - """Get JavaScript language instance.""" - return get_language("javascript") - - def test_language_property(self, js_lang): - """Test language property returns 'javascript'.""" - assert js_lang.language == "javascript" - - def test_get_code_block_tag(self, js_lang): - """Test get_code_block_tag returns 'javascript'.""" - assert js_lang.get_code_block_tag() == "javascript" - - def test_get_validator_returns_validator(self, js_lang): - """Test get_validator returns a CodeValidator.""" - validator = js_lang.get_validator() - assert isinstance(validator, CodeValidator) - - def test_is_multi_context_false_for_plain_code(self, js_lang): - """Test is_multi_context returns False for plain JavaScript code.""" - assert not js_lang.is_multi_context("function foo() {}") - assert not js_lang.is_multi_context("```javascript\nfunction foo() {}\n```") - - def test_is_multi_context_true_for_markdown_with_path(self, js_lang): - """Test is_multi_context returns True for markdown with filepath.""" - code = "```javascript:test.js\nfunction foo() {}\n```" - assert js_lang.is_multi_context(code) - - code_short = "```js:test.js\nfunction foo() {}\n```" - assert js_lang.is_multi_context(code_short) - - def test_group_code(self, js_lang): - """Test group_code formats code correctly.""" - file_to_code = { - "file1.js": "function foo() {}", - "file2.js": "function bar() {}", - } - result = js_lang.group_code(file_to_code) - assert "```javascript:file1.js" in result - assert "```javascript:file2.js" in result - - -class TestTypeScriptLanguage: - """Tests for the TypeScript language implementation.""" - - @pytest.fixture - def ts_lang(self): - """Get TypeScript language instance.""" - return get_language("typescript") - - def test_language_property(self, ts_lang): - """Test language property returns 'typescript'.""" - assert ts_lang.language == "typescript" - - def test_get_code_block_tag(self, ts_lang): - """Test get_code_block_tag returns 'typescript'.""" - assert ts_lang.get_code_block_tag() == "typescript" - - def test_is_multi_context_true_for_markdown_with_path(self, ts_lang): - """Test is_multi_context returns True for markdown with filepath.""" - code = "```typescript:test.ts\nfunction foo(): void {}\n```" - assert ts_lang.is_multi_context(code) - - code_short = "```ts:test.ts\nfunction foo(): void {}\n```" - assert ts_lang.is_multi_context(code_short) - - def test_group_code(self, ts_lang): - """Test group_code formats code correctly.""" - file_to_code = {"file1.ts": "function foo(): void {}"} - result = ts_lang.group_code(file_to_code) - assert "```typescript:file1.ts" in result - - -class TestLanguageProtocolCompliance: - """Tests to verify language implementations satisfy the protocols.""" - - @pytest.mark.parametrize("lang_id", ["python", "javascript", "typescript"]) - def test_language_satisfies_protocol(self, lang_id): - """Test that each language satisfies the LanguageSupport protocol.""" - lang = get_language(lang_id) - assert isinstance(lang, LanguageSupport) - - # Verify all protocol methods exist and return expected types - assert isinstance(lang.language, str) - assert isinstance(lang.get_code_block_tag(), str) - assert isinstance(lang.get_validator(), CodeValidator) - assert isinstance(lang.is_multi_context("test"), bool) - assert isinstance(lang.split_markdown_code("test"), dict) - assert isinstance(lang.group_code({"a.py": "code"}), str) - - @pytest.mark.parametrize("lang_id", ["python", "javascript", "typescript"]) - def test_validator_satisfies_protocol(self, lang_id): - """Test that each validator satisfies the CodeValidator protocol.""" - lang = get_language(lang_id) - validator = lang.get_validator() - assert isinstance(validator, CodeValidator) - - # Verify validate_syntax returns expected tuple - result = validator.validate_syntax("test code") - assert isinstance(result, tuple) - assert len(result) == 2 - assert isinstance(result[0], bool) - assert result[1] is None or isinstance(result[1], str) From ec97ebd4e74f9f302db8e71768eaa198029d58fb Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 28 Jan 2026 22:23:54 +0200 Subject: [PATCH 011/184] more cleanup --- .idea/codeflash_internal.iml | 49 --------------------- django/aiservice/languages/js_ts/testgen.py | 4 +- django/aiservice/testgen/models.py | 2 +- django/aiservice/testgen/testgen.py | 6 --- 4 files changed, 3 insertions(+), 58 deletions(-) delete mode 100644 .idea/codeflash_internal.iml diff --git a/.idea/codeflash_internal.iml b/.idea/codeflash_internal.iml deleted file mode 100644 index 5081d9849..000000000 --- a/.idea/codeflash_internal.iml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index 201f298de..a50406027 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -280,8 +280,8 @@ def validate_javascript_testgen_request_data(data: TestGenSchema) -> None: HttpError: If validation fails """ - if data.test_framework not in ["jest", "mocha"]: - raise HttpError(400, "Invalid test framework for JavaScript/TypeScript. We only support jest and mocha.") + if data.test_framework not in ["jest"]: + raise HttpError(400, "Invalid test framework for JavaScript/TypeScript. We only support jest.") if not data.function_to_optimize: raise HttpError(400, "Invalid function to optimize. It is empty.") if not validate_trace_id(data.trace_id): diff --git a/django/aiservice/testgen/models.py b/django/aiservice/testgen/models.py index 273e89dd8..134b941f1 100644 --- a/django/aiservice/testgen/models.py +++ b/django/aiservice/testgen/models.py @@ -19,7 +19,7 @@ class TestGenSchema(Schema): dependent_function_names: list[str] | None = None # Only for backwards compatibility module_path: str test_module_path: str - test_framework: str # "pytest", "unittest", "jest", "mocha" + test_framework: str # "pytest", "jest" test_timeout: int trace_id: str python_version: str | None = None # Made optional for multi-language support diff --git a/django/aiservice/testgen/testgen.py b/django/aiservice/testgen/testgen.py index da20579f5..af6521017 100644 --- a/django/aiservice/testgen/testgen.py +++ b/django/aiservice/testgen/testgen.py @@ -465,15 +465,9 @@ async def testgen( request: AuthenticatedRequest, data: TestGenSchema ) -> tuple[int, TestGenResponseSchema | TestGenErrorResponseSchema]: # Route based on language - logging.warning( - f"[TESTGEN DEBUG] Received request with language='{data.language}', framework='{data.test_framework}'" - ) if data.language in ("javascript", "typescript"): - logging.warning("[TESTGEN DEBUG] Routing to testgen_javascript") return await testgen_javascript(request, data) - # Default: Python test generation - logging.warning("[TESTGEN DEBUG] Routing to testgen_python") return await testgen_python(request, data) From db3f269b37a9ce64a4b8c90a61e781282d493208 Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 28 Jan 2026 22:41:27 +0200 Subject: [PATCH 012/184] linting and formatting --- django/aiservice/aiservice/common_utils.py | 4 +++- django/aiservice/languages/js_ts/__init__.py | 6 +----- django/aiservice/optimizer/optimizer_line_profiler.py | 7 ++----- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/django/aiservice/aiservice/common_utils.py b/django/aiservice/aiservice/common_utils.py index 3de325a47..67b5050ee 100644 --- a/django/aiservice/aiservice/common_utils.py +++ b/django/aiservice/aiservice/common_utils.py @@ -22,7 +22,9 @@ def safe_isort(code: str, **kwargs) -> str: # noqa: ANN003 return code -def parse_python_version(version: str) -> tuple[int, int, int]: +def parse_python_version(version: str | None) -> tuple[int, int, int]: + if not version: + raise ValueError("Python version is required") assert len(version) < 30, "Invalid version format" split_version = version.split(".") if len(split_version) != 3: diff --git a/django/aiservice/languages/js_ts/__init__.py b/django/aiservice/languages/js_ts/__init__.py index 7f328731e..c34fdc85d 100644 --- a/django/aiservice/languages/js_ts/__init__.py +++ b/django/aiservice/languages/js_ts/__init__.py @@ -10,8 +10,4 @@ from languages.js_ts.optimizer import optimize_javascript from languages.js_ts.optimizer_lp import optimize_javascript_code_line_profiler from languages.js_ts.testgen import testgen_javascript -__all__ = [ - "optimize_javascript", - "optimize_javascript_code_line_profiler", - "testgen_javascript", -] +__all__ = ["optimize_javascript", "optimize_javascript_code_line_profiler", "testgen_javascript"] diff --git a/django/aiservice/optimizer/optimizer_line_profiler.py b/django/aiservice/optimizer/optimizer_line_profiler.py index 492012c11..3bb135732 100644 --- a/django/aiservice/optimizer/optimizer_line_profiler.py +++ b/django/aiservice/optimizer/optimizer_line_profiler.py @@ -18,11 +18,7 @@ from languages.js_ts.optimizer_lp import optimize_javascript_code_line_profiler from log_features.log_event import update_optimization_cost from log_features.log_features import log_features from optimizer.config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution -from optimizer.context_utils.context_helpers import ( - is_multi_context_js, - is_multi_context_ts, - split_markdown_code, -) +from optimizer.context_utils.context_helpers import is_multi_context_js, is_multi_context_ts, split_markdown_code from optimizer.context_utils.optimizer_context import ( BaseOptimizerContext, OptimizeErrorResponseSchema, @@ -47,6 +43,7 @@ SYSTEM_PROMPT = (current_dir / "system_prompt.md").read_text() USER_PROMPT = (current_dir / "user_prompt.md").read_text() JIT_INSTRUCTIONS = (current_dir / "jit_instructions.md").read_text() + async def optimize_python_code_line_profiler_single( user_id: str, trace_id: str, From f0480fac399fb8c12822d796a880d10e48d9d5c1 Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 28 Jan 2026 23:15:30 +0200 Subject: [PATCH 013/184] use treesitter for validating js & ts code syntax --- .../validators/javascript_validator.py | 152 +++--------------- django/aiservice/pyproject.toml | 3 + django/aiservice/uv.lock | 66 ++++++++ 3 files changed, 87 insertions(+), 134 deletions(-) diff --git a/django/aiservice/aiservice/validators/javascript_validator.py b/django/aiservice/aiservice/validators/javascript_validator.py index 05a94e1c4..b02c0513f 100644 --- a/django/aiservice/aiservice/validators/javascript_validator.py +++ b/django/aiservice/aiservice/validators/javascript_validator.py @@ -5,146 +5,30 @@ Uses Node.js for validation when available, with a basic regex fallback. from __future__ import annotations -import json -import re -import subprocess from functools import lru_cache +import tree_sitter_javascript +import tree_sitter_typescript +from tree_sitter import Language, Parser + +js_parser = Parser(Language(tree_sitter_javascript.language())) + +ts_parser = Parser(Language(tree_sitter_typescript.language_typescript())) + @lru_cache(maxsize=100) def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: - # TODO(claude): DON'T do this, use some pytohn lib for this instead of spawning a new subprocess - # Note: code can be a multi-file code (markdown with file paths) + tree = js_parser.parse(bytes(code, "utf8")) + has_error = tree.root_node.has_error + if has_error: + return False, "Invalid syntax" return True, None - """Validate JavaScript syntax using Node.js. - - Args: - code: The JavaScript code to validate - - Returns: - A tuple of (is_valid, error_message) - - is_valid: True if the code is syntactically valid - - error_message: None if valid, otherwise the error description - - """ - try: - # Use Node.js to parse the code - # We use acorn parser via a small script that reports syntax errors - validation_script = f""" -const acorn = require('acorn'); -try {{ - acorn.parse({json.dumps(code)}, {{ - ecmaVersion: 'latest', - sourceType: 'module', - allowHashBang: true, - allowAwaitOutsideFunction: true, - allowImportExportEverywhere: true, - }}); - console.log('VALID'); -}} catch (e) {{ - console.error(e.message); - process.exit(1); -}} -""" - result = subprocess.run(["node", "-e", validation_script], capture_output=True, text=True, timeout=5) - - if result.returncode != 0: - error_msg = result.stderr.strip() or result.stdout.strip() - # Check if the error is due to missing acorn module - if "Cannot find module 'acorn'" in error_msg: - return _fallback_validation(code) - return False, error_msg - - return True, None - - except subprocess.TimeoutExpired: - return False, "Syntax validation timed out" - - except FileNotFoundError: - # Node.js not available, try fallback - return _fallback_validation(code) - - except Exception: - # Fallback for any other error - return _fallback_validation(code) - - -def _fallback_validation(code: str) -> tuple[bool, str | None]: - """Fallback validation using basic checks when Node.js is not available. - - This performs simple bracket matching and basic syntax checks. - """ - try: - # Check for obvious syntax errors - - # 1. Bracket matching - brackets = {"(": ")", "[": "]", "{": "}"} - stack = [] - in_string = False - string_char = None - escape_next = False - - for i, char in enumerate(code): - if escape_next: - escape_next = False - continue - - if char == "\\": - escape_next = True - continue - - # Handle strings - if char in "'\"`": - if not in_string: - in_string = True - string_char = char - elif char == string_char: - # Check for template literal - if char == "`" or (i + 1 < len(code) and code[i + 1] != string_char): - in_string = False - string_char = None - - if in_string: - continue - - # Check brackets - if char in brackets: - stack.append((brackets[char], i)) - elif char in brackets.values(): - if not stack: - return False, f"Unexpected closing bracket '{char}' at position {i}" - expected, _ = stack.pop() - if expected != char: - return False, f"Mismatched bracket '{char}' at position {i}" - - if stack: - return False, f"Unclosed bracket '{stack[-1][0]}'" - - # 2. Check for common invalid patterns - invalid_patterns = [ - (r"function\s*\(\s*\)\s*\{[^}]*\breturn\b[^;]*$", "Missing semicolon after return"), - (r"\bconst\s+\d", "Invalid const declaration - cannot start with number"), - (r"\blet\s+\d", "Invalid let declaration - cannot start with number"), - (r"\bvar\s+\d", "Invalid var declaration - cannot start with number"), - ] - - for pattern, error_msg in invalid_patterns: - if re.search(pattern, code): - return False, error_msg - - return True, None - - except Exception as e: - return False, f"Validation error: {e}" +@lru_cache(maxsize=100) def validate_typescript_syntax(code: str) -> tuple[bool, str | None]: - """Validate TypeScript syntax. - - Since acorn doesn't support TypeScript type annotations, we use the - fallback validation which checks for basic syntax errors like bracket - matching. For complete TypeScript validation, tsc would be needed. - """ - # Acorn doesn't support TypeScript syntax (type annotations, interfaces, etc.) - # Use the fallback validation which is more lenient - return _fallback_validation(code) + tree = ts_parser.parse(bytes(code, "utf8")) + has_error = tree.root_node.has_error + if has_error: + return False, "Invalid syntax" + return True, None diff --git a/django/aiservice/pyproject.toml b/django/aiservice/pyproject.toml index b207a26a9..df53f1312 100644 --- a/django/aiservice/pyproject.toml +++ b/django/aiservice/pyproject.toml @@ -26,6 +26,9 @@ dependencies = [ "jedi>=0.19.2", "anthropic>=0.75.0", "wcwidth>=0.2.15", + "tree-sitter>=0.25.2", + "tree-sitter-javascript>=0.25.0", + "tree-sitter-typescript>=0.23.2", ] [project.urls] diff --git a/django/aiservice/uv.lock b/django/aiservice/uv.lock index 0223053f2..2ecc813c3 100644 --- a/django/aiservice/uv.lock +++ b/django/aiservice/uv.lock @@ -136,6 +136,9 @@ dependencies = [ { name = "ruff" }, { name = "sentry-sdk", extra = ["django"] }, { name = "stamina" }, + { name = "tree-sitter" }, + { name = "tree-sitter-javascript" }, + { name = "tree-sitter-typescript" }, { name = "uvicorn" }, { name = "wcwidth" }, ] @@ -176,6 +179,9 @@ requires-dist = [ { name = "ruff", specifier = ">=0.7.0" }, { name = "sentry-sdk", extras = ["django"], specifier = ">=2.35.0" }, { name = "stamina", specifier = ">=25.1.0" }, + { name = "tree-sitter", specifier = ">=0.25.2" }, + { name = "tree-sitter-javascript", specifier = ">=0.25.0" }, + { name = "tree-sitter-typescript", specifier = ">=0.23.2" }, { name = "uvicorn", specifier = ">=0.32.0,<0.33" }, { name = "wcwidth", specifier = ">=0.2.15" }, ] @@ -1726,6 +1732,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "tree-sitter" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" }, + { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, + { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, + { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/e0/e63103c72a9d3dfd89a31e02e660263ad84b7438e5f44ee82e443e65bbde/tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38", size = 132338, upload-time = "2025-09-01T07:13:44.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/df/5106ac250cd03661ebc3cc75da6b3d9f6800a3606393a0122eca58038104/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc", size = 64052, upload-time = "2025-09-01T07:13:36.865Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/6b4b2bc90d8ab3955856ce852cc9d1e82c81d7ab9646385f0e75ffd5b5d3/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1", size = 66440, upload-time = "2025-09-01T07:13:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c4/7da74ecdcd8a398f88bd003a87c65403b5fe0e958cdd43fbd5fd4a398fcf/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75", size = 99728, upload-time = "2025-09-01T07:13:38.755Z" }, + { url = "https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b", size = 106072, upload-time = "2025-09-01T07:13:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/13/be/c964e8130be08cc9bd6627d845f0e4460945b158429d39510953bbcb8fcc/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc", size = 104388, upload-time = "2025-09-01T07:13:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/89/9b773dee0f8961d1bb8d7baf0a204ab587618df19897c1ef260916f318ec/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54", size = 98377, upload-time = "2025-09-01T07:13:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/d90cb1790f8cec9b4878d278ad9faf7c8f893189ce0f855304fd704fc274/tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c", size = 62975, upload-time = "2025-09-01T07:13:42.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1f/f9eba1038b7d4394410f3c0a6ec2122b590cd7acb03f196e52fa57ebbe72/tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b", size = 61668, upload-time = "2025-09-01T07:13:43.803Z" }, +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053, upload-time = "2024-11-11T02:36:11.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677, upload-time = "2024-11-11T02:35:58.839Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008, upload-time = "2024-11-11T02:36:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987, upload-time = "2024-11-11T02:36:02.669Z" }, + { url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960, upload-time = "2024-11-11T02:36:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245, upload-time = "2024-11-11T02:36:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015, upload-time = "2024-11-11T02:36:07.631Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" }, +] + [[package]] name = "ty" version = "0.0.14" From c19d9f445064d6ebdc6a9d266de6a31e467abdcd Mon Sep 17 00:00:00 2001 From: ali Date: Wed, 28 Jan 2026 23:20:48 +0200 Subject: [PATCH 014/184] fix unit tests --- .../optimizer/test_javascript_validator.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/django/aiservice/tests/optimizer/test_javascript_validator.py b/django/aiservice/tests/optimizer/test_javascript_validator.py index 82521c7dd..2c33688fc 100644 --- a/django/aiservice/tests/optimizer/test_javascript_validator.py +++ b/django/aiservice/tests/optimizer/test_javascript_validator.py @@ -1,10 +1,6 @@ """Tests for JavaScript syntax validation.""" -from aiservice.validators.javascript_validator import ( - _fallback_validation, - validate_javascript_syntax, - validate_typescript_syntax, -) +from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax class TestJavaScriptValidatorFallback: @@ -17,14 +13,14 @@ function add(a, b) { return a + b; } """ - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is True assert error is None def test_valid_arrow_function(self) -> None: """Test that arrow functions pass validation.""" code = "const add = (a, b) => a + b;" - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is True assert error is None @@ -42,7 +38,7 @@ class Calculator { } } """ - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is True assert error is None @@ -54,7 +50,7 @@ async function fetchData(url) { return response.json(); } """ - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is True assert error is None @@ -67,7 +63,7 @@ const multiline = ` multiline string `; """ - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is True assert error is None @@ -77,32 +73,30 @@ const multiline = ` function broken() { return true; """ - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is False assert error is not None - assert "Unclosed" in error or "bracket" in error.lower() def test_unclosed_parenthesis(self) -> None: """Test that unclosed parentheses are detected.""" code = "const result = add(1, 2" - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is False assert error is not None def test_unclosed_bracket(self) -> None: """Test that unclosed brackets are detected.""" code = "const arr = [1, 2, 3" - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is False assert error is not None def test_mismatched_brackets(self) -> None: """Test that mismatched brackets are detected.""" code = "const arr = [1, 2, 3}" - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is False assert error is not None - assert "Mismatched" in error or "Unexpected" in error def test_string_with_brackets_ignored(self) -> None: """Test that brackets inside strings are ignored.""" @@ -110,13 +104,13 @@ function broken() { const str = "This string has { and } and [ and ]"; const obj = { valid: true }; """ - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is True assert error is None def test_empty_code(self) -> None: """Test that empty code passes validation.""" - is_valid, error = _fallback_validation("") + is_valid, error = validate_javascript_syntax("") assert is_valid is True assert error is None @@ -127,7 +121,7 @@ const obj = { valid: true }; /* This is a multiline comment */ """ - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is True assert error is None @@ -144,7 +138,7 @@ const nested = { } }; """ - is_valid, error = _fallback_validation(code) + is_valid, error = validate_javascript_syntax(code) assert is_valid is True assert error is None From 215e6ad390dd787dcdfcbe8972c3413ed935ca8f Mon Sep 17 00:00:00 2001 From: aseembits93 Date: Wed, 28 Jan 2026 15:33:11 -0800 Subject: [PATCH 015/184] fixed merge issues --- django/aiservice/ranker/ranker.py | 291 +++++------------------------- 1 file changed, 42 insertions(+), 249 deletions(-) diff --git a/django/aiservice/ranker/ranker.py b/django/aiservice/ranker/ranker.py index f03af3a0f..34529ecb7 100644 --- a/django/aiservice/ranker/ranker.py +++ b/django/aiservice/ranker/ranker.py @@ -3,9 +3,7 @@ from __future__ import annotations import asyncio import json import logging -import random import re -from collections import defaultdict from dataclasses import dataclass from typing import TYPE_CHECKING @@ -131,8 +129,6 @@ Here are the function references """ -NUM_RANKING_PASSES = 3 - # Type alias for candidate scores: {candidate_idx: {dimension: score}} CandidateScores = dict[int, dict[str, float]] @@ -310,81 +306,42 @@ def _scores_to_ranking(scores: CandidateScores) -> list[int]: """Convert candidate scores to a ranking (best first, 1-indexed).""" # Calculate weighted score for each candidate weighted_scores = { - candidate_idx: _compute_weighted_score(dim_scores) - for candidate_idx, dim_scores in scores.items() + candidate_idx: _compute_weighted_score(dim_scores) for candidate_idx, dim_scores in scores.items() } # Sort by weighted score descending (higher is better) sorted_candidates = sorted(weighted_scores.keys(), key=lambda c: weighted_scores[c], reverse=True) return sorted_candidates -def _aggregate_scores( - all_scores: list[CandidateScores], num_candidates: int -) -> tuple[CandidateScores, list[int]]: - """Aggregate scores from multiple passes by averaging. +async def rank_optimizations( # noqa: D417 + user_id: str, data: RankInputSchema, rank_model: LLM = RANKING_MODEL +) -> RankResponseSchema | RankErrorResponseSchema: + """Rank optimization candidates using multi-dimensional scoring. + + Parameters + ---------- + - speedups list[str]: list of speedups of optimized candidates. + - diffs list[str]: list of diffs of optimized candidates. + - python_version (tuple[int, int, int]): The python version to use. Default is (3,12,9). + + Returns: - List[Tuple[Union[str, None], Union[str, None]]]: A list of tuples where the first element is the + optimized code and the second is the explanation. + :param optimization_ids: - Returns: - Tuple of (averaged_scores, final_ranking) - """ - # Initialize accumulators - score_sums: dict[int, dict[str, float]] = { - i: {dim: 0.0 for dim in SCORING_DIMENSIONS} for i in range(1, num_candidates + 1) - } - counts: dict[int, int] = {i: 0 for i in range(1, num_candidates + 1)} - - # Sum all scores - for scores in all_scores: - for candidate_idx, dim_scores in scores.items(): - for dim, score in dim_scores.items(): - if dim in SCORING_DIMENSIONS: - score_sums[candidate_idx][dim] += score - counts[candidate_idx] += 1 - - # Average the scores - averaged_scores: CandidateScores = {} - for candidate_idx in range(1, num_candidates + 1): - if counts[candidate_idx] > 0: - averaged_scores[candidate_idx] = { - dim: score_sums[candidate_idx][dim] / counts[candidate_idx] - for dim in SCORING_DIMENSIONS - } - else: - # Fallback to neutral scores if no data - averaged_scores[candidate_idx] = {dim: 5.0 for dim in SCORING_DIMENSIONS} - - # Derive ranking from averaged scores - final_ranking = _scores_to_ranking(averaged_scores) - return averaged_scores, final_ranking - - -async def _single_ranking_pass( - user_id: str, - trace_id: str, - diffs: list[str], - speedups: list[float], - python_version: str | None, - function_references: str | None, - rank_model: LLM, - pass_number: int, -) -> tuple[list[int], str, CandidateScores | None] | None: - """Execute a single ranking pass with the given candidate order. - - Returns a tuple of (ranking, explanation, scores) or None if the ranking failed. - The ranking is 1-indexed based on the order candidates were presented. - Scores may be None if parsing failed but ranking succeeded. """ + debug_log_sensitive_data(f"Generating a ranking for {user_id}") ranking_context = "" - for i, (diff, speedup) in enumerate(zip(diffs, speedups, strict=False)): + for i, (diff, speedup) in enumerate(zip(data.diffs, data.speedups, strict=False)): ranking_context += f"{i + 1}. Diff:\n```diff\n{diff}\n```\nSpeedup: {speedup:.3f}\n" user_prompt = USER_PROMPT.format( ranking_context=ranking_context, - python_version=python_version or "Not available", - function_references=function_references or "Not available", + python_version=data.python_version or "Not available", + function_references=data.function_references or "Not available", ) system_message = ChatCompletionSystemMessageParam(role="system", content=SYSTEM_PROMPT) user_message = ChatCompletionUserMessageParam(role="user", content=user_prompt) - debug_log_sensitive_data(f"Pass {pass_number}: {SYSTEM_PROMPT}{user_prompt}") + debug_log_sensitive_data(f"{SYSTEM_PROMPT}{user_prompt}") messages: list[ChatCompletionMessageParam] = [system_message, user_message] try: @@ -392,47 +349,43 @@ async def _single_ranking_pass( llm=rank_model, messages=messages, call_type="ranking", - trace_id=trace_id, + trace_id=data.trace_id, user_id=user_id, context={ - "num_candidates": len(diffs), - "speedups": speedups, - "python_version": python_version, - "pass_number": pass_number, + "num_candidates": len(data.diffs), + "speedups": data.speedups, + "python_version": data.python_version, }, ) await update_optimization_cost( - trace_id=trace_id, cost=calculate_llm_cost(output.raw_response, rank_model), user_id=user_id + trace_id=data.trace_id, cost=calculate_llm_cost(output.raw_response, rank_model), user_id=user_id ) except Exception as e: - logging.exception(f"Failed to generate ranking in pass {pass_number}") + logging.exception("Failed to generate ranking") sentry_sdk.capture_exception(e) - return None + return RankErrorResponseSchema(error=str(e)) - debug_log_sensitive_data(f"Pass {pass_number} AIClient optimization response:\n{output}") + debug_log_sensitive_data(f"AIClient optimization response:\n{output}") if output.raw_response.usage is not None: await asyncio.to_thread( ph, user_id, "aiservice-optimize-openai-usage", - properties={ - "model": rank_model.name, - "n": 1, - "usage": output.raw_response.usage.model_dump_json(), - "pass_number": pass_number, - }, + properties={"model": rank_model.name, "n": 1, "usage": output.raw_response.usage.model_dump_json()}, ) - num_candidates = len(diffs) + num_candidates = len(data.diffs) # Try JSON parsing first (preferred structured format) json_response = _parse_json_response(output.content, num_candidates) if json_response is not None: - logging.info(f"Pass {pass_number}: Successfully parsed JSON response") - return json_response.ranking, json_response.explanation, json_response.scores + logging.info("Successfully parsed JSON response") + return RankResponseSchema( + ranking=json_response.ranking, explanation=json_response.explanation, scores=json_response.scores + ) # Fall back to regex parsing (legacy XML-tag format) - logging.info(f"Pass {pass_number}: JSON parsing failed, falling back to regex") + logging.info("JSON parsing failed, falling back to regex") # Parse scores (optional - we can fall back to ranking if scores fail) scores: CandidateScores | None = None @@ -441,9 +394,9 @@ async def _single_ranking_pass( if scores_match is not None: scores = _parse_scores(scores_match.group(1), num_candidates) if scores is None: - logging.warning(f"Failed to parse scores in pass {pass_number}") + logging.warning("Failed to parse scores") except Exception: # noqa: BLE001 - logging.warning(f"Error parsing scores in pass {pass_number}") + logging.warning("Error parsing scores") # Parse explanation explanation = "" @@ -468,170 +421,12 @@ async def _single_ranking_pass( # If ranking is invalid but we have scores, derive from scores if scores is not None: ranking = _scores_to_ranking(scores) - logging.info(f"Pass {pass_number}: Derived ranking from scores") + logging.info("Derived ranking from scores") else: - logging.warning(f"Pass {pass_number}: No valid ranking found") - return None + logging.warning("No valid ranking found") + return RankErrorResponseSchema(error="No ranking found") - return ranking, explanation, scores - - -def _aggregate_rankings( - rankings: list[list[int]], num_candidates: int -) -> list[int]: - """Aggregate multiple rankings using Borda count method. - - Each ranking assigns points based on position: last place gets 1 point, - second-to-last gets 2, etc. Lower total score = better rank. - - Args: - rankings: List of rankings, each is a list of 1-indexed candidate positions - in decreasing order of preference (first is best). - num_candidates: Total number of candidates. - - Returns: - Aggregated ranking as 1-indexed list in decreasing order of preference. - """ - # Score accumulator for each original candidate (1-indexed) - scores: dict[int, int] = defaultdict(int) - - for ranking in rankings: - # ranking[0] is the best candidate (should get lowest score) - # ranking[-1] is the worst candidate (should get highest score) - for position, candidate in enumerate(ranking): - # Position 0 (best) gets score 1, position 1 gets 2, etc. - scores[candidate] += position + 1 - - # Sort candidates by their total score (lower is better) - sorted_candidates = sorted(range(1, num_candidates + 1), key=lambda c: scores[c]) - return sorted_candidates - - -async def rank_optimizations( # noqa: D417 - user_id: str, data: RankInputSchema, rank_model: LLM = RANKING_MODEL -) -> RankResponseSchema | RankErrorResponseSchema: - """Rank optimization candidates using multiple shuffled passes to reduce position bias. - - Runs the ranking model multiple times with different candidate orderings, - then aggregates results using Borda count to produce a final ranking. - - Parameters - ---------- - - speedups list[str]: list of speedups of optimized candidates. - - diffs list[str]: list of diffs of optimized candidates. - - python_version (tuple[int, int, int]): The python version to use. Default is (3,12,9). - - Returns: - List[Tuple[Union[str, None], Union[str, None]]]: A list of tuples where the first element is the - optimized code and the second is the explanation. - :param optimization_ids: - - """ - debug_log_sensitive_data(f"Generating a ranking for {user_id} with {NUM_RANKING_PASSES} passes") - num_candidates = len(data.diffs) - - # Generate shuffled orderings for each pass - # First pass uses original order, subsequent passes use random shuffles - orderings: list[list[int]] = [] - original_indices = list(range(num_candidates)) - orderings.append(original_indices.copy()) # Pass 1: original order - - for _ in range(NUM_RANKING_PASSES - 1): - shuffled = original_indices.copy() - random.shuffle(shuffled) - orderings.append(shuffled) - - # Run all ranking passes concurrently - async def run_pass(pass_idx: int) -> tuple[list[int], str, CandidateScores | None] | None: - ordering = orderings[pass_idx] - # Reorder diffs and speedups according to the shuffled ordering - shuffled_diffs = [data.diffs[i] for i in ordering] - shuffled_speedups = [data.speedups[i] for i in ordering] - - result = await _single_ranking_pass( - user_id=user_id, - trace_id=data.trace_id, - diffs=shuffled_diffs, - speedups=shuffled_speedups, - python_version=data.python_version, - function_references=data.function_references, - rank_model=rank_model, - pass_number=pass_idx + 1, - ) - - if result is None: - return None - - ranking, explanation, scores = result - # Map the ranking back to original indices - # ranking is 1-indexed positions in decreasing preference order - # e.g., [2, 1, 3] means shuffled candidate 2 is best, then 1, then 3 - # We need to convert to original indices - original_ranking = [ordering[r - 1] + 1 for r in ranking] # Convert back to 1-indexed original - - # Map scores back to original indices if present - original_scores: CandidateScores | None = None - if scores is not None: - original_scores = { - ordering[shuffled_idx - 1] + 1: dim_scores - for shuffled_idx, dim_scores in scores.items() - } - - return original_ranking, explanation, original_scores - - tasks = [run_pass(i) for i in range(NUM_RANKING_PASSES)] - results = await asyncio.gather(*tasks) - - # Filter out failed passes - valid_results = [r for r in results if r is not None] - - if not valid_results: - return RankErrorResponseSchema(error="All ranking passes failed") - - # Extract rankings, explanations, and scores - valid_rankings = [r[0] for r in valid_results] - explanations = [r[1] for r in valid_results] - valid_scores = [r[2] for r in valid_results if r[2] is not None] - - # Determine final ranking: prefer score-based aggregation if we have scores - final_scores: dict[int, dict[str, float]] | None = None - if valid_scores: - # Aggregate scores and derive ranking from them - final_scores, final_ranking = _aggregate_scores(valid_scores, num_candidates) - debug_log_sensitive_data( - f"Aggregated {len(valid_scores)} score sets -> final scores: {final_scores}" - ) - else: - # Fall back to Borda count on rankings - final_ranking = _aggregate_rankings(valid_rankings, num_candidates) - debug_log_sensitive_data( - f"No scores available, using Borda count on {len(valid_rankings)} rankings" - ) - - # Combine explanations from all passes - combined_explanation = explanations[0] if explanations else "" - if len(valid_results) > 1: - score_info = "" - if final_scores: - # Add score summary to explanation - score_summaries = [] - for candidate_idx in final_ranking[:3]: # Top 3 - weighted = _compute_weighted_score(final_scores[candidate_idx]) - score_summaries.append(f"#{candidate_idx}: {weighted:.2f}") - score_info = f" Top scores: {', '.join(score_summaries)}." - combined_explanation = ( - f"Aggregated from {len(valid_results)} ranking passes.{score_info} " - f"Primary explanation: {explanations[0]}" - ) - - debug_log_sensitive_data( - f"Aggregated {len(valid_results)} rankings: {valid_rankings} -> final: {final_ranking}" - ) - - return RankResponseSchema( - ranking=final_ranking, - explanation=combined_explanation, - scores=final_scores, - ) + return RankResponseSchema(ranking=ranking, explanation=explanation, scores=scores) class RankInputSchema(Schema): @@ -680,8 +475,6 @@ async def rank( scores_0_idx = {k - 1: v for k, v in ranking_response.scores.items()} response = RankResponseSchema( - explanation=ranking_response.explanation, - ranking=ranking_with_0_idx, - scores=scores_0_idx, + explanation=ranking_response.explanation, ranking=ranking_with_0_idx, scores=scores_0_idx ) return 200, response From 7386dd20b5085eb7884cb64833922a5baf5e2ef8 Mon Sep 17 00:00:00 2001 From: aseembits93 Date: Wed, 28 Jan 2026 16:02:24 -0800 Subject: [PATCH 016/184] 1-indexed ranking everywhere --- django/aiservice/ranker/ranker.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/django/aiservice/ranker/ranker.py b/django/aiservice/ranker/ranker.py index 3abd01a7d..f97418ba1 100644 --- a/django/aiservice/ranker/ranker.py +++ b/django/aiservice/ranker/ranker.py @@ -462,20 +462,15 @@ async def rank( debug_log_sensitive_data("No valid ranking was generated") return 500, RankErrorResponseSchema(error="Error generating ranking. Internal server error.") await asyncio.to_thread(ph, request.user, "ranking generated", properties={"ranking": ranking_response}) - ranking_with_0_idx = [x - 1 for x in ranking_response.ranking] if hasattr(request, "should_log_features") and request.should_log_features: - ranked_opt_ids = [data.optimization_ids[i] for i in ranking_with_0_idx] + # Convert to 0-indexed only for log_features optimization_ids lookup + ranking_0_idx = [x - 1 for x in ranking_response.ranking] + ranked_opt_ids = [data.optimization_ids[i] for i in ranking_0_idx] await log_features( trace_id=data.trace_id, user_id=request.user, ranking={"ranking": ranked_opt_ids, "explanation": ranking_response.explanation}, ) - # Convert scores to 0-indexed if present - scores_0_idx: dict[int, dict[str, float]] | None = None - if ranking_response.scores: - scores_0_idx = {k - 1: v for k, v in ranking_response.scores.items()} - response = RankResponseSchema( - explanation=ranking_response.explanation, ranking=ranking_with_0_idx, scores=scores_0_idx - ) - return 200, response + # Return 1-indexed ranking and scores + return 200, ranking_response From f1b6fbf7373c02c7f77b0d48ac222ccda6d80498 Mon Sep 17 00:00:00 2001 From: aseembits93 Date: Wed, 28 Jan 2026 16:13:28 -0800 Subject: [PATCH 017/184] adding back the instructions --- django/aiservice/ranker/ranker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/django/aiservice/ranker/ranker.py b/django/aiservice/ranker/ranker.py index f97418ba1..e1a638185 100644 --- a/django/aiservice/ranker/ranker.py +++ b/django/aiservice/ranker/ranker.py @@ -80,9 +80,12 @@ You are also provided with the following information. - 1-3: Risky, may break edge cases or change behavior ## Rules: -- DISREGARD new dependencies - they are already installed -- Micro-optimizations (inlining, localizing variables, attribute lookup optimizations) should score LOW on speedup_quality unless speedup is very high -- The goal is acceptance of the pull request by an expert engineer +- DISREGARD the fact that new dependencies could be introduced in the diff, these are already installed in the system. +- Prefer optimizations with higher speedup ratios. If the higher speedups happen due to strange hacks or micro-optimizations or something an expert won't write then prefer it less. +- Prefer optimizations which contain precise diffs unless the speedup provided is very high. Larger pull requests are typically harder to accept then more precise smaller pull requests. +- Introduction of the `global` and `nonlocal` keywords in optimizations is **HIGHLY DISCOURAGED** as it reduces code clarity and maintainability, introduces hidden dependencies, can cause subtle bugs and breaks modularity. **DO NOT** prefer such optimizations. +- Replacement of `isinstance()` checks with `type()` checks is **HIGHLY DISCOURAGED** as `isinstance()` correctly handles inheritance and subclasses, while `type()` checks are incorrect for subclass instances and represent a micro-optimization that should be avoided. Do not prefer such optimizations. +- If the only optimizations are micro-optimizations like inlining a function call, or localizing variables or methods (attribute lookup optimizations), do not prefer the optimizations. The performance improvements are minimal and come at a substantial cost to readability. ## Response Format From 04197195e8e77333f2bf37bd570855ef25e8d392 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:09:47 +0000 Subject: [PATCH 018/184] Store instrumented performance tests in feature logging (#2330) ## Summary - Add `instrumented_perf_test` field to `OptimizationFeatures` model - Update `log_features` function to accept and store performance instrumented tests --------- Co-authored-by: Sarthak Agarwal --- django/aiservice/log_features/log_features.py | 8 +++++++- django/aiservice/log_features/models.py | 1 + django/aiservice/testgen/testgen.py | 1 + .../migration.sql | 2 ++ js/common/prisma/schema.prisma | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 js/common/prisma/migrations/20260129080055_add_instrumented_perf_test/migration.sql diff --git a/django/aiservice/log_features/log_features.py b/django/aiservice/log_features/log_features.py index 4b75f1ade..ec29c9d31 100644 --- a/django/aiservice/log_features/log_features.py +++ b/django/aiservice/log_features/log_features.py @@ -36,6 +36,7 @@ def log_features( is_correct: dict[str, bool | None] | None = None, generated_tests: list[str] | None = None, instrumented_generated_tests: list[str] | None = None, + instrumented_perf_tests: list[str] | None = None, test_framework: str | None = None, datetime: dt.datetime | None = None, aiservice_commit: str | None = None, @@ -63,7 +64,8 @@ def log_features( :param optimized_runtime: The time taken in ns to run the optimized code. :param is_correct: Behavioural correctness of the optimized code. :param generated_tests: Generated tests for the optimized code, output of the aiservice test generation endpoint. - :param instrumented_generated_tests: Instrumented generated tests for the optimized code, output of the aiservice test generation endpoint. + :param instrumented_generated_tests: Behavior instrumented tests for the optimized code. + :param instrumented_perf_tests: Performance instrumented tests for the optimized code. :param test_framework: The test framework used to generate the tests. :param datetime: The datetime of the optimization run. Should be calculated by the aiservice. :param aiservice_commit: The commit hash of the AIService code used for the feature logging. hopefully should be the same for the entire run. @@ -88,6 +90,7 @@ def log_features( "is_correct": is_correct, "generated_test": generated_tests, "instrumented_generated_test": instrumented_generated_tests, + "instrumented_perf_test": instrumented_perf_tests, "test_framework": test_framework, "created_at": datetime, "aiservice_commit_id": aiservice_commit, @@ -152,6 +155,9 @@ def log_features( if instrumented_generated_tests: f.instrumented_generated_test = (f.instrumented_generated_test or []) + instrumented_generated_tests update_fields.append("instrumented_generated_test") + if instrumented_perf_tests: + f.instrumented_perf_test = (f.instrumented_perf_test or []) + instrumented_perf_tests + update_fields.append("instrumented_perf_test") if update_fields: f.save(update_fields=update_fields) diff --git a/django/aiservice/log_features/models.py b/django/aiservice/log_features/models.py index 168f5074e..7f1b7a055 100644 --- a/django/aiservice/log_features/models.py +++ b/django/aiservice/log_features/models.py @@ -27,6 +27,7 @@ class OptimizationFeatures(models.Model): is_correct = models.JSONField(null=True, blank=True) generated_test = ArrayField(models.TextField(), null=True, blank=True) instrumented_generated_test = ArrayField(models.TextField(), null=True, blank=True) + instrumented_perf_test = ArrayField(models.TextField(), null=True, blank=True) test_framework = models.CharField(max_length=32, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True) aiservice_commit_id = models.CharField(max_length=50, null=True, blank=True) diff --git a/django/aiservice/testgen/testgen.py b/django/aiservice/testgen/testgen.py index af6521017..a1f3cdb15 100644 --- a/django/aiservice/testgen/testgen.py +++ b/django/aiservice/testgen/testgen.py @@ -533,6 +533,7 @@ async def testgen_python( user_id=request.user, generated_tests=[generated_test_source], instrumented_generated_tests=[instrumented_behavior_tests], + instrumented_perf_tests=[instrumented_perf_tests], test_framework=data.test_framework, metadata={ "test_timeout": data.test_timeout, diff --git a/js/common/prisma/migrations/20260129080055_add_instrumented_perf_test/migration.sql b/js/common/prisma/migrations/20260129080055_add_instrumented_perf_test/migration.sql new file mode 100644 index 000000000..203bbc239 --- /dev/null +++ b/js/common/prisma/migrations/20260129080055_add_instrumented_perf_test/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."optimization_features" ADD COLUMN "instrumented_perf_test" TEXT[]; diff --git a/js/common/prisma/schema.prisma b/js/common/prisma/schema.prisma index 792ef5580..b45a360b0 100644 --- a/js/common/prisma/schema.prisma +++ b/js/common/prisma/schema.prisma @@ -65,6 +65,7 @@ model optimization_features { user_id String? experiment_metadata Json? instrumented_generated_test String[] + instrumented_perf_test String[] pull_request Json? dependency_code String? line_profiler_results String? From 1e8010c1ce856ec618b1e1e6922a29aba97446d8 Mon Sep 17 00:00:00 2001 From: HeshamHM28 Date: Thu, 29 Jan 2026 18:40:45 +0200 Subject: [PATCH 019/184] fix tests --- .../endpoints/tests/close-pr.unit.test.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/js/cf-api/endpoints/tests/close-pr.unit.test.ts b/js/cf-api/endpoints/tests/close-pr.unit.test.ts index 917c25ffe..b25d0d5aa 100644 --- a/js/cf-api/endpoints/tests/close-pr.unit.test.ts +++ b/js/cf-api/endpoints/tests/close-pr.unit.test.ts @@ -85,7 +85,7 @@ describe("closePr", () => { body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, userId: "test-user-id", } - ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue(null) + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue(null) await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow() }) @@ -95,11 +95,11 @@ describe("closePr", () => { body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, userId: "test-user-id", } - ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") - ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") + ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ rest: { pulls: { get: jest.fn() } }, }) - ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(false) + ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(false) await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow( "not a collaborator", @@ -113,11 +113,11 @@ describe("closePr", () => { body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, userId: "test-user-id", } - ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") - ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") + ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ rest: { pulls: { - get: jest.fn().mockResolvedValue({ + get: jest.fn().mockResolvedValue({ data: { number: 123, state: "open", @@ -128,7 +128,7 @@ describe("closePr", () => { }, }, }) - ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) + ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) await expect(closePr(mockReq as Request, mockRes as Response)).rejects.toThrow( "not created by Codeflash", @@ -142,11 +142,11 @@ describe("closePr", () => { body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, userId: "test-user-id", } - ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") - ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") + ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ rest: { pulls: { - get: jest.fn().mockResolvedValue({ + get: jest.fn().mockResolvedValue({ data: { number: 123, state: "closed", @@ -157,7 +157,7 @@ describe("closePr", () => { }, }, }) - ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) + ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) await closePr(mockReq as Request, mockRes as Response) @@ -170,18 +170,18 @@ describe("closePr", () => { }) it("should successfully close a PR created by Codeflash", async () => { - const mockUpdate = jest.fn().mockResolvedValue({}) - const mockCreateComment = jest.fn().mockResolvedValue({}) + const mockUpdate = jest.fn().mockResolvedValue({}) + const mockCreateComment = jest.fn().mockResolvedValue({}) mockReq = { body: { owner: "test-owner", repo: "test-repo", pr_number: 123 }, userId: "test-user-id", } - ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") - ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ + ;(mockDependencies.userNickname as jest.Mock).mockResolvedValue("test-user") + ;(mockDependencies.getInstallationOctokitByOwner as jest.Mock).mockResolvedValue({ rest: { pulls: { - get: jest.fn().mockResolvedValue({ + get: jest.fn().mockResolvedValue({ data: { number: 123, state: "open", @@ -196,7 +196,7 @@ describe("closePr", () => { }, }, }) - ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) + ;(mockDependencies.isUserCollaborator as jest.Mock).mockResolvedValue(true) await closePr(mockReq as Request, mockRes as Response) From c24f35071925f6f3786230e871b7c6a8b01ad125 Mon Sep 17 00:00:00 2001 From: HeshamHM28 Date: Thu, 29 Jan 2026 11:28:30 -0800 Subject: [PATCH 020/184] Fix Prevent log code for paid org in the optimization feature "AI service " (#2325) Fixes Cf-1038 --- django/aiservice/authapp/auth.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/django/aiservice/authapp/auth.py b/django/aiservice/authapp/auth.py index 9b9ef44c7..1dcc0cc4d 100644 --- a/django/aiservice/authapp/auth.py +++ b/django/aiservice/authapp/auth.py @@ -6,7 +6,7 @@ from ninja.errors import HttpError from ninja.security import HttpBearer from authapp.auth_utils import hash_api_key, instance_for_api_key -from authapp.models import CFAPIKeys, Subscriptions +from authapp.models import CFAPIKeys, Organizations, Subscriptions class AuthenticatedRequest(Protocol): @@ -21,15 +21,16 @@ class AuthenticatedRequest(Protocol): should_log_features: bool # whether to log optimization features -async def check_subscription_status(user_id, tier) -> bool: +async def check_subscription_status(user_id, tier, organization_id=None) -> bool: """Check if a user has a premium subscription that doesn't require feature logging. Args: user_id: The ID of the user to check tier: The user's tier if already available + organization_id: The ID of the user's organization if available Returns: - bool: False if features should not be logged (premium user), True otherwise + bool: False if features should not be logged (premium user or paid org), True otherwise """ # If tier is already set, no need to check subscription @@ -37,6 +38,15 @@ async def check_subscription_status(user_id, tier) -> bool: return False try: + # Check if user belongs to a paid organization + if organization_id: + org = await Organizations.objects.filter(id=organization_id).afirst() + if org and org.name == "codeflash-ai": + return True + if org and org.subscription: + # Paid organization - don't log features + return False + subscription = await Subscriptions.objects.filter(user_id=user_id).afirst() if subscription and subscription.plan_type.lower() in ["pro", "enterprise"]: # Premium users for CF- don't log features @@ -65,7 +75,9 @@ class AuthBearer(HttpBearer): request.tier = api_key_instance.tier request.api_key_id = api_key_instance.id request.organization_id = api_key_instance.organization_id - request.should_log_features = await check_subscription_status(user_id=request.user, tier=request.tier) + request.should_log_features = await check_subscription_status( + user_id=request.user, tier=request.tier, organization_id=request.organization_id + ) return token print("THIS SHOULD NOT HAPPEN! More than one users found in the db with the same api key!") From 879aa93967d692b1e9345d5a0bf262cc7d2bc504 Mon Sep 17 00:00:00 2001 From: ali Date: Fri, 30 Jan 2026 19:44:31 +0200 Subject: [PATCH 021/184] fix validating js/ts code with markdown syntax --- .../validators/javascript_validator.py | 19 +++++++++ .../optimizer/test_javascript_validator.py | 41 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/django/aiservice/aiservice/validators/javascript_validator.py b/django/aiservice/aiservice/validators/javascript_validator.py index b02c0513f..2dd1681bf 100644 --- a/django/aiservice/aiservice/validators/javascript_validator.py +++ b/django/aiservice/aiservice/validators/javascript_validator.py @@ -11,6 +11,8 @@ import tree_sitter_javascript import tree_sitter_typescript from tree_sitter import Language, Parser +from aiservice.common.markdown_utils import split_markdown_code + js_parser = Parser(Language(tree_sitter_javascript.language())) ts_parser = Parser(Language(tree_sitter_typescript.language_typescript())) @@ -18,6 +20,15 @@ ts_parser = Parser(Language(tree_sitter_typescript.language_typescript())) @lru_cache(maxsize=100) def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: + if code.strip().startswith("```"): + # markdown code block + file_to_code = split_markdown_code(code, "javascript") + for _code in file_to_code.values(): + valid, _ = validate_javascript_syntax(_code) + if not valid: + return False, "Invalid syntax" + return True, None + tree = js_parser.parse(bytes(code, "utf8")) has_error = tree.root_node.has_error if has_error: @@ -27,6 +38,14 @@ def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: @lru_cache(maxsize=100) def validate_typescript_syntax(code: str) -> tuple[bool, str | None]: + if code.strip().startswith("```"): + # markdown code block + file_to_code = split_markdown_code(code, "typescript") + for _code in file_to_code.values(): + valid, _ = validate_typescript_syntax(_code) + if not valid: + return False, "Invalid syntax" + return True, None tree = ts_parser.parse(bytes(code, "utf8")) has_error = tree.root_node.has_error if has_error: diff --git a/django/aiservice/tests/optimizer/test_javascript_validator.py b/django/aiservice/tests/optimizer/test_javascript_validator.py index 2c33688fc..81aa3f62d 100644 --- a/django/aiservice/tests/optimizer/test_javascript_validator.py +++ b/django/aiservice/tests/optimizer/test_javascript_validator.py @@ -221,3 +221,44 @@ function greet(user: User): string { """ is_valid, error = validate_typescript_syntax(code) assert isinstance(is_valid, bool) + + def test_markdown_code(self) -> None: + code = """```typescript:generateCorrelationId.ts +import { randomUUID } from "crypto" + +export function generateCorrelationId(service: string = "cf-api"): string { + const timestamp = Date.now().toString(36) + const random = Math.random().toString(36).substring(2, 8) + return `${service}-${timestamp}-${random}` +} +``` +""" + is_valid, error = validate_typescript_syntax(code) + assert isinstance(is_valid, bool) + assert is_valid + assert error is None + + def test_markdown_code_with_error(self) -> None: + code = """```typescript:generateCorrelationId.ts +import { randomUUID } from "crypto" + +export function generateCorrelationId(service: string = "cf-api"): string { + const timestamp = Date.now().toString(36) + const random = Math.random().toString(36).substring(2, 8) + return `${service}-${timestamp}-${random}` +} +``` +```typescript:generateCorrelationId_1.ts +import { randomUUID } from "crypto" + +export function generateCorrelationId(service: string default "cf-api"): string { + const timestamp = Date.now().toString(36) + const random = Math.random().toString(36).substring(2, 8) + return `${service}-${timestamp}-${random}` +} +``` +""" + is_valid, error = validate_typescript_syntax(code) + assert isinstance(is_valid, bool) + assert not is_valid + assert error is not None From 99a7a32b32d321986fac29a94d741afb18914707 Mon Sep 17 00:00:00 2001 From: ali Date: Fri, 30 Jan 2026 19:52:18 +0200 Subject: [PATCH 022/184] safer caching --- .../validators/javascript_validator.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/django/aiservice/aiservice/validators/javascript_validator.py b/django/aiservice/aiservice/validators/javascript_validator.py index 2dd1681bf..60634151f 100644 --- a/django/aiservice/aiservice/validators/javascript_validator.py +++ b/django/aiservice/aiservice/validators/javascript_validator.py @@ -18,36 +18,39 @@ js_parser = Parser(Language(tree_sitter_javascript.language())) ts_parser = Parser(Language(tree_sitter_typescript.language_typescript())) -@lru_cache(maxsize=100) +@lru_cache(maxsize=200) +def _validate(code: str, lang: str) -> bool: + parser = js_parser if lang == "js" else ts_parser + tree = parser.parse(code.encode("utf8")) + return not tree.root_node.has_error + + def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: if code.strip().startswith("```"): # markdown code block file_to_code = split_markdown_code(code, "javascript") for _code in file_to_code.values(): - valid, _ = validate_javascript_syntax(_code) + valid = _validate(_code, "js") if not valid: return False, "Invalid syntax" return True, None - tree = js_parser.parse(bytes(code, "utf8")) - has_error = tree.root_node.has_error - if has_error: + valid = _validate(code, "js") + if not valid: return False, "Invalid syntax" return True, None -@lru_cache(maxsize=100) def validate_typescript_syntax(code: str) -> tuple[bool, str | None]: if code.strip().startswith("```"): # markdown code block file_to_code = split_markdown_code(code, "typescript") for _code in file_to_code.values(): - valid, _ = validate_typescript_syntax(_code) + valid = _validate(_code, "ts") if not valid: return False, "Invalid syntax" return True, None - tree = ts_parser.parse(bytes(code, "utf8")) - has_error = tree.root_node.has_error - if has_error: + valid = _validate(code, "ts") + if not valid: return False, "Invalid syntax" return True, None From af2935f4f22f0cc5c08ecaf9485d2785cf7207c7 Mon Sep 17 00:00:00 2001 From: aseembits93 Date: Fri, 30 Jan 2026 14:42:28 -0800 Subject: [PATCH 023/184] 0-index finally --- django/aiservice/ranker/ranker.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/django/aiservice/ranker/ranker.py b/django/aiservice/ranker/ranker.py index e1a638185..ffb8c9482 100644 --- a/django/aiservice/ranker/ranker.py +++ b/django/aiservice/ranker/ranker.py @@ -465,15 +465,14 @@ async def rank( debug_log_sensitive_data("No valid ranking was generated") return 500, RankErrorResponseSchema(error="Error generating ranking. Internal server error.") await asyncio.to_thread(ph, request.user, "ranking generated", properties={"ranking": ranking_response}) + ranking_0_idx = [x - 1 for x in ranking_response.ranking] if hasattr(request, "should_log_features") and request.should_log_features: - # Convert to 0-indexed only for log_features optimization_ids lookup - ranking_0_idx = [x - 1 for x in ranking_response.ranking] ranked_opt_ids = [data.optimization_ids[i] for i in ranking_0_idx] await log_features( trace_id=data.trace_id, user_id=request.user, ranking={"ranking": ranked_opt_ids, "explanation": ranking_response.explanation}, ) - - # Return 1-indexed ranking and scores - return 200, ranking_response + return 200, RankResponseSchema( + ranking=ranking_0_idx, explanation=ranking_response.explanation, scores=ranking_response.scores + ) # we don't really use `explanation` and `score` but still returning it for future use in CLI From 07ae9db684e6c92cc1014bbb7dd69f0f07c20844 Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Fri, 30 Jan 2026 23:47:39 +0000 Subject: [PATCH 024/184] fix: improve TypeScript/JavaScript validation error messages Add better error diagnostics for TypeScript/JavaScript syntax validation: - Add line numbers and code snippets to error messages - Log warnings when markdown parsing finds no code blocks - Show the actual problematic code in error logs - Help debug "Invalid syntax" errors by showing exact location This helps diagnose issues where the API rejects code that tree-sitter parses correctly on the client side by providing more context in the error messages and logs. Co-Authored-By: Claude Opus 4.5 --- .../validators/javascript_validator.py | 86 ++++++++++++++++--- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/django/aiservice/aiservice/validators/javascript_validator.py b/django/aiservice/aiservice/validators/javascript_validator.py index 60634151f..4003a1da6 100644 --- a/django/aiservice/aiservice/validators/javascript_validator.py +++ b/django/aiservice/aiservice/validators/javascript_validator.py @@ -1,10 +1,11 @@ """JavaScript/TypeScript syntax validation. -Uses Node.js for validation when available, with a basic regex fallback. +Uses tree-sitter for validation, with support for markdown code blocks. """ from __future__ import annotations +import logging from functools import lru_cache import tree_sitter_javascript @@ -25,19 +26,61 @@ def _validate(code: str, lang: str) -> bool: return not tree.root_node.has_error +def _find_error_location(code: str, lang: str) -> str | None: + """Find the location of the first syntax error in the code.""" + parser = js_parser if lang == "js" else ts_parser + tree = parser.parse(code.encode("utf8")) + if not tree.root_node.has_error: + return None + + def find_error(node) -> tuple[int, int] | None: + if node.type == "ERROR": + return node.start_point + for child in node.children: + result = find_error(child) + if result: + return result + return None + + error_point = find_error(tree.root_node) + if error_point: + line, col = error_point + lines = code.split("\n") + if line < len(lines): + error_line = lines[line] + return f"line {line + 1}, col {col}: {error_line[:80]}" + return "unknown location" + + def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: if code.strip().startswith("```"): # markdown code block file_to_code = split_markdown_code(code, "javascript") - for _code in file_to_code.values(): - valid = _validate(_code, "js") - if not valid: - return False, "Invalid syntax" - return True, None + if not file_to_code: + logging.warning( + "No JavaScript code blocks found in markdown. " + f"Code starts with: {code[:100]!r}" + ) + # Fall through to validate the raw code + else: + for filepath, _code in file_to_code.items(): + valid = _validate(_code, "js") + if not valid: + error_loc = _find_error_location(_code, "js") + logging.error( + f"Invalid JavaScript syntax in {filepath}: {error_loc}. " + f"Code snippet: {_code[:200]!r}" + ) + return False, f"Invalid syntax at {error_loc}" + return True, None valid = _validate(code, "js") if not valid: - return False, "Invalid syntax" + error_loc = _find_error_location(code, "js") + logging.error( + f"Invalid JavaScript syntax: {error_loc}. Code snippet: {code[:200]!r}" + ) + return False, f"Invalid syntax at {error_loc}" return True, None @@ -45,12 +88,29 @@ def validate_typescript_syntax(code: str) -> tuple[bool, str | None]: if code.strip().startswith("```"): # markdown code block file_to_code = split_markdown_code(code, "typescript") - for _code in file_to_code.values(): - valid = _validate(_code, "ts") - if not valid: - return False, "Invalid syntax" - return True, None + if not file_to_code: + logging.warning( + "No TypeScript code blocks found in markdown. " + f"Code starts with: {code[:100]!r}" + ) + # Fall through to validate the raw code + else: + for filepath, _code in file_to_code.items(): + valid = _validate(_code, "ts") + if not valid: + error_loc = _find_error_location(_code, "ts") + logging.error( + f"Invalid TypeScript syntax in {filepath}: {error_loc}. " + f"Code snippet: {_code[:200]!r}" + ) + return False, f"Invalid syntax at {error_loc}" + return True, None + valid = _validate(code, "ts") if not valid: - return False, "Invalid syntax" + error_loc = _find_error_location(code, "ts") + logging.error( + f"Invalid TypeScript syntax: {error_loc}. Code snippet: {code[:200]!r}" + ) + return False, f"Invalid syntax at {error_loc}" return True, None From 8800614d1c907f2e8be6a86b74339d963164347a Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Fri, 30 Jan 2026 23:53:26 +0000 Subject: [PATCH 025/184] Add unit tests for TypeScript/JavaScript validator error reporting Tests for: - Error location reporting with line numbers and code snippets - Markdown code block parsing with various scenarios - Multiple code blocks with mixed valid/invalid content - Real-world TypeScript patterns (async, try-catch, template literals) Co-Authored-By: Claude Opus 4.5 --- .../optimizer/test_javascript_validator.py | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/django/aiservice/tests/optimizer/test_javascript_validator.py b/django/aiservice/tests/optimizer/test_javascript_validator.py index 81aa3f62d..5cfe34727 100644 --- a/django/aiservice/tests/optimizer/test_javascript_validator.py +++ b/django/aiservice/tests/optimizer/test_javascript_validator.py @@ -262,3 +262,209 @@ export function generateCorrelationId(service: string default "cf-api"): string assert isinstance(is_valid, bool) assert not is_valid assert error is not None + + +class TestErrorLocationReporting: + """Tests for error location reporting in validation errors.""" + + def test_error_includes_line_number(self) -> None: + """Test that syntax errors include line number in error message.""" + code = """function broken( { + return 123; +}""" + is_valid, error = validate_javascript_syntax(code) + assert is_valid is False + assert error is not None + assert "line" in error.lower() + + def test_error_includes_code_snippet(self) -> None: + """Test that syntax errors include code snippet in error message.""" + code = """function broken( { + return 123; +}""" + is_valid, error = validate_javascript_syntax(code) + assert is_valid is False + assert error is not None + # Error should contain part of the problematic line + assert "broken" in error or "function" in error + + def test_typescript_error_includes_line_number(self) -> None: + """Test that TypeScript syntax errors include line number.""" + code = """interface User { + name: string + age number // missing colon +}""" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is False + assert error is not None + assert "line" in error.lower() + + def test_markdown_error_includes_line_number(self) -> None: + """Test that errors in markdown code blocks include line number.""" + code = """```typescript:test.ts +function valid(): string { + return "hello"; +} + +function broken( { + return 123; +} +```""" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is False + assert error is not None + assert "line" in error.lower() + + def test_error_on_specific_line(self) -> None: + """Test that error reports correct line number for error on line 3.""" + code = """const a = 1; +const b = 2; +const c = broken(; +const d = 4;""" + is_valid, error = validate_javascript_syntax(code) + assert is_valid is False + assert error is not None + # Error should be on line 3 + assert "line 3" in error.lower() or "line 3," in error + + def test_typescript_async_function_with_template_literal(self) -> None: + """Test that async functions with template literals validate correctly.""" + code = """```typescript:src/ctl/mongo_shell_utils.ts +import * as utils from "./utils"; + +const command_args = process.argv.slice(3); + +async function execMongoEval(queryExpression, appsmithMongoURI) { + queryExpression = queryExpression.trim(); + + if (command_args.includes("--pretty")) { + queryExpression += ".pretty()"; + } + + return await utils.execCommand([ + "mongosh", + appsmithMongoURI, + `--eval=${queryExpression}`, + ]); +} +```""" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is True + assert error is None + + def test_typescript_try_catch_function(self) -> None: + """Test that functions with try-catch blocks validate correctly.""" + code = """```typescript:src/ctl/restore.ts +import fsPromises from "fs/promises"; +import path from "path"; + +async function figureOutContentsPath(root: string): Promise { + const subfolders = await fsPromises.readdir(root, { withFileTypes: true }); + + try { + await fsPromises.access(path.join(root, "manifest.json")); + return root; + } catch (error) { + // Ignore + } + + for (const subfolder of subfolders) { + if (subfolder.isDirectory()) { + try { + await fsPromises.access( + path.join(root, subfolder.name, "manifest.json"), + ); + return path.join(root, subfolder.name); + } catch (error) { + // Ignore + } + } + } + + throw new Error("Could not find the contents."); +} +```""" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is True + assert error is None + + +class TestMarkdownParsing: + """Tests for markdown code block parsing in validation.""" + + def test_empty_markdown_no_code_blocks(self) -> None: + """Test validation when markdown has no matching code blocks.""" + # This markdown has python blocks, not typescript + # Note: Raw markdown with ``` actually parses as valid TypeScript + # because backticks form template literals in TypeScript/JavaScript + code = """```python +def hello(): + return "world" +```""" + # When no typescript blocks found, it should fall through to validate raw + is_valid, error = validate_typescript_syntax(code) + # The raw markdown happens to be valid TypeScript (template literals) + # This verifies the warning is logged and fallback validation runs + assert is_valid is True + assert error is None + + def test_multiple_valid_code_blocks(self) -> None: + """Test that multiple valid code blocks all pass validation.""" + code = """```typescript:file1.ts +function add(a: number, b: number): number { + return a + b; +} +``` +```typescript:file2.ts +function multiply(a: number, b: number): number { + return a * b; +} +```""" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is True + assert error is None + + def test_one_invalid_block_fails_all(self) -> None: + """Test that one invalid block in multiple blocks fails validation.""" + code = """```typescript:valid.ts +function valid(): number { + return 42; +} +``` +```typescript:invalid.ts +function invalid( { + return broken; +} +```""" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is False + assert error is not None + + def test_javascript_markdown_blocks(self) -> None: + """Test JavaScript code in markdown blocks.""" + code = """```javascript:utils.js +function formatDate(date) { + return date.toISOString(); +} +```""" + is_valid, error = validate_javascript_syntax(code) + assert is_valid is True + assert error is None + + def test_js_shorthand_in_markdown(self) -> None: + """Test that 'js' shorthand works in markdown blocks.""" + code = """```js:utils.js +const add = (a, b) => a + b; +```""" + is_valid, error = validate_javascript_syntax(code) + assert is_valid is True + assert error is None + + def test_ts_shorthand_in_markdown(self) -> None: + """Test that 'ts' shorthand works in markdown blocks.""" + code = """```ts:utils.ts +const add = (a: number, b: number): number => a + b; +```""" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is True + assert error is None From d255a29203c649dd60a38aee08e1e3ecee7b9b6d Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Fri, 30 Jan 2026 16:00:05 -0800 Subject: [PATCH 026/184] Update django/aiservice/aiservice/validators/javascript_validator.py Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- django/aiservice/aiservice/validators/javascript_validator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/django/aiservice/aiservice/validators/javascript_validator.py b/django/aiservice/aiservice/validators/javascript_validator.py index 4003a1da6..28bd69139 100644 --- a/django/aiservice/aiservice/validators/javascript_validator.py +++ b/django/aiservice/aiservice/validators/javascript_validator.py @@ -46,9 +46,12 @@ def _find_error_location(code: str, lang: str) -> str | None: if error_point: line, col = error_point lines = code.split("\n") + if line < len(lines): + error_line = lines[line] if line < len(lines): error_line = lines[line] return f"line {line + 1}, col {col}: {error_line[:80]}" + return f"line {line + 1}, col {col}" return "unknown location" From 795c157d12181f8e4199878d83041d6e57e05806 Mon Sep 17 00:00:00 2001 From: HeshamHM28 Date: Fri, 30 Jan 2026 16:04:29 -0800 Subject: [PATCH 027/184] Fix Line Profiler query (#2338) # Pull Request Checklist ## Description - [ ] **Description of PR**: Clear and concise description of what this PR accomplishes - [ ] **Breaking Changes**: Document any breaking changes (if applicable) - [ ] **Related Issues**: Link to any related issues or tickets ## Testing - [ ] **Test cases Attached**: All relevant test cases have been added/updated - [ ] **Manual Testing**: Manual testing completed for the changes ## Monitoring & Debugging - [ ] **Logging in place**: Appropriate logging has been added for debugging user issues - [ ] **Sentry will be able to catch errors**: Error handling ensures Sentry can capture and report errors - [ ] **Avoid Dev based/Prisma logging**: No development-only or Prisma-specific logging in production code ## Configuration - [ ] **Env variables newly added**: Any new environment variables are documented in .env.example file or mentioned in description --- ## Additional Notes --- .../src/app/(dashboard)/review-optimizations/[traceId]/action.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts index 03e07421b..7aa68db1a 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts @@ -154,7 +154,6 @@ export async function getOptimizationEventById({ const repoIds = (await getRepositoriesForAccountCached(payload)).repoIds const where: any = { trace_id, - is_staging: true, ...buildOptimizationOrCondition(payload, repoIds), } const event = await prisma.optimization_events.findFirst({ From b2fb58eba6a60119d4607d9d87a0b4f3ceac823c Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Sat, 31 Jan 2026 00:29:13 +0000 Subject: [PATCH 028/184] Fix invalid JavaScript import syntax for class methods When generating test imports for class methods like `Validator.validateRequest`, the previous code produced invalid JavaScript: const { Validator.validateRequest } = require('../middlewares/Validator'); This is invalid because dots are not allowed in destructuring patterns. The fix: - Add _generate_import_statement() function to detect class methods (names with dots) - For class methods: generate `const ClassName = require('...')` - For simple functions: keep destructuring `const { funcName } = require('...')` - Update prompt templates to use {import_statement} placeholder Includes unit tests for the new import generation logic. --- .../testgen/execute_async_user_prompt.md | 4 +- .../prompts/testgen/execute_user_prompt.md | 4 +- django/aiservice/languages/js_ts/testgen.py | 42 ++++++- .../optimizer/test_javascript_testgen.py | 108 ++++++++++++++++++ 4 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 django/aiservice/tests/optimizer/test_javascript_testgen.py diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md index 84cb42963..fe2ac4551 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md @@ -12,13 +12,13 @@ Using the {test_framework} testing framework, write a test suite for the followi **CRITICAL: Use this exact import statement (do not modify the path):** ```javascript -const {{ {function_name} }} = require('{module_path}'); +{import_statement} ``` **Template to Follow:** ```javascript // imports -const {{ {function_name} }} = require('{module_path}'); +{import_statement} // unit tests describe('{function_name}', () => {{ diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md index c9ef67ad3..f1d5a8c62 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md @@ -7,13 +7,13 @@ Using the {test_framework} testing framework, write a test suite for the followi **CRITICAL: Use this exact import statement (do not modify the path):** ```javascript -const {{ {function_name} }} = require('{module_path}'); +{import_statement} ``` **Template to Follow:** ```javascript // imports -const {{ {function_name} }} = require('{module_path}'); +{import_statement} // unit tests describe('{function_name}', () => {{ diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index a50406027..e9e7b967a 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -51,6 +51,40 @@ JS_EXECUTE_ASYNC_USER_PROMPT = (JS_PROMPTS_DIR / "execute_async_user_prompt.md") JS_PATTERN = re.compile(r"^```(?:javascript|js|typescript|ts)?\s*\n(.*?)\n```", re.MULTILINE | re.DOTALL) +def _generate_import_statement(function_name: str, module_path: str) -> tuple[str, str]: + """Generate appropriate import statement and accessor for JavaScript/TypeScript. + + For class methods (e.g., 'Validator.validateRequest'), generates: + import: const Validator = require('../middlewares/Validator'); + accessor: Validator.validateRequest + + For simple functions (e.g., 'execMongoEval'), generates: + import: const { execMongoEval } = require('../ctl/utils'); + accessor: execMongoEval + + Args: + function_name: Name of the function, may include class name (e.g., 'Class.method') + module_path: Import path for the module + + Returns: + Tuple of (import_statement, function_accessor) + + """ + if "." in function_name: + # Class method: import the class, then access the method + parts = function_name.split(".") + class_name = parts[0] + import_statement = f"const {class_name} = require('{module_path}');" + # Keep the full accessor for calling the method + function_accessor = function_name + else: + # Simple function: use destructuring import + import_statement = f"const {{ {function_name} }} = require('{module_path}');" + function_accessor = function_name + + return import_statement, function_accessor + + def build_javascript_prompt( function_name: str, function_code: str, module_path: str, test_framework: str, is_async: bool ) -> tuple[list[ChatCompletionMessageParam], str]: @@ -76,19 +110,23 @@ def build_javascript_prompt( user_prompt = JS_EXECUTE_USER_PROMPT posthog_event_suffix = "" + # Generate proper import statement for the function + import_statement, function_accessor = _generate_import_statement(function_name, module_path) + # Format prompts system_message: ChatCompletionMessageParam = { "role": "system", - "content": system_prompt.format(function_name=function_name), + "content": system_prompt.format(function_name=function_accessor), } user_message: ChatCompletionMessageParam = { "role": "user", "content": user_prompt.format( test_framework=test_framework, - function_name=function_name, + function_name=function_accessor, function_code=function_code, module_path=module_path, + import_statement=import_statement, package_comment="", ), } diff --git a/django/aiservice/tests/optimizer/test_javascript_testgen.py b/django/aiservice/tests/optimizer/test_javascript_testgen.py new file mode 100644 index 000000000..3d1a71c50 --- /dev/null +++ b/django/aiservice/tests/optimizer/test_javascript_testgen.py @@ -0,0 +1,108 @@ +"""Tests for JavaScript/TypeScript test generation.""" + +from languages.js_ts.testgen import _generate_import_statement, build_javascript_prompt + + +class TestGenerateImportStatement: + """Tests for the _generate_import_statement function.""" + + def test_simple_function_import(self) -> None: + """Test import generation for a simple function.""" + import_stmt, accessor = _generate_import_statement( + "execMongoEval", "../ctl/mongo_shell_utils" + ) + assert import_stmt == "const { execMongoEval } = require('../ctl/mongo_shell_utils');" + assert accessor == "execMongoEval" + + def test_class_method_import(self) -> None: + """Test import generation for a class method.""" + import_stmt, accessor = _generate_import_statement( + "Validator.validateRequest", "../middlewares/Validator" + ) + assert import_stmt == "const Validator = require('../middlewares/Validator');" + assert accessor == "Validator.validateRequest" + + def test_nested_class_method_import(self) -> None: + """Test import generation for a deeply nested method.""" + import_stmt, accessor = _generate_import_statement( + "DSLController.getLatestDSLVersion", "../controllers/Dsl/DslController" + ) + assert import_stmt == "const DSLController = require('../controllers/Dsl/DslController');" + assert accessor == "DSLController.getLatestDSLVersion" + + def test_function_with_underscore(self) -> None: + """Test import generation for a function with underscores.""" + import_stmt, accessor = _generate_import_statement( + "get_git_root", "../utils/git" + ) + assert import_stmt == "const { get_git_root } = require('../utils/git');" + assert accessor == "get_git_root" + + def test_static_method_import(self) -> None: + """Test import generation for what looks like a static method.""" + import_stmt, accessor = _generate_import_statement( + "Utils.formatDate", "../utils/Utils" + ) + assert import_stmt == "const Utils = require('../utils/Utils');" + assert accessor == "Utils.formatDate" + + +class TestBuildJavascriptPrompt: + """Tests for the build_javascript_prompt function.""" + + def test_simple_function_prompt_has_valid_import(self) -> None: + """Test that prompts for simple functions have valid JS import syntax.""" + messages, _ = build_javascript_prompt( + function_name="execMongoEval", + function_code="async function execMongoEval() { return true; }", + module_path="../ctl/utils", + test_framework="jest", + is_async=True, + ) + user_content = messages[1]["content"] + # Should contain valid destructuring import + assert "const { execMongoEval }" in user_content + # Should NOT contain invalid syntax with dots in destructuring + assert "const { execMongoEval." not in user_content + + def test_class_method_prompt_has_valid_import(self) -> None: + """Test that prompts for class methods have valid JS import syntax.""" + messages, _ = build_javascript_prompt( + function_name="Validator.validateRequest", + function_code="class Validator { validateRequest() {} }", + module_path="../middlewares/Validator", + test_framework="jest", + is_async=False, + ) + user_content = messages[1]["content"] + # Should contain valid class import + assert "const Validator = require('../middlewares/Validator');" in user_content + # Should NOT contain invalid syntax like { Validator.validateRequest } + assert "{ Validator.validateRequest }" not in user_content + + def test_async_prompt_has_valid_import(self) -> None: + """Test that async prompts also have valid import syntax.""" + messages, suffix = build_javascript_prompt( + function_name="Controller.asyncMethod", + function_code="class Controller { async asyncMethod() {} }", + module_path="../controllers/Controller", + test_framework="jest", + is_async=True, + ) + assert suffix == "async-" + user_content = messages[1]["content"] + # Should contain valid class import + assert "const Controller = require('../controllers/Controller');" in user_content + + def test_prompt_contains_function_accessor(self) -> None: + """Test that the prompt contains the correct function accessor.""" + messages, _ = build_javascript_prompt( + function_name="MyClass.myMethod", + function_code="class MyClass { myMethod() {} }", + module_path="../MyClass", + test_framework="jest", + is_async=False, + ) + user_content = messages[1]["content"] + # The function accessor should be used in describe blocks + assert "describe('MyClass.myMethod'" in user_content From 09e6a1710f781f3d24da1dce764428facea395ba Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Sat, 31 Jan 2026 00:33:14 +0000 Subject: [PATCH 029/184] Address review: add validation for edge cases in import generation - Add _is_valid_js_identifier() to check for reserved words (module, exports, prototype, etc.) - Only use class import pattern for single-dot names where class name is valid identifier - Fall back to module import for: - Multiple dots (e.g., Constructor.prototype.method) - Reserved words (e.g., module.exports) - Add comprehensive tests for edge cases --- django/aiservice/languages/js_ts/testgen.py | 52 +++++++++++--- .../optimizer/test_javascript_testgen.py | 69 +++++++++++++++++-- 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index e9e7b967a..4d29c7367 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -51,6 +51,22 @@ JS_EXECUTE_ASYNC_USER_PROMPT = (JS_PROMPTS_DIR / "execute_async_user_prompt.md") JS_PATTERN = re.compile(r"^```(?:javascript|js|typescript|ts)?\s*\n(.*?)\n```", re.MULTILINE | re.DOTALL) +def _is_valid_js_identifier(name: str) -> bool: + """Check if a name is a valid JavaScript identifier (not a reserved word).""" + # Reserved words that cannot be used as variable names + reserved_words = { + "module", "exports", "require", "global", "process", # Node.js globals + "prototype", "constructor", "__proto__", # Object properties + "break", "case", "catch", "continue", "debugger", "default", "delete", + "do", "else", "finally", "for", "function", "if", "in", "instanceof", + "new", "return", "switch", "this", "throw", "try", "typeof", "var", + "void", "while", "with", "class", "const", "enum", "export", "extends", + "import", "super", "implements", "interface", "let", "package", "private", + "protected", "public", "static", "yield", "null", "true", "false", + } + return name not in reserved_words and name.isidentifier() + + def _generate_import_statement(function_name: str, module_path: str) -> tuple[str, str]: """Generate appropriate import statement and accessor for JavaScript/TypeScript. @@ -62,6 +78,9 @@ def _generate_import_statement(function_name: str, module_path: str) -> tuple[st import: const { execMongoEval } = require('../ctl/utils'); accessor: execMongoEval + For unsupported patterns (e.g., 'module.exports', 'Constructor.prototype.method'), + falls back to default module import. + Args: function_name: Name of the function, may include class name (e.g., 'Class.method') module_path: Import path for the module @@ -70,19 +89,32 @@ def _generate_import_statement(function_name: str, module_path: str) -> tuple[st Tuple of (import_statement, function_accessor) """ - if "." in function_name: - # Class method: import the class, then access the method - parts = function_name.split(".") - class_name = parts[0] - import_statement = f"const {class_name} = require('{module_path}');" - # Keep the full accessor for calling the method - function_accessor = function_name - else: + parts = function_name.split(".") + + # Handle single-dot ClassName.methodName pattern + if len(parts) == 2: + class_name, method_name = parts + # Only use class import if both parts are valid identifiers + # and the class name is not a reserved word + if _is_valid_js_identifier(class_name) and method_name.isidentifier(): + import_statement = f"const {class_name} = require('{module_path}');" + return import_statement, function_name + + # For simple functions (no dot) or unsupported patterns, + # use default module import and let LLM figure out the accessor + if len(parts) == 1 and parts[0].isidentifier(): # Simple function: use destructuring import import_statement = f"const {{ {function_name} }} = require('{module_path}');" - function_accessor = function_name + return import_statement, function_name - return import_statement, function_accessor + # Fallback for complex patterns: import the module as a whole + # Extract a safe module name from the path + module_name = module_path.rstrip("/").split("/")[-1].replace("-", "_").replace(".", "_") + if not module_name.isidentifier(): + module_name = "mod" + import_statement = f"const {module_name} = require('{module_path}');" + # For complex patterns, use the full function_name as accessor + return import_statement, function_name def build_javascript_prompt( diff --git a/django/aiservice/tests/optimizer/test_javascript_testgen.py b/django/aiservice/tests/optimizer/test_javascript_testgen.py index 3d1a71c50..5912ca7a4 100644 --- a/django/aiservice/tests/optimizer/test_javascript_testgen.py +++ b/django/aiservice/tests/optimizer/test_javascript_testgen.py @@ -1,6 +1,36 @@ """Tests for JavaScript/TypeScript test generation.""" -from languages.js_ts.testgen import _generate_import_statement, build_javascript_prompt +from languages.js_ts.testgen import ( + _generate_import_statement, + _is_valid_js_identifier, + build_javascript_prompt, +) + + +class TestIsValidJsIdentifier: + """Tests for the _is_valid_js_identifier function.""" + + def test_valid_identifier(self) -> None: + """Test that valid identifiers return True.""" + assert _is_valid_js_identifier("MyClass") is True + assert _is_valid_js_identifier("validName") is True + assert _is_valid_js_identifier("_private") is True + assert _is_valid_js_identifier("name123") is True + + def test_reserved_words(self) -> None: + """Test that reserved words return False.""" + assert _is_valid_js_identifier("module") is False + assert _is_valid_js_identifier("exports") is False + assert _is_valid_js_identifier("prototype") is False + assert _is_valid_js_identifier("constructor") is False + assert _is_valid_js_identifier("class") is False + assert _is_valid_js_identifier("function") is False + + def test_invalid_identifiers(self) -> None: + """Test that invalid identifiers return False.""" + assert _is_valid_js_identifier("123invalid") is False + assert _is_valid_js_identifier("has-dash") is False + assert _is_valid_js_identifier("has space") is False class TestGenerateImportStatement: @@ -23,12 +53,13 @@ class TestGenerateImportStatement: assert accessor == "Validator.validateRequest" def test_nested_class_method_import(self) -> None: - """Test import generation for a deeply nested method.""" + """Test import generation for a deeply nested method - falls back to module import.""" import_stmt, accessor = _generate_import_statement( - "DSLController.getLatestDSLVersion", "../controllers/Dsl/DslController" + "Constructor.prototype.method", "../utils/Constructor" ) - assert import_stmt == "const DSLController = require('../controllers/Dsl/DslController');" - assert accessor == "DSLController.getLatestDSLVersion" + # Multiple dots should fall back to module import + assert "Constructor" in import_stmt + assert accessor == "Constructor.prototype.method" def test_function_with_underscore(self) -> None: """Test import generation for a function with underscores.""" @@ -46,6 +77,34 @@ class TestGenerateImportStatement: assert import_stmt == "const Utils = require('../utils/Utils');" assert accessor == "Utils.formatDate" + def test_reserved_word_class_name_falls_back(self) -> None: + """Test that reserved words like 'module' fall back to module import.""" + import_stmt, accessor = _generate_import_statement( + "module.exports", "../utils" + ) + # 'module' is a reserved word, should fall back + assert "module" not in import_stmt.split("=")[0] # Not used as variable name + assert accessor == "module.exports" + + def test_prototype_pattern_falls_back(self) -> None: + """Test that prototype patterns fall back correctly.""" + import_stmt, accessor = _generate_import_statement( + "Array.prototype.map", "../polyfills" + ) + # 'prototype' in method name should still work for single dot + # but Array.prototype.map has multiple dots, so falls back + assert "polyfills" in import_stmt + assert accessor == "Array.prototype.map" + + def test_exports_reserved_word(self) -> None: + """Test that 'exports' as class name falls back.""" + import_stmt, accessor = _generate_import_statement( + "exports.helper", "../utils" + ) + # 'exports' is reserved, should fall back + assert "const exports" not in import_stmt + assert accessor == "exports.helper" + class TestBuildJavascriptPrompt: """Tests for the build_javascript_prompt function.""" From a394db33824a975198cbbda1cd6ccc277a07f564 Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Fri, 30 Jan 2026 20:00:11 -0500 Subject: [PATCH 030/184] formatting --- .../validators/javascript_validator.py | 24 ++------ django/aiservice/languages/js_ts/testgen.py | 61 ++++++++++++++++--- .../optimizer/test_javascript_testgen.py | 38 +++--------- 3 files changed, 68 insertions(+), 55 deletions(-) diff --git a/django/aiservice/aiservice/validators/javascript_validator.py b/django/aiservice/aiservice/validators/javascript_validator.py index 28bd69139..e17caf8f4 100644 --- a/django/aiservice/aiservice/validators/javascript_validator.py +++ b/django/aiservice/aiservice/validators/javascript_validator.py @@ -60,10 +60,7 @@ def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: # markdown code block file_to_code = split_markdown_code(code, "javascript") if not file_to_code: - logging.warning( - "No JavaScript code blocks found in markdown. " - f"Code starts with: {code[:100]!r}" - ) + logging.warning(f"No JavaScript code blocks found in markdown. Code starts with: {code[:100]!r}") # Fall through to validate the raw code else: for filepath, _code in file_to_code.items(): @@ -71,8 +68,7 @@ def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: if not valid: error_loc = _find_error_location(_code, "js") logging.error( - f"Invalid JavaScript syntax in {filepath}: {error_loc}. " - f"Code snippet: {_code[:200]!r}" + f"Invalid JavaScript syntax in {filepath}: {error_loc}. Code snippet: {_code[:200]!r}" ) return False, f"Invalid syntax at {error_loc}" return True, None @@ -80,9 +76,7 @@ def validate_javascript_syntax(code: str) -> tuple[bool, str | None]: valid = _validate(code, "js") if not valid: error_loc = _find_error_location(code, "js") - logging.error( - f"Invalid JavaScript syntax: {error_loc}. Code snippet: {code[:200]!r}" - ) + logging.error(f"Invalid JavaScript syntax: {error_loc}. Code snippet: {code[:200]!r}") return False, f"Invalid syntax at {error_loc}" return True, None @@ -92,10 +86,7 @@ def validate_typescript_syntax(code: str) -> tuple[bool, str | None]: # markdown code block file_to_code = split_markdown_code(code, "typescript") if not file_to_code: - logging.warning( - "No TypeScript code blocks found in markdown. " - f"Code starts with: {code[:100]!r}" - ) + logging.warning(f"No TypeScript code blocks found in markdown. Code starts with: {code[:100]!r}") # Fall through to validate the raw code else: for filepath, _code in file_to_code.items(): @@ -103,8 +94,7 @@ def validate_typescript_syntax(code: str) -> tuple[bool, str | None]: if not valid: error_loc = _find_error_location(_code, "ts") logging.error( - f"Invalid TypeScript syntax in {filepath}: {error_loc}. " - f"Code snippet: {_code[:200]!r}" + f"Invalid TypeScript syntax in {filepath}: {error_loc}. Code snippet: {_code[:200]!r}" ) return False, f"Invalid syntax at {error_loc}" return True, None @@ -112,8 +102,6 @@ def validate_typescript_syntax(code: str) -> tuple[bool, str | None]: valid = _validate(code, "ts") if not valid: error_loc = _find_error_location(code, "ts") - logging.error( - f"Invalid TypeScript syntax: {error_loc}. Code snippet: {code[:200]!r}" - ) + logging.error(f"Invalid TypeScript syntax: {error_loc}. Code snippet: {code[:200]!r}") return False, f"Invalid syntax at {error_loc}" return True, None diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index 4d29c7367..0d4125baa 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -55,14 +55,59 @@ def _is_valid_js_identifier(name: str) -> bool: """Check if a name is a valid JavaScript identifier (not a reserved word).""" # Reserved words that cannot be used as variable names reserved_words = { - "module", "exports", "require", "global", "process", # Node.js globals - "prototype", "constructor", "__proto__", # Object properties - "break", "case", "catch", "continue", "debugger", "default", "delete", - "do", "else", "finally", "for", "function", "if", "in", "instanceof", - "new", "return", "switch", "this", "throw", "try", "typeof", "var", - "void", "while", "with", "class", "const", "enum", "export", "extends", - "import", "super", "implements", "interface", "let", "package", "private", - "protected", "public", "static", "yield", "null", "true", "false", + "module", + "exports", + "require", + "global", + "process", # Node.js globals + "prototype", + "constructor", + "__proto__", # Object properties + "break", + "case", + "catch", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "finally", + "for", + "function", + "if", + "in", + "instanceof", + "new", + "return", + "switch", + "this", + "throw", + "try", + "typeof", + "var", + "void", + "while", + "with", + "class", + "const", + "enum", + "export", + "extends", + "import", + "super", + "implements", + "interface", + "let", + "package", + "private", + "protected", + "public", + "static", + "yield", + "null", + "true", + "false", } return name not in reserved_words and name.isidentifier() diff --git a/django/aiservice/tests/optimizer/test_javascript_testgen.py b/django/aiservice/tests/optimizer/test_javascript_testgen.py index 5912ca7a4..d85b416e8 100644 --- a/django/aiservice/tests/optimizer/test_javascript_testgen.py +++ b/django/aiservice/tests/optimizer/test_javascript_testgen.py @@ -1,10 +1,6 @@ """Tests for JavaScript/TypeScript test generation.""" -from languages.js_ts.testgen import ( - _generate_import_statement, - _is_valid_js_identifier, - build_javascript_prompt, -) +from languages.js_ts.testgen import _generate_import_statement, _is_valid_js_identifier, build_javascript_prompt class TestIsValidJsIdentifier: @@ -38,59 +34,45 @@ class TestGenerateImportStatement: def test_simple_function_import(self) -> None: """Test import generation for a simple function.""" - import_stmt, accessor = _generate_import_statement( - "execMongoEval", "../ctl/mongo_shell_utils" - ) + import_stmt, accessor = _generate_import_statement("execMongoEval", "../ctl/mongo_shell_utils") assert import_stmt == "const { execMongoEval } = require('../ctl/mongo_shell_utils');" assert accessor == "execMongoEval" def test_class_method_import(self) -> None: """Test import generation for a class method.""" - import_stmt, accessor = _generate_import_statement( - "Validator.validateRequest", "../middlewares/Validator" - ) + import_stmt, accessor = _generate_import_statement("Validator.validateRequest", "../middlewares/Validator") assert import_stmt == "const Validator = require('../middlewares/Validator');" assert accessor == "Validator.validateRequest" def test_nested_class_method_import(self) -> None: """Test import generation for a deeply nested method - falls back to module import.""" - import_stmt, accessor = _generate_import_statement( - "Constructor.prototype.method", "../utils/Constructor" - ) + import_stmt, accessor = _generate_import_statement("Constructor.prototype.method", "../utils/Constructor") # Multiple dots should fall back to module import assert "Constructor" in import_stmt assert accessor == "Constructor.prototype.method" def test_function_with_underscore(self) -> None: """Test import generation for a function with underscores.""" - import_stmt, accessor = _generate_import_statement( - "get_git_root", "../utils/git" - ) + import_stmt, accessor = _generate_import_statement("get_git_root", "../utils/git") assert import_stmt == "const { get_git_root } = require('../utils/git');" assert accessor == "get_git_root" def test_static_method_import(self) -> None: """Test import generation for what looks like a static method.""" - import_stmt, accessor = _generate_import_statement( - "Utils.formatDate", "../utils/Utils" - ) + import_stmt, accessor = _generate_import_statement("Utils.formatDate", "../utils/Utils") assert import_stmt == "const Utils = require('../utils/Utils');" assert accessor == "Utils.formatDate" def test_reserved_word_class_name_falls_back(self) -> None: """Test that reserved words like 'module' fall back to module import.""" - import_stmt, accessor = _generate_import_statement( - "module.exports", "../utils" - ) + import_stmt, accessor = _generate_import_statement("module.exports", "../utils") # 'module' is a reserved word, should fall back assert "module" not in import_stmt.split("=")[0] # Not used as variable name assert accessor == "module.exports" def test_prototype_pattern_falls_back(self) -> None: """Test that prototype patterns fall back correctly.""" - import_stmt, accessor = _generate_import_statement( - "Array.prototype.map", "../polyfills" - ) + import_stmt, accessor = _generate_import_statement("Array.prototype.map", "../polyfills") # 'prototype' in method name should still work for single dot # but Array.prototype.map has multiple dots, so falls back assert "polyfills" in import_stmt @@ -98,9 +80,7 @@ class TestGenerateImportStatement: def test_exports_reserved_word(self) -> None: """Test that 'exports' as class name falls back.""" - import_stmt, accessor = _generate_import_statement( - "exports.helper", "../utils" - ) + import_stmt, accessor = _generate_import_statement("exports.helper", "../utils") # 'exports' is reserved, should fall back assert "const exports" not in import_stmt assert accessor == "exports.helper" From bf8d8efd5f8b1284e3e3c194d229d92ec41cca28 Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Fri, 30 Jan 2026 20:04:28 -0500 Subject: [PATCH 031/184] Update prek.yaml --- .github/workflows/prek.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prek.yaml b/.github/workflows/prek.yaml index 018477fc9..598f9bd65 100644 --- a/.github/workflows/prek.yaml +++ b/.github/workflows/prek.yaml @@ -4,6 +4,7 @@ on: [pull_request] jobs: prek: runs-on: ubuntu-latest + continue-on-error: true steps: - uses: actions/checkout@v5 with: From 476bbc230596f0013954b538e0c3deb601a68c8a Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Fri, 30 Jan 2026 20:10:52 -0500 Subject: [PATCH 032/184] for now --- .github/workflows/codeflash-aiservice.yaml | 3 ++- .github/workflows/django-unit-tests.yaml | 3 ++- .github/workflows/end-to-end-tests.yaml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeflash-aiservice.yaml b/.github/workflows/codeflash-aiservice.yaml index 030658cbd..ce88790f2 100644 --- a/.github/workflows/codeflash-aiservice.yaml +++ b/.github/workflows/codeflash-aiservice.yaml @@ -54,6 +54,7 @@ jobs: if: needs.check-changes.outputs.should-run == 'true' name: Wait for prek checks runs-on: ubuntu-latest + continue-on-error: true steps: - uses: lewagon/wait-on-check-action@v1.3.4 with: @@ -64,7 +65,7 @@ jobs: optimize: needs: [check-changes, wait-for-prek] - if: needs.check-changes.outputs.should-run == 'true' && github.actor != 'codeflash-ai[bot]' + if: always() && needs.check-changes.outputs.should-run == 'true' && github.actor != 'codeflash-ai[bot]' name: Optimize new code in this PR runs-on: ubuntu-latest env: diff --git a/.github/workflows/django-unit-tests.yaml b/.github/workflows/django-unit-tests.yaml index badb06213..4e612cca0 100644 --- a/.github/workflows/django-unit-tests.yaml +++ b/.github/workflows/django-unit-tests.yaml @@ -51,6 +51,7 @@ jobs: needs: check-changes if: needs.check-changes.outputs.should-run == 'true' && github.event_name == 'pull_request' runs-on: ubuntu-latest + continue-on-error: true steps: - uses: lewagon/wait-on-check-action@v1.3.4 with: @@ -64,7 +65,7 @@ jobs: if: | always() && needs.check-changes.outputs.should-run == 'true' && - (needs.wait-for-prek.result == 'success' || needs.wait-for-prek.result == 'skipped') + (needs.wait-for-prek.result == 'success' || needs.wait-for-prek.result == 'skipped' || needs.wait-for-prek.result == 'failure') runs-on: ubuntu-latest env: diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index d9c9a051c..9564110ea 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -55,6 +55,7 @@ jobs: needs: check-changes if: ${{ github.event_name == 'pull_request' && needs.check-changes.outputs.should_run == 'true' }} runs-on: ubuntu-latest + continue-on-error: true permissions: contents: read pull-requests: read @@ -70,7 +71,7 @@ jobs: unit-tests-check: name: Wait for unit tests needs: [check-changes, prek-check] - if: ${{ github.event_name == 'pull_request' && needs.check-changes.outputs.aiservice_changed == 'true' }} + if: ${{ always() && github.event_name == 'pull_request' && needs.check-changes.outputs.aiservice_changed == 'true' }} runs-on: ubuntu-latest permissions: contents: read From 911f3e6c7be746ca9ecca8f32eea4e2c6f1379d7 Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Fri, 30 Jan 2026 20:20:51 -0500 Subject: [PATCH 033/184] Remove wait-for-prek dependencies from CI workflows Prek checks should not block other workflows from running. This removes the wait-for-prek jobs entirely so unit tests, e2e tests, and codeflash optimization can run independently of pre-commit checks. --- .github/workflows/codeflash-aiservice.yaml | 18 ++-------------- .github/workflows/django-unit-tests.yaml | 23 +++----------------- .github/workflows/end-to-end-tests.yaml | 25 +++------------------- 3 files changed, 8 insertions(+), 58 deletions(-) diff --git a/.github/workflows/codeflash-aiservice.yaml b/.github/workflows/codeflash-aiservice.yaml index ce88790f2..6767feea0 100644 --- a/.github/workflows/codeflash-aiservice.yaml +++ b/.github/workflows/codeflash-aiservice.yaml @@ -49,23 +49,9 @@ jobs: - name: Skip optimization run: echo "Skipping codeflash optimization - no changes in django/aiservice/" - wait-for-prek: - needs: check-changes - if: needs.check-changes.outputs.should-run == 'true' - name: Wait for prek checks - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: lewagon/wait-on-check-action@v1.3.4 - with: - ref: ${{ github.event.pull_request.head.sha }} - check-name: prek - repo-token: ${{ secrets.GITHUB_TOKEN }} - wait-interval: 10 - optimize: - needs: [check-changes, wait-for-prek] - if: always() && needs.check-changes.outputs.should-run == 'true' && github.actor != 'codeflash-ai[bot]' + needs: [check-changes] + if: needs.check-changes.outputs.should-run == 'true' && github.actor != 'codeflash-ai[bot]' name: Optimize new code in this PR runs-on: ubuntu-latest env: diff --git a/.github/workflows/django-unit-tests.yaml b/.github/workflows/django-unit-tests.yaml index 4e612cca0..6b6240d4a 100644 --- a/.github/workflows/django-unit-tests.yaml +++ b/.github/workflows/django-unit-tests.yaml @@ -46,26 +46,9 @@ jobs: - name: Skip tests run: echo "Skipping django unit tests - no changes in django/aiservice/" - wait-for-prek: - name: Wait for prek checks - needs: check-changes - if: needs.check-changes.outputs.should-run == 'true' && github.event_name == 'pull_request' - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: lewagon/wait-on-check-action@v1.3.4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - check-name: prek - repo-token: ${{ secrets.GITHUB_TOKEN }} - wait-interval: 10 - unit-tests: - needs: [check-changes, wait-for-prek] - if: | - always() && - needs.check-changes.outputs.should-run == 'true' && - (needs.wait-for-prek.result == 'success' || needs.wait-for-prek.result == 'skipped' || needs.wait-for-prek.result == 'failure') + needs: [check-changes] + if: needs.check-changes.outputs.should-run == 'true' runs-on: ubuntu-latest env: @@ -97,7 +80,7 @@ jobs: django-unit-tests-status: runs-on: ubuntu-latest - needs: [check-changes, no-aiservice-changes, wait-for-prek, unit-tests] + needs: [check-changes, no-aiservice-changes, unit-tests] if: always() defaults: run: diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 9564110ea..23997cc81 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -50,28 +50,10 @@ jobs: - name: Mark as success run: exit 0 - prek-check: - name: Wait for prek checks - needs: check-changes - if: ${{ github.event_name == 'pull_request' && needs.check-changes.outputs.should_run == 'true' }} - runs-on: ubuntu-latest - continue-on-error: true - permissions: - contents: read - pull-requests: read - checks: read - steps: - - uses: lewagon/wait-on-check-action@v1.3.4 - with: - ref: ${{ github.event.pull_request.head.sha }} - check-name: prek - repo-token: ${{ secrets.GITHUB_TOKEN }} - wait-interval: 10 - unit-tests-check: name: Wait for unit tests - needs: [check-changes, prek-check] - if: ${{ always() && github.event_name == 'pull_request' && needs.check-changes.outputs.aiservice_changed == 'true' }} + needs: [check-changes] + if: ${{ github.event_name == 'pull_request' && needs.check-changes.outputs.aiservice_changed == 'true' }} runs-on: ubuntu-latest permissions: contents: read @@ -210,7 +192,7 @@ jobs: e2e-status: name: E2E Tests Status runs-on: ubuntu-latest - needs: [check-changes, no-changes-detected, prek-check, unit-tests-check, e2e-test] + needs: [check-changes, no-changes-detected, unit-tests-check, e2e-test] if: always() steps: - name: Check all job statuses @@ -223,7 +205,6 @@ jobs: else echo "✗ End-to-end tests workflow failed" echo "no-changes-detected: ${{ needs.no-changes-detected.result }}" - echo "prek-check: ${{ needs.prek-check.result }}" echo "unit-tests-check: ${{ needs.unit-tests-check.result }}" echo "e2e-test: ${{ needs.e2e-test.result }}" exit 1 From 8461f716682234cb6df3865904594ddf7ffb834d Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Sat, 31 Jan 2026 02:20:54 +0000 Subject: [PATCH 034/184] fix: use JavaScript identifier regex instead of Python isidentifier() Python's str.isidentifier() validates Python identifiers, not JavaScript identifiers. This caused valid JS identifiers like '$handler' to be rejected (since $ is not valid in Python identifiers). Changed to use a regex pattern that matches JavaScript identifier rules: - Can start with letter, underscore, or $ - Can contain letters, digits, underscores, or $ Added tests for $ identifiers to ensure they are correctly handled. --- django/aiservice/languages/js_ts/testgen.py | 14 ++++++++++++-- .../tests/optimizer/test_javascript_testgen.py | 9 +++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index 0d4125baa..c6b4e19a0 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -51,8 +51,18 @@ JS_EXECUTE_ASYNC_USER_PROMPT = (JS_PROMPTS_DIR / "execute_async_user_prompt.md") JS_PATTERN = re.compile(r"^```(?:javascript|js|typescript|ts)?\s*\n(.*?)\n```", re.MULTILINE | re.DOTALL) +# JavaScript identifier pattern: starts with letter, underscore, or $, +# followed by letters, digits, underscores, or $ +JS_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_$][a-zA-Z0-9_$]*$") + + def _is_valid_js_identifier(name: str) -> bool: - """Check if a name is a valid JavaScript identifier (not a reserved word).""" + """Check if a name is a valid JavaScript identifier (not a reserved word). + + JavaScript identifiers can start with a letter, underscore, or $, + followed by letters, digits, underscores, or $. This differs from + Python identifiers (e.g., '$handler' is valid in JS but not Python). + """ # Reserved words that cannot be used as variable names reserved_words = { "module", @@ -109,7 +119,7 @@ def _is_valid_js_identifier(name: str) -> bool: "true", "false", } - return name not in reserved_words and name.isidentifier() + return bool(JS_IDENTIFIER_PATTERN.match(name)) and name not in reserved_words def _generate_import_statement(function_name: str, module_path: str) -> tuple[str, str]: diff --git a/django/aiservice/tests/optimizer/test_javascript_testgen.py b/django/aiservice/tests/optimizer/test_javascript_testgen.py index d85b416e8..d573a78bb 100644 --- a/django/aiservice/tests/optimizer/test_javascript_testgen.py +++ b/django/aiservice/tests/optimizer/test_javascript_testgen.py @@ -13,6 +13,15 @@ class TestIsValidJsIdentifier: assert _is_valid_js_identifier("_private") is True assert _is_valid_js_identifier("name123") is True + def test_dollar_sign_identifiers(self) -> None: + """Test that $ identifiers (valid in JavaScript) return True.""" + assert _is_valid_js_identifier("$handler") is True + assert _is_valid_js_identifier("$") is True + assert _is_valid_js_identifier("$scope") is True + assert _is_valid_js_identifier("jQuery$") is True + assert _is_valid_js_identifier("$_private") is True + assert _is_valid_js_identifier("$$double") is True + def test_reserved_words(self) -> None: """Test that reserved words return False.""" assert _is_valid_js_identifier("module") is False From d59c48426e1f63f61feefb356ff0022ab7b7ef36 Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Sat, 31 Jan 2026 02:32:58 +0000 Subject: [PATCH 035/184] fix: merge prompt extension fixes and LLM client improvements - Cherry-pick: Remove .js extension guidance from prompts (from fix/js-import-extension-prompt) - Add get_llm_client() to create fresh clients per request (fixes event loop issues) Co-Authored-By: Claude Opus 4.5 --- django/aiservice/aiservice/llm.py | 17 ++++++++++++++++- .../testgen/execute_async_system_prompt.md | 4 ++-- .../prompts/testgen/execute_system_prompt.md | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/django/aiservice/aiservice/llm.py b/django/aiservice/aiservice/llm.py index 4d109a2d2..b00d1a5c6 100644 --- a/django/aiservice/aiservice/llm.py +++ b/django/aiservice/aiservice/llm.py @@ -111,6 +111,20 @@ def _create_anthropic_client() -> AsyncAnthropicFoundry | None: return None +def get_llm_client(model_type: str) -> AsyncAzureOpenAI | AsyncAnthropicFoundry | None: + """Get a fresh LLM client for the request. + + Creates a new client for each request to avoid event loop issues + with Django dev server. + """ + if model_type == "openai": + return _create_openai_client() + elif model_type == "anthropic": + return _create_anthropic_client() + return None + + +# Keep module-level clients for backwards compatibility _openai_client = _create_openai_client() _anthropic_client = _create_anthropic_client() @@ -160,7 +174,8 @@ async def call_llm( """Call LLM with OpenAI or Anthropic client.""" from aiservice.observability.database import record_llm_call # noqa: PLC0415 - client = llm_clients[llm.model_type] + # Create a fresh client for each request to avoid event loop issues with Django dev server + client = get_llm_client(llm.model_type) if client is None: msg = f"LLM client for model type '{llm.model_type}' is not available" raise ValueError(msg) diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md index 42ec28680..078ebb3f0 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md @@ -33,8 +33,8 @@ **CRITICAL: MOCKING RULES FOR JEST**: - **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. - **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. -- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics.js')` ✓ -- **ALWAYS include the `.js` extension** in mock paths when the project uses ESM imports. +- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` ✓ +- **DO NOT add file extensions to import paths** - TypeScript/Jest resolves extensions automatically. Use `import { fn } from '../utils'` NOT `import { fn } from '../utils.js'`. **Output Format Requirements**: diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md index e1d42c1dd..2f3627d98 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md @@ -24,8 +24,8 @@ **CRITICAL: MOCKING RULES FOR JEST**: - **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. - **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. -- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics.js')` ✓ -- **ALWAYS include the `.js` extension** in mock paths when the project uses ESM imports. +- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` ✓ +- **DO NOT add file extensions to import paths** - TypeScript/Jest resolves extensions automatically. Use `import { fn } from '../utils'` NOT `import { fn } from '../utils.js'`. **Output Format Requirements**: From b801254d1315c538013a467e606c15a096a23187 Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Sat, 31 Jan 2026 02:35:21 +0000 Subject: [PATCH 036/184] fix: strengthen import path extension guidance in prompts Add more explicit instructions to prevent LLMs from adding .js/.ts extensions to import paths. The previous guidance was being ignored by some models. - Add dedicated "CRITICAL: IMPORT PATH RULES" section with examples - Show both WRONG and CORRECT patterns explicitly - Remind to copy the provided import statement exactly Co-Authored-By: Claude Opus 4.5 --- .../js_ts/prompts/testgen/execute_async_system_prompt.md | 7 ++++++- .../js_ts/prompts/testgen/execute_system_prompt.md | 7 ++++++- .../languages/js_ts/prompts/testgen/execute_user_prompt.md | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md index 078ebb3f0..90c1e01d7 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md @@ -30,11 +30,16 @@ - **CRITICAL: DO NOT MOCK THE FUNCTION UNDER TEST** - Never mock, stub, or spy on the {function_name} function itself. - **CRITICAL: TEST REJECTION CASES** - Use `expect(...).rejects.toThrow()` for testing async errors. +**CRITICAL: IMPORT PATH RULES**: +- **NEVER add file extensions (.js, .ts, .tsx) to import paths** - Jest/TypeScript resolves extensions automatically. +- **WRONG**: `import { fn } from '../utils.js'` or `import { fn } from '../utils.ts'` +- **CORRECT**: `import { fn } from '../utils'` +- The user message provides the exact import statement to use - copy it exactly without modification. + **CRITICAL: MOCKING RULES FOR JEST**: - **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. - **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. - **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` ✓ -- **DO NOT add file extensions to import paths** - TypeScript/Jest resolves extensions automatically. Use `import { fn } from '../utils'` NOT `import { fn } from '../utils.js'`. **Output Format Requirements**: diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md index 2f3627d98..fccb5a935 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md @@ -21,11 +21,16 @@ - **CRITICAL: IMPORT FROM REAL MODULES** - Import the function and any related classes/utilities from their actual module paths as shown in the context. - **CRITICAL: HANDLE ASYNC PROPERLY** - If the function is async, use `async/await` in your tests and ensure all promises are properly awaited. +**CRITICAL: IMPORT PATH RULES**: +- **NEVER add file extensions (.js, .ts, .tsx) to import paths** - Jest/TypeScript resolves extensions automatically. +- **WRONG**: `import { fn } from '../utils.js'` or `import { fn } from '../utils.ts'` +- **CORRECT**: `import { fn } from '../utils'` +- The user message provides the exact import statement to use - copy it exactly without modification. + **CRITICAL: MOCKING RULES FOR JEST**: - **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. - **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. - **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` ✓ -- **DO NOT add file extensions to import paths** - TypeScript/Jest resolves extensions automatically. Use `import { fn } from '../utils'` NOT `import { fn } from '../utils.js'`. **Output Format Requirements**: diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md index f1d5a8c62..040aaaaf9 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md @@ -5,7 +5,7 @@ Using the {test_framework} testing framework, write a test suite for the followi {function_code} ``` -**CRITICAL: Use this exact import statement (do not modify the path):** +**CRITICAL: Use this exact import statement - copy it EXACTLY as shown, do NOT add .js or any file extension:** ```javascript {import_statement} ``` From 70360436bdd8a9fc3156e77f86d069279de2e748 Mon Sep 17 00:00:00 2001 From: Saurabh Misra Date: Sat, 31 Jan 2026 04:22:07 +0000 Subject: [PATCH 037/184] fix: strip file extensions from JS/TS import paths in generated tests LLMs often add .js extensions to TypeScript import paths (e.g., `import { func } from '../module.js'`), but TypeScript/Jest module resolution doesn't require explicit extensions. This causes "Cannot find module" errors. This change adds `strip_js_extensions()` function that removes .js/.ts/.tsx/.jsx/.mjs/.mts extensions from relative import paths in generated tests. The function handles: - ES module imports: import { x } from '../path.js' - CommonJS requires: require('../path.js') - Jest mocks: jest.mock('../path.js'), jest.doMock(), etc. External package imports (lodash, react, etc.) are preserved. Co-Authored-By: Claude Opus 4.5 --- django/aiservice/languages/js_ts/testgen.py | 40 +++++++++ .../tests/testgen/test_testgen_javascript.py | 82 +++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index c6b4e19a0..7ea7c8f53 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -122,6 +122,43 @@ def _is_valid_js_identifier(name: str) -> bool: return bool(JS_IDENTIFIER_PATTERN.match(name)) and name not in reserved_words +# Patterns to strip file extensions from import paths +# LLMs sometimes add .js extensions to TypeScript imports, which breaks module resolution +_JS_EXTENSION_PATTERN = re.compile( + r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""" +) +_REQUIRE_EXTENSION_PATTERN = re.compile( + r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))""" +) +_JEST_MOCK_EXTENSION_PATTERN = re.compile( + r"""(jest\.(?:mock|doMock|unmock|requireActual|requireMock)\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""" +) + + +def strip_js_extensions(source: str) -> str: + """Strip .js/.ts/.tsx/.jsx extensions from relative import paths. + + TypeScript and Jest's module resolution automatically resolve file extensions, + so adding them explicitly can cause "Cannot find module" errors when the LLM + adds incorrect extensions (e.g., .js to a .ts file). + + This function removes extensions from: + - ES module imports: import { x } from '../path/file.js' + - CommonJS requires: require('../path/file.js') + - Jest mocks: jest.mock('../path/file.js') + + Args: + source: The test source code. + + Returns: + Source code with extensions stripped from relative import paths. + + """ + source = _JS_EXTENSION_PATTERN.sub(r"\1\2\4", source) + source = _REQUIRE_EXTENSION_PATTERN.sub(r"\1\2\4", source) + return _JEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source) + + def _generate_import_statement(function_name: str, module_path: str) -> tuple[str, str]: """Generate appropriate import statement and accessor for JavaScript/TypeScript. @@ -480,6 +517,9 @@ async def testgen_javascript( language=data.language, ) + # Strip incorrect file extensions from import paths (LLMs sometimes add .js to .ts imports) + generated_test_source = strip_js_extensions(generated_test_source) + ph(request.user, "aiservice-testgen-tests-generated", properties={"language": language}) if hasattr(request, "should_log_features") and request.should_log_features: diff --git a/django/aiservice/tests/testgen/test_testgen_javascript.py b/django/aiservice/tests/testgen/test_testgen_javascript.py index 746556dc3..ae114bbe0 100644 --- a/django/aiservice/tests/testgen/test_testgen_javascript.py +++ b/django/aiservice/tests/testgen/test_testgen_javascript.py @@ -285,3 +285,85 @@ class TestJavaScriptTestGenPromptContent: system_content = messages[0]["content"] # Should warn against mocking assert "mock" in system_content.lower() or "Mock" in system_content + + +class TestStripJsExtensions: + """Tests for stripping file extensions from import paths. + + These tests copy the regex patterns and function directly to avoid Django dependencies. + """ + + # Copy of patterns from aiservice/languages/js_ts/testgen.py + _JS_EXTENSION_PATTERN = re.compile( + r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""" + ) + _REQUIRE_EXTENSION_PATTERN = re.compile( + r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))""" + ) + _JEST_MOCK_EXTENSION_PATTERN = re.compile( + r"""(jest\.(?:mock|doMock|unmock|requireActual|requireMock)\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""" + ) + + @staticmethod + def strip_js_extensions(source: str) -> str: + """Strip .js/.ts/.tsx/.jsx extensions from relative import paths.""" + source = TestStripJsExtensions._JS_EXTENSION_PATTERN.sub(r"\1\2\4", source) + source = TestStripJsExtensions._REQUIRE_EXTENSION_PATTERN.sub(r"\1\2\4", source) + return TestStripJsExtensions._JEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source) + + def test_strip_js_extension_from_esm_import(self) -> None: + """Test stripping .js from ES module imports.""" + code = "import { getDifferences } from '../src/utils/DynamicBindingUtils.js';" + expected = "import { getDifferences } from '../src/utils/DynamicBindingUtils';" + + result = self.strip_js_extensions(code) + assert result == expected + + def test_strip_ts_extension_from_esm_import(self) -> None: + """Test stripping .ts from ES module imports.""" + code = "import { func } from './module.ts';" + expected = "import { func } from './module';" + + result = self.strip_js_extensions(code) + assert result == expected + + def test_strip_extension_from_require(self) -> None: + """Test stripping extensions from require() calls.""" + code = "const { func } = require('../utils/helper.js');" + expected = "const { func } = require('../utils/helper');" + + result = self.strip_js_extensions(code) + assert result == expected + + def test_strip_extension_from_jest_mock(self) -> None: + """Test stripping extensions from jest.mock() calls.""" + code = "jest.mock('../src/utils/DynamicBindingUtils.js');" + expected = "jest.mock('../src/utils/DynamicBindingUtils');" + + result = self.strip_js_extensions(code) + assert result == expected + + def test_preserve_external_package_imports(self) -> None: + """Test that external package imports are not modified.""" + code = "import lodash from 'lodash';" + + result = self.strip_js_extensions(code) + assert result == code # Should be unchanged + + def test_strip_multiple_extensions_in_file(self) -> None: + """Test stripping multiple extensions in a single file.""" + code = """ +import { func1 } from '../utils/helper.js'; +import { func2 } from './local.ts'; +const { func3 } = require('../lib/util.tsx'); +jest.mock('../mocks/mock.jsx'); +""" + expected = """ +import { func1 } from '../utils/helper'; +import { func2 } from './local'; +const { func3 } = require('../lib/util'); +jest.mock('../mocks/mock'); +""" + + result = self.strip_js_extensions(code) + assert result == expected From cbfebf8ee4318027930858ed2390f2e2c0468182 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Sun, 1 Feb 2026 03:50:05 +0530 Subject: [PATCH 038/184] fix(js-testgen): escape curly braces in prompt template The JavaScript test generation prompt contained `{fn}` as part of example code showing import syntax. However, Python's `.format()` method interprets this as a placeholder and tries to substitute it, causing a KeyError. Fixed by escaping the curly braces as `{{fn}}` so they render as literal `{fn}` in the final prompt. Co-Authored-By: Claude Opus 4.5 --- .../languages/js_ts/prompts/testgen/execute_system_prompt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md index fccb5a935..b015b1c9e 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md @@ -23,8 +23,8 @@ **CRITICAL: IMPORT PATH RULES**: - **NEVER add file extensions (.js, .ts, .tsx) to import paths** - Jest/TypeScript resolves extensions automatically. -- **WRONG**: `import { fn } from '../utils.js'` or `import { fn } from '../utils.ts'` -- **CORRECT**: `import { fn } from '../utils'` +- **WRONG**: `import {{fn}} from '../utils.js'` or `import {{fn}} from '../utils.ts'` +- **CORRECT**: `import {{fn}} from '../utils'` - The user message provides the exact import statement to use - copy it exactly without modification. **CRITICAL: MOCKING RULES FOR JEST**: From b48a8d9a43a9314230416dd90b55c6c2601911f0 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Mon, 2 Feb 2026 20:51:52 +0530 Subject: [PATCH 039/184] Add vitest support in backend (#2363) --- .../prompts/testgen/execute_async_system_prompt.md | 14 +++++++------- .../js_ts/prompts/testgen/execute_system_prompt.md | 14 +++++++------- django/aiservice/languages/js_ts/testgen.py | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md index 90c1e01d7..53e94d077 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md @@ -1,4 +1,4 @@ -**Role**: You are Codeflash, a world-class JavaScript/TypeScript developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests for **asynchronous** code using Jest. When asked to reply only with code, you write all of your code in a single markdown code block. +**Role**: You are Codeflash, a world-class JavaScript/TypeScript developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests for **asynchronous** code. When asked to reply only with code, you write all of your code in a single markdown code block. **Task** Your task is to create comprehensive, high quality test cases for the **async** {function_name} function. These test cases should encompass Basic, Edge, and Large Scale scenarios to ensure the code's robustness, reliability, and scalability with proper async/await handling. @@ -20,7 +20,7 @@ **Instructions**: - Implement a comprehensive set of test cases following the guidelines above. -- Use Jest testing framework with `describe`, `test`, and `expect`. +- Use the testing framework specified in the user message with `describe`, `test`/`it`, and `expect`. - **ALL test functions must be async**: `test('...', async () => {{ ... }})` - **ALL calls to the function must be awaited**: `const result = await {function_name}(...)` - Ensure each test case is well-documented with comments explaining the scenario it covers. @@ -31,15 +31,15 @@ - **CRITICAL: TEST REJECTION CASES** - Use `expect(...).rejects.toThrow()` for testing async errors. **CRITICAL: IMPORT PATH RULES**: -- **NEVER add file extensions (.js, .ts, .tsx) to import paths** - Jest/TypeScript resolves extensions automatically. +- **NEVER add file extensions (.js, .ts, .tsx) to import paths** - The test framework resolves extensions automatically. - **WRONG**: `import { fn } from '../utils.js'` or `import { fn } from '../utils.ts'` - **CORRECT**: `import { fn } from '../utils'` - The user message provides the exact import statement to use - copy it exactly without modification. -**CRITICAL: MOCKING RULES FOR JEST**: -- **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. -- **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. -- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` ✓ +**CRITICAL: MOCKING RULES**: +- For Jest: `jest.mock()` calls are HOISTED to the top of the file. NEVER use dynamic expressions - use static string literals only. +- For Vitest: `vi.mock()` calls are also hoisted. Use static string literals only. +- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` or `vi.mock('../analytics')` ✓ **Output Format Requirements**: diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md index b015b1c9e..89fdf03b4 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md @@ -1,4 +1,4 @@ -**Role**: You are Codeflash, a world-class JavaScript/TypeScript developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests using Jest. When asked to reply only with code, you write all of your code in a single markdown code block. +**Role**: You are Codeflash, a world-class JavaScript/TypeScript developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests. When asked to reply only with code, you write all of your code in a single markdown code block. **Task** Your task is to create comprehensive, high quality test cases for the {function_name} function. These test cases should encompass Basic, Edge, and Large Scale scenarios to ensure the code's robustness, reliability, and scalability. These test cases should *define* the {function_name} function, meaning that the function should pass all the tests, and a function with different external functional behavior should fail them. @@ -13,7 +13,7 @@ **Instructions**: - Implement a comprehensive set of test cases following the guidelines above. -- Use Jest testing framework with `describe`, `test`, and `expect`. +- Use the testing framework specified in the user message with `describe`, `test`/`it`, and `expect`. - Ensure each test case is well-documented with comments explaining the scenario it covers. - Pay special attention to edge cases as they often reveal hidden bugs. - For large-scale tests, focus on the function's efficiency and performance under heavy loads. Avoid loops exceeding 1000 iterations, and keep data structures under 1000 elements. @@ -22,15 +22,15 @@ - **CRITICAL: HANDLE ASYNC PROPERLY** - If the function is async, use `async/await` in your tests and ensure all promises are properly awaited. **CRITICAL: IMPORT PATH RULES**: -- **NEVER add file extensions (.js, .ts, .tsx) to import paths** - Jest/TypeScript resolves extensions automatically. +- **NEVER add file extensions (.js, .ts, .tsx) to import paths** - The test framework resolves extensions automatically. - **WRONG**: `import {{fn}} from '../utils.js'` or `import {{fn}} from '../utils.ts'` - **CORRECT**: `import {{fn}} from '../utils'` - The user message provides the exact import statement to use - copy it exactly without modification. -**CRITICAL: MOCKING RULES FOR JEST**: -- **jest.mock() calls are HOISTED** to the top of the file by Jest's transformer. This means they execute BEFORE any other code, including variable declarations. -- **NEVER use dynamic expressions in jest.mock()** - Do NOT use variables, `path.join()`, `require.resolve()`, or any computed values in jest.mock() paths. These will fail because the variables are not yet defined when the hoisted mock executes. -- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` ✓ +**CRITICAL: MOCKING RULES**: +- For Jest: `jest.mock()` calls are HOISTED to the top of the file. NEVER use dynamic expressions - use static string literals only. +- For Vitest: `vi.mock()` calls are also hoisted. Use static string literals only. +- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` or `vi.mock('../analytics')` ✓ **Output Format Requirements**: diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index 7ea7c8f53..340301087 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -442,8 +442,8 @@ def validate_javascript_testgen_request_data(data: TestGenSchema) -> None: HttpError: If validation fails """ - if data.test_framework not in ["jest"]: - raise HttpError(400, "Invalid test framework for JavaScript/TypeScript. We only support jest.") + if data.test_framework not in ["jest", "vitest", "mocha"]: + raise HttpError(400, "Invalid test framework for JavaScript/TypeScript. Supported: jest, vitest, mocha.") if not data.function_to_optimize: raise HttpError(400, "Invalid function to optimize. It is empty.") if not validate_trace_id(data.trace_id): From a39e155a8421b258dd57456e5ac0cd3f31523685 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Mon, 2 Feb 2026 09:08:17 -0800 Subject: [PATCH 040/184] bug: mismatch in cli and internal schema for code repair Change test_src_code to allow None type --- django/aiservice/code_repair/code_repair_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/code_repair/code_repair_context.py index 1936f9b8c..b5cac098f 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/code_repair/code_repair_context.py @@ -42,7 +42,7 @@ class TestDiff(Schema): candidate_value: bool | str | int | float | dict | list | None = None original_pass: bool candidate_pass: bool - test_src_code: str + test_src_code: str | None = None candidate_pytest_error: str | None = None original_pytest_error: str | None = None From 1ffdee3000310f8b8fb2d72c6bbc5f1a2031b67e Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Mon, 2 Feb 2026 09:24:28 -0800 Subject: [PATCH 041/184] Fix check for empty test source code section Ensure sections[diff.test_src_code] is not None before assignment. --- django/aiservice/code_repair/code_repair_context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/code_repair/code_repair_context.py index b5cac098f..ae960b304 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/code_repair/code_repair_context.py @@ -79,7 +79,9 @@ class CodeRepairContext: test_error_label = "Pytest error" if language == "python" else "Test error" for diff in test_diffs: try: - if sections[diff.test_src_code] == "": + if not sections[diff.test_src_code]: + #could be None or "", make sure it is None + sections[diff.test_src_code] = "" if sections[diff.test_src_code] is None else sections[diff.test_src_code] # add error strings and test def only once per test function sections[diff.test_src_code] += f"""Test Source: ```{language} From 2e523313b5283c4cc9c6d10e59372a3d92268a45 Mon Sep 17 00:00:00 2001 From: aseembits93 Date: Mon, 2 Feb 2026 09:25:44 -0800 Subject: [PATCH 042/184] prek fixes --- django/aiservice/code_repair/code_repair_context.py | 6 ++++-- django/aiservice/languages/js_ts/testgen.py | 4 +--- django/aiservice/tests/testgen/test_testgen_javascript.py | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/code_repair/code_repair_context.py index ae960b304..60c46678d 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/code_repair/code_repair_context.py @@ -80,8 +80,10 @@ class CodeRepairContext: for diff in test_diffs: try: if not sections[diff.test_src_code]: - #could be None or "", make sure it is None - sections[diff.test_src_code] = "" if sections[diff.test_src_code] is None else sections[diff.test_src_code] + # could be None or "", make sure it is None + sections[diff.test_src_code] = ( + "" if sections[diff.test_src_code] is None else sections[diff.test_src_code] + ) # add error strings and test def only once per test function sections[diff.test_src_code] += f"""Test Source: ```{language} diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index 340301087..da6281f53 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -124,9 +124,7 @@ def _is_valid_js_identifier(name: str) -> bool: # Patterns to strip file extensions from import paths # LLMs sometimes add .js extensions to TypeScript imports, which breaks module resolution -_JS_EXTENSION_PATTERN = re.compile( - r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""" -) +_JS_EXTENSION_PATTERN = re.compile(r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""") _REQUIRE_EXTENSION_PATTERN = re.compile( r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))""" ) diff --git a/django/aiservice/tests/testgen/test_testgen_javascript.py b/django/aiservice/tests/testgen/test_testgen_javascript.py index ae114bbe0..c3d0da93a 100644 --- a/django/aiservice/tests/testgen/test_testgen_javascript.py +++ b/django/aiservice/tests/testgen/test_testgen_javascript.py @@ -294,9 +294,7 @@ class TestStripJsExtensions: """ # Copy of patterns from aiservice/languages/js_ts/testgen.py - _JS_EXTENSION_PATTERN = re.compile( - r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""" - ) + _JS_EXTENSION_PATTERN = re.compile(r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""") _REQUIRE_EXTENSION_PATTERN = re.compile( r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))""" ) From c2cd6e5e7239fc14305f0889df94281fbd35d097 Mon Sep 17 00:00:00 2001 From: aseembits93 Date: Mon, 2 Feb 2026 09:37:34 -0800 Subject: [PATCH 043/184] minor fix --- django/aiservice/code_repair/code_repair_context.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/code_repair/code_repair_context.py index 60c46678d..9070b1b1a 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/code_repair/code_repair_context.py @@ -79,11 +79,10 @@ class CodeRepairContext: test_error_label = "Pytest error" if language == "python" else "Test error" for diff in test_diffs: try: + if not diff.test_src_code: + # make sure it is "" and not None + diff.test_src_code = "" if diff.test_src_code is None else diff.test_src_code if not sections[diff.test_src_code]: - # could be None or "", make sure it is None - sections[diff.test_src_code] = ( - "" if sections[diff.test_src_code] is None else sections[diff.test_src_code] - ) # add error strings and test def only once per test function sections[diff.test_src_code] += f"""Test Source: ```{language} From 3276f9542ebe6526680dc877c6ff025b723ed3ec Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Mon, 2 Feb 2026 09:44:36 -0800 Subject: [PATCH 044/184] Update django/aiservice/code_repair/code_repair_context.py Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- django/aiservice/code_repair/code_repair_context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/code_repair/code_repair_context.py index 9070b1b1a..b981c590a 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/code_repair/code_repair_context.py @@ -81,7 +81,9 @@ class CodeRepairContext: try: if not diff.test_src_code: # make sure it is "" and not None - diff.test_src_code = "" if diff.test_src_code is None else diff.test_src_code + if not diff.test_src_code: + # Convert None to empty string for dict key usage + diff.test_src_code = "" if not sections[diff.test_src_code]: # add error strings and test def only once per test function sections[diff.test_src_code] += f"""Test Source: From 019f220c11f58819977738b305a0b33c848e742c Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Mon, 2 Feb 2026 09:45:48 -0800 Subject: [PATCH 045/184] cleaning up --- django/aiservice/code_repair/code_repair_context.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/code_repair/code_repair_context.py index b981c590a..3b46746f7 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/code_repair/code_repair_context.py @@ -79,8 +79,6 @@ class CodeRepairContext: test_error_label = "Pytest error" if language == "python" else "Test error" for diff in test_diffs: try: - if not diff.test_src_code: - # make sure it is "" and not None if not diff.test_src_code: # Convert None to empty string for dict key usage diff.test_src_code = "" From 5d0ca8d01b7be0a05b9e0cf5b627b57db4dd3b4a Mon Sep 17 00:00:00 2001 From: aseembits93 Date: Mon, 2 Feb 2026 10:00:40 -0800 Subject: [PATCH 046/184] fn var was not used in .format() --- .../js_ts/prompts/testgen/execute_async_system_prompt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md index 53e94d077..2afe993c2 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md @@ -32,8 +32,8 @@ **CRITICAL: IMPORT PATH RULES**: - **NEVER add file extensions (.js, .ts, .tsx) to import paths** - The test framework resolves extensions automatically. -- **WRONG**: `import { fn } from '../utils.js'` or `import { fn } from '../utils.ts'` -- **CORRECT**: `import { fn } from '../utils'` +- **WRONG**: `import {function_name} from '../utils.js'` or `import {function_name} from '../utils.ts'` +- **CORRECT**: `import {function_name} from '../utils'` - The user message provides the exact import statement to use - copy it exactly without modification. **CRITICAL: MOCKING RULES**: From 90597c52e3968257e2e19d281fdd44e9ccf87c75 Mon Sep 17 00:00:00 2001 From: Aseem Saxena Date: Mon, 2 Feb 2026 10:11:44 -0800 Subject: [PATCH 047/184] markdown more info --- django/aiservice/code_repair/code_repair_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/code_repair/code_repair_context.py index 3b46746f7..3944397ca 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/code_repair/code_repair_context.py @@ -86,7 +86,7 @@ class CodeRepairContext: # add error strings and test def only once per test function sections[diff.test_src_code] += f"""Test Source: ```{language} - {diff.test_src_code} + {diff.test_src_code or "Not Available"} ``` {test_error_label} (original code): {diff.original_pytest_error if diff.original_pytest_error else ""} {test_error_label} (optimized code): {diff.candidate_pytest_error if diff.candidate_pytest_error else ""} From eb8ad603ffb8d5a8ddd8d787507fafa8ac672cc8 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Tue, 3 Feb 2026 03:29:36 +0530 Subject: [PATCH 048/184] vitest related changes to prompt (#2366) --- .../testgen/execute_async_system_prompt.md | 20 ++++++++++++++----- .../testgen/execute_async_user_prompt.md | 15 ++++++++++++-- .../prompts/testgen/execute_system_prompt.md | 16 ++++++++++++--- .../prompts/testgen/execute_user_prompt.md | 15 ++++++++++++-- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md index 53e94d077..b3558df10 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md @@ -32,14 +32,24 @@ **CRITICAL: IMPORT PATH RULES**: - **NEVER add file extensions (.js, .ts, .tsx) to import paths** - The test framework resolves extensions automatically. -- **WRONG**: `import { fn } from '../utils.js'` or `import { fn } from '../utils.ts'` -- **CORRECT**: `import { fn } from '../utils'` +- **WRONG**: `import {{ fn }} from '../utils.js'` or `import {{ fn }} from '../utils.ts'` +- **CORRECT**: `import {{ fn }} from '../utils'` - The user message provides the exact import statement to use - copy it exactly without modification. +**CRITICAL: VITEST IMPORTS REQUIRED**: +- If test_framework is "vitest", you MUST import test functions from 'vitest' since globals are NOT enabled by default: + ```javascript + import {{ describe, test, expect, vi, beforeEach, afterEach }} from 'vitest'; + ``` +- For "jest", globals are typically enabled so no import is needed. + **CRITICAL: MOCKING RULES**: -- For Jest: `jest.mock()` calls are HOISTED to the top of the file. NEVER use dynamic expressions - use static string literals only. -- For Vitest: `vi.mock()` calls are also hoisted. Use static string literals only. -- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` or `vi.mock('../analytics')` ✓ +- **USE THE CORRECT MOCK SYNTAX FOR THE SPECIFIED FRAMEWORK**: + - For **Jest**: Use `jest.mock()` and `jest.fn()` + - For **Vitest**: Use `vi.mock()` and `vi.fn()` - NEVER use jest.mock with Vitest! +- Mock calls are HOISTED to the top of the file. NEVER use dynamic expressions - use static string literals only. +- **ALWAYS use static string literals** for mock paths. +- **IMPORTANT**: Check the test framework specified in the user message and use the matching syntax. **Output Format Requirements**: diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md index fe2ac4551..d642d1c58 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md @@ -1,4 +1,6 @@ -Using the {test_framework} testing framework, write a test suite for the following **ASYNC** JavaScript function. +Using the **{test_framework}** testing framework, write a test suite for the following **ASYNC** JavaScript function. + +**IMPORTANT**: You MUST use {test_framework} syntax. If {test_framework} is "vitest", use `vi.mock()` and `vi.fn()`. If {test_framework} is "jest", use `jest.mock()` and `jest.fn()`. Do NOT mix frameworks. **CRITICAL: This function is ASYNCHRONOUS** - All test functions MUST be async: `test('...', async () => {{ ... }})` @@ -15,9 +17,18 @@ Using the {test_framework} testing framework, write a test suite for the followi {import_statement} ``` +**CRITICAL: VITEST IMPORTS REQUIRED** +If test_framework is "vitest", you MUST import test functions from 'vitest' since globals are NOT enabled: +```javascript +import {{ describe, test, expect, vi, beforeEach, afterEach }} from 'vitest'; +``` +For "jest", globals are typically enabled so no import is needed. + **Template to Follow:** ```javascript -// imports +// vitest imports (REQUIRED for vitest - globals are NOT enabled by default) +import {{ describe, test, expect, vi, beforeEach, afterEach }} from 'vitest'; +// function import {import_statement} // unit tests diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md index 89fdf03b4..dbe648c35 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md @@ -27,10 +27,20 @@ - **CORRECT**: `import {{fn}} from '../utils'` - The user message provides the exact import statement to use - copy it exactly without modification. +**CRITICAL: VITEST IMPORTS REQUIRED**: +- If test_framework is "vitest", you MUST import test functions from 'vitest' since globals are NOT enabled by default: + ```javascript + import {{ describe, test, expect, vi, beforeEach, afterEach }} from 'vitest'; + ``` +- For "jest", globals are typically enabled so no import is needed. + **CRITICAL: MOCKING RULES**: -- For Jest: `jest.mock()` calls are HOISTED to the top of the file. NEVER use dynamic expressions - use static string literals only. -- For Vitest: `vi.mock()` calls are also hoisted. Use static string literals only. -- **ALWAYS use static string literals** for mock paths: `jest.mock('../analytics')` or `vi.mock('../analytics')` ✓ +- **USE THE CORRECT MOCK SYNTAX FOR THE SPECIFIED FRAMEWORK**: + - For **Jest**: Use `jest.mock()` and `jest.fn()` + - For **Vitest**: Use `vi.mock()` and `vi.fn()` - NEVER use jest.mock with Vitest! +- Mock calls are HOISTED to the top of the file. NEVER use dynamic expressions - use static string literals only. +- **ALWAYS use static string literals** for mock paths. +- **IMPORTANT**: Check the test framework specified in the user message and use the matching syntax. **Output Format Requirements**: diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md index 040aaaaf9..67ca47ae1 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md @@ -1,4 +1,6 @@ -Using the {test_framework} testing framework, write a test suite for the following JavaScript function. +Using the **{test_framework}** testing framework, write a test suite for the following JavaScript function. + +**IMPORTANT**: You MUST use {test_framework} syntax. If {test_framework} is "vitest", use `vi.mock()` and `vi.fn()`. If {test_framework} is "jest", use `jest.mock()` and `jest.fn()`. Do NOT mix frameworks. **Function to Test:** ```javascript @@ -10,9 +12,18 @@ Using the {test_framework} testing framework, write a test suite for the followi {import_statement} ``` +**CRITICAL: VITEST IMPORTS REQUIRED** +If test_framework is "vitest", you MUST import test functions from 'vitest' since globals are NOT enabled: +```javascript +import {{ describe, test, expect, vi, beforeEach, afterEach }} from 'vitest'; +``` +For "jest", globals are typically enabled so no import is needed. + **Template to Follow:** ```javascript -// imports +// vitest imports (REQUIRED for vitest - globals are NOT enabled by default) +import {{ describe, test, expect, vi, beforeEach, afterEach }} from 'vitest'; +// function import {import_statement} // unit tests From 7272b71e6d894c2dcc5fa93ba8fbd64cc819469e Mon Sep 17 00:00:00 2001 From: HeshamHM28 Date: Mon, 2 Feb 2026 16:02:29 -0800 Subject: [PATCH 049/184] [Feat] Allow multi language in staging and line profiler (#2365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes CF-1051 Screenshot 2026-02-02 at 11 03
02 PM Screenshot 2026-02-02 at 10 47
32 PM Screenshot 2026-02-02 at 10 47
24 PM --------- Co-authored-by: Aseem Saxena Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../Editor/monaco-diff-editor-github.tsx | 7 +- .../components/trace/monaco-diff-viewer.tsx | 5 +- js/cf-webapp/src/lib/lineProfilerParser.ts | 93 +++++++++++++++++++ js/cf-webapp/src/lib/utils.ts | 18 ++++ 4 files changed, 118 insertions(+), 5 deletions(-) diff --git a/js/cf-webapp/src/components/Editor/monaco-diff-editor-github.tsx b/js/cf-webapp/src/components/Editor/monaco-diff-editor-github.tsx index 60b927cc7..72d1d4f25 100644 --- a/js/cf-webapp/src/components/Editor/monaco-diff-editor-github.tsx +++ b/js/cf-webapp/src/components/Editor/monaco-diff-editor-github.tsx @@ -40,6 +40,7 @@ import { Star, } from "lucide-react" import { ReviewQualityBadge } from "../ui/quality_badge" +import { getMonacoLanguage } from "@/lib/utils" interface DiffContent { newContent: string @@ -1126,7 +1127,7 @@ const MonacoDiffEditorGithub: React.FC = ({ = ({ = ({ key={diffEditorKey} original={originalContent} modified={editorContent} - language="python" + language={getMonacoLanguage(selectedFile)} theme={isDarkMode ? "vs-dark" : "light"} onMount={handleDiffEditorDidMount} keepCurrentOriginalModel={true} diff --git a/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx b/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx index f6f99dcd2..fc1771cac 100644 --- a/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx +++ b/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx @@ -23,6 +23,7 @@ import { // Ensure you have lucide-react installed as per your package.json import { Loader2, FileText, AlertTriangle } from "lucide-react" import type { ExperimentMetadata, DiffContents } from "@/lib/types" // Adjust path if needed +import { getMonacoLanguage } from "@/lib/utils" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" @@ -612,7 +613,7 @@ const MonacoDiffViewer: React.FC = ({ height="100%" original={currentDiff.oldContent || ""} modified={currentEdit[activeFileKey] || currentDiff.newContent || ""} - language="python" + language={activeFileKey ? getMonacoLanguage(activeFileKey) : "python"} theme="codeflash-python-dark" onMount={handleEditorOnMount} options={{ @@ -659,7 +660,7 @@ const MonacoDiffViewer: React.FC = ({ height="100%" original={currentDiff.oldContent || ""} modified={currentDiff.newContent || ""} - language="python" + language={activeFileKey ? getMonacoLanguage(activeFileKey) : "python"} theme="codeflash-python-dark" onMount={handleEditorOnMount} options={{ diff --git a/js/cf-webapp/src/lib/lineProfilerParser.ts b/js/cf-webapp/src/lib/lineProfilerParser.ts index 1144c19b3..cdc555da1 100644 --- a/js/cf-webapp/src/lib/lineProfilerParser.ts +++ b/js/cf-webapp/src/lib/lineProfilerParser.ts @@ -61,6 +61,99 @@ export function parseLineProfilerResults(rawResults: string): LineProfilerReport return report } + // Detect format: JS format starts with "Line Profile Results:" or has space-separated columns + const isJsFormat = + rawResults.includes("Line Profile Results:") || + (rawResults.includes("Line") && rawResults.includes("Hits") && rawResults.includes("Content") && !rawResults.includes("|")) + + if (isJsFormat) { + return parseJsLineProfilerResults(rawResults) + } + + return parsePythonLineProfilerResults(rawResults) +} + +/** + * Parse JS/TS line profiler format: + * Line Profile Results: + * File: /path/to/file.js + * -------------------------------------------------------------------------------- + * Line Hits Time (ms) % Time Content + * -------------------------------------------------------------------------------- + * 12 3442092 397.958 100.0% if (n <= 1) { + */ +function parseJsLineProfilerResults(rawResults: string): LineProfilerReport { + const report: LineProfilerReport = { + timerUnit: "1e-03 s", // JS profiler uses milliseconds + functions: [], + } + + const lines = rawResults.split("\n") + let currentFunction: LineProfilerFunction | null = null + let inData = false + + for (const line of lines) { + const trimmedLine = line.trim() + + // Extract file path as function name + if (trimmedLine.startsWith("File:")) { + if (currentFunction) { + report.functions.push(currentFunction) + } + const filePath = trimmedLine.replace("File:", "").trim() + const fileName = filePath.split("/").pop() || filePath + currentFunction = { + functionName: fileName, + totalTime: "", + entries: [], + } + inData = false + continue + } + + // Skip header line and separator lines + if (trimmedLine.startsWith("Line") && trimmedLine.includes("Hits")) { + inData = false + continue + } + if (trimmedLine.match(/^-+$/)) { + inData = true + continue + } + + // Parse data rows: " 12 3442092 397.958 100.0% if (n <= 1) {" + if (inData && currentFunction && trimmedLine.length > 0) { + // Match: line_number, hits, time, percent, content + const match = trimmedLine.match(/^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+([\d.]+)%\s+(.*)$/) + if (match) { + const [, , hits, time, percent, content] = match + currentFunction.entries.push({ + hits: hits, + time: time, + perHit: hits === "0" ? "0.000" : (parseFloat(time) / parseInt(hits)).toFixed(3), + percentTime: parseFloat(percent) || 0, + lineContents: content || " ", + }) + } + } + } + + if (currentFunction) { + report.functions.push(currentFunction) + } + + return report +} + +/** + * Parse Python line_profiler markdown table format + */ +function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport { + const report: LineProfilerReport = { + timerUnit: "", + functions: [], + } + const lines = rawResults.split("\n") let currentFunction: LineProfilerFunction | null = null let inTable = false diff --git a/js/cf-webapp/src/lib/utils.ts b/js/cf-webapp/src/lib/utils.ts index a49443c97..9e5607441 100644 --- a/js/cf-webapp/src/lib/utils.ts +++ b/js/cf-webapp/src/lib/utils.ts @@ -5,6 +5,24 @@ export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)) } +/** + * Get Monaco editor language identifier from file path + */ +export function getMonacoLanguage(filePath: string): string { + const ext = filePath.split(".").pop()?.toLowerCase() || "" + const languageMap: Record = { + py: "python", + js: "javascript", + jsx: "javascript", + ts: "typescript", + tsx: "typescript", + java: "java", + html: "html", + css: "css", + } + return languageMap[ext] || "python" +} + /** * Round optimization attempts to nearest 0.5 by dividing by 100 and rounding to half increments * (e.g., 4000 -> 40, 450 -> 4.5, 550 -> 5.5) From e7cf9bf29e16bb1948f3dfb978fc05b51cb6e453 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:18:47 -0500 Subject: [PATCH 050/184] feat: sync Claude workflow with CLI (#2368) ## Summary - Add prek auto-fix step (format/lint changed files, commit & push) - Add coverage analysis step (compare PR vs main, enforce 75% for new code) - Add uv setup and dependency install to pr-review job - Change pr-review permissions to allow pushing fixes Syncs with recent improvements made to the CLI repo. --- .github/workflows/claude.yml | 88 ++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index bec02e09a..079cfdb4d 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -13,21 +13,33 @@ on: types: [submitted] jobs: - # Automatic PR review (read-only) + # Automatic PR review (can fix linting issues and push) pr-review: if: github.event_name == 'pull_request' runs-on: ubuntu-latest permissions: - contents: read + contents: write pull-requests: write issues: read id-token: write actions: read + defaults: + run: + working-directory: django/aiservice steps: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 2 + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref }} + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Install dependencies + run: | + uv venv --seed + uv sync - name: Run Claude Code id: claude @@ -40,6 +52,25 @@ jobs: PR NUMBER: ${{ github.event.pull_request.number }} EVENT: ${{ github.event.action }} + IMPORTANT: This repo has Python code in `django/aiservice/`. Run uv/prek commands from that directory. + + ## STEP 1: Run pre-commit checks and fix issues + + First, run `cd django/aiservice && uv run prek run --from-ref origin/main` to check for linting/formatting issues on files changed in this PR. + + If there are any issues: + - For SAFE auto-fixable issues (formatting, import sorting, trailing whitespace, etc.), run the command again to auto-fix them + - Stage the fixed files with `git add` + - Commit with message "style: auto-fix linting issues" + - Push the changes with `git push` + + Do NOT attempt to fix: + - Type errors that require logic changes + - Complex refactoring suggestions + - Anything that could change behavior + + ## STEP 2: Review the PR + ${{ github.event.action == 'synchronize' && 'This is a RE-REVIEW after new commits. First, get the list of changed files in this latest push using `gh pr diff`. Review ONLY the changed files. Check ALL existing review comments and resolve ones that are now fixed.' || 'This is the INITIAL REVIEW.' }} Review this PR focusing ONLY on: @@ -56,7 +87,42 @@ jobs: - Use CLAUDE.md for project-specific guidance. - Use `gh pr comment` for summary-level feedback. - Use `mcp__github_inline_comment__create_inline_comment` sparingly for critical code issues only. - claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Read,Glob,Grep"' + + ## STEP 3: Coverage analysis + + Analyze test coverage for changed files: + + 1. Get the list of Python files changed in this PR (excluding tests): + `git diff --name-only origin/main...HEAD -- '*.py' | grep -v test` + + 2. Run tests with coverage on the PR branch (from django/aiservice): + `cd django/aiservice && uv run coverage run -m pytest -q --tb=no` + `cd django/aiservice && uv run coverage json -o coverage-pr.json` + + 3. Get coverage for changed files only: + `cd django/aiservice && uv run coverage report --include=""` + + 4. Compare with main branch coverage: + - Checkout main: `git checkout origin/main` + - Run coverage: `cd django/aiservice && uv run coverage run -m pytest -q --tb=no && uv run coverage json -o coverage-main.json` + - Checkout back: `git checkout -` + + 5. Analyze the diff to identify: + - NEW FILES: Files that don't exist on main (require good test coverage) + - MODIFIED FILES: Files with changes (changes must be covered by tests) + + 6. Report in PR comment with a markdown table: + - Coverage % for each changed file (PR vs main) + - Overall coverage change + - For NEW files: Flag if coverage is below 75% + - For MODIFIED files: Flag if the changed lines are not covered by tests + - Flag if overall coverage decreased + + Coverage requirements: + - New implementations/files: Must have ≥75% test coverage + - Modified code: Changed lines should be exercised by existing or new tests + - No coverage regressions: Overall coverage should not decrease + claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh api:*),Bash(cd django/aiservice*),Bash(uv run prek *),Bash(uv run coverage *),Bash(uv run pytest *),Bash(git status*),Bash(git add *),Bash(git commit *),Bash(git push*),Bash(git diff *),Bash(git checkout *),Read,Glob,Grep"' additional_permissions: | actions: read env: @@ -77,9 +143,13 @@ jobs: issues: read id-token: write actions: read + defaults: + run: + working-directory: django/aiservice steps: - name: Get PR head ref id: pr-ref + working-directory: . env: GH_TOKEN: ${{ github.token }} run: | @@ -97,12 +167,20 @@ jobs: fetch-depth: 0 ref: ${{ steps.pr-ref.outputs.ref }} + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Install dependencies + run: | + uv venv --seed + uv sync + - name: Run Claude Code id: claude uses: anthropics/claude-code-action@v1 with: use_foundry: "true" - claude_args: '--allowedTools "Read,Edit,Write,Glob,Grep,Bash(git status*),Bash(git diff*),Bash(git add *),Bash(git commit *),Bash(git push*),Bash(git log*),Bash(uv run prek *),Bash(prek *),Bash(gh pr comment*),Bash(gh pr view*)"' + claude_args: '--allowedTools "Read,Edit,Write,Glob,Grep,Bash(git status*),Bash(git diff*),Bash(git add *),Bash(git commit *),Bash(git push*),Bash(git log*),Bash(cd django/aiservice*),Bash(uv run prek *),Bash(prek *),Bash(uv run ruff *),Bash(uv run pytest *),Bash(uv run mypy *),Bash(uv run coverage *),Bash(gh pr comment*),Bash(gh pr view*),Bash(gh pr diff*)"' additional_permissions: | actions: read env: From 08fd1a8787ecc8cbfc7d8f63cbb93603cd43b5d8 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Thu, 5 Feb 2026 04:24:44 +0530 Subject: [PATCH 051/184] adding validation for ts in refiner and testgen (#2372) 1. languages/js_ts/testgen.py: - Updated parse_and_validate_js_output to accept a language parameter - Uses validate_typescript_syntax when language="typescript", otherwise uses validate_javascript_syntax - Updated generate_and_validate_js_test_code to accept and pass the language parameter - Updated the call chain to pass language through to the validation 2. optimizer/context_utils/refiner_context.py: - Added import for validate_typescript_syntax - Fixed is_valid_refinement method to use correct validator based on language - Fixed validate_code_syntax in SingleRefinerContext class - Fixed validate_code_syntax in MultiRefinerContext class 3. tests/optimizer/test_javascript_validator.py: - Added test_typescript_type_assertion_valid_in_ts - verifies as unknown as number is valid TypeScript - Added test_typescript_type_assertion_invalid_in_js - verifies as unknown as number is INVALID JavaScript (this would have caught the original bug) - Added test_typescript_generic_valid_in_ts - verifies generics are valid TypeScript - Added test_typescript_generic_invalid_in_js - verifies generics are INVALID JavaScript Files Already Correct (no changes needed): - languages/js_ts/optimizer.py - already correctly checks language - languages/js_ts/optimizer_lp.py - already correctly checks language - optimizer/optimizer_line_profiler.py - already correctly checks language --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- django/aiservice/languages/js_ts/testgen.py | 34 ++++++++++------- .../context_utils/refiner_context.py | 27 ++++++++++---- .../optimizer/test_javascript_validator.py | 37 ++++++++++++++++++- 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/languages/js_ts/testgen.py index 340301087..5390eb660 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/languages/js_ts/testgen.py @@ -124,9 +124,7 @@ def _is_valid_js_identifier(name: str) -> bool: # Patterns to strip file extensions from import paths # LLMs sometimes add .js extensions to TypeScript imports, which breaks module resolution -_JS_EXTENSION_PATTERN = re.compile( - r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""" -) +_JS_EXTENSION_PATTERN = re.compile(r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""") _REQUIRE_EXTENSION_PATTERN = re.compile( r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))""" ) @@ -259,14 +257,15 @@ def build_javascript_prompt( return messages, posthog_event_suffix -def parse_and_validate_js_output(response_content: str) -> str: - """Parse and validate the LLM response for JavaScript code. +def parse_and_validate_js_output(response_content: str, language: str = "javascript") -> str: + """Parse and validate the LLM response for JavaScript/TypeScript code. Args: response_content: Raw LLM response + language: Language to validate against ("javascript" or "typescript") Returns: - Validated JavaScript code + Validated JavaScript/TypeScript code Raises: ValueError: If no valid code block found @@ -284,10 +283,16 @@ def parse_and_validate_js_output(response_content: str) -> str: code = pattern_res.group(1).strip() - # Validate syntax - is_valid, error = validate_javascript_syntax(code) + # Validate syntax using the appropriate validator based on language + if language == "typescript": + is_valid, error = validate_typescript_syntax(code) + lang_name = "TypeScript" + else: + is_valid, error = validate_javascript_syntax(code) + lang_name = "JavaScript" + if not is_valid: - raise SyntaxError(f"Invalid JavaScript code: {error}") + raise SyntaxError(f"Invalid {lang_name} syntax: {error}") # Check for test functions if not _has_test_functions(code): @@ -311,8 +316,9 @@ async def generate_and_validate_js_test_code( posthog_event_suffix: str, trace_id: str = "", call_sequence: int | None = None, + language: str = "javascript", ) -> str: - """Generate and validate JavaScript test code using LLM. + """Generate and validate JavaScript/TypeScript test code using LLM. Args: messages: Prompt messages @@ -322,9 +328,10 @@ async def generate_and_validate_js_test_code( posthog_event_suffix: Suffix for PostHog events trace_id: Trace ID for logging call_sequence: Call sequence number + language: Language to validate against ("javascript" or "typescript") Returns: - Validated JavaScript test code + Validated JavaScript/TypeScript test code Raises: SyntaxError: If code is invalid @@ -357,8 +364,8 @@ async def generate_and_validate_js_test_code( properties={"model": model.name, "usage": response.raw_response.usage.model_dump_json()}, ) - # Parse and validate - validated_code = parse_and_validate_js_output(response.content) + # Parse and validate using the appropriate language validator + validated_code = parse_and_validate_js_output(response.content, language=language) return validated_code @@ -415,6 +422,7 @@ async def generate_javascript_tests_from_function( posthog_event_suffix=posthog_event_suffix, trace_id=trace_id, call_sequence=call_sequence, + language=language, ) total_llm_cost = sum(cost_tracker) diff --git a/django/aiservice/optimizer/context_utils/refiner_context.py b/django/aiservice/optimizer/context_utils/refiner_context.py index cb09b332f..05ecf17cb 100644 --- a/django/aiservice/optimizer/context_utils/refiner_context.py +++ b/django/aiservice/optimizer/context_utils/refiner_context.py @@ -8,7 +8,7 @@ from pydantic import ValidationError from aiservice.common.cst_utils import parse_module_to_cst from aiservice.common.markdown_utils import wrap_code_in_markdown -from aiservice.validators.javascript_validator import validate_javascript_syntax +from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax from optimizer.context_utils.context_helpers import ( group_code, is_markdown_structure_changed, @@ -92,7 +92,10 @@ class BaseRefinerContext: return False if self.data.language in ("javascript", "typescript"): - valid, _ = validate_javascript_syntax(stripped_code) + if self.data.language == "typescript": + valid, _ = validate_typescript_syntax(stripped_code) + else: + valid, _ = validate_javascript_syntax(stripped_code) return bool(valid) try: @@ -154,9 +157,14 @@ class SingleRefinerContext(BaseRefinerContext): def validate_code_syntax(self, code: str) -> None: """Validate code syntax based on language.""" if self.data.language in ("javascript", "typescript"): - valid, _ = validate_javascript_syntax(code) + if self.data.language == "typescript": + valid, _ = validate_typescript_syntax(code) + lang_name = "TypeScript" + else: + valid, _ = validate_javascript_syntax(code) + lang_name = "JavaScript" if not valid: - raise ValueError("Invalid JavaScript syntax") + raise ValueError(f"Invalid {lang_name} syntax") return # Python validation using libcst @@ -195,11 +203,16 @@ class MultiRefinerContext(BaseRefinerContext): def validate_code_syntax(self, code: str) -> None: """Validate code syntax based on language.""" - # For JavaScript/TypeScript, skip Python-specific validation + # For JavaScript/TypeScript, use the appropriate validator if self.data.language in ("javascript", "typescript"): - valid, _ = validate_javascript_syntax(code) + if self.data.language == "typescript": + valid, _ = validate_typescript_syntax(code) + lang_name = "TypeScript" + else: + valid, _ = validate_javascript_syntax(code) + lang_name = "JavaScript" if not valid: - raise ValueError("Invalid JavaScript syntax") + raise ValueError(f"Invalid {lang_name} syntax") return # Python validation using libcst diff --git a/django/aiservice/tests/optimizer/test_javascript_validator.py b/django/aiservice/tests/optimizer/test_javascript_validator.py index 5cfe34727..ab1ec2e9d 100644 --- a/django/aiservice/tests/optimizer/test_javascript_validator.py +++ b/django/aiservice/tests/optimizer/test_javascript_validator.py @@ -204,8 +204,41 @@ function add(a: number, b: number): number { } """ is_valid, error = validate_typescript_syntax(code) - # TypeScript uses the same validator as JavaScript - assert isinstance(is_valid, bool) + assert is_valid is True + assert error is None + + def test_typescript_type_assertion_valid_in_ts(self) -> None: + """Test that TypeScript type assertions are valid in TypeScript.""" + code = "const value = 4.9 as unknown as number;" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is True + assert error is None + + def test_typescript_type_assertion_invalid_in_js(self) -> None: + """Test that TypeScript type assertions are INVALID in JavaScript. + + This is a critical test - TypeScript-specific syntax like 'as unknown as number' + should fail when validated as JavaScript. This bug caused production issues + where generated TypeScript tests were incorrectly validated with JS parser. + """ + code = "const value = 4.9 as unknown as number;" + is_valid, error = validate_javascript_syntax(code) + assert is_valid is False + assert error is not None + + def test_typescript_generic_valid_in_ts(self) -> None: + """Test that TypeScript generics are valid in TypeScript.""" + code = "function identity(arg: T): T { return arg; }" + is_valid, error = validate_typescript_syntax(code) + assert is_valid is True + assert error is None + + def test_typescript_generic_invalid_in_js(self) -> None: + """Test that TypeScript generics are INVALID in JavaScript.""" + code = "function identity(arg: T): T { return arg; }" + is_valid, error = validate_javascript_syntax(code) + assert is_valid is False + assert error is not None def test_typescript_interface(self) -> None: """Test that TypeScript interfaces pass validation (if Node available).""" From 07d33edd9ffa756cf8bd31c700dd45e014b4724a Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:08:02 -0500 Subject: [PATCH 052/184] CF-1041 observability v2 (#2329) introducing this due to pain points in V1, not a complete rewrite, based off v1 --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Kevin Turcios Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .gitignore | 1 + .vscode/settings.json | 3 + .../prompts/testgen/execute_system_prompt.md | 4 +- django/aiservice/testgen/testgen.py | 27 +- js/cf-webapp/.gitignore | 6 +- js/cf-webapp/next.config.mjs | 12 +- js/cf-webapp/package-lock.json | 760 ++++++++++++-- js/cf-webapp/package.json | 11 + .../src/app/(auth)/codeflash/auth/content.tsx | 7 +- .../apikeys/dialog-create-api-key.tsx | 5 +- .../review-optimizations/[traceId]/page.tsx | 7 +- .../[traceId]/profiler/page.tsx | 3 +- js/cf-webapp/src/app/dashboard/page.tsx | 1 + js/cf-webapp/src/app/globals.css | 136 +-- js/cf-webapp/src/app/layout.tsx | 11 +- js/cf-webapp/src/app/observability/layout.tsx | 17 +- .../app/observability/llm-call/[id]/page.tsx | 524 ---------- .../src/app/observability/llm-calls/page.tsx | 800 --------------- .../src/app/observability/loading.tsx | 39 + js/cf-webapp/src/app/observability/page.tsx | 328 +++++++ .../observability/trace/[trace_id]/page.tsx | 767 --------------- .../src/app/observability/traces/page.tsx | 654 ------------ .../src/app/trace/[trace_id]/page.tsx | 174 ---- .../src/components/conditional-layout.tsx | 1 - .../src/components/dashboard/sidebar.tsx | 2 +- .../observability/code-context-section.tsx | 378 +++++++ .../observability/code-highlighter.tsx | 172 ++++ .../observability/column-header.tsx | 27 - .../observability/errors-section.tsx | 201 ++++ .../function-to-optimize-section.tsx | 204 ++++ .../components/observability/help-button.tsx | 51 - .../src/components/observability/index.ts | 9 + .../observability/observability-nav.tsx | 55 -- .../observability/parsed-response-view.tsx | 223 ----- .../components/observability/python-parser.ts | 136 +++ .../components/observability/stat-card.tsx | 72 -- .../observability/timeline-page-view.tsx | 823 ++++++++++++++++ .../observability/timeline-types.ts | 289 ++++++ .../components/observability/trace-search.tsx | 83 ++ .../observability/trace-summary.tsx | 124 +++ .../src/components/observability/utils.ts | 16 + .../components/trace/monaco-diff-viewer.tsx | 928 ------------------ js/cf-webapp/src/components/ui/badge.tsx | 10 +- js/cf-webapp/src/components/ui/button.tsx | 20 +- js/cf-webapp/src/components/ui/card.tsx | 12 +- .../src/components/ui/icon-example.tsx | 45 + js/cf-webapp/src/components/ui/input.tsx | 2 +- js/cf-webapp/src/components/ui/select.tsx | 16 +- js/cf-webapp/src/components/ui/separator.tsx | 4 +- js/cf-webapp/src/components/ui/switch.tsx | 4 +- js/cf-webapp/src/components/ui/table.tsx | 10 +- js/cf-webapp/src/components/ui/tabs.tsx | 4 +- .../src/lib/observability-response-parse.ts | 68 -- js/cf-webapp/src/lib/observability-utils.ts | 38 - js/cf-webapp/src/middleware.ts | 1 - js/cf-webapp/src/styles/spacing.css | 47 + js/cf-webapp/src/styles/tokens.css | 184 ++++ js/cf-webapp/src/styles/typography.css | 29 + js/cf-webapp/tailwind.config.ts | 98 +- js/common/package-lock.json | 6 - 60 files changed, 4069 insertions(+), 4620 deletions(-) delete mode 100644 js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx delete mode 100644 js/cf-webapp/src/app/observability/llm-calls/page.tsx create mode 100644 js/cf-webapp/src/app/observability/loading.tsx create mode 100644 js/cf-webapp/src/app/observability/page.tsx delete mode 100644 js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx delete mode 100644 js/cf-webapp/src/app/observability/traces/page.tsx delete mode 100644 js/cf-webapp/src/app/trace/[trace_id]/page.tsx create mode 100644 js/cf-webapp/src/components/observability/code-context-section.tsx create mode 100644 js/cf-webapp/src/components/observability/code-highlighter.tsx delete mode 100644 js/cf-webapp/src/components/observability/column-header.tsx create mode 100644 js/cf-webapp/src/components/observability/errors-section.tsx create mode 100644 js/cf-webapp/src/components/observability/function-to-optimize-section.tsx delete mode 100644 js/cf-webapp/src/components/observability/help-button.tsx create mode 100644 js/cf-webapp/src/components/observability/index.ts delete mode 100644 js/cf-webapp/src/components/observability/observability-nav.tsx delete mode 100644 js/cf-webapp/src/components/observability/parsed-response-view.tsx create mode 100644 js/cf-webapp/src/components/observability/python-parser.ts delete mode 100644 js/cf-webapp/src/components/observability/stat-card.tsx create mode 100644 js/cf-webapp/src/components/observability/timeline-page-view.tsx create mode 100644 js/cf-webapp/src/components/observability/timeline-types.ts create mode 100644 js/cf-webapp/src/components/observability/trace-search.tsx create mode 100644 js/cf-webapp/src/components/observability/trace-summary.tsx create mode 100644 js/cf-webapp/src/components/observability/utils.ts delete mode 100644 js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx create mode 100644 js/cf-webapp/src/components/ui/icon-example.tsx delete mode 100644 js/cf-webapp/src/lib/observability-response-parse.ts delete mode 100644 js/cf-webapp/src/lib/observability-utils.ts create mode 100644 js/cf-webapp/src/styles/spacing.css create mode 100644 js/cf-webapp/src/styles/tokens.css create mode 100644 js/cf-webapp/src/styles/typography.css diff --git a/.gitignore b/.gitignore index 955c94b53..2f62d43ea 100644 --- a/.gitignore +++ b/.gitignore @@ -163,6 +163,7 @@ cython_debug/ #.idea/ .aider* .serena/ +.planning/ /js/common/node_modules/ /node_modules/ *.xml diff --git a/.vscode/settings.json b/.vscode/settings.json index dcf0e78be..17f421bd4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,8 @@ ], "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" } } \ No newline at end of file diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md index dbe648c35..2125f9e4a 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md @@ -23,8 +23,8 @@ **CRITICAL: IMPORT PATH RULES**: - **NEVER add file extensions (.js, .ts, .tsx) to import paths** - The test framework resolves extensions automatically. -- **WRONG**: `import {{fn}} from '../utils.js'` or `import {{fn}} from '../utils.ts'` -- **CORRECT**: `import {{fn}} from '../utils'` +- **WRONG**: `import {{ fn }} from '../utils.js'` or `import {{ fn }} from '../utils.ts'` +- **CORRECT**: `import {{ fn }} from '../utils'` - The user message provides the exact import statement to use - copy it exactly without modification. **CRITICAL: VITEST IMPORTS REQUIRED**: diff --git a/django/aiservice/testgen/testgen.py b/django/aiservice/testgen/testgen.py index a1f3cdb15..1d82661ee 100644 --- a/django/aiservice/testgen/testgen.py +++ b/django/aiservice/testgen/testgen.py @@ -241,8 +241,30 @@ async def generate_and_validate_test_code( call_sequence: int | None = None, function_to_optimize: FunctionToOptimize | None = None, module_path: str | None = None, + test_module_path: str | None = None, + helper_function_names: list[str] | None = None, + is_async: bool = False, ) -> str: - obs_context: dict | None = {"call_sequence": call_sequence} if call_sequence is not None else None + obs_context: dict | None = ( + { + "call_sequence": call_sequence, + "module_path": module_path, + "test_module_path": test_module_path, + "helper_function_names": helper_function_names, + "is_async": is_async, + "function_to_optimize": { + "function_name": function_to_optimize.function_name, + "file_path": function_to_optimize.file_path, + "qualified_name": function_to_optimize.qualified_name, + "starting_line": function_to_optimize.starting_line, + "ending_line": function_to_optimize.ending_line, + } + if function_to_optimize is not None + else None, + } + if call_sequence is not None + else None + ) response = await call_llm( llm=model, messages=messages, @@ -318,6 +340,9 @@ async def generate_regression_tests_from_function( call_sequence=call_sequence, function_to_optimize=data.function_to_optimize, module_path=data.module_path, + test_module_path=data.test_module_path, + helper_function_names=data.helper_function_names, + is_async=data.function_to_optimize.is_async or data.is_async or False, ) total_llm_cost = sum(cost_tracker) await update_optimization_cost(trace_id=trace_id, cost=total_llm_cost, user_id=user_id) diff --git a/js/cf-webapp/.gitignore b/js/cf-webapp/.gitignore index 79b38816a..6f7554965 100644 --- a/js/cf-webapp/.gitignore +++ b/js/cf-webapp/.gitignore @@ -42,4 +42,8 @@ next-env.d.ts /.npmrc .npmrc /.azure/config -*.next/* \ No newline at end of file +*.next/* + +# Generated WASM files (built by postinstall) +/public/web-tree-sitter.wasm +/public/tree-sitter-python.wasm \ No newline at end of file diff --git a/js/cf-webapp/next.config.mjs b/js/cf-webapp/next.config.mjs index 08455a984..34001279e 100644 --- a/js/cf-webapp/next.config.mjs +++ b/js/cf-webapp/next.config.mjs @@ -1,11 +1,21 @@ /** @type {import("next").NextConfig} */ let nextConfig = { transpilePackages: ["@codeflash-ai/common"], - webpack: (config) => { + webpack: (config, { isServer }) => { config.watchOptions = { poll: 1000, aggregateTimeout: 300, } + // Handle web-tree-sitter's Node.js module imports in browser + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + "fs/promises": false, + path: false, + module: false, + } + } return config }, experimental: { diff --git a/js/cf-webapp/package-lock.json b/js/cf-webapp/package-lock.json index 4f7668962..33fbba8a1 100644 --- a/js/cf-webapp/package-lock.json +++ b/js/cf-webapp/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "codeflash-webapp", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@auth0/nextjs-auth0": "^3.3.0", "@azure/msal-node": "^3.7.3", @@ -14,6 +15,8 @@ "@hookform/resolvers": "^3.3.2", "@monaco-editor/react": "^4.7.0", "@prisma/client": "^6.7.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -34,6 +37,7 @@ "chart.js": "^4.4.9", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cmdk": "^1.1.1", "date-fns": "^4.1.0", "diff": "^8.0.2", "framer-motion": "^12.12.1", @@ -52,6 +56,7 @@ "prism-react-renderer": "^2.4.1", "react": "^18", "react-chartjs-2": "^5.3.0", + "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18", "react-hook-form": "^7.48.2", "react-markdown": "^9.0.1", @@ -59,10 +64,13 @@ "react-syntax-highlighter": "^16.1.0", "remark-gfm": "^4.0.0", "sharp": "^0.34.2", + "shiki": "^3.21.0", "sonner": "^2.0.6", "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", + "web-tree-sitter": "^0.26.3", "zod": "^3.22.4" }, "devDependencies": { @@ -82,9 +90,12 @@ "eslint-plugin-react": "^7.33.2", "jsdom": "^24.1.0", "lint-staged": "^15.4.3", + "postcss-import": "^16.1.1", "prettier": "3.2.5", "prisma": "^6.7.0", "simple-git-hooks": "^2.9.0", + "tree-sitter-cli": "^0.26.3", + "tree-sitter-python": "^0.25.0", "typescript": "^5.4.5", "vitest": "^3.0.8" }, @@ -383,15 +394,6 @@ "node": ">=0.8.0" } }, - "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": { - "version": "15.13.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", - "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@azure/msal-common": { "version": "15.13.0", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", @@ -443,7 +445,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -832,7 +833,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -856,7 +856,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -894,6 +893,115 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "license": "MIT", + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", @@ -2098,6 +2206,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2371,7 +2480,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2422,7 +2530,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", @@ -3105,7 +3212,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz", "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -3277,6 +3383,37 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -3300,6 +3437,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -5075,6 +5242,73 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@shikijs/core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", + "integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz", + "integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz", + "integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz", + "integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz", + "integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz", + "integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -5184,7 +5418,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5272,6 +5507,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5282,6 +5518,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -5315,7 +5552,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -5364,7 +5602,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5378,6 +5615,12 @@ "@types/node": "*" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/pg": { "version": "8.15.5", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", @@ -5415,7 +5658,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5426,7 +5668,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5455,13 +5696,6 @@ "@types/node": "*" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5513,7 +5747,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -6165,6 +6398,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -6174,25 +6408,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6203,13 +6441,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6222,6 +6462,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -6231,6 +6472,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -6239,13 +6481,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6262,6 +6506,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -6275,6 +6520,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6287,6 +6533,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6301,6 +6548,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -6310,20 +6558,21 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6345,6 +6594,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -6393,6 +6643,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -6410,6 +6661,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6425,7 +6677,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ansi-escapes": { "version": "7.1.1", @@ -6529,6 +6782,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -6811,6 +7065,21 @@ "node": ">= 0.4" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -6894,7 +7163,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6919,7 +7187,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/builtin-modules": { "version": "3.3.0", @@ -7061,7 +7330,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7181,7 +7449,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -7220,6 +7487,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -7252,6 +7520,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -7300,6 +7574,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -7402,6 +7692,31 @@ "node": ">= 0.6" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7756,9 +8071,9 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -7788,13 +8103,15 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -7878,6 +8195,7 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -7912,6 +8230,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -8146,7 +8473,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8162,7 +8488,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8386,7 +8711,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8877,6 +9201,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -9015,7 +9340,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fastq": { "version": "1.19.1", @@ -9087,6 +9413,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9494,7 +9826,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/glob/node_modules/minimatch": { "version": "8.0.4", @@ -9672,6 +10005,29 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -9776,6 +10132,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -9851,7 +10217,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -9968,6 +10333,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -10559,6 +10930,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -10573,6 +10945,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10589,7 +10962,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -10926,6 +11298,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -11160,6 +11533,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11486,6 +11860,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -12181,6 +12561,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -12191,6 +12572,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -12275,14 +12657,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "14.2.35", "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", @@ -12366,6 +12748,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -12415,6 +12807,18 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", @@ -12698,6 +13102,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -12831,7 +13252,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -12865,6 +13285,24 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -12943,6 +13381,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -12972,7 +13419,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -13147,7 +13593,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13158,9 +13603,10 @@ } }, "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.1.tgz", + "integrity": "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -13168,7 +13614,7 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -13347,9 +13793,9 @@ } }, "node_modules/preact": { - "version": "10.27.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", - "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", "funding": { "type": "opencollective", @@ -13388,6 +13834,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -13403,6 +13850,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -13415,7 +13863,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/prism-react-renderer": { "version": "2.4.1", @@ -13437,7 +13886,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" @@ -13479,7 +13927,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -13544,9 +13991,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -13590,6 +14037,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -13610,7 +14058,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13628,12 +14075,40 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-diff-viewer-continued": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz", + "integrity": "sha512-kMZmUyb3Pv5L9vUtCfIGYsdOHs8mUojblGy1U1Sm0D7FhAOEsH9QhnngEIRo5hXWIPNGupNRJls1TJ6Eqx84eg==", + "license": "MIT", + "dependencies": { + "@emotion/css": "^11.11.2", + "classnames": "^2.3.2", + "diff": "^5.1.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-diff-viewer-continued/node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13647,7 +14122,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13913,6 +14387,30 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -14005,6 +14503,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14051,7 +14550,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -14185,7 +14683,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -14373,6 +14870,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -14409,6 +14907,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -14420,7 +14919,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "7.7.3", @@ -14439,6 +14939,7 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -14555,6 +15056,22 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", + "integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.21.0", + "@shikijs/engine-javascript": "3.21.0", + "@shikijs/engine-oniguruma": "3.21.0", + "@shikijs/langs": "3.21.0", + "@shikijs/themes": "3.21.0", + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, "node_modules/shimmer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", @@ -14708,6 +15225,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14726,6 +15244,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15183,6 +15702,12 @@ } } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -15424,6 +15949,23 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tailwindcss/node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/tailwindcss/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -15441,6 +15983,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" }, @@ -15454,6 +15997,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -15472,6 +16016,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -15505,7 +16050,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/text-table": { "version": "0.2.0", @@ -15637,6 +16183,40 @@ "node": ">=18" } }, + "node_modules/tree-sitter-cli": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.26.3.tgz", + "integrity": "sha512-1VHpmjnTsYJk03HDqzLGn9dmJaLsJ7YeGsnnSudC6XOZu5oasz0GEVOIVCTe6hA01YZJgHd1XGO6XJZe0Sj7tw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "tree-sitter": "cli.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tree-sitter-python": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz", + "integrity": "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -15818,7 +16398,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16171,6 +16750,19 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -16205,7 +16797,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16396,6 +16987,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16404,6 +16996,12 @@ "node": ">=10.13.0" } }, + "node_modules/web-tree-sitter": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.3.tgz", + "integrity": "sha512-JIVgIKFS1w6lejxSntCtsS/QsE/ecTS00en809cMxMPxaor6MvUnQ+ovG8uTTTvQCFosSh4MeDdI5bSGw5SoBw==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -16419,6 +17017,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -16482,6 +17081,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -16495,6 +17095,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -16856,7 +17457,6 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "devOptional": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/js/cf-webapp/package.json b/js/cf-webapp/package.json index 9fa2c2fdd..52d96ca53 100644 --- a/js/cf-webapp/package.json +++ b/js/cf-webapp/package.json @@ -14,6 +14,7 @@ "prisma:generate": "npx prisma generate", "prisma:migrate": "npx prisma migrate dev", "prepare": "simple-git-hooks", + "postinstall": "cp node_modules/web-tree-sitter/web-tree-sitter.wasm public/ && npx tree-sitter build --wasm node_modules/tree-sitter-python -o public/tree-sitter-python.wasm", "format": "prettier --write \"**/*.{js,ts,tsx,json,md}\"", "format:check": "prettier --check \"**/*.{js,ts,tsx,json,md}\"" }, @@ -24,6 +25,8 @@ "@hookform/resolvers": "^3.3.2", "@monaco-editor/react": "^4.7.0", "@prisma/client": "^6.7.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -44,6 +47,7 @@ "chart.js": "^4.4.9", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cmdk": "^1.1.1", "date-fns": "^4.1.0", "diff": "^8.0.2", "framer-motion": "^12.12.1", @@ -62,6 +66,7 @@ "prism-react-renderer": "^2.4.1", "react": "^18", "react-chartjs-2": "^5.3.0", + "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18", "react-hook-form": "^7.48.2", "react-markdown": "^9.0.1", @@ -69,10 +74,13 @@ "react-syntax-highlighter": "^16.1.0", "remark-gfm": "^4.0.0", "sharp": "^0.34.2", + "shiki": "^3.21.0", "sonner": "^2.0.6", "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", + "web-tree-sitter": "^0.26.3", "zod": "^3.22.4" }, "devDependencies": { @@ -92,9 +100,12 @@ "eslint-plugin-react": "^7.33.2", "jsdom": "^24.1.0", "lint-staged": "^15.4.3", + "postcss-import": "^16.1.1", "prettier": "3.2.5", "prisma": "^6.7.0", "simple-git-hooks": "^2.9.0", + "tree-sitter-cli": "^0.26.3", + "tree-sitter-python": "^0.25.0", "typescript": "^5.4.5", "vitest": "^3.0.8" }, diff --git a/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx b/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx index ed8e454c5..3d0af8bf7 100644 --- a/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx +++ b/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx @@ -3,6 +3,7 @@ import LogoBox from "@/components/dashboard/logo-box" import { useState, useEffect } from "react" import { useRouter, useSearchParams } from "next/navigation" +import Image from "next/image" import { Loading } from "@/components/ui/loading" import { authorizeOAuth, @@ -287,9 +288,11 @@ export default function CodeFlashAuthContent() { }`} > {userInfo?.avatarUrl ? ( - {userInfo.name} ) : ( @@ -327,7 +330,7 @@ export default function CodeFlashAuthContent() { }`} > {org.avatarUrl ? ( - {org.name} + {org.name} ) : (
{getInitials(org.name)} diff --git a/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx b/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx index d9097c2ab..80827fadb 100644 --- a/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx +++ b/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx @@ -22,6 +22,7 @@ import React from "react" import { generateToken } from "./tokenfuncs" import { Plus, User, Building2, Check } from "lucide-react" import { useViewMode } from "@/app/app/ViewModeContext" +import Image from "next/image" import { Select, SelectContent, @@ -175,9 +176,11 @@ export function CreateApiKeyDialog(): React.JSX.Element {
{org.avatarUrl ? ( - {org.name} ) : ( diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx index 90ed1d440..f492e9502 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx @@ -287,6 +287,7 @@ export default function OptimizationReviewPage() { } } loadEvent() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [params.traceId, currentOrg?.id]) const loadComments = async (eventId: string) => { @@ -603,9 +604,9 @@ export default function OptimizationReviewPage() { window.open(constructedUrl, "_blank") } }, 1000) - } catch (error: any) { + } catch (error: unknown) { console.error("[handleCreatePR] Exception:", error) - const errorMessage = error?.message || "Failed to create pull request" + const errorMessage = error instanceof Error ? error.message : "Failed to create pull request" toast.error(errorMessage, { duration: 5000, }) @@ -682,7 +683,7 @@ export default function OptimizationReviewPage() { )} {event.speedup_x && ( - + diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx index c2b748a60..da6bf0316 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx @@ -122,6 +122,7 @@ export default function LineProfilerPage() { } loadEvent() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [params.traceId, currentOrg?.id]) const handleBack = () => { @@ -204,7 +205,7 @@ export default function LineProfilerPage() { )} {event.speedup_x && ( - + diff --git a/js/cf-webapp/src/app/dashboard/page.tsx b/js/cf-webapp/src/app/dashboard/page.tsx index 399bec2ec..609ebc659 100644 --- a/js/cf-webapp/src/app/dashboard/page.tsx +++ b/js/cf-webapp/src/app/dashboard/page.tsx @@ -203,6 +203,7 @@ function Dashboard() { setLoading(false) fetchingRef.current = false } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedYear, currentOrgId]) useEffect(() => { diff --git a/js/cf-webapp/src/app/globals.css b/js/cf-webapp/src/app/globals.css index adb19e016..a790ed1e1 100644 --- a/js/cf-webapp/src/app/globals.css +++ b/js/cf-webapp/src/app/globals.css @@ -2,84 +2,18 @@ @tailwind components; @tailwind utilities; +/* Import the design token system */ +@import "../styles/tokens.css"; +@import "../styles/typography.css"; +@import "../styles/spacing.css"; + @layer base { - :root { - /* Background and foreground */ - --background: 0 0% 99%; - --foreground: 222.2 84% 4.9%; - - /* Card styles */ - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - - /* Popover styles */ - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - - /* Codeflash primary colors - converted from hex to HSL */ - --primary: 38 100% 63%; /* #d08e0d - Codeflash yellow */ - --primary-foreground: 0 6% 4%; - - /* Secondary colors - complementary to Codeflash yellow */ - --secondary: 41 88% 95%; /* Lighter version of primary */ - --secondary-foreground: 41 88% 20%; /* Darker version for contrast */ - - /* Accent colors - variation of the Codeflash yellow */ - --accent: 41 70% 90%; /* Softer version of primary */ - --accent-foreground: 41 88% 20%; - - /* Other UI colors aligned with brand */ - --muted: 41 20% 96%; - --muted-foreground: 41 8% 46%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 100%; - - --border: 41 30% 90%; - --input: 41 30% 90%; - --ring: 38 100% 63%; /* Matching primary - Codeflash yellow */ - - --radius: 0.5rem; - - /* Code highlighting */ - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); - } + /* Light mode removed - dark mode only implementation */ .dark { - /* Background and foreground */ - --background: 0, 6%, 5%; - --foreground: 0 0% 100%; - - /* Card styles */ - --card: 0 3% 11%; - --card-foreground: 0 0% 100%; - - /* Popover styles */ - --popover: 222.2 84% 4.9%; - --popover-foreground: 0 0% 100%; - - /* Codeflash primary colors for dark mode */ - --primary: 38 100% 63%; /* #ffd227 - Codeflash yellow for dark mode */ - --primary-foreground: 222.2 47.4% 11.2%; - - /* Secondary colors - complementary to Codeflash yellow in dark mode */ - --secondary: 48 60% 25%; /* Darker version of primary */ - --secondary-foreground: 48 100% 80%; /* Lighter version for contrast */ - - /* Accent colors - variation of the Codeflash yellow */ - --accent: 48 70% 30%; /* Softer version of primary */ - --accent-foreground: 48 100% 80%; - - /* Other UI colors aligned with brand */ - --muted: 48 15% 20%; - --muted-foreground: 48 20% 65%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 100%; - - --border: 48 20% 25%; - --input: 48 20% 25%; - --ring: 38 100% 63%; /* Matching primary - Codeflash yellow */ + /* All color tokens are defined in tokens.css */ + /* The .dark class enables Tailwind's dark: variant */ + /* Since we're dark mode only, tokens are already set for dark mode */ /* Code highlighting */ --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); @@ -88,8 +22,8 @@ @layer base { ::selection { - background: #ffd227; /* CF brand color */ - color: #1f2937; /* Tailwind's gray-800 for selected text color */ + background: rgb(113 113 122); /* zinc-500 - no brand colors */ + color: rgb(250 250 250); /* zinc-50 for contrast */ } * { @@ -146,11 +80,11 @@ } .prose code { - background-color: hsl(var(--muted)); + background-color: rgb(var(--muted)); padding: 0.125em 0.25em; border-radius: 0.25em; font-size: 0.875em; - font-family: ui-monospace, monospace; + font-family: var(--font-mono); } .prose pre { @@ -164,10 +98,10 @@ } .prose blockquote { - border-left: 3px solid hsl(var(--border)); + border-left: 3px solid rgb(var(--border)); padding-left: 1em; margin-left: 0; - color: hsl(var(--muted-foreground)); + color: rgb(var(--muted-foreground)); } /* Fixed list styles to show bullets and numbers */ @@ -204,7 +138,7 @@ } .prose a { - color: hsl(var(--primary)); + color: rgb(var(--primary)); text-decoration: underline; } @@ -239,10 +173,42 @@ /* Dark mode adjustments */ .dark .prose code { - background-color: hsl(var(--muted)); + background-color: rgb(var(--muted)); } .dark .prose blockquote { - border-left-color: hsl(var(--border)); - color: hsl(var(--muted-foreground)); + border-left-color: rgb(var(--border)); + color: rgb(var(--muted-foreground)); +} + +/* Typography utility classes for common patterns */ +@layer utilities { + /* Apply monospace font for inline code */ + .text-code { + font-family: var(--font-mono); + font-size: var(--text-sm); + } + + /* Apply monospace font with tight line height for data */ + .text-data { + font-family: var(--font-mono); + line-height: var(--leading-tight); + } + + /* Apply sans font with medium weight for UI labels */ + .text-label { + font-family: var(--font-sans); + font-weight: 500; + } + + /* Standard container padding using spacing tokens */ + .container-spacing { + padding-left: var(--space-4); + padding-right: var(--space-4); + } + + /* Standard card internal spacing */ + .card-spacing { + padding: var(--space-3); + } } diff --git a/js/cf-webapp/src/app/layout.tsx b/js/cf-webapp/src/app/layout.tsx index 9281c9fa8..b34ff8053 100644 --- a/js/cf-webapp/src/app/layout.tsx +++ b/js/cf-webapp/src/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next" -import { Inter as FontSans } from "next/font/google" +import { Inter as FontSans, JetBrains_Mono } from "next/font/google" import "./globals.css" import { cn } from "@/lib/utils" import { ThemeProvider } from "@/components/theme-provider" @@ -23,6 +23,13 @@ const fontSans = FontSans({ variable: "--font-sans", }) +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700"], + variable: "--font-jetbrains-mono", + display: "swap", +}) + export const metadata: Metadata = { title: "Codeflash", description: "Optimize the performance of your code.", @@ -91,7 +98,7 @@ export default async function RootLayout({ }} /> - + - -
{children}
+
+ +
{children}
) } diff --git a/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx b/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx deleted file mode 100644 index ba0ad4d3e..000000000 --- a/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx +++ /dev/null @@ -1,524 +0,0 @@ -import Link from "next/link" -import { Metadata } from "next" -import { notFound } from "next/navigation" -import { - CheckCircle, - XCircle, - Hash, - FileText, - Code, - AlertTriangle, -} from "lucide-react" -import { prisma } from "@/lib/prisma" -import { StatCard } from "@/components/observability/stat-card" -import { InfoIcon } from "@/components/observability/info-icon" -import { CopyButton } from "@/components/observability/copy-button" -import { ParsedResponseView } from "@/components/observability/parsed-response-view" - -interface LLMCallDetailPageProps { - params: { - id: string - } -} - -export async function generateMetadata({ params }: LLMCallDetailPageProps): Promise { - return { - title: `LLM Call ${params.id.substring(0, 8)} - Observability`, - description: "View LLM call details for prompt engineering analysis", - } -} - -export default async function LLMCallDetailPage({ params }: LLMCallDetailPageProps) { - // Fetch LLM call details - const llmCall = await prisma.llm_calls.findUnique({ - where: { id: params.id }, - }) - - if (!llmCall) { - notFound() - } - - // Fetch related errors - const relatedErrors = await prisma.optimization_errors.findMany({ - where: { llm_call_id: params.id }, - orderBy: { created_at: "desc" }, - }) - - return ( -
- {/* Header */} -
- {/* Breadcrumb */} -
- - LLM Calls - - / - - {llmCall.id.substring(0, 8)}... - -
- -

- LLM Call Detail -

- - {/* ID and Trace with Copy Buttons */} -
-
- Call ID: - - {llmCall.id} - - -
- {llmCall.trace_id && ( -
- Trace: - - {llmCall.trace_id} - - -
- )} -
-
- - {/* Summary Cards */} -
- - - - -
- - {/* Metadata */} -
-

Metadata

-
-
-
-
- - Call Type - - -
- - {llmCall.call_type} - -
-
-
-
-
- Model - -
- - {llmCall.model_name} - -
-
-
-
-
- - Temperature - - -
- - {llmCall.temperature || "default"} - -
-
-
-
-
- - Candidates Requested - - -
- - {llmCall.n_candidates || "N/A"} - -
-
-
-
-
- - Created - - -
- - {new Date(llmCall.created_at).toLocaleString()} - -
-
-
-
-
- - Parsing Status - - -
- - {llmCall.parsing_status || "N/A"} - -
-
-
-
- - {/* Token Breakdown */} - {llmCall.prompt_tokens && ( -
-
- -

Token Usage

-
- - {/* Visual Token Ratio Bar */} -
-
- - Token Distribution - - -
-
- {(() => { - const promptTokens = llmCall.prompt_tokens ?? 0 - const completionTokens = llmCall.completion_tokens ?? 0 - const totalTokens = llmCall.total_tokens ?? (promptTokens + completionTokens) - // Use 1 as fallback only for division to prevent division by zero - const safeTotalTokens = totalTokens || 1 - const promptPercent = Math.round((promptTokens / safeTotalTokens) * 100) - const completionPercent = Math.round((completionTokens / safeTotalTokens) * 100) - - return ( - <> -
- {promptPercent > 0 && {promptPercent}%} -
-
- {completionPercent > 0 && {completionPercent}%} -
- - ) - })()} -
-
- -
- {(() => { - const promptTokens = llmCall.prompt_tokens ?? 0 - const completionTokens = llmCall.completion_tokens ?? 0 - const totalTokens = llmCall.total_tokens ?? (promptTokens + completionTokens) - - return ( - <> -
-
-
- - Prompt Tokens - -
-
- {promptTokens.toLocaleString()} -
-
-
-
-
- - Completion Tokens - -
-
- {completionTokens.toLocaleString()} -
-
-
-
- - Total Tokens - -
-
- {totalTokens.toLocaleString()} -
-
- - ) - })()} -
-
- )} - - {/* Parsing Results */} - {llmCall.candidates_generated !== null && ( -
-
- -

Parsing Results

- -
-
-
-
- - Candidates Generated - -
-
- {llmCall.candidates_generated} -
-
-
-
- - - Candidates Valid - -
-
- {llmCall.candidates_valid} -
-
-
- {llmCall.parsing_errors && ( -
-
- -
- Parsing Errors -
-
-
-                {JSON.stringify(llmCall.parsing_errors, null, 2)}
-              
-
- )} -
- )} - - {/* Prompts */} -
-
-
- -

System Prompt

- -
-
- - {llmCall.system_prompt?.length.toLocaleString() || 0} characters - - -
-
-
-          {llmCall.system_prompt}
-        
-
- -
-
-
- -

User Prompt

- -
-
- - {llmCall.user_prompt?.length.toLocaleString() || 0} characters - - -
-
-
-          {llmCall.user_prompt}
-        
-
- - {/* Response — parsed by call type (ranking: rank/explain; optimization: code blocks + text), with View raw */} - {llmCall.raw_response && ( -
-
-
- -

LLM Response

- -
-
- - {llmCall.raw_response.length.toLocaleString()} characters - - -
-
- -
- )} - - {/* Error Information */} - {llmCall.status === "failed" && llmCall.error_message && ( -
-
- -

- Error Information -

-
-
-
- Error Type: - - {llmCall.error_type} - -
-
-
-
- Error Message: - -
-
-              {llmCall.error_message}
-            
-
-
- )} - - {/* Related Errors */} - {relatedErrors.length > 0 && ( -
-
- -

Related Errors

- - {relatedErrors.length} - -
-
- {relatedErrors.map(error => ( -
-
- - {error.severity} - - - {error.error_type} - - - {new Date(error.created_at).toLocaleString()} - -
-
- {error.error_message} -
- {error.context && ( -
- - View context - -
-                      {JSON.stringify(error.context, null, 2)}
-                    
-
- )} -
- ))} -
-
- )} - {/* Actions */} -
- - View Full Trace → - - - ← Back to List - -
-
- ) -} diff --git a/js/cf-webapp/src/app/observability/llm-calls/page.tsx b/js/cf-webapp/src/app/observability/llm-calls/page.tsx deleted file mode 100644 index 966d7b5dd..000000000 --- a/js/cf-webapp/src/app/observability/llm-calls/page.tsx +++ /dev/null @@ -1,800 +0,0 @@ -import Link from "next/link" -import { Metadata } from "next" -import { unstable_cache } from "next/cache" -import { Award, Database as DatabaseIcon, Github, Terminal } from "lucide-react" -import { prisma } from "@/lib/prisma" -import { getCallSource } from "@/lib/observability-utils" -import { HelpButton } from "@/components/observability/help-button" -import { StatCard } from "@/components/observability/stat-card" -import { ColumnHeader } from "@/components/observability/column-header" -import { InfoIcon } from "@/components/observability/info-icon" - -export const metadata: Metadata = { - title: "LLM Calls - Observability", - description: "View all LLM API calls for prompt engineering analysis", -} - -interface SearchParams { - call_type?: string - model?: string - status?: string - trace_id?: string - page?: string - organization?: string -} - -// Cached function to get unique organizations list -// Revalidates every 5 minutes - organizations change infrequently -const getUniqueOrganizations = unstable_cache( - async () => { - const allOrganizations = await prisma.optimization_features.findMany({ - select: { organization: true }, - distinct: ["organization"], - where: { organization: { not: null } }, - }) - return allOrganizations - .map(f => f.organization) - .filter(Boolean) - .sort() as string[] - }, - ["unique-organizations"], - { revalidate: 300 }, // 5 minutes -) - -// Cached function to get unique call types -// Revalidates every 5 minutes - call types change infrequently -const getCallTypes = unstable_cache( - async () => { - const callTypes = await prisma.llm_calls.findMany({ - select: { call_type: true }, - distinct: ["call_type"], - }) - return callTypes.filter(ct => ct.call_type !== null) - }, - ["call-types"], - { revalidate: 300 }, // 5 minutes -) - -// Cached function to get unique model names -// Revalidates every 5 minutes - models change infrequently -const getModels = unstable_cache( - async () => { - const models = await prisma.llm_calls.findMany({ - select: { model_name: true }, - distinct: ["model_name"], - }) - return models.filter(m => m.model_name !== null) - }, - ["model-names"], - { revalidate: 300 }, // 5 minutes -) - -export default async function LLMCallsPage({ searchParams }: { searchParams: SearchParams }) { - try { - const page = parseInt(searchParams.page || "1") - const pageSize = 50 - const skip = (page - 1) * pageSize - - // Build where clause based on filters - type WhereClause = { - call_type?: string - model_name?: { contains: string } - status?: string - trace_id?: { startsWith: string } | { in: string[] } | { contains: string } - OR?: Array<{ trace_id: { startsWith: string } }> - } - - const where: WhereClause = {} - - if (searchParams.call_type) { - where.call_type = searchParams.call_type - } - if (searchParams.model) { - where.model_name = { contains: searchParams.model } - } - if (searchParams.status) { - where.status = searchParams.status - } - if (searchParams.trace_id) { - // Use startsWith for prefix matching to find multi-model related calls - where.trace_id = { startsWith: searchParams.trace_id } - } - - // Get unique organizations for filter dropdown (cached) - const uniqueOrganizations = await getUniqueOrganizations() - - // If organization filter is specified, get matching trace_ids - let filteredTraceIds: string[] = [] - if (searchParams.organization) { - const orgFeatures = await prisma.optimization_features.findMany({ - where: { organization: searchParams.organization }, - select: { trace_id: true }, - distinct: ["trace_id"], - }) - filteredTraceIds = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] - - // If organization filter is applied but no traces found, return empty result early - if (filteredTraceIds.length === 0) { - // Get unique call types and models for filters (cached) - const [callTypes, models] = await Promise.all([ - getCallTypes(), - getModels(), - ]) - - // Return early with empty results - return ( -
-
- {/* Title and Search Bar on Same Line */} -
-

- LLM Calls -

- {/* Compact Search Bar */} -
- - - {searchParams.trace_id && ( - - Clear - - )} -
-
-

- Track and analyze all LLM API calls for prompt engineering -

-
- - {/* Filters */} -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
-
- -
-

- No LLM calls found for organization "{searchParams.organization}". -

-
-
- ) - } - } - - // Apply organization filter using IN clause for exact trace ID matches - // NOTE: Filtering happens at DB level BEFORE pagination, not client-side. - // We use IN clause because there's no Prisma relation between llm_calls and optimization_features - // (they're only related by trace_id as a string field, not a foreign key relation) - if (filteredTraceIds.length > 0 && !searchParams.trace_id) { - // Use IN clause for exact trace ID matches - much more efficient than OR with startsWith - // For very large organizations (>10k traces), consider chunking the array - where.trace_id = { in: filteredTraceIds } - } - - // Fetch LLM calls with pagination, aggregate stats - const [llmCalls, totalCount, aggregateStats, successCount] = await Promise.all([ - prisma.llm_calls.findMany({ - where, - orderBy: { created_at: "desc" }, - take: pageSize, - skip, - select: { - id: true, - trace_id: true, - call_type: true, - model_name: true, - status: true, - parsing_status: true, - candidates_generated: true, - candidates_valid: true, - prompt_tokens: true, - completion_tokens: true, - llm_cost: true, - latency_ms: true, - created_at: true, - error_message: true, - context: true, - }, - }), - prisma.llm_calls.count({ where }), - // Get aggregate stats for all filtered data (not just current page) - prisma.llm_calls.aggregate({ - where, - _sum: { - llm_cost: true, - latency_ms: true, - }, - _avg: { - latency_ms: true, - }, - _count: { - status: true, - }, - }), - // Get success count for success rate calculation - prisma.llm_calls.count({ - where: { - ...where, - status: "success", - }, - }), - ]) - - // Fetch optimization_features and optimization_events for the trace_ids we got - const traceIds = llmCalls.map(call => call.trace_id).filter(Boolean) as string[] - const uniqueTraceIdPrefixes = Array.from( - new Set(traceIds.map(id => id.substring(0, 36))), // Get base UUID (first 36 chars) - ) - - // NOTE: Using Promise.all for parallel fetching, not N+1 queries. - // Both queries execute simultaneously for the same set of trace_ids. - const [optimizationFeatures, optimizationEvents] = await Promise.all([ - uniqueTraceIdPrefixes.length > 0 - ? prisma.optimization_features.findMany({ - where: { trace_id: { in: uniqueTraceIdPrefixes } }, - select: { - trace_id: true, - organization: true, - ranking: true, - }, - }) - : [], - uniqueTraceIdPrefixes.length > 0 - ? prisma.optimization_events.findMany({ - where: { trace_id: { in: uniqueTraceIdPrefixes } }, - select: { - trace_id: true, - event_type: true, - }, - distinct: ["trace_id"], - }) - : [], - ]) - - // Create maps for trace_id to event_type and organization - const traceIdToEventType = new Map() - optimizationEvents.forEach(event => { - if (event.trace_id) { - traceIdToEventType.set(event.trace_id, event.event_type) - } - }) - - const traceIdToOrganization = new Map() - optimizationFeatures.forEach(feature => { - if (feature.trace_id && feature.organization) { - traceIdToOrganization.set(feature.trace_id, feature.organization) - } - }) - - // Trace IDs that have a chosen best candidate (ranking.ranking[0] present) - const traceIdsWithBest = new Set( - optimizationFeatures - .filter( - f => - f.trace_id && - (f.ranking as { ranking?: string[] } | null)?.ranking?.[0], - ) - .map(f => f.trace_id.substring(0, 36)), - ) - - // Get unique call types and models for filters - const [callTypes, models] = await Promise.all([ - prisma.llm_calls.findMany({ - select: { call_type: true }, - distinct: ["call_type"], - }), - prisma.llm_calls.findMany({ - select: { model_name: true }, - distinct: ["model_name"], - }), - ]) - - const totalPages = Math.ceil(totalCount / pageSize) - - return ( -
-
- {/* Title, Search Bar, and Help Button */} -
-
-

- LLM Calls -

- -
-

What is an LLM call?

-

Each individual API request to an AI model (like GPT-4, Claude, etc.) for generating code optimizations, validations, or other tasks.

-
-
-

Call sequence (#)

-

Shows the order of calls within a trace. Multiple calls may be part of the same optimization request.

-
-
-

Call types

-
    -
  • optimization: Generates optimized code
  • -
  • validation: Checks quality of generated code
  • -
  • line_profiler: Analyzes performance bottlenecks
  • -
-
-
-

Parsing status

-

Indicates whether the model's response was successfully parsed and extracted into usable code candidates.

-
-
- } - /> -
- {/* Compact Search Bar */} -
- - - {searchParams.trace_id && ( - - Clear - - )} -
-
-

- Track and analyze all LLM API calls for prompt engineering -

-
- - {/* Filters */} -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - {(searchParams.call_type || searchParams.model || searchParams.status || searchParams.organization) && ( - - Clear All - - )} -
-
-
- - {/* Stats Summary */} -
- - 0 ? Math.round((successCount / totalCount) * 100) : 0}%`} - helpText="Percentage of successful calls vs total calls. Excludes in-progress calls." - icon="CheckCircle2" - variant="success" - /> - - -
- - {/* LLM Calls Table */} -
-
- - - - - - - - - - - - - - - - - - - {llmCalls.map(call => { - const ctx = call.context as { call_sequence?: number } | null - const callSequence = ctx?.call_sequence - const traceIdPrefix = call.trace_id?.substring(0, 36) || "" - const eventType = traceIdPrefix ? traceIdToEventType.get(traceIdPrefix) || null : null - const organization = traceIdPrefix ? traceIdToOrganization.get(traceIdPrefix) : null - const source = getCallSource(eventType, call.context as Record | null) - const isBestOptimizationCall = - call.call_type === "optimization" && traceIdsWithBest.has(traceIdPrefix) - return ( - - - - - - - - - - - - - - - ) - })} - -
- {callSequence ? `#${callSequence}` : "-"} - - - {new Date(call.created_at).toLocaleString()} - - - {call.trace_id && call.trace_id.trim() ? ( - - {call.trace_id.substring(0, 8)}... - - ) : ( - N/A - )} - - {organization || "N/A"} - - - - {call.call_type} - - {isBestOptimizationCall && ( - - - Best - - )} - - - - {source.toLowerCase().includes("github") ? ( - - ) : source.toLowerCase().includes("vscode") || source.toLowerCase().includes("cli") ? ( - - ) : null} - {source} - - - {call.model_name} - - - {call.status} - - - {call.prompt_tokens && call.completion_tokens - ? `${call.prompt_tokens + call.completion_tokens}` - : "-"} - - {call.llm_cost ? `$${call.llm_cost.toFixed(4)}` : "-"} - - {call.latency_ms ? `${call.latency_ms}ms` : "-"} - - {call.candidates_valid}/{call.candidates_generated || 0} -
-
- - {llmCalls.length === 0 && ( -
- -

- No LLM Calls Found -

- {searchParams.call_type || searchParams.model || searchParams.status || searchParams.organization || searchParams.trace_id ? ( -
-

- Try adjusting your filters above -

- - Clear All Filters - -
- ) : ( -
-

- Run the test script to generate sample data -

- - python django/aiservice/test_observability_local.py - -
- )} -
- )} -
- - {/* Pagination */} - {totalPages > 1 && ( -
- {page > 1 && ( - - Previous - - )} - - Page {page} of {totalPages} - - {page < totalPages && ( - - Next - - )} -
- )} -
- ) - } catch (error) { - throw error - } -} diff --git a/js/cf-webapp/src/app/observability/loading.tsx b/js/cf-webapp/src/app/observability/loading.tsx new file mode 100644 index 000000000..3c86324ef --- /dev/null +++ b/js/cf-webapp/src/app/observability/loading.tsx @@ -0,0 +1,39 @@ +export default function ObservabilityLoading() { + return ( +
+ {/* Search Section Skeleton */} +
+
+
+
+
+
+
+
+
+
+ + {/* Content Skeleton */} +
+ {/* Progress skeleton */} +
+
+
+
+ + {/* Timeline items skeleton */} +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+ ))} +
+
+
+ ) +} diff --git a/js/cf-webapp/src/app/observability/page.tsx b/js/cf-webapp/src/app/observability/page.tsx new file mode 100644 index 000000000..e15e6c3ae --- /dev/null +++ b/js/cf-webapp/src/app/observability/page.tsx @@ -0,0 +1,328 @@ +import { Suspense } from "react" +import { unstable_cache } from "next/cache" +import { Search } from "lucide-react" +import { prisma } from "@/lib/prisma" +import { TraceSearch } from "@/components/observability/trace-search" +import { TimelinePageView } from "@/components/observability/timeline-page-view" +import { transformToTimelineSections } from "@/components/observability/timeline-types" +import { ErrorsSection } from "@/components/observability/errors-section" +import { FunctionToOptimizeSection } from "@/components/observability/function-to-optimize-section" +import { CodeContextSection } from "@/components/observability/code-context-section" + +export const revalidate = 60 + +interface Observability2PageProps { + searchParams: Promise<{ + trace_id?: string + }> +} + +const getTraceData = unstable_cache( + async (tracePrefix: string) => { + const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([ + prisma.llm_calls.findMany({ + where: { trace_id: { startsWith: tracePrefix } }, + orderBy: { created_at: "asc" }, + }), + prisma.optimization_errors.findMany({ + where: { trace_id: { startsWith: tracePrefix } }, + orderBy: { created_at: "asc" }, + }), + prisma.optimization_features.findFirst({ + where: { trace_id: { startsWith: tracePrefix } }, + }), + prisma.optimization_events.findFirst({ + where: { trace_id: { startsWith: tracePrefix } }, + select: { event_type: true, function_name: true, file_path: true }, + }), + ]) + return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } + }, + ["observability-trace-detail"], + { revalidate: 60 }, +) + +export default async function Observability2Page({ searchParams }: Observability2PageProps) { + const params = await searchParams + const traceId = params.trace_id?.trim() + + let traceData: Awaited> | null = null + if (traceId) { + const tracePrefix = traceId.substring(0, 33) + traceData = await getTraceData(tracePrefix) + } + + const hasResults = traceData + ? traceData.rawLlmCalls.length > 0 || traceData.errors.length > 0 + : false + + return ( +
+
+
+ }> + + +
+
+ + {traceId && traceData ? ( + }> + + + ) : traceId ? ( + + ) : ( + + )} +
+ ) +} + +interface TraceData { + rawLlmCalls: Awaited>["rawLlmCalls"] + errors: Awaited>["errors"] + optimizationFeatures: Awaited>["optimizationFeatures"] + optimizationEvent: Awaited>["optimizationEvent"] +} + +function TraceContent({ traceId, traceData }: { traceId: string; traceData: TraceData }) { + const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = traceData + + if (rawLlmCalls.length === 0 && errors.length === 0) { + return + } + + const optimizationsOrigin = + (optimizationFeatures?.optimizations_origin as Record< + string, + { source: string; model?: string; call_sequence?: number; parent?: string } + >) || {} + + const candidateExplanations = + (optimizationFeatures?.explanations_post as Record) || {} + + const allCandidates = optimizationFeatures?.optimizations_post + ? Object.entries(optimizationFeatures.optimizations_post as Record).map( + ([id, code]) => ({ + id, + code: typeof code === "string" ? code : "", + source: optimizationsOrigin[id]?.source || "OPTIMIZE", + model: optimizationsOrigin[id]?.model, + callSequence: optimizationsOrigin[id]?.call_sequence, + explanation: candidateExplanations[id], + }), + ) + : [] + + const optimizationCandidates = allCandidates + .filter(c => c.source === "OPTIMIZE") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ ...c, index: index + 1 })) + + const lineProfilerCandidates = allCandidates + .filter(c => c.source === "OPTIMIZE_LP") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ ...c, index: index + 1 })) + + const refinementCandidates = allCandidates + .filter(c => c.source === "REFINE") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ + ...c, + index: index + 1, + parentId: optimizationsOrigin[c.id]?.parent || null, + })) + + const rankingData = optimizationFeatures?.ranking as + | { ranking?: string[]; explanation?: string } + | null + const bestCandidateId = rankingData?.ranking?.[0] ?? null + + const pullRequestRaw = optimizationFeatures?.pull_request + const usedForPr = Boolean( + pullRequestRaw != null && + typeof pullRequestRaw === "object" && + !Array.isArray(pullRequestRaw) && + Object.keys(pullRequestRaw as Record).length > 0, + ) + + const candidateRankMap: Record = {} + if (rankingData?.ranking) { + rankingData.ranking.forEach((id, index) => { + candidateRankMap[id] = index + 1 + }) + } + + const generatedTests = (optimizationFeatures?.generated_test ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + + const instrumentedTests = (optimizationFeatures?.instrumented_generated_test ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + + const instrumentedPerfTests = ((optimizationFeatures as Record)?.instrumented_perf_test as string[] ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + + const llmCalls = rawLlmCalls.sort((a, b) => { + const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity + const seqB = (b.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity + if (seqA !== seqB) return seqA - seqB + return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + }) + + const transformedCalls = llmCalls.map(call => ({ + id: call.id, + call_type: call.call_type, + model_name: call.model_name, + status: call.status, + latency_ms: call.latency_ms, + llm_cost: call.llm_cost, + total_tokens: call.total_tokens, + created_at: call.created_at, + context: call.context as { call_sequence?: number } | null, + })) + + const { sections, totalDuration } = transformToTimelineSections({ + calls: transformedCalls, + optimizationCandidates, + lineProfilerCandidates, + refinementCandidates, + generatedTests, + instrumentedTests, + instrumentedPerfTests, + originalCode: optimizationFeatures?.original_code ?? null, + testFramework: optimizationFeatures?.test_framework ?? null, + candidateRankMap, + bestCandidateId, + rankingExplanation: rankingData?.explanation ?? null, + usedForPr, + }) + + const transformedErrors = errors.map(error => ({ + id: error.id, + error_type: error.error_type, + severity: error.severity, + error_message: error.error_message, + context: error.context as { + test_name?: string + failure_reason?: string + test_output?: string + expected?: string + actual?: string + } | null, + created_at: error.created_at, + })) + + const functionName = (optimizationFeatures?.metadata as Record)?.function_to_optimize as string ?? optimizationEvent?.function_name ?? null + const filePath = optimizationEvent?.file_path ?? null + const originalCode = optimizationFeatures?.original_code ?? null + const dependencyCode = optimizationFeatures?.dependency_code ?? null + + return ( +
+
+ + +
+ + + + {transformedErrors.length > 0 && ( +
+ +
+ )} +
+ ) +} + +function EmptyState() { + return ( +
+
+ +
+

+ Enter a Trace ID to Get Started +

+

+ Paste or type a trace ID in the search box above to view the complete optimization timeline, + including all LLM calls, generated candidates, and any errors. +

+
+ ) +} + +function NotFoundState({ traceId }: { traceId: string }) { + return ( +
+
+ +
+

Trace Not Found

+

+ No data was found for the trace ID: +

+ + {traceId} + +

+ Please check that the trace ID is correct and try again. +

+
+ ) +} + +function SearchSkeleton() { + return ( +
+
+
+
+
+
+ ) +} + +function TraceContentSkeleton() { + return ( +
+
+
+
+
+ +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+ ))} +
+
+ ) +} diff --git a/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx b/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx deleted file mode 100644 index 3f9ec4448..000000000 --- a/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx +++ /dev/null @@ -1,767 +0,0 @@ -import Link from "next/link" -import { notFound } from "next/navigation" -import { unstable_cache } from "next/cache" -import { - CheckCircle, - Timer, - DollarSign, - Hash, - Code as CodeIcon, - Github, - Terminal, - AlertCircle, - XCircle, -} from "lucide-react" -import { prisma } from "@/lib/prisma" -import { getCallSource } from "@/lib/observability-utils" -import { HelpButton } from "@/components/observability/help-button" -import { InfoIcon } from "@/components/observability/info-icon" -import { CopyButton } from "@/components/observability/copy-button" - -export const revalidate = 60 - -interface TracePageProps { - params: { - trace_id: string - } -} - -const getTraceData = unstable_cache( - async (tracePrefix: string) => { - const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([ - prisma.llm_calls.findMany({ - where: { trace_id: { startsWith: tracePrefix } }, - orderBy: { created_at: "asc" }, - }), - prisma.optimization_errors.findMany({ - where: { trace_id: { startsWith: tracePrefix } }, - orderBy: { created_at: "asc" }, - }), - prisma.optimization_features.findFirst({ - where: { trace_id: { startsWith: tracePrefix } }, - }), - prisma.optimization_events.findFirst({ - where: { trace_id: { startsWith: tracePrefix } }, - select: { event_type: true }, - }), - ]) - return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } - }, - ["trace-detail"], - { revalidate: 60 }, -) - -export default async function TracePage({ params }: TracePageProps) { - const { trace_id } = params - - // Use prefix matching (first 33 chars) to group multi-model calls that share the same base trace_id - const tracePrefix = trace_id.substring(0, 33) - const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = await getTraceData(tracePrefix) - - const traceSource = getCallSource(optimizationEvent?.event_type || null, null) - - // Extract optimization candidates from optimization_features - // Also get the origin of each candidate (OPTIMIZE vs OPTIMIZE_LP) and model info - const optimizationsOrigin = - (optimizationFeatures?.optimizations_origin as Record< - string, - { source: string; model?: string } - >) || {} - - const allCandidates = optimizationFeatures?.optimizations_post - ? Object.entries(optimizationFeatures.optimizations_post as Record).map( - ([id, code]) => ({ - id, - code: typeof code === "string" ? code : "", - source: optimizationsOrigin[id]?.source || "OPTIMIZE", - model: optimizationsOrigin[id]?.model, - }), - ) - : [] - - // Filter candidates by source for display under the correct section - const optimizationCandidates = allCandidates - .filter(c => c.source === "OPTIMIZE") - .map((c, index) => ({ ...c, index: index + 1 })) - - const lineProfilerCandidates = allCandidates - .filter(c => c.source === "OPTIMIZE_LP") - .map((c, index) => ({ ...c, index: index + 1 })) - - // Get explanations for candidates if available - const candidateExplanations = - (optimizationFeatures?.explanations_post as Record) || {} - - // Best candidate (first in ranking) and whether it was used for PR - const rankingData = optimizationFeatures?.ranking as - | { ranking?: string[]; explanation?: string } - | null - const bestCandidateId = rankingData?.ranking?.[0] ?? null - const pullRequestRaw = optimizationFeatures?.pull_request - const usedForPr = Boolean( - pullRequestRaw != null && - typeof pullRequestRaw === "object" && - !Array.isArray(pullRequestRaw) && - Object.keys(pullRequestRaw as Record).length > 0, - ) - - // Map candidate ID to rank position (1-based, 1 = best) - const candidateRankMap = new Map() - if (rankingData?.ranking) { - rankingData.ranking.forEach((id, index) => { - candidateRankMap.set(id, index + 1) - }) - } - - // Sort by call_sequence from context if available, otherwise by created_at - const llmCalls = rawLlmCalls.sort((a, b) => { - const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity - const seqB = (b.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity - if (seqA !== seqB) return seqA - seqB - return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() - }) - - // If no data found, show 404 - if (llmCalls.length === 0 && errors.length === 0) { - notFound() - } - - // Calculate summary metrics - const totalCost = llmCalls.reduce((sum, call) => sum + (call.llm_cost ?? 0), 0) - const totalTokens = llmCalls.reduce((sum, call) => sum + (call.total_tokens ?? 0), 0) - const failedCalls = llmCalls.filter(c => c.status === "failed").length - - // Calculate timeline data using Math.min/Math.max to handle out-of-order timestamps - const timestamps = llmCalls.map(call => new Date(call.created_at).getTime()) - const minTimestamp = timestamps.length > 0 ? Math.min(...timestamps) : 0 - const maxTimestamp = timestamps.length > 0 ? Math.max(...timestamps) : 0 - const totalDuration = maxTimestamp > minTimestamp ? (maxTimestamp - minTimestamp) / 1000 : 0 - - // Status determination - check for partial_success, failed, or success - const hasPartial = llmCalls.some(c => c.status === "partial_success") - const status = failedCalls > 0 ? "Failed" : hasPartial ? "Partial" : "Completed" - const statusColor = - failedCalls > 0 - ? "text-red-600 dark:text-red-400" - : hasPartial - ? "text-yellow-600 dark:text-yellow-400" - : "text-green-600 dark:text-green-400" - - // Group calls by call_type - const groupedCalls = llmCalls.reduce( - (acc, call) => { - const type = call.call_type || "unknown" - if (!acc[type]) { - acc[type] = [] - } - acc[type].push(call) - return acc - }, - {} as Record, - ) - - // Get call types in order of first appearance - const orderedTypes = [...new Set(llmCalls.map(c => c.call_type || "unknown"))] - - // Create a map of call_type to LLM call for candidate linking - const callTypeToLlmCall = new Map() - llmCalls.forEach(call => { - if (call.call_type && !callTypeToLlmCall.has(call.call_type)) { - callTypeToLlmCall.set(call.call_type, call) - } - }) - - return ( -
- {/* Header */} -
-
- - Traces - - / - - {trace_id && trace_id.trim() ? `${trace_id.substring(0, 8)}...` : "N/A"} - -
-
-
-

- Trace Details -

-
-

- {trace_id && trace_id.trim() ? trace_id : "Invalid Trace ID"} -

- {trace_id && trace_id.trim() && } -
-
- -
-

- Summary Metrics -

-

- View key metrics including status, source, duration, cost, tokens, and number - of generated candidates for this optimization request. -

-
-
-

- LLM Calls Timeline -

-

- All LLM API calls grouped by type. Expand each call to see detailed metrics - including tokens, latency, and timestamp. For optimization and line_profiler - types, candidates are displayed directly. -

-
-
-

- Generated Candidates -

-

- Code optimization candidates generated during this trace. Each candidate includes - an explanation and the generated code. Use the copy button to copy candidate code. -

-
-
-

- Error Handling -

-

- If any errors occurred during optimization, they are displayed with severity - indicators, error messages, and detailed context for test failures. -

-
-
- } - /> -
-
- - {/* Summary Card */} -
-
-
-
- {status === "Completed" ? ( - - ) : status === "Failed" ? ( - - ) : ( - - )} - Status - -
-
{status}
-
-
-
- {traceSource.toLowerCase().includes("github") ? ( - - ) : ( - - )} - Source - -
-
- - {traceSource} - -
-
-
-
- - Duration - -
-
- {(totalDuration / 1000).toFixed(2)}s -
-
-
-
- - Cost - -
-
- ${totalCost.toFixed(4)} -
-
-
-
- - Tokens - -
-
- {totalTokens.toLocaleString()} -
-
-
-
- - Candidates - -
-
- {optimizationCandidates.length} -
-
-
-
- - {/* Unified LLM Calls View */} -
-
-
-
-

LLM Calls

- - {llmCalls.length} - -
- -
-

- Call Sequence -

-

- Calls are ordered by sequence number showing the execution order. Each call - displays its status, duration, cost, and model used. -

-
-
-

- Optimization & Line Profiler -

-

- For these call types, generated candidates are displayed directly. Each - candidate includes the code and an explanation of the optimization. -

-
-
-

- Expanding Calls -

-

- Click any call to expand and see detailed metrics including token usage, - latency, and timestamp. Use "View full details" to see prompts and responses. -

-
-
- } - size="sm" - /> -
-
-
- {orderedTypes.map(callType => { - const calls = groupedCalls[callType] - const isOptimizationType = callType === "optimization" - const isLineProfilerType = callType === "line_profiler" - - // Get the candidates for this call type - const candidatesForType = isOptimizationType - ? optimizationCandidates - : isLineProfilerType - ? lineProfilerCandidates - : [] - - // For optimization/line_profiler types, only show candidates (not individual LLM calls) - const showIndividualCalls = !isOptimizationType && !isLineProfilerType - - // Get the LLM call for this call type to link candidates - const llmCallForType = callTypeToLlmCall.get(callType) - - return ( -
-
- {callType} - {candidatesForType.length > 0 && ( - - {candidatesForType.length} candidates - - )} -
- {showIndividualCalls && ( -
- {calls.map((call, typeIndex) => { - const durationSec = ((call.latency_ms || 0) / 1000).toFixed(2) - const statusIcon = call.status === "success" ? "✓" : "✗" - const callStatusColor = - call.status === "success" - ? "text-green-600 dark:text-green-400" - : "text-red-600 dark:text-red-400" - const ctx = call.context as { call_sequence?: number } | null - const callSequence = ctx?.call_sequence - - return ( -
- -
- - {callSequence ? `#${callSequence}` : "-"} - - {statusIcon} - - {calls.length > 1 ? `${callType} ${typeIndex + 1}` : callType} - - - {durationSec}s · ${(call.llm_cost || 0).toFixed(4)} ·{" "} - {call.model_name} - -
- - ▼ - -
- -
- {/* Call details */} -
-
- Tokens - - {call.total_tokens?.toLocaleString() ?? "N/A"} - -
-
- Latency - - {call.latency_ms ? `${call.latency_ms}ms` : "N/A"} - -
-
- Time - - {new Date(call.created_at).toLocaleTimeString()} - -
-
- Details - - View full details → - -
-
-
-
- ) - })} -
- )} - {/* Show candidates for optimization/line_profiler types */} - {candidatesForType.length > 0 && ( -
-
-

- Generated Candidates -

- -
-
- {candidatesForType.map(candidate => { - const isBest = - bestCandidateId != null && candidate.id === bestCandidateId - const showUsedForPr = isBest && usedForPr - const rank = candidateRankMap.get(candidate.id) - return ( -
- -
- - Candidate {candidate.index} - - - {candidate.id.substring(0, 8)}... - - {rank != null && ( - - Rank #{rank} - - )} - {isBest && ( - - Best - - )} - {showUsedForPr && ( - - Used for PR - - )} - {candidate.model && ( - - {candidate.model} - - )} -
- -
-
- {candidateExplanations[candidate.id] && ( -
-
- -
- Explanation -
-
-

- {candidateExplanations[candidate.id]} -

-
- )} -
-
-
- Code -
- -
-
-                                {candidate.code}
-                              
-
- {llmCallForType && ( -
- - View LLM Call Details → - -
- )} -
-
- )})} -
-
- )} -
- ) - })} -
-
- - {/* Ranking explanation — shown when ranker ran */} - {rankingData?.explanation && ( -
-
-
-

- Ranking explanation -

- -
-
-
-

- {rankingData.explanation} -

-
-
- )} - - {/* Errors */} -
-
-
- {errors.length > 0 ? ( - <> - -

- Errors -

- - {errors.length} - - - ) : ( - <> - -

- No Errors Detected -

- - )} -
-
- {errors.length > 0 ? ( -
- {errors.map(error => { - const isTestFailure = error.error_type === "test_failure" - const errorContext = error.context as - | { - test_name?: string - failure_reason?: string - test_output?: string - expected?: string - actual?: string - } - | null - - return ( -
-
- {error.severity === "error" ? ( - - ) : ( - - )} -
-
- - {error.error_type} - - - {error.severity} - - - {new Date(error.created_at).toLocaleString()} - -
-
-

- {error.error_message} -

- -
- {/* Test Failure Details */} - {isTestFailure && errorContext && ( -
-

- Test Failure Details -

- {errorContext.test_name && ( -
- - Test: - {" "} - - {errorContext.test_name} - -
- )} - {errorContext.failure_reason && ( -
- - Reason: - -

- {errorContext.failure_reason} -

-
- )} - {errorContext.expected && ( -
- - Expected: - -
-                                {String(errorContext.expected)}
-                              
-
- )} - {errorContext.actual && ( -
- - Actual: - -
-                                {String(errorContext.actual)}
-                              
-
- )} - {errorContext.test_output && ( -
- - Test Output: - -
-                                {String(errorContext.test_output)}
-                              
-
- )} -
- )} -
-
-
- ) - })} -
- ) : ( -
- -

- All Clear! -

-

- This trace completed successfully with no errors detected. -

-
- )} -
-
- ) -} diff --git a/js/cf-webapp/src/app/observability/traces/page.tsx b/js/cf-webapp/src/app/observability/traces/page.tsx deleted file mode 100644 index 2a9a514b9..000000000 --- a/js/cf-webapp/src/app/observability/traces/page.tsx +++ /dev/null @@ -1,654 +0,0 @@ -import Link from "next/link" -import { unstable_cache } from "next/cache" -import { Search as SearchIcon } from "lucide-react" -import { prisma } from "@/lib/prisma" -import { safeCostTokens } from "@/lib/observability-utils" -import type { Prisma } from "@prisma/client" -import { HelpButton } from "@/components/observability/help-button" -import { StatCard } from "@/components/observability/stat-card" -import { ColumnHeader } from "@/components/observability/column-header" -import { InfoIcon } from "@/components/observability/info-icon" - -// Revalidate every 30 seconds -export const revalidate = 30 - -interface SearchParams { - trace_id?: string - page?: string - organization?: string -} - -// Cached function to get unique organizations list -// Revalidates every 5 minutes - organizations change infrequently -const getUniqueOrganizations = unstable_cache( - async () => { - const uniqueOrganizations = await prisma.optimization_features.findMany({ - select: { organization: true }, - distinct: ["organization"], - where: { organization: { not: null } }, - }) - return uniqueOrganizations.map(f => f.organization).filter(Boolean).sort() as string[] - }, - ["unique-organizations"], - { revalidate: 300 }, // 5 minutes -) - -// Optimized function to count distinct trace_ids using groupBy -const getTotalTracesCount = unstable_cache( - async (traceIdFilter: string | undefined, organizationFilter: string | undefined) => { - // Get trace IDs filtered by organization if specified - let traceIdPrefixes: string[] = [] - if (organizationFilter) { - const orgFeatures = await prisma.optimization_features.findMany({ - where: { organization: organizationFilter }, - select: { trace_id: true }, - distinct: ["trace_id"], - }) - traceIdPrefixes = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] - if (traceIdPrefixes.length === 0) return 0 - } - - // Build where clause - const where: Prisma.llm_callsWhereInput = {} - - if (traceIdFilter) { - where.trace_id = { contains: traceIdFilter } - } else if (traceIdPrefixes.length > 0) { - // Use IN clause for exact trace ID matches - much more efficient than OR with startsWith - where.trace_id = { in: traceIdPrefixes } - } - - // Use groupBy for efficient distinct count - // Filter out null trace_ids in the result - const result = await prisma.llm_calls.groupBy({ - by: ["trace_id"], - where, - }) - - // Filter out null trace_ids - return result.filter(r => r.trace_id !== null).length - }, - ["traces-count"], - { revalidate: 30 }, -) - -export default async function TracesPage({ searchParams }: { searchParams: SearchParams }) { - try { - const page = parseInt(searchParams.page || "1") - const pageSize = 50 - const skip = (page - 1) * pageSize - - // Get trace IDs filtered by organization if specified - let filteredTraceIds: string[] = [] - if (searchParams.organization) { - const orgFeatures = await prisma.optimization_features.findMany({ - where: { organization: searchParams.organization }, - select: { trace_id: true }, - distinct: ["trace_id"], - }) - filteredTraceIds = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] - - // If organization filter is applied but no traces found, return empty result early - if (filteredTraceIds.length === 0) { - const uniqueOrganizations = await prisma.optimization_features.findMany({ - select: { organization: true }, - distinct: ["organization"], - where: { organization: { not: null } }, - }) - const orgs = uniqueOrganizations.map(f => f.organization).filter(Boolean).sort() as string[] - - // Return early with empty results - return ( -
- {/* Header with search form */} -
-
-
-

- All Traces -

- -
-

What is a trace?

-

A trace represents a complete optimization request from start to finish. Each trace contains all the LLM API calls made during that optimization.

-
-
-

Multi-model traces

-

When using multiple models for optimization, all calls share the same base trace_id (first 33 characters). This helps track related operations together.

-
-
-

Page sections

-
    -
  • Summary Stats: Quick overview of trace metrics on this page
  • -
  • Traces Table: Detailed list of all traces with aggregated data
  • -
  • Filters: Search by trace ID or filter by organization
  • -
-
-
- } - /> -
-
-
- -
-
- - -
- - - - Clear - -
-
-

- View optimization request traces with aggregated metrics -

-
-
-
- -
-

- No Traces Found -

-

- No traces found for organization "{searchParams.organization}". Try selecting a different organization. -

-
- - View All LLM Calls → - -
-
-
- ) - } - } - - // Build where clause for LLM calls with organization filter applied at database level - // NOTE: Filtering happens at DB level BEFORE pagination, not client-side. - // We use IN clause because there's no Prisma relation between llm_calls and optimization_features - // (they're only related by trace_id as a string field, not a foreign key relation) - const where: Prisma.llm_callsWhereInput = {} - - if (searchParams.trace_id) { - where.trace_id = { contains: searchParams.trace_id } - } else if (filteredTraceIds.length > 0) { - // Use IN clause for exact trace ID matches - much more efficient than OR with startsWith - // For very large organizations (>10k traces), consider chunking the array - where.trace_id = { in: filteredTraceIds } - } - - // STEP 1: Get distinct trace_ids with pagination using groupBy - const [distinctTraces, totalTracesCount] = await Promise.all([ - prisma.llm_calls.groupBy({ - by: ["trace_id"], - where, - orderBy: { _max: { created_at: "desc" } }, - take: pageSize, - skip, - _max: { created_at: true }, - }), - getTotalTracesCount(searchParams.trace_id, searchParams.organization), - ]) - - // Extract trace_ids from the paginated results - const paginatedTraceIds = distinctTraces - .map(t => t.trace_id) - .filter(Boolean) as string[] - - // STEP 2: Fetch all LLM calls ONLY for the paginated trace_ids - const llmCallsRaw = paginatedTraceIds.length > 0 - ? await prisma.llm_calls.findMany({ - where: { trace_id: { in: paginatedTraceIds } }, - orderBy: { created_at: "desc" }, - select: { - trace_id: true, - created_at: true, - llm_cost: true, - total_tokens: true, - status: true, - call_type: true, - }, - }) - : [] - - // Filter out null trace_ids - const llmCalls = llmCallsRaw.filter(call => call.trace_id !== null) - - // Fetch organizations ONLY for the paginated trace_ids - const [allOptimizationFeatures] = await Promise.all([ - paginatedTraceIds.length > 0 - ? prisma.optimization_features.findMany({ - where: { trace_id: { in: paginatedTraceIds } }, - select: { - trace_id: true, - organization: true, - }, - }) - : [], - ]) - - // Create a map of trace_id to organization - const traceIdToOrganization = new Map() - allOptimizationFeatures.forEach(feature => { - if (feature.trace_id && feature.organization) { - traceIdToOrganization.set(feature.trace_id, feature.organization) - } - }) - - // Get unique organizations for filter dropdown (cached) - const orgs = await getUniqueOrganizations() - - // Group by trace_id and calculate aggregates - const traceMap = new Map< - string, - { - trace_id: string - first_seen: Date - last_seen: Date - call_count: number - total_cost: number - total_tokens: number - failed_calls: number - status: string - call_types: Set - } - >() - - llmCalls.forEach(call => { - if (!call.trace_id) return - - const callTimestamp = new Date(call.created_at).getTime() - const existing = traceMap.get(call.trace_id) - const { cost: callCost, tokens: callTokens } = safeCostTokens(call.llm_cost, call.total_tokens) - - if (existing) { - existing.call_count++ - existing.total_cost += callCost - existing.total_tokens += callTokens - if (call.status === "failed") existing.failed_calls++ - if (call.call_type) existing.call_types.add(call.call_type) - // Use Math.min/Math.max to ensure correct first/last timestamps - existing.first_seen = new Date(Math.min(existing.first_seen.getTime(), callTimestamp)) - existing.last_seen = new Date(Math.max(existing.last_seen.getTime(), callTimestamp)) - } else { - traceMap.set(call.trace_id, { - trace_id: call.trace_id, - first_seen: new Date(callTimestamp), - last_seen: new Date(callTimestamp), - call_count: 1, - total_cost: callCost, - total_tokens: callTokens, - failed_calls: call.status === "failed" ? 1 : 0, - status: call.status || "unknown", - call_types: new Set(call.call_type ? [call.call_type] : []), - }) - } - }) - - // Convert to array and sort by last_seen desc - // No need to paginate here - already done at database level - const traces = Array.from(traceMap.values()).sort( - (a, b) => b.last_seen.getTime() - a.last_seen.getTime(), - ) - const totalPages = Math.ceil(totalTracesCount / pageSize) - - return ( -
- {/* Header */} -
- {/* Title, Search Bar, and Help Button */} -
-
-

- All Traces -

- -
-

What is a trace?

-

A trace represents a complete optimization request from start to finish. Each trace contains all the LLM API calls made during that optimization.

-
-
-

Multi-model traces

-

When using multiple models for optimization, all calls share the same base trace_id (first 33 characters). This helps track related operations together.

-
-
-

Page sections

-
    -
  • Summary Stats: Quick overview of trace metrics on this page
  • -
  • Traces Table: Detailed list of all traces with aggregated data
  • -
  • Filters: Search by trace ID or filter by organization
  • -
-
-
- } - /> -
- {/* Compact Search Bar */} -
-
- -
-
- - -
- - - {(searchParams.trace_id || searchParams.organization) && ( - - Clear - - )} -
-
-

- View optimization request traces with aggregated metrics -

-
- - {/* Summary Stats */} -
- - - sum + t.total_cost, 0).toFixed(4)}`} - helpText="Total cost in USD for all LLM calls shown on this page. Based on model pricing and token usage." - icon="DollarSign" - /> - t.failed_calls > 0).length} - helpText="Traces containing at least one failed LLM call. Click a trace to see error details." - icon="AlertTriangle" - variant="error" - /> -
- - {/* Traces Table */} -
-
- - - - - - - - - - - - - - - - {traces.length === 0 ? ( - - - - ) : ( - traces.map(trace => { - // Ensure duration is never negative by using Math.max - const duration = Math.max( - 0, - (trace.last_seen.getTime() - trace.first_seen.getTime()) / 1000, - ) - // Determine status: check if any call has partial_success, failed, or all success - const hasPartial = - trace.status === "partial_success" || - llmCalls.some(c => c.trace_id === trace.trace_id && c.status === "partial_success") - const statusColor = - trace.failed_calls > 0 - ? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" - : hasPartial - ? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" - : "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" - const statusText = trace.failed_calls > 0 ? "Failed" : hasPartial ? "Partial" : "Success" - const statusBorderColor = trace.failed_calls > 0 ? "border-red-500" : hasPartial ? "border-yellow-500" : "border-green-500" - - return ( - - - - - - - - - - - - ) - }) - )} - -
-
- -
-

- No Traces Found -

-

- {searchParams.trace_id || searchParams.organization - ? "Try adjusting your filters above" - : "Run an optimization to see traces here"} -

-
- {(searchParams.trace_id || searchParams.organization) && ( - - View All LLM Calls → - - )} -
-
- {trace.trace_id && trace.trace_id.trim() ? ( - - {trace.trace_id.substring(0, 8)}... - - ) : ( - N/A - )} - - {traceIdToOrganization.get(trace.trace_id) || "N/A"} - - - {statusText} - - - {trace.call_count} - {trace.failed_calls > 0 && ( - - ({trace.failed_calls} failed) - - )} - -
- {Array.from(trace.call_types) - .slice(0, 3) - .map(type => ( - - {type} - - ))} - {trace.call_types.size > 3 && ( - - +{trace.call_types.size - 3} - - )} -
-
- ${trace.total_cost.toFixed(4)} - - {trace.total_tokens.toLocaleString()} - - {duration.toFixed(2)}s - - {trace.last_seen.toLocaleString()} -
-
-
- - {/* Pagination */} - {totalPages > 1 && ( -
- {page > 1 && ( - - Previous - - )} - - Page {page} of {totalPages} • Showing {traces.length} traces - - {page < totalPages && ( - - Next - - )} -
- )} -
- ) - } catch (error) { - throw error - } -} diff --git a/js/cf-webapp/src/app/trace/[trace_id]/page.tsx b/js/cf-webapp/src/app/trace/[trace_id]/page.tsx deleted file mode 100644 index 45fb7f31c..000000000 --- a/js/cf-webapp/src/app/trace/[trace_id]/page.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { PrismaClient } from "@prisma/client" -import { notFound } from "next/navigation" -import Link from "next/link" -import { ExperimentMetadata } from "@/lib/types" // Your defined types -import MonacoDiffViewer from "@/components/trace/monaco-diff-viewer" // The client component -import { Metadata } from "next" // For Next.js metadata API -import { getSession } from "@auth0/nextjs-auth0" -import { isTeamMember } from "@/app/utils/auth" - -interface TraceDetailsPageProps { - params: { - trace_id: string - } -} -const prisma = new PrismaClient() -// Function to generate dynamic metadata (e.g., page title) -export async function generateMetadata({ params }: TraceDetailsPageProps): Promise { - const { trace_id } = params - - // Optionally fetch minimal data for title generation to avoid over-fetching - // For simplicity, we'll use a generic title or one derived if data is fetched quickly - // A more optimized approach might involve a separate lightweight query or using default values. - - const optimizationFeature = await prisma.optimization_features.findUnique({ - where: { trace_id }, - select: { - experiment_metadata: true, - organization: true, - repository: true, - review_quality: true, - review_explanation: true, - }, - }) - - let title = `Python Diff Trace: ${trace_id.substring(0, 8)}` - if (optimizationFeature?.experiment_metadata) { - const metadata = optimizationFeature.experiment_metadata as unknown as ExperimentMetadata // Type assertion - const repoName = - optimizationFeature.organization && optimizationFeature.repository - ? `${optimizationFeature.organization}/${optimizationFeature.repository}` - : metadata.owner && metadata.repo - ? `${metadata.owner}/${metadata.repo}` - : "" - - if (metadata.prCommentFields?.function_name) { - title = `Diff: ${metadata.prCommentFields.function_name} (${repoName})` - } else if (repoName) { - title = `Diff: ${repoName} - Trace ${trace_id.substring(0, 8)}` - } - } - - return { - title: `${title} | Codeflash AI`, - description: `Review CodeFlash Python code optimization diffs for trace ID ${trace_id}.`, - // You can add more OpenGraph tags, etc. - } -} - -// The main page component -export default async function TraceDetailsPage({ params }: TraceDetailsPageProps) { - const { trace_id } = params - - if (!trace_id) { - // This case should ideally be handled by Next.js routing if trace_id is missing in URL structure - notFound() - } - - const session = await getSession() - if (!session?.user) return null - - // Check team member access - only team members can view traces - const hasTeamAccess = await isTeamMember() - if (!hasTeamAccess) { - // Create a custom access denied page or redirect to a generic error - return ( -
-
-
- - - -
-

Access Denied

-

- This trace is restricted to CodeFlash team members only. -

-
-

- Logged in as:{" "} - {session.user.email || session.user.nickname} -

-

- Trace ID: {trace_id} -

-
- - Go to Dashboard - -
-
- ) - } - - let optimizationFeature: { - experiment_metadata: unknown - metadata: unknown - organization: string | null - repository: string | null - review_quality: string | null - review_explanation: string | null - } | null = null - try { - optimizationFeature = await prisma.optimization_features.findUnique({ - where: { trace_id: trace_id }, - select: { - experiment_metadata: true, // Prisma handles JSONB parsing - metadata: true, // Include metadata field which stores modified code - organization: true, - repository: true, - review_quality: true, - review_explanation: true, - // Select other fields if needed by MonacoDiffViewer for its header/display - }, - }) - } catch (error) { - console.error(`[TracePage] Failed to fetch data for trace_id ${trace_id}:`, error) - // Optionally, render a specific error UI component here instead of notFound() - // For now, notFound() will trigger the 404 page, which is reasonable if data fetch fails badly. - // Or you could pass an error state to MonacoDiffViewer to display. - // For this detailed guide, we assume MonacoDiffViewer will handle 'null' metadata. - } - - // If feature is not found, or metadata is explicitly null (and you expect it for valid traces) - if (!optimizationFeature) { - notFound() // Triggers the Next.js 404 page - } - - // Type assertion is safe here due to the check above or if your DB guarantees metadata for valid traces. - // If experiment_metadata can be legitimately null for an existing trace_id, handle it gracefully. - // Pass experiment metadata directly since modifications are now stored in diffContents - const metadata = optimizationFeature.experiment_metadata as ExperimentMetadata | null - const review_quality = optimizationFeature.review_quality as string | null - const review_explanation = optimizationFeature.review_explanation as string | null - // Determine repository full name for display - const repoFullName = - optimizationFeature.organization && optimizationFeature.repository - ? `${optimizationFeature.organization}/${optimizationFeature.repository}` - : metadata?.owner && metadata?.repo - ? `${metadata.owner}/${metadata.repo}` - : "N/A" - - return ( - - ) -} diff --git a/js/cf-webapp/src/components/conditional-layout.tsx b/js/cf-webapp/src/components/conditional-layout.tsx index 4118265db..30bf6e76c 100644 --- a/js/cf-webapp/src/components/conditional-layout.tsx +++ b/js/cf-webapp/src/components/conditional-layout.tsx @@ -23,7 +23,6 @@ export function ConditionalLayout({ const shouldHideLayout = pathname !== null && ( HIDDEN_PAGES.includes(pathname) || - pathname.startsWith("/trace/") || pathname.startsWith("/observability") || !user ) diff --git a/js/cf-webapp/src/components/dashboard/sidebar.tsx b/js/cf-webapp/src/components/dashboard/sidebar.tsx index 741f3d342..0ea7a45e9 100644 --- a/js/cf-webapp/src/components/dashboard/sidebar.tsx +++ b/js/cf-webapp/src/components/dashboard/sidebar.tsx @@ -301,7 +301,7 @@ export function Sidebar({ className, user, isLoading, error }: SidebarProps): JS {/* Observability Group */} >(new Set()) + const [expandedFiles, setExpandedFiles] = useState>(new Set()) + + const { rwFiles, roFiles, metrics } = useMemo(() => { + const rwFiles = originalCode ? parseMarkdownCodeBlocks(originalCode) : [] + const roFiles = dependencyCode ? parseMarkdownCodeBlocks(dependencyCode) : [] + const rwTokens = rwFiles.reduce((sum, f) => sum + f.tokens, 0) + const roTokens = roFiles.reduce((sum, f) => sum + f.tokens, 0) + const totalFiles = rwFiles.length + roFiles.length + const totalTokens = rwTokens + roTokens + const rwChars = rwFiles.reduce((sum, f) => sum + f.code.length, 0) + const roChars = roFiles.reduce((sum, f) => sum + f.code.length, 0) + + return { + rwFiles, + roFiles, + metrics: { rwTokens, roTokens, totalFiles, totalTokens, rwChars, roChars }, + } + }, [originalCode, dependencyCode]) + + const toggleSection = useCallback((section: string): void => { + setExpandedSections(prev => { + const next = new Set(prev) + if (next.has(section)) { + next.delete(section) + } else { + next.add(section) + } + return next + }) + }, []) + + const toggleFile = useCallback((fileKey: string): void => { + setExpandedFiles(prev => { + const next = new Set(prev) + if (next.has(fileKey)) { + next.delete(fileKey) + } else { + next.add(fileKey) + } + return next + }) + }, []) + + if (!originalCode && !dependencyCode) { + return null + } + + return ( +
+
setIsExpanded(!isExpanded)} + onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }} + className="w-full p-6 flex items-center justify-between hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors rounded-sm cursor-pointer" + > +
+ +

Code Context

+ +
+
+
+ + + {metrics.totalTokens.toLocaleString()} tokens + + + + {metrics.totalFiles} files + +
+ +
+
+ + {isExpanded && ( +
+ {(functionName || filePath) && ( +
+ {functionName && ( +
+ Function + + {functionName} + +
+ )} + {filePath && ( +
+ File + + {filePath} + +
+ )} +
+ )} + + {rwFiles.length > 0 && roFiles.length > 0 && ( +
+
+ + + Token Distribution (estimated) + +
+ +
+
+
+ + Read-Writable: {metrics.rwTokens.toLocaleString()} ({rwFiles.length} files) + +
+
+
+ + Read-Only: {metrics.roTokens.toLocaleString()} ({roFiles.length} files) + +
+
+
+ )} + + {rwFiles.length > 0 && ( + toggleSection("rw")} + expandedFiles={expandedFiles} + onToggleFile={toggleFile} + sectionKey="rw" + /> + )} + + {roFiles.length > 0 && ( + toggleSection("ro")} + expandedFiles={expandedFiles} + onToggleFile={toggleFile} + sectionKey="ro" + /> + )} +
+ )} +
+ ) +}) + +interface CodeGroupSectionProps { + title: string + subtitle: string + accentColor: "emerald" | "slate" + tokenCount: number + charCount: number + files: ParsedFile[] + isExpanded: boolean + onToggle: () => void + expandedFiles: Set + onToggleFile: (fileKey: string) => void + sectionKey: string +} + +function getAccentColorClasses(accentColor: "emerald" | "slate"): { border: string; bg: string; icon: string } { + switch (accentColor) { + case "emerald": + return { + border: "border-zinc-200 dark:border-zinc-800", + bg: "bg-zinc-50 dark:bg-zinc-900", + icon: "text-emerald-500", + } + case "slate": + return { + border: "border-zinc-200 dark:border-zinc-800", + bg: "bg-zinc-50 dark:bg-zinc-900", + icon: "text-zinc-500", + } + } +} + +const CodeGroupSection = memo(function CodeGroupSection({ + title, + subtitle, + accentColor, + tokenCount, + charCount, + files, + isExpanded, + onToggle, + expandedFiles, + onToggleFile, + sectionKey, +}: CodeGroupSectionProps) { + const { border: borderColor, bg: bgColor, icon: iconColor } = getAccentColorClasses(accentColor) + + return ( +
+
{ if (e.key === "Enter" || e.key === " ") onToggle() }} + className={`w-full p-4 flex items-center justify-between hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer ${bgColor}`} + > +
+ +
+ {title} + {subtitle} +
+
+
+ + {tokenCount.toLocaleString()} tokens · {charCount.toLocaleString()} chars · {files.length} files + + +
+
+ + {isExpanded && ( +
+ {files.map((file, index) => { + const fileKey = `${sectionKey}-${index}` + const isFileExpanded = expandedFiles.has(fileKey) + return ( +
+
+
onToggleFile(fileKey)} + onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggleFile(fileKey) }} + className="flex items-center gap-2 cursor-pointer hover:opacity-80 flex-1" + > + + + {file.filename} + + + {file.path !== file.filename && `(${file.path})`} + + +
+
+ + {file.code.split("\n").length} lines + + +
+
+ + {isFileExpanded && ( +
+ +
+ )} +
+ ) + })} +
+ )} +
+ ) +}) + +interface TokenDistributionBarProps { + rwTokens: number + roTokens: number + totalTokens: number +} + +const TokenDistributionBar = memo(function TokenDistributionBar({ + rwTokens, + roTokens, + totalTokens, +}: TokenDistributionBarProps) { + const rwPercent = Math.round((rwTokens / totalTokens) * 100) + const roPercent = Math.round((roTokens / totalTokens) * 100) + + return ( +
+
+ {rwPercent > 15 && `${rwPercent}%`} +
+
+ {roPercent > 15 && `${roPercent}%`} +
+
+ ) +}) diff --git a/js/cf-webapp/src/components/observability/code-highlighter.tsx b/js/cf-webapp/src/components/observability/code-highlighter.tsx new file mode 100644 index 000000000..57871822c --- /dev/null +++ b/js/cf-webapp/src/components/observability/code-highlighter.tsx @@ -0,0 +1,172 @@ +"use client" + +import dynamic from "next/dynamic" +import { memo } from "react" + +const SyntaxHighlighter = dynamic( + () => import("react-syntax-highlighter").then(m => m.Prism), + { + ssr: false, + loading: () => ( +
+
+
+
+
+ ), + } +) + +export const zincDarkTheme = { + 'code[class*="language-"]': { + color: 'rgb(250, 250, 250)', + background: 'none', + fontFamily: 'var(--font-mono)', + fontSize: '1em', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + wordWrap: 'normal', + lineHeight: '1.5', + tabSize: 4, + hyphens: 'none', + }, + 'pre[class*="language-"]': { + color: 'rgb(250, 250, 250)', + background: 'rgb(24, 24, 27)', + fontFamily: 'var(--font-mono)', + fontSize: '1em', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + wordWrap: 'normal', + lineHeight: '1.5', + tabSize: 4, + hyphens: 'none', + padding: '1em', + margin: '0', + overflow: 'auto', + }, + comment: { + color: 'rgb(113, 113, 122)', + fontStyle: 'italic', + }, + prolog: { color: 'rgb(113, 113, 122)' }, + doctype: { color: 'rgb(113, 113, 122)' }, + cdata: { color: 'rgb(113, 113, 122)' }, + keyword: { color: 'rgb(96, 165, 250)' }, + 'control-flow': { color: 'rgb(96, 165, 250)' }, + string: { color: 'rgb(134, 239, 172)' }, + 'attr-value': { color: 'rgb(134, 239, 172)' }, + function: { color: 'rgb(253, 224, 71)' }, + 'class-name': { color: 'rgb(253, 224, 71)' }, + number: { color: 'rgb(251, 146, 60)' }, + boolean: { color: 'rgb(251, 146, 60)' }, + operator: { color: 'rgb(161, 161, 170)' }, + punctuation: { color: 'rgb(161, 161, 170)' }, + variable: { color: 'rgb(250, 250, 250)' }, + property: { color: 'rgb(250, 250, 250)' }, + tag: { color: 'rgb(96, 165, 250)' }, + 'attr-name': { color: 'rgb(250, 250, 250)' }, + namespace: { opacity: 0.7 }, + selector: { color: 'rgb(253, 224, 71)' }, + important: { + color: 'rgb(251, 146, 60)', + fontWeight: 'bold', + }, + atrule: { color: 'rgb(96, 165, 250)' }, + builtin: { color: 'rgb(253, 224, 71)' }, + entity: { + color: 'rgb(250, 250, 250)', + cursor: 'help', + }, + url: { + color: 'rgb(96, 165, 250)', + textDecoration: 'underline', + }, + inserted: { + color: 'rgb(134, 239, 172)', + background: 'rgba(134, 239, 172, 0.1)', + }, + deleted: { + color: 'rgb(248, 113, 113)', + background: 'rgba(248, 113, 113, 0.1)', + }, +} as const + +export const CODE_STYLE = { + margin: 0, + padding: "1rem", + fontSize: "0.875rem", + lineHeight: 1.5, + background: 'rgb(24, 24, 27)', +} as const + +export const CODE_STYLE_RELAXED = { + margin: 0, + padding: "1rem", + fontSize: "0.875rem", + lineHeight: 1.6, + background: 'rgb(24, 24, 27)', +} as const + +export const CODE_STYLE_SMALL = { + margin: 0, + padding: "1rem", + fontSize: "0.8125rem", + lineHeight: 1.5, + background: 'rgb(24, 24, 27)', +} as const + +interface CodeHighlighterProps { + code: string + language: string + showLineNumbers?: boolean + customStyle?: React.CSSProperties + highlightLines?: number[] +} + +const highlightStyle = { + backgroundColor: 'rgba(250, 204, 21, 0.15)', + display: 'block', + marginLeft: '-1rem', + marginRight: '-1rem', + paddingLeft: '1rem', + paddingRight: '1rem', + borderLeft: '3px solid rgb(250, 204, 21)', +} + +export const CodeHighlighter = memo(function CodeHighlighter({ + code, + language, + showLineNumbers = true, + customStyle = CODE_STYLE, + highlightLines, +}: CodeHighlighterProps) { + const lineProps = highlightLines && highlightLines.length > 0 + ? (lineNumber: number) => { + const isHighlighted = highlightLines.includes(lineNumber) + return { + style: isHighlighted ? highlightStyle : { display: 'block' }, + 'data-highlighted': isHighlighted ? 'true' : undefined, + } + } + : undefined + + const shouldWrapLines = !!(highlightLines && highlightLines.length > 0) + + return ( + + {code} + + ) +}) diff --git a/js/cf-webapp/src/components/observability/column-header.tsx b/js/cf-webapp/src/components/observability/column-header.tsx deleted file mode 100644 index c09e36c76..000000000 --- a/js/cf-webapp/src/components/observability/column-header.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client" - -import { InfoIcon } from "./info-icon" -import { cn } from "@/lib/utils" - -interface ColumnHeaderProps { - label: string - tooltip: string - className?: string -} - -export function ColumnHeader({ label, tooltip, className }: ColumnHeaderProps) { - return ( - -
- {label} - -
- - ) -} diff --git a/js/cf-webapp/src/components/observability/errors-section.tsx b/js/cf-webapp/src/components/observability/errors-section.tsx new file mode 100644 index 000000000..80d7ef401 --- /dev/null +++ b/js/cf-webapp/src/components/observability/errors-section.tsx @@ -0,0 +1,201 @@ +"use client" + +import { useState, useCallback, memo } from "react" +import { + XCircle, + AlertCircle, + AlertTriangle, + ChevronDown, +} from "lucide-react" +import { CopyButton } from "./copy-button" + +interface ErrorContext { + test_name?: string + failure_reason?: string + test_output?: string + expected?: string + actual?: string +} + +interface TraceError { + id: string + error_type: string + severity: string + error_message: string + context: ErrorContext | null + created_at: Date +} + +interface ErrorsSectionProps { + errors: TraceError[] +} + +export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSectionProps) { + const [expandedErrors, setExpandedErrors] = useState>(new Set()) + + const toggleError = useCallback((errorId: string) => { + setExpandedErrors(prev => { + const next = new Set(prev) + if (next.has(errorId)) { + next.delete(errorId) + } else { + next.add(errorId) + } + return next + }) + }, []) + + if (errors.length === 0) { + return null + } + + return ( +
+
+
+ +

Errors

+ + {errors.length} + +
+
+ +
+ {errors.map(error => { + const isExpanded = expandedErrors.has(error.id) + const isTestFailure = error.error_type === "test_failure" + const hasContext = error.context && Object.keys(error.context).length > 0 + + const SeverityIcon = + error.severity === "critical" || error.severity === "error" ? XCircle : AlertTriangle + + let severityColor: string + if (error.severity === "critical") { + severityColor = "text-red-400 border border-red-600 px-1.5 py-0.5" + } else if (error.severity === "error") { + severityColor = "text-orange-400 border border-orange-600 px-1.5 py-0.5" + } else { + severityColor = "text-yellow-400 border border-yellow-600 px-1.5 py-0.5" + } + + return ( +
+
+
toggleError(error.id)} + onKeyDown={e => { if (e.key === "Enter" || e.key === " ") toggleError(error.id) }} + className="flex items-start gap-3 cursor-pointer hover:opacity-80 flex-1 transition-opacity duration-150" + > + +
+
+ + {error.error_type} + + + {error.severity} + + + {new Date(error.created_at).toLocaleString()} + +
+

+ {error.error_message} +

+
+ {(hasContext || isTestFailure) && ( + + )} +
+
+ +
+
+ + {isExpanded && isTestFailure && error.context && ( +
+
+

+ Test Failure Details +

+ + {error.context.test_name && ( +
+ + Test Name + +

+ {error.context.test_name} +

+
+ )} + + {error.context.failure_reason && ( +
+ + Failure Reason + +

+ {error.context.failure_reason} +

+
+ )} + + {error.context.expected && ( +
+ + Expected + +
+                          {String(error.context.expected)}
+                        
+
+ )} + + {error.context.actual && ( +
+ + Actual + +
+                          {String(error.context.actual)}
+                        
+
+ )} + + {error.context.test_output && ( +
+ + Test Output + +
+                          {String(error.context.test_output)}
+                        
+
+ )} +
+
+ )} + + {isExpanded && !isTestFailure && hasContext && ( +
+
+ + Context + +
+                      {JSON.stringify(error.context, null, 2)}
+                    
+
+
+ )} +
+ ) + })} +
+
+ ) +}) diff --git a/js/cf-webapp/src/components/observability/function-to-optimize-section.tsx b/js/cf-webapp/src/components/observability/function-to-optimize-section.tsx new file mode 100644 index 000000000..5ad49b0b7 --- /dev/null +++ b/js/cf-webapp/src/components/observability/function-to-optimize-section.tsx @@ -0,0 +1,204 @@ +"use client" + +import { memo, useMemo, useState, useRef, useEffect } from "react" +import { Code, FileText, ChevronDown } from "lucide-react" +import { CodeHighlighter, CODE_STYLE_RELAXED } from "./code-highlighter" +import { CopyButton } from "./copy-button" +import { findFunctionInCode, type FunctionLocation } from "./python-parser" + +interface FunctionToOptimizeSectionProps { + functionName: string | null + filePath: string | null + originalCode: string | null +} + +interface ParsedFile { + path: string + filename: string + language: string + code: string +} + +function getFilename(path: string): string { + return path.split("/").pop() || path +} + +function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] { + const files: ParsedFile[] = [] + const regex = /```(\w+):([^\n]+)\n([\s\S]*?)```/g + let match + + while ((match = regex.exec(markdown)) !== null) { + const [, language, path, code] = match + files.push({ + path, + filename: getFilename(path), + language: language || "python", + code: code.trimEnd(), + }) + } + + return files +} + +export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection({ + functionName, + filePath, + originalCode, +}: FunctionToOptimizeSectionProps) { + const [isExpanded, setIsExpanded] = useState(true) + const [functionLocation, setFunctionLocation] = useState(null) + const [actualFile, setActualFile] = useState(null) + const codeContainerRef = useRef(null) + + const allFiles = useMemo(() => { + if (!originalCode) return [] + return parseMarkdownCodeBlocks(originalCode) + }, [originalCode]) + + useEffect(() => { + if (!functionName || allFiles.length === 0) { + setFunctionLocation(null) + setActualFile(null) + return + } + + let cancelled = false + + async function findFunction() { + const searchPromises = allFiles.map(async (file) => { + const location = await findFunctionInCode(file.code, functionName!) + return location ? { file, location } : null + }) + + const results = await Promise.all(searchPromises) + if (cancelled) return + + const found = results.find(r => r !== null) + if (found) { + setFunctionLocation(found.location) + setActualFile(found.file) + return + } + + let fallbackFile = allFiles[0] + if (filePath) { + const match = allFiles.find(f => + filePath.endsWith(f.path) || f.path.endsWith(filePath) || f.path === filePath + ) + if (match) fallbackFile = match + } + setFunctionLocation(null) + setActualFile(fallbackFile) + } + + findFunction() + return () => { cancelled = true } + }, [functionName, filePath, allFiles]) + + const functionFile = actualFile ?? allFiles[0] ?? null + + const functionLines = useMemo(() => { + if (!functionLocation) return null + const lines: number[] = [] + for (let i = functionLocation.startLine; i <= functionLocation.endLine; i++) { + lines.push(i) + } + return lines + }, [functionLocation]) + + useEffect(() => { + if (!isExpanded || !functionLocation || !codeContainerRef.current) return + + const scrollToFunction = () => { + if (!codeContainerRef.current) return + const container = codeContainerRef.current + + const lineHeight = 22.4 + const paddingTop = 16 + const targetLine = functionLocation.startLine - 1 + const scrollPosition = paddingTop + (targetLine * lineHeight) - (container.clientHeight / 3) + + container.scrollTo({ + top: Math.max(0, scrollPosition), + behavior: 'smooth' + }) + } + + const timer = setTimeout(scrollToFunction, 300) + return () => clearTimeout(timer) + }, [isExpanded, functionLocation]) + + if (!functionFile) { + return null + } + + const highlightLines = functionLines && functionLines.length > 0 ? functionLines : undefined + + return ( +
+
+
setIsExpanded(!isExpanded)} + onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }} + className="flex items-center gap-3 cursor-pointer hover:opacity-80 flex-1" + > +
+ +
+
+

+ Function to Optimize +

+
+ {functionName && ( + + {functionName} + + )} + {functionLocation && ( + + lines {functionLocation.startLine}-{functionLocation.endLine} + + )} +
+
+ +
+
+ +
+
+ + {isExpanded && ( + <> +
+ + + {functionFile.filename} + + {functionFile.path !== functionFile.filename && ( + + ({functionFile.path}) + + )} + + {functionFile.code.split("\n").length} lines + +
+ +
+ +
+ + )} +
+ ) +}) diff --git a/js/cf-webapp/src/components/observability/help-button.tsx b/js/cf-webapp/src/components/observability/help-button.tsx deleted file mode 100644 index f58175748..000000000 --- a/js/cf-webapp/src/components/observability/help-button.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client" - -import { Info } from "lucide-react" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { cn } from "@/lib/utils" - -interface HelpButtonProps { - title: string - content: React.ReactNode - size?: "sm" | "md" - triggerClassName?: string -} - -export function HelpButton({ title, content, size = "sm", triggerClassName }: HelpButtonProps) { - return ( - - - - - - - {title} - -
{content}
-
-
-
-
- ) -} diff --git a/js/cf-webapp/src/components/observability/index.ts b/js/cf-webapp/src/components/observability/index.ts new file mode 100644 index 000000000..d2684cace --- /dev/null +++ b/js/cf-webapp/src/components/observability/index.ts @@ -0,0 +1,9 @@ +export { TraceSearch } from "./trace-search" +export { TraceSummary } from "./trace-summary" +export { TimelinePageView } from "./timeline-page-view" +export { transformToTimelineSections } from "./timeline-types" +export { ErrorsSection } from "./errors-section" +export { CodeHighlighter, CODE_STYLE, CODE_STYLE_RELAXED, CODE_STYLE_SMALL } from "./code-highlighter" +export { CopyButton } from "./copy-button" +export { InfoIcon } from "./info-icon" +export { getTraceSource } from "./utils" diff --git a/js/cf-webapp/src/components/observability/observability-nav.tsx b/js/cf-webapp/src/components/observability/observability-nav.tsx deleted file mode 100644 index ee99e2f48..000000000 --- a/js/cf-webapp/src/components/observability/observability-nav.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client" - -import Link from "next/link" -import { usePathname } from "next/navigation" -import { Activity, ListTree } from "lucide-react" -import { cn } from "@/lib/utils" - -const navItems = [ - { href: "/observability/traces", label: "Traces", icon: ListTree }, - { href: "/observability/llm-calls", label: "LLM Calls", icon: Activity }, -] - -export function ObservabilityNav() { - const pathname = usePathname() - - return ( - - ) -} diff --git a/js/cf-webapp/src/components/observability/parsed-response-view.tsx b/js/cf-webapp/src/components/observability/parsed-response-view.tsx deleted file mode 100644 index 2de97554f..000000000 --- a/js/cf-webapp/src/components/observability/parsed-response-view.tsx +++ /dev/null @@ -1,223 +0,0 @@ -"use client" - -import { - extractExplainTag, - extractRankTag, - getResponseContentForParsing, - splitMarkdownCodeBlocks, - type ResponseSegment, -} from "@/lib/observability-response-parse" -import { CopyButton } from "@/components/observability/copy-button" -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" -import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism" -import { useState } from "react" - -interface ParsedResponseViewProps { - rawResponse: string - callType: string | null -} - -/** Try to parse JSON and return pretty-printed version, or null if not JSON */ -function tryFormatJSON(content: string): string | null { - try { - const parsed = JSON.parse(content) - return JSON.stringify(parsed, null, 2) - } catch { - return null - } -} - -export function ParsedResponseView({ rawResponse, callType }: ParsedResponseViewProps) { - const [showRaw, setShowRaw] = useState(false) - const isRanking = callType === "ranking" - // Use inner message content when raw_response is API JSON (e.g. OpenAI) - const contentForParsing = getResponseContentForParsing(rawResponse) - const rankContent = isRanking ? extractRankTag(contentForParsing) : null - const explainContent = isRanking ? extractExplainTag(contentForParsing) : null - const hasRankingSections = isRanking && (rankContent != null || explainContent != null) - - const segments: ResponseSegment[] = hasRankingSections - ? [] - : splitMarkdownCodeBlocks(contentForParsing) - const hasSegments = segments.length > 0 - - // Check if raw response is JSON (for fallback display) - const formattedJSON = tryFormatJSON(rawResponse) - const isJSON = formattedJSON != null - - return ( -
- {/* View raw toggle button - always visible in header */} -
- - -
- - {showRaw && ( -
-
-            {rawResponse}
-          
-
- )} - - {!showRaw && ( -
- {hasRankingSections && ( -
- {rankContent != null && ( -
-
-

- Ranking (best first) -

- -
-
-
    - {rankContent - .split(/[\s,]+/) - .filter(Boolean) - .map((id, i) => { - const pos = i + 1 - const label = - pos === 1 - ? "1st" - : pos === 2 - ? "2nd" - : pos === 3 - ? "3rd" - : `${pos}th` - return ( -
  1. - - {label} - - - {id.trim()} - -
  2. - ) - })} -
-
-
- )} - {explainContent != null && ( -
-
-

- Explanation -

-
-
-
-

- {explainContent} -

- -
-
-
- )} -
- )} - - {!hasRankingSections && hasSegments && ( -
- {segments.map((seg, i) => { - const textJSON = seg.kind === "text" ? tryFormatJSON(seg.content) : null - return seg.kind === "text" ? ( -
- {textJSON ? ( - 10} - > - {textJSON} - - ) : ( -
-                        {seg.content}
-                      
- )} -
- ) : ( -
-
- - {seg.language || "code"} - - -
- 5} - > - {seg.content} - -
- ) - })} -
- )} - - {!hasRankingSections && !hasSegments && ( -
- {isJSON ? ( - 10} - > - {formattedJSON} - - ) : ( -
-                  {rawResponse}
-                
- )} -
- )} -
- )} -
- ) -} diff --git a/js/cf-webapp/src/components/observability/python-parser.ts b/js/cf-webapp/src/components/observability/python-parser.ts new file mode 100644 index 000000000..96a5f7f19 --- /dev/null +++ b/js/cf-webapp/src/components/observability/python-parser.ts @@ -0,0 +1,136 @@ +"use client" + +import type { Node, Parser as ParserType } from "web-tree-sitter" + +export interface FunctionLocation { + startLine: number + endLine: number +} + +let parserPromise: Promise | null = null + +async function getParser(): Promise { + if (typeof window === "undefined") { + return null + } + + if (!parserPromise) { + parserPromise = (async () => { + try { + const { Parser, Language } = await import("web-tree-sitter") + await Parser.init({ + locateFile: (scriptName: string) => `/${scriptName}`, + }) + const parser = new Parser() + const Python = await Language.load("/tree-sitter-python.wasm") + parser.setLanguage(Python) + return parser + } catch (error) { + console.error("Tree-sitter initialization failed:", error) + parserPromise = null + throw error + } + })() + } + return parserPromise +} + +export async function findFunctionInCode( + code: string, + functionName: string +): Promise { + try { + const parser = await getParser() + if (parser) { + const tree = parser.parse(code) + if (tree) { + const result = findFunctionNode(tree.rootNode, functionName) + if (result) { + return { + startLine: result.startPosition.row + 1, + endLine: result.endPosition.row + 1, + } + } + } + } + } catch (error) { + console.warn("Tree-sitter parse failed, trying regex fallback:", error) + } + + return findFunctionWithRegex(code, functionName) +} + +function findFunctionWithRegex( + code: string, + functionName: string +): FunctionLocation | null { + const lines = code.split("\n") + + const defPattern = new RegExp( + `^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(` + ) + + let startLine = -1 + let startIndent = -1 + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + if (startLine === -1) { + const match = line.match(defPattern) + if (match) { + startLine = i + 1 + startIndent = match[1].length + } + } else { + const trimmed = line.trim() + if (trimmed === "" || trimmed.startsWith("#")) { + continue + } + + const currentIndent = line.length - line.trimStart().length + if (currentIndent <= startIndent) { + return { startLine, endLine: i } + } + } + } + + if (startLine !== -1) { + return { startLine, endLine: lines.length } + } + + return null +} + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} + +function findFunctionNode(node: Node, functionName: string): Node | null { + if ( + node.type === "function_definition" || + node.type === "async_function_definition" + ) { + const nameNode = node.childForFieldName("name") + if (nameNode && nameNode.text === functionName) { + return node + } + } + + if (node.type === "class_definition") { + const classBody = node.childForFieldName("body") + if (classBody) { + for (const child of classBody.children) { + const result = findFunctionNode(child, functionName) + if (result) return result + } + } + } + + for (const child of node.children) { + const result = findFunctionNode(child, functionName) + if (result) return result + } + + return null +} diff --git a/js/cf-webapp/src/components/observability/stat-card.tsx b/js/cf-webapp/src/components/observability/stat-card.tsx deleted file mode 100644 index b43974d46..000000000 --- a/js/cf-webapp/src/components/observability/stat-card.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client" - -import { - Database, - Zap, - DollarSign, - AlertTriangle, - Activity, - CheckCircle2, - Clock, -} from "lucide-react" -import { InfoIcon } from "./info-icon" -import { cn } from "@/lib/utils" - -const variantStyles = { - default: "", - success: "border-l-4 border-green-500", - warning: "border-l-4 border-yellow-500", - error: "border-l-4 border-red-500", -} - -// Icon mapping - add more icons as needed -const iconMap = { - Database, - Zap, - DollarSign, - AlertTriangle, - Activity, - CheckCircle2, - Clock, -} as const - -type IconName = keyof typeof iconMap - -interface StatCardProps { - label: string - value: string | number - helpText?: string - icon?: IconName - variant?: "default" | "success" | "warning" | "error" - className?: string -} - -export function StatCard({ label, value, helpText, icon, variant = "default", className }: StatCardProps) { - const IconComponent = icon ? iconMap[icon] : null - - return ( -
-
-
-
- {IconComponent && } -
- {label} - {helpText && } -
-
-
- {value} -
-
-
-
- ) -} diff --git a/js/cf-webapp/src/components/observability/timeline-page-view.tsx b/js/cf-webapp/src/components/observability/timeline-page-view.tsx new file mode 100644 index 000000000..a814503b8 --- /dev/null +++ b/js/cf-webapp/src/components/observability/timeline-page-view.tsx @@ -0,0 +1,823 @@ +"use client" + +import { useState, useRef, useEffect, memo, useMemo } from "react" +import { + Clock, + FlaskConical, + Activity, + Box, + RefreshCw, + ChevronDown, + FileText, + Code, + GitCompare, + CheckCircle2, + XCircle, + AlertCircle, + BarChart3, +} from "lucide-react" +import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" +import type { TimelineSection, TimelineSectionContent } from "./timeline-types" + +function stripCodeHeader(code: string): string { + let lines = code.split("\n") + if (lines[0] && /^`{3}[a-z]*(:.*)?$/i.test(lines[0].trim())) { + lines = lines.slice(1) + } + if (lines.length > 0 && lines[lines.length - 1]?.trim() === "```") { + lines = lines.slice(0, -1) + } + return lines.join("\n") +} + +interface TimelinePageViewProps { + sections: TimelineSection[] + totalDuration: number + functionName?: string | null + filePath?: string | null +} + +const TYPE_CONFIG = { + test_generation: { icon: FlaskConical }, + optimization: { icon: Box }, + line_profiler: { icon: Activity }, + refinement: { icon: RefreshCw }, + ranking: { icon: BarChart3 }, + summary: { icon: CheckCircle2 }, +} + +function formatTime(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms` + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` + return `${(ms / 60000).toFixed(1)}m` +} + +function getStatusIcon(status: string) { + switch (status) { + case "success": + return + case "failed": + return + case "partial": + return + default: + return + } +} + +interface ParsedCodeBlock { + language: string + filename: string | null + path: string | null + code: string +} + +function parseCodeBlock(rawCode: string): ParsedCodeBlock { + const markdownMatch = rawCode.match(/^```(\w+)(?::([^\n]+))?\n([\s\S]*?)```\s*$/) + if (markdownMatch) { + const [, language, path, code] = markdownMatch + const filename = path ? path.split("/").pop() || null : null + return { language: language || "python", filename, path: path || null, code: code.trimEnd() } + } + return { language: "python", filename: null, path: null, code: rawCode } +} + +function parseAllCodeBlocks(markdown: string): ParsedCodeBlock[] { + const files: ParsedCodeBlock[] = [] + const regex = /```(\w+)(?::([^\n]+))?\n([\s\S]*?)```/g + let match + + while ((match = regex.exec(markdown)) !== null) { + const [, language, path, code] = match + const filename = path ? path.split("/").pop() || null : null + files.push({ + path: path || null, + filename, + language: language || "python", + code: code.trimEnd(), + }) + } + + if (files.length === 0 && markdown.trim()) { + return [parseCodeBlock(markdown)] + } + + return files +} + +function findMatchingFile( + files: ParsedCodeBlock[], + targetPath: string | null +): ParsedCodeBlock | null { + if (!targetPath || files.length === 0) return files[0] || null + + const exactMatch = files.find(f => f.path === targetPath) + if (exactMatch) return exactMatch + + const targetFilename = targetPath.split("/").pop() + const filenameMatch = files.find(f => f.filename === targetFilename) + if (filenameMatch) return filenameMatch + + const partialMatch = files.find(f => + f.path && (targetPath.endsWith(f.path) || f.path.endsWith(targetPath)) + ) + if (partialMatch) return partialMatch + + return files[0] || null +} + +const DiffView = memo(function DiffView({ diff }: { diff: string }) { + const lines = diff.split("\n") + + return ( +
+ {lines.map((line, index) => { + const isAddition = line.startsWith("+") + const isDeletion = line.startsWith("-") + const isHunkHeader = line.startsWith("@@") + const isNoNewline = line.startsWith("\\ No newline") || line.startsWith("\\") + + if (index === lines.length - 1 && line === "") return null + if ((line === "+" || line === "-") || (isAddition && line.substring(1).trim() === "") || (isDeletion && line.substring(1).trim() === "")) { + return null + } + if (isNoNewline) return null + + let bgClass = "" + let textClass = "text-zinc-300" + let lineContent = line + let indicator: React.ReactNode = null + let borderClass = "border-transparent" + + if (isHunkHeader) { + bgClass = "bg-blue-900/30" + textClass = "text-blue-400" + } else if (isAddition) { + bgClass = "bg-green-900/40" + textClass = "text-green-300" + lineContent = line.substring(1) + indicator = + + borderClass = "border-green-500" + } else if (isDeletion) { + bgClass = "bg-red-900/40" + textClass = "text-red-300" + lineContent = line.substring(1) + indicator = + borderClass = "border-red-500" + } else if (line.startsWith(" ")) { + lineContent = line.substring(1) + } + + return ( +
+
+ {indicator} +
+
+              {lineContent || " "}
+            
+
+ ) + })} +
+ ) +}) + +const TestContent = memo(function TestContent({ content }: { content: Extract }) { + const [showDetails, setShowDetails] = useState(false) + const [expandedTest, setExpandedTest] = useState(null) + const [activeVariant, setActiveVariant] = useState<"generated" | "instrumented" | "instrumentedPerf">("generated") + + const testCount = content.testGroups.length + const hasInstrumented = content.testGroups.some(g => g.instrumented) + const hasInstrumentedPerf = content.testGroups.some(g => g.instrumentedPerf) + + return ( +
+
+
+
+ + + {testCount} test{testCount !== 1 ? "s" : ""} generated + +
+
+ {content.testFramework && ( + + {content.testFramework} + + )} + {hasInstrumented && ( + + +behavior + + )} + {hasInstrumentedPerf && ( + + +perf + + )} +
+
+ +
+ + {showDetails && ( +
+ {content.testGroups.map((group) => { + const isExpanded = expandedTest === group.index + const hasMultipleVariants = [group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length > 1 + const currentCode = activeVariant === "generated" ? group.generated + : activeVariant === "instrumented" ? group.instrumented + : group.instrumentedPerf + + return ( +
+ + + {isExpanded && ( +
+ {hasMultipleVariants && ( +
+ {group.generated && ( + + )} + {group.instrumented && ( + + )} + {group.instrumentedPerf && ( + + )} +
+ )} + +
+ {currentCode ? ( + + ) : ( +
+ No {activeVariant === "generated" ? "generated" : activeVariant === "instrumented" ? "instrumented behavior" : "instrumented perf"} test available +
+ )} +
+
+ )} +
+ ) + })} +
+ )} +
+ ) +}) + +const CandidateContent = memo(function CandidateContent({ + content, + isActive, +}: { + content: Extract + isActive: boolean +}) { + const [viewMode, setViewMode] = useState<"code" | "diff">("diff") + const [selectedFileIndex, setSelectedFileIndex] = useState(0) + const [unifiedDiff, setUnifiedDiff] = useState(null) + const [diffLoading, setDiffLoading] = useState(false) + + const originalCode = content.type === "refinement" ? content.parentCode : content.originalCode + + const candidateFiles = useMemo(() => parseAllCodeBlocks(content.code), [content.code]) + const originalFiles = useMemo(() => originalCode ? parseAllCodeBlocks(originalCode) : [], [originalCode]) + + const selectedCandidateFile = candidateFiles[selectedFileIndex] || candidateFiles[0] + + const matchingOriginalFile = useMemo(() => { + if (!selectedCandidateFile || originalFiles.length === 0) return null + return findMatchingFile(originalFiles, selectedCandidateFile.path) + }, [selectedCandidateFile, originalFiles]) + + useEffect(() => { + setUnifiedDiff(null) + }, [selectedFileIndex]) + + useEffect(() => { + if (viewMode !== "diff" || !matchingOriginalFile || !selectedCandidateFile || unifiedDiff !== null) { + return + } + + setDiffLoading(true) + import("diff").then(({ createTwoFilesPatch }) => { + const filename = selectedCandidateFile.filename || matchingOriginalFile.filename || "code.py" + const diff = createTwoFilesPatch( + `a/${filename}`, + `b/${filename}`, + matchingOriginalFile.code, + selectedCandidateFile.code, + "", + "", + { context: 3 } + ) + + const lines = diff.split("\n") + const hunkStartIndex = lines.findIndex(line => line.startsWith("@@")) + setUnifiedDiff(hunkStartIndex > 0 ? lines.slice(hunkStartIndex).join("\n") : diff) + setDiffLoading(false) + }).catch(error => { + console.error("Failed to load diff library:", error) + setDiffLoading(false) + }) + }, [viewMode, matchingOriginalFile, selectedCandidateFile, unifiedDiff]) + + const hasDiff = matchingOriginalFile !== null + const hasMultipleFiles = candidateFiles.length > 1 + + const codeContainerStyle = useMemo( + () => ({ maxHeight: isActive ? "70vh" : "200px" }), + [isActive] + ) + + return ( +
+
+ {content.rank != null && ( + + #{content.rank} + + )} + {content.isBest && ( + + Best + + )} +
+ + {content.explanation && ( +

+ {content.explanation} +

+ )} + +
+ {hasDiff && ( +
+ + +
+ )} + + {hasMultipleFiles && ( + + )} +
+ + {viewMode === "code" ? ( + selectedCandidateFile ? ( +
+
+
+ + + {selectedCandidateFile.filename || "Code"} + + {selectedCandidateFile.path && selectedCandidateFile.path !== selectedCandidateFile.filename && ( + + ({selectedCandidateFile.path}) + + )} +
+ + {selectedCandidateFile.code.split("\n").length} lines + +
+
+ +
+
+ ) : ( +
+ No code available +
+ ) + ) : diffLoading ? ( +
+
+
+
+
+
+
+ ) : unifiedDiff ? ( +
+ +
+ ) : ( +
+ No original code available for comparison +
+ )} +
+ ) +}) + +const RankingContent = memo(function RankingContent({ content }: { content: Extract }) { + return ( +
+ {content.explanation && ( +
+

+ {content.explanation} +

+
+ )} + + {content.rankings.length >= 1 && ( +
+ {content.rankings.map((item) => ( +
+
+ + {item.label} + + + Rank #{item.rank} + + {item.isBest && ( + + Best + + )} + {item.isBest && content.usedForPr && ( + + Used for PR + + )} +
+
+ +
+
+ ))} +
+ )} + +
+ ) +}) + +const SummaryContent = memo(function SummaryContent({ content }: { content: Extract }) { + const { metrics } = content + return ( +
+
+
Total Duration
+
+ {formatTime(metrics.totalDuration)} +
+
+
+
Total Cost
+
+ ${metrics.totalCost.toFixed(4)} +
+
+
+
Total Tokens
+
+ {metrics.totalTokens.toLocaleString()} +
+
+
+
Candidates
+
+ {metrics.candidatesCount} +
+
+
+ ) +}) + +const TimelineSectionCard = memo(function TimelineSectionCard({ + section, + isActive, + index, + totalSections, +}: { + section: TimelineSection + isActive: boolean + index: number + totalSections: number +}) { + const config = TYPE_CONFIG[section.type] + const Icon = config.icon + + return ( +
+
+ +
+
+ + +{formatTime(section.timestamp)} + + {section.duration && ( + <> + · + + {formatTime(section.duration)} + + + )} +
+ + {index + 1}/{totalSections} + +
+ +
+
+
+ +
+

+ {section.title} +

+ {section.subtitle && ( +

+ {section.subtitle} +

+ )} +
+
+ {getStatusIcon(section.status)} + {section.model && ( + + {section.model} + + )} + {section.cost != null && ( + + ${section.cost.toFixed(4)} + + )} +
+
+
+ +
+ {section.content.type === "tests" && } + {(section.content.type === "candidate" || section.content.type === "refinement") && ( + + )} + {section.content.type === "ranking" && } + {section.content.type === "summary" && } +
+
+
+
+ ) +}) + +export const TimelinePageView = memo(function TimelinePageView({ + sections, + totalDuration, + functionName, + filePath, +}: TimelinePageViewProps) { + const [activeIndex, setActiveIndex] = useState(0) + const sectionRefs = useRef<(HTMLDivElement | null)[]>([]) + const rafId = useRef(null) + + useEffect(() => { + const handleScroll = () => { + if (rafId.current !== null) return + rafId.current = requestAnimationFrame(() => { + rafId.current = null + const scrollTarget = window.innerHeight * 0.35 + + let closestIndex = 0 + let closestDistance = Infinity + + sectionRefs.current.forEach((ref, index) => { + if (ref) { + const rect = ref.getBoundingClientRect() + const sectionMiddle = rect.top + rect.height / 2 + const distance = Math.abs(sectionMiddle - scrollTarget) + + if (distance < closestDistance) { + closestDistance = distance + closestIndex = index + } + } + }) + + setActiveIndex(closestIndex) + }) + } + + window.addEventListener("scroll", handleScroll, { passive: true }) + handleScroll() + return () => { + window.removeEventListener("scroll", handleScroll) + if (rafId.current !== null) { + cancelAnimationFrame(rafId.current) + } + } + }, [sections.length]) + + if (sections.length === 0) { + return ( +
+ No timeline data available +
+ ) + } + + return ( +
+
+
+
+
+

+ Optimization Timeline +

+ {functionName && ( +

+ {functionName} + {filePath && in {filePath}} +

+ )} +
+
+ + {activeIndex + 1} of {sections.length} · {formatTime(totalDuration)} + +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ + {sections.map((section, index) => ( +
{ sectionRefs.current[index] = el }} + className="scroll-mt-24" + > + +
+ ))} + +
+
+
+
+ + End + +
+
+
+
+ ) +}) diff --git a/js/cf-webapp/src/components/observability/timeline-types.ts b/js/cf-webapp/src/components/observability/timeline-types.ts new file mode 100644 index 000000000..f3497af2d --- /dev/null +++ b/js/cf-webapp/src/components/observability/timeline-types.ts @@ -0,0 +1,289 @@ +export interface TimelineSection { + id: string + type: "test_generation" | "optimization" | "line_profiler" | "refinement" | "ranking" | "summary" + title: string + subtitle?: string + timestamp: number + duration?: number + status: "success" | "failed" | "partial" | "pending" + model?: string | null + cost?: number | null + tokens?: number | null + content: TimelineSectionContent +} + +export interface TestGroup { + index: number + generated?: { code: string; lines: number } + instrumented?: { code: string; lines: number } + instrumentedPerf?: { code: string; lines: number } +} + +export type TimelineSectionContent = + | { type: "tests"; testGroups: TestGroup[]; testFramework?: string } + | { type: "candidate"; code: string; originalCode: string | null; explanation?: string; rank?: number; isBest?: boolean } + | { type: "refinement"; code: string; parentCode: string | null; explanation?: string; rank?: number; isBest?: boolean } + | { type: "ranking"; explanation: string; rankings: Array<{ id: string; rank: number; label: string; code: string; isBest: boolean }>; usedForPr: boolean } + | { type: "summary"; metrics: { totalCost: number; totalTokens: number; totalDuration: number; candidatesCount: number } } + +export interface TransformInput { + calls: Array<{ + id: string + call_type: string | null + model_name: string | null + status: string + latency_ms: number | null + llm_cost: number | null + total_tokens: number | null + created_at: Date + context: { call_sequence?: number } | null + }> + optimizationCandidates: Array<{ + id: string + code: string + explanation?: string + index: number + }> + lineProfilerCandidates: Array<{ + id: string + code: string + explanation?: string + index: number + }> + refinementCandidates: Array<{ + id: string + code: string + explanation?: string + parentId: string | null + index: number + }> + generatedTests: Array<{ code: string; index: number }> + instrumentedTests: Array<{ code: string; index: number }> + instrumentedPerfTests: Array<{ code: string; index: number }> + originalCode: string | null + testFramework: string | null + candidateRankMap: Record + bestCandidateId: string | null + rankingExplanation: string | null + usedForPr: boolean +} + +export function transformToTimelineSections(input: TransformInput): { sections: TimelineSection[]; totalDuration: number } { + const { calls, optimizationCandidates, lineProfilerCandidates, refinementCandidates, generatedTests, instrumentedTests, instrumentedPerfTests, originalCode, testFramework, candidateRankMap, bestCandidateId, rankingExplanation, usedForPr } = input + + if (calls.length === 0) { + return { sections: [], totalDuration: 0 } + } + + const timestamps = calls.map(c => new Date(c.created_at).getTime()) + const minTime = Math.min(...timestamps) + const maxTime = Math.max(...timestamps) + const maxLatency = Math.max(...calls.map(c => c.latency_ms ?? 0)) + const totalDuration = maxTime - minTime + maxLatency + + const sections: TimelineSection[] = [] + + const maxTestIndex = Math.max( + generatedTests.length, + instrumentedTests.length, + instrumentedPerfTests.length + ) + + const testGroups: TestGroup[] = [] + for (let i = 1; i <= maxTestIndex; i++) { + const generated = generatedTests.find(t => t.index === i) + const instrumented = instrumentedTests.find(t => t.index === i) + const instrumentedPerf = instrumentedPerfTests.find(t => t.index === i) + + if (generated || instrumented || instrumentedPerf) { + testGroups.push({ + index: i, + generated: generated ? { code: generated.code, lines: generated.code.split("\n").length } : undefined, + instrumented: instrumented ? { code: instrumented.code, lines: instrumented.code.split("\n").length } : undefined, + instrumentedPerf: instrumentedPerf ? { code: instrumentedPerf.code, lines: instrumentedPerf.code.split("\n").length } : undefined, + }) + } + } + + const testCalls = calls.filter(c => c.call_type === "test_generation") + if (testCalls.length > 0 || testGroups.length > 0) { + const firstTestCall = testCalls[0] + const firstTimestamp = firstTestCall ? new Date(firstTestCall.created_at).getTime() - minTime : 0 + const totalTestDuration = testCalls.reduce((sum, c) => sum + (c.latency_ms ?? 0), 0) + const totalTestCost = testCalls.reduce((sum, c) => sum + (c.llm_cost ?? 0), 0) + const totalTestTokens = testCalls.reduce((sum, c) => sum + (c.total_tokens ?? 0), 0) + const allSuccess = testCalls.length === 0 || testCalls.every(c => c.status === "success") + const anyFailed = testCalls.some(c => c.status === "failed") + + const subtitle = testFramework + ? `${testGroups.length} test${testGroups.length > 1 ? "s" : ""} using ${testFramework}` + : `${testGroups.length} test${testGroups.length > 1 ? "s" : ""} generated` + + sections.push({ + id: firstTestCall ? `tests-${firstTestCall.id}` : "tests", + type: "test_generation", + title: "Test Generation", + subtitle, + timestamp: firstTimestamp, + duration: totalTestDuration, + status: allSuccess ? "success" : anyFailed ? "failed" : "partial", + model: firstTestCall?.model_name ?? null, + cost: totalTestCost, + tokens: totalTestTokens, + content: { + type: "tests", + testGroups, + testFramework: testFramework ?? undefined, + }, + }) + } + + const callIndexByType = new Map() + for (const call of calls) { + const timestamp = new Date(call.created_at).getTime() - minTime + const callType = call.call_type || "unknown" + const typeIndex = callIndexByType.get(callType) ?? 0 + callIndexByType.set(callType, typeIndex + 1) + + if (callType === "optimization") { + const optIndex = typeIndex + const candidate = optimizationCandidates[optIndex] + if (candidate) { + const rank = candidateRankMap[candidate.id] + sections.push({ + id: call.id, + type: "optimization", + title: `Optimization Candidate ${candidate.index}`, + timestamp, + duration: call.latency_ms ?? undefined, + status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", + model: call.model_name, + cost: call.llm_cost, + tokens: call.total_tokens, + content: { + type: "candidate", + code: candidate.code, + originalCode, + explanation: candidate.explanation, + rank, + isBest: candidate.id === bestCandidateId, + }, + }) + } + } else if (callType === "line_profiler") { + const lpIndex = typeIndex + const candidate = lineProfilerCandidates[lpIndex] + if (candidate) { + const rank = candidateRankMap[candidate.id] + sections.push({ + id: call.id, + type: "line_profiler", + title: `Line Profiler Candidate ${candidate.index}`, + subtitle: "Guided by profiling data", + timestamp, + duration: call.latency_ms ?? undefined, + status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", + model: call.model_name, + cost: call.llm_cost, + tokens: call.total_tokens, + content: { + type: "candidate", + code: candidate.code, + originalCode, + explanation: candidate.explanation, + rank, + isBest: candidate.id === bestCandidateId, + }, + }) + } + } else if (callType === "refinement") { + const refIndex = typeIndex + const candidate = refinementCandidates[refIndex] + if (candidate) { + const rank = candidateRankMap[candidate.id] + const parentCandidate = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates].find(c => c.id === candidate.parentId) + const parentLabel = parentCandidate + ? (parentCandidate as { source?: string }).source === "REFINE" + ? `From Refinement ${parentCandidate.index}` + : `From Candidate ${parentCandidate.index}` + : undefined + sections.push({ + id: call.id, + type: "refinement", + title: `Refinement ${candidate.index}`, + subtitle: parentLabel, + timestamp, + duration: call.latency_ms ?? undefined, + status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", + model: call.model_name, + cost: call.llm_cost, + tokens: call.total_tokens, + content: { + type: "refinement", + code: candidate.code, + parentCode: parentCandidate?.code ?? originalCode, + explanation: candidate.explanation, + rank, + isBest: candidate.id === bestCandidateId, + }, + }) + } + } else if (callType === "ranking") { + const allCandidates = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates] + const rankings = Object.entries(candidateRankMap) + .sort(([, a], [, b]) => a - b) + .map(([id]) => { + const cand = allCandidates.find(c => c.id === id) + if (!cand) return null + const source = (cand as { source?: string }).source + const prefix = source === "REFINE" ? "Refinement" : source === "OPTIMIZE_LP" ? "LP Candidate" : "Candidate" + return { id, rank: 0, label: `${prefix} ${cand.index}`, code: cand.code, isBest: false } + }) + .filter((r): r is NonNullable => r !== null) + .map((r, index) => ({ ...r, rank: index + 1, isBest: index === 0 })) + + sections.push({ + id: call.id, + type: "ranking", + title: "Candidate Ranking", + subtitle: "Selecting the best optimization", + timestamp, + duration: call.latency_ms ?? undefined, + status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", + model: call.model_name, + cost: call.llm_cost, + tokens: call.total_tokens, + content: { + type: "ranking", + explanation: rankingExplanation ?? "", + rankings, + usedForPr, + }, + }) + } + } + + const typeOrder: Record = { + test_generation: 0, + optimization: 1, + line_profiler: 2, + refinement: 3, + ranking: 4, + summary: 5, + } + + sections.sort((a, b) => { + const orderA = typeOrder[a.type] ?? 99 + const orderB = typeOrder[b.type] ?? 99 + if (orderA !== orderB) return orderA - orderB + const candidateTypes = ["optimization", "line_profiler", "refinement"] + if (candidateTypes.includes(a.type)) { + const indexA = parseInt(a.title.match(/\d+$/)?.[0] ?? "0", 10) + const indexB = parseInt(b.title.match(/\d+$/)?.[0] ?? "0", 10) + return indexA - indexB + } + return a.timestamp - b.timestamp + }) + + return { sections, totalDuration } +} diff --git a/js/cf-webapp/src/components/observability/trace-search.tsx b/js/cf-webapp/src/components/observability/trace-search.tsx new file mode 100644 index 000000000..cfe58d35a --- /dev/null +++ b/js/cf-webapp/src/components/observability/trace-search.tsx @@ -0,0 +1,83 @@ +"use client" + +import { useState, useCallback, type ChangeEvent } from "react" +import { Search, Loader2, CheckCircle } from "lucide-react" +import { useRouter } from "next/navigation" + +interface TraceSearchProps { + initialTraceId?: string + isLoading?: boolean + hasResults?: boolean +} + +export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults = false }: TraceSearchProps) { + const [traceId, setTraceId] = useState(initialTraceId) + const router = useRouter() + + const handleChange = useCallback((e: ChangeEvent) => { + setTraceId(e.target.value) + }, []) + + const handleSearch = useCallback(() => { + const trimmedId = traceId.trim() + if (!trimmedId) return + + const params = new URLSearchParams(window.location.search) + params.set("trace_id", trimmedId) + router.push(`/observability?${params.toString()}`) + }, [traceId, router]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSearch() + } + }, + [handleSearch], + ) + + const inputBorderClass = hasResults + ? "border-green-500 dark:border-green-500 focus:ring-green-500" + : "border-zinc-300 dark:border-zinc-600 focus:ring-blue-500" + + return ( +
+
+
+ + + {hasResults && ( + + )} +
+ +
+ {!hasResults && ( +

+ Paste or type a trace ID to view all associated LLM calls, candidates, and errors +

+ )} +
+ ) +} diff --git a/js/cf-webapp/src/components/observability/trace-summary.tsx b/js/cf-webapp/src/components/observability/trace-summary.tsx new file mode 100644 index 000000000..483e8fa10 --- /dev/null +++ b/js/cf-webapp/src/components/observability/trace-summary.tsx @@ -0,0 +1,124 @@ +import { + CheckCircle, + XCircle, + AlertCircle, + Timer, + DollarSign, + Github, + Terminal, + Hash, + Code as CodeIcon, +} from "lucide-react" +import { InfoIcon } from "./info-icon" + +interface TraceSummaryProps { + status: "Completed" | "Partial" | "Failed" + source: string + durationSeconds: number + totalCost: number + totalTokens: number + candidatesCount?: number +} + +export function TraceSummary({ + status, + source, + durationSeconds, + totalCost, + totalTokens, + candidatesCount, +}: TraceSummaryProps) { + let statusColor: string + if (status === "Failed") { + statusColor = "text-red-600 dark:text-red-400" + } else if (status === "Partial") { + statusColor = "text-yellow-600 dark:text-yellow-400" + } else { + statusColor = "text-green-600 dark:text-green-400" + } + + let StatusIcon + if (status === "Completed") { + StatusIcon = CheckCircle + } else if (status === "Failed") { + StatusIcon = XCircle + } else { + StatusIcon = AlertCircle + } + + const SourceIcon = source.toLowerCase().includes("github") ? Github : Terminal + + return ( +
+
+
+
+ + Status + +
+
{status}
+
+ +
+
+ + Source + +
+
+ + {source} + +
+
+ +
+
+ + Duration + +
+
+ {durationSeconds.toFixed(2)}s +
+
+ +
+
+ + Cost + +
+
+ ${totalCost.toFixed(4)} +
+
+ +
+
+ + Tokens + +
+
+ {totalTokens.toLocaleString()} +
+
+ + {candidatesCount !== undefined && ( +
+
+ + Candidates + +
+
+ {candidatesCount} +
+
+ )} +
+
+ ) +} diff --git a/js/cf-webapp/src/components/observability/utils.ts b/js/cf-webapp/src/components/observability/utils.ts new file mode 100644 index 000000000..8d040e231 --- /dev/null +++ b/js/cf-webapp/src/components/observability/utils.ts @@ -0,0 +1,16 @@ +/** + * Determines the source of an optimization based on event_type + */ +export function getTraceSource(eventType: string | null): string { + if (!eventType) return "Unknown" + + if (eventType === "pr_created" || eventType === "pr_merged" || eventType === "pr_closed") { + return "GitHub Action" + } + + if (eventType === "no-pr") { + return "CLI/VSCode" + } + + return eventType +} diff --git a/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx b/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx deleted file mode 100644 index fc1771cac..000000000 --- a/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx +++ /dev/null @@ -1,928 +0,0 @@ -"use client" - -import React, { useState, useEffect, useMemo, useCallback, useRef } from "react" -import { DiffEditor, useMonaco, DiffOnMount } from "@monaco-editor/react" -import { - CheckCircle2, - XCircle, - GitPullRequest, - Zap, - TestTube, - ChevronDown, - ChevronUp, - ExternalLink, - FileCode, - Edit3, - Save, - X, - Lock, - Monitor, - Smartphone, - BarChart3, -} from "lucide-react" -// Ensure you have lucide-react installed as per your package.json -import { Loader2, FileText, AlertTriangle } from "lucide-react" -import type { ExperimentMetadata, DiffContents } from "@/lib/types" // Adjust path if needed -import { getMonacoLanguage } from "@/lib/utils" -import ReactMarkdown from "react-markdown" -import remarkGfm from "remark-gfm" -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" -import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" - -interface MonacoDiffViewerProps { - metadata: ExperimentMetadata | null // Full metadata object - repoFullName: string // Formatted as "owner/repo" - traceId: string - review_quality: string - review_explanation: string -} - -const MonacoDiffViewer: React.FC = ({ - metadata, - repoFullName, - traceId, - review_quality, - review_explanation, -}) => { - const monaco = useMonaco() - const [activeFileKey, setActiveFileKey] = useState(null) - const [showTestDetails, setShowTestDetails] = useState(false) - const [showGeneratedTests, setShowGeneratedTests] = useState(false) - const [showOptimizationQuality, setShowOptimizationQuality] = useState(false) - const [showOptimizationExplanation, setShowOptimizationExplanation] = useState(false) - const [isEditing, setIsEditing] = useState(false) - const [editSecret, setEditSecret] = useState("") - const [showSecretPrompt, setShowSecretPrompt] = useState(false) - const [currentEdit, setCurrentEdit] = useState<{ [key: string]: string }>({}) - const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle") - const [savedChanges, setSavedChanges] = useState<{ [key: string]: string }>({}) - const [isMobile, setIsMobile] = useState(false) - const [useInlineView, setUseInlineView] = useState(false) - - const isEditingRef = useRef(isEditing) - const activeFileKeyRef = useRef(activeFileKey) - - useEffect(() => { - isEditingRef.current = isEditing - }, [isEditing]) - - useEffect(() => { - activeFileKeyRef.current = activeFileKey - }, [activeFileKey]) - - const handleEditorOnMount: DiffOnMount = useCallback(editor => { - // Always set up the change listener, but only update state when editing - const modifiedEditor = editor.getModifiedEditor() - modifiedEditor.onDidChangeModelContent(() => { - if (isEditingRef.current) { - const value = modifiedEditor.getValue() - if (activeFileKeyRef.current && value !== undefined) { - setCurrentEdit(prev => ({ - ...prev, - [activeFileKeyRef.current!]: value, - })) - } - } - }) - }, []) - - // Merge metadata with saved changes for display - const diffContents: DiffContents | null = useMemo(() => { - if (!metadata?.diffContents) return null - - const updatedContents = { ...metadata.diffContents } - Object.entries(savedChanges).forEach(([fileKey, newContent]) => { - if (updatedContents[fileKey]) { - updatedContents[fileKey] = { - ...updatedContents[fileKey], - newContent, - } - } - }) - return updatedContents - }, [metadata?.diffContents, savedChanges]) - const prCommentFields = metadata?.prCommentFields - const fileKeys = useMemo(() => { - return diffContents ? Object.keys(diffContents) : [] - }, [diffContents]) - - // Calculate test statistics - const testStats = useMemo(() => { - const stats = { - totalPassed: 0, - totalFailed: 0, - categories: [] as { name: string; passed: number; failed: number; icon: string }[], - } - - if (prCommentFields?.report_table) { - Object.entries(prCommentFields.report_table).forEach(([category, results]) => { - stats.totalPassed += results.passed - stats.totalFailed += results.failed - - // Map category names to icons - let icon = "🧪" - if (category.includes("Replay")) icon = "⏪" - else if (category.includes("Unit")) icon = "⚙️" - else if (category.includes("Coverage")) icon = "🔎" - else if (category.includes("Regression")) icon = "🌀" - else if (category.includes("Inspired")) icon = "🎨" - - stats.categories.push({ - name: category, - passed: results.passed, - failed: results.failed, - icon, - }) - }) - } - - return stats - }, [prCommentFields]) - - const handleEditClick = () => { - setShowSecretPrompt(true) - } - - const handleSecretSubmit = () => { - if (editSecret === "codeflash-edit-2025") { - setIsEditing(true) - setShowSecretPrompt(false) - // Initialize current edit with current content if not already set - if (activeFileKey && diffContents?.[activeFileKey] && !currentEdit[activeFileKey]) { - setCurrentEdit(prev => ({ - ...prev, - [activeFileKey]: - diffContents[activeFileKey].newContent || diffContents[activeFileKey].oldContent || "", - })) - } - } else { - alert("Invalid secret!") - } - } - - const handleSaveCode = async () => { - if (!activeFileKey || !currentEdit[activeFileKey]) return - - setSaveStatus("saving") - try { - const response = await fetch(`/api/traces/${traceId}/save-modified-code`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - fileKey: activeFileKey, - modifiedCode: currentEdit[activeFileKey], - secret: editSecret, - }), - }) - - if (response.ok) { - setSaveStatus("saved") - - // Update local saved changes state to show the changes immediately - setSavedChanges(prev => ({ - ...prev, - [activeFileKey]: currentEdit[activeFileKey], - })) - - // Clear current edit and auto-return to diff view after successful save - setTimeout(() => { - setSaveStatus("idle") - setIsEditing(false) - setCurrentEdit(prev => { - const newEdit = { ...prev } - delete newEdit[activeFileKey!] - return newEdit - }) - }, 1500) - } else { - setSaveStatus("error") - setTimeout(() => setSaveStatus("idle"), 3000) - } - } catch (error) { - console.error("Failed to save modified code:", error) - setSaveStatus("error") - setTimeout(() => setSaveStatus("idle"), 3000) - } - } - - const handleCancelEdit = () => { - setIsEditing(false) - setEditSecret("") - // Revert changes for current file - if (activeFileKey) { - setCurrentEdit(prev => { - const newEdit = { ...prev } - delete newEdit[activeFileKey] - return newEdit - }) - } - } - - useEffect(() => { - if (fileKeys.length > 0 && !activeFileKey) { - setActiveFileKey(fileKeys[0]) - } - }, [fileKeys, activeFileKey]) - - // Mobile detection and responsive handler - useEffect(() => { - const checkMobile = () => { - const mobile = window.innerWidth < 768 - setIsMobile(mobile) - setUseInlineView(mobile) - } - - checkMobile() - window.addEventListener("resize", checkMobile) - return () => window.removeEventListener("resize", checkMobile) - }, []) - - useEffect(() => { - if (monaco) { - // Define your custom dark theme for Monaco Editor - monaco.editor.defineTheme("codeflash-python-dark", { - base: "vs-dark", - inherit: true, - rules: [ - { token: "comment.python", foreground: "6A9955" }, // Python comments - { token: "keyword.python", foreground: "569CD6" }, // Python keywords - { token: "string.python", foreground: "CE9178" }, // Python strings - { token: "number.python", foreground: "B5CEA8" }, // Python numbers - { token: "identifier.python", foreground: "9CDCFE" }, - { token: "type.identifier.python", foreground: "4EC9B0" }, // class names, etc. - ], - colors: { - "editor.background": "#0A0E14", - "editor.foreground": "#F8F8F2", - "editorLineNumber.foreground": "#6272A4", - "editor.selectionBackground": "#44475A", - "editor.lineHighlightBackground": "#1A1F29", - "diffEditor.insertedTextBackground": "#50FA7B33", - "diffEditor.removedTextBackground": "#FF555533", - "diffEditor.insertedLineBackground": "#50FA7B22", - "diffEditor.removedLineBackground": "#FF555522", - "diffEditorGutter.insertedLineBackground": "#50FA7B", - "diffEditorGutter.removedLineBackground": "#FF5555", - }, - }) - } - }, [monaco]) - - // Loading or error states - NOW AFTER ALL HOOKS - if (!metadata) { - return ( -
- -

- Loading trace details for {traceId}... -

-
- ) - } - - if (!diffContents || fileKeys.length === 0) { - return ( -
- -

No diff content available for this trace.

-

Trace ID: {traceId}

-
- ) - } - - const currentDiff = activeFileKey && diffContents ? diffContents[activeFileKey] : null - const functionName = prCommentFields?.function_name || "N/A" - const speedup = prCommentFields?.speedup_pct || prCommentFields?.speedup_x || "N/A" - const explanation = prCommentFields?.optimization_explanation - - return ( -
- {/* Header Section - Mobile Optimized */} -
-
- {/* Top Row - Title with PR Link and Observability Link */} -
-
-

- CodeFlash Optimization -

- {metadata.pullNumber && ( - - - PR #{metadata.pullNumber} - #{metadata.pullNumber} - - - )} -
- {/* Observability Link */} - - - Observability - Obs - - -
- - {/* Info Row - Repository and Function (Compact on Mobile) */} -
-
- - Repo: - {repoFullName} -
-
- Function: - - {functionName} - -
-
- - {/* Performance Metrics Row - Compact on Mobile */} -
- {/* Performance Boost - Smaller on Mobile */} -
- -
-
- Boost -
- - {speedup} - -
-
- - {/* Additional Metrics - Hidden on very small screens */} - {prCommentFields?.loop_count && ( -
-
- Loops -
-
- {prCommentFields.loop_count.toLocaleString()} -
-
- )} - {prCommentFields?.original_runtime && prCommentFields?.best_runtime && ( -
-
- Runtime -
-
- - {prCommentFields.original_runtime} - - - {prCommentFields.best_runtime} -
-
- )} - - {/* Edit Button - Compact on Mobile */} -
- {!isEditing ? ( - - ) : ( -
- - -
- )} -
-
- - {/* Test Results Summary - Collapsed by default on mobile */} - {testStats.totalPassed > 0 || testStats.totalFailed > 0 ? ( -
-
setShowTestDetails(!showTestDetails)} - > -
- - - Test Results - -
-
- - - {testStats.totalPassed} - -
- {testStats.totalFailed > 0 && ( -
- - - {testStats.totalFailed} - -
- )} -
-
- {showTestDetails ? ( - - ) : ( - - )} -
- - {showTestDetails && ( -
- {testStats.categories.map((category, idx) => ( -
-
- - {category.name} - -
-
- {category.passed > 0 && ( - {category.passed}✓ - )} - {category.failed > 0 && ( - {category.failed}✗ - )} -
-
- ))} -
- )} -
- ) : null} -
-
- - {/* File Path/Tabs - Mobile Optimized */} - {fileKeys.length === 1 ? ( - // Single file - show full path with view toggle -
-
- - - {fileKeys[0]} - -
- {/* View Toggle for mobile compatibility */} -
- -
-
- ) : ( - // Multiple files - show tabs with full path on hover -
-
-
- {fileKeys.map(fileKey => ( - - ))} -
- {/* View Toggle for mobile compatibility */} -
- -
-
-
- )} - - {/* Monaco Editor Container */} -
- {activeFileKey && currentDiff && monaco ? ( - // Check for empty diff scenarios first (only in non-editing mode) - !isEditing && - (currentDiff.oldContent || "") === (currentDiff.newContent || "") && - (currentDiff.oldContent || "").trim() === "" ? ( - // Both contents are empty -
- -

No Changes to Display

-

- The file content is empty. The staging branch may have been merged or the changes - have been reverted. -

-
- ) : !isEditing && (currentDiff.oldContent || "") === (currentDiff.newContent || "") ? ( - // Contents are identical (no diff) -
- -

No Differences Found

-

- The original and optimized code are identical. The changes may have already been - applied or reverted. -

-
- ) : isEditing ? ( - // Edit Mode with Diff View - Like VS Code changes view - - -

Loading Diff Editor...

-
- } - /> - ) : ( - // Diff View Mode - - -

Loading Diff Editor...

-
- } - /> - ) - ) : ( -
- -

{!monaco ? "Initializing Editor Subsystem..." : "Preparing View..."}

-
- )} -
- {/* Bottom Section with Explanation and Generated Tests - Mobile Optimized */} -
- {/* Optimization review details */} - {review_quality && ( -
-
setShowOptimizationQuality(!showOptimizationQuality)} - > -

- - 🎯 Quality: {review_quality} -

- {showOptimizationQuality ? ( - - ) : ( - - )} -
- {showOptimizationQuality && ( -
- {review_explanation} -
- )} -
- )} - {/* Optimization Explanation */} - {explanation && ( -
-
setShowOptimizationExplanation(!showOptimizationExplanation)} - > -

- - Optimization Explanation - Explanation -

- {showOptimizationExplanation ? ( - - ) : ( - - )} -
- {showOptimizationExplanation && ( -
- {explanation} -
- )} -
- )} - - {/* Generated Tests Toggle */} - {metadata.generatedTests && ( -
- - {showGeneratedTests && ( -
- { - return ( -
-
-                            {children}
-                          
-
- ) - }, - code: props => { - const { inline, className, children, ...restProps } = props as { - inline?: boolean - className?: string - children: React.ReactNode - [key: string]: unknown - } - const match = /language-(\w+)/.exec(className || "") - const language = match ? match[1] : null - - return !inline && language ? ( - - {String(children).replace(/\n$/, "")} - - ) : ( - - {children} - - ) - }, - p: ({ children }) => ( -

{children}

- ), - h1: ({ children }) => ( -

- {children} -

- ), - h2: ({ children }) => ( -

- {children} -

- ), - h3: ({ children }) => ( -

- {children} -

- ), - ul: ({ children }) => ( -
    - {children} -
- ), - ol: ({ children }) => ( -
    - {children} -
- ), - li: ({ children }) => ( -
  • - {children} -
  • - ), - }} - > - {(() => { - // Check if generatedTests already has markdown code blocks - const testsContent = metadata.generatedTests.trim() - const hasCodeBlocks = testsContent.includes("```") - - // If it doesn't have code blocks, wrap it as Python code - if (!hasCodeBlocks) { - return "```python\n" + testsContent + "\n```" - } - - // Otherwise, return as-is - return testsContent - })()} -
    -
    - )} -
    - )} -
    - - {/* Secret Prompt Modal - Mobile Optimized */} - {showSecretPrompt && ( -
    -
    -
    - -

    Enter Edit Secret

    -
    -

    - Please enter the secret key to enable code editing. -

    - setEditSecret(e.target.value)} - onKeyDown={e => e.key === "Enter" && handleSecretSubmit()} - className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent mb-3 sm:mb-4 text-sm sm:text-base" - autoFocus - /> -
    - - -
    -
    -
    - )} -
    - ) -} - -export default MonacoDiffViewer diff --git a/js/cf-webapp/src/components/ui/badge.tsx b/js/cf-webapp/src/components/ui/badge.tsx index 27195fca3..cf923ccb9 100644 --- a/js/cf-webapp/src/components/ui/badge.tsx +++ b/js/cf-webapp/src/components/ui/badge.tsx @@ -4,16 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "inline-flex items-center rounded-sm border px-2 py-0.5 text-xs font-semibold transition-colors", { variants: { variant: { - default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + default: "border-transparent bg-zinc-700 text-zinc-100 hover:bg-zinc-600", secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + "border-transparent bg-zinc-800 text-zinc-300 hover:bg-zinc-700", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", + outline: "border-zinc-700 text-zinc-300", }, }, defaultVariants: { @@ -31,4 +31,4 @@ function Badge({ className, variant, ...props }: BadgeProps) { return
    } -export { Badge, badgeVariants } +export { Badge, badgeVariants } \ No newline at end of file diff --git a/js/cf-webapp/src/components/ui/button.tsx b/js/cf-webapp/src/components/ui/button.tsx index 635b9ad4e..3c81bcd42 100644 --- a/js/cf-webapp/src/components/ui/button.tsx +++ b/js/cf-webapp/src/components/ui/button.tsx @@ -5,22 +5,22 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-sm text-sm font-medium ring-offset-background transition-all duration-150 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-600 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: "bg-zinc-100 dark:bg-zinc-700 text-zinc-950 dark:text-zinc-50 hover:bg-zinc-200 dark:hover:bg-zinc-600", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + outline: "border border-zinc-700 hover:bg-zinc-800 hover:text-zinc-50", + secondary: "bg-zinc-800 text-zinc-50 hover:bg-zinc-700", + ghost: "hover:bg-zinc-800 hover:text-zinc-50", + link: "text-zinc-400 underline-offset-4 hover:underline hover:text-zinc-300", }, size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", + default: "h-9 px-3 py-2", + sm: "h-8 rounded-sm px-2", + lg: "h-10 rounded-sm px-4", + icon: "h-9 w-9", }, }, defaultVariants: { diff --git a/js/cf-webapp/src/components/ui/card.tsx b/js/cf-webapp/src/components/ui/card.tsx index 3f29d9bcf..8f2ece0af 100644 --- a/js/cf-webapp/src/components/ui/card.tsx +++ b/js/cf-webapp/src/components/ui/card.tsx @@ -6,7 +6,7 @@ const Card = React.forwardRef (
    ), @@ -15,7 +15,7 @@ Card.displayName = "Card" const CardHeader = React.forwardRef>( ({ className, ...props }, ref) => ( -
    +
    ), ) CardHeader.displayName = "CardHeader" @@ -24,7 +24,7 @@ const CardTitle = React.forwardRef (

    ), @@ -35,20 +35,20 @@ const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

    +

    )) CardDescription.displayName = "CardDescription" const CardContent = React.forwardRef>( ({ className, ...props }, ref) => ( -

    +
    ), ) CardContent.displayName = "CardContent" const CardFooter = React.forwardRef>( ({ className, ...props }, ref) => ( -
    +
    ), ) CardFooter.displayName = "CardFooter" diff --git a/js/cf-webapp/src/components/ui/icon-example.tsx b/js/cf-webapp/src/components/ui/icon-example.tsx new file mode 100644 index 000000000..0741b7ae3 --- /dev/null +++ b/js/cf-webapp/src/components/ui/icon-example.tsx @@ -0,0 +1,45 @@ +/** + * Icon Standardization Examples + * + * This file demonstrates the standardized icon treatment across components + * following the zinc color palette and consistent sizing patterns. + * + * Icon Guidelines: + * - Small contexts (buttons, badges): w-4 h-4 + * - Medium contexts (headers, titles): w-5 h-5 + * - Consistent stroke width: strokeWidth={2} + * - Color hierarchy: zinc-400 (muted) → zinc-300 (hover) → zinc-50 (active) + * - All icons from lucide-react should follow these standards + */ + +import { Search, Settings, ChevronRight } from "lucide-react" +import { Button } from "./button" + +export function IconExamples() { + return ( +
    + {/* Button with icon - w-4 h-4 standard */} + + + {/* Icon button - icon only */} + + + {/* Card header icon - w-5 h-5 for larger contexts */} +
    + +

    Card Title

    +
    + + {/* Active state example */} + +
    + ) +} \ No newline at end of file diff --git a/js/cf-webapp/src/components/ui/input.tsx b/js/cf-webapp/src/components/ui/input.tsx index 5b517860f..ec74db68d 100644 --- a/js/cf-webapp/src/components/ui/input.tsx +++ b/js/cf-webapp/src/components/ui/input.tsx @@ -10,7 +10,7 @@ const Input = React.forwardRef( span]:line-clamp-1", + "flex h-9 w-full items-center justify-between rounded-sm border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-zinc-50 ring-offset-background placeholder:text-zinc-500 focus:outline-none focus:ring-1 focus:ring-zinc-600 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} > {children} - + )) @@ -41,7 +41,7 @@ const SelectScrollUpButton = React.forwardRef< className={cn("flex cursor-default items-center justify-center py-1", className)} {...props} > - + )) SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName @@ -55,7 +55,7 @@ const SelectScrollDownButton = React.forwardRef< className={cn("flex cursor-default items-center justify-center py-1", className)} {...props} > - + )) SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName @@ -69,7 +69,7 @@ const SelectContent = React.forwardRef< - + @@ -134,7 +134,7 @@ const SelectSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/js/cf-webapp/src/components/ui/separator.tsx b/js/cf-webapp/src/components/ui/separator.tsx index aa7acb100..8d8e78810 100644 --- a/js/cf-webapp/src/components/ui/separator.tsx +++ b/js/cf-webapp/src/components/ui/separator.tsx @@ -15,7 +15,7 @@ const Separator = React.forwardRef< decorative={decorative} orientation={orientation} className={cn( - "shrink-0 bg-border", + "shrink-0 bg-zinc-800", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className, )} @@ -24,4 +24,4 @@ const Separator = React.forwardRef< )) Separator.displayName = SeparatorPrimitive.Root.displayName -export { Separator } +export { Separator } \ No newline at end of file diff --git a/js/cf-webapp/src/components/ui/switch.tsx b/js/cf-webapp/src/components/ui/switch.tsx index 58693e107..efb2265ca 100644 --- a/js/cf-webapp/src/components/ui/switch.tsx +++ b/js/cf-webapp/src/components/ui/switch.tsx @@ -21,7 +21,7 @@ export function Switch({ checked, onCheckedChange, disabled, className, id }: Sw disabled={disabled} onClick={() => onCheckedChange(!checked)} className={cn( - "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors", + "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-all duration-200 ease-in-out", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", "disabled:cursor-not-allowed disabled:opacity-50", checked ? "bg-primary" : "bg-muted-foreground/30", @@ -30,7 +30,7 @@ export function Switch({ checked, onCheckedChange, disabled, className, id }: Sw > diff --git a/js/cf-webapp/src/components/ui/table.tsx b/js/cf-webapp/src/components/ui/table.tsx index 065a962d3..f16a68b3a 100644 --- a/js/cf-webapp/src/components/ui/table.tsx +++ b/js/cf-webapp/src/components/ui/table.tsx @@ -33,7 +33,7 @@ const TableFooter = React.forwardRef< >(({ className, ...props }, ref) => ( tr]:last:border-b-0", className)} + className={cn("border-t border-zinc-800 bg-zinc-900 font-medium [&>tr]:last:border-b-0", className)} {...props} /> )) @@ -44,7 +44,7 @@ const TableRow = React.forwardRef(({ className, ...props }, ref) => ( )) @@ -88,4 +88,4 @@ const TableCaption = React.forwardRef< )) TableCaption.displayName = "TableCaption" -export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } +export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } \ No newline at end of file diff --git a/js/cf-webapp/src/components/ui/tabs.tsx b/js/cf-webapp/src/components/ui/tabs.tsx index bf5794d4f..a6543a39a 100644 --- a/js/cf-webapp/src/components/ui/tabs.tsx +++ b/js/cf-webapp/src/components/ui/tabs.tsx @@ -30,7 +30,7 @@ const TabsTrigger = React.forwardRef< and tags; optimization uses markdown code blocks. - * raw_response is often the full API JSON (e.g. OpenAI); we extract message content when present. - */ - -/** Extract message content from OpenAI-style API response JSON, or return null */ -export function extractMessageContentFromApiResponse(raw: string): string | null { - try { - const parsed = JSON.parse(raw) as { - choices?: Array<{ message?: { content?: string } }> - } - const content = parsed?.choices?.[0]?.message?.content - return typeof content === "string" ? content : null - } catch { - return null - } -} - -/** Get the string to use for rank/explain and markdown parsing (inner content if API JSON, else raw) */ -export function getResponseContentForParsing(rawResponse: string): string { - return extractMessageContentFromApiResponse(rawResponse) ?? rawResponse -} - -/** Extract content inside ... */ -export function extractRankTag(content: string): string | null { - const m = content.match(/([\s\S]*?)<\/rank>/i) - return m ? m[1].trim() : null -} - -/** Extract content inside ... */ -export function extractExplainTag(content: string): string | null { - const m = content.match(/([\s\S]*?)<\/explain>/i) - return m ? m[1].trim() : null -} - -export type ResponseSegment = - | { kind: "text"; content: string } - | { kind: "code"; language: string; content: string } - -/** - * Split markdown-like content into text and code blocks (```lang ... ```). - * Language is taken from the first word after ```; default "text". - */ -export function splitMarkdownCodeBlocks(content: string): ResponseSegment[] { - const segments: ResponseSegment[] = [] - const re = /```(\w*)\n?([\s\S]*?)```/g - let lastEnd = 0 - let m: RegExpExecArray | null - while ((m = re.exec(content)) !== null) { - if (m.index > lastEnd) { - const text = content.slice(lastEnd, m.index) - if (text.trim()) { - segments.push({ kind: "text", content: text }) - } - } - const lang = m[1] || "text" - segments.push({ kind: "code", language: lang, content: m[2].trim() }) - lastEnd = re.lastIndex - } - if (lastEnd < content.length) { - const text = content.slice(lastEnd) - if (text.trim()) { - segments.push({ kind: "text", content: text }) - } - } - return segments -} diff --git a/js/cf-webapp/src/lib/observability-utils.ts b/js/cf-webapp/src/lib/observability-utils.ts deleted file mode 100644 index 72c6ef896..000000000 --- a/js/cf-webapp/src/lib/observability-utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Shared utilities for observability pages - -/** - * Determines the source of an LLM call based on event_type and context - */ -export function getCallSource( - eventType: string | null, - context: Record | null, -): string { - if ( - context && - typeof context === "object" && - !Array.isArray(context) && - "source" in context - ) { - return String(context.source) - } - if (eventType) { - if (eventType === "pr_created" || eventType === "pr_merged" || eventType === "pr_closed") { - return "GitHub Action" - } - if (eventType === "no-pr") { - return "CLI/VSCode" - } - return eventType - } - return "Unknown" -} - -/** - * Safely extracts cost and tokens from nullable values - */ -export function safeCostTokens(cost: number | null, tokens: number | null) { - return { - cost: cost ?? 0, - tokens: tokens ?? 0, - } -} diff --git a/js/cf-webapp/src/middleware.ts b/js/cf-webapp/src/middleware.ts index c9438c9ea..bd2549e00 100644 --- a/js/cf-webapp/src/middleware.ts +++ b/js/cf-webapp/src/middleware.ts @@ -53,7 +53,6 @@ export const config = { matcher: [ "/", "/app/:path*", - "/trace/:path*", "/billing", "/billing/:path*", "/apikeys", diff --git a/js/cf-webapp/src/styles/spacing.css b/js/cf-webapp/src/styles/spacing.css new file mode 100644 index 000000000..e3087c6e4 --- /dev/null +++ b/js/cf-webapp/src/styles/spacing.css @@ -0,0 +1,47 @@ +/* Spacing and Layout Token System */ +/* Based on 8px grid for consistent rhythm */ + +:root { + /* Base spacing unit - foundation of the 8px grid */ + --space-unit: 8px; + + /* Border Radius Tokens */ + /* Professional, subtle radius values (2-4px max except for pills) */ + --radius-sm: 2px; /* Tight, professional for small elements */ + --radius-md: 3px; /* Default for cards, panels, buttons */ + --radius-lg: 4px; /* Maximum for larger elements */ + --radius-full: 9999px; /* Only for pills, badges, circular elements */ + + /* Spacing Tokens - 8px increments */ + --space-0: 0px; /* 0 * 8px */ + --space-px: 1px; /* For borders, hairlines */ + --space-0\.5: 4px; /* 0.5 * 8px - fine adjustments */ + --space-1: 8px; /* 1 * 8px */ + --space-2: 16px; /* 2 * 8px */ + --space-3: 24px; /* 3 * 8px */ + --space-4: 32px; /* 4 * 8px */ + --space-5: 40px; /* 5 * 8px */ + --space-6: 48px; /* 6 * 8px */ + --space-7: 56px; /* 7 * 8px */ + --space-8: 64px; /* 8 * 8px */ + --space-9: 72px; /* 9 * 8px */ + --space-10: 80px; /* 10 * 8px */ + + /* Common Layout Spacing Patterns */ + /* These map to the base spacing tokens for consistency */ + --space-gap-xs: var(--space-1); /* 8px - Tight spacing */ + --space-gap-sm: var(--space-2); /* 16px - Small spacing */ + --space-gap-md: var(--space-3); /* 24px - Medium spacing */ + --space-gap-lg: var(--space-4); /* 32px - Large spacing */ + --space-gap-xl: var(--space-5); /* 40px - Extra large spacing */ + + /* Container and Content Spacing */ + --space-container-sm: var(--space-2); /* 16px - Mobile/small screens */ + --space-container-md: var(--space-3); /* 24px - Tablet/medium screens */ + --space-container-lg: var(--space-4); /* 32px - Desktop/large screens */ + + /* Card and Panel Internal Spacing */ + --space-card-sm: var(--space-2); /* 16px - Compact cards */ + --space-card-md: var(--space-3); /* 24px - Standard cards */ + --space-card-lg: var(--space-4); /* 32px - Spacious cards */ +} \ No newline at end of file diff --git a/js/cf-webapp/src/styles/tokens.css b/js/cf-webapp/src/styles/tokens.css new file mode 100644 index 000000000..d02631efc --- /dev/null +++ b/js/cf-webapp/src/styles/tokens.css @@ -0,0 +1,184 @@ +/** + * Design Token System + * + * Professional developer-focused design tokens using CSS custom properties. + * Dark mode only, zinc color scale, semantic status colors. + * + * RGB format (e.g., "24 24 27") for Tailwind's alpha channel support. + */ + +:root { + /* ============================================ + * ZINC COLOR SCALE + * Neutral colors for all UI elements + * RGB values without rgb() wrapper for Tailwind + * ============================================ */ + + /* Lightest to darkest */ + --color-zinc-50: 250 250 250; /* Almost white */ + --color-zinc-100: 244 244 245; /* Very light gray */ + --color-zinc-200: 228 228 231; /* Light gray */ + --color-zinc-300: 212 212 216; /* Medium light gray */ + --color-zinc-400: 161 161 170; /* Medium gray */ + --color-zinc-500: 113 113 122; /* True medium gray */ + --color-zinc-600: 82 82 91; /* Medium dark gray */ + --color-zinc-700: 63 63 70; /* Dark gray */ + --color-zinc-800: 39 39 42; /* Very dark gray */ + --color-zinc-900: 24 24 27; /* Near black */ + --color-zinc-950: 9 9 11; /* Deep black */ + + /* ============================================ + * SEMANTIC STATUS COLORS + * For errors, warnings, and success states + * ============================================ */ + + /* Error states (red-based) */ + --color-error: 239 68 68; /* red-500 - Primary error */ + --color-error-foreground: 254 242 242; /* red-50 - Error text on error bg */ + --color-error-muted: 254 202 202; /* red-200 - Subtle error background */ + --color-error-border: 248 113 113; /* red-400 - Error borders */ + + /* Success states (green-based) */ + --color-success: 34 197 94; /* green-500 - Primary success */ + --color-success-foreground: 240 253 244; /* green-50 - Success text on success bg */ + --color-success-muted: 187 247 208; /* green-200 - Subtle success background */ + --color-success-border: 74 222 128; /* green-400 - Success borders */ + + /* Warning states (amber-based, not bright yellow) */ + --color-warning: 245 158 11; /* amber-500 - Primary warning */ + --color-warning-foreground: 255 251 235; /* amber-50 - Warning text on warning bg */ + --color-warning-muted: 254 215 170; /* amber-200 - Subtle warning background */ + --color-warning-border: 251 191 36; /* amber-400 - Warning borders */ + + /* Info states (blue-based) */ + --color-info: 59 130 246; /* blue-500 - Primary info */ + --color-info-foreground: 239 246 255; /* blue-50 - Info text on info bg */ + --color-info-muted: 191 219 254; /* blue-200 - Subtle info background */ + --color-info-border: 96 165 250; /* blue-400 - Info borders */ + + /* ============================================ + * FUNCTIONAL TOKENS + * High-level tokens that reference color scale + * These are what components should use + * ============================================ */ + + /* Core UI surfaces */ + --background: var(--color-zinc-950); /* Main app background */ + --foreground: var(--color-zinc-50); /* Main text color */ + + /* Card and panel surfaces */ + --card: var(--color-zinc-900); /* Card background */ + --card-foreground: var(--color-zinc-50); /* Card text */ + + /* Popover/dropdown surfaces */ + --popover: var(--color-zinc-900); /* Popover background */ + --popover-foreground: var(--color-zinc-50); /* Popover text */ + + /* Interactive elements */ + --primary: var(--color-zinc-50); /* Primary button bg */ + --primary-foreground: var(--color-zinc-950); /* Primary button text */ + + --secondary: var(--color-zinc-800); /* Secondary button bg */ + --secondary-foreground: var(--color-zinc-50); /* Secondary button text */ + + --accent: var(--color-zinc-700); /* Accent elements */ + --accent-foreground: var(--color-zinc-50); /* Accent text */ + + /* Muted states */ + --muted: var(--color-zinc-800); /* Muted backgrounds */ + --muted-foreground: var(--color-zinc-400); /* Muted text */ + + /* Destructive actions */ + --destructive: var(--color-error); /* Destructive button bg */ + --destructive-foreground: var(--color-error-foreground); /* Destructive button text */ + + /* Borders and inputs */ + --border: var(--color-zinc-800); /* Default borders */ + --input: var(--color-zinc-800); /* Input borders */ + --ring: var(--color-zinc-600); /* Focus rings */ + + /* ============================================ + * TYPOGRAPHY TOKENS + * Font stacks and text properties + * ============================================ */ + + /* Font families */ + --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Roboto Mono', ui-monospace, 'Courier New', monospace; + --font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + + /* Font sizes - scale for data-dense layouts */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + + /* Line heights */ + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + + /* ============================================ + * SPACING TOKENS + * 8px grid system + * ============================================ */ + + --space-unit: 8px; + --space-0: 0; + --space-0-5: 4px; /* Half unit for fine adjustments */ + --space-1: 8px; /* Base unit */ + --space-1-5: 12px; + --space-2: 16px; + --space-2-5: 20px; + --space-3: 24px; + --space-4: 32px; + --space-5: 40px; + --space-6: 48px; + --space-7: 56px; + --space-8: 64px; + --space-9: 72px; + --space-10: 80px; + + /* ============================================ + * BORDER RADIUS TOKENS + * Flat, minimal rounded corners + * ============================================ */ + + --radius-none: 0; + --radius-sm: 2px; /* Subtle rounding */ + --radius-md: 3px; /* Default */ + --radius-lg: 4px; /* Maximum rounding */ + --radius: var(--radius-md); /* Default radius */ + + /* ============================================ + * SHADOW TOKENS + * Minimal shadows for depth + * ============================================ */ + + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 2px 4px 0 rgba(0, 0, 0, 0.4); + --shadow-lg: 0 4px 6px 0 rgba(0, 0, 0, 0.5); + --shadow-xl: 0 8px 16px 0 rgba(0, 0, 0, 0.6); + + /* ============================================ + * TRANSITION TOKENS + * Consistent animation timing + * ============================================ */ + + --transition-fast: 150ms; + --transition-base: 250ms; + --transition-slow: 350ms; + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ============================================ + * DARK MODE ONLY + * We're not supporting light mode + * This ensures dark mode is always active + * ============================================ */ + +.dark { + /* All tokens are already defined for dark mode in :root */ + /* This class exists for Tailwind's dark: variant to work */ +} \ No newline at end of file diff --git a/js/cf-webapp/src/styles/typography.css b/js/cf-webapp/src/styles/typography.css new file mode 100644 index 000000000..964ab8d7c --- /dev/null +++ b/js/cf-webapp/src/styles/typography.css @@ -0,0 +1,29 @@ +/* Typography Token System */ + +:root { + /* Font Family Tokens */ + --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', monospace; + --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + + /* Font Size Scale - optimized for dark mode readability */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-code: 0.875rem; /* 14px - inline code specific */ + + /* Font Weight Tokens - optimized for dark backgrounds */ + --font-light: 300; + --font-normal: 400; + --font-medium: 500; + --font-semibold: 600; + --font-bold: 700; + + /* Line Height Tokens - for data density control */ + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-relaxed: 1.75; + --leading-code: 1.4; /* Specific for code blocks */ +} \ No newline at end of file diff --git a/js/cf-webapp/tailwind.config.ts b/js/cf-webapp/tailwind.config.ts index 1d627ab03..4e78882be 100644 --- a/js/cf-webapp/tailwind.config.ts +++ b/js/cf-webapp/tailwind.config.ts @@ -1,20 +1,108 @@ import type { Config } from "tailwindcss" -import { fontFamily } from "tailwindcss/defaultTheme" const config: Config = { + darkMode: 'class', content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { + spacing: { + 'px': '1px', + '0': '0', + '0.5': '4px', + '1': '8px', + '2': '16px', + '3': '24px', + '4': '32px', + '5': '40px', + '6': '48px', + '7': '56px', + '8': '64px', + '9': '72px', + '10': '80px', + }, + borderRadius: { + 'none': '0px', + 'sm': 'var(--radius-sm)', + DEFAULT: 'var(--radius-md)', + 'md': 'var(--radius-md)', + 'lg': 'var(--radius-lg)', + 'full': 'var(--radius-full)', + }, extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + zinc: { + '50': 'rgb(var(--color-zinc-50) / )', + '100': 'rgb(var(--color-zinc-100) / )', + '200': 'rgb(var(--color-zinc-200) / )', + '300': 'rgb(var(--color-zinc-300) / )', + '400': 'rgb(var(--color-zinc-400) / )', + '500': 'rgb(var(--color-zinc-500) / )', + '600': 'rgb(var(--color-zinc-600) / )', + '700': 'rgb(var(--color-zinc-700) / )', + '800': 'rgb(var(--color-zinc-800) / )', + '900': 'rgb(var(--color-zinc-900) / )', + '950': 'rgb(var(--color-zinc-950) / )', + }, + background: 'rgb(var(--background) / )', + foreground: 'rgb(var(--foreground) / )', + card: { + DEFAULT: 'rgb(var(--card) / )', + foreground: 'rgb(var(--card-foreground) / )', + }, + popover: { + DEFAULT: 'rgb(var(--popover) / )', + foreground: 'rgb(var(--popover-foreground) / )', + }, + primary: { + DEFAULT: 'rgb(var(--primary) / )', + foreground: 'rgb(var(--primary-foreground) / )', + }, + secondary: { + DEFAULT: 'rgb(var(--secondary) / )', + foreground: 'rgb(var(--secondary-foreground) / )', + }, + muted: { + DEFAULT: 'rgb(var(--muted) / )', + foreground: 'rgb(var(--muted-foreground) / )', + }, + accent: { + DEFAULT: 'rgb(var(--accent) / )', + foreground: 'rgb(var(--accent-foreground) / )', + }, + error: { + DEFAULT: 'rgb(var(--error) / )', + foreground: 'rgb(var(--error-foreground) / )', + muted: 'rgb(var(--error-muted) / )', + border: 'rgb(var(--error-border) / )', + }, + warning: { + DEFAULT: 'rgb(var(--warning) / )', + foreground: 'rgb(var(--warning-foreground) / )', + muted: 'rgb(var(--warning-muted) / )', + border: 'rgb(var(--warning-border) / )', + }, + success: { + DEFAULT: 'rgb(var(--success) / )', + foreground: 'rgb(var(--success-foreground) / )', + muted: 'rgb(var(--success-muted) / )', + border: 'rgb(var(--success-border) / )', + }, + info: { + DEFAULT: 'rgb(var(--info) / )', + foreground: 'rgb(var(--info-foreground) / )', + muted: 'rgb(var(--info-muted) / )', + border: 'rgb(var(--info-border) / )', + }, + border: 'rgb(var(--border) / )', + input: 'rgb(var(--input) / )', + ring: 'rgb(var(--ring) / )', }, fontFamily: { - sans: ["var(--font-sans)", ...fontFamily.sans], + sans: ['var(--font-sans)'], + mono: ['var(--font-mono)'], }, keyframes: { shimmer: { diff --git a/js/common/package-lock.json b/js/common/package-lock.json index 3a1757a50..4b62693f3 100644 --- a/js/common/package-lock.json +++ b/js/common/package-lock.json @@ -566,7 +566,6 @@ "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -617,7 +616,6 @@ "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", @@ -883,7 +881,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1893,7 +1890,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4079,7 +4075,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.15.0", "@prisma/engines": "6.15.0" @@ -5046,7 +5041,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From 98fb2d1579b192b0e4e07a4683dd46f67a904f84 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Fri, 6 Feb 2026 01:18:17 +0530 Subject: [PATCH 053/184] Revert "CF-1041 observability v2 " need more changes and testing (#2375) Reverts codeflash-ai/codeflash-internal#2329 --- .gitignore | 1 - .vscode/settings.json | 3 - .../prompts/testgen/execute_system_prompt.md | 4 +- django/aiservice/testgen/testgen.py | 27 +- js/cf-webapp/.gitignore | 6 +- js/cf-webapp/next.config.mjs | 12 +- js/cf-webapp/package-lock.json | 760 ++------------ js/cf-webapp/package.json | 11 - .../src/app/(auth)/codeflash/auth/content.tsx | 7 +- .../apikeys/dialog-create-api-key.tsx | 5 +- .../review-optimizations/[traceId]/page.tsx | 7 +- .../[traceId]/profiler/page.tsx | 3 +- js/cf-webapp/src/app/dashboard/page.tsx | 1 - js/cf-webapp/src/app/globals.css | 136 ++- js/cf-webapp/src/app/layout.tsx | 11 +- js/cf-webapp/src/app/observability/layout.tsx | 17 +- .../app/observability/llm-call/[id]/page.tsx | 524 ++++++++++ .../src/app/observability/llm-calls/page.tsx | 800 +++++++++++++++ .../src/app/observability/loading.tsx | 39 - js/cf-webapp/src/app/observability/page.tsx | 328 ------- .../observability/trace/[trace_id]/page.tsx | 767 +++++++++++++++ .../src/app/observability/traces/page.tsx | 654 ++++++++++++ .../src/app/trace/[trace_id]/page.tsx | 174 ++++ .../src/components/conditional-layout.tsx | 1 + .../src/components/dashboard/sidebar.tsx | 2 +- .../observability/code-context-section.tsx | 378 ------- .../observability/code-highlighter.tsx | 172 ---- .../observability/column-header.tsx | 27 + .../observability/errors-section.tsx | 201 ---- .../function-to-optimize-section.tsx | 204 ---- .../components/observability/help-button.tsx | 51 + .../src/components/observability/index.ts | 9 - .../observability/observability-nav.tsx | 55 ++ .../observability/parsed-response-view.tsx | 223 +++++ .../components/observability/python-parser.ts | 136 --- .../components/observability/stat-card.tsx | 72 ++ .../observability/timeline-page-view.tsx | 823 ---------------- .../observability/timeline-types.ts | 289 ------ .../components/observability/trace-search.tsx | 83 -- .../observability/trace-summary.tsx | 124 --- .../src/components/observability/utils.ts | 16 - .../components/trace/monaco-diff-viewer.tsx | 928 ++++++++++++++++++ js/cf-webapp/src/components/ui/badge.tsx | 10 +- js/cf-webapp/src/components/ui/button.tsx | 20 +- js/cf-webapp/src/components/ui/card.tsx | 12 +- .../src/components/ui/icon-example.tsx | 45 - js/cf-webapp/src/components/ui/input.tsx | 2 +- js/cf-webapp/src/components/ui/select.tsx | 16 +- js/cf-webapp/src/components/ui/separator.tsx | 4 +- js/cf-webapp/src/components/ui/switch.tsx | 4 +- js/cf-webapp/src/components/ui/table.tsx | 10 +- js/cf-webapp/src/components/ui/tabs.tsx | 4 +- .../src/lib/observability-response-parse.ts | 68 ++ js/cf-webapp/src/lib/observability-utils.ts | 38 + js/cf-webapp/src/middleware.ts | 1 + js/cf-webapp/src/styles/spacing.css | 47 - js/cf-webapp/src/styles/tokens.css | 184 ---- js/cf-webapp/src/styles/typography.css | 29 - js/cf-webapp/tailwind.config.ts | 98 +- js/common/package-lock.json | 6 + 60 files changed, 4620 insertions(+), 4069 deletions(-) create mode 100644 js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx create mode 100644 js/cf-webapp/src/app/observability/llm-calls/page.tsx delete mode 100644 js/cf-webapp/src/app/observability/loading.tsx delete mode 100644 js/cf-webapp/src/app/observability/page.tsx create mode 100644 js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx create mode 100644 js/cf-webapp/src/app/observability/traces/page.tsx create mode 100644 js/cf-webapp/src/app/trace/[trace_id]/page.tsx delete mode 100644 js/cf-webapp/src/components/observability/code-context-section.tsx delete mode 100644 js/cf-webapp/src/components/observability/code-highlighter.tsx create mode 100644 js/cf-webapp/src/components/observability/column-header.tsx delete mode 100644 js/cf-webapp/src/components/observability/errors-section.tsx delete mode 100644 js/cf-webapp/src/components/observability/function-to-optimize-section.tsx create mode 100644 js/cf-webapp/src/components/observability/help-button.tsx delete mode 100644 js/cf-webapp/src/components/observability/index.ts create mode 100644 js/cf-webapp/src/components/observability/observability-nav.tsx create mode 100644 js/cf-webapp/src/components/observability/parsed-response-view.tsx delete mode 100644 js/cf-webapp/src/components/observability/python-parser.ts create mode 100644 js/cf-webapp/src/components/observability/stat-card.tsx delete mode 100644 js/cf-webapp/src/components/observability/timeline-page-view.tsx delete mode 100644 js/cf-webapp/src/components/observability/timeline-types.ts delete mode 100644 js/cf-webapp/src/components/observability/trace-search.tsx delete mode 100644 js/cf-webapp/src/components/observability/trace-summary.tsx delete mode 100644 js/cf-webapp/src/components/observability/utils.ts create mode 100644 js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx delete mode 100644 js/cf-webapp/src/components/ui/icon-example.tsx create mode 100644 js/cf-webapp/src/lib/observability-response-parse.ts create mode 100644 js/cf-webapp/src/lib/observability-utils.ts delete mode 100644 js/cf-webapp/src/styles/spacing.css delete mode 100644 js/cf-webapp/src/styles/tokens.css delete mode 100644 js/cf-webapp/src/styles/typography.css diff --git a/.gitignore b/.gitignore index 2f62d43ea..955c94b53 100644 --- a/.gitignore +++ b/.gitignore @@ -163,7 +163,6 @@ cython_debug/ #.idea/ .aider* .serena/ -.planning/ /js/common/node_modules/ /node_modules/ *.xml diff --git a/.vscode/settings.json b/.vscode/settings.json index 17f421bd4..dcf0e78be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,8 +15,5 @@ ], "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "vscode.typescript-language-features" } } \ No newline at end of file diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md index 2125f9e4a..dbe648c35 100644 --- a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md +++ b/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md @@ -23,8 +23,8 @@ **CRITICAL: IMPORT PATH RULES**: - **NEVER add file extensions (.js, .ts, .tsx) to import paths** - The test framework resolves extensions automatically. -- **WRONG**: `import {{ fn }} from '../utils.js'` or `import {{ fn }} from '../utils.ts'` -- **CORRECT**: `import {{ fn }} from '../utils'` +- **WRONG**: `import {{fn}} from '../utils.js'` or `import {{fn}} from '../utils.ts'` +- **CORRECT**: `import {{fn}} from '../utils'` - The user message provides the exact import statement to use - copy it exactly without modification. **CRITICAL: VITEST IMPORTS REQUIRED**: diff --git a/django/aiservice/testgen/testgen.py b/django/aiservice/testgen/testgen.py index 1d82661ee..a1f3cdb15 100644 --- a/django/aiservice/testgen/testgen.py +++ b/django/aiservice/testgen/testgen.py @@ -241,30 +241,8 @@ async def generate_and_validate_test_code( call_sequence: int | None = None, function_to_optimize: FunctionToOptimize | None = None, module_path: str | None = None, - test_module_path: str | None = None, - helper_function_names: list[str] | None = None, - is_async: bool = False, ) -> str: - obs_context: dict | None = ( - { - "call_sequence": call_sequence, - "module_path": module_path, - "test_module_path": test_module_path, - "helper_function_names": helper_function_names, - "is_async": is_async, - "function_to_optimize": { - "function_name": function_to_optimize.function_name, - "file_path": function_to_optimize.file_path, - "qualified_name": function_to_optimize.qualified_name, - "starting_line": function_to_optimize.starting_line, - "ending_line": function_to_optimize.ending_line, - } - if function_to_optimize is not None - else None, - } - if call_sequence is not None - else None - ) + obs_context: dict | None = {"call_sequence": call_sequence} if call_sequence is not None else None response = await call_llm( llm=model, messages=messages, @@ -340,9 +318,6 @@ async def generate_regression_tests_from_function( call_sequence=call_sequence, function_to_optimize=data.function_to_optimize, module_path=data.module_path, - test_module_path=data.test_module_path, - helper_function_names=data.helper_function_names, - is_async=data.function_to_optimize.is_async or data.is_async or False, ) total_llm_cost = sum(cost_tracker) await update_optimization_cost(trace_id=trace_id, cost=total_llm_cost, user_id=user_id) diff --git a/js/cf-webapp/.gitignore b/js/cf-webapp/.gitignore index 6f7554965..79b38816a 100644 --- a/js/cf-webapp/.gitignore +++ b/js/cf-webapp/.gitignore @@ -42,8 +42,4 @@ next-env.d.ts /.npmrc .npmrc /.azure/config -*.next/* - -# Generated WASM files (built by postinstall) -/public/web-tree-sitter.wasm -/public/tree-sitter-python.wasm \ No newline at end of file +*.next/* \ No newline at end of file diff --git a/js/cf-webapp/next.config.mjs b/js/cf-webapp/next.config.mjs index 34001279e..08455a984 100644 --- a/js/cf-webapp/next.config.mjs +++ b/js/cf-webapp/next.config.mjs @@ -1,21 +1,11 @@ /** @type {import("next").NextConfig} */ let nextConfig = { transpilePackages: ["@codeflash-ai/common"], - webpack: (config, { isServer }) => { + webpack: (config) => { config.watchOptions = { poll: 1000, aggregateTimeout: 300, } - // Handle web-tree-sitter's Node.js module imports in browser - if (!isServer) { - config.resolve.fallback = { - ...config.resolve.fallback, - fs: false, - "fs/promises": false, - path: false, - module: false, - } - } return config }, experimental: { diff --git a/js/cf-webapp/package-lock.json b/js/cf-webapp/package-lock.json index 33fbba8a1..4f7668962 100644 --- a/js/cf-webapp/package-lock.json +++ b/js/cf-webapp/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "codeflash-webapp", "version": "0.1.0", - "hasInstallScript": true, "dependencies": { "@auth0/nextjs-auth0": "^3.3.0", "@azure/msal-node": "^3.7.3", @@ -15,8 +14,6 @@ "@hookform/resolvers": "^3.3.2", "@monaco-editor/react": "^4.7.0", "@prisma/client": "^6.7.0", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -37,7 +34,6 @@ "chart.js": "^4.4.9", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", - "cmdk": "^1.1.1", "date-fns": "^4.1.0", "diff": "^8.0.2", "framer-motion": "^12.12.1", @@ -56,7 +52,6 @@ "prism-react-renderer": "^2.4.1", "react": "^18", "react-chartjs-2": "^5.3.0", - "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18", "react-hook-form": "^7.48.2", "react-markdown": "^9.0.1", @@ -64,13 +59,10 @@ "react-syntax-highlighter": "^16.1.0", "remark-gfm": "^4.0.0", "sharp": "^0.34.2", - "shiki": "^3.21.0", "sonner": "^2.0.6", "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", "tailwindcss-animate": "^1.0.7", - "vaul": "^1.1.2", - "web-tree-sitter": "^0.26.3", "zod": "^3.22.4" }, "devDependencies": { @@ -90,12 +82,9 @@ "eslint-plugin-react": "^7.33.2", "jsdom": "^24.1.0", "lint-staged": "^15.4.3", - "postcss-import": "^16.1.1", "prettier": "3.2.5", "prisma": "^6.7.0", "simple-git-hooks": "^2.9.0", - "tree-sitter-cli": "^0.26.3", - "tree-sitter-python": "^0.25.0", "typescript": "^5.4.5", "vitest": "^3.0.8" }, @@ -394,6 +383,15 @@ "node": ">=0.8.0" } }, + "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": { + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", + "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@azure/msal-common": { "version": "15.13.0", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", @@ -445,6 +443,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -833,6 +832,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -856,6 +856,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -893,115 +894,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/@emotion/babel-plugin/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/css": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", - "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", - "license": "MIT", - "dependencies": { - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", @@ -2206,7 +2098,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2480,6 +2371,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2530,6 +2422,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", @@ -3212,6 +3105,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz", "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -3383,37 +3277,6 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", - "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -3437,36 +3300,6 @@ } } }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -5242,73 +5075,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/@shikijs/core": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", - "integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.21.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.5" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz", - "integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.21.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz", - "integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.21.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@shikijs/langs": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz", - "integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.21.0" - } - }, - "node_modules/@shikijs/themes": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz", - "integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.21.0" - } - }, - "node_modules/@shikijs/types": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz", - "integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT" - }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -5418,8 +5184,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5507,7 +5272,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5518,7 +5282,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -5552,8 +5315,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -5602,6 +5364,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5615,12 +5378,6 @@ "@types/node": "*" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, "node_modules/@types/pg": { "version": "8.15.5", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", @@ -5658,6 +5415,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5668,6 +5426,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5696,6 +5455,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5747,6 +5513,7 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -6398,7 +6165,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -6408,29 +6174,25 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6441,15 +6203,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6462,7 +6222,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -6472,7 +6231,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -6481,15 +6239,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6506,7 +6262,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -6520,7 +6275,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6533,7 +6287,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6548,7 +6301,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -6558,21 +6310,20 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6594,7 +6345,6 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -6643,7 +6393,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -6661,7 +6410,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6677,8 +6425,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ansi-escapes": { "version": "7.1.1", @@ -6782,7 +6529,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -7065,21 +6811,6 @@ "node": ">= 0.4" } }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -7163,6 +6894,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7187,8 +6919,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/builtin-modules": { "version": "3.3.0", @@ -7330,6 +7061,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7449,6 +7181,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -7487,7 +7220,6 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -7520,12 +7252,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -7574,22 +7300,6 @@ "node": ">=6" } }, - "node_modules/cmdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.2" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -7692,31 +7402,6 @@ "node": ">= 0.6" } }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8071,9 +7756,9 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -8103,15 +7788,13 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -8195,7 +7878,6 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -8230,15 +7912,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -8473,6 +8146,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8488,6 +8162,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8711,6 +8386,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9201,7 +8877,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -9340,8 +9015,7 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/fastq": { "version": "1.19.1", @@ -9413,12 +9087,6 @@ "node": ">=8" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9826,8 +9494,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/minimatch": { "version": "8.0.4", @@ -10005,29 +9672,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -10132,16 +9776,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -10217,6 +9851,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -10333,12 +9968,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -10930,7 +10559,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -10945,7 +10573,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10962,6 +10589,7 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -11298,7 +10926,6 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" }, @@ -11533,7 +11160,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11860,12 +11486,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "license": "MIT" - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -12561,7 +12181,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", - "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -12572,7 +12191,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -12657,14 +12275,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/next": { "version": "14.2.35", "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", @@ -12748,16 +12366,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -12807,18 +12415,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "dev": true, - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-releases": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", @@ -13102,23 +12698,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", - "license": "MIT" - }, - "node_modules/oniguruma-to-es": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", - "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", - "license": "MIT", - "dependencies": { - "oniguruma-parser": "^0.12.1", - "regex": "^6.0.1", - "regex-recursion": "^6.0.2" - } - }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -13252,6 +12831,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -13285,24 +12865,6 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -13381,15 +12943,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -13419,6 +12972,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -13593,6 +13147,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13603,10 +13158,9 @@ } }, "node_modules/postcss-import": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.1.tgz", - "integrity": "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==", - "dev": true, + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -13614,7 +13168,7 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -13793,9 +13347,9 @@ } }, "node_modules/preact": { - "version": "10.28.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", - "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", + "version": "10.27.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "license": "MIT", "funding": { "type": "opencollective", @@ -13834,7 +13388,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -13850,7 +13403,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -13863,8 +13415,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prism-react-renderer": { "version": "2.4.1", @@ -13886,6 +13437,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" @@ -13927,6 +13479,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -13991,9 +13544,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -14037,7 +13590,6 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -14058,6 +13610,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -14075,40 +13628,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-diff-viewer-continued": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz", - "integrity": "sha512-kMZmUyb3Pv5L9vUtCfIGYsdOHs8mUojblGy1U1Sm0D7FhAOEsH9QhnngEIRo5hXWIPNGupNRJls1TJ6Eqx84eg==", - "license": "MIT", - "dependencies": { - "@emotion/css": "^11.11.2", - "classnames": "^2.3.2", - "diff": "^5.1.0", - "memoize-one": "^6.0.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">= 8" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-diff-viewer-continued/node_modules/diff": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", - "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -14122,6 +13647,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14387,30 +13913,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", - "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-recursion": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -14503,7 +14005,6 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14550,6 +14051,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -14683,6 +14185,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -14870,7 +14373,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -14907,7 +14409,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -14919,8 +14420,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "7.7.3", @@ -14939,7 +14439,6 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -15056,22 +14555,6 @@ "node": ">=8" } }, - "node_modules/shiki": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", - "integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==", - "license": "MIT", - "dependencies": { - "@shikijs/core": "3.21.0", - "@shikijs/engine-javascript": "3.21.0", - "@shikijs/engine-oniguruma": "3.21.0", - "@shikijs/langs": "3.21.0", - "@shikijs/themes": "3.21.0", - "@shikijs/types": "3.21.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, "node_modules/shimmer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", @@ -15225,7 +14708,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -15244,7 +14726,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15702,12 +15183,6 @@ } } }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -15949,23 +15424,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tailwindcss/node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, "node_modules/tailwindcss/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -15983,7 +15441,6 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" }, @@ -15997,7 +15454,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -16016,7 +15472,6 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -16050,8 +15505,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/text-table": { "version": "0.2.0", @@ -16183,40 +15637,6 @@ "node": ">=18" } }, - "node_modules/tree-sitter-cli": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.26.3.tgz", - "integrity": "sha512-1VHpmjnTsYJk03HDqzLGn9dmJaLsJ7YeGsnnSudC6XOZu5oasz0GEVOIVCTe6hA01YZJgHd1XGO6XJZe0Sj7tw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "tree-sitter": "cli.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/tree-sitter-python": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz", - "integrity": "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.5.0", - "node-gyp-build": "^4.8.4" - }, - "peerDependencies": { - "tree-sitter": "^0.25.0" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -16398,6 +15818,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16750,19 +16171,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/vaul": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", - "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-dialog": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -16797,6 +16205,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16987,7 +16396,6 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16996,12 +16404,6 @@ "node": ">=10.13.0" } }, - "node_modules/web-tree-sitter": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.3.tgz", - "integrity": "sha512-JIVgIKFS1w6lejxSntCtsS/QsE/ecTS00en809cMxMPxaor6MvUnQ+ovG8uTTTvQCFosSh4MeDdI5bSGw5SoBw==", - "license": "MIT" - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -17017,7 +16419,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -17081,7 +16482,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -17095,7 +16495,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -17457,6 +16856,7 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "devOptional": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/js/cf-webapp/package.json b/js/cf-webapp/package.json index 52d96ca53..9fa2c2fdd 100644 --- a/js/cf-webapp/package.json +++ b/js/cf-webapp/package.json @@ -14,7 +14,6 @@ "prisma:generate": "npx prisma generate", "prisma:migrate": "npx prisma migrate dev", "prepare": "simple-git-hooks", - "postinstall": "cp node_modules/web-tree-sitter/web-tree-sitter.wasm public/ && npx tree-sitter build --wasm node_modules/tree-sitter-python -o public/tree-sitter-python.wasm", "format": "prettier --write \"**/*.{js,ts,tsx,json,md}\"", "format:check": "prettier --check \"**/*.{js,ts,tsx,json,md}\"" }, @@ -25,8 +24,6 @@ "@hookform/resolvers": "^3.3.2", "@monaco-editor/react": "^4.7.0", "@prisma/client": "^6.7.0", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -47,7 +44,6 @@ "chart.js": "^4.4.9", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", - "cmdk": "^1.1.1", "date-fns": "^4.1.0", "diff": "^8.0.2", "framer-motion": "^12.12.1", @@ -66,7 +62,6 @@ "prism-react-renderer": "^2.4.1", "react": "^18", "react-chartjs-2": "^5.3.0", - "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18", "react-hook-form": "^7.48.2", "react-markdown": "^9.0.1", @@ -74,13 +69,10 @@ "react-syntax-highlighter": "^16.1.0", "remark-gfm": "^4.0.0", "sharp": "^0.34.2", - "shiki": "^3.21.0", "sonner": "^2.0.6", "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", "tailwindcss-animate": "^1.0.7", - "vaul": "^1.1.2", - "web-tree-sitter": "^0.26.3", "zod": "^3.22.4" }, "devDependencies": { @@ -100,12 +92,9 @@ "eslint-plugin-react": "^7.33.2", "jsdom": "^24.1.0", "lint-staged": "^15.4.3", - "postcss-import": "^16.1.1", "prettier": "3.2.5", "prisma": "^6.7.0", "simple-git-hooks": "^2.9.0", - "tree-sitter-cli": "^0.26.3", - "tree-sitter-python": "^0.25.0", "typescript": "^5.4.5", "vitest": "^3.0.8" }, diff --git a/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx b/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx index 3d0af8bf7..ed8e454c5 100644 --- a/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx +++ b/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx @@ -3,7 +3,6 @@ import LogoBox from "@/components/dashboard/logo-box" import { useState, useEffect } from "react" import { useRouter, useSearchParams } from "next/navigation" -import Image from "next/image" import { Loading } from "@/components/ui/loading" import { authorizeOAuth, @@ -288,11 +287,9 @@ export default function CodeFlashAuthContent() { }`} > {userInfo?.avatarUrl ? ( - {userInfo.name} ) : ( @@ -330,7 +327,7 @@ export default function CodeFlashAuthContent() { }`} > {org.avatarUrl ? ( - {org.name} + {org.name} ) : (
    {getInitials(org.name)} diff --git a/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx b/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx index 80827fadb..d9097c2ab 100644 --- a/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx +++ b/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx @@ -22,7 +22,6 @@ import React from "react" import { generateToken } from "./tokenfuncs" import { Plus, User, Building2, Check } from "lucide-react" import { useViewMode } from "@/app/app/ViewModeContext" -import Image from "next/image" import { Select, SelectContent, @@ -176,11 +175,9 @@ export function CreateApiKeyDialog(): React.JSX.Element {
    {org.avatarUrl ? ( - {org.name} ) : ( diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx index f492e9502..90ed1d440 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx @@ -287,7 +287,6 @@ export default function OptimizationReviewPage() { } } loadEvent() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [params.traceId, currentOrg?.id]) const loadComments = async (eventId: string) => { @@ -604,9 +603,9 @@ export default function OptimizationReviewPage() { window.open(constructedUrl, "_blank") } }, 1000) - } catch (error: unknown) { + } catch (error: any) { console.error("[handleCreatePR] Exception:", error) - const errorMessage = error instanceof Error ? error.message : "Failed to create pull request" + const errorMessage = error?.message || "Failed to create pull request" toast.error(errorMessage, { duration: 5000, }) @@ -683,7 +682,7 @@ export default function OptimizationReviewPage() { )}

    {event.speedup_x && ( - + diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx index da6bf0316..c2b748a60 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx @@ -122,7 +122,6 @@ export default function LineProfilerPage() { } loadEvent() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [params.traceId, currentOrg?.id]) const handleBack = () => { @@ -205,7 +204,7 @@ export default function LineProfilerPage() { )} {event.speedup_x && ( - + diff --git a/js/cf-webapp/src/app/dashboard/page.tsx b/js/cf-webapp/src/app/dashboard/page.tsx index 609ebc659..399bec2ec 100644 --- a/js/cf-webapp/src/app/dashboard/page.tsx +++ b/js/cf-webapp/src/app/dashboard/page.tsx @@ -203,7 +203,6 @@ function Dashboard() { setLoading(false) fetchingRef.current = false } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedYear, currentOrgId]) useEffect(() => { diff --git a/js/cf-webapp/src/app/globals.css b/js/cf-webapp/src/app/globals.css index a790ed1e1..adb19e016 100644 --- a/js/cf-webapp/src/app/globals.css +++ b/js/cf-webapp/src/app/globals.css @@ -2,18 +2,84 @@ @tailwind components; @tailwind utilities; -/* Import the design token system */ -@import "../styles/tokens.css"; -@import "../styles/typography.css"; -@import "../styles/spacing.css"; - @layer base { - /* Light mode removed - dark mode only implementation */ + :root { + /* Background and foreground */ + --background: 0 0% 99%; + --foreground: 222.2 84% 4.9%; + + /* Card styles */ + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + /* Popover styles */ + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + /* Codeflash primary colors - converted from hex to HSL */ + --primary: 38 100% 63%; /* #d08e0d - Codeflash yellow */ + --primary-foreground: 0 6% 4%; + + /* Secondary colors - complementary to Codeflash yellow */ + --secondary: 41 88% 95%; /* Lighter version of primary */ + --secondary-foreground: 41 88% 20%; /* Darker version for contrast */ + + /* Accent colors - variation of the Codeflash yellow */ + --accent: 41 70% 90%; /* Softer version of primary */ + --accent-foreground: 41 88% 20%; + + /* Other UI colors aligned with brand */ + --muted: 41 20% 96%; + --muted-foreground: 41 8% 46%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 100%; + + --border: 41 30% 90%; + --input: 41 30% 90%; + --ring: 38 100% 63%; /* Matching primary - Codeflash yellow */ + + --radius: 0.5rem; + + /* Code highlighting */ + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + } .dark { - /* All color tokens are defined in tokens.css */ - /* The .dark class enables Tailwind's dark: variant */ - /* Since we're dark mode only, tokens are already set for dark mode */ + /* Background and foreground */ + --background: 0, 6%, 5%; + --foreground: 0 0% 100%; + + /* Card styles */ + --card: 0 3% 11%; + --card-foreground: 0 0% 100%; + + /* Popover styles */ + --popover: 222.2 84% 4.9%; + --popover-foreground: 0 0% 100%; + + /* Codeflash primary colors for dark mode */ + --primary: 38 100% 63%; /* #ffd227 - Codeflash yellow for dark mode */ + --primary-foreground: 222.2 47.4% 11.2%; + + /* Secondary colors - complementary to Codeflash yellow in dark mode */ + --secondary: 48 60% 25%; /* Darker version of primary */ + --secondary-foreground: 48 100% 80%; /* Lighter version for contrast */ + + /* Accent colors - variation of the Codeflash yellow */ + --accent: 48 70% 30%; /* Softer version of primary */ + --accent-foreground: 48 100% 80%; + + /* Other UI colors aligned with brand */ + --muted: 48 15% 20%; + --muted-foreground: 48 20% 65%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 100%; + + --border: 48 20% 25%; + --input: 48 20% 25%; + --ring: 38 100% 63%; /* Matching primary - Codeflash yellow */ /* Code highlighting */ --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); @@ -22,8 +88,8 @@ @layer base { ::selection { - background: rgb(113 113 122); /* zinc-500 - no brand colors */ - color: rgb(250 250 250); /* zinc-50 for contrast */ + background: #ffd227; /* CF brand color */ + color: #1f2937; /* Tailwind's gray-800 for selected text color */ } * { @@ -80,11 +146,11 @@ } .prose code { - background-color: rgb(var(--muted)); + background-color: hsl(var(--muted)); padding: 0.125em 0.25em; border-radius: 0.25em; font-size: 0.875em; - font-family: var(--font-mono); + font-family: ui-monospace, monospace; } .prose pre { @@ -98,10 +164,10 @@ } .prose blockquote { - border-left: 3px solid rgb(var(--border)); + border-left: 3px solid hsl(var(--border)); padding-left: 1em; margin-left: 0; - color: rgb(var(--muted-foreground)); + color: hsl(var(--muted-foreground)); } /* Fixed list styles to show bullets and numbers */ @@ -138,7 +204,7 @@ } .prose a { - color: rgb(var(--primary)); + color: hsl(var(--primary)); text-decoration: underline; } @@ -173,42 +239,10 @@ /* Dark mode adjustments */ .dark .prose code { - background-color: rgb(var(--muted)); + background-color: hsl(var(--muted)); } .dark .prose blockquote { - border-left-color: rgb(var(--border)); - color: rgb(var(--muted-foreground)); -} - -/* Typography utility classes for common patterns */ -@layer utilities { - /* Apply monospace font for inline code */ - .text-code { - font-family: var(--font-mono); - font-size: var(--text-sm); - } - - /* Apply monospace font with tight line height for data */ - .text-data { - font-family: var(--font-mono); - line-height: var(--leading-tight); - } - - /* Apply sans font with medium weight for UI labels */ - .text-label { - font-family: var(--font-sans); - font-weight: 500; - } - - /* Standard container padding using spacing tokens */ - .container-spacing { - padding-left: var(--space-4); - padding-right: var(--space-4); - } - - /* Standard card internal spacing */ - .card-spacing { - padding: var(--space-3); - } + border-left-color: hsl(var(--border)); + color: hsl(var(--muted-foreground)); } diff --git a/js/cf-webapp/src/app/layout.tsx b/js/cf-webapp/src/app/layout.tsx index b34ff8053..9281c9fa8 100644 --- a/js/cf-webapp/src/app/layout.tsx +++ b/js/cf-webapp/src/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next" -import { Inter as FontSans, JetBrains_Mono } from "next/font/google" +import { Inter as FontSans } from "next/font/google" import "./globals.css" import { cn } from "@/lib/utils" import { ThemeProvider } from "@/components/theme-provider" @@ -23,13 +23,6 @@ const fontSans = FontSans({ variable: "--font-sans", }) -const jetbrainsMono = JetBrains_Mono({ - subsets: ["latin"], - weight: ["300", "400", "500", "600", "700"], - variable: "--font-jetbrains-mono", - display: "swap", -}) - export const metadata: Metadata = { title: "Codeflash", description: "Optimize the performance of your code.", @@ -98,7 +91,7 @@ export default async function RootLayout({ }} /> - + - -
    {children}
    +
    + +
    {children}
    ) } diff --git a/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx b/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx new file mode 100644 index 000000000..ba0ad4d3e --- /dev/null +++ b/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx @@ -0,0 +1,524 @@ +import Link from "next/link" +import { Metadata } from "next" +import { notFound } from "next/navigation" +import { + CheckCircle, + XCircle, + Hash, + FileText, + Code, + AlertTriangle, +} from "lucide-react" +import { prisma } from "@/lib/prisma" +import { StatCard } from "@/components/observability/stat-card" +import { InfoIcon } from "@/components/observability/info-icon" +import { CopyButton } from "@/components/observability/copy-button" +import { ParsedResponseView } from "@/components/observability/parsed-response-view" + +interface LLMCallDetailPageProps { + params: { + id: string + } +} + +export async function generateMetadata({ params }: LLMCallDetailPageProps): Promise { + return { + title: `LLM Call ${params.id.substring(0, 8)} - Observability`, + description: "View LLM call details for prompt engineering analysis", + } +} + +export default async function LLMCallDetailPage({ params }: LLMCallDetailPageProps) { + // Fetch LLM call details + const llmCall = await prisma.llm_calls.findUnique({ + where: { id: params.id }, + }) + + if (!llmCall) { + notFound() + } + + // Fetch related errors + const relatedErrors = await prisma.optimization_errors.findMany({ + where: { llm_call_id: params.id }, + orderBy: { created_at: "desc" }, + }) + + return ( +
    + {/* Header */} +
    + {/* Breadcrumb */} +
    + + LLM Calls + + / + + {llmCall.id.substring(0, 8)}... + +
    + +

    + LLM Call Detail +

    + + {/* ID and Trace with Copy Buttons */} +
    +
    + Call ID: + + {llmCall.id} + + +
    + {llmCall.trace_id && ( +
    + Trace: + + {llmCall.trace_id} + + +
    + )} +
    +
    + + {/* Summary Cards */} +
    + + + + +
    + + {/* Metadata */} +
    +

    Metadata

    +
    +
    +
    +
    + + Call Type + + +
    + + {llmCall.call_type} + +
    +
    +
    +
    +
    + Model + +
    + + {llmCall.model_name} + +
    +
    +
    +
    +
    + + Temperature + + +
    + + {llmCall.temperature || "default"} + +
    +
    +
    +
    +
    + + Candidates Requested + + +
    + + {llmCall.n_candidates || "N/A"} + +
    +
    +
    +
    +
    + + Created + + +
    + + {new Date(llmCall.created_at).toLocaleString()} + +
    +
    +
    +
    +
    + + Parsing Status + + +
    + + {llmCall.parsing_status || "N/A"} + +
    +
    +
    +
    + + {/* Token Breakdown */} + {llmCall.prompt_tokens && ( +
    +
    + +

    Token Usage

    +
    + + {/* Visual Token Ratio Bar */} +
    +
    + + Token Distribution + + +
    +
    + {(() => { + const promptTokens = llmCall.prompt_tokens ?? 0 + const completionTokens = llmCall.completion_tokens ?? 0 + const totalTokens = llmCall.total_tokens ?? (promptTokens + completionTokens) + // Use 1 as fallback only for division to prevent division by zero + const safeTotalTokens = totalTokens || 1 + const promptPercent = Math.round((promptTokens / safeTotalTokens) * 100) + const completionPercent = Math.round((completionTokens / safeTotalTokens) * 100) + + return ( + <> +
    + {promptPercent > 0 && {promptPercent}%} +
    +
    + {completionPercent > 0 && {completionPercent}%} +
    + + ) + })()} +
    +
    + +
    + {(() => { + const promptTokens = llmCall.prompt_tokens ?? 0 + const completionTokens = llmCall.completion_tokens ?? 0 + const totalTokens = llmCall.total_tokens ?? (promptTokens + completionTokens) + + return ( + <> +
    +
    +
    + + Prompt Tokens + +
    +
    + {promptTokens.toLocaleString()} +
    +
    +
    +
    +
    + + Completion Tokens + +
    +
    + {completionTokens.toLocaleString()} +
    +
    +
    +
    + + Total Tokens + +
    +
    + {totalTokens.toLocaleString()} +
    +
    + + ) + })()} +
    +
    + )} + + {/* Parsing Results */} + {llmCall.candidates_generated !== null && ( +
    +
    + +

    Parsing Results

    + +
    +
    +
    +
    + + Candidates Generated + +
    +
    + {llmCall.candidates_generated} +
    +
    +
    +
    + + + Candidates Valid + +
    +
    + {llmCall.candidates_valid} +
    +
    +
    + {llmCall.parsing_errors && ( +
    +
    + +
    + Parsing Errors +
    +
    +
    +                {JSON.stringify(llmCall.parsing_errors, null, 2)}
    +              
    +
    + )} +
    + )} + + {/* Prompts */} +
    +
    +
    + +

    System Prompt

    + +
    +
    + + {llmCall.system_prompt?.length.toLocaleString() || 0} characters + + +
    +
    +
    +          {llmCall.system_prompt}
    +        
    +
    + +
    +
    +
    + +

    User Prompt

    + +
    +
    + + {llmCall.user_prompt?.length.toLocaleString() || 0} characters + + +
    +
    +
    +          {llmCall.user_prompt}
    +        
    +
    + + {/* Response — parsed by call type (ranking: rank/explain; optimization: code blocks + text), with View raw */} + {llmCall.raw_response && ( +
    +
    +
    + +

    LLM Response

    + +
    +
    + + {llmCall.raw_response.length.toLocaleString()} characters + + +
    +
    + +
    + )} + + {/* Error Information */} + {llmCall.status === "failed" && llmCall.error_message && ( +
    +
    + +

    + Error Information +

    +
    +
    +
    + Error Type: + + {llmCall.error_type} + +
    +
    +
    +
    + Error Message: + +
    +
    +              {llmCall.error_message}
    +            
    +
    +
    + )} + + {/* Related Errors */} + {relatedErrors.length > 0 && ( +
    +
    + +

    Related Errors

    + + {relatedErrors.length} + +
    +
    + {relatedErrors.map(error => ( +
    +
    + + {error.severity} + + + {error.error_type} + + + {new Date(error.created_at).toLocaleString()} + +
    +
    + {error.error_message} +
    + {error.context && ( +
    + + View context + +
    +                      {JSON.stringify(error.context, null, 2)}
    +                    
    +
    + )} +
    + ))} +
    +
    + )} + {/* Actions */} +
    + + View Full Trace → + + + ← Back to List + +
    +
    + ) +} diff --git a/js/cf-webapp/src/app/observability/llm-calls/page.tsx b/js/cf-webapp/src/app/observability/llm-calls/page.tsx new file mode 100644 index 000000000..966d7b5dd --- /dev/null +++ b/js/cf-webapp/src/app/observability/llm-calls/page.tsx @@ -0,0 +1,800 @@ +import Link from "next/link" +import { Metadata } from "next" +import { unstable_cache } from "next/cache" +import { Award, Database as DatabaseIcon, Github, Terminal } from "lucide-react" +import { prisma } from "@/lib/prisma" +import { getCallSource } from "@/lib/observability-utils" +import { HelpButton } from "@/components/observability/help-button" +import { StatCard } from "@/components/observability/stat-card" +import { ColumnHeader } from "@/components/observability/column-header" +import { InfoIcon } from "@/components/observability/info-icon" + +export const metadata: Metadata = { + title: "LLM Calls - Observability", + description: "View all LLM API calls for prompt engineering analysis", +} + +interface SearchParams { + call_type?: string + model?: string + status?: string + trace_id?: string + page?: string + organization?: string +} + +// Cached function to get unique organizations list +// Revalidates every 5 minutes - organizations change infrequently +const getUniqueOrganizations = unstable_cache( + async () => { + const allOrganizations = await prisma.optimization_features.findMany({ + select: { organization: true }, + distinct: ["organization"], + where: { organization: { not: null } }, + }) + return allOrganizations + .map(f => f.organization) + .filter(Boolean) + .sort() as string[] + }, + ["unique-organizations"], + { revalidate: 300 }, // 5 minutes +) + +// Cached function to get unique call types +// Revalidates every 5 minutes - call types change infrequently +const getCallTypes = unstable_cache( + async () => { + const callTypes = await prisma.llm_calls.findMany({ + select: { call_type: true }, + distinct: ["call_type"], + }) + return callTypes.filter(ct => ct.call_type !== null) + }, + ["call-types"], + { revalidate: 300 }, // 5 minutes +) + +// Cached function to get unique model names +// Revalidates every 5 minutes - models change infrequently +const getModels = unstable_cache( + async () => { + const models = await prisma.llm_calls.findMany({ + select: { model_name: true }, + distinct: ["model_name"], + }) + return models.filter(m => m.model_name !== null) + }, + ["model-names"], + { revalidate: 300 }, // 5 minutes +) + +export default async function LLMCallsPage({ searchParams }: { searchParams: SearchParams }) { + try { + const page = parseInt(searchParams.page || "1") + const pageSize = 50 + const skip = (page - 1) * pageSize + + // Build where clause based on filters + type WhereClause = { + call_type?: string + model_name?: { contains: string } + status?: string + trace_id?: { startsWith: string } | { in: string[] } | { contains: string } + OR?: Array<{ trace_id: { startsWith: string } }> + } + + const where: WhereClause = {} + + if (searchParams.call_type) { + where.call_type = searchParams.call_type + } + if (searchParams.model) { + where.model_name = { contains: searchParams.model } + } + if (searchParams.status) { + where.status = searchParams.status + } + if (searchParams.trace_id) { + // Use startsWith for prefix matching to find multi-model related calls + where.trace_id = { startsWith: searchParams.trace_id } + } + + // Get unique organizations for filter dropdown (cached) + const uniqueOrganizations = await getUniqueOrganizations() + + // If organization filter is specified, get matching trace_ids + let filteredTraceIds: string[] = [] + if (searchParams.organization) { + const orgFeatures = await prisma.optimization_features.findMany({ + where: { organization: searchParams.organization }, + select: { trace_id: true }, + distinct: ["trace_id"], + }) + filteredTraceIds = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] + + // If organization filter is applied but no traces found, return empty result early + if (filteredTraceIds.length === 0) { + // Get unique call types and models for filters (cached) + const [callTypes, models] = await Promise.all([ + getCallTypes(), + getModels(), + ]) + + // Return early with empty results + return ( +
    +
    + {/* Title and Search Bar on Same Line */} +
    +

    + LLM Calls +

    + {/* Compact Search Bar */} +
    + + + {searchParams.trace_id && ( + + Clear + + )} +
    +
    +

    + Track and analyze all LLM API calls for prompt engineering +

    +
    + + {/* Filters */} +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    + +
    +

    + No LLM calls found for organization "{searchParams.organization}". +

    +
    +
    + ) + } + } + + // Apply organization filter using IN clause for exact trace ID matches + // NOTE: Filtering happens at DB level BEFORE pagination, not client-side. + // We use IN clause because there's no Prisma relation between llm_calls and optimization_features + // (they're only related by trace_id as a string field, not a foreign key relation) + if (filteredTraceIds.length > 0 && !searchParams.trace_id) { + // Use IN clause for exact trace ID matches - much more efficient than OR with startsWith + // For very large organizations (>10k traces), consider chunking the array + where.trace_id = { in: filteredTraceIds } + } + + // Fetch LLM calls with pagination, aggregate stats + const [llmCalls, totalCount, aggregateStats, successCount] = await Promise.all([ + prisma.llm_calls.findMany({ + where, + orderBy: { created_at: "desc" }, + take: pageSize, + skip, + select: { + id: true, + trace_id: true, + call_type: true, + model_name: true, + status: true, + parsing_status: true, + candidates_generated: true, + candidates_valid: true, + prompt_tokens: true, + completion_tokens: true, + llm_cost: true, + latency_ms: true, + created_at: true, + error_message: true, + context: true, + }, + }), + prisma.llm_calls.count({ where }), + // Get aggregate stats for all filtered data (not just current page) + prisma.llm_calls.aggregate({ + where, + _sum: { + llm_cost: true, + latency_ms: true, + }, + _avg: { + latency_ms: true, + }, + _count: { + status: true, + }, + }), + // Get success count for success rate calculation + prisma.llm_calls.count({ + where: { + ...where, + status: "success", + }, + }), + ]) + + // Fetch optimization_features and optimization_events for the trace_ids we got + const traceIds = llmCalls.map(call => call.trace_id).filter(Boolean) as string[] + const uniqueTraceIdPrefixes = Array.from( + new Set(traceIds.map(id => id.substring(0, 36))), // Get base UUID (first 36 chars) + ) + + // NOTE: Using Promise.all for parallel fetching, not N+1 queries. + // Both queries execute simultaneously for the same set of trace_ids. + const [optimizationFeatures, optimizationEvents] = await Promise.all([ + uniqueTraceIdPrefixes.length > 0 + ? prisma.optimization_features.findMany({ + where: { trace_id: { in: uniqueTraceIdPrefixes } }, + select: { + trace_id: true, + organization: true, + ranking: true, + }, + }) + : [], + uniqueTraceIdPrefixes.length > 0 + ? prisma.optimization_events.findMany({ + where: { trace_id: { in: uniqueTraceIdPrefixes } }, + select: { + trace_id: true, + event_type: true, + }, + distinct: ["trace_id"], + }) + : [], + ]) + + // Create maps for trace_id to event_type and organization + const traceIdToEventType = new Map() + optimizationEvents.forEach(event => { + if (event.trace_id) { + traceIdToEventType.set(event.trace_id, event.event_type) + } + }) + + const traceIdToOrganization = new Map() + optimizationFeatures.forEach(feature => { + if (feature.trace_id && feature.organization) { + traceIdToOrganization.set(feature.trace_id, feature.organization) + } + }) + + // Trace IDs that have a chosen best candidate (ranking.ranking[0] present) + const traceIdsWithBest = new Set( + optimizationFeatures + .filter( + f => + f.trace_id && + (f.ranking as { ranking?: string[] } | null)?.ranking?.[0], + ) + .map(f => f.trace_id.substring(0, 36)), + ) + + // Get unique call types and models for filters + const [callTypes, models] = await Promise.all([ + prisma.llm_calls.findMany({ + select: { call_type: true }, + distinct: ["call_type"], + }), + prisma.llm_calls.findMany({ + select: { model_name: true }, + distinct: ["model_name"], + }), + ]) + + const totalPages = Math.ceil(totalCount / pageSize) + + return ( +
    +
    + {/* Title, Search Bar, and Help Button */} +
    +
    +

    + LLM Calls +

    + +
    +

    What is an LLM call?

    +

    Each individual API request to an AI model (like GPT-4, Claude, etc.) for generating code optimizations, validations, or other tasks.

    +
    +
    +

    Call sequence (#)

    +

    Shows the order of calls within a trace. Multiple calls may be part of the same optimization request.

    +
    +
    +

    Call types

    +
      +
    • optimization: Generates optimized code
    • +
    • validation: Checks quality of generated code
    • +
    • line_profiler: Analyzes performance bottlenecks
    • +
    +
    +
    +

    Parsing status

    +

    Indicates whether the model's response was successfully parsed and extracted into usable code candidates.

    +
    +
    + } + /> +
    + {/* Compact Search Bar */} +
    + + + {searchParams.trace_id && ( + + Clear + + )} +
    +
    +

    + Track and analyze all LLM API calls for prompt engineering +

    +
    + + {/* Filters */} +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + {(searchParams.call_type || searchParams.model || searchParams.status || searchParams.organization) && ( + + Clear All + + )} +
    +
    +
    + + {/* Stats Summary */} +
    + + 0 ? Math.round((successCount / totalCount) * 100) : 0}%`} + helpText="Percentage of successful calls vs total calls. Excludes in-progress calls." + icon="CheckCircle2" + variant="success" + /> + + +
    + + {/* LLM Calls Table */} +
    +
    + + + + + + + + + + + + + + + + + + + {llmCalls.map(call => { + const ctx = call.context as { call_sequence?: number } | null + const callSequence = ctx?.call_sequence + const traceIdPrefix = call.trace_id?.substring(0, 36) || "" + const eventType = traceIdPrefix ? traceIdToEventType.get(traceIdPrefix) || null : null + const organization = traceIdPrefix ? traceIdToOrganization.get(traceIdPrefix) : null + const source = getCallSource(eventType, call.context as Record | null) + const isBestOptimizationCall = + call.call_type === "optimization" && traceIdsWithBest.has(traceIdPrefix) + return ( + + + + + + + + + + + + + + + ) + })} + +
    + {callSequence ? `#${callSequence}` : "-"} + + + {new Date(call.created_at).toLocaleString()} + + + {call.trace_id && call.trace_id.trim() ? ( + + {call.trace_id.substring(0, 8)}... + + ) : ( + N/A + )} + + {organization || "N/A"} + + + + {call.call_type} + + {isBestOptimizationCall && ( + + + Best + + )} + + + + {source.toLowerCase().includes("github") ? ( + + ) : source.toLowerCase().includes("vscode") || source.toLowerCase().includes("cli") ? ( + + ) : null} + {source} + + + {call.model_name} + + + {call.status} + + + {call.prompt_tokens && call.completion_tokens + ? `${call.prompt_tokens + call.completion_tokens}` + : "-"} + + {call.llm_cost ? `$${call.llm_cost.toFixed(4)}` : "-"} + + {call.latency_ms ? `${call.latency_ms}ms` : "-"} + + {call.candidates_valid}/{call.candidates_generated || 0} +
    +
    + + {llmCalls.length === 0 && ( +
    + +

    + No LLM Calls Found +

    + {searchParams.call_type || searchParams.model || searchParams.status || searchParams.organization || searchParams.trace_id ? ( +
    +

    + Try adjusting your filters above +

    + + Clear All Filters + +
    + ) : ( +
    +

    + Run the test script to generate sample data +

    + + python django/aiservice/test_observability_local.py + +
    + )} +
    + )} +
    + + {/* Pagination */} + {totalPages > 1 && ( +
    + {page > 1 && ( + + Previous + + )} + + Page {page} of {totalPages} + + {page < totalPages && ( + + Next + + )} +
    + )} +
    + ) + } catch (error) { + throw error + } +} diff --git a/js/cf-webapp/src/app/observability/loading.tsx b/js/cf-webapp/src/app/observability/loading.tsx deleted file mode 100644 index 3c86324ef..000000000 --- a/js/cf-webapp/src/app/observability/loading.tsx +++ /dev/null @@ -1,39 +0,0 @@ -export default function ObservabilityLoading() { - return ( -
    - {/* Search Section Skeleton */} -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - {/* Content Skeleton */} -
    - {/* Progress skeleton */} -
    -
    -
    -
    - - {/* Timeline items skeleton */} -
    - {[1, 2, 3].map((i) => ( -
    -
    -
    -
    -
    -
    -
    - ))} -
    -
    -
    - ) -} diff --git a/js/cf-webapp/src/app/observability/page.tsx b/js/cf-webapp/src/app/observability/page.tsx deleted file mode 100644 index e15e6c3ae..000000000 --- a/js/cf-webapp/src/app/observability/page.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import { Suspense } from "react" -import { unstable_cache } from "next/cache" -import { Search } from "lucide-react" -import { prisma } from "@/lib/prisma" -import { TraceSearch } from "@/components/observability/trace-search" -import { TimelinePageView } from "@/components/observability/timeline-page-view" -import { transformToTimelineSections } from "@/components/observability/timeline-types" -import { ErrorsSection } from "@/components/observability/errors-section" -import { FunctionToOptimizeSection } from "@/components/observability/function-to-optimize-section" -import { CodeContextSection } from "@/components/observability/code-context-section" - -export const revalidate = 60 - -interface Observability2PageProps { - searchParams: Promise<{ - trace_id?: string - }> -} - -const getTraceData = unstable_cache( - async (tracePrefix: string) => { - const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([ - prisma.llm_calls.findMany({ - where: { trace_id: { startsWith: tracePrefix } }, - orderBy: { created_at: "asc" }, - }), - prisma.optimization_errors.findMany({ - where: { trace_id: { startsWith: tracePrefix } }, - orderBy: { created_at: "asc" }, - }), - prisma.optimization_features.findFirst({ - where: { trace_id: { startsWith: tracePrefix } }, - }), - prisma.optimization_events.findFirst({ - where: { trace_id: { startsWith: tracePrefix } }, - select: { event_type: true, function_name: true, file_path: true }, - }), - ]) - return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } - }, - ["observability-trace-detail"], - { revalidate: 60 }, -) - -export default async function Observability2Page({ searchParams }: Observability2PageProps) { - const params = await searchParams - const traceId = params.trace_id?.trim() - - let traceData: Awaited> | null = null - if (traceId) { - const tracePrefix = traceId.substring(0, 33) - traceData = await getTraceData(tracePrefix) - } - - const hasResults = traceData - ? traceData.rawLlmCalls.length > 0 || traceData.errors.length > 0 - : false - - return ( -
    -
    -
    - }> - - -
    -
    - - {traceId && traceData ? ( - }> - - - ) : traceId ? ( - - ) : ( - - )} -
    - ) -} - -interface TraceData { - rawLlmCalls: Awaited>["rawLlmCalls"] - errors: Awaited>["errors"] - optimizationFeatures: Awaited>["optimizationFeatures"] - optimizationEvent: Awaited>["optimizationEvent"] -} - -function TraceContent({ traceId, traceData }: { traceId: string; traceData: TraceData }) { - const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = traceData - - if (rawLlmCalls.length === 0 && errors.length === 0) { - return - } - - const optimizationsOrigin = - (optimizationFeatures?.optimizations_origin as Record< - string, - { source: string; model?: string; call_sequence?: number; parent?: string } - >) || {} - - const candidateExplanations = - (optimizationFeatures?.explanations_post as Record) || {} - - const allCandidates = optimizationFeatures?.optimizations_post - ? Object.entries(optimizationFeatures.optimizations_post as Record).map( - ([id, code]) => ({ - id, - code: typeof code === "string" ? code : "", - source: optimizationsOrigin[id]?.source || "OPTIMIZE", - model: optimizationsOrigin[id]?.model, - callSequence: optimizationsOrigin[id]?.call_sequence, - explanation: candidateExplanations[id], - }), - ) - : [] - - const optimizationCandidates = allCandidates - .filter(c => c.source === "OPTIMIZE") - .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) - .map((c, index) => ({ ...c, index: index + 1 })) - - const lineProfilerCandidates = allCandidates - .filter(c => c.source === "OPTIMIZE_LP") - .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) - .map((c, index) => ({ ...c, index: index + 1 })) - - const refinementCandidates = allCandidates - .filter(c => c.source === "REFINE") - .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) - .map((c, index) => ({ - ...c, - index: index + 1, - parentId: optimizationsOrigin[c.id]?.parent || null, - })) - - const rankingData = optimizationFeatures?.ranking as - | { ranking?: string[]; explanation?: string } - | null - const bestCandidateId = rankingData?.ranking?.[0] ?? null - - const pullRequestRaw = optimizationFeatures?.pull_request - const usedForPr = Boolean( - pullRequestRaw != null && - typeof pullRequestRaw === "object" && - !Array.isArray(pullRequestRaw) && - Object.keys(pullRequestRaw as Record).length > 0, - ) - - const candidateRankMap: Record = {} - if (rankingData?.ranking) { - rankingData.ranking.forEach((id, index) => { - candidateRankMap[id] = index + 1 - }) - } - - const generatedTests = (optimizationFeatures?.generated_test ?? []).map((code, index) => ({ - code, - index: index + 1, - })) - - const instrumentedTests = (optimizationFeatures?.instrumented_generated_test ?? []).map((code, index) => ({ - code, - index: index + 1, - })) - - const instrumentedPerfTests = ((optimizationFeatures as Record)?.instrumented_perf_test as string[] ?? []).map((code, index) => ({ - code, - index: index + 1, - })) - - const llmCalls = rawLlmCalls.sort((a, b) => { - const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity - const seqB = (b.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity - if (seqA !== seqB) return seqA - seqB - return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() - }) - - const transformedCalls = llmCalls.map(call => ({ - id: call.id, - call_type: call.call_type, - model_name: call.model_name, - status: call.status, - latency_ms: call.latency_ms, - llm_cost: call.llm_cost, - total_tokens: call.total_tokens, - created_at: call.created_at, - context: call.context as { call_sequence?: number } | null, - })) - - const { sections, totalDuration } = transformToTimelineSections({ - calls: transformedCalls, - optimizationCandidates, - lineProfilerCandidates, - refinementCandidates, - generatedTests, - instrumentedTests, - instrumentedPerfTests, - originalCode: optimizationFeatures?.original_code ?? null, - testFramework: optimizationFeatures?.test_framework ?? null, - candidateRankMap, - bestCandidateId, - rankingExplanation: rankingData?.explanation ?? null, - usedForPr, - }) - - const transformedErrors = errors.map(error => ({ - id: error.id, - error_type: error.error_type, - severity: error.severity, - error_message: error.error_message, - context: error.context as { - test_name?: string - failure_reason?: string - test_output?: string - expected?: string - actual?: string - } | null, - created_at: error.created_at, - })) - - const functionName = (optimizationFeatures?.metadata as Record)?.function_to_optimize as string ?? optimizationEvent?.function_name ?? null - const filePath = optimizationEvent?.file_path ?? null - const originalCode = optimizationFeatures?.original_code ?? null - const dependencyCode = optimizationFeatures?.dependency_code ?? null - - return ( -
    -
    - - -
    - - - - {transformedErrors.length > 0 && ( -
    - -
    - )} -
    - ) -} - -function EmptyState() { - return ( -
    -
    - -
    -

    - Enter a Trace ID to Get Started -

    -

    - Paste or type a trace ID in the search box above to view the complete optimization timeline, - including all LLM calls, generated candidates, and any errors. -

    -
    - ) -} - -function NotFoundState({ traceId }: { traceId: string }) { - return ( -
    -
    - -
    -

    Trace Not Found

    -

    - No data was found for the trace ID: -

    - - {traceId} - -

    - Please check that the trace ID is correct and try again. -

    -
    - ) -} - -function SearchSkeleton() { - return ( -
    -
    -
    -
    -
    -
    - ) -} - -function TraceContentSkeleton() { - return ( -
    -
    -
    -
    -
    - -
    - {[1, 2, 3].map((i) => ( -
    -
    -
    -
    -
    -
    -
    - ))} -
    -
    - ) -} diff --git a/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx b/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx new file mode 100644 index 000000000..3f9ec4448 --- /dev/null +++ b/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx @@ -0,0 +1,767 @@ +import Link from "next/link" +import { notFound } from "next/navigation" +import { unstable_cache } from "next/cache" +import { + CheckCircle, + Timer, + DollarSign, + Hash, + Code as CodeIcon, + Github, + Terminal, + AlertCircle, + XCircle, +} from "lucide-react" +import { prisma } from "@/lib/prisma" +import { getCallSource } from "@/lib/observability-utils" +import { HelpButton } from "@/components/observability/help-button" +import { InfoIcon } from "@/components/observability/info-icon" +import { CopyButton } from "@/components/observability/copy-button" + +export const revalidate = 60 + +interface TracePageProps { + params: { + trace_id: string + } +} + +const getTraceData = unstable_cache( + async (tracePrefix: string) => { + const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([ + prisma.llm_calls.findMany({ + where: { trace_id: { startsWith: tracePrefix } }, + orderBy: { created_at: "asc" }, + }), + prisma.optimization_errors.findMany({ + where: { trace_id: { startsWith: tracePrefix } }, + orderBy: { created_at: "asc" }, + }), + prisma.optimization_features.findFirst({ + where: { trace_id: { startsWith: tracePrefix } }, + }), + prisma.optimization_events.findFirst({ + where: { trace_id: { startsWith: tracePrefix } }, + select: { event_type: true }, + }), + ]) + return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } + }, + ["trace-detail"], + { revalidate: 60 }, +) + +export default async function TracePage({ params }: TracePageProps) { + const { trace_id } = params + + // Use prefix matching (first 33 chars) to group multi-model calls that share the same base trace_id + const tracePrefix = trace_id.substring(0, 33) + const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = await getTraceData(tracePrefix) + + const traceSource = getCallSource(optimizationEvent?.event_type || null, null) + + // Extract optimization candidates from optimization_features + // Also get the origin of each candidate (OPTIMIZE vs OPTIMIZE_LP) and model info + const optimizationsOrigin = + (optimizationFeatures?.optimizations_origin as Record< + string, + { source: string; model?: string } + >) || {} + + const allCandidates = optimizationFeatures?.optimizations_post + ? Object.entries(optimizationFeatures.optimizations_post as Record).map( + ([id, code]) => ({ + id, + code: typeof code === "string" ? code : "", + source: optimizationsOrigin[id]?.source || "OPTIMIZE", + model: optimizationsOrigin[id]?.model, + }), + ) + : [] + + // Filter candidates by source for display under the correct section + const optimizationCandidates = allCandidates + .filter(c => c.source === "OPTIMIZE") + .map((c, index) => ({ ...c, index: index + 1 })) + + const lineProfilerCandidates = allCandidates + .filter(c => c.source === "OPTIMIZE_LP") + .map((c, index) => ({ ...c, index: index + 1 })) + + // Get explanations for candidates if available + const candidateExplanations = + (optimizationFeatures?.explanations_post as Record) || {} + + // Best candidate (first in ranking) and whether it was used for PR + const rankingData = optimizationFeatures?.ranking as + | { ranking?: string[]; explanation?: string } + | null + const bestCandidateId = rankingData?.ranking?.[0] ?? null + const pullRequestRaw = optimizationFeatures?.pull_request + const usedForPr = Boolean( + pullRequestRaw != null && + typeof pullRequestRaw === "object" && + !Array.isArray(pullRequestRaw) && + Object.keys(pullRequestRaw as Record).length > 0, + ) + + // Map candidate ID to rank position (1-based, 1 = best) + const candidateRankMap = new Map() + if (rankingData?.ranking) { + rankingData.ranking.forEach((id, index) => { + candidateRankMap.set(id, index + 1) + }) + } + + // Sort by call_sequence from context if available, otherwise by created_at + const llmCalls = rawLlmCalls.sort((a, b) => { + const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity + const seqB = (b.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity + if (seqA !== seqB) return seqA - seqB + return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + }) + + // If no data found, show 404 + if (llmCalls.length === 0 && errors.length === 0) { + notFound() + } + + // Calculate summary metrics + const totalCost = llmCalls.reduce((sum, call) => sum + (call.llm_cost ?? 0), 0) + const totalTokens = llmCalls.reduce((sum, call) => sum + (call.total_tokens ?? 0), 0) + const failedCalls = llmCalls.filter(c => c.status === "failed").length + + // Calculate timeline data using Math.min/Math.max to handle out-of-order timestamps + const timestamps = llmCalls.map(call => new Date(call.created_at).getTime()) + const minTimestamp = timestamps.length > 0 ? Math.min(...timestamps) : 0 + const maxTimestamp = timestamps.length > 0 ? Math.max(...timestamps) : 0 + const totalDuration = maxTimestamp > minTimestamp ? (maxTimestamp - minTimestamp) / 1000 : 0 + + // Status determination - check for partial_success, failed, or success + const hasPartial = llmCalls.some(c => c.status === "partial_success") + const status = failedCalls > 0 ? "Failed" : hasPartial ? "Partial" : "Completed" + const statusColor = + failedCalls > 0 + ? "text-red-600 dark:text-red-400" + : hasPartial + ? "text-yellow-600 dark:text-yellow-400" + : "text-green-600 dark:text-green-400" + + // Group calls by call_type + const groupedCalls = llmCalls.reduce( + (acc, call) => { + const type = call.call_type || "unknown" + if (!acc[type]) { + acc[type] = [] + } + acc[type].push(call) + return acc + }, + {} as Record, + ) + + // Get call types in order of first appearance + const orderedTypes = [...new Set(llmCalls.map(c => c.call_type || "unknown"))] + + // Create a map of call_type to LLM call for candidate linking + const callTypeToLlmCall = new Map() + llmCalls.forEach(call => { + if (call.call_type && !callTypeToLlmCall.has(call.call_type)) { + callTypeToLlmCall.set(call.call_type, call) + } + }) + + return ( +
    + {/* Header */} +
    +
    + + Traces + + / + + {trace_id && trace_id.trim() ? `${trace_id.substring(0, 8)}...` : "N/A"} + +
    +
    +
    +

    + Trace Details +

    +
    +

    + {trace_id && trace_id.trim() ? trace_id : "Invalid Trace ID"} +

    + {trace_id && trace_id.trim() && } +
    +
    + +
    +

    + Summary Metrics +

    +

    + View key metrics including status, source, duration, cost, tokens, and number + of generated candidates for this optimization request. +

    +
    +
    +

    + LLM Calls Timeline +

    +

    + All LLM API calls grouped by type. Expand each call to see detailed metrics + including tokens, latency, and timestamp. For optimization and line_profiler + types, candidates are displayed directly. +

    +
    +
    +

    + Generated Candidates +

    +

    + Code optimization candidates generated during this trace. Each candidate includes + an explanation and the generated code. Use the copy button to copy candidate code. +

    +
    +
    +

    + Error Handling +

    +

    + If any errors occurred during optimization, they are displayed with severity + indicators, error messages, and detailed context for test failures. +

    +
    +
    + } + /> +
    +
    + + {/* Summary Card */} +
    +
    +
    +
    + {status === "Completed" ? ( + + ) : status === "Failed" ? ( + + ) : ( + + )} + Status + +
    +
    {status}
    +
    +
    +
    + {traceSource.toLowerCase().includes("github") ? ( + + ) : ( + + )} + Source + +
    +
    + + {traceSource} + +
    +
    +
    +
    + + Duration + +
    +
    + {(totalDuration / 1000).toFixed(2)}s +
    +
    +
    +
    + + Cost + +
    +
    + ${totalCost.toFixed(4)} +
    +
    +
    +
    + + Tokens + +
    +
    + {totalTokens.toLocaleString()} +
    +
    +
    +
    + + Candidates + +
    +
    + {optimizationCandidates.length} +
    +
    +
    +
    + + {/* Unified LLM Calls View */} +
    +
    +
    +
    +

    LLM Calls

    + + {llmCalls.length} + +
    + +
    +

    + Call Sequence +

    +

    + Calls are ordered by sequence number showing the execution order. Each call + displays its status, duration, cost, and model used. +

    +
    +
    +

    + Optimization & Line Profiler +

    +

    + For these call types, generated candidates are displayed directly. Each + candidate includes the code and an explanation of the optimization. +

    +
    +
    +

    + Expanding Calls +

    +

    + Click any call to expand and see detailed metrics including token usage, + latency, and timestamp. Use "View full details" to see prompts and responses. +

    +
    +
    + } + size="sm" + /> +
    +
    +
    + {orderedTypes.map(callType => { + const calls = groupedCalls[callType] + const isOptimizationType = callType === "optimization" + const isLineProfilerType = callType === "line_profiler" + + // Get the candidates for this call type + const candidatesForType = isOptimizationType + ? optimizationCandidates + : isLineProfilerType + ? lineProfilerCandidates + : [] + + // For optimization/line_profiler types, only show candidates (not individual LLM calls) + const showIndividualCalls = !isOptimizationType && !isLineProfilerType + + // Get the LLM call for this call type to link candidates + const llmCallForType = callTypeToLlmCall.get(callType) + + return ( +
    +
    + {callType} + {candidatesForType.length > 0 && ( + + {candidatesForType.length} candidates + + )} +
    + {showIndividualCalls && ( +
    + {calls.map((call, typeIndex) => { + const durationSec = ((call.latency_ms || 0) / 1000).toFixed(2) + const statusIcon = call.status === "success" ? "✓" : "✗" + const callStatusColor = + call.status === "success" + ? "text-green-600 dark:text-green-400" + : "text-red-600 dark:text-red-400" + const ctx = call.context as { call_sequence?: number } | null + const callSequence = ctx?.call_sequence + + return ( +
    + +
    + + {callSequence ? `#${callSequence}` : "-"} + + {statusIcon} + + {calls.length > 1 ? `${callType} ${typeIndex + 1}` : callType} + + + {durationSec}s · ${(call.llm_cost || 0).toFixed(4)} ·{" "} + {call.model_name} + +
    + + ▼ + +
    + +
    + {/* Call details */} +
    +
    + Tokens + + {call.total_tokens?.toLocaleString() ?? "N/A"} + +
    +
    + Latency + + {call.latency_ms ? `${call.latency_ms}ms` : "N/A"} + +
    +
    + Time + + {new Date(call.created_at).toLocaleTimeString()} + +
    +
    + Details + + View full details → + +
    +
    +
    +
    + ) + })} +
    + )} + {/* Show candidates for optimization/line_profiler types */} + {candidatesForType.length > 0 && ( +
    +
    +

    + Generated Candidates +

    + +
    +
    + {candidatesForType.map(candidate => { + const isBest = + bestCandidateId != null && candidate.id === bestCandidateId + const showUsedForPr = isBest && usedForPr + const rank = candidateRankMap.get(candidate.id) + return ( +
    + +
    + + Candidate {candidate.index} + + + {candidate.id.substring(0, 8)}... + + {rank != null && ( + + Rank #{rank} + + )} + {isBest && ( + + Best + + )} + {showUsedForPr && ( + + Used for PR + + )} + {candidate.model && ( + + {candidate.model} + + )} +
    + +
    +
    + {candidateExplanations[candidate.id] && ( +
    +
    + +
    + Explanation +
    +
    +

    + {candidateExplanations[candidate.id]} +

    +
    + )} +
    +
    +
    + Code +
    + +
    +
    +                                {candidate.code}
    +                              
    +
    + {llmCallForType && ( +
    + + View LLM Call Details → + +
    + )} +
    +
    + )})} +
    +
    + )} +
    + ) + })} +
    +
    + + {/* Ranking explanation — shown when ranker ran */} + {rankingData?.explanation && ( +
    +
    +
    +

    + Ranking explanation +

    + +
    +
    +
    +

    + {rankingData.explanation} +

    +
    +
    + )} + + {/* Errors */} +
    +
    +
    + {errors.length > 0 ? ( + <> + +

    + Errors +

    + + {errors.length} + + + ) : ( + <> + +

    + No Errors Detected +

    + + )} +
    +
    + {errors.length > 0 ? ( +
    + {errors.map(error => { + const isTestFailure = error.error_type === "test_failure" + const errorContext = error.context as + | { + test_name?: string + failure_reason?: string + test_output?: string + expected?: string + actual?: string + } + | null + + return ( +
    +
    + {error.severity === "error" ? ( + + ) : ( + + )} +
    +
    + + {error.error_type} + + + {error.severity} + + + {new Date(error.created_at).toLocaleString()} + +
    +
    +

    + {error.error_message} +

    + +
    + {/* Test Failure Details */} + {isTestFailure && errorContext && ( +
    +

    + Test Failure Details +

    + {errorContext.test_name && ( +
    + + Test: + {" "} + + {errorContext.test_name} + +
    + )} + {errorContext.failure_reason && ( +
    + + Reason: + +

    + {errorContext.failure_reason} +

    +
    + )} + {errorContext.expected && ( +
    + + Expected: + +
    +                                {String(errorContext.expected)}
    +                              
    +
    + )} + {errorContext.actual && ( +
    + + Actual: + +
    +                                {String(errorContext.actual)}
    +                              
    +
    + )} + {errorContext.test_output && ( +
    + + Test Output: + +
    +                                {String(errorContext.test_output)}
    +                              
    +
    + )} +
    + )} +
    +
    +
    + ) + })} +
    + ) : ( +
    + +

    + All Clear! +

    +

    + This trace completed successfully with no errors detected. +

    +
    + )} +
    +
    + ) +} diff --git a/js/cf-webapp/src/app/observability/traces/page.tsx b/js/cf-webapp/src/app/observability/traces/page.tsx new file mode 100644 index 000000000..2a9a514b9 --- /dev/null +++ b/js/cf-webapp/src/app/observability/traces/page.tsx @@ -0,0 +1,654 @@ +import Link from "next/link" +import { unstable_cache } from "next/cache" +import { Search as SearchIcon } from "lucide-react" +import { prisma } from "@/lib/prisma" +import { safeCostTokens } from "@/lib/observability-utils" +import type { Prisma } from "@prisma/client" +import { HelpButton } from "@/components/observability/help-button" +import { StatCard } from "@/components/observability/stat-card" +import { ColumnHeader } from "@/components/observability/column-header" +import { InfoIcon } from "@/components/observability/info-icon" + +// Revalidate every 30 seconds +export const revalidate = 30 + +interface SearchParams { + trace_id?: string + page?: string + organization?: string +} + +// Cached function to get unique organizations list +// Revalidates every 5 minutes - organizations change infrequently +const getUniqueOrganizations = unstable_cache( + async () => { + const uniqueOrganizations = await prisma.optimization_features.findMany({ + select: { organization: true }, + distinct: ["organization"], + where: { organization: { not: null } }, + }) + return uniqueOrganizations.map(f => f.organization).filter(Boolean).sort() as string[] + }, + ["unique-organizations"], + { revalidate: 300 }, // 5 minutes +) + +// Optimized function to count distinct trace_ids using groupBy +const getTotalTracesCount = unstable_cache( + async (traceIdFilter: string | undefined, organizationFilter: string | undefined) => { + // Get trace IDs filtered by organization if specified + let traceIdPrefixes: string[] = [] + if (organizationFilter) { + const orgFeatures = await prisma.optimization_features.findMany({ + where: { organization: organizationFilter }, + select: { trace_id: true }, + distinct: ["trace_id"], + }) + traceIdPrefixes = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] + if (traceIdPrefixes.length === 0) return 0 + } + + // Build where clause + const where: Prisma.llm_callsWhereInput = {} + + if (traceIdFilter) { + where.trace_id = { contains: traceIdFilter } + } else if (traceIdPrefixes.length > 0) { + // Use IN clause for exact trace ID matches - much more efficient than OR with startsWith + where.trace_id = { in: traceIdPrefixes } + } + + // Use groupBy for efficient distinct count + // Filter out null trace_ids in the result + const result = await prisma.llm_calls.groupBy({ + by: ["trace_id"], + where, + }) + + // Filter out null trace_ids + return result.filter(r => r.trace_id !== null).length + }, + ["traces-count"], + { revalidate: 30 }, +) + +export default async function TracesPage({ searchParams }: { searchParams: SearchParams }) { + try { + const page = parseInt(searchParams.page || "1") + const pageSize = 50 + const skip = (page - 1) * pageSize + + // Get trace IDs filtered by organization if specified + let filteredTraceIds: string[] = [] + if (searchParams.organization) { + const orgFeatures = await prisma.optimization_features.findMany({ + where: { organization: searchParams.organization }, + select: { trace_id: true }, + distinct: ["trace_id"], + }) + filteredTraceIds = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] + + // If organization filter is applied but no traces found, return empty result early + if (filteredTraceIds.length === 0) { + const uniqueOrganizations = await prisma.optimization_features.findMany({ + select: { organization: true }, + distinct: ["organization"], + where: { organization: { not: null } }, + }) + const orgs = uniqueOrganizations.map(f => f.organization).filter(Boolean).sort() as string[] + + // Return early with empty results + return ( +
    + {/* Header with search form */} +
    +
    +
    +

    + All Traces +

    + +
    +

    What is a trace?

    +

    A trace represents a complete optimization request from start to finish. Each trace contains all the LLM API calls made during that optimization.

    +
    +
    +

    Multi-model traces

    +

    When using multiple models for optimization, all calls share the same base trace_id (first 33 characters). This helps track related operations together.

    +
    +
    +

    Page sections

    +
      +
    • Summary Stats: Quick overview of trace metrics on this page
    • +
    • Traces Table: Detailed list of all traces with aggregated data
    • +
    • Filters: Search by trace ID or filter by organization
    • +
    +
    +
    + } + /> +
    +
    +
    + +
    +
    + + +
    + + + + Clear + +
    +
    +

    + View optimization request traces with aggregated metrics +

    +
    +
    +
    + +
    +

    + No Traces Found +

    +

    + No traces found for organization "{searchParams.organization}". Try selecting a different organization. +

    +
    + + View All LLM Calls → + +
    +
    +
    + ) + } + } + + // Build where clause for LLM calls with organization filter applied at database level + // NOTE: Filtering happens at DB level BEFORE pagination, not client-side. + // We use IN clause because there's no Prisma relation between llm_calls and optimization_features + // (they're only related by trace_id as a string field, not a foreign key relation) + const where: Prisma.llm_callsWhereInput = {} + + if (searchParams.trace_id) { + where.trace_id = { contains: searchParams.trace_id } + } else if (filteredTraceIds.length > 0) { + // Use IN clause for exact trace ID matches - much more efficient than OR with startsWith + // For very large organizations (>10k traces), consider chunking the array + where.trace_id = { in: filteredTraceIds } + } + + // STEP 1: Get distinct trace_ids with pagination using groupBy + const [distinctTraces, totalTracesCount] = await Promise.all([ + prisma.llm_calls.groupBy({ + by: ["trace_id"], + where, + orderBy: { _max: { created_at: "desc" } }, + take: pageSize, + skip, + _max: { created_at: true }, + }), + getTotalTracesCount(searchParams.trace_id, searchParams.organization), + ]) + + // Extract trace_ids from the paginated results + const paginatedTraceIds = distinctTraces + .map(t => t.trace_id) + .filter(Boolean) as string[] + + // STEP 2: Fetch all LLM calls ONLY for the paginated trace_ids + const llmCallsRaw = paginatedTraceIds.length > 0 + ? await prisma.llm_calls.findMany({ + where: { trace_id: { in: paginatedTraceIds } }, + orderBy: { created_at: "desc" }, + select: { + trace_id: true, + created_at: true, + llm_cost: true, + total_tokens: true, + status: true, + call_type: true, + }, + }) + : [] + + // Filter out null trace_ids + const llmCalls = llmCallsRaw.filter(call => call.trace_id !== null) + + // Fetch organizations ONLY for the paginated trace_ids + const [allOptimizationFeatures] = await Promise.all([ + paginatedTraceIds.length > 0 + ? prisma.optimization_features.findMany({ + where: { trace_id: { in: paginatedTraceIds } }, + select: { + trace_id: true, + organization: true, + }, + }) + : [], + ]) + + // Create a map of trace_id to organization + const traceIdToOrganization = new Map() + allOptimizationFeatures.forEach(feature => { + if (feature.trace_id && feature.organization) { + traceIdToOrganization.set(feature.trace_id, feature.organization) + } + }) + + // Get unique organizations for filter dropdown (cached) + const orgs = await getUniqueOrganizations() + + // Group by trace_id and calculate aggregates + const traceMap = new Map< + string, + { + trace_id: string + first_seen: Date + last_seen: Date + call_count: number + total_cost: number + total_tokens: number + failed_calls: number + status: string + call_types: Set + } + >() + + llmCalls.forEach(call => { + if (!call.trace_id) return + + const callTimestamp = new Date(call.created_at).getTime() + const existing = traceMap.get(call.trace_id) + const { cost: callCost, tokens: callTokens } = safeCostTokens(call.llm_cost, call.total_tokens) + + if (existing) { + existing.call_count++ + existing.total_cost += callCost + existing.total_tokens += callTokens + if (call.status === "failed") existing.failed_calls++ + if (call.call_type) existing.call_types.add(call.call_type) + // Use Math.min/Math.max to ensure correct first/last timestamps + existing.first_seen = new Date(Math.min(existing.first_seen.getTime(), callTimestamp)) + existing.last_seen = new Date(Math.max(existing.last_seen.getTime(), callTimestamp)) + } else { + traceMap.set(call.trace_id, { + trace_id: call.trace_id, + first_seen: new Date(callTimestamp), + last_seen: new Date(callTimestamp), + call_count: 1, + total_cost: callCost, + total_tokens: callTokens, + failed_calls: call.status === "failed" ? 1 : 0, + status: call.status || "unknown", + call_types: new Set(call.call_type ? [call.call_type] : []), + }) + } + }) + + // Convert to array and sort by last_seen desc + // No need to paginate here - already done at database level + const traces = Array.from(traceMap.values()).sort( + (a, b) => b.last_seen.getTime() - a.last_seen.getTime(), + ) + const totalPages = Math.ceil(totalTracesCount / pageSize) + + return ( +
    + {/* Header */} +
    + {/* Title, Search Bar, and Help Button */} +
    +
    +

    + All Traces +

    + +
    +

    What is a trace?

    +

    A trace represents a complete optimization request from start to finish. Each trace contains all the LLM API calls made during that optimization.

    +
    +
    +

    Multi-model traces

    +

    When using multiple models for optimization, all calls share the same base trace_id (first 33 characters). This helps track related operations together.

    +
    +
    +

    Page sections

    +
      +
    • Summary Stats: Quick overview of trace metrics on this page
    • +
    • Traces Table: Detailed list of all traces with aggregated data
    • +
    • Filters: Search by trace ID or filter by organization
    • +
    +
    +
    + } + /> +
    + {/* Compact Search Bar */} +
    +
    + +
    +
    + + +
    + + + {(searchParams.trace_id || searchParams.organization) && ( + + Clear + + )} +
    +
    +

    + View optimization request traces with aggregated metrics +

    +
    + + {/* Summary Stats */} +
    + + + sum + t.total_cost, 0).toFixed(4)}`} + helpText="Total cost in USD for all LLM calls shown on this page. Based on model pricing and token usage." + icon="DollarSign" + /> + t.failed_calls > 0).length} + helpText="Traces containing at least one failed LLM call. Click a trace to see error details." + icon="AlertTriangle" + variant="error" + /> +
    + + {/* Traces Table */} +
    +
    + + + + + + + + + + + + + + + + {traces.length === 0 ? ( + + + + ) : ( + traces.map(trace => { + // Ensure duration is never negative by using Math.max + const duration = Math.max( + 0, + (trace.last_seen.getTime() - trace.first_seen.getTime()) / 1000, + ) + // Determine status: check if any call has partial_success, failed, or all success + const hasPartial = + trace.status === "partial_success" || + llmCalls.some(c => c.trace_id === trace.trace_id && c.status === "partial_success") + const statusColor = + trace.failed_calls > 0 + ? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" + : hasPartial + ? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" + : "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" + const statusText = trace.failed_calls > 0 ? "Failed" : hasPartial ? "Partial" : "Success" + const statusBorderColor = trace.failed_calls > 0 ? "border-red-500" : hasPartial ? "border-yellow-500" : "border-green-500" + + return ( + + + + + + + + + + + + ) + }) + )} + +
    +
    + +
    +

    + No Traces Found +

    +

    + {searchParams.trace_id || searchParams.organization + ? "Try adjusting your filters above" + : "Run an optimization to see traces here"} +

    +
    + {(searchParams.trace_id || searchParams.organization) && ( + + View All LLM Calls → + + )} +
    +
    + {trace.trace_id && trace.trace_id.trim() ? ( + + {trace.trace_id.substring(0, 8)}... + + ) : ( + N/A + )} + + {traceIdToOrganization.get(trace.trace_id) || "N/A"} + + + {statusText} + + + {trace.call_count} + {trace.failed_calls > 0 && ( + + ({trace.failed_calls} failed) + + )} + +
    + {Array.from(trace.call_types) + .slice(0, 3) + .map(type => ( + + {type} + + ))} + {trace.call_types.size > 3 && ( + + +{trace.call_types.size - 3} + + )} +
    +
    + ${trace.total_cost.toFixed(4)} + + {trace.total_tokens.toLocaleString()} + + {duration.toFixed(2)}s + + {trace.last_seen.toLocaleString()} +
    +
    +
    + + {/* Pagination */} + {totalPages > 1 && ( +
    + {page > 1 && ( + + Previous + + )} + + Page {page} of {totalPages} • Showing {traces.length} traces + + {page < totalPages && ( + + Next + + )} +
    + )} +
    + ) + } catch (error) { + throw error + } +} diff --git a/js/cf-webapp/src/app/trace/[trace_id]/page.tsx b/js/cf-webapp/src/app/trace/[trace_id]/page.tsx new file mode 100644 index 000000000..45fb7f31c --- /dev/null +++ b/js/cf-webapp/src/app/trace/[trace_id]/page.tsx @@ -0,0 +1,174 @@ +import { PrismaClient } from "@prisma/client" +import { notFound } from "next/navigation" +import Link from "next/link" +import { ExperimentMetadata } from "@/lib/types" // Your defined types +import MonacoDiffViewer from "@/components/trace/monaco-diff-viewer" // The client component +import { Metadata } from "next" // For Next.js metadata API +import { getSession } from "@auth0/nextjs-auth0" +import { isTeamMember } from "@/app/utils/auth" + +interface TraceDetailsPageProps { + params: { + trace_id: string + } +} +const prisma = new PrismaClient() +// Function to generate dynamic metadata (e.g., page title) +export async function generateMetadata({ params }: TraceDetailsPageProps): Promise { + const { trace_id } = params + + // Optionally fetch minimal data for title generation to avoid over-fetching + // For simplicity, we'll use a generic title or one derived if data is fetched quickly + // A more optimized approach might involve a separate lightweight query or using default values. + + const optimizationFeature = await prisma.optimization_features.findUnique({ + where: { trace_id }, + select: { + experiment_metadata: true, + organization: true, + repository: true, + review_quality: true, + review_explanation: true, + }, + }) + + let title = `Python Diff Trace: ${trace_id.substring(0, 8)}` + if (optimizationFeature?.experiment_metadata) { + const metadata = optimizationFeature.experiment_metadata as unknown as ExperimentMetadata // Type assertion + const repoName = + optimizationFeature.organization && optimizationFeature.repository + ? `${optimizationFeature.organization}/${optimizationFeature.repository}` + : metadata.owner && metadata.repo + ? `${metadata.owner}/${metadata.repo}` + : "" + + if (metadata.prCommentFields?.function_name) { + title = `Diff: ${metadata.prCommentFields.function_name} (${repoName})` + } else if (repoName) { + title = `Diff: ${repoName} - Trace ${trace_id.substring(0, 8)}` + } + } + + return { + title: `${title} | Codeflash AI`, + description: `Review CodeFlash Python code optimization diffs for trace ID ${trace_id}.`, + // You can add more OpenGraph tags, etc. + } +} + +// The main page component +export default async function TraceDetailsPage({ params }: TraceDetailsPageProps) { + const { trace_id } = params + + if (!trace_id) { + // This case should ideally be handled by Next.js routing if trace_id is missing in URL structure + notFound() + } + + const session = await getSession() + if (!session?.user) return null + + // Check team member access - only team members can view traces + const hasTeamAccess = await isTeamMember() + if (!hasTeamAccess) { + // Create a custom access denied page or redirect to a generic error + return ( +
    +
    +
    + + + +
    +

    Access Denied

    +

    + This trace is restricted to CodeFlash team members only. +

    +
    +

    + Logged in as:{" "} + {session.user.email || session.user.nickname} +

    +

    + Trace ID: {trace_id} +

    +
    + + Go to Dashboard + +
    +
    + ) + } + + let optimizationFeature: { + experiment_metadata: unknown + metadata: unknown + organization: string | null + repository: string | null + review_quality: string | null + review_explanation: string | null + } | null = null + try { + optimizationFeature = await prisma.optimization_features.findUnique({ + where: { trace_id: trace_id }, + select: { + experiment_metadata: true, // Prisma handles JSONB parsing + metadata: true, // Include metadata field which stores modified code + organization: true, + repository: true, + review_quality: true, + review_explanation: true, + // Select other fields if needed by MonacoDiffViewer for its header/display + }, + }) + } catch (error) { + console.error(`[TracePage] Failed to fetch data for trace_id ${trace_id}:`, error) + // Optionally, render a specific error UI component here instead of notFound() + // For now, notFound() will trigger the 404 page, which is reasonable if data fetch fails badly. + // Or you could pass an error state to MonacoDiffViewer to display. + // For this detailed guide, we assume MonacoDiffViewer will handle 'null' metadata. + } + + // If feature is not found, or metadata is explicitly null (and you expect it for valid traces) + if (!optimizationFeature) { + notFound() // Triggers the Next.js 404 page + } + + // Type assertion is safe here due to the check above or if your DB guarantees metadata for valid traces. + // If experiment_metadata can be legitimately null for an existing trace_id, handle it gracefully. + // Pass experiment metadata directly since modifications are now stored in diffContents + const metadata = optimizationFeature.experiment_metadata as ExperimentMetadata | null + const review_quality = optimizationFeature.review_quality as string | null + const review_explanation = optimizationFeature.review_explanation as string | null + // Determine repository full name for display + const repoFullName = + optimizationFeature.organization && optimizationFeature.repository + ? `${optimizationFeature.organization}/${optimizationFeature.repository}` + : metadata?.owner && metadata?.repo + ? `${metadata.owner}/${metadata.repo}` + : "N/A" + + return ( + + ) +} diff --git a/js/cf-webapp/src/components/conditional-layout.tsx b/js/cf-webapp/src/components/conditional-layout.tsx index 30bf6e76c..4118265db 100644 --- a/js/cf-webapp/src/components/conditional-layout.tsx +++ b/js/cf-webapp/src/components/conditional-layout.tsx @@ -23,6 +23,7 @@ export function ConditionalLayout({ const shouldHideLayout = pathname !== null && ( HIDDEN_PAGES.includes(pathname) || + pathname.startsWith("/trace/") || pathname.startsWith("/observability") || !user ) diff --git a/js/cf-webapp/src/components/dashboard/sidebar.tsx b/js/cf-webapp/src/components/dashboard/sidebar.tsx index 0ea7a45e9..741f3d342 100644 --- a/js/cf-webapp/src/components/dashboard/sidebar.tsx +++ b/js/cf-webapp/src/components/dashboard/sidebar.tsx @@ -301,7 +301,7 @@ export function Sidebar({ className, user, isLoading, error }: SidebarProps): JS {/* Observability Group */} >(new Set()) - const [expandedFiles, setExpandedFiles] = useState>(new Set()) - - const { rwFiles, roFiles, metrics } = useMemo(() => { - const rwFiles = originalCode ? parseMarkdownCodeBlocks(originalCode) : [] - const roFiles = dependencyCode ? parseMarkdownCodeBlocks(dependencyCode) : [] - const rwTokens = rwFiles.reduce((sum, f) => sum + f.tokens, 0) - const roTokens = roFiles.reduce((sum, f) => sum + f.tokens, 0) - const totalFiles = rwFiles.length + roFiles.length - const totalTokens = rwTokens + roTokens - const rwChars = rwFiles.reduce((sum, f) => sum + f.code.length, 0) - const roChars = roFiles.reduce((sum, f) => sum + f.code.length, 0) - - return { - rwFiles, - roFiles, - metrics: { rwTokens, roTokens, totalFiles, totalTokens, rwChars, roChars }, - } - }, [originalCode, dependencyCode]) - - const toggleSection = useCallback((section: string): void => { - setExpandedSections(prev => { - const next = new Set(prev) - if (next.has(section)) { - next.delete(section) - } else { - next.add(section) - } - return next - }) - }, []) - - const toggleFile = useCallback((fileKey: string): void => { - setExpandedFiles(prev => { - const next = new Set(prev) - if (next.has(fileKey)) { - next.delete(fileKey) - } else { - next.add(fileKey) - } - return next - }) - }, []) - - if (!originalCode && !dependencyCode) { - return null - } - - return ( -
    -
    setIsExpanded(!isExpanded)} - onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }} - className="w-full p-6 flex items-center justify-between hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors rounded-sm cursor-pointer" - > -
    - -

    Code Context

    - -
    -
    -
    - - - {metrics.totalTokens.toLocaleString()} tokens - - - - {metrics.totalFiles} files - -
    - -
    -
    - - {isExpanded && ( -
    - {(functionName || filePath) && ( -
    - {functionName && ( -
    - Function - - {functionName} - -
    - )} - {filePath && ( -
    - File - - {filePath} - -
    - )} -
    - )} - - {rwFiles.length > 0 && roFiles.length > 0 && ( -
    -
    - - - Token Distribution (estimated) - -
    - -
    -
    -
    - - Read-Writable: {metrics.rwTokens.toLocaleString()} ({rwFiles.length} files) - -
    -
    -
    - - Read-Only: {metrics.roTokens.toLocaleString()} ({roFiles.length} files) - -
    -
    -
    - )} - - {rwFiles.length > 0 && ( - toggleSection("rw")} - expandedFiles={expandedFiles} - onToggleFile={toggleFile} - sectionKey="rw" - /> - )} - - {roFiles.length > 0 && ( - toggleSection("ro")} - expandedFiles={expandedFiles} - onToggleFile={toggleFile} - sectionKey="ro" - /> - )} -
    - )} -
    - ) -}) - -interface CodeGroupSectionProps { - title: string - subtitle: string - accentColor: "emerald" | "slate" - tokenCount: number - charCount: number - files: ParsedFile[] - isExpanded: boolean - onToggle: () => void - expandedFiles: Set - onToggleFile: (fileKey: string) => void - sectionKey: string -} - -function getAccentColorClasses(accentColor: "emerald" | "slate"): { border: string; bg: string; icon: string } { - switch (accentColor) { - case "emerald": - return { - border: "border-zinc-200 dark:border-zinc-800", - bg: "bg-zinc-50 dark:bg-zinc-900", - icon: "text-emerald-500", - } - case "slate": - return { - border: "border-zinc-200 dark:border-zinc-800", - bg: "bg-zinc-50 dark:bg-zinc-900", - icon: "text-zinc-500", - } - } -} - -const CodeGroupSection = memo(function CodeGroupSection({ - title, - subtitle, - accentColor, - tokenCount, - charCount, - files, - isExpanded, - onToggle, - expandedFiles, - onToggleFile, - sectionKey, -}: CodeGroupSectionProps) { - const { border: borderColor, bg: bgColor, icon: iconColor } = getAccentColorClasses(accentColor) - - return ( -
    -
    { if (e.key === "Enter" || e.key === " ") onToggle() }} - className={`w-full p-4 flex items-center justify-between hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer ${bgColor}`} - > -
    - -
    - {title} - {subtitle} -
    -
    -
    - - {tokenCount.toLocaleString()} tokens · {charCount.toLocaleString()} chars · {files.length} files - - -
    -
    - - {isExpanded && ( -
    - {files.map((file, index) => { - const fileKey = `${sectionKey}-${index}` - const isFileExpanded = expandedFiles.has(fileKey) - return ( -
    -
    -
    onToggleFile(fileKey)} - onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggleFile(fileKey) }} - className="flex items-center gap-2 cursor-pointer hover:opacity-80 flex-1" - > - - - {file.filename} - - - {file.path !== file.filename && `(${file.path})`} - - -
    -
    - - {file.code.split("\n").length} lines - - -
    -
    - - {isFileExpanded && ( -
    - -
    - )} -
    - ) - })} -
    - )} -
    - ) -}) - -interface TokenDistributionBarProps { - rwTokens: number - roTokens: number - totalTokens: number -} - -const TokenDistributionBar = memo(function TokenDistributionBar({ - rwTokens, - roTokens, - totalTokens, -}: TokenDistributionBarProps) { - const rwPercent = Math.round((rwTokens / totalTokens) * 100) - const roPercent = Math.round((roTokens / totalTokens) * 100) - - return ( -
    -
    - {rwPercent > 15 && `${rwPercent}%`} -
    -
    - {roPercent > 15 && `${roPercent}%`} -
    -
    - ) -}) diff --git a/js/cf-webapp/src/components/observability/code-highlighter.tsx b/js/cf-webapp/src/components/observability/code-highlighter.tsx deleted file mode 100644 index 57871822c..000000000 --- a/js/cf-webapp/src/components/observability/code-highlighter.tsx +++ /dev/null @@ -1,172 +0,0 @@ -"use client" - -import dynamic from "next/dynamic" -import { memo } from "react" - -const SyntaxHighlighter = dynamic( - () => import("react-syntax-highlighter").then(m => m.Prism), - { - ssr: false, - loading: () => ( -
    -
    -
    -
    -
    - ), - } -) - -export const zincDarkTheme = { - 'code[class*="language-"]': { - color: 'rgb(250, 250, 250)', - background: 'none', - fontFamily: 'var(--font-mono)', - fontSize: '1em', - textAlign: 'left', - whiteSpace: 'pre', - wordSpacing: 'normal', - wordBreak: 'normal', - wordWrap: 'normal', - lineHeight: '1.5', - tabSize: 4, - hyphens: 'none', - }, - 'pre[class*="language-"]': { - color: 'rgb(250, 250, 250)', - background: 'rgb(24, 24, 27)', - fontFamily: 'var(--font-mono)', - fontSize: '1em', - textAlign: 'left', - whiteSpace: 'pre', - wordSpacing: 'normal', - wordBreak: 'normal', - wordWrap: 'normal', - lineHeight: '1.5', - tabSize: 4, - hyphens: 'none', - padding: '1em', - margin: '0', - overflow: 'auto', - }, - comment: { - color: 'rgb(113, 113, 122)', - fontStyle: 'italic', - }, - prolog: { color: 'rgb(113, 113, 122)' }, - doctype: { color: 'rgb(113, 113, 122)' }, - cdata: { color: 'rgb(113, 113, 122)' }, - keyword: { color: 'rgb(96, 165, 250)' }, - 'control-flow': { color: 'rgb(96, 165, 250)' }, - string: { color: 'rgb(134, 239, 172)' }, - 'attr-value': { color: 'rgb(134, 239, 172)' }, - function: { color: 'rgb(253, 224, 71)' }, - 'class-name': { color: 'rgb(253, 224, 71)' }, - number: { color: 'rgb(251, 146, 60)' }, - boolean: { color: 'rgb(251, 146, 60)' }, - operator: { color: 'rgb(161, 161, 170)' }, - punctuation: { color: 'rgb(161, 161, 170)' }, - variable: { color: 'rgb(250, 250, 250)' }, - property: { color: 'rgb(250, 250, 250)' }, - tag: { color: 'rgb(96, 165, 250)' }, - 'attr-name': { color: 'rgb(250, 250, 250)' }, - namespace: { opacity: 0.7 }, - selector: { color: 'rgb(253, 224, 71)' }, - important: { - color: 'rgb(251, 146, 60)', - fontWeight: 'bold', - }, - atrule: { color: 'rgb(96, 165, 250)' }, - builtin: { color: 'rgb(253, 224, 71)' }, - entity: { - color: 'rgb(250, 250, 250)', - cursor: 'help', - }, - url: { - color: 'rgb(96, 165, 250)', - textDecoration: 'underline', - }, - inserted: { - color: 'rgb(134, 239, 172)', - background: 'rgba(134, 239, 172, 0.1)', - }, - deleted: { - color: 'rgb(248, 113, 113)', - background: 'rgba(248, 113, 113, 0.1)', - }, -} as const - -export const CODE_STYLE = { - margin: 0, - padding: "1rem", - fontSize: "0.875rem", - lineHeight: 1.5, - background: 'rgb(24, 24, 27)', -} as const - -export const CODE_STYLE_RELAXED = { - margin: 0, - padding: "1rem", - fontSize: "0.875rem", - lineHeight: 1.6, - background: 'rgb(24, 24, 27)', -} as const - -export const CODE_STYLE_SMALL = { - margin: 0, - padding: "1rem", - fontSize: "0.8125rem", - lineHeight: 1.5, - background: 'rgb(24, 24, 27)', -} as const - -interface CodeHighlighterProps { - code: string - language: string - showLineNumbers?: boolean - customStyle?: React.CSSProperties - highlightLines?: number[] -} - -const highlightStyle = { - backgroundColor: 'rgba(250, 204, 21, 0.15)', - display: 'block', - marginLeft: '-1rem', - marginRight: '-1rem', - paddingLeft: '1rem', - paddingRight: '1rem', - borderLeft: '3px solid rgb(250, 204, 21)', -} - -export const CodeHighlighter = memo(function CodeHighlighter({ - code, - language, - showLineNumbers = true, - customStyle = CODE_STYLE, - highlightLines, -}: CodeHighlighterProps) { - const lineProps = highlightLines && highlightLines.length > 0 - ? (lineNumber: number) => { - const isHighlighted = highlightLines.includes(lineNumber) - return { - style: isHighlighted ? highlightStyle : { display: 'block' }, - 'data-highlighted': isHighlighted ? 'true' : undefined, - } - } - : undefined - - const shouldWrapLines = !!(highlightLines && highlightLines.length > 0) - - return ( - - {code} - - ) -}) diff --git a/js/cf-webapp/src/components/observability/column-header.tsx b/js/cf-webapp/src/components/observability/column-header.tsx new file mode 100644 index 000000000..c09e36c76 --- /dev/null +++ b/js/cf-webapp/src/components/observability/column-header.tsx @@ -0,0 +1,27 @@ +"use client" + +import { InfoIcon } from "./info-icon" +import { cn } from "@/lib/utils" + +interface ColumnHeaderProps { + label: string + tooltip: string + className?: string +} + +export function ColumnHeader({ label, tooltip, className }: ColumnHeaderProps) { + return ( + +
    + {label} + +
    + + ) +} diff --git a/js/cf-webapp/src/components/observability/errors-section.tsx b/js/cf-webapp/src/components/observability/errors-section.tsx deleted file mode 100644 index 80d7ef401..000000000 --- a/js/cf-webapp/src/components/observability/errors-section.tsx +++ /dev/null @@ -1,201 +0,0 @@ -"use client" - -import { useState, useCallback, memo } from "react" -import { - XCircle, - AlertCircle, - AlertTriangle, - ChevronDown, -} from "lucide-react" -import { CopyButton } from "./copy-button" - -interface ErrorContext { - test_name?: string - failure_reason?: string - test_output?: string - expected?: string - actual?: string -} - -interface TraceError { - id: string - error_type: string - severity: string - error_message: string - context: ErrorContext | null - created_at: Date -} - -interface ErrorsSectionProps { - errors: TraceError[] -} - -export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSectionProps) { - const [expandedErrors, setExpandedErrors] = useState>(new Set()) - - const toggleError = useCallback((errorId: string) => { - setExpandedErrors(prev => { - const next = new Set(prev) - if (next.has(errorId)) { - next.delete(errorId) - } else { - next.add(errorId) - } - return next - }) - }, []) - - if (errors.length === 0) { - return null - } - - return ( -
    -
    -
    - -

    Errors

    - - {errors.length} - -
    -
    - -
    - {errors.map(error => { - const isExpanded = expandedErrors.has(error.id) - const isTestFailure = error.error_type === "test_failure" - const hasContext = error.context && Object.keys(error.context).length > 0 - - const SeverityIcon = - error.severity === "critical" || error.severity === "error" ? XCircle : AlertTriangle - - let severityColor: string - if (error.severity === "critical") { - severityColor = "text-red-400 border border-red-600 px-1.5 py-0.5" - } else if (error.severity === "error") { - severityColor = "text-orange-400 border border-orange-600 px-1.5 py-0.5" - } else { - severityColor = "text-yellow-400 border border-yellow-600 px-1.5 py-0.5" - } - - return ( -
    -
    -
    toggleError(error.id)} - onKeyDown={e => { if (e.key === "Enter" || e.key === " ") toggleError(error.id) }} - className="flex items-start gap-3 cursor-pointer hover:opacity-80 flex-1 transition-opacity duration-150" - > - -
    -
    - - {error.error_type} - - - {error.severity} - - - {new Date(error.created_at).toLocaleString()} - -
    -

    - {error.error_message} -

    -
    - {(hasContext || isTestFailure) && ( - - )} -
    -
    - -
    -
    - - {isExpanded && isTestFailure && error.context && ( -
    -
    -

    - Test Failure Details -

    - - {error.context.test_name && ( -
    - - Test Name - -

    - {error.context.test_name} -

    -
    - )} - - {error.context.failure_reason && ( -
    - - Failure Reason - -

    - {error.context.failure_reason} -

    -
    - )} - - {error.context.expected && ( -
    - - Expected - -
    -                          {String(error.context.expected)}
    -                        
    -
    - )} - - {error.context.actual && ( -
    - - Actual - -
    -                          {String(error.context.actual)}
    -                        
    -
    - )} - - {error.context.test_output && ( -
    - - Test Output - -
    -                          {String(error.context.test_output)}
    -                        
    -
    - )} -
    -
    - )} - - {isExpanded && !isTestFailure && hasContext && ( -
    -
    - - Context - -
    -                      {JSON.stringify(error.context, null, 2)}
    -                    
    -
    -
    - )} -
    - ) - })} -
    -
    - ) -}) diff --git a/js/cf-webapp/src/components/observability/function-to-optimize-section.tsx b/js/cf-webapp/src/components/observability/function-to-optimize-section.tsx deleted file mode 100644 index 5ad49b0b7..000000000 --- a/js/cf-webapp/src/components/observability/function-to-optimize-section.tsx +++ /dev/null @@ -1,204 +0,0 @@ -"use client" - -import { memo, useMemo, useState, useRef, useEffect } from "react" -import { Code, FileText, ChevronDown } from "lucide-react" -import { CodeHighlighter, CODE_STYLE_RELAXED } from "./code-highlighter" -import { CopyButton } from "./copy-button" -import { findFunctionInCode, type FunctionLocation } from "./python-parser" - -interface FunctionToOptimizeSectionProps { - functionName: string | null - filePath: string | null - originalCode: string | null -} - -interface ParsedFile { - path: string - filename: string - language: string - code: string -} - -function getFilename(path: string): string { - return path.split("/").pop() || path -} - -function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] { - const files: ParsedFile[] = [] - const regex = /```(\w+):([^\n]+)\n([\s\S]*?)```/g - let match - - while ((match = regex.exec(markdown)) !== null) { - const [, language, path, code] = match - files.push({ - path, - filename: getFilename(path), - language: language || "python", - code: code.trimEnd(), - }) - } - - return files -} - -export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection({ - functionName, - filePath, - originalCode, -}: FunctionToOptimizeSectionProps) { - const [isExpanded, setIsExpanded] = useState(true) - const [functionLocation, setFunctionLocation] = useState(null) - const [actualFile, setActualFile] = useState(null) - const codeContainerRef = useRef(null) - - const allFiles = useMemo(() => { - if (!originalCode) return [] - return parseMarkdownCodeBlocks(originalCode) - }, [originalCode]) - - useEffect(() => { - if (!functionName || allFiles.length === 0) { - setFunctionLocation(null) - setActualFile(null) - return - } - - let cancelled = false - - async function findFunction() { - const searchPromises = allFiles.map(async (file) => { - const location = await findFunctionInCode(file.code, functionName!) - return location ? { file, location } : null - }) - - const results = await Promise.all(searchPromises) - if (cancelled) return - - const found = results.find(r => r !== null) - if (found) { - setFunctionLocation(found.location) - setActualFile(found.file) - return - } - - let fallbackFile = allFiles[0] - if (filePath) { - const match = allFiles.find(f => - filePath.endsWith(f.path) || f.path.endsWith(filePath) || f.path === filePath - ) - if (match) fallbackFile = match - } - setFunctionLocation(null) - setActualFile(fallbackFile) - } - - findFunction() - return () => { cancelled = true } - }, [functionName, filePath, allFiles]) - - const functionFile = actualFile ?? allFiles[0] ?? null - - const functionLines = useMemo(() => { - if (!functionLocation) return null - const lines: number[] = [] - for (let i = functionLocation.startLine; i <= functionLocation.endLine; i++) { - lines.push(i) - } - return lines - }, [functionLocation]) - - useEffect(() => { - if (!isExpanded || !functionLocation || !codeContainerRef.current) return - - const scrollToFunction = () => { - if (!codeContainerRef.current) return - const container = codeContainerRef.current - - const lineHeight = 22.4 - const paddingTop = 16 - const targetLine = functionLocation.startLine - 1 - const scrollPosition = paddingTop + (targetLine * lineHeight) - (container.clientHeight / 3) - - container.scrollTo({ - top: Math.max(0, scrollPosition), - behavior: 'smooth' - }) - } - - const timer = setTimeout(scrollToFunction, 300) - return () => clearTimeout(timer) - }, [isExpanded, functionLocation]) - - if (!functionFile) { - return null - } - - const highlightLines = functionLines && functionLines.length > 0 ? functionLines : undefined - - return ( -
    -
    -
    setIsExpanded(!isExpanded)} - onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }} - className="flex items-center gap-3 cursor-pointer hover:opacity-80 flex-1" - > -
    - -
    -
    -

    - Function to Optimize -

    -
    - {functionName && ( - - {functionName} - - )} - {functionLocation && ( - - lines {functionLocation.startLine}-{functionLocation.endLine} - - )} -
    -
    - -
    -
    - -
    -
    - - {isExpanded && ( - <> -
    - - - {functionFile.filename} - - {functionFile.path !== functionFile.filename && ( - - ({functionFile.path}) - - )} - - {functionFile.code.split("\n").length} lines - -
    - -
    - -
    - - )} -
    - ) -}) diff --git a/js/cf-webapp/src/components/observability/help-button.tsx b/js/cf-webapp/src/components/observability/help-button.tsx new file mode 100644 index 000000000..f58175748 --- /dev/null +++ b/js/cf-webapp/src/components/observability/help-button.tsx @@ -0,0 +1,51 @@ +"use client" + +import { Info } from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { cn } from "@/lib/utils" + +interface HelpButtonProps { + title: string + content: React.ReactNode + size?: "sm" | "md" + triggerClassName?: string +} + +export function HelpButton({ title, content, size = "sm", triggerClassName }: HelpButtonProps) { + return ( + + + + + + + {title} + +
    {content}
    +
    +
    +
    +
    + ) +} diff --git a/js/cf-webapp/src/components/observability/index.ts b/js/cf-webapp/src/components/observability/index.ts deleted file mode 100644 index d2684cace..000000000 --- a/js/cf-webapp/src/components/observability/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { TraceSearch } from "./trace-search" -export { TraceSummary } from "./trace-summary" -export { TimelinePageView } from "./timeline-page-view" -export { transformToTimelineSections } from "./timeline-types" -export { ErrorsSection } from "./errors-section" -export { CodeHighlighter, CODE_STYLE, CODE_STYLE_RELAXED, CODE_STYLE_SMALL } from "./code-highlighter" -export { CopyButton } from "./copy-button" -export { InfoIcon } from "./info-icon" -export { getTraceSource } from "./utils" diff --git a/js/cf-webapp/src/components/observability/observability-nav.tsx b/js/cf-webapp/src/components/observability/observability-nav.tsx new file mode 100644 index 000000000..ee99e2f48 --- /dev/null +++ b/js/cf-webapp/src/components/observability/observability-nav.tsx @@ -0,0 +1,55 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { Activity, ListTree } from "lucide-react" +import { cn } from "@/lib/utils" + +const navItems = [ + { href: "/observability/traces", label: "Traces", icon: ListTree }, + { href: "/observability/llm-calls", label: "LLM Calls", icon: Activity }, +] + +export function ObservabilityNav() { + const pathname = usePathname() + + return ( + + ) +} diff --git a/js/cf-webapp/src/components/observability/parsed-response-view.tsx b/js/cf-webapp/src/components/observability/parsed-response-view.tsx new file mode 100644 index 000000000..2de97554f --- /dev/null +++ b/js/cf-webapp/src/components/observability/parsed-response-view.tsx @@ -0,0 +1,223 @@ +"use client" + +import { + extractExplainTag, + extractRankTag, + getResponseContentForParsing, + splitMarkdownCodeBlocks, + type ResponseSegment, +} from "@/lib/observability-response-parse" +import { CopyButton } from "@/components/observability/copy-button" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism" +import { useState } from "react" + +interface ParsedResponseViewProps { + rawResponse: string + callType: string | null +} + +/** Try to parse JSON and return pretty-printed version, or null if not JSON */ +function tryFormatJSON(content: string): string | null { + try { + const parsed = JSON.parse(content) + return JSON.stringify(parsed, null, 2) + } catch { + return null + } +} + +export function ParsedResponseView({ rawResponse, callType }: ParsedResponseViewProps) { + const [showRaw, setShowRaw] = useState(false) + const isRanking = callType === "ranking" + // Use inner message content when raw_response is API JSON (e.g. OpenAI) + const contentForParsing = getResponseContentForParsing(rawResponse) + const rankContent = isRanking ? extractRankTag(contentForParsing) : null + const explainContent = isRanking ? extractExplainTag(contentForParsing) : null + const hasRankingSections = isRanking && (rankContent != null || explainContent != null) + + const segments: ResponseSegment[] = hasRankingSections + ? [] + : splitMarkdownCodeBlocks(contentForParsing) + const hasSegments = segments.length > 0 + + // Check if raw response is JSON (for fallback display) + const formattedJSON = tryFormatJSON(rawResponse) + const isJSON = formattedJSON != null + + return ( +
    + {/* View raw toggle button - always visible in header */} +
    + + +
    + + {showRaw && ( +
    +
    +            {rawResponse}
    +          
    +
    + )} + + {!showRaw && ( +
    + {hasRankingSections && ( +
    + {rankContent != null && ( +
    +
    +

    + Ranking (best first) +

    + +
    +
    +
      + {rankContent + .split(/[\s,]+/) + .filter(Boolean) + .map((id, i) => { + const pos = i + 1 + const label = + pos === 1 + ? "1st" + : pos === 2 + ? "2nd" + : pos === 3 + ? "3rd" + : `${pos}th` + return ( +
    1. + + {label} + + + {id.trim()} + +
    2. + ) + })} +
    +
    +
    + )} + {explainContent != null && ( +
    +
    +

    + Explanation +

    +
    +
    +
    +

    + {explainContent} +

    + +
    +
    +
    + )} +
    + )} + + {!hasRankingSections && hasSegments && ( +
    + {segments.map((seg, i) => { + const textJSON = seg.kind === "text" ? tryFormatJSON(seg.content) : null + return seg.kind === "text" ? ( +
    + {textJSON ? ( + 10} + > + {textJSON} + + ) : ( +
    +                        {seg.content}
    +                      
    + )} +
    + ) : ( +
    +
    + + {seg.language || "code"} + + +
    + 5} + > + {seg.content} + +
    + ) + })} +
    + )} + + {!hasRankingSections && !hasSegments && ( +
    + {isJSON ? ( + 10} + > + {formattedJSON} + + ) : ( +
    +                  {rawResponse}
    +                
    + )} +
    + )} +
    + )} +
    + ) +} diff --git a/js/cf-webapp/src/components/observability/python-parser.ts b/js/cf-webapp/src/components/observability/python-parser.ts deleted file mode 100644 index 96a5f7f19..000000000 --- a/js/cf-webapp/src/components/observability/python-parser.ts +++ /dev/null @@ -1,136 +0,0 @@ -"use client" - -import type { Node, Parser as ParserType } from "web-tree-sitter" - -export interface FunctionLocation { - startLine: number - endLine: number -} - -let parserPromise: Promise | null = null - -async function getParser(): Promise { - if (typeof window === "undefined") { - return null - } - - if (!parserPromise) { - parserPromise = (async () => { - try { - const { Parser, Language } = await import("web-tree-sitter") - await Parser.init({ - locateFile: (scriptName: string) => `/${scriptName}`, - }) - const parser = new Parser() - const Python = await Language.load("/tree-sitter-python.wasm") - parser.setLanguage(Python) - return parser - } catch (error) { - console.error("Tree-sitter initialization failed:", error) - parserPromise = null - throw error - } - })() - } - return parserPromise -} - -export async function findFunctionInCode( - code: string, - functionName: string -): Promise { - try { - const parser = await getParser() - if (parser) { - const tree = parser.parse(code) - if (tree) { - const result = findFunctionNode(tree.rootNode, functionName) - if (result) { - return { - startLine: result.startPosition.row + 1, - endLine: result.endPosition.row + 1, - } - } - } - } - } catch (error) { - console.warn("Tree-sitter parse failed, trying regex fallback:", error) - } - - return findFunctionWithRegex(code, functionName) -} - -function findFunctionWithRegex( - code: string, - functionName: string -): FunctionLocation | null { - const lines = code.split("\n") - - const defPattern = new RegExp( - `^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(` - ) - - let startLine = -1 - let startIndent = -1 - - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - - if (startLine === -1) { - const match = line.match(defPattern) - if (match) { - startLine = i + 1 - startIndent = match[1].length - } - } else { - const trimmed = line.trim() - if (trimmed === "" || trimmed.startsWith("#")) { - continue - } - - const currentIndent = line.length - line.trimStart().length - if (currentIndent <= startIndent) { - return { startLine, endLine: i } - } - } - } - - if (startLine !== -1) { - return { startLine, endLine: lines.length } - } - - return null -} - -function escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") -} - -function findFunctionNode(node: Node, functionName: string): Node | null { - if ( - node.type === "function_definition" || - node.type === "async_function_definition" - ) { - const nameNode = node.childForFieldName("name") - if (nameNode && nameNode.text === functionName) { - return node - } - } - - if (node.type === "class_definition") { - const classBody = node.childForFieldName("body") - if (classBody) { - for (const child of classBody.children) { - const result = findFunctionNode(child, functionName) - if (result) return result - } - } - } - - for (const child of node.children) { - const result = findFunctionNode(child, functionName) - if (result) return result - } - - return null -} diff --git a/js/cf-webapp/src/components/observability/stat-card.tsx b/js/cf-webapp/src/components/observability/stat-card.tsx new file mode 100644 index 000000000..b43974d46 --- /dev/null +++ b/js/cf-webapp/src/components/observability/stat-card.tsx @@ -0,0 +1,72 @@ +"use client" + +import { + Database, + Zap, + DollarSign, + AlertTriangle, + Activity, + CheckCircle2, + Clock, +} from "lucide-react" +import { InfoIcon } from "./info-icon" +import { cn } from "@/lib/utils" + +const variantStyles = { + default: "", + success: "border-l-4 border-green-500", + warning: "border-l-4 border-yellow-500", + error: "border-l-4 border-red-500", +} + +// Icon mapping - add more icons as needed +const iconMap = { + Database, + Zap, + DollarSign, + AlertTriangle, + Activity, + CheckCircle2, + Clock, +} as const + +type IconName = keyof typeof iconMap + +interface StatCardProps { + label: string + value: string | number + helpText?: string + icon?: IconName + variant?: "default" | "success" | "warning" | "error" + className?: string +} + +export function StatCard({ label, value, helpText, icon, variant = "default", className }: StatCardProps) { + const IconComponent = icon ? iconMap[icon] : null + + return ( +
    +
    +
    +
    + {IconComponent && } +
    + {label} + {helpText && } +
    +
    +
    + {value} +
    +
    +
    +
    + ) +} diff --git a/js/cf-webapp/src/components/observability/timeline-page-view.tsx b/js/cf-webapp/src/components/observability/timeline-page-view.tsx deleted file mode 100644 index a814503b8..000000000 --- a/js/cf-webapp/src/components/observability/timeline-page-view.tsx +++ /dev/null @@ -1,823 +0,0 @@ -"use client" - -import { useState, useRef, useEffect, memo, useMemo } from "react" -import { - Clock, - FlaskConical, - Activity, - Box, - RefreshCw, - ChevronDown, - FileText, - Code, - GitCompare, - CheckCircle2, - XCircle, - AlertCircle, - BarChart3, -} from "lucide-react" -import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" -import type { TimelineSection, TimelineSectionContent } from "./timeline-types" - -function stripCodeHeader(code: string): string { - let lines = code.split("\n") - if (lines[0] && /^`{3}[a-z]*(:.*)?$/i.test(lines[0].trim())) { - lines = lines.slice(1) - } - if (lines.length > 0 && lines[lines.length - 1]?.trim() === "```") { - lines = lines.slice(0, -1) - } - return lines.join("\n") -} - -interface TimelinePageViewProps { - sections: TimelineSection[] - totalDuration: number - functionName?: string | null - filePath?: string | null -} - -const TYPE_CONFIG = { - test_generation: { icon: FlaskConical }, - optimization: { icon: Box }, - line_profiler: { icon: Activity }, - refinement: { icon: RefreshCw }, - ranking: { icon: BarChart3 }, - summary: { icon: CheckCircle2 }, -} - -function formatTime(ms: number): string { - if (ms < 1000) return `${Math.round(ms)}ms` - if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` - return `${(ms / 60000).toFixed(1)}m` -} - -function getStatusIcon(status: string) { - switch (status) { - case "success": - return - case "failed": - return - case "partial": - return - default: - return - } -} - -interface ParsedCodeBlock { - language: string - filename: string | null - path: string | null - code: string -} - -function parseCodeBlock(rawCode: string): ParsedCodeBlock { - const markdownMatch = rawCode.match(/^```(\w+)(?::([^\n]+))?\n([\s\S]*?)```\s*$/) - if (markdownMatch) { - const [, language, path, code] = markdownMatch - const filename = path ? path.split("/").pop() || null : null - return { language: language || "python", filename, path: path || null, code: code.trimEnd() } - } - return { language: "python", filename: null, path: null, code: rawCode } -} - -function parseAllCodeBlocks(markdown: string): ParsedCodeBlock[] { - const files: ParsedCodeBlock[] = [] - const regex = /```(\w+)(?::([^\n]+))?\n([\s\S]*?)```/g - let match - - while ((match = regex.exec(markdown)) !== null) { - const [, language, path, code] = match - const filename = path ? path.split("/").pop() || null : null - files.push({ - path: path || null, - filename, - language: language || "python", - code: code.trimEnd(), - }) - } - - if (files.length === 0 && markdown.trim()) { - return [parseCodeBlock(markdown)] - } - - return files -} - -function findMatchingFile( - files: ParsedCodeBlock[], - targetPath: string | null -): ParsedCodeBlock | null { - if (!targetPath || files.length === 0) return files[0] || null - - const exactMatch = files.find(f => f.path === targetPath) - if (exactMatch) return exactMatch - - const targetFilename = targetPath.split("/").pop() - const filenameMatch = files.find(f => f.filename === targetFilename) - if (filenameMatch) return filenameMatch - - const partialMatch = files.find(f => - f.path && (targetPath.endsWith(f.path) || f.path.endsWith(targetPath)) - ) - if (partialMatch) return partialMatch - - return files[0] || null -} - -const DiffView = memo(function DiffView({ diff }: { diff: string }) { - const lines = diff.split("\n") - - return ( -
    - {lines.map((line, index) => { - const isAddition = line.startsWith("+") - const isDeletion = line.startsWith("-") - const isHunkHeader = line.startsWith("@@") - const isNoNewline = line.startsWith("\\ No newline") || line.startsWith("\\") - - if (index === lines.length - 1 && line === "") return null - if ((line === "+" || line === "-") || (isAddition && line.substring(1).trim() === "") || (isDeletion && line.substring(1).trim() === "")) { - return null - } - if (isNoNewline) return null - - let bgClass = "" - let textClass = "text-zinc-300" - let lineContent = line - let indicator: React.ReactNode = null - let borderClass = "border-transparent" - - if (isHunkHeader) { - bgClass = "bg-blue-900/30" - textClass = "text-blue-400" - } else if (isAddition) { - bgClass = "bg-green-900/40" - textClass = "text-green-300" - lineContent = line.substring(1) - indicator = + - borderClass = "border-green-500" - } else if (isDeletion) { - bgClass = "bg-red-900/40" - textClass = "text-red-300" - lineContent = line.substring(1) - indicator = - borderClass = "border-red-500" - } else if (line.startsWith(" ")) { - lineContent = line.substring(1) - } - - return ( -
    -
    - {indicator} -
    -
    -              {lineContent || " "}
    -            
    -
    - ) - })} -
    - ) -}) - -const TestContent = memo(function TestContent({ content }: { content: Extract }) { - const [showDetails, setShowDetails] = useState(false) - const [expandedTest, setExpandedTest] = useState(null) - const [activeVariant, setActiveVariant] = useState<"generated" | "instrumented" | "instrumentedPerf">("generated") - - const testCount = content.testGroups.length - const hasInstrumented = content.testGroups.some(g => g.instrumented) - const hasInstrumentedPerf = content.testGroups.some(g => g.instrumentedPerf) - - return ( -
    -
    -
    -
    - - - {testCount} test{testCount !== 1 ? "s" : ""} generated - -
    -
    - {content.testFramework && ( - - {content.testFramework} - - )} - {hasInstrumented && ( - - +behavior - - )} - {hasInstrumentedPerf && ( - - +perf - - )} -
    -
    - -
    - - {showDetails && ( -
    - {content.testGroups.map((group) => { - const isExpanded = expandedTest === group.index - const hasMultipleVariants = [group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length > 1 - const currentCode = activeVariant === "generated" ? group.generated - : activeVariant === "instrumented" ? group.instrumented - : group.instrumentedPerf - - return ( -
    - - - {isExpanded && ( -
    - {hasMultipleVariants && ( -
    - {group.generated && ( - - )} - {group.instrumented && ( - - )} - {group.instrumentedPerf && ( - - )} -
    - )} - -
    - {currentCode ? ( - - ) : ( -
    - No {activeVariant === "generated" ? "generated" : activeVariant === "instrumented" ? "instrumented behavior" : "instrumented perf"} test available -
    - )} -
    -
    - )} -
    - ) - })} -
    - )} -
    - ) -}) - -const CandidateContent = memo(function CandidateContent({ - content, - isActive, -}: { - content: Extract - isActive: boolean -}) { - const [viewMode, setViewMode] = useState<"code" | "diff">("diff") - const [selectedFileIndex, setSelectedFileIndex] = useState(0) - const [unifiedDiff, setUnifiedDiff] = useState(null) - const [diffLoading, setDiffLoading] = useState(false) - - const originalCode = content.type === "refinement" ? content.parentCode : content.originalCode - - const candidateFiles = useMemo(() => parseAllCodeBlocks(content.code), [content.code]) - const originalFiles = useMemo(() => originalCode ? parseAllCodeBlocks(originalCode) : [], [originalCode]) - - const selectedCandidateFile = candidateFiles[selectedFileIndex] || candidateFiles[0] - - const matchingOriginalFile = useMemo(() => { - if (!selectedCandidateFile || originalFiles.length === 0) return null - return findMatchingFile(originalFiles, selectedCandidateFile.path) - }, [selectedCandidateFile, originalFiles]) - - useEffect(() => { - setUnifiedDiff(null) - }, [selectedFileIndex]) - - useEffect(() => { - if (viewMode !== "diff" || !matchingOriginalFile || !selectedCandidateFile || unifiedDiff !== null) { - return - } - - setDiffLoading(true) - import("diff").then(({ createTwoFilesPatch }) => { - const filename = selectedCandidateFile.filename || matchingOriginalFile.filename || "code.py" - const diff = createTwoFilesPatch( - `a/${filename}`, - `b/${filename}`, - matchingOriginalFile.code, - selectedCandidateFile.code, - "", - "", - { context: 3 } - ) - - const lines = diff.split("\n") - const hunkStartIndex = lines.findIndex(line => line.startsWith("@@")) - setUnifiedDiff(hunkStartIndex > 0 ? lines.slice(hunkStartIndex).join("\n") : diff) - setDiffLoading(false) - }).catch(error => { - console.error("Failed to load diff library:", error) - setDiffLoading(false) - }) - }, [viewMode, matchingOriginalFile, selectedCandidateFile, unifiedDiff]) - - const hasDiff = matchingOriginalFile !== null - const hasMultipleFiles = candidateFiles.length > 1 - - const codeContainerStyle = useMemo( - () => ({ maxHeight: isActive ? "70vh" : "200px" }), - [isActive] - ) - - return ( -
    -
    - {content.rank != null && ( - - #{content.rank} - - )} - {content.isBest && ( - - Best - - )} -
    - - {content.explanation && ( -

    - {content.explanation} -

    - )} - -
    - {hasDiff && ( -
    - - -
    - )} - - {hasMultipleFiles && ( - - )} -
    - - {viewMode === "code" ? ( - selectedCandidateFile ? ( -
    -
    -
    - - - {selectedCandidateFile.filename || "Code"} - - {selectedCandidateFile.path && selectedCandidateFile.path !== selectedCandidateFile.filename && ( - - ({selectedCandidateFile.path}) - - )} -
    - - {selectedCandidateFile.code.split("\n").length} lines - -
    -
    - -
    -
    - ) : ( -
    - No code available -
    - ) - ) : diffLoading ? ( -
    -
    -
    -
    -
    -
    -
    - ) : unifiedDiff ? ( -
    - -
    - ) : ( -
    - No original code available for comparison -
    - )} -
    - ) -}) - -const RankingContent = memo(function RankingContent({ content }: { content: Extract }) { - return ( -
    - {content.explanation && ( -
    -

    - {content.explanation} -

    -
    - )} - - {content.rankings.length >= 1 && ( -
    - {content.rankings.map((item) => ( -
    -
    - - {item.label} - - - Rank #{item.rank} - - {item.isBest && ( - - Best - - )} - {item.isBest && content.usedForPr && ( - - Used for PR - - )} -
    -
    - -
    -
    - ))} -
    - )} - -
    - ) -}) - -const SummaryContent = memo(function SummaryContent({ content }: { content: Extract }) { - const { metrics } = content - return ( -
    -
    -
    Total Duration
    -
    - {formatTime(metrics.totalDuration)} -
    -
    -
    -
    Total Cost
    -
    - ${metrics.totalCost.toFixed(4)} -
    -
    -
    -
    Total Tokens
    -
    - {metrics.totalTokens.toLocaleString()} -
    -
    -
    -
    Candidates
    -
    - {metrics.candidatesCount} -
    -
    -
    - ) -}) - -const TimelineSectionCard = memo(function TimelineSectionCard({ - section, - isActive, - index, - totalSections, -}: { - section: TimelineSection - isActive: boolean - index: number - totalSections: number -}) { - const config = TYPE_CONFIG[section.type] - const Icon = config.icon - - return ( -
    -
    - -
    -
    - - +{formatTime(section.timestamp)} - - {section.duration && ( - <> - · - - {formatTime(section.duration)} - - - )} -
    - - {index + 1}/{totalSections} - -
    - -
    -
    -
    - -
    -

    - {section.title} -

    - {section.subtitle && ( -

    - {section.subtitle} -

    - )} -
    -
    - {getStatusIcon(section.status)} - {section.model && ( - - {section.model} - - )} - {section.cost != null && ( - - ${section.cost.toFixed(4)} - - )} -
    -
    -
    - -
    - {section.content.type === "tests" && } - {(section.content.type === "candidate" || section.content.type === "refinement") && ( - - )} - {section.content.type === "ranking" && } - {section.content.type === "summary" && } -
    -
    -
    -
    - ) -}) - -export const TimelinePageView = memo(function TimelinePageView({ - sections, - totalDuration, - functionName, - filePath, -}: TimelinePageViewProps) { - const [activeIndex, setActiveIndex] = useState(0) - const sectionRefs = useRef<(HTMLDivElement | null)[]>([]) - const rafId = useRef(null) - - useEffect(() => { - const handleScroll = () => { - if (rafId.current !== null) return - rafId.current = requestAnimationFrame(() => { - rafId.current = null - const scrollTarget = window.innerHeight * 0.35 - - let closestIndex = 0 - let closestDistance = Infinity - - sectionRefs.current.forEach((ref, index) => { - if (ref) { - const rect = ref.getBoundingClientRect() - const sectionMiddle = rect.top + rect.height / 2 - const distance = Math.abs(sectionMiddle - scrollTarget) - - if (distance < closestDistance) { - closestDistance = distance - closestIndex = index - } - } - }) - - setActiveIndex(closestIndex) - }) - } - - window.addEventListener("scroll", handleScroll, { passive: true }) - handleScroll() - return () => { - window.removeEventListener("scroll", handleScroll) - if (rafId.current !== null) { - cancelAnimationFrame(rafId.current) - } - } - }, [sections.length]) - - if (sections.length === 0) { - return ( -
    - No timeline data available -
    - ) - } - - return ( -
    -
    -
    -
    -
    -

    - Optimization Timeline -

    - {functionName && ( -

    - {functionName} - {filePath && in {filePath}} -

    - )} -
    -
    - - {activeIndex + 1} of {sections.length} · {formatTime(totalDuration)} - -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    - - {sections.map((section, index) => ( -
    { sectionRefs.current[index] = el }} - className="scroll-mt-24" - > - -
    - ))} - -
    -
    -
    -
    - - End - -
    -
    -
    -
    - ) -}) diff --git a/js/cf-webapp/src/components/observability/timeline-types.ts b/js/cf-webapp/src/components/observability/timeline-types.ts deleted file mode 100644 index f3497af2d..000000000 --- a/js/cf-webapp/src/components/observability/timeline-types.ts +++ /dev/null @@ -1,289 +0,0 @@ -export interface TimelineSection { - id: string - type: "test_generation" | "optimization" | "line_profiler" | "refinement" | "ranking" | "summary" - title: string - subtitle?: string - timestamp: number - duration?: number - status: "success" | "failed" | "partial" | "pending" - model?: string | null - cost?: number | null - tokens?: number | null - content: TimelineSectionContent -} - -export interface TestGroup { - index: number - generated?: { code: string; lines: number } - instrumented?: { code: string; lines: number } - instrumentedPerf?: { code: string; lines: number } -} - -export type TimelineSectionContent = - | { type: "tests"; testGroups: TestGroup[]; testFramework?: string } - | { type: "candidate"; code: string; originalCode: string | null; explanation?: string; rank?: number; isBest?: boolean } - | { type: "refinement"; code: string; parentCode: string | null; explanation?: string; rank?: number; isBest?: boolean } - | { type: "ranking"; explanation: string; rankings: Array<{ id: string; rank: number; label: string; code: string; isBest: boolean }>; usedForPr: boolean } - | { type: "summary"; metrics: { totalCost: number; totalTokens: number; totalDuration: number; candidatesCount: number } } - -export interface TransformInput { - calls: Array<{ - id: string - call_type: string | null - model_name: string | null - status: string - latency_ms: number | null - llm_cost: number | null - total_tokens: number | null - created_at: Date - context: { call_sequence?: number } | null - }> - optimizationCandidates: Array<{ - id: string - code: string - explanation?: string - index: number - }> - lineProfilerCandidates: Array<{ - id: string - code: string - explanation?: string - index: number - }> - refinementCandidates: Array<{ - id: string - code: string - explanation?: string - parentId: string | null - index: number - }> - generatedTests: Array<{ code: string; index: number }> - instrumentedTests: Array<{ code: string; index: number }> - instrumentedPerfTests: Array<{ code: string; index: number }> - originalCode: string | null - testFramework: string | null - candidateRankMap: Record - bestCandidateId: string | null - rankingExplanation: string | null - usedForPr: boolean -} - -export function transformToTimelineSections(input: TransformInput): { sections: TimelineSection[]; totalDuration: number } { - const { calls, optimizationCandidates, lineProfilerCandidates, refinementCandidates, generatedTests, instrumentedTests, instrumentedPerfTests, originalCode, testFramework, candidateRankMap, bestCandidateId, rankingExplanation, usedForPr } = input - - if (calls.length === 0) { - return { sections: [], totalDuration: 0 } - } - - const timestamps = calls.map(c => new Date(c.created_at).getTime()) - const minTime = Math.min(...timestamps) - const maxTime = Math.max(...timestamps) - const maxLatency = Math.max(...calls.map(c => c.latency_ms ?? 0)) - const totalDuration = maxTime - minTime + maxLatency - - const sections: TimelineSection[] = [] - - const maxTestIndex = Math.max( - generatedTests.length, - instrumentedTests.length, - instrumentedPerfTests.length - ) - - const testGroups: TestGroup[] = [] - for (let i = 1; i <= maxTestIndex; i++) { - const generated = generatedTests.find(t => t.index === i) - const instrumented = instrumentedTests.find(t => t.index === i) - const instrumentedPerf = instrumentedPerfTests.find(t => t.index === i) - - if (generated || instrumented || instrumentedPerf) { - testGroups.push({ - index: i, - generated: generated ? { code: generated.code, lines: generated.code.split("\n").length } : undefined, - instrumented: instrumented ? { code: instrumented.code, lines: instrumented.code.split("\n").length } : undefined, - instrumentedPerf: instrumentedPerf ? { code: instrumentedPerf.code, lines: instrumentedPerf.code.split("\n").length } : undefined, - }) - } - } - - const testCalls = calls.filter(c => c.call_type === "test_generation") - if (testCalls.length > 0 || testGroups.length > 0) { - const firstTestCall = testCalls[0] - const firstTimestamp = firstTestCall ? new Date(firstTestCall.created_at).getTime() - minTime : 0 - const totalTestDuration = testCalls.reduce((sum, c) => sum + (c.latency_ms ?? 0), 0) - const totalTestCost = testCalls.reduce((sum, c) => sum + (c.llm_cost ?? 0), 0) - const totalTestTokens = testCalls.reduce((sum, c) => sum + (c.total_tokens ?? 0), 0) - const allSuccess = testCalls.length === 0 || testCalls.every(c => c.status === "success") - const anyFailed = testCalls.some(c => c.status === "failed") - - const subtitle = testFramework - ? `${testGroups.length} test${testGroups.length > 1 ? "s" : ""} using ${testFramework}` - : `${testGroups.length} test${testGroups.length > 1 ? "s" : ""} generated` - - sections.push({ - id: firstTestCall ? `tests-${firstTestCall.id}` : "tests", - type: "test_generation", - title: "Test Generation", - subtitle, - timestamp: firstTimestamp, - duration: totalTestDuration, - status: allSuccess ? "success" : anyFailed ? "failed" : "partial", - model: firstTestCall?.model_name ?? null, - cost: totalTestCost, - tokens: totalTestTokens, - content: { - type: "tests", - testGroups, - testFramework: testFramework ?? undefined, - }, - }) - } - - const callIndexByType = new Map() - for (const call of calls) { - const timestamp = new Date(call.created_at).getTime() - minTime - const callType = call.call_type || "unknown" - const typeIndex = callIndexByType.get(callType) ?? 0 - callIndexByType.set(callType, typeIndex + 1) - - if (callType === "optimization") { - const optIndex = typeIndex - const candidate = optimizationCandidates[optIndex] - if (candidate) { - const rank = candidateRankMap[candidate.id] - sections.push({ - id: call.id, - type: "optimization", - title: `Optimization Candidate ${candidate.index}`, - timestamp, - duration: call.latency_ms ?? undefined, - status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", - model: call.model_name, - cost: call.llm_cost, - tokens: call.total_tokens, - content: { - type: "candidate", - code: candidate.code, - originalCode, - explanation: candidate.explanation, - rank, - isBest: candidate.id === bestCandidateId, - }, - }) - } - } else if (callType === "line_profiler") { - const lpIndex = typeIndex - const candidate = lineProfilerCandidates[lpIndex] - if (candidate) { - const rank = candidateRankMap[candidate.id] - sections.push({ - id: call.id, - type: "line_profiler", - title: `Line Profiler Candidate ${candidate.index}`, - subtitle: "Guided by profiling data", - timestamp, - duration: call.latency_ms ?? undefined, - status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", - model: call.model_name, - cost: call.llm_cost, - tokens: call.total_tokens, - content: { - type: "candidate", - code: candidate.code, - originalCode, - explanation: candidate.explanation, - rank, - isBest: candidate.id === bestCandidateId, - }, - }) - } - } else if (callType === "refinement") { - const refIndex = typeIndex - const candidate = refinementCandidates[refIndex] - if (candidate) { - const rank = candidateRankMap[candidate.id] - const parentCandidate = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates].find(c => c.id === candidate.parentId) - const parentLabel = parentCandidate - ? (parentCandidate as { source?: string }).source === "REFINE" - ? `From Refinement ${parentCandidate.index}` - : `From Candidate ${parentCandidate.index}` - : undefined - sections.push({ - id: call.id, - type: "refinement", - title: `Refinement ${candidate.index}`, - subtitle: parentLabel, - timestamp, - duration: call.latency_ms ?? undefined, - status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", - model: call.model_name, - cost: call.llm_cost, - tokens: call.total_tokens, - content: { - type: "refinement", - code: candidate.code, - parentCode: parentCandidate?.code ?? originalCode, - explanation: candidate.explanation, - rank, - isBest: candidate.id === bestCandidateId, - }, - }) - } - } else if (callType === "ranking") { - const allCandidates = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates] - const rankings = Object.entries(candidateRankMap) - .sort(([, a], [, b]) => a - b) - .map(([id]) => { - const cand = allCandidates.find(c => c.id === id) - if (!cand) return null - const source = (cand as { source?: string }).source - const prefix = source === "REFINE" ? "Refinement" : source === "OPTIMIZE_LP" ? "LP Candidate" : "Candidate" - return { id, rank: 0, label: `${prefix} ${cand.index}`, code: cand.code, isBest: false } - }) - .filter((r): r is NonNullable => r !== null) - .map((r, index) => ({ ...r, rank: index + 1, isBest: index === 0 })) - - sections.push({ - id: call.id, - type: "ranking", - title: "Candidate Ranking", - subtitle: "Selecting the best optimization", - timestamp, - duration: call.latency_ms ?? undefined, - status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", - model: call.model_name, - cost: call.llm_cost, - tokens: call.total_tokens, - content: { - type: "ranking", - explanation: rankingExplanation ?? "", - rankings, - usedForPr, - }, - }) - } - } - - const typeOrder: Record = { - test_generation: 0, - optimization: 1, - line_profiler: 2, - refinement: 3, - ranking: 4, - summary: 5, - } - - sections.sort((a, b) => { - const orderA = typeOrder[a.type] ?? 99 - const orderB = typeOrder[b.type] ?? 99 - if (orderA !== orderB) return orderA - orderB - const candidateTypes = ["optimization", "line_profiler", "refinement"] - if (candidateTypes.includes(a.type)) { - const indexA = parseInt(a.title.match(/\d+$/)?.[0] ?? "0", 10) - const indexB = parseInt(b.title.match(/\d+$/)?.[0] ?? "0", 10) - return indexA - indexB - } - return a.timestamp - b.timestamp - }) - - return { sections, totalDuration } -} diff --git a/js/cf-webapp/src/components/observability/trace-search.tsx b/js/cf-webapp/src/components/observability/trace-search.tsx deleted file mode 100644 index cfe58d35a..000000000 --- a/js/cf-webapp/src/components/observability/trace-search.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client" - -import { useState, useCallback, type ChangeEvent } from "react" -import { Search, Loader2, CheckCircle } from "lucide-react" -import { useRouter } from "next/navigation" - -interface TraceSearchProps { - initialTraceId?: string - isLoading?: boolean - hasResults?: boolean -} - -export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults = false }: TraceSearchProps) { - const [traceId, setTraceId] = useState(initialTraceId) - const router = useRouter() - - const handleChange = useCallback((e: ChangeEvent) => { - setTraceId(e.target.value) - }, []) - - const handleSearch = useCallback(() => { - const trimmedId = traceId.trim() - if (!trimmedId) return - - const params = new URLSearchParams(window.location.search) - params.set("trace_id", trimmedId) - router.push(`/observability?${params.toString()}`) - }, [traceId, router]) - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleSearch() - } - }, - [handleSearch], - ) - - const inputBorderClass = hasResults - ? "border-green-500 dark:border-green-500 focus:ring-green-500" - : "border-zinc-300 dark:border-zinc-600 focus:ring-blue-500" - - return ( -
    -
    -
    - - - {hasResults && ( - - )} -
    - -
    - {!hasResults && ( -

    - Paste or type a trace ID to view all associated LLM calls, candidates, and errors -

    - )} -
    - ) -} diff --git a/js/cf-webapp/src/components/observability/trace-summary.tsx b/js/cf-webapp/src/components/observability/trace-summary.tsx deleted file mode 100644 index 483e8fa10..000000000 --- a/js/cf-webapp/src/components/observability/trace-summary.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { - CheckCircle, - XCircle, - AlertCircle, - Timer, - DollarSign, - Github, - Terminal, - Hash, - Code as CodeIcon, -} from "lucide-react" -import { InfoIcon } from "./info-icon" - -interface TraceSummaryProps { - status: "Completed" | "Partial" | "Failed" - source: string - durationSeconds: number - totalCost: number - totalTokens: number - candidatesCount?: number -} - -export function TraceSummary({ - status, - source, - durationSeconds, - totalCost, - totalTokens, - candidatesCount, -}: TraceSummaryProps) { - let statusColor: string - if (status === "Failed") { - statusColor = "text-red-600 dark:text-red-400" - } else if (status === "Partial") { - statusColor = "text-yellow-600 dark:text-yellow-400" - } else { - statusColor = "text-green-600 dark:text-green-400" - } - - let StatusIcon - if (status === "Completed") { - StatusIcon = CheckCircle - } else if (status === "Failed") { - StatusIcon = XCircle - } else { - StatusIcon = AlertCircle - } - - const SourceIcon = source.toLowerCase().includes("github") ? Github : Terminal - - return ( -
    -
    -
    -
    - - Status - -
    -
    {status}
    -
    - -
    -
    - - Source - -
    -
    - - {source} - -
    -
    - -
    -
    - - Duration - -
    -
    - {durationSeconds.toFixed(2)}s -
    -
    - -
    -
    - - Cost - -
    -
    - ${totalCost.toFixed(4)} -
    -
    - -
    -
    - - Tokens - -
    -
    - {totalTokens.toLocaleString()} -
    -
    - - {candidatesCount !== undefined && ( -
    -
    - - Candidates - -
    -
    - {candidatesCount} -
    -
    - )} -
    -
    - ) -} diff --git a/js/cf-webapp/src/components/observability/utils.ts b/js/cf-webapp/src/components/observability/utils.ts deleted file mode 100644 index 8d040e231..000000000 --- a/js/cf-webapp/src/components/observability/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Determines the source of an optimization based on event_type - */ -export function getTraceSource(eventType: string | null): string { - if (!eventType) return "Unknown" - - if (eventType === "pr_created" || eventType === "pr_merged" || eventType === "pr_closed") { - return "GitHub Action" - } - - if (eventType === "no-pr") { - return "CLI/VSCode" - } - - return eventType -} diff --git a/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx b/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx new file mode 100644 index 000000000..fc1771cac --- /dev/null +++ b/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx @@ -0,0 +1,928 @@ +"use client" + +import React, { useState, useEffect, useMemo, useCallback, useRef } from "react" +import { DiffEditor, useMonaco, DiffOnMount } from "@monaco-editor/react" +import { + CheckCircle2, + XCircle, + GitPullRequest, + Zap, + TestTube, + ChevronDown, + ChevronUp, + ExternalLink, + FileCode, + Edit3, + Save, + X, + Lock, + Monitor, + Smartphone, + BarChart3, +} from "lucide-react" +// Ensure you have lucide-react installed as per your package.json +import { Loader2, FileText, AlertTriangle } from "lucide-react" +import type { ExperimentMetadata, DiffContents } from "@/lib/types" // Adjust path if needed +import { getMonacoLanguage } from "@/lib/utils" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" + +interface MonacoDiffViewerProps { + metadata: ExperimentMetadata | null // Full metadata object + repoFullName: string // Formatted as "owner/repo" + traceId: string + review_quality: string + review_explanation: string +} + +const MonacoDiffViewer: React.FC = ({ + metadata, + repoFullName, + traceId, + review_quality, + review_explanation, +}) => { + const monaco = useMonaco() + const [activeFileKey, setActiveFileKey] = useState(null) + const [showTestDetails, setShowTestDetails] = useState(false) + const [showGeneratedTests, setShowGeneratedTests] = useState(false) + const [showOptimizationQuality, setShowOptimizationQuality] = useState(false) + const [showOptimizationExplanation, setShowOptimizationExplanation] = useState(false) + const [isEditing, setIsEditing] = useState(false) + const [editSecret, setEditSecret] = useState("") + const [showSecretPrompt, setShowSecretPrompt] = useState(false) + const [currentEdit, setCurrentEdit] = useState<{ [key: string]: string }>({}) + const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle") + const [savedChanges, setSavedChanges] = useState<{ [key: string]: string }>({}) + const [isMobile, setIsMobile] = useState(false) + const [useInlineView, setUseInlineView] = useState(false) + + const isEditingRef = useRef(isEditing) + const activeFileKeyRef = useRef(activeFileKey) + + useEffect(() => { + isEditingRef.current = isEditing + }, [isEditing]) + + useEffect(() => { + activeFileKeyRef.current = activeFileKey + }, [activeFileKey]) + + const handleEditorOnMount: DiffOnMount = useCallback(editor => { + // Always set up the change listener, but only update state when editing + const modifiedEditor = editor.getModifiedEditor() + modifiedEditor.onDidChangeModelContent(() => { + if (isEditingRef.current) { + const value = modifiedEditor.getValue() + if (activeFileKeyRef.current && value !== undefined) { + setCurrentEdit(prev => ({ + ...prev, + [activeFileKeyRef.current!]: value, + })) + } + } + }) + }, []) + + // Merge metadata with saved changes for display + const diffContents: DiffContents | null = useMemo(() => { + if (!metadata?.diffContents) return null + + const updatedContents = { ...metadata.diffContents } + Object.entries(savedChanges).forEach(([fileKey, newContent]) => { + if (updatedContents[fileKey]) { + updatedContents[fileKey] = { + ...updatedContents[fileKey], + newContent, + } + } + }) + return updatedContents + }, [metadata?.diffContents, savedChanges]) + const prCommentFields = metadata?.prCommentFields + const fileKeys = useMemo(() => { + return diffContents ? Object.keys(diffContents) : [] + }, [diffContents]) + + // Calculate test statistics + const testStats = useMemo(() => { + const stats = { + totalPassed: 0, + totalFailed: 0, + categories: [] as { name: string; passed: number; failed: number; icon: string }[], + } + + if (prCommentFields?.report_table) { + Object.entries(prCommentFields.report_table).forEach(([category, results]) => { + stats.totalPassed += results.passed + stats.totalFailed += results.failed + + // Map category names to icons + let icon = "🧪" + if (category.includes("Replay")) icon = "⏪" + else if (category.includes("Unit")) icon = "⚙️" + else if (category.includes("Coverage")) icon = "🔎" + else if (category.includes("Regression")) icon = "🌀" + else if (category.includes("Inspired")) icon = "🎨" + + stats.categories.push({ + name: category, + passed: results.passed, + failed: results.failed, + icon, + }) + }) + } + + return stats + }, [prCommentFields]) + + const handleEditClick = () => { + setShowSecretPrompt(true) + } + + const handleSecretSubmit = () => { + if (editSecret === "codeflash-edit-2025") { + setIsEditing(true) + setShowSecretPrompt(false) + // Initialize current edit with current content if not already set + if (activeFileKey && diffContents?.[activeFileKey] && !currentEdit[activeFileKey]) { + setCurrentEdit(prev => ({ + ...prev, + [activeFileKey]: + diffContents[activeFileKey].newContent || diffContents[activeFileKey].oldContent || "", + })) + } + } else { + alert("Invalid secret!") + } + } + + const handleSaveCode = async () => { + if (!activeFileKey || !currentEdit[activeFileKey]) return + + setSaveStatus("saving") + try { + const response = await fetch(`/api/traces/${traceId}/save-modified-code`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fileKey: activeFileKey, + modifiedCode: currentEdit[activeFileKey], + secret: editSecret, + }), + }) + + if (response.ok) { + setSaveStatus("saved") + + // Update local saved changes state to show the changes immediately + setSavedChanges(prev => ({ + ...prev, + [activeFileKey]: currentEdit[activeFileKey], + })) + + // Clear current edit and auto-return to diff view after successful save + setTimeout(() => { + setSaveStatus("idle") + setIsEditing(false) + setCurrentEdit(prev => { + const newEdit = { ...prev } + delete newEdit[activeFileKey!] + return newEdit + }) + }, 1500) + } else { + setSaveStatus("error") + setTimeout(() => setSaveStatus("idle"), 3000) + } + } catch (error) { + console.error("Failed to save modified code:", error) + setSaveStatus("error") + setTimeout(() => setSaveStatus("idle"), 3000) + } + } + + const handleCancelEdit = () => { + setIsEditing(false) + setEditSecret("") + // Revert changes for current file + if (activeFileKey) { + setCurrentEdit(prev => { + const newEdit = { ...prev } + delete newEdit[activeFileKey] + return newEdit + }) + } + } + + useEffect(() => { + if (fileKeys.length > 0 && !activeFileKey) { + setActiveFileKey(fileKeys[0]) + } + }, [fileKeys, activeFileKey]) + + // Mobile detection and responsive handler + useEffect(() => { + const checkMobile = () => { + const mobile = window.innerWidth < 768 + setIsMobile(mobile) + setUseInlineView(mobile) + } + + checkMobile() + window.addEventListener("resize", checkMobile) + return () => window.removeEventListener("resize", checkMobile) + }, []) + + useEffect(() => { + if (monaco) { + // Define your custom dark theme for Monaco Editor + monaco.editor.defineTheme("codeflash-python-dark", { + base: "vs-dark", + inherit: true, + rules: [ + { token: "comment.python", foreground: "6A9955" }, // Python comments + { token: "keyword.python", foreground: "569CD6" }, // Python keywords + { token: "string.python", foreground: "CE9178" }, // Python strings + { token: "number.python", foreground: "B5CEA8" }, // Python numbers + { token: "identifier.python", foreground: "9CDCFE" }, + { token: "type.identifier.python", foreground: "4EC9B0" }, // class names, etc. + ], + colors: { + "editor.background": "#0A0E14", + "editor.foreground": "#F8F8F2", + "editorLineNumber.foreground": "#6272A4", + "editor.selectionBackground": "#44475A", + "editor.lineHighlightBackground": "#1A1F29", + "diffEditor.insertedTextBackground": "#50FA7B33", + "diffEditor.removedTextBackground": "#FF555533", + "diffEditor.insertedLineBackground": "#50FA7B22", + "diffEditor.removedLineBackground": "#FF555522", + "diffEditorGutter.insertedLineBackground": "#50FA7B", + "diffEditorGutter.removedLineBackground": "#FF5555", + }, + }) + } + }, [monaco]) + + // Loading or error states - NOW AFTER ALL HOOKS + if (!metadata) { + return ( +
    + +

    + Loading trace details for {traceId}... +

    +
    + ) + } + + if (!diffContents || fileKeys.length === 0) { + return ( +
    + +

    No diff content available for this trace.

    +

    Trace ID: {traceId}

    +
    + ) + } + + const currentDiff = activeFileKey && diffContents ? diffContents[activeFileKey] : null + const functionName = prCommentFields?.function_name || "N/A" + const speedup = prCommentFields?.speedup_pct || prCommentFields?.speedup_x || "N/A" + const explanation = prCommentFields?.optimization_explanation + + return ( +
    + {/* Header Section - Mobile Optimized */} +
    +
    + {/* Top Row - Title with PR Link and Observability Link */} +
    +
    +

    + CodeFlash Optimization +

    + {metadata.pullNumber && ( + + + PR #{metadata.pullNumber} + #{metadata.pullNumber} + + + )} +
    + {/* Observability Link */} + + + Observability + Obs + + +
    + + {/* Info Row - Repository and Function (Compact on Mobile) */} +
    +
    + + Repo: + {repoFullName} +
    +
    + Function: + + {functionName} + +
    +
    + + {/* Performance Metrics Row - Compact on Mobile */} +
    + {/* Performance Boost - Smaller on Mobile */} +
    + +
    +
    + Boost +
    + + {speedup} + +
    +
    + + {/* Additional Metrics - Hidden on very small screens */} + {prCommentFields?.loop_count && ( +
    +
    + Loops +
    +
    + {prCommentFields.loop_count.toLocaleString()} +
    +
    + )} + {prCommentFields?.original_runtime && prCommentFields?.best_runtime && ( +
    +
    + Runtime +
    +
    + + {prCommentFields.original_runtime} + + + {prCommentFields.best_runtime} +
    +
    + )} + + {/* Edit Button - Compact on Mobile */} +
    + {!isEditing ? ( + + ) : ( +
    + + +
    + )} +
    +
    + + {/* Test Results Summary - Collapsed by default on mobile */} + {testStats.totalPassed > 0 || testStats.totalFailed > 0 ? ( +
    +
    setShowTestDetails(!showTestDetails)} + > +
    + + + Test Results + +
    +
    + + + {testStats.totalPassed} + +
    + {testStats.totalFailed > 0 && ( +
    + + + {testStats.totalFailed} + +
    + )} +
    +
    + {showTestDetails ? ( + + ) : ( + + )} +
    + + {showTestDetails && ( +
    + {testStats.categories.map((category, idx) => ( +
    +
    + + {category.name} + +
    +
    + {category.passed > 0 && ( + {category.passed}✓ + )} + {category.failed > 0 && ( + {category.failed}✗ + )} +
    +
    + ))} +
    + )} +
    + ) : null} +
    +
    + + {/* File Path/Tabs - Mobile Optimized */} + {fileKeys.length === 1 ? ( + // Single file - show full path with view toggle +
    +
    + + + {fileKeys[0]} + +
    + {/* View Toggle for mobile compatibility */} +
    + +
    +
    + ) : ( + // Multiple files - show tabs with full path on hover +
    +
    +
    + {fileKeys.map(fileKey => ( + + ))} +
    + {/* View Toggle for mobile compatibility */} +
    + +
    +
    +
    + )} + + {/* Monaco Editor Container */} +
    + {activeFileKey && currentDiff && monaco ? ( + // Check for empty diff scenarios first (only in non-editing mode) + !isEditing && + (currentDiff.oldContent || "") === (currentDiff.newContent || "") && + (currentDiff.oldContent || "").trim() === "" ? ( + // Both contents are empty +
    + +

    No Changes to Display

    +

    + The file content is empty. The staging branch may have been merged or the changes + have been reverted. +

    +
    + ) : !isEditing && (currentDiff.oldContent || "") === (currentDiff.newContent || "") ? ( + // Contents are identical (no diff) +
    + +

    No Differences Found

    +

    + The original and optimized code are identical. The changes may have already been + applied or reverted. +

    +
    + ) : isEditing ? ( + // Edit Mode with Diff View - Like VS Code changes view + + +

    Loading Diff Editor...

    +
    + } + /> + ) : ( + // Diff View Mode + + +

    Loading Diff Editor...

    +
    + } + /> + ) + ) : ( +
    + +

    {!monaco ? "Initializing Editor Subsystem..." : "Preparing View..."}

    +
    + )} +
    + {/* Bottom Section with Explanation and Generated Tests - Mobile Optimized */} +
    + {/* Optimization review details */} + {review_quality && ( +
    +
    setShowOptimizationQuality(!showOptimizationQuality)} + > +

    + + 🎯 Quality: {review_quality} +

    + {showOptimizationQuality ? ( + + ) : ( + + )} +
    + {showOptimizationQuality && ( +
    + {review_explanation} +
    + )} +
    + )} + {/* Optimization Explanation */} + {explanation && ( +
    +
    setShowOptimizationExplanation(!showOptimizationExplanation)} + > +

    + + Optimization Explanation + Explanation +

    + {showOptimizationExplanation ? ( + + ) : ( + + )} +
    + {showOptimizationExplanation && ( +
    + {explanation} +
    + )} +
    + )} + + {/* Generated Tests Toggle */} + {metadata.generatedTests && ( +
    + + {showGeneratedTests && ( +
    + { + return ( +
    +
    +                            {children}
    +                          
    +
    + ) + }, + code: props => { + const { inline, className, children, ...restProps } = props as { + inline?: boolean + className?: string + children: React.ReactNode + [key: string]: unknown + } + const match = /language-(\w+)/.exec(className || "") + const language = match ? match[1] : null + + return !inline && language ? ( + + {String(children).replace(/\n$/, "")} + + ) : ( + + {children} + + ) + }, + p: ({ children }) => ( +

    {children}

    + ), + h1: ({ children }) => ( +

    + {children} +

    + ), + h2: ({ children }) => ( +

    + {children} +

    + ), + h3: ({ children }) => ( +

    + {children} +

    + ), + ul: ({ children }) => ( +
      + {children} +
    + ), + ol: ({ children }) => ( +
      + {children} +
    + ), + li: ({ children }) => ( +
  • + {children} +
  • + ), + }} + > + {(() => { + // Check if generatedTests already has markdown code blocks + const testsContent = metadata.generatedTests.trim() + const hasCodeBlocks = testsContent.includes("```") + + // If it doesn't have code blocks, wrap it as Python code + if (!hasCodeBlocks) { + return "```python\n" + testsContent + "\n```" + } + + // Otherwise, return as-is + return testsContent + })()} +
    +
    + )} +
    + )} +
    + + {/* Secret Prompt Modal - Mobile Optimized */} + {showSecretPrompt && ( +
    +
    +
    + +

    Enter Edit Secret

    +
    +

    + Please enter the secret key to enable code editing. +

    + setEditSecret(e.target.value)} + onKeyDown={e => e.key === "Enter" && handleSecretSubmit()} + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent mb-3 sm:mb-4 text-sm sm:text-base" + autoFocus + /> +
    + + +
    +
    +
    + )} +
    + ) +} + +export default MonacoDiffViewer diff --git a/js/cf-webapp/src/components/ui/badge.tsx b/js/cf-webapp/src/components/ui/badge.tsx index cf923ccb9..27195fca3 100644 --- a/js/cf-webapp/src/components/ui/badge.tsx +++ b/js/cf-webapp/src/components/ui/badge.tsx @@ -4,16 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( - "inline-flex items-center rounded-sm border px-2 py-0.5 text-xs font-semibold transition-colors", + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { - default: "border-transparent bg-zinc-700 text-zinc-100 hover:bg-zinc-600", + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: - "border-transparent bg-zinc-800 text-zinc-300 hover:bg-zinc-700", + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "border-zinc-700 text-zinc-300", + outline: "text-foreground", }, }, defaultVariants: { @@ -31,4 +31,4 @@ function Badge({ className, variant, ...props }: BadgeProps) { return
    } -export { Badge, badgeVariants } \ No newline at end of file +export { Badge, badgeVariants } diff --git a/js/cf-webapp/src/components/ui/button.tsx b/js/cf-webapp/src/components/ui/button.tsx index 3c81bcd42..635b9ad4e 100644 --- a/js/cf-webapp/src/components/ui/button.tsx +++ b/js/cf-webapp/src/components/ui/button.tsx @@ -5,22 +5,22 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-sm text-sm font-medium ring-offset-background transition-all duration-150 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-600 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { - default: "bg-zinc-100 dark:bg-zinc-700 text-zinc-950 dark:text-zinc-50 hover:bg-zinc-200 dark:hover:bg-zinc-600", + default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: "border border-zinc-700 hover:bg-zinc-800 hover:text-zinc-50", - secondary: "bg-zinc-800 text-zinc-50 hover:bg-zinc-700", - ghost: "hover:bg-zinc-800 hover:text-zinc-50", - link: "text-zinc-400 underline-offset-4 hover:underline hover:text-zinc-300", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 px-3 py-2", - sm: "h-8 rounded-sm px-2", - lg: "h-10 rounded-sm px-4", - icon: "h-9 w-9", + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", }, }, defaultVariants: { diff --git a/js/cf-webapp/src/components/ui/card.tsx b/js/cf-webapp/src/components/ui/card.tsx index 8f2ece0af..3f29d9bcf 100644 --- a/js/cf-webapp/src/components/ui/card.tsx +++ b/js/cf-webapp/src/components/ui/card.tsx @@ -6,7 +6,7 @@ const Card = React.forwardRef (
    ), @@ -15,7 +15,7 @@ Card.displayName = "Card" const CardHeader = React.forwardRef>( ({ className, ...props }, ref) => ( -
    +
    ), ) CardHeader.displayName = "CardHeader" @@ -24,7 +24,7 @@ const CardTitle = React.forwardRef (

    ), @@ -35,20 +35,20 @@ const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

    +

    )) CardDescription.displayName = "CardDescription" const CardContent = React.forwardRef>( ({ className, ...props }, ref) => ( -

    +
    ), ) CardContent.displayName = "CardContent" const CardFooter = React.forwardRef>( ({ className, ...props }, ref) => ( -
    +
    ), ) CardFooter.displayName = "CardFooter" diff --git a/js/cf-webapp/src/components/ui/icon-example.tsx b/js/cf-webapp/src/components/ui/icon-example.tsx deleted file mode 100644 index 0741b7ae3..000000000 --- a/js/cf-webapp/src/components/ui/icon-example.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Icon Standardization Examples - * - * This file demonstrates the standardized icon treatment across components - * following the zinc color palette and consistent sizing patterns. - * - * Icon Guidelines: - * - Small contexts (buttons, badges): w-4 h-4 - * - Medium contexts (headers, titles): w-5 h-5 - * - Consistent stroke width: strokeWidth={2} - * - Color hierarchy: zinc-400 (muted) → zinc-300 (hover) → zinc-50 (active) - * - All icons from lucide-react should follow these standards - */ - -import { Search, Settings, ChevronRight } from "lucide-react" -import { Button } from "./button" - -export function IconExamples() { - return ( -
    - {/* Button with icon - w-4 h-4 standard */} - - - {/* Icon button - icon only */} - - - {/* Card header icon - w-5 h-5 for larger contexts */} -
    - -

    Card Title

    -
    - - {/* Active state example */} - -
    - ) -} \ No newline at end of file diff --git a/js/cf-webapp/src/components/ui/input.tsx b/js/cf-webapp/src/components/ui/input.tsx index ec74db68d..5b517860f 100644 --- a/js/cf-webapp/src/components/ui/input.tsx +++ b/js/cf-webapp/src/components/ui/input.tsx @@ -10,7 +10,7 @@ const Input = React.forwardRef( span]:line-clamp-1", + "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} > {children} - + )) @@ -41,7 +41,7 @@ const SelectScrollUpButton = React.forwardRef< className={cn("flex cursor-default items-center justify-center py-1", className)} {...props} > - + )) SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName @@ -55,7 +55,7 @@ const SelectScrollDownButton = React.forwardRef< className={cn("flex cursor-default items-center justify-center py-1", className)} {...props} > - + )) SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName @@ -69,7 +69,7 @@ const SelectContent = React.forwardRef< - + @@ -134,7 +134,7 @@ const SelectSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/js/cf-webapp/src/components/ui/separator.tsx b/js/cf-webapp/src/components/ui/separator.tsx index 8d8e78810..aa7acb100 100644 --- a/js/cf-webapp/src/components/ui/separator.tsx +++ b/js/cf-webapp/src/components/ui/separator.tsx @@ -15,7 +15,7 @@ const Separator = React.forwardRef< decorative={decorative} orientation={orientation} className={cn( - "shrink-0 bg-zinc-800", + "shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className, )} @@ -24,4 +24,4 @@ const Separator = React.forwardRef< )) Separator.displayName = SeparatorPrimitive.Root.displayName -export { Separator } \ No newline at end of file +export { Separator } diff --git a/js/cf-webapp/src/components/ui/switch.tsx b/js/cf-webapp/src/components/ui/switch.tsx index efb2265ca..58693e107 100644 --- a/js/cf-webapp/src/components/ui/switch.tsx +++ b/js/cf-webapp/src/components/ui/switch.tsx @@ -21,7 +21,7 @@ export function Switch({ checked, onCheckedChange, disabled, className, id }: Sw disabled={disabled} onClick={() => onCheckedChange(!checked)} className={cn( - "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-all duration-200 ease-in-out", + "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", "disabled:cursor-not-allowed disabled:opacity-50", checked ? "bg-primary" : "bg-muted-foreground/30", @@ -30,7 +30,7 @@ export function Switch({ checked, onCheckedChange, disabled, className, id }: Sw > diff --git a/js/cf-webapp/src/components/ui/table.tsx b/js/cf-webapp/src/components/ui/table.tsx index f16a68b3a..065a962d3 100644 --- a/js/cf-webapp/src/components/ui/table.tsx +++ b/js/cf-webapp/src/components/ui/table.tsx @@ -33,7 +33,7 @@ const TableFooter = React.forwardRef< >(({ className, ...props }, ref) => ( tr]:last:border-b-0", className)} + className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} /> )) @@ -44,7 +44,7 @@ const TableRow = React.forwardRef(({ className, ...props }, ref) => ( )) @@ -88,4 +88,4 @@ const TableCaption = React.forwardRef< )) TableCaption.displayName = "TableCaption" -export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } \ No newline at end of file +export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } diff --git a/js/cf-webapp/src/components/ui/tabs.tsx b/js/cf-webapp/src/components/ui/tabs.tsx index a6543a39a..bf5794d4f 100644 --- a/js/cf-webapp/src/components/ui/tabs.tsx +++ b/js/cf-webapp/src/components/ui/tabs.tsx @@ -30,7 +30,7 @@ const TabsTrigger = React.forwardRef< and tags; optimization uses markdown code blocks. + * raw_response is often the full API JSON (e.g. OpenAI); we extract message content when present. + */ + +/** Extract message content from OpenAI-style API response JSON, or return null */ +export function extractMessageContentFromApiResponse(raw: string): string | null { + try { + const parsed = JSON.parse(raw) as { + choices?: Array<{ message?: { content?: string } }> + } + const content = parsed?.choices?.[0]?.message?.content + return typeof content === "string" ? content : null + } catch { + return null + } +} + +/** Get the string to use for rank/explain and markdown parsing (inner content if API JSON, else raw) */ +export function getResponseContentForParsing(rawResponse: string): string { + return extractMessageContentFromApiResponse(rawResponse) ?? rawResponse +} + +/** Extract content inside ... */ +export function extractRankTag(content: string): string | null { + const m = content.match(/([\s\S]*?)<\/rank>/i) + return m ? m[1].trim() : null +} + +/** Extract content inside ... */ +export function extractExplainTag(content: string): string | null { + const m = content.match(/([\s\S]*?)<\/explain>/i) + return m ? m[1].trim() : null +} + +export type ResponseSegment = + | { kind: "text"; content: string } + | { kind: "code"; language: string; content: string } + +/** + * Split markdown-like content into text and code blocks (```lang ... ```). + * Language is taken from the first word after ```; default "text". + */ +export function splitMarkdownCodeBlocks(content: string): ResponseSegment[] { + const segments: ResponseSegment[] = [] + const re = /```(\w*)\n?([\s\S]*?)```/g + let lastEnd = 0 + let m: RegExpExecArray | null + while ((m = re.exec(content)) !== null) { + if (m.index > lastEnd) { + const text = content.slice(lastEnd, m.index) + if (text.trim()) { + segments.push({ kind: "text", content: text }) + } + } + const lang = m[1] || "text" + segments.push({ kind: "code", language: lang, content: m[2].trim() }) + lastEnd = re.lastIndex + } + if (lastEnd < content.length) { + const text = content.slice(lastEnd) + if (text.trim()) { + segments.push({ kind: "text", content: text }) + } + } + return segments +} diff --git a/js/cf-webapp/src/lib/observability-utils.ts b/js/cf-webapp/src/lib/observability-utils.ts new file mode 100644 index 000000000..72c6ef896 --- /dev/null +++ b/js/cf-webapp/src/lib/observability-utils.ts @@ -0,0 +1,38 @@ +// Shared utilities for observability pages + +/** + * Determines the source of an LLM call based on event_type and context + */ +export function getCallSource( + eventType: string | null, + context: Record | null, +): string { + if ( + context && + typeof context === "object" && + !Array.isArray(context) && + "source" in context + ) { + return String(context.source) + } + if (eventType) { + if (eventType === "pr_created" || eventType === "pr_merged" || eventType === "pr_closed") { + return "GitHub Action" + } + if (eventType === "no-pr") { + return "CLI/VSCode" + } + return eventType + } + return "Unknown" +} + +/** + * Safely extracts cost and tokens from nullable values + */ +export function safeCostTokens(cost: number | null, tokens: number | null) { + return { + cost: cost ?? 0, + tokens: tokens ?? 0, + } +} diff --git a/js/cf-webapp/src/middleware.ts b/js/cf-webapp/src/middleware.ts index bd2549e00..c9438c9ea 100644 --- a/js/cf-webapp/src/middleware.ts +++ b/js/cf-webapp/src/middleware.ts @@ -53,6 +53,7 @@ export const config = { matcher: [ "/", "/app/:path*", + "/trace/:path*", "/billing", "/billing/:path*", "/apikeys", diff --git a/js/cf-webapp/src/styles/spacing.css b/js/cf-webapp/src/styles/spacing.css deleted file mode 100644 index e3087c6e4..000000000 --- a/js/cf-webapp/src/styles/spacing.css +++ /dev/null @@ -1,47 +0,0 @@ -/* Spacing and Layout Token System */ -/* Based on 8px grid for consistent rhythm */ - -:root { - /* Base spacing unit - foundation of the 8px grid */ - --space-unit: 8px; - - /* Border Radius Tokens */ - /* Professional, subtle radius values (2-4px max except for pills) */ - --radius-sm: 2px; /* Tight, professional for small elements */ - --radius-md: 3px; /* Default for cards, panels, buttons */ - --radius-lg: 4px; /* Maximum for larger elements */ - --radius-full: 9999px; /* Only for pills, badges, circular elements */ - - /* Spacing Tokens - 8px increments */ - --space-0: 0px; /* 0 * 8px */ - --space-px: 1px; /* For borders, hairlines */ - --space-0\.5: 4px; /* 0.5 * 8px - fine adjustments */ - --space-1: 8px; /* 1 * 8px */ - --space-2: 16px; /* 2 * 8px */ - --space-3: 24px; /* 3 * 8px */ - --space-4: 32px; /* 4 * 8px */ - --space-5: 40px; /* 5 * 8px */ - --space-6: 48px; /* 6 * 8px */ - --space-7: 56px; /* 7 * 8px */ - --space-8: 64px; /* 8 * 8px */ - --space-9: 72px; /* 9 * 8px */ - --space-10: 80px; /* 10 * 8px */ - - /* Common Layout Spacing Patterns */ - /* These map to the base spacing tokens for consistency */ - --space-gap-xs: var(--space-1); /* 8px - Tight spacing */ - --space-gap-sm: var(--space-2); /* 16px - Small spacing */ - --space-gap-md: var(--space-3); /* 24px - Medium spacing */ - --space-gap-lg: var(--space-4); /* 32px - Large spacing */ - --space-gap-xl: var(--space-5); /* 40px - Extra large spacing */ - - /* Container and Content Spacing */ - --space-container-sm: var(--space-2); /* 16px - Mobile/small screens */ - --space-container-md: var(--space-3); /* 24px - Tablet/medium screens */ - --space-container-lg: var(--space-4); /* 32px - Desktop/large screens */ - - /* Card and Panel Internal Spacing */ - --space-card-sm: var(--space-2); /* 16px - Compact cards */ - --space-card-md: var(--space-3); /* 24px - Standard cards */ - --space-card-lg: var(--space-4); /* 32px - Spacious cards */ -} \ No newline at end of file diff --git a/js/cf-webapp/src/styles/tokens.css b/js/cf-webapp/src/styles/tokens.css deleted file mode 100644 index d02631efc..000000000 --- a/js/cf-webapp/src/styles/tokens.css +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Design Token System - * - * Professional developer-focused design tokens using CSS custom properties. - * Dark mode only, zinc color scale, semantic status colors. - * - * RGB format (e.g., "24 24 27") for Tailwind's alpha channel support. - */ - -:root { - /* ============================================ - * ZINC COLOR SCALE - * Neutral colors for all UI elements - * RGB values without rgb() wrapper for Tailwind - * ============================================ */ - - /* Lightest to darkest */ - --color-zinc-50: 250 250 250; /* Almost white */ - --color-zinc-100: 244 244 245; /* Very light gray */ - --color-zinc-200: 228 228 231; /* Light gray */ - --color-zinc-300: 212 212 216; /* Medium light gray */ - --color-zinc-400: 161 161 170; /* Medium gray */ - --color-zinc-500: 113 113 122; /* True medium gray */ - --color-zinc-600: 82 82 91; /* Medium dark gray */ - --color-zinc-700: 63 63 70; /* Dark gray */ - --color-zinc-800: 39 39 42; /* Very dark gray */ - --color-zinc-900: 24 24 27; /* Near black */ - --color-zinc-950: 9 9 11; /* Deep black */ - - /* ============================================ - * SEMANTIC STATUS COLORS - * For errors, warnings, and success states - * ============================================ */ - - /* Error states (red-based) */ - --color-error: 239 68 68; /* red-500 - Primary error */ - --color-error-foreground: 254 242 242; /* red-50 - Error text on error bg */ - --color-error-muted: 254 202 202; /* red-200 - Subtle error background */ - --color-error-border: 248 113 113; /* red-400 - Error borders */ - - /* Success states (green-based) */ - --color-success: 34 197 94; /* green-500 - Primary success */ - --color-success-foreground: 240 253 244; /* green-50 - Success text on success bg */ - --color-success-muted: 187 247 208; /* green-200 - Subtle success background */ - --color-success-border: 74 222 128; /* green-400 - Success borders */ - - /* Warning states (amber-based, not bright yellow) */ - --color-warning: 245 158 11; /* amber-500 - Primary warning */ - --color-warning-foreground: 255 251 235; /* amber-50 - Warning text on warning bg */ - --color-warning-muted: 254 215 170; /* amber-200 - Subtle warning background */ - --color-warning-border: 251 191 36; /* amber-400 - Warning borders */ - - /* Info states (blue-based) */ - --color-info: 59 130 246; /* blue-500 - Primary info */ - --color-info-foreground: 239 246 255; /* blue-50 - Info text on info bg */ - --color-info-muted: 191 219 254; /* blue-200 - Subtle info background */ - --color-info-border: 96 165 250; /* blue-400 - Info borders */ - - /* ============================================ - * FUNCTIONAL TOKENS - * High-level tokens that reference color scale - * These are what components should use - * ============================================ */ - - /* Core UI surfaces */ - --background: var(--color-zinc-950); /* Main app background */ - --foreground: var(--color-zinc-50); /* Main text color */ - - /* Card and panel surfaces */ - --card: var(--color-zinc-900); /* Card background */ - --card-foreground: var(--color-zinc-50); /* Card text */ - - /* Popover/dropdown surfaces */ - --popover: var(--color-zinc-900); /* Popover background */ - --popover-foreground: var(--color-zinc-50); /* Popover text */ - - /* Interactive elements */ - --primary: var(--color-zinc-50); /* Primary button bg */ - --primary-foreground: var(--color-zinc-950); /* Primary button text */ - - --secondary: var(--color-zinc-800); /* Secondary button bg */ - --secondary-foreground: var(--color-zinc-50); /* Secondary button text */ - - --accent: var(--color-zinc-700); /* Accent elements */ - --accent-foreground: var(--color-zinc-50); /* Accent text */ - - /* Muted states */ - --muted: var(--color-zinc-800); /* Muted backgrounds */ - --muted-foreground: var(--color-zinc-400); /* Muted text */ - - /* Destructive actions */ - --destructive: var(--color-error); /* Destructive button bg */ - --destructive-foreground: var(--color-error-foreground); /* Destructive button text */ - - /* Borders and inputs */ - --border: var(--color-zinc-800); /* Default borders */ - --input: var(--color-zinc-800); /* Input borders */ - --ring: var(--color-zinc-600); /* Focus rings */ - - /* ============================================ - * TYPOGRAPHY TOKENS - * Font stacks and text properties - * ============================================ */ - - /* Font families */ - --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Roboto Mono', ui-monospace, 'Courier New', monospace; - --font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - - /* Font sizes - scale for data-dense layouts */ - --text-xs: 0.75rem; /* 12px */ - --text-sm: 0.875rem; /* 14px */ - --text-base: 1rem; /* 16px */ - --text-lg: 1.125rem; /* 18px */ - --text-xl: 1.25rem; /* 20px */ - --text-2xl: 1.5rem; /* 24px */ - - /* Line heights */ - --leading-tight: 1.25; - --leading-normal: 1.5; - --leading-relaxed: 1.625; - - /* ============================================ - * SPACING TOKENS - * 8px grid system - * ============================================ */ - - --space-unit: 8px; - --space-0: 0; - --space-0-5: 4px; /* Half unit for fine adjustments */ - --space-1: 8px; /* Base unit */ - --space-1-5: 12px; - --space-2: 16px; - --space-2-5: 20px; - --space-3: 24px; - --space-4: 32px; - --space-5: 40px; - --space-6: 48px; - --space-7: 56px; - --space-8: 64px; - --space-9: 72px; - --space-10: 80px; - - /* ============================================ - * BORDER RADIUS TOKENS - * Flat, minimal rounded corners - * ============================================ */ - - --radius-none: 0; - --radius-sm: 2px; /* Subtle rounding */ - --radius-md: 3px; /* Default */ - --radius-lg: 4px; /* Maximum rounding */ - --radius: var(--radius-md); /* Default radius */ - - /* ============================================ - * SHADOW TOKENS - * Minimal shadows for depth - * ============================================ */ - - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); - --shadow-md: 0 2px 4px 0 rgba(0, 0, 0, 0.4); - --shadow-lg: 0 4px 6px 0 rgba(0, 0, 0, 0.5); - --shadow-xl: 0 8px 16px 0 rgba(0, 0, 0, 0.6); - - /* ============================================ - * TRANSITION TOKENS - * Consistent animation timing - * ============================================ */ - - --transition-fast: 150ms; - --transition-base: 250ms; - --transition-slow: 350ms; - --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); -} - -/* ============================================ - * DARK MODE ONLY - * We're not supporting light mode - * This ensures dark mode is always active - * ============================================ */ - -.dark { - /* All tokens are already defined for dark mode in :root */ - /* This class exists for Tailwind's dark: variant to work */ -} \ No newline at end of file diff --git a/js/cf-webapp/src/styles/typography.css b/js/cf-webapp/src/styles/typography.css deleted file mode 100644 index 964ab8d7c..000000000 --- a/js/cf-webapp/src/styles/typography.css +++ /dev/null @@ -1,29 +0,0 @@ -/* Typography Token System */ - -:root { - /* Font Family Tokens */ - --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', monospace; - --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - - /* Font Size Scale - optimized for dark mode readability */ - --text-xs: 0.75rem; /* 12px */ - --text-sm: 0.875rem; /* 14px */ - --text-base: 1rem; /* 16px */ - --text-lg: 1.125rem; /* 18px */ - --text-xl: 1.25rem; /* 20px */ - --text-2xl: 1.5rem; /* 24px */ - --text-code: 0.875rem; /* 14px - inline code specific */ - - /* Font Weight Tokens - optimized for dark backgrounds */ - --font-light: 300; - --font-normal: 400; - --font-medium: 500; - --font-semibold: 600; - --font-bold: 700; - - /* Line Height Tokens - for data density control */ - --leading-tight: 1.25; - --leading-normal: 1.5; - --leading-relaxed: 1.75; - --leading-code: 1.4; /* Specific for code blocks */ -} \ No newline at end of file diff --git a/js/cf-webapp/tailwind.config.ts b/js/cf-webapp/tailwind.config.ts index 4e78882be..1d627ab03 100644 --- a/js/cf-webapp/tailwind.config.ts +++ b/js/cf-webapp/tailwind.config.ts @@ -1,108 +1,20 @@ import type { Config } from "tailwindcss" +import { fontFamily } from "tailwindcss/defaultTheme" const config: Config = { - darkMode: 'class', content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { - spacing: { - 'px': '1px', - '0': '0', - '0.5': '4px', - '1': '8px', - '2': '16px', - '3': '24px', - '4': '32px', - '5': '40px', - '6': '48px', - '7': '56px', - '8': '64px', - '9': '72px', - '10': '80px', - }, - borderRadius: { - 'none': '0px', - 'sm': 'var(--radius-sm)', - DEFAULT: 'var(--radius-md)', - 'md': 'var(--radius-md)', - 'lg': 'var(--radius-lg)', - 'full': 'var(--radius-full)', - }, extend: { - colors: { - zinc: { - '50': 'rgb(var(--color-zinc-50) / )', - '100': 'rgb(var(--color-zinc-100) / )', - '200': 'rgb(var(--color-zinc-200) / )', - '300': 'rgb(var(--color-zinc-300) / )', - '400': 'rgb(var(--color-zinc-400) / )', - '500': 'rgb(var(--color-zinc-500) / )', - '600': 'rgb(var(--color-zinc-600) / )', - '700': 'rgb(var(--color-zinc-700) / )', - '800': 'rgb(var(--color-zinc-800) / )', - '900': 'rgb(var(--color-zinc-900) / )', - '950': 'rgb(var(--color-zinc-950) / )', - }, - background: 'rgb(var(--background) / )', - foreground: 'rgb(var(--foreground) / )', - card: { - DEFAULT: 'rgb(var(--card) / )', - foreground: 'rgb(var(--card-foreground) / )', - }, - popover: { - DEFAULT: 'rgb(var(--popover) / )', - foreground: 'rgb(var(--popover-foreground) / )', - }, - primary: { - DEFAULT: 'rgb(var(--primary) / )', - foreground: 'rgb(var(--primary-foreground) / )', - }, - secondary: { - DEFAULT: 'rgb(var(--secondary) / )', - foreground: 'rgb(var(--secondary-foreground) / )', - }, - muted: { - DEFAULT: 'rgb(var(--muted) / )', - foreground: 'rgb(var(--muted-foreground) / )', - }, - accent: { - DEFAULT: 'rgb(var(--accent) / )', - foreground: 'rgb(var(--accent-foreground) / )', - }, - error: { - DEFAULT: 'rgb(var(--error) / )', - foreground: 'rgb(var(--error-foreground) / )', - muted: 'rgb(var(--error-muted) / )', - border: 'rgb(var(--error-border) / )', - }, - warning: { - DEFAULT: 'rgb(var(--warning) / )', - foreground: 'rgb(var(--warning-foreground) / )', - muted: 'rgb(var(--warning-muted) / )', - border: 'rgb(var(--warning-border) / )', - }, - success: { - DEFAULT: 'rgb(var(--success) / )', - foreground: 'rgb(var(--success-foreground) / )', - muted: 'rgb(var(--success-muted) / )', - border: 'rgb(var(--success-border) / )', - }, - info: { - DEFAULT: 'rgb(var(--info) / )', - foreground: 'rgb(var(--info-foreground) / )', - muted: 'rgb(var(--info-muted) / )', - border: 'rgb(var(--info-border) / )', - }, - border: 'rgb(var(--border) / )', - input: 'rgb(var(--input) / )', - ring: 'rgb(var(--ring) / )', + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", }, fontFamily: { - sans: ['var(--font-sans)'], - mono: ['var(--font-mono)'], + sans: ["var(--font-sans)", ...fontFamily.sans], }, keyframes: { shimmer: { diff --git a/js/common/package-lock.json b/js/common/package-lock.json index 4b62693f3..3a1757a50 100644 --- a/js/common/package-lock.json +++ b/js/common/package-lock.json @@ -566,6 +566,7 @@ "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -616,6 +617,7 @@ "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", @@ -881,6 +883,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1890,6 +1893,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4075,6 +4079,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.15.0", "@prisma/engines": "6.15.0" @@ -5041,6 +5046,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From f03a06f4e114770198613c4682fd839e31409599 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:33:13 -0500 Subject: [PATCH 054/184] Reintroduce enriched obs_context for testgen LLM calls (#2377) ## Summary - Re-adds the enriched observability context from CF-1041 that was reverted - Passes `module_path`, `test_module_path`, `helper_function_names`, `is_async`, and `function_to_optimize` details to `call_llm` in testgen ## Test plan - [ ] Verify testgen LLM calls include the enriched context - [ ] Confirm no regressions in test generation flow --- django/aiservice/testgen/testgen.py | 32 +++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/django/aiservice/testgen/testgen.py b/django/aiservice/testgen/testgen.py index a1f3cdb15..eea95c21f 100644 --- a/django/aiservice/testgen/testgen.py +++ b/django/aiservice/testgen/testgen.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, TypedDict import sentry_sdk import stamina +from libcst import parse_module from ninja import NinjaAPI from ninja.errors import HttpError from openai import OpenAIError @@ -21,10 +22,9 @@ from aiservice.env_specific import debug_log_sensitive_data from aiservice.llm import EXECUTE_MODEL, HAIKU_MODEL, OPENAI_MODEL, calculate_llm_cost, call_llm from aiservice.models.functions_to_optimize import FunctionToOptimize from authapp.auth import AuthenticatedRequest +from languages.js_ts.testgen import testgen_javascript from log_features.log_event import update_optimization_cost from log_features.log_features import log_features -from libcst import parse_module - from testgen.instrumentation.edit_generated_test import replace_definition_with_import from testgen.instrumentation.instrument_new_tests import instrument_test_source from testgen.models import ( @@ -38,7 +38,6 @@ from testgen.models import ( from testgen.postprocessing.code_validator import CodeValidationError, has_test_functions, validate_testgen_code from testgen.postprocessing.postprocess_pipeline import postprocessing_testgen_pipeline from testgen.testgen_context import BaseTestGenContext, TestGenContextData -from languages.js_ts.testgen import testgen_javascript if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam @@ -241,8 +240,30 @@ async def generate_and_validate_test_code( call_sequence: int | None = None, function_to_optimize: FunctionToOptimize | None = None, module_path: str | None = None, + test_module_path: str | None = None, + helper_function_names: list[str] | None = None, + is_async: bool = False, ) -> str: - obs_context: dict | None = {"call_sequence": call_sequence} if call_sequence is not None else None + obs_context: dict | None = ( + { + "call_sequence": call_sequence, + "module_path": module_path, + "test_module_path": test_module_path, + "helper_function_names": helper_function_names, + "is_async": is_async, + "function_to_optimize": { + "function_name": function_to_optimize.function_name, + "file_path": function_to_optimize.file_path, + "qualified_name": function_to_optimize.qualified_name, + "starting_line": function_to_optimize.starting_line, + "ending_line": function_to_optimize.ending_line, + } + if function_to_optimize is not None + else None, + } + if call_sequence is not None + else None + ) response = await call_llm( llm=model, messages=messages, @@ -318,6 +339,9 @@ async def generate_regression_tests_from_function( call_sequence=call_sequence, function_to_optimize=data.function_to_optimize, module_path=data.module_path, + test_module_path=data.test_module_path, + helper_function_names=data.helper_function_names, + is_async=data.function_to_optimize.is_async or data.is_async or False, ) total_llm_cost = sum(cost_tracker) await update_optimization_cost(trace_id=trace_id, cost=total_llm_cost, user_id=user_id) From 47053591f42436327f64c1c36d628c87fc6d0a79 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:50:12 -0500 Subject: [PATCH 055/184] observability v2 toggle (#2378) --- .../tests/testgen/test_testgen_javascript.py | 4 +- js/cf-webapp/.gitignore | 5 +- js/cf-webapp/next.config.mjs | 21 +- js/cf-webapp/package-lock.json | 313 +++-- js/cf-webapp/package.json | 4 + .../src/app/(auth)/codeflash/auth/content.tsx | 16 +- .../apikeys/dialog-create-api-key.tsx | 6 +- .../review-optimizations/[traceId]/page.tsx | 19 +- .../[traceId]/profiler/page.tsx | 7 +- js/cf-webapp/src/app/dashboard/page.tsx | 21 +- js/cf-webapp/src/app/globals.css | 3 + js/cf-webapp/src/app/layout.tsx | 11 +- .../components/code-context-section.tsx | 378 ++++++ .../components/code-highlighter.tsx | 175 +++ .../observability/components/copy-button.tsx | 64 + .../components/errors-section.tsx | 204 ++++ .../function-to-optimize-section.tsx | 204 ++++ .../src/app/observability/components/index.ts | 14 + .../observability/components/info-icon.tsx | 38 + .../observability/components/python-parser.ts | 134 +++ .../components/timeline-page-view.tsx | 1069 +++++++++++++++++ .../components/timeline-types.ts | 324 +++++ .../observability/components/trace-search.tsx | 86 ++ .../components/trace-summary.tsx | 131 ++ .../src/app/observability/components/utils.ts | 16 + js/cf-webapp/src/app/observability/layout.tsx | 2 +- .../src/app/observability/loading.tsx | 39 + js/cf-webapp/src/app/observability/page.tsx | 345 ++++++ .../observability/observability-nav.tsx | 105 +- js/cf-webapp/src/styles/obs-theme.css | 49 + 30 files changed, 3632 insertions(+), 175 deletions(-) create mode 100644 js/cf-webapp/src/app/observability/components/code-context-section.tsx create mode 100644 js/cf-webapp/src/app/observability/components/code-highlighter.tsx create mode 100644 js/cf-webapp/src/app/observability/components/copy-button.tsx create mode 100644 js/cf-webapp/src/app/observability/components/errors-section.tsx create mode 100644 js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx create mode 100644 js/cf-webapp/src/app/observability/components/index.ts create mode 100644 js/cf-webapp/src/app/observability/components/info-icon.tsx create mode 100644 js/cf-webapp/src/app/observability/components/python-parser.ts create mode 100644 js/cf-webapp/src/app/observability/components/timeline-page-view.tsx create mode 100644 js/cf-webapp/src/app/observability/components/timeline-types.ts create mode 100644 js/cf-webapp/src/app/observability/components/trace-search.tsx create mode 100644 js/cf-webapp/src/app/observability/components/trace-summary.tsx create mode 100644 js/cf-webapp/src/app/observability/components/utils.ts create mode 100644 js/cf-webapp/src/app/observability/loading.tsx create mode 100644 js/cf-webapp/src/app/observability/page.tsx create mode 100644 js/cf-webapp/src/styles/obs-theme.css diff --git a/django/aiservice/tests/testgen/test_testgen_javascript.py b/django/aiservice/tests/testgen/test_testgen_javascript.py index ae114bbe0..c3d0da93a 100644 --- a/django/aiservice/tests/testgen/test_testgen_javascript.py +++ b/django/aiservice/tests/testgen/test_testgen_javascript.py @@ -294,9 +294,7 @@ class TestStripJsExtensions: """ # Copy of patterns from aiservice/languages/js_ts/testgen.py - _JS_EXTENSION_PATTERN = re.compile( - r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""" - ) + _JS_EXTENSION_PATTERN = re.compile(r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])""") _REQUIRE_EXTENSION_PATTERN = re.compile( r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))""" ) diff --git a/js/cf-webapp/.gitignore b/js/cf-webapp/.gitignore index 79b38816a..b6f7df743 100644 --- a/js/cf-webapp/.gitignore +++ b/js/cf-webapp/.gitignore @@ -42,4 +42,7 @@ next-env.d.ts /.npmrc .npmrc /.azure/config -*.next/* \ No newline at end of file +*.next/* + +# wasm (built by postinstall) +*.wasm \ No newline at end of file diff --git a/js/cf-webapp/next.config.mjs b/js/cf-webapp/next.config.mjs index 08455a984..455460cf2 100644 --- a/js/cf-webapp/next.config.mjs +++ b/js/cf-webapp/next.config.mjs @@ -1,11 +1,28 @@ /** @type {import("next").NextConfig} */ -let nextConfig = { +const nextConfig = { transpilePackages: ["@codeflash-ai/common"], - webpack: (config) => { + webpack: (config, { isServer }) => { config.watchOptions = { poll: 1000, aggregateTimeout: 300, } + + // Handle web-tree-sitter's Node.js module imports in browser. + // fallback handles static require(); alias handles dynamic import() + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + "fs/promises": false, + path: false, + module: false, + } + config.resolve.alias = { + ...config.resolve.alias, + module: false, + } + } + return config }, experimental: { diff --git a/js/cf-webapp/package-lock.json b/js/cf-webapp/package-lock.json index 4f7668962..476ce3d0c 100644 --- a/js/cf-webapp/package-lock.json +++ b/js/cf-webapp/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "codeflash-webapp", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@auth0/nextjs-auth0": "^3.3.0", "@azure/msal-node": "^3.7.3", @@ -63,6 +64,7 @@ "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", "tailwindcss-animate": "^1.0.7", + "web-tree-sitter": "^0.26.5", "zod": "^3.22.4" }, "devDependencies": { @@ -85,6 +87,8 @@ "prettier": "3.2.5", "prisma": "^6.7.0", "simple-git-hooks": "^2.9.0", + "tree-sitter-cli": "^0.26.3", + "tree-sitter-python": "^0.25.0", "typescript": "^5.4.5", "vitest": "^3.0.8" }, @@ -383,15 +387,6 @@ "node": ">=0.8.0" } }, - "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": { - "version": "15.13.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", - "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@azure/msal-common": { "version": "15.13.0", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", @@ -443,7 +438,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -832,7 +826,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -856,7 +849,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2098,6 +2090,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2371,7 +2364,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2422,7 +2414,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", @@ -3105,7 +3096,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz", "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -5184,7 +5174,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5272,6 +5263,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5282,6 +5274,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -5315,7 +5308,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -5364,7 +5358,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5415,7 +5408,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5426,7 +5418,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5455,13 +5446,6 @@ "@types/node": "*" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5513,7 +5497,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -6165,6 +6148,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -6174,25 +6158,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6203,13 +6191,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6222,6 +6212,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -6231,6 +6222,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -6239,13 +6231,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6262,6 +6256,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -6275,6 +6270,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -6287,6 +6283,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6301,6 +6298,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -6310,20 +6308,21 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6345,6 +6344,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -6393,6 +6393,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -6410,6 +6411,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6425,7 +6427,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ansi-escapes": { "version": "7.1.1", @@ -6529,6 +6532,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -6876,9 +6880,9 @@ } }, "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -6894,13 +6898,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -6919,7 +6922,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/builtin-modules": { "version": "3.3.0", @@ -7077,9 +7081,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "funding": [ { "type": "opencollective", @@ -7181,7 +7185,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -7220,6 +7223,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -7756,9 +7760,9 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -7788,13 +7792,15 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -7852,9 +7858,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.238", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.238.tgz", - "integrity": "sha512-khBdc+w/Gv+cS8e/Pbnaw/FXcBUeKrRVik9IxfXtgREOWyJhR4tj43n3amkVogJ/yeQUqzkrZcFhtIxIdqmmcQ==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -7874,13 +7880,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -8031,6 +8038,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -8162,7 +8170,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8386,7 +8393,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8877,6 +8883,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -9015,7 +9022,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fastq": { "version": "1.19.1", @@ -9494,7 +9502,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/glob/node_modules/minimatch": { "version": "8.0.4", @@ -10559,6 +10568,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -10573,6 +10583,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10589,7 +10600,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -10713,7 +10723,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -10926,6 +10937,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -11160,6 +11172,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -12181,6 +12194,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -12191,6 +12205,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -12275,14 +12290,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "14.2.35", "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", @@ -12366,6 +12381,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -12415,10 +12440,22 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", - "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/node-ts-cache": { @@ -12972,7 +13009,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -13147,7 +13183,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13347,9 +13382,9 @@ } }, "node_modules/preact": { - "version": "10.27.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", - "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "version": "10.28.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz", + "integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==", "license": "MIT", "funding": { "type": "opencollective", @@ -13388,6 +13423,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -13403,6 +13439,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -13415,7 +13452,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/prism-react-renderer": { "version": "2.4.1", @@ -13437,7 +13475,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" @@ -13544,9 +13581,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -13590,6 +13627,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -13610,7 +13648,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13633,7 +13670,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13647,7 +13683,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14005,6 +14040,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14185,7 +14221,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -14373,6 +14408,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -14409,6 +14445,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -14420,7 +14457,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "7.7.3", @@ -14439,6 +14477,7 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -14708,6 +14747,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14726,6 +14766,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15441,6 +15482,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" }, @@ -15454,6 +15496,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -15468,10 +15511,11 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -15505,7 +15549,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/text-table": { "version": "0.2.0", @@ -15637,6 +15682,40 @@ "node": ">=18" } }, + "node_modules/tree-sitter-cli": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.26.5.tgz", + "integrity": "sha512-joGY67M2XUVM+ZEs7vTYmSbiDgxtwbuMf1OdKk8q1Dd6wTlbhgtU/mr3j0krBgQs2Zwom6N7vxZaqoM85b79Mw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "tree-sitter": "cli.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tree-sitter-python": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz", + "integrity": "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -15818,7 +15897,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16057,9 +16135,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -16205,7 +16283,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16392,10 +16469,11 @@ } }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16404,6 +16482,12 @@ "node": ">=10.13.0" } }, + "node_modules/web-tree-sitter": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.5.tgz", + "integrity": "sha512-u9sl+q21VSKX2T8dhpQw8bMGGqNfwaIyuoYE3kdOQGVDrOqrmcS9GmaQoCS602iaFnuokn3WCHW374c7GAnuaQ==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -16415,10 +16499,11 @@ } }, "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -16428,22 +16513,22 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { @@ -16477,11 +16562,19 @@ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", "license": "MIT" }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT", + "peer": true + }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -16495,6 +16588,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -16856,7 +16950,6 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "devOptional": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/js/cf-webapp/package.json b/js/cf-webapp/package.json index 9fa2c2fdd..314701500 100644 --- a/js/cf-webapp/package.json +++ b/js/cf-webapp/package.json @@ -14,6 +14,7 @@ "prisma:generate": "npx prisma generate", "prisma:migrate": "npx prisma migrate dev", "prepare": "simple-git-hooks", + "postinstall": "cp node_modules/web-tree-sitter/web-tree-sitter.wasm public/ && npx tree-sitter build --wasm node_modules/tree-sitter-python -o public/tree-sitter-python.wasm", "format": "prettier --write \"**/*.{js,ts,tsx,json,md}\"", "format:check": "prettier --check \"**/*.{js,ts,tsx,json,md}\"" }, @@ -73,6 +74,7 @@ "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", "tailwindcss-animate": "^1.0.7", + "web-tree-sitter": "^0.26.5", "zod": "^3.22.4" }, "devDependencies": { @@ -95,6 +97,8 @@ "prettier": "3.2.5", "prisma": "^6.7.0", "simple-git-hooks": "^2.9.0", + "tree-sitter-cli": "^0.26.3", + "tree-sitter-python": "^0.25.0", "typescript": "^5.4.5", "vitest": "^3.0.8" }, diff --git a/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx b/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx index ed8e454c5..f6055e43e 100644 --- a/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx +++ b/js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx @@ -1,6 +1,7 @@ "use client" import LogoBox from "@/components/dashboard/logo-box" +import Image from "next/image" import { useState, useEffect } from "react" import { useRouter, useSearchParams } from "next/navigation" import { Loading } from "@/components/ui/loading" @@ -17,7 +18,8 @@ export default function CodeFlashAuthContent() { const [isLoading, setIsLoading] = useState(false) const [isCheckingAuth, setIsCheckingAuth] = useState(true) const [error, setError] = useState(null) - const [step, setStep] = useState<"checking" | "ready" | "authorizing" | "waiting">("checking") + type AuthStep = "checking" | "ready" | "authorizing" | "waiting" + const [step, setStep] = useState("checking") const [hasAuthenticated, setHasAuthenticated] = useState(false) const [theme, setTheme] = useState<"light" | "dark">("light") const [organizations, setOrganizations] = useState([]) @@ -224,8 +226,7 @@ export default function CodeFlashAuthContent() { return } - // Helper to get initials from name - const getInitials = (name: string) => { + function getInitials(name: string): string { return name .split(" ") .map(n => n[0]) @@ -287,14 +288,17 @@ export default function CodeFlashAuthContent() { }`} > {userInfo?.avatarUrl ? ( - {userInfo.name} ) : (
    - {userInfo ? getInitials(userInfo.name) : "U"} + {getInitials(userInfo?.name || "User")}
    )}
    @@ -327,7 +331,7 @@ export default function CodeFlashAuthContent() { }`} > {org.avatarUrl ? ( - {org.name} + {org.name} ) : (
    {getInitials(org.name)} diff --git a/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx b/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx index d9097c2ab..96c28623a 100644 --- a/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx +++ b/js/cf-webapp/src/app/(dashboard)/apikeys/dialog-create-api-key.tsx @@ -1,4 +1,5 @@ "use client" +import Image from "next/image" import { Button } from "@/components/ui/button" import { useForm } from "react-hook-form" import { @@ -175,10 +176,13 @@ export function CreateApiKeyDialog(): React.JSX.Element {
    {org.avatarUrl ? ( - {org.name} ) : ( diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx index 90ed1d440..9f548f0cc 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/page.tsx @@ -161,16 +161,9 @@ export default function OptimizationReviewPage() { // State for base branch dialog const [showBaseBranchDialog, setShowBaseBranchDialog] = useState(false) - // Store currentOrg.id in a ref to avoid unnecessary re-fetches - const currentOrgIdRef = useRef(currentOrg?.id) + const currentOrgId = currentOrg?.id useEffect(() => { - // Only re-fetch if org actually changed - if (currentOrgIdRef.current === currentOrg?.id && event !== null) { - return - } - currentOrgIdRef.current = currentOrg?.id - // Prevent concurrent calls if (isLoadingRef.current) { return @@ -183,8 +176,8 @@ export default function OptimizationReviewPage() { setUserId(userSession.userId) const data = await getOptimizationEventById({ - payload: currentOrg - ? { orgId: currentOrg.id } + payload: currentOrgId + ? { orgId: currentOrgId } : { userId: userSession.userId, username: userSession.username }, trace_id: params.traceId as string, }) @@ -287,7 +280,7 @@ export default function OptimizationReviewPage() { } } loadEvent() - }, [params.traceId, currentOrg?.id]) + }, [params.traceId, currentOrgId]) const loadComments = async (eventId: string) => { setLoadingComments(true) @@ -603,9 +596,9 @@ export default function OptimizationReviewPage() { window.open(constructedUrl, "_blank") } }, 1000) - } catch (error: any) { + } catch (error: unknown) { console.error("[handleCreatePR] Exception:", error) - const errorMessage = error?.message || "Failed to create pull request" + const errorMessage = error instanceof Error ? error.message : "Failed to create pull request" toast.error(errorMessage, { duration: 5000, }) diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx index c2b748a60..8262b2265 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx @@ -86,6 +86,7 @@ export default function LineProfilerPage() { const [event, setEvent] = useState(null) const [loading, setLoading] = useState(true) const { currentOrg } = useViewMode() + const currentOrgId = currentOrg?.id useEffect(() => { async function loadEvent() { @@ -93,8 +94,8 @@ export default function LineProfilerPage() { const userSession = (await getUserIdAndUsername()) ?? { userId: "", username: "" } const data = await getOptimizationEventById({ - payload: currentOrg - ? { orgId: currentOrg.id } + payload: currentOrgId + ? { orgId: currentOrgId } : { userId: userSession.userId, username: userSession.username }, trace_id: params.traceId as string, }) @@ -122,7 +123,7 @@ export default function LineProfilerPage() { } loadEvent() - }, [params.traceId, currentOrg?.id]) + }, [params.traceId, currentOrgId]) const handleBack = () => { router.push(`/review-optimizations/${params.traceId}`) diff --git a/js/cf-webapp/src/app/dashboard/page.tsx b/js/cf-webapp/src/app/dashboard/page.tsx index 399bec2ec..6714369f9 100644 --- a/js/cf-webapp/src/app/dashboard/page.tsx +++ b/js/cf-webapp/src/app/dashboard/page.tsx @@ -98,16 +98,17 @@ function Dashboard() { const startYear = format(last30DaysStart, "yyyy") const endYear = format(now, "yyyy") - let dateRangeDisplay: string - if (startMonth === endMonth && startYear === endYear) { - dateRangeDisplay = `${startMonth} ${format(last30DaysStart, "d")}-${format(now, "d")}, ${startYear}` - } else if (startYear === endYear) { - dateRangeDisplay = `${format(last30DaysStart, "MMMM d")} - ${format(now, "MMMM d")}, ${startYear}` - } else { - dateRangeDisplay = `${format(last30DaysStart, "MMMM d, yyyy")} - ${format(now, "MMMM d, yyyy")}` + function getDateRangeDisplay(): string { + if (startMonth === endMonth && startYear === endYear) { + return `${startMonth} ${format(last30DaysStart, "d")}-${format(now, "d")}, ${startYear}` + } + if (startYear === endYear) { + return `${format(last30DaysStart, "MMMM d")} - ${format(now, "MMMM d")}, ${startYear}` + } + return `${format(last30DaysStart, "MMMM d, yyyy")} - ${format(now, "MMMM d, yyyy")}` } - return { now, last30DaysStart, dateRangeDisplay } + return { now, last30DaysStart, dateRangeDisplay: getDateRangeDisplay() } }, []) const repoCounts = useMemo(() => { @@ -160,8 +161,8 @@ function Dashboard() { throw new Error("User authentication data not found") } - const payload: AccountPayload = currentOrg - ? { orgId: currentOrg.id } + const payload: AccountPayload = currentOrgId + ? { orgId: currentOrgId } : { userId: currentUser.userId, username: currentUser.username } // Store payload for the PR table component diff --git a/js/cf-webapp/src/app/globals.css b/js/cf-webapp/src/app/globals.css index adb19e016..878747854 100644 --- a/js/cf-webapp/src/app/globals.css +++ b/js/cf-webapp/src/app/globals.css @@ -2,6 +2,9 @@ @tailwind components; @tailwind utilities; +/* Scoped observability theme - only affects pages wrapped with .obs-v2 */ +@import "../styles/obs-theme.css"; + @layer base { :root { /* Background and foreground */ diff --git a/js/cf-webapp/src/app/layout.tsx b/js/cf-webapp/src/app/layout.tsx index 9281c9fa8..b34ff8053 100644 --- a/js/cf-webapp/src/app/layout.tsx +++ b/js/cf-webapp/src/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next" -import { Inter as FontSans } from "next/font/google" +import { Inter as FontSans, JetBrains_Mono } from "next/font/google" import "./globals.css" import { cn } from "@/lib/utils" import { ThemeProvider } from "@/components/theme-provider" @@ -23,6 +23,13 @@ const fontSans = FontSans({ variable: "--font-sans", }) +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700"], + variable: "--font-jetbrains-mono", + display: "swap", +}) + export const metadata: Metadata = { title: "Codeflash", description: "Optimize the performance of your code.", @@ -91,7 +98,7 @@ export default async function RootLayout({ }} /> - + >(new Set()) + const [expandedFiles, setExpandedFiles] = useState>(new Set()) + + const { rwFiles, roFiles, metrics } = useMemo(() => { + const rwFiles = originalCode ? parseMarkdownCodeBlocks(originalCode) : [] + const roFiles = dependencyCode ? parseMarkdownCodeBlocks(dependencyCode) : [] + const rwTokens = rwFiles.reduce((sum, f) => sum + f.tokens, 0) + const roTokens = roFiles.reduce((sum, f) => sum + f.tokens, 0) + const totalFiles = rwFiles.length + roFiles.length + const totalTokens = rwTokens + roTokens + const rwChars = rwFiles.reduce((sum, f) => sum + f.code.length, 0) + const roChars = roFiles.reduce((sum, f) => sum + f.code.length, 0) + + return { + rwFiles, + roFiles, + metrics: { rwTokens, roTokens, totalFiles, totalTokens, rwChars, roChars }, + } + }, [originalCode, dependencyCode]) + + const toggleSection = useCallback((section: string): void => { + setExpandedSections(prev => { + const next = new Set(prev) + if (next.has(section)) { + next.delete(section) + } else { + next.add(section) + } + return next + }) + }, []) + + const toggleFile = useCallback((fileKey: string): void => { + setExpandedFiles(prev => { + const next = new Set(prev) + if (next.has(fileKey)) { + next.delete(fileKey) + } else { + next.add(fileKey) + } + return next + }) + }, []) + + if (!originalCode && !dependencyCode) { + return null + } + + return ( +
    +
    setIsExpanded(!isExpanded)} + onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }} + className="w-full p-6 flex items-center justify-between hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors rounded-sm cursor-pointer" + > +
    + +

    Code Context

    + +
    +
    +
    + + + {metrics.totalTokens.toLocaleString()} tokens + + + + {metrics.totalFiles} files + +
    + +
    +
    + + {isExpanded && ( +
    + {(functionName || filePath) && ( +
    + {functionName && ( +
    + Function + + {functionName} + +
    + )} + {filePath && ( +
    + File + + {filePath} + +
    + )} +
    + )} + + {rwFiles.length > 0 && roFiles.length > 0 && ( +
    +
    + + + Token Distribution (estimated) + +
    + +
    +
    +
    + + Read-Writable: {metrics.rwTokens.toLocaleString()} ({rwFiles.length} files) + +
    +
    +
    + + Read-Only: {metrics.roTokens.toLocaleString()} ({roFiles.length} files) + +
    +
    +
    + )} + + {rwFiles.length > 0 && ( + toggleSection("rw")} + expandedFiles={expandedFiles} + onToggleFile={toggleFile} + sectionKey="rw" + /> + )} + + {roFiles.length > 0 && ( + toggleSection("ro")} + expandedFiles={expandedFiles} + onToggleFile={toggleFile} + sectionKey="ro" + /> + )} +
    + )} +
    + ) +}) + +interface CodeGroupSectionProps { + title: string + subtitle: string + accentColor: "emerald" | "slate" + tokenCount: number + charCount: number + files: ParsedFile[] + isExpanded: boolean + onToggle: () => void + expandedFiles: Set + onToggleFile: (fileKey: string) => void + sectionKey: string +} + +function getAccentColorClasses(accentColor: "emerald" | "slate"): { border: string; bg: string; icon: string } { + switch (accentColor) { + case "emerald": + return { + border: "border-zinc-200 dark:border-zinc-800", + bg: "bg-zinc-50 dark:bg-zinc-900", + icon: "text-emerald-500", + } + case "slate": + return { + border: "border-zinc-200 dark:border-zinc-800", + bg: "bg-zinc-50 dark:bg-zinc-900", + icon: "text-zinc-500", + } + } +} + +const CodeGroupSection = memo(function CodeGroupSection({ + title, + subtitle, + accentColor, + tokenCount, + charCount, + files, + isExpanded, + onToggle, + expandedFiles, + onToggleFile, + sectionKey, +}: CodeGroupSectionProps) { + const { border: borderColor, bg: bgColor, icon: iconColor } = getAccentColorClasses(accentColor) + + return ( +
    +
    { if (e.key === "Enter" || e.key === " ") onToggle() }} + className={`w-full p-4 flex items-center justify-between hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer ${bgColor}`} + > +
    + +
    + {title} + {subtitle} +
    +
    +
    + + {tokenCount.toLocaleString()} tokens · {charCount.toLocaleString()} chars · {files.length} files + + +
    +
    + + {isExpanded && ( +
    + {files.map((file, index) => { + const fileKey = `${sectionKey}-${index}` + const isFileExpanded = expandedFiles.has(fileKey) + return ( +
    +
    +
    onToggleFile(fileKey)} + onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggleFile(fileKey) }} + className="flex items-center gap-2 cursor-pointer hover:opacity-80 flex-1" + > + + + {file.filename} + + + {file.path !== file.filename && `(${file.path})`} + + +
    +
    + + {file.code.split("\n").length} lines + + +
    +
    + + {isFileExpanded && ( +
    + +
    + )} +
    + ) + })} +
    + )} +
    + ) +}) + +interface TokenDistributionBarProps { + rwTokens: number + roTokens: number + totalTokens: number +} + +const TokenDistributionBar = memo(function TokenDistributionBar({ + rwTokens, + roTokens, + totalTokens, +}: TokenDistributionBarProps) { + const rwPercent = Math.round((rwTokens / totalTokens) * 100) + const roPercent = Math.round((roTokens / totalTokens) * 100) + + return ( +
    +
    + {rwPercent > 15 && `${rwPercent}%`} +
    +
    + {roPercent > 15 && `${roPercent}%`} +
    +
    + ) +}) \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/code-highlighter.tsx b/js/cf-webapp/src/app/observability/components/code-highlighter.tsx new file mode 100644 index 000000000..49f5f0253 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/code-highlighter.tsx @@ -0,0 +1,175 @@ +"use client" + +import dynamic from "next/dynamic" +import { memo } from "react" + +const SyntaxHighlighter = dynamic( + () => import("react-syntax-highlighter").then(m => m.Prism), + { + ssr: false, + loading: () => ( +
    +
    +
    +
    +
    + ), + } +) + +export const zincDarkTheme = { + 'code[class*="language-"]': { + color: 'rgb(250, 250, 250)', + background: 'none', + fontFamily: 'var(--font-mono)', + fontSize: '1em', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + wordWrap: 'normal', + lineHeight: '1.5', + tabSize: 4, + hyphens: 'none', + }, + 'pre[class*="language-"]': { + color: 'rgb(250, 250, 250)', + background: 'rgb(24, 24, 27)', + fontFamily: 'var(--font-mono)', + fontSize: '1em', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + wordWrap: 'normal', + lineHeight: '1.5', + tabSize: 4, + hyphens: 'none', + padding: '1em', + margin: '0', + overflow: 'auto', + }, + comment: { + color: 'rgb(113, 113, 122)', + fontStyle: 'italic', + }, + prolog: { color: 'rgb(113, 113, 122)' }, + doctype: { color: 'rgb(113, 113, 122)' }, + cdata: { color: 'rgb(113, 113, 122)' }, + keyword: { color: 'rgb(96, 165, 250)' }, + 'control-flow': { color: 'rgb(96, 165, 250)' }, + string: { color: 'rgb(134, 239, 172)' }, + 'attr-value': { color: 'rgb(134, 239, 172)' }, + function: { color: 'rgb(253, 224, 71)' }, + 'class-name': { color: 'rgb(253, 224, 71)' }, + number: { color: 'rgb(251, 146, 60)' }, + boolean: { color: 'rgb(251, 146, 60)' }, + operator: { color: 'rgb(161, 161, 170)' }, + punctuation: { color: 'rgb(161, 161, 170)' }, + variable: { color: 'rgb(250, 250, 250)' }, + property: { color: 'rgb(250, 250, 250)' }, + tag: { color: 'rgb(96, 165, 250)' }, + 'attr-name': { color: 'rgb(250, 250, 250)' }, + namespace: { opacity: 0.7 }, + selector: { color: 'rgb(253, 224, 71)' }, + important: { + color: 'rgb(251, 146, 60)', + fontWeight: 'bold', + }, + atrule: { color: 'rgb(96, 165, 250)' }, + builtin: { color: 'rgb(253, 224, 71)' }, + entity: { + color: 'rgb(250, 250, 250)', + cursor: 'help', + }, + url: { + color: 'rgb(96, 165, 250)', + textDecoration: 'underline', + }, + inserted: { + color: 'rgb(134, 239, 172)', + background: 'rgba(134, 239, 172, 0.1)', + }, + deleted: { + color: 'rgb(248, 113, 113)', + background: 'rgba(248, 113, 113, 0.1)', + }, +} as const + +export const CODE_STYLE = { + margin: 0, + padding: "1rem", + fontSize: "0.875rem", + lineHeight: 1.5, + background: 'rgb(24, 24, 27)', +} as const + +export const CODE_STYLE_RELAXED = { + margin: 0, + padding: "1rem", + fontSize: "0.875rem", + lineHeight: 1.6, + background: 'rgb(24, 24, 27)', +} as const + +export const CODE_STYLE_SMALL = { + margin: 0, + padding: "1rem", + fontSize: "0.8125rem", + lineHeight: 1.5, + background: 'rgb(24, 24, 27)', +} as const + +interface CodeHighlighterProps { + code: string + language: string + showLineNumbers?: boolean + customStyle?: React.CSSProperties + highlightLines?: number[] +} + +const highlightStyle = { + backgroundColor: 'rgba(250, 204, 21, 0.15)', + display: 'block', + marginLeft: '-1rem', + marginRight: '-1rem', + paddingLeft: '1rem', + paddingRight: '1rem', + borderLeft: '3px solid rgb(250, 204, 21)', +} + +export const CodeHighlighter = memo(function CodeHighlighter({ + code, + language, + showLineNumbers = true, + customStyle = CODE_STYLE, + highlightLines, +}: CodeHighlighterProps) { + function getLineProps() { + if (!highlightLines || highlightLines.length === 0) return undefined + + return (lineNumber: number) => { + const isHighlighted = highlightLines.includes(lineNumber) + return { + style: isHighlighted ? highlightStyle : { display: 'block' }, + 'data-highlighted': isHighlighted ? 'true' : undefined, + } + } + } + const lineProps = getLineProps() + + const shouldWrapLines = !!(highlightLines && highlightLines.length > 0) + + return ( + + {code} + + ) +}) \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/copy-button.tsx b/js/cf-webapp/src/app/observability/components/copy-button.tsx new file mode 100644 index 000000000..cbbd38301 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/copy-button.tsx @@ -0,0 +1,64 @@ +"use client" + +import { useState, useEffect } from "react" +import { Copy, Check } from "lucide-react" +import { cn } from "@/lib/utils" + +interface CopyButtonProps { + text: string + label?: string + size?: "sm" | "md" + className?: string +} + +export function CopyButton({ text, label, size = "sm", className }: CopyButtonProps) { + const [copied, setCopied] = useState(false) + + // Clean up timeout on unmount + useEffect(() => { + let timeoutId: NodeJS.Timeout | undefined + + if (copied) { + timeoutId = setTimeout(() => setCopied(false), 2000) + } + + return () => { + if (timeoutId) { + clearTimeout(timeoutId) + } + } + }, [copied]) + + async function handleCopy(): Promise { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + } catch (err) { + console.error("Failed to copy text:", err) + } + } + + return ( + + ) +} diff --git a/js/cf-webapp/src/app/observability/components/errors-section.tsx b/js/cf-webapp/src/app/observability/components/errors-section.tsx new file mode 100644 index 000000000..a857bc7c1 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/errors-section.tsx @@ -0,0 +1,204 @@ +"use client" + +import { useState, useCallback, memo } from "react" +import { + XCircle, + AlertCircle, + AlertTriangle, + ChevronDown, +} from "lucide-react" +import { CopyButton } from "./copy-button" + +interface ErrorContext { + test_name?: string + failure_reason?: string + test_output?: string + expected?: string + actual?: string +} + +interface TraceError { + id: string + error_type: string + severity: string + error_message: string + context: ErrorContext | null + created_at: Date +} + +interface ErrorsSectionProps { + errors: TraceError[] +} + +export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSectionProps) { + const [expandedErrors, setExpandedErrors] = useState>(new Set()) + + const toggleError = useCallback((errorId: string) => { + setExpandedErrors(prev => { + const next = new Set(prev) + if (next.has(errorId)) { + next.delete(errorId) + } else { + next.add(errorId) + } + return next + }) + }, []) + + if (errors.length === 0) { + return null + } + + return ( +
    +
    +
    + +

    Errors

    + + {errors.length} + +
    +
    + +
    + {errors.map(error => { + const isExpanded = expandedErrors.has(error.id) + const isTestFailure = error.error_type === "test_failure" + const hasContext = error.context && Object.keys(error.context).length > 0 + + const SeverityIcon = + error.severity === "critical" || error.severity === "error" ? XCircle : AlertTriangle + + function getSeverityColor(severity: string): string { + switch (severity) { + case "critical": + return "text-red-400 border border-red-600 px-1.5 py-0.5" + case "error": + return "text-orange-400 border border-orange-600 px-1.5 py-0.5" + default: + return "text-yellow-400 border border-yellow-600 px-1.5 py-0.5" + } + } + const severityColor = getSeverityColor(error.severity) + + return ( +
    +
    +
    toggleError(error.id)} + onKeyDown={e => { if (e.key === "Enter" || e.key === " ") toggleError(error.id) }} + className="flex items-start gap-3 cursor-pointer hover:opacity-80 flex-1 transition-opacity duration-150" + > + +
    +
    + + {error.error_type} + + + {error.severity} + + + {new Date(error.created_at).toLocaleString()} + +
    +

    + {error.error_message} +

    +
    + {(hasContext || isTestFailure) && ( + + )} +
    +
    + +
    +
    + + {isExpanded && isTestFailure && error.context && ( +
    +
    +

    + Test Failure Details +

    + + {error.context.test_name && ( +
    + + Test Name + +

    + {error.context.test_name} +

    +
    + )} + + {error.context.failure_reason && ( +
    + + Failure Reason + +

    + {error.context.failure_reason} +

    +
    + )} + + {error.context.expected && ( +
    + + Expected + +
    +                          {String(error.context.expected)}
    +                        
    +
    + )} + + {error.context.actual && ( +
    + + Actual + +
    +                          {String(error.context.actual)}
    +                        
    +
    + )} + + {error.context.test_output && ( +
    + + Test Output + +
    +                          {String(error.context.test_output)}
    +                        
    +
    + )} +
    +
    + )} + + {isExpanded && !isTestFailure && hasContext && ( +
    +
    + + Context + +
    +                      {JSON.stringify(error.context, null, 2)}
    +                    
    +
    +
    + )} +
    + ) + })} +
    +
    + ) +}) \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx b/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx new file mode 100644 index 000000000..ed8eee1e3 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx @@ -0,0 +1,204 @@ +"use client" + +import { memo, useMemo, useState, useRef, useEffect } from "react" +import { Code, FileText, ChevronDown } from "lucide-react" +import { CodeHighlighter, CODE_STYLE_RELAXED } from "./code-highlighter" +import { CopyButton } from "./copy-button" +import { findFunctionInCode, type FunctionLocation } from "./python-parser" + +interface FunctionToOptimizeSectionProps { + functionName: string | null + filePath: string | null + originalCode: string | null +} + +interface ParsedFile { + path: string + filename: string + language: string + code: string +} + +function getFilename(path: string): string { + return path.split("/").pop() || path +} + +function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] { + const files: ParsedFile[] = [] + const regex = /```(\w+):([^\n]+)\n([\s\S]*?)```/g + let match + + while ((match = regex.exec(markdown)) !== null) { + const [, language, path, code] = match + files.push({ + path, + filename: getFilename(path), + language: language || "python", + code: code.trimEnd(), + }) + } + + return files +} + +export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection({ + functionName, + filePath, + originalCode, +}: FunctionToOptimizeSectionProps) { + const [isExpanded, setIsExpanded] = useState(true) + const [functionLocation, setFunctionLocation] = useState(null) + const [actualFile, setActualFile] = useState(null) + const codeContainerRef = useRef(null) + + const allFiles = useMemo(() => { + if (!originalCode) return [] + return parseMarkdownCodeBlocks(originalCode) + }, [originalCode]) + + useEffect(() => { + if (!functionName || allFiles.length === 0) { + setFunctionLocation(null) + setActualFile(null) + return + } + + let cancelled = false + + async function findFunction() { + const searchPromises = allFiles.map(async (file) => { + const location = await findFunctionInCode(file.code, functionName!) + return location ? { file, location } : null + }) + + const results = await Promise.all(searchPromises) + if (cancelled) return + + const found = results.find(r => r !== null) + if (found) { + setFunctionLocation(found.location) + setActualFile(found.file) + return + } + + let fallbackFile = allFiles[0] + if (filePath) { + const match = allFiles.find(f => + filePath.endsWith(f.path) || f.path.endsWith(filePath) || f.path === filePath + ) + if (match) fallbackFile = match + } + setFunctionLocation(null) + setActualFile(fallbackFile) + } + + findFunction() + return () => { cancelled = true } + }, [functionName, filePath, allFiles]) + + const functionFile = actualFile ?? allFiles[0] ?? null + + const functionLines = useMemo(() => { + if (!functionLocation) return null + const lines: number[] = [] + for (let i = functionLocation.startLine; i <= functionLocation.endLine; i++) { + lines.push(i) + } + return lines + }, [functionLocation]) + + useEffect(() => { + if (!isExpanded || !functionLocation || !codeContainerRef.current) return + + const scrollToFunction = () => { + if (!codeContainerRef.current) return + const container = codeContainerRef.current + + const lineHeight = 22.4 + const paddingTop = 16 + const targetLine = functionLocation.startLine - 1 + const scrollPosition = paddingTop + (targetLine * lineHeight) - (container.clientHeight / 3) + + container.scrollTo({ + top: Math.max(0, scrollPosition), + behavior: 'smooth' + }) + } + + const timer = setTimeout(scrollToFunction, 300) + return () => clearTimeout(timer) + }, [isExpanded, functionLocation]) + + if (!functionFile) { + return null + } + + const highlightLines = functionLines && functionLines.length > 0 ? functionLines : undefined + + return ( +
    +
    +
    setIsExpanded(!isExpanded)} + onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }} + className="flex items-center gap-3 cursor-pointer hover:opacity-80 flex-1" + > +
    + +
    +
    +

    + Function to Optimize +

    +
    + {functionName && ( + + {functionName} + + )} + {functionLocation && ( + + lines {functionLocation.startLine}-{functionLocation.endLine} + + )} +
    +
    + +
    +
    + +
    +
    + + {isExpanded && ( + <> +
    + + + {functionFile.filename} + + {functionFile.path !== functionFile.filename && ( + + ({functionFile.path}) + + )} + + {functionFile.code.split("\n").length} lines + +
    + +
    + +
    + + )} +
    + ) +}) \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/index.ts b/js/cf-webapp/src/app/observability/components/index.ts new file mode 100644 index 000000000..3bc896831 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/index.ts @@ -0,0 +1,14 @@ +// Barrel export for all observability components +export { TraceSearch } from "./trace-search" +export { TraceSummary } from "./trace-summary" +export { TimelinePageView } from "./timeline-page-view" +export { transformToTimelineSections } from "./timeline-types" +export { ErrorsSection } from "./errors-section" +export { FunctionToOptimizeSection } from "./function-to-optimize-section" +export { CodeContextSection } from "./code-context-section" +export { CodeHighlighter, CODE_STYLE, CODE_STYLE_RELAXED, CODE_STYLE_SMALL } from "./code-highlighter" +export { findFunctionInCode } from "./python-parser" +export type { FunctionLocation } from "./python-parser" +export { CopyButton } from "./copy-button" +export { InfoIcon } from "./info-icon" +export { getTraceSource } from "./utils" \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/info-icon.tsx b/js/cf-webapp/src/app/observability/components/info-icon.tsx new file mode 100644 index 000000000..a7a4da314 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/info-icon.tsx @@ -0,0 +1,38 @@ +"use client" + +import { HelpCircle } from "lucide-react" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { cn } from "@/lib/utils" + +interface InfoIconProps { + content: string + side?: "top" | "right" | "bottom" | "left" + className?: string +} + +export function InfoIcon({ content, side = "top", className }: InfoIconProps) { + return ( + + + + + + +

    {content}

    +
    +
    +
    + ) +} diff --git a/js/cf-webapp/src/app/observability/components/python-parser.ts b/js/cf-webapp/src/app/observability/components/python-parser.ts new file mode 100644 index 000000000..ad3a70d32 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/python-parser.ts @@ -0,0 +1,134 @@ +import type { Node, Parser as ParserType } from "web-tree-sitter" + +export interface FunctionLocation { + startLine: number + endLine: number +} + +let parserPromise: Promise | null = null + +async function getParser(): Promise { + if (typeof window === "undefined") { + return null + } + + if (!parserPromise) { + parserPromise = (async () => { + try { + const { Parser, Language } = await import("web-tree-sitter") + await Parser.init({ + locateFile: (scriptName: string) => `/${scriptName}`, + }) + const parser = new Parser() + const Python = await Language.load("/tree-sitter-python.wasm") + parser.setLanguage(Python) + return parser + } catch (error) { + console.error("Tree-sitter initialization failed:", error) + parserPromise = null + throw error + } + })() + } + return parserPromise +} + +export async function findFunctionInCode( + code: string, + functionName: string +): Promise { + try { + const parser = await getParser() + if (parser) { + const tree = parser.parse(code) + if (tree) { + const result = findFunctionNode(tree.rootNode, functionName) + if (result) { + return { + startLine: result.startPosition.row + 1, + endLine: result.endPosition.row + 1, + } + } + } + } + } catch (error) { + console.warn("Tree-sitter parse failed, trying regex fallback:", error) + } + + return findFunctionWithRegex(code, functionName) +} + +function findFunctionWithRegex( + code: string, + functionName: string +): FunctionLocation | null { + const lines = code.split("\n") + + const defPattern = new RegExp( + `^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(` + ) + + let startLine = -1 + let startIndent = -1 + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + if (startLine === -1) { + const match = line.match(defPattern) + if (match) { + startLine = i + 1 + startIndent = match[1].length + } + } else { + const trimmed = line.trim() + if (trimmed === "" || trimmed.startsWith("#")) { + continue + } + + const currentIndent = line.length - line.trimStart().length + if (currentIndent <= startIndent) { + return { startLine, endLine: i } + } + } + } + + if (startLine !== -1) { + return { startLine, endLine: lines.length } + } + + return null +} + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} + +function findFunctionNode(node: Node, functionName: string): Node | null { + if ( + node.type === "function_definition" || + node.type === "async_function_definition" + ) { + const nameNode = node.childForFieldName("name") + if (nameNode && nameNode.text === functionName) { + return node + } + } + + if (node.type === "class_definition") { + const classBody = node.childForFieldName("body") + if (classBody) { + for (const child of classBody.children) { + const result = findFunctionNode(child, functionName) + if (result) return result + } + } + } + + for (const child of node.children) { + const result = findFunctionNode(child, functionName) + if (result) return result + } + + return null +} \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx b/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx new file mode 100644 index 000000000..d2a69a57e --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx @@ -0,0 +1,1069 @@ +"use client" + +import { useState, useRef, useEffect, memo, useMemo } from "react" +import { + Clock, + FlaskConical, + Activity, + Box, + RefreshCw, + ChevronDown, + FileText, + Code, + GitCompare, + CheckCircle2, + XCircle, + AlertCircle, + BarChart3, + Bug, +} from "lucide-react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" +import type { TimelineSection, TimelineSectionContent, LLMCallDebugData } from "./timeline-types" + +function stripCodeHeader(code: string): string { + let lines = code.split("\n") + if (lines[0] && /^`{3}[a-z]*(:.*)?$/i.test(lines[0].trim())) { + lines = lines.slice(1) + } + if (lines.length > 0 && lines[lines.length - 1]?.trim() === "```") { + lines = lines.slice(0, -1) + } + return lines.join("\n") +} + +interface TimelinePageViewProps { + sections: TimelineSection[] + totalDuration: number + functionName?: string | null + filePath?: string | null +} + +const TYPE_CONFIG = { + test_generation: { icon: FlaskConical }, + optimization: { icon: Box }, + line_profiler: { icon: Activity }, + refinement: { icon: RefreshCw }, + ranking: { icon: BarChart3 }, + summary: { icon: CheckCircle2 }, +} + +function formatTime(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms` + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` + return `${(ms / 60000).toFixed(1)}m` +} + +function getStatusIcon(status: string) { + switch (status) { + case "success": + return + case "failed": + return + case "partial": + return + default: + return + } +} + +interface ParsedCodeBlock { + language: string + filename: string | null + path: string | null + code: string +} + +function parseCodeBlock(rawCode: string): ParsedCodeBlock { + const markdownMatch = rawCode.match(/^```(\w+)(?::([^\n]+))?\n([\s\S]*?)```\s*$/) + if (markdownMatch) { + const [, language, path, code] = markdownMatch + const filename = path ? path.split("/").pop() || null : null + return { language: language || "python", filename, path: path || null, code: code.trimEnd() } + } + return { language: "python", filename: null, path: null, code: rawCode } +} + +function parseAllCodeBlocks(markdown: string): ParsedCodeBlock[] { + const files: ParsedCodeBlock[] = [] + const regex = /```(\w+)(?::([^\n]+))?\n([\s\S]*?)```/g + let match + + while ((match = regex.exec(markdown)) !== null) { + const [, language, path, code] = match + const filename = path ? path.split("/").pop() || null : null + files.push({ + path: path || null, + filename, + language: language || "python", + code: code.trimEnd(), + }) + } + + if (files.length === 0 && markdown.trim()) { + return [parseCodeBlock(markdown)] + } + + return files +} + +function findMatchingFile( + files: ParsedCodeBlock[], + targetPath: string | null +): ParsedCodeBlock | null { + if (!targetPath || files.length === 0) return files[0] || null + + const exactMatch = files.find(f => f.path === targetPath) + if (exactMatch) return exactMatch + + const targetFilename = targetPath.split("/").pop() + const filenameMatch = files.find(f => f.filename === targetFilename) + if (filenameMatch) return filenameMatch + + const partialMatch = files.find(f => + f.path && (targetPath.endsWith(f.path) || f.path.endsWith(targetPath)) + ) + if (partialMatch) return partialMatch + + return files[0] || null +} + +/** Renders prompt content with syntax-highlighted code blocks */ +const PromptContent = memo(function PromptContent({ content }: { content: string }) { + const parts = useMemo(() => { + const result: { type: "text" | "code"; content: string; language?: string }[] = [] + const codeBlockRegex = /```(\w+)?\n?([\s\S]*?)```/g + let lastIndex = 0 + let match + + while ((match = codeBlockRegex.exec(content)) !== null) { + if (match.index > lastIndex) { + result.push({ type: "text", content: content.slice(lastIndex, match.index) }) + } + result.push({ + type: "code", + content: match[2].trim(), + language: match[1] || "python", + }) + lastIndex = match.index + match[0].length + } + + if (lastIndex < content.length) { + result.push({ type: "text", content: content.slice(lastIndex) }) + } + + return result.length > 0 ? result : [{ type: "text" as const, content }] + }, [content]) + + return ( +
    + {parts.map((part, index) => + part.type === "code" ? ( +
    + +
    + ) : ( +
    +            {part.content}
    +          
    + ) + )} +
    + ) +}) + +interface LLMCallDebugDialogProps { + debugData: LLMCallDebugData + title: string + model?: string | null +} + +const LLMCallDebugDialog = memo(function LLMCallDebugDialog({ + debugData, + title, + model, +}: LLMCallDebugDialogProps) { + const [open, setOpen] = useState(false) + const [activeTab, setActiveTab] = useState<"user" | "system">("user") + const [showResponse, setShowResponse] = useState(false) + const [contentReady, setContentReady] = useState(false) + + useEffect(() => { + if (open) { + const timer = requestAnimationFrame(() => setContentReady(true)) + return () => cancelAnimationFrame(timer) + } else { + setContentReady(false) + setShowResponse(false) + } + }, [open]) + + const hasContent = debugData.systemPrompt || debugData.userPrompt || debugData.rawResponse + + if (!hasContent) return null + + return ( + + + + + + +
    + + + {title} + + {/* Response debug button */} + +
    + {model && ( +
    + + {model} + +
    + )} +
    + + {showResponse ? ( + /* Raw Response View */ +
    + {debugData.rawResponse ? ( +
    +                {debugData.rawResponse}
    +              
    + ) : ( + No response + )} +
    + ) : ( + /* Prompts View */ + setActiveTab(v as "user" | "system")} className="flex-1 flex flex-col min-h-0 mt-3"> + + + User Prompt + + ({(debugData.userPrompt?.length || 0).toLocaleString()} chars) + + + + System Prompt + + ({(debugData.systemPrompt?.length || 0).toLocaleString()} chars) + + + + +
    + {!contentReady ? ( +
    +
    +
    +
    +
    + ) : activeTab === "user" ? ( + debugData.userPrompt ? ( + + ) : ( + No user prompt + ) + ) : ( + debugData.systemPrompt ? ( +
    +                    {debugData.systemPrompt}
    +                  
    + ) : ( + No system prompt + ) + )} +
    + + )} + +
    + ) +}) + +const DiffView = memo(function DiffView({ diff }: { diff: string }) { + const lines = diff.split("\n") + + function shouldSkipLine(line: string, index: number): boolean { + // Skip empty last line + if (index === lines.length - 1 && line === "") return true + + // Skip standalone + or - markers or empty additions/deletions + const isEmptyAddition = line.startsWith("+") && line.substring(1).trim() === "" + const isEmptyDeletion = line.startsWith("-") && line.substring(1).trim() === "" + if (line === "+" || line === "-" || isEmptyAddition || isEmptyDeletion) return true + + // Skip "No newline" markers + if (line.startsWith("\\ No newline") || line.startsWith("\\")) return true + + return false + } + + function getDiffLineStyle(line: string) { + if (line.startsWith("@@")) { + return { + bgClass: "bg-blue-900/30", + textClass: "text-blue-400", + lineContent: line, + indicator: null, + borderClass: "border-transparent" + } + } + + if (line.startsWith("+")) { + return { + bgClass: "bg-green-900/40", + textClass: "text-green-300", + lineContent: line.substring(1), + indicator: +, + borderClass: "border-green-500" + } + } + + if (line.startsWith("-")) { + return { + bgClass: "bg-red-900/40", + textClass: "text-red-300", + lineContent: line.substring(1), + indicator: , + borderClass: "border-red-500" + } + } + + if (line.startsWith(" ")) { + return { + bgClass: "", + textClass: "text-zinc-300", + lineContent: line.substring(1), + indicator: null, + borderClass: "border-transparent" + } + } + + return { + bgClass: "", + textClass: "text-zinc-300", + lineContent: line, + indicator: null, + borderClass: "border-transparent" + } + } + + return ( +
    + {lines.map((line, index) => { + if (shouldSkipLine(line, index)) return null + + const { bgClass, textClass, lineContent, indicator, borderClass } = getDiffLineStyle(line) + + return ( +
    +
    + {indicator} +
    +
    +              {lineContent || " "}
    +            
    +
    + ) + })} +
    + ) +}) + +const TestContent = memo(function TestContent({ content }: { content: Extract }) { + const [showDetails, setShowDetails] = useState(false) + const [expandedTest, setExpandedTest] = useState(null) + const [activeVariant, setActiveVariant] = useState<"generated" | "instrumented" | "instrumentedPerf">("generated") + + const testCount = content.testGroups.length + const hasInstrumented = content.testGroups.some(g => g.instrumented) + const hasInstrumentedPerf = content.testGroups.some(g => g.instrumentedPerf) + + return ( +
    +
    +
    +
    + + + {testCount} test{testCount !== 1 ? "s" : ""} generated + +
    +
    + {content.testFramework && ( + + {content.testFramework} + + )} + {hasInstrumented && ( + + +behavior + + )} + {hasInstrumentedPerf && ( + + +perf + + )} +
    +
    + +
    + + {showDetails && ( +
    + {content.testGroups.map((group) => { + const isExpanded = expandedTest === group.index + const hasMultipleVariants = [group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length > 1 + + function getCurrentCode() { + switch (activeVariant) { + case "generated": return group.generated + case "instrumented": return group.instrumented + case "instrumentedPerf": return group.instrumentedPerf + default: return null + } + } + const currentCode = getCurrentCode() + + return ( +
    + + + {isExpanded && ( +
    + {hasMultipleVariants && ( +
    + {group.generated && ( + + )} + {group.instrumented && ( + + )} + {group.instrumentedPerf && ( + + )} +
    + )} + +
    + {currentCode ? ( + + ) : ( +
    + {(() => { + switch (activeVariant) { + case "generated": return "No generated test available" + case "instrumented": return "No instrumented behavior test available" + case "instrumentedPerf": return "No instrumented perf test available" + default: return "No test available" + } + })()} +
    + )} +
    +
    + )} +
    + ) + })} +
    + )} +
    + ) +}) + +const CandidateContent = memo(function CandidateContent({ + content, + isActive, +}: { + content: Extract + isActive: boolean +}) { + const [viewMode, setViewMode] = useState<"code" | "diff">("diff") + const [selectedFileIndex, setSelectedFileIndex] = useState(0) + const [unifiedDiff, setUnifiedDiff] = useState(null) + const [diffLoading, setDiffLoading] = useState(false) + + const originalCode = content.type === "refinement" ? content.parentCode : content.originalCode + + const candidateFiles = useMemo(() => parseAllCodeBlocks(content.code), [content.code]) + const originalFiles = useMemo(() => originalCode ? parseAllCodeBlocks(originalCode) : [], [originalCode]) + + const selectedCandidateFile = candidateFiles[selectedFileIndex] || candidateFiles[0] + + const matchingOriginalFile = useMemo(() => { + if (!selectedCandidateFile || originalFiles.length === 0) return null + return findMatchingFile(originalFiles, selectedCandidateFile.path) + }, [selectedCandidateFile, originalFiles]) + + useEffect(() => { + setUnifiedDiff(null) + }, [selectedFileIndex]) + + useEffect(() => { + if (viewMode !== "diff" || !matchingOriginalFile || !selectedCandidateFile || unifiedDiff !== null) { + return + } + + setDiffLoading(true) + import("diff").then(({ createTwoFilesPatch }) => { + const filename = selectedCandidateFile.filename || matchingOriginalFile.filename || "code.py" + const diff = createTwoFilesPatch( + `a/${filename}`, + `b/${filename}`, + matchingOriginalFile.code, + selectedCandidateFile.code, + "", + "", + { context: 3 } + ) + + const lines = diff.split("\n") + const hunkStartIndex = lines.findIndex(line => line.startsWith("@@")) + setUnifiedDiff(hunkStartIndex > 0 ? lines.slice(hunkStartIndex).join("\n") : diff) + setDiffLoading(false) + }).catch(error => { + console.error("Failed to load diff library:", error) + setDiffLoading(false) + }) + }, [viewMode, matchingOriginalFile, selectedCandidateFile, unifiedDiff]) + + const hasDiff = matchingOriginalFile !== null + const hasMultipleFiles = candidateFiles.length > 1 + + const codeContainerStyle = useMemo( + () => ({ maxHeight: isActive ? "70vh" : "200px" }), + [isActive] + ) + + return ( +
    +
    + {content.rank != null && ( + + #{content.rank} + + )} + {content.isBest && ( + + Best + + )} +
    + + {content.explanation && ( +

    + {content.explanation} +

    + )} + +
    + {hasDiff && ( +
    + + +
    + )} + + {hasMultipleFiles && ( + + )} +
    + + {viewMode === "code" ? ( + selectedCandidateFile ? ( +
    +
    +
    + + + {selectedCandidateFile.filename || "Code"} + + {selectedCandidateFile.path && selectedCandidateFile.path !== selectedCandidateFile.filename && ( + + ({selectedCandidateFile.path}) + + )} +
    + + {selectedCandidateFile.code.split("\n").length} lines + +
    +
    + +
    +
    + ) : ( +
    + No code available +
    + ) + ) : diffLoading ? ( +
    +
    +
    +
    +
    +
    +
    + ) : unifiedDiff ? ( +
    + +
    + ) : ( +
    + No original code available for comparison +
    + )} +
    + ) +}) + +const RankingContent = memo(function RankingContent({ content }: { content: Extract }) { + return ( +
    + {content.explanation && ( +
    +

    + {content.explanation} +

    +
    + )} + + {content.rankings.length >= 1 && ( +
    + {content.rankings.map((item) => ( +
    +
    + + {item.label} + + + Rank #{item.rank} + + {item.isBest && ( + + Best + + )} + {item.isBest && content.usedForPr && ( + + Used for PR + + )} +
    +
    + +
    +
    + ))} +
    + )} + +
    + ) +}) + +const SummaryContent = memo(function SummaryContent({ content }: { content: Extract }) { + const { metrics } = content + return ( +
    +
    +
    Total Duration
    +
    + {formatTime(metrics.totalDuration)} +
    +
    +
    +
    Total Cost
    +
    + ${metrics.totalCost.toFixed(4)} +
    +
    +
    +
    Total Tokens
    +
    + {metrics.totalTokens.toLocaleString()} +
    +
    +
    +
    Candidates
    +
    + {metrics.candidatesCount} +
    +
    +
    + ) +}) + +const TimelineSectionCard = memo(function TimelineSectionCard({ + section, + isActive, + index, + totalSections, +}: { + section: TimelineSection + isActive: boolean + index: number + totalSections: number +}) { + const config = TYPE_CONFIG[section.type] + const Icon = config.icon + + return ( +
    +
    + +
    +
    + + +{formatTime(section.timestamp)} + + {section.duration && ( + <> + · + + {formatTime(section.duration)} + + + )} +
    + + {index + 1}/{totalSections} + +
    + +
    +
    +
    + +
    +

    + {section.title} +

    + {section.subtitle && ( +

    + {section.subtitle} +

    + )} +
    +
    + {getStatusIcon(section.status)} + {section.model && ( + + {section.model} + + )} + {section.cost != null && ( + + ${section.cost.toFixed(4)} + + )} + {section.debugData && ( + + )} +
    +
    +
    + +
    + {section.content.type === "tests" && } + {(section.content.type === "candidate" || section.content.type === "refinement") && ( + + )} + {section.content.type === "ranking" && } + {section.content.type === "summary" && } +
    +
    +
    +
    + ) +}) + +export const TimelinePageView = memo(function TimelinePageView({ + sections, + totalDuration, + functionName, + filePath, +}: TimelinePageViewProps) { + const [activeIndex, setActiveIndex] = useState(0) + const sectionRefs = useRef<(HTMLDivElement | null)[]>([]) + const rafId = useRef(null) + + useEffect(() => { + const handleScroll = () => { + if (rafId.current !== null) return + rafId.current = requestAnimationFrame(() => { + rafId.current = null + const scrollTarget = window.innerHeight * 0.35 + + let closestIndex = 0 + let closestDistance = Infinity + + sectionRefs.current.forEach((ref, index) => { + if (ref) { + const rect = ref.getBoundingClientRect() + const sectionMiddle = rect.top + rect.height / 2 + const distance = Math.abs(sectionMiddle - scrollTarget) + + if (distance < closestDistance) { + closestDistance = distance + closestIndex = index + } + } + }) + + setActiveIndex(closestIndex) + }) + } + + window.addEventListener("scroll", handleScroll, { passive: true }) + handleScroll() + return () => { + window.removeEventListener("scroll", handleScroll) + if (rafId.current !== null) { + cancelAnimationFrame(rafId.current) + } + } + }, [sections.length]) + + if (sections.length === 0) { + return ( +
    + No timeline data available +
    + ) + } + + return ( +
    +
    +
    +
    +
    +

    + Optimization Timeline +

    + {functionName && ( +

    + {functionName} + {filePath && in {filePath}} +

    + )} +
    +
    + + {activeIndex + 1} of {sections.length} · {formatTime(totalDuration)} + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + {sections.map((section, index) => ( +
    { sectionRefs.current[index] = el }} + className="scroll-mt-24" + > + +
    + ))} + +
    +
    +
    +
    + + End + +
    +
    +
    +
    + ) +}) \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/timeline-types.ts b/js/cf-webapp/src/app/observability/components/timeline-types.ts new file mode 100644 index 000000000..e055a0960 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/timeline-types.ts @@ -0,0 +1,324 @@ +export interface LLMCallDebugData { + systemPrompt: string | null + userPrompt: string | null + rawResponse: string | null +} + +export interface TimelineSection { + id: string + type: "test_generation" | "optimization" | "line_profiler" | "refinement" | "ranking" | "summary" + title: string + subtitle?: string + timestamp: number + duration?: number + status: "success" | "failed" | "partial" | "pending" + model?: string | null + cost?: number | null + tokens?: number | null + content: TimelineSectionContent + debugData?: LLMCallDebugData +} + +export interface TestGroup { + index: number + generated?: { code: string; lines: number } + instrumented?: { code: string; lines: number } + instrumentedPerf?: { code: string; lines: number } +} + +export type TimelineSectionContent = + | { type: "tests"; testGroups: TestGroup[]; testFramework?: string } + | { type: "candidate"; code: string; originalCode: string | null; explanation?: string; rank?: number; isBest?: boolean } + | { type: "refinement"; code: string; parentCode: string | null; explanation?: string; rank?: number; isBest?: boolean } + | { type: "ranking"; explanation: string; rankings: Array<{ id: string; rank: number; label: string; code: string; isBest: boolean }>; usedForPr: boolean } + | { type: "summary"; metrics: { totalCost: number; totalTokens: number; totalDuration: number; candidatesCount: number } } + +export interface TransformInput { + calls: Array<{ + id: string + call_type: string | null + model_name: string | null + status: string + latency_ms: number | null + llm_cost: number | null + total_tokens: number | null + created_at: Date + context: { call_sequence?: number } | null + system_prompt?: string | null + user_prompt?: string | null + raw_response?: string | null + }> + optimizationCandidates: Array<{ + id: string + code: string + explanation?: string + index: number + }> + lineProfilerCandidates: Array<{ + id: string + code: string + explanation?: string + index: number + }> + refinementCandidates: Array<{ + id: string + code: string + explanation?: string + parentId: string | null + index: number + }> + generatedTests: Array<{ code: string; index: number }> + instrumentedTests: Array<{ code: string; index: number }> + instrumentedPerfTests: Array<{ code: string; index: number }> + originalCode: string | null + testFramework: string | null + candidateRankMap: Record + bestCandidateId: string | null + rankingExplanation: string | null + usedForPr: boolean +} + +export function transformToTimelineSections(input: TransformInput): { sections: TimelineSection[]; totalDuration: number } { + const { calls, optimizationCandidates, lineProfilerCandidates, refinementCandidates, generatedTests, instrumentedTests, instrumentedPerfTests, originalCode, testFramework, candidateRankMap, bestCandidateId, rankingExplanation, usedForPr } = input + + if (calls.length === 0) { + return { sections: [], totalDuration: 0 } + } + + const timestamps = calls.map(c => new Date(c.created_at).getTime()) + const minTime = Math.min(...timestamps) + const maxTime = Math.max(...timestamps) + const maxLatency = Math.max(...calls.map(c => c.latency_ms ?? 0)) + const totalDuration = maxTime - minTime + maxLatency + + const sections: TimelineSection[] = [] + + const maxTestIndex = Math.max( + generatedTests.length, + instrumentedTests.length, + instrumentedPerfTests.length + ) + + const testGroups: TestGroup[] = [] + for (let i = 1; i <= maxTestIndex; i++) { + const generated = generatedTests.find(t => t.index === i) + const instrumented = instrumentedTests.find(t => t.index === i) + const instrumentedPerf = instrumentedPerfTests.find(t => t.index === i) + + if (generated || instrumented || instrumentedPerf) { + testGroups.push({ + index: i, + generated: generated ? { code: generated.code, lines: generated.code.split("\n").length } : undefined, + instrumented: instrumented ? { code: instrumented.code, lines: instrumented.code.split("\n").length } : undefined, + instrumentedPerf: instrumentedPerf ? { code: instrumentedPerf.code, lines: instrumentedPerf.code.split("\n").length } : undefined, + }) + } + } + + const testCalls = calls.filter(c => c.call_type === "test_generation") + if (testCalls.length > 0 || testGroups.length > 0) { + const firstTestCall = testCalls[0] + const firstTimestamp = firstTestCall ? new Date(firstTestCall.created_at).getTime() - minTime : 0 + const totalTestDuration = testCalls.reduce((sum, c) => sum + (c.latency_ms ?? 0), 0) + const totalTestCost = testCalls.reduce((sum, c) => sum + (c.llm_cost ?? 0), 0) + const totalTestTokens = testCalls.reduce((sum, c) => sum + (c.total_tokens ?? 0), 0) + const allSuccess = testCalls.length === 0 || testCalls.every(c => c.status === "success") + const anyFailed = testCalls.some(c => c.status === "failed") + + const subtitle = testFramework + ? `${testGroups.length} test${testGroups.length > 1 ? "s" : ""} using ${testFramework}` + : `${testGroups.length} test${testGroups.length > 1 ? "s" : ""} generated` + + sections.push({ + id: firstTestCall ? `tests-${firstTestCall.id}` : "tests", + type: "test_generation", + title: "Test Generation", + subtitle, + timestamp: firstTimestamp, + duration: totalTestDuration, + status: allSuccess ? "success" : anyFailed ? "failed" : "partial", + model: firstTestCall?.model_name ?? null, + cost: totalTestCost, + tokens: totalTestTokens, + content: { + type: "tests", + testGroups, + testFramework: testFramework ?? undefined, + }, + debugData: firstTestCall ? { + systemPrompt: firstTestCall.system_prompt ?? null, + userPrompt: firstTestCall.user_prompt ?? null, + rawResponse: firstTestCall.raw_response ?? null, + } : undefined, + }) + } + + const callIndexByType = new Map() + for (const call of calls) { + const timestamp = new Date(call.created_at).getTime() - minTime + const callType = call.call_type || "unknown" + const typeIndex = callIndexByType.get(callType) ?? 0 + callIndexByType.set(callType, typeIndex + 1) + + if (callType === "optimization") { + const optIndex = typeIndex + const candidate = optimizationCandidates[optIndex] + if (candidate) { + const rank = candidateRankMap[candidate.id] + sections.push({ + id: call.id, + type: "optimization", + title: `Optimization Candidate ${candidate.index}`, + timestamp, + duration: call.latency_ms ?? undefined, + status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", + model: call.model_name, + cost: call.llm_cost, + tokens: call.total_tokens, + content: { + type: "candidate", + code: candidate.code, + originalCode, + explanation: candidate.explanation, + rank, + isBest: candidate.id === bestCandidateId, + }, + debugData: { + systemPrompt: call.system_prompt ?? null, + userPrompt: call.user_prompt ?? null, + rawResponse: call.raw_response ?? null, + }, + }) + } + } else if (callType === "line_profiler") { + const lpIndex = typeIndex + const candidate = lineProfilerCandidates[lpIndex] + if (candidate) { + const rank = candidateRankMap[candidate.id] + sections.push({ + id: call.id, + type: "line_profiler", + title: `Line Profiler Candidate ${candidate.index}`, + subtitle: "Guided by profiling data", + timestamp, + duration: call.latency_ms ?? undefined, + status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", + model: call.model_name, + cost: call.llm_cost, + tokens: call.total_tokens, + content: { + type: "candidate", + code: candidate.code, + originalCode, + explanation: candidate.explanation, + rank, + isBest: candidate.id === bestCandidateId, + }, + debugData: { + systemPrompt: call.system_prompt ?? null, + userPrompt: call.user_prompt ?? null, + rawResponse: call.raw_response ?? null, + }, + }) + } + } else if (callType === "refinement") { + const refIndex = typeIndex + const candidate = refinementCandidates[refIndex] + if (candidate) { + const rank = candidateRankMap[candidate.id] + const parentCandidate = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates].find(c => c.id === candidate.parentId) + const parentLabel = parentCandidate + ? (parentCandidate as { source?: string }).source === "REFINE" + ? `From Refinement ${parentCandidate.index}` + : `From Candidate ${parentCandidate.index}` + : undefined + sections.push({ + id: call.id, + type: "refinement", + title: `Refinement ${candidate.index}`, + subtitle: parentLabel, + timestamp, + duration: call.latency_ms ?? undefined, + status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", + model: call.model_name, + cost: call.llm_cost, + tokens: call.total_tokens, + content: { + type: "refinement", + code: candidate.code, + parentCode: parentCandidate?.code ?? originalCode, + explanation: candidate.explanation, + rank, + isBest: candidate.id === bestCandidateId, + }, + debugData: { + systemPrompt: call.system_prompt ?? null, + userPrompt: call.user_prompt ?? null, + rawResponse: call.raw_response ?? null, + }, + }) + } + } else if (callType === "ranking") { + const allCandidates = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates] + const rankings = Object.entries(candidateRankMap) + .sort(([, a], [, b]) => a - b) + .map(([id]) => { + const cand = allCandidates.find(c => c.id === id) + if (!cand) return null + const source = (cand as { source?: string }).source + const prefix = source === "REFINE" ? "Refinement" : source === "OPTIMIZE_LP" ? "LP Candidate" : "Candidate" + return { id, rank: 0, label: `${prefix} ${cand.index}`, code: cand.code, isBest: false } + }) + .filter((r): r is NonNullable => r !== null) + .map((r, index) => ({ ...r, rank: index + 1, isBest: index === 0 })) + + sections.push({ + id: call.id, + type: "ranking", + title: "Candidate Ranking", + subtitle: "Selecting the best optimization", + timestamp, + duration: call.latency_ms ?? undefined, + status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial", + model: call.model_name, + cost: call.llm_cost, + tokens: call.total_tokens, + content: { + type: "ranking", + explanation: rankingExplanation ?? "", + rankings, + usedForPr, + }, + debugData: { + systemPrompt: call.system_prompt ?? null, + userPrompt: call.user_prompt ?? null, + rawResponse: call.raw_response ?? null, + }, + }) + } + } + + const typeOrder: Record = { + test_generation: 0, + optimization: 1, + line_profiler: 2, + refinement: 3, + ranking: 4, + summary: 5, + } + + sections.sort((a, b) => { + const orderA = typeOrder[a.type] ?? 99 + const orderB = typeOrder[b.type] ?? 99 + if (orderA !== orderB) return orderA - orderB + const candidateTypes = ["optimization", "line_profiler", "refinement"] + if (candidateTypes.includes(a.type)) { + const indexA = parseInt(a.title.match(/\d+$/)?.[0] ?? "0", 10) + const indexB = parseInt(b.title.match(/\d+$/)?.[0] ?? "0", 10) + return indexA - indexB + } + return a.timestamp - b.timestamp + }) + + return { sections, totalDuration } +} \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/trace-search.tsx b/js/cf-webapp/src/app/observability/components/trace-search.tsx new file mode 100644 index 000000000..ea2ec9106 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/trace-search.tsx @@ -0,0 +1,86 @@ +"use client" + +import { useState, useCallback, type ChangeEvent } from "react" +import { Search, Loader2, CheckCircle } from "lucide-react" +import { useRouter } from "next/navigation" + +interface TraceSearchProps { + initialTraceId?: string + isLoading?: boolean + hasResults?: boolean +} + +export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults = false }: TraceSearchProps) { + const [traceId, setTraceId] = useState(initialTraceId) + const router = useRouter() + + const handleChange = useCallback((e: ChangeEvent) => { + setTraceId(e.target.value) + }, []) + + const handleSearch = useCallback(() => { + const trimmedId = traceId.trim() + if (!trimmedId) return + + const params = new URLSearchParams(window.location.search) + params.set("trace_id", trimmedId) + router.push(`/observability?${params.toString()}`) + }, [traceId, router]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSearch() + } + }, + [handleSearch], + ) + + function getInputBorderClass(): string { + if (hasResults) { + return "border-green-500 dark:border-green-500 focus:ring-green-500" + } + return "border-zinc-300 dark:border-zinc-600 focus:ring-blue-500" + } + + return ( +
    +
    +
    + + + {hasResults && ( + + )} +
    + +
    + {!hasResults && ( +

    + Paste or type a trace ID to view all associated LLM calls, candidates, and errors +

    + )} +
    + ) +} \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/trace-summary.tsx b/js/cf-webapp/src/app/observability/components/trace-summary.tsx new file mode 100644 index 000000000..d8a2065a2 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/trace-summary.tsx @@ -0,0 +1,131 @@ +import { + CheckCircle, + XCircle, + AlertCircle, + Timer, + DollarSign, + Github, + Terminal, + Hash, + Code as CodeIcon, +} from "lucide-react" +import { InfoIcon } from "./info-icon" + +interface TraceSummaryProps { + status: "Completed" | "Partial" | "Failed" + source: string + durationSeconds: number + totalCost: number + totalTokens: number + candidatesCount?: number +} + +export function TraceSummary({ + status, + source, + durationSeconds, + totalCost, + totalTokens, + candidatesCount, +}: TraceSummaryProps) { + function getStatusColor(status: string): string { + switch (status) { + case "Failed": + return "text-red-600 dark:text-red-400" + case "Partial": + return "text-yellow-600 dark:text-yellow-400" + default: + return "text-green-600 dark:text-green-400" + } + } + + function getStatusIcon(status: string) { + switch (status) { + case "Completed": + return CheckCircle + case "Failed": + return XCircle + default: + return AlertCircle + } + } + + const statusColor = getStatusColor(status) + const StatusIcon = getStatusIcon(status) + + const SourceIcon = source.toLowerCase().includes("github") ? Github : Terminal + + return ( +
    +
    +
    +
    + + Status + +
    +
    {status}
    +
    + +
    +
    + + Source + +
    +
    + + {source} + +
    +
    + +
    +
    + + Duration + +
    +
    + {durationSeconds.toFixed(2)}s +
    +
    + +
    +
    + + Cost + +
    +
    + ${totalCost.toFixed(4)} +
    +
    + +
    +
    + + Tokens + +
    +
    + {totalTokens.toLocaleString()} +
    +
    + + {candidatesCount !== undefined && ( +
    +
    + + Candidates + +
    +
    + {candidatesCount} +
    +
    + )} +
    +
    + ) +} \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/components/utils.ts b/js/cf-webapp/src/app/observability/components/utils.ts new file mode 100644 index 000000000..25b849e6b --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/utils.ts @@ -0,0 +1,16 @@ +/** + * Determines the source of an optimization based on event_type + */ +export function getTraceSource(eventType: string | null): string { + if (!eventType) return "Unknown" + + if (eventType === "pr_created" || eventType === "pr_merged" || eventType === "pr_closed") { + return "GitHub Action" + } + + if (eventType === "no-pr") { + return "CLI/VSCode" + } + + return eventType +} \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/layout.tsx b/js/cf-webapp/src/app/observability/layout.tsx index c2a513101..949aaa8b2 100644 --- a/js/cf-webapp/src/app/observability/layout.tsx +++ b/js/cf-webapp/src/app/observability/layout.tsx @@ -3,7 +3,7 @@ import { ObservabilityNav } from "@/components/observability/observability-nav" export default function ObservabilityLayout({ children }: { children: ReactNode }) { return ( -
    +
    {children}
    diff --git a/js/cf-webapp/src/app/observability/loading.tsx b/js/cf-webapp/src/app/observability/loading.tsx new file mode 100644 index 000000000..9fadad779 --- /dev/null +++ b/js/cf-webapp/src/app/observability/loading.tsx @@ -0,0 +1,39 @@ +export default function ObservabilityLoading() { + return ( +
    + {/* Search Section Skeleton */} +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + {/* Content Skeleton */} +
    + {/* Progress skeleton */} +
    +
    +
    +
    + + {/* Timeline items skeleton */} +
    + {[1, 2, 3].map((i) => ( +
    +
    +
    +
    +
    +
    +
    + ))} +
    +
    +
    + ) +} \ No newline at end of file diff --git a/js/cf-webapp/src/app/observability/page.tsx b/js/cf-webapp/src/app/observability/page.tsx new file mode 100644 index 000000000..5f6ea4318 --- /dev/null +++ b/js/cf-webapp/src/app/observability/page.tsx @@ -0,0 +1,345 @@ +import { Suspense } from "react" +import { unstable_cache } from "next/cache" +import { Search } from "lucide-react" +import { prisma } from "@/lib/prisma" +import { TraceSearch } from "@/app/observability/components/trace-search" +import { TimelinePageView } from "@/app/observability/components/timeline-page-view" +import { transformToTimelineSections } from "@/app/observability/components/timeline-types" +import { ErrorsSection } from "@/app/observability/components/errors-section" +import { FunctionToOptimizeSection } from "@/app/observability/components/function-to-optimize-section" +import { CodeContextSection } from "@/app/observability/components/code-context-section" + +export const revalidate = 60 + +interface Observability2PageProps { + searchParams: Promise<{ + trace_id?: string + }> +} + +const getTraceData = unstable_cache( + async (tracePrefix: string) => { + const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([ + prisma.llm_calls.findMany({ + where: { trace_id: { startsWith: tracePrefix } }, + orderBy: { created_at: "asc" }, + }), + prisma.optimization_errors.findMany({ + where: { trace_id: { startsWith: tracePrefix } }, + orderBy: { created_at: "asc" }, + }), + prisma.optimization_features.findFirst({ + where: { trace_id: { startsWith: tracePrefix } }, + }), + prisma.optimization_events.findFirst({ + where: { trace_id: { startsWith: tracePrefix } }, + select: { event_type: true, function_name: true, file_path: true }, + }), + ]) + return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } + }, + ["observability-trace-detail"], + { revalidate: 60 }, +) + +export default async function Observability2Page({ searchParams }: Observability2PageProps) { + const params = await searchParams + const traceId = params.trace_id?.trim() + + let traceData: Awaited> | null = null + if (traceId) { + const tracePrefix = traceId.substring(0, 33) + traceData = await getTraceData(tracePrefix) + } + + const hasResults = traceData + ? traceData.rawLlmCalls.length > 0 || traceData.errors.length > 0 + : false + + return ( +
    +
    +
    + }> + + +
    +
    + + {traceId && traceData ? ( + }> + + + ) : traceId ? ( + + ) : ( + + )} +
    + ) +} + +interface TraceData { + rawLlmCalls: Awaited>["rawLlmCalls"] + errors: Awaited>["errors"] + optimizationFeatures: Awaited>["optimizationFeatures"] + optimizationEvent: Awaited>["optimizationEvent"] +} + +function TraceContent({ traceId, traceData }: { traceId: string; traceData: TraceData }) { + const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = traceData + + if (rawLlmCalls.length === 0 && errors.length === 0) { + return + } + + const optimizationsOrigin = + (optimizationFeatures?.optimizations_origin as Record< + string, + { source: string; model?: string; call_sequence?: number; parent?: string } + >) || {} + + const candidateExplanations = + (optimizationFeatures?.explanations_post as Record) || {} + + const allCandidates = optimizationFeatures?.optimizations_post + ? Object.entries(optimizationFeatures.optimizations_post as Record).map( + ([id, code]) => ({ + id, + code: typeof code === "string" ? code : "", + source: optimizationsOrigin[id]?.source || "OPTIMIZE", + model: optimizationsOrigin[id]?.model, + callSequence: optimizationsOrigin[id]?.call_sequence, + explanation: candidateExplanations[id], + }), + ) + : [] + + const optimizationCandidates = allCandidates + .filter(c => c.source === "OPTIMIZE") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ ...c, index: index + 1 })) + + const lineProfilerCandidates = allCandidates + .filter(c => c.source === "OPTIMIZE_LP") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ ...c, index: index + 1 })) + + const refinementCandidates = allCandidates + .filter(c => c.source === "REFINE") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ + ...c, + index: index + 1, + parentId: optimizationsOrigin[c.id]?.parent || null, + })) + + const rankingData = optimizationFeatures?.ranking as + | { ranking?: string[]; explanation?: string } + | null + const bestCandidateId = rankingData?.ranking?.[0] ?? null + + const pullRequestRaw = optimizationFeatures?.pull_request + const usedForPr = Boolean( + pullRequestRaw != null && + typeof pullRequestRaw === "object" && + !Array.isArray(pullRequestRaw) && + Object.keys(pullRequestRaw as Record).length > 0, + ) + + function buildCandidateRankMap(): Record { + const rankMap: Record = {} + if (rankingData?.ranking) { + rankingData.ranking.forEach((id, index) => { + rankMap[id] = index + 1 + }) + } + return rankMap + } + const candidateRankMap = buildCandidateRankMap() + + const generatedTests = (optimizationFeatures?.generated_test ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + + const instrumentedTests = (optimizationFeatures?.instrumented_generated_test ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + + function getInstrumentedPerfTests(): Array<{ code: string; index: number }> { + const features = optimizationFeatures as Record | null + const tests = features?.instrumented_perf_test as string[] | undefined + return (tests ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + } + const instrumentedPerfTests = getInstrumentedPerfTests() + + const llmCalls = rawLlmCalls.sort((a, b) => { + const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity + const seqB = (b.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity + if (seqA !== seqB) return seqA - seqB + return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + }) + + const transformedCalls = llmCalls.map(call => ({ + id: call.id, + call_type: call.call_type, + model_name: call.model_name, + status: call.status, + latency_ms: call.latency_ms, + llm_cost: call.llm_cost, + total_tokens: call.total_tokens, + created_at: call.created_at, + context: call.context as { call_sequence?: number } | null, + system_prompt: call.system_prompt, + user_prompt: call.user_prompt, + raw_response: call.raw_response, + })) + + const { sections, totalDuration } = transformToTimelineSections({ + calls: transformedCalls, + optimizationCandidates, + lineProfilerCandidates, + refinementCandidates, + generatedTests, + instrumentedTests, + instrumentedPerfTests, + originalCode: optimizationFeatures?.original_code ?? null, + testFramework: optimizationFeatures?.test_framework ?? null, + candidateRankMap, + bestCandidateId, + rankingExplanation: rankingData?.explanation ?? null, + usedForPr, + }) + + const transformedErrors = errors.map(error => ({ + id: error.id, + error_type: error.error_type, + severity: error.severity, + error_message: error.error_message, + context: error.context as { + test_name?: string + failure_reason?: string + test_output?: string + expected?: string + actual?: string + } | null, + created_at: error.created_at, + })) + + function getFunctionName(): string | null { + const metadata = optimizationFeatures?.metadata as Record | undefined + const fromMetadata = metadata?.function_to_optimize as string | undefined + return fromMetadata ?? optimizationEvent?.function_name ?? null + } + const functionName = getFunctionName() + const filePath = optimizationEvent?.file_path ?? null + const originalCode = optimizationFeatures?.original_code ?? null + const dependencyCode = optimizationFeatures?.dependency_code ?? null + + return ( +
    +
    + + +
    + + + + {transformedErrors.length > 0 && ( +
    + +
    + )} +
    + ) +} + +function EmptyState() { + return ( +
    +
    + +
    +

    + Enter a Trace ID to Get Started +

    +

    + Paste or type a trace ID in the search box above to view the complete optimization timeline, + including all LLM calls, generated candidates, and any errors. +

    +
    + ) +} + +function NotFoundState({ traceId }: { traceId: string }) { + return ( +
    +
    + +
    +

    Trace Not Found

    +

    + No data was found for the trace ID: +

    + + {traceId} + +

    + Please check that the trace ID is correct and try again. +

    +
    + ) +} + +function SearchSkeleton() { + return ( +
    +
    +
    +
    +
    +
    + ) +} + +function TraceContentSkeleton() { + return ( +
    +
    +
    +
    +
    + +
    + {[1, 2, 3].map((i) => ( +
    +
    +
    +
    +
    +
    +
    + ))} +
    +
    + ) +} \ No newline at end of file diff --git a/js/cf-webapp/src/components/observability/observability-nav.tsx b/js/cf-webapp/src/components/observability/observability-nav.tsx index ee99e2f48..fb9b540c4 100644 --- a/js/cf-webapp/src/components/observability/observability-nav.tsx +++ b/js/cf-webapp/src/components/observability/observability-nav.tsx @@ -1,17 +1,43 @@ "use client" import Link from "next/link" -import { usePathname } from "next/navigation" -import { Activity, ListTree } from "lucide-react" +import { usePathname, useRouter } from "next/navigation" +import { Activity, ListTree, Clock, Layers } from "lucide-react" import { cn } from "@/lib/utils" +import { useEffect, useState } from "react" const navItems = [ { href: "/observability/traces", label: "Traces", icon: ListTree }, { href: "/observability/llm-calls", label: "LLM Calls", icon: Activity }, ] +type ViewMode = "classic" | "timeline" + export function ObservabilityNav() { const pathname = usePathname() + const router = useRouter() + const [viewMode, setViewMode] = useState("classic") + + // Initialize view mode from localStorage + useEffect(() => { + const storedMode = localStorage.getItem("observability-view-mode") as ViewMode | null + if (storedMode === "timeline" || storedMode === "classic") { + setViewMode(storedMode) + } + }, []) + + // Handle view mode changes + const handleViewModeChange = (mode: ViewMode) => { + setViewMode(mode) + localStorage.setItem("observability-view-mode", mode) + + // Navigate based on the selected mode + if (mode === "timeline") { + router.push("/observability") + } else { + router.push("/observability/traces") + } + } return (

    - {/* Navigation Links */} -
    - {navItems.map(item => { - const Icon = item.icon - const isActive = pathname === item.href || pathname.startsWith(item.href + "/") + {/* Navigation Links and View Toggle */} +
    + {/* Show navigation links only in classic mode */} + {viewMode === "classic" && ( +
    + {navItems.map(item => { + const Icon = item.icon + const isActive = pathname === item.href || pathname.startsWith(item.href + "/") - return ( - - - {item.label} - - ) - })} + return ( + + + {item.label} + + ) + })} +
    + )} + + {/* View Mode Toggle */} +
    + + +
    diff --git a/js/cf-webapp/src/styles/obs-theme.css b/js/cf-webapp/src/styles/obs-theme.css new file mode 100644 index 000000000..d070f852c --- /dev/null +++ b/js/cf-webapp/src/styles/obs-theme.css @@ -0,0 +1,49 @@ +/** + * Observability V2 Theme + * + * Scoped design token overrides for the observability pages only. + * Uses zinc color scale in HSL format (compatible with existing hsl(var(--x)) usage). + * Apply the .obs-v2 class to a parent element to activate these overrides. + */ + +.obs-v2 { + /* Background and foreground */ + --background: 240 10% 4%; /* zinc-950 */ + --foreground: 0 0% 98%; /* zinc-50 */ + + /* Card styles */ + --card: 240 6% 10%; /* zinc-900 */ + --card-foreground: 0 0% 98%; /* zinc-50 */ + + /* Popover styles */ + --popover: 240 6% 10%; /* zinc-900 */ + --popover-foreground: 0 0% 98%; /* zinc-50 */ + + /* Primary - neutral zinc instead of brand yellow */ + --primary: 0 0% 98%; /* zinc-50 */ + --primary-foreground: 240 10% 4%; /* zinc-950 */ + + /* Secondary */ + --secondary: 240 4% 16%; /* zinc-800 */ + --secondary-foreground: 0 0% 98%; /* zinc-50 */ + + /* Accent */ + --accent: 240 4% 26%; /* zinc-700 */ + --accent-foreground: 0 0% 98%; /* zinc-50 */ + + /* Muted */ + --muted: 240 4% 16%; /* zinc-800 */ + --muted-foreground: 240 4% 65%; /* zinc-400 */ + + /* Destructive */ + --destructive: 0 84% 60%; /* red-500 */ + --destructive-foreground: 0 86% 97%; /* red-50 */ + + /* Borders and inputs */ + --border: 240 4% 16%; /* zinc-800 */ + --input: 240 4% 16%; /* zinc-800 */ + --ring: 240 5% 34%; /* zinc-600 */ + + /* Radius - tighter for developer-tool aesthetic */ + --radius: 0.25rem; +} From 752e2504e40cb839d9d1c74d212ab2350c6cfeda Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:10:20 -0500 Subject: [PATCH 056/184] Restructure and improve refinement prompt (#2379) ## Summary - Restructure the refinement system prompt into clear numbered sections (Preserve Behavior, Minimize Diff, Revert Anti-Patterns, Maintain Readability) with an explicit 6-step refinement process - Extract inline prompt strings into separate markdown files (`refinement_system_prompt.md`, `refinement_user_prompt.md`), matching the convention used by other optimizer prompts - Add `AuthenticatedRequest` type hint to `refine()` endpoint and fix grammar in tool use section ## Test plan - [ ] Verify refinement endpoint still works end-to-end with a test optimization candidate - [ ] Confirm prompt content is loaded correctly from markdown files at startup --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- django/aiservice/optimizer/refinement.py | 166 +----------------- .../optimizer/refinement_system_prompt.md | 119 +++++++++++++ .../optimizer/refinement_user_prompt.md | 47 +++++ 3 files changed, 170 insertions(+), 162 deletions(-) create mode 100644 django/aiservice/optimizer/refinement_system_prompt.md create mode 100644 django/aiservice/optimizer/refinement_user_prompt.md diff --git a/django/aiservice/optimizer/refinement.py b/django/aiservice/optimizer/refinement.py index 3f7879cfb..f7dab9d49 100644 --- a/django/aiservice/optimizer/refinement.py +++ b/django/aiservice/optimizer/refinement.py @@ -17,6 +17,7 @@ from aiservice.common.xml_utils import extract_xml_tag from aiservice.common_utils import validate_trace_id from aiservice.env_specific import debug_log_sensitive_data from aiservice.llm import REFINEMENT_MODEL, calculate_llm_cost, call_llm +from authapp.auth import AuthenticatedRequest from log_features.log_event import update_optimization_cost from log_features.log_features import log_features from optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema @@ -33,167 +34,8 @@ refinement_api = NinjaAPI(urls_namespace="refinement") # Get the directory of the current file current_dir = Path(__file__).parent -SYSTEM_PROMPT = """You are an expert software engineer who writes really fast programs and is an expert in optimizing runtime and memory requirements of a program by rewriting it. You have deep expertise in modern programming best practices, and clean code principles. - -You are part of a code optimization system called Codeflash. Codeflash analyzes user code to optimize it for performance. It does so by first establishing the original code baseline by generating regression tests and discovering existing test cases, which finds the runtime of the original code and the behavior. Then, Codeflash generates multiple candidate optimizations which are applied to the codebase and ran to find the new runtime and behavior. Codeflash discards any optimizations that are either slower or have different behavior, in order to find the real optimizations. - -Even though the optimizations may be technically correct, we want the quality of the optimizations to be really high, so your task is to refine the code of the optimizations so that they are expert quality. - -The goal of refining the quality is to make the optimizations more precise. You want to reduce the number of lines and characters different between the optimizations and the original code to deliver very similar optimizations. These precise optimizations are highly preferred by the user and makes it is easier to accept the optimizations. - -The refinement process should NEVER change the behavior of the optimization and we should try to preserve the main optimizations. - -You are provided the following information to succeed in the quality refinement process - - -- original_source_code - This is the original code being optimized -- optimized_source_code - This is the previously found optimization source code. -- original_line_profiler_results - The results after running line_profiler on the original_source_code -- optimized_line_profiler_results - The results after running line_profiler on the optimized_source_code -- optimization_speedup_results - The runtime for the original_source_code and optimized_source_code over a series of tests -- optimization_explanation - The explanation generated by codeflash earlier for the optimization in the optimized_source_code -- read_only_dependency_code - The READ ONLY dependencies for the code provided, to help you better understand the code being provided. Do no modify the code here, it is only provided for your reference. -- python_version - The version of python the code would be executed on. -- function_references - Python markdown blocks with filename and references of some functions which call the function being optimized. The filenames and/or references could indicate if the function being optimized is in a hot path. The reference could have the function being called from a place that is important, for example in a loop, which means the effect of optimization might be important. - -Rules to follow while refining the quality of the optimized code - -- Analyze the original code and the optimized code and look at the line profiler info and the explanation to understand how the optimization works -- Introduction of the `global` and `nonlocal` keywords in optimized_source_code is **HIGHLY DISCOURAGED** as it reduces code clarity and maintainability, introduces hidden dependencies, can cause subtle bugs and breaks modularity. Revert any such changes. -- Revert any changes which are micro-optimizations like inlining a function call, or localizing variables or methods (attribute lookup optimizations). The performance improvements are minimal and come at a substantial cost to readability. -- **Revert any conversion of `isinstance()` checks to `type()` checks**. `isinstance()` correctly handles inheritance and subclasses, while `type()` checks are incorrect for subclass instances and represent a micro-optimization that should be avoided. Preserve the original `isinstance()` usage. -- Figure out the code difference between the original_source_code and the optimized_source_code to see what part of the optimized_source_code is not contributing to the optimization. In such a case, we want to revert that part of the optimized_source_code to the original_source_code. It is okay to revert parts of the changes that aren't faster by at least 1%. -- If there are any changes in the optimized code that make that code section slower than the original then we want to revert such a change to the original. -- Revert the new comments in the optimized_source_code that are different from the original_source_code unless the new code is complex and requires additional context. -- The the variables names for the same logical variable in the optimized_source_code is different from the original_source_code then prefer the variable name in the original_source_code. - -Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. - -TOOL USE - -You have access to a set of tools that are don't need any approval to run and is required to use to succeed with the task. - -Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure: - - -value1 -value2 -... - - -For example: - - -src/main.py - -<<<<<<< SEARCH -a = 2 -======= -a = 3 ->>>>>>> REPLACE - - - - -... - - -Always adhere to this format for tool use to ensure proper parsing and execution. - -# Tools - -## replace_in_file -Description: Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file. -Parameters: -- path: (required) The path of the file to modify -- diff: (required) One or more SEARCH/REPLACE blocks following this exact format: - ``` - <<<<<<< SEARCH - [exact content to find] - ======= - [new content to replace with] - >>>>>>> REPLACE - ``` - Critical rules: - 1. SEARCH content must match the associated file section to find EXACTLY: - * Match character-for-character including whitespace, indentation, line endings - * Include all comments, docstrings, etc. - 2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence. - * Including multiple unique SEARCH/REPLACE blocks if you need to make multiple changes. - * Include *just* enough lines in each SEARCH section to uniquely match each set of lines that need to change. - * When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file. - 3. Keep SEARCH/REPLACE blocks concise: - * Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file. - * Include just the changing lines, and a few surrounding lines if needed for uniqueness. - * Do not include long runs of unchanging lines in SEARCH/REPLACE blocks. - * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures. - 4. Special operations: - * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location) - * To delete code: Use empty REPLACE section - -## explanation -Description: Request to provide a clear and short summary of the changes made by the tool. - -Usage: - -File path here - -Search and replace blocks here - - - -... - - -Please edit the optimized_source_code to implement the refinement process to improve the quality of the optimization. -""" -USER_PROMPT = """ -Please edit the optimized_source_code to implement the refinement process to improve the quality of the optimization given the following information. - - -{original_source_code} - - -Here is the line profiler information for the original_source_code - - -{original_line_profiler_results} - - -Here is the optimized_source_code - - -{optimized_source_code} - - -Here is the explanation generated by Codeflash for the optimized_source_code - - - -{optimized_explanation} - - -Here is the line profiler information for the optimized_source_code - - -{optimized_line_profiler_results} - - -The original_source_code takes {original_code_runtime} to run and the optimized_source_code takes {optimized_code_runtime} to run, making it {speedup} faster. - -Here is the read_only_dependency_code - - -{read_only_dependency_code} - - -Here is the python version - -{python_version} - - -Here is the function_references - -{function_references} - -""" +SYSTEM_PROMPT = (current_dir / "refinement_system_prompt.md").read_text() +USER_PROMPT = (current_dir / "refinement_user_prompt.md").read_text() async def refinement( # noqa: D417 @@ -333,7 +175,7 @@ class Refinementschema(Schema): @refinement_api.post("/", response={200: Refinementschema, 400: Refinementschema, 500: Refinementschema}) async def refine( - request, data: list[RefinementRequestSchema] + request: AuthenticatedRequest, data: list[RefinementRequestSchema] ) -> tuple[int, Refinementschema | OptimizeErrorResponseSchema]: await asyncio.to_thread(ph, request.user, "aiservice-refinement-called") ctx_data_list = [ diff --git a/django/aiservice/optimizer/refinement_system_prompt.md b/django/aiservice/optimizer/refinement_system_prompt.md new file mode 100644 index 000000000..3ed81bb0e --- /dev/null +++ b/django/aiservice/optimizer/refinement_system_prompt.md @@ -0,0 +1,119 @@ +You are an expert software engineer specializing in performance optimization with deep expertise in clean code principles. You are part of Codeflash, a code optimization system that generates candidate optimizations, validates them against regression tests, and discards any that are slower or change behavior. + +Your task is to refine optimization candidates so they are expert quality. You will analyze each optimization and apply refinements that: + +1. **Preserve Optimization Behavior**: Never change what the optimized code does — only how it expresses it. The speedup and all original optimization behavior must remain intact. + +2. **Minimize the Diff**: Reduce the number of lines and characters different between the optimization and the original code. Precise optimizations that change only what is necessary are highly preferred by users and easier to accept. + - Evaluate each change in the optimization independently against the line profiler data and speedup results. + - Revert changes that don't contribute meaningfully to the speedup (less than 1% improvement). + - Revert changes that make any code section slower than the original. + +3. **Revert Anti-Patterns**: Certain optimization patterns harm code quality and must be reverted to the original code: + - `global` and `nonlocal` keyword additions — they reduce clarity, introduce hidden dependencies, and break modularity. + - Micro-optimizations like inlining function calls, localizing variables, or attribute lookup optimizations — minimal performance gain at substantial readability cost. + - Conversion of `isinstance()` to `type()` checks — `isinstance()` correctly handles inheritance and must be preserved. + +4. **Maintain Readability**: The refined code should be as readable as the original: + - Prefer original variable names for the same logical variables. + - Remove new comments unless the new code is genuinely complex and requires additional context. + - Simplify unnecessarily complex or "clever" patterns (dense one-liners, deeply nested expressions) while preserving the performance benefit. Prefer clarity over brevity. + +Your refinement process: + +1. Analyze the original code, optimized code, line profiler results, and optimization explanation to understand how the optimization works +2. Identify each distinct change between the original and optimized code +3. For each change, determine whether it contributes meaningfully to the speedup using the profiler data +4. Revert non-contributing changes, anti-patterns, and micro-optimizations back to the original code +5. Simplify any remaining optimization code that is unnecessarily complex +6. Verify the refined code preserves the optimization's performance benefit + +You are provided the following information: + +- original_source_code — the original code being optimized +- optimized_source_code — the previously found optimization candidate +- original_line_profiler_results — line_profiler output for the original code +- optimized_line_profiler_results — line_profiler output for the optimized code +- optimization_speedup_results — runtime comparison between original and optimized over a series of tests +- optimization_explanation — the explanation generated by Codeflash for the optimization +- read_only_dependency_code — READ ONLY dependencies for context. Do not modify this code. +- python_version — the target Python version +- function_references — markdown blocks showing where the optimized function is called, indicating if it is in a hot path (e.g., called in a loop) + +TOOL USE + +You have access to a set of tools that don't need any approval to run and are required to succeed with the task. + +Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure: + + +value1 +value2 +... + + +For example: + + +src/main.py + +<<<<<<< SEARCH +a = 2 +======= +a = 3 +>>>>>>> REPLACE + + + + +... + + +Always adhere to this format for tool use to ensure proper parsing and execution. + +# Tools + +## replace_in_file +Description: Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file. +Parameters: +- path: (required) The path of the file to modify +- diff: (required) One or more SEARCH/REPLACE blocks following this exact format: + ``` + <<<<<<< SEARCH + [exact content to find] + ======= + [new content to replace with] + >>>>>>> REPLACE + ``` + Critical rules: + 1. SEARCH content must match the associated file section to find EXACTLY: + * Match character-for-character including whitespace, indentation, line endings + * Include all comments, docstrings, etc. + 2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence. + * Including multiple unique SEARCH/REPLACE blocks if you need to make multiple changes. + * Include *just* enough lines in each SEARCH section to uniquely match each set of lines that need to change. + * When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file. + 3. Keep SEARCH/REPLACE blocks concise: + * Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file. + * Include just the changing lines, and a few surrounding lines if needed for uniqueness. + * Do not include long runs of unchanging lines in SEARCH/REPLACE blocks. + * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures. + 4. Special operations: + * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location) + * To delete code: Use empty REPLACE section + +## explanation +Description: Request to provide a clear and short summary of the changes made by the tool. + +Usage: + +File path here + +Search and replace blocks here + + + +... + + +Please edit the optimized_source_code to implement the refinement process to improve the quality of the optimization. diff --git a/django/aiservice/optimizer/refinement_user_prompt.md b/django/aiservice/optimizer/refinement_user_prompt.md new file mode 100644 index 000000000..22216ad34 --- /dev/null +++ b/django/aiservice/optimizer/refinement_user_prompt.md @@ -0,0 +1,47 @@ +Please edit the optimized_source_code to implement the refinement process to improve the quality of the optimization given the following information. + + +{original_source_code} + + +Here is the line profiler information for the original_source_code + + +{original_line_profiler_results} + + +Here is the optimized_source_code + + +{optimized_source_code} + + +Here is the explanation generated by Codeflash for the optimized_source_code - + + +{optimized_explanation} + + +Here is the line profiler information for the optimized_source_code + + +{optimized_line_profiler_results} + + +The original_source_code takes {original_code_runtime} to run and the optimized_source_code takes {optimized_code_runtime} to run, making it {speedup} faster. + +Here is the read_only_dependency_code + + +{read_only_dependency_code} + + +Here is the python version + +{python_version} + + +Here is the function_references + +{function_references} + From 223a730dffb16b568e30bfe2630579d4c620e55d Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:31:12 -0500 Subject: [PATCH 057/184] chore: bump @codeflash-ai/common to 1.0.29 (#2380) ## Summary - Bump `@codeflash-ai/common` from 1.0.28 to 1.0.29 to include the `instrumented_perf_test` Prisma schema field in the published package - This unblocks the observability timeline from displaying performance tests (currently only generated + behavior tests show) The field was added to the schema in #2330 but the package version was never bumped, so the deployed webapp's Prisma client doesn't SELECT `instrumented_perf_test`. After merging: publish the package and redeploy the webapp. --- js/common/package-lock.json | 10 ++-------- js/common/package.json | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/js/common/package-lock.json b/js/common/package-lock.json index 3a1757a50..74f13dcfc 100644 --- a/js/common/package-lock.json +++ b/js/common/package-lock.json @@ -1,12 +1,12 @@ { "name": "@codeflash-ai/common", - "version": "1.0.28", + "version": "1.0.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@codeflash-ai/common", - "version": "1.0.28", + "version": "1.0.29", "dependencies": { "@azure/identity": "^4.2.0", "@azure/keyvault-secrets": "^4.8.0", @@ -566,7 +566,6 @@ "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -617,7 +616,6 @@ "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", @@ -883,7 +881,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1893,7 +1890,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4079,7 +4075,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.15.0", "@prisma/engines": "6.15.0" @@ -5046,7 +5041,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/js/common/package.json b/js/common/package.json index edf04a816..3a8b87508 100644 --- a/js/common/package.json +++ b/js/common/package.json @@ -1,6 +1,6 @@ { "name": "@codeflash-ai/common", - "version": "1.0.28", + "version": "1.0.29", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", "repository": { From 2c56875f83a67e014121eedc2e2f5cb3fa580091 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Sun, 8 Feb 2026 03:21:57 -0500 Subject: [PATCH 058/184] fix: display instrumented perf tests in observability timeline (#2381) ## Summary - Published `@codeflash-ai/common@1.0.30` with `dist/` and `instrumented_perf_test` schema field - Updated webapp to use the new package so Prisma generates correct types - Removed `Record` type cast workaround in `page.tsx` The instrumented perf test data was already being stored in the DB but the webapp's Prisma client didn't have the field in its generated types, so it was never returned from queries. ## Test plan - [ ] Search a trace that has perf tests (e.g. `59a508fb-8d00-4830-992b-fa342e5d6c94`) and verify the `+perf` badge and "Perf" tab appear in Test Generation --- js/cf-webapp/package-lock.json | 8 ++++---- js/cf-webapp/package.json | 2 +- js/cf-webapp/src/app/observability/page.tsx | 13 ++++--------- js/common/package-lock.json | 4 ++-- js/common/package.json | 2 +- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/js/cf-webapp/package-lock.json b/js/cf-webapp/package-lock.json index 476ce3d0c..6ac6815c6 100644 --- a/js/cf-webapp/package-lock.json +++ b/js/cf-webapp/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@auth0/nextjs-auth0": "^3.3.0", "@azure/msal-node": "^3.7.3", - "@codeflash-ai/common": "^1.0.28", + "@codeflash-ai/common": "^1.0.30", "@hookform/resolvers": "^3.3.2", "@monaco-editor/react": "^4.7.0", "@prisma/client": "^6.7.0", @@ -704,9 +704,9 @@ } }, "node_modules/@codeflash-ai/common": { - "version": "1.0.28", - "resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.28/12bed47e58afcf7237ebba9f6fa301f1f5a5d435", - "integrity": "sha512-EmnUy07dyZOfA03Ewub8C6/Qs/CO0thDLBfYdsW501qT5BpjPyZ2NQvGmWeOSFIQPuYpLpbe+zGEGcP7kS0Sxw==", + "version": "1.0.30", + "resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.30/2b9b7399dc24b978b78429cbce77e39e3cec77c4", + "integrity": "sha512-Vk8n8EN/a08VzZJUQtQDYtRodDJO4SyGoxyfjT0YkZcVI/RZQo1yP4e5oYxoe2yZdzkSAi7LtUwTMe9HZ99tPQ==", "dependencies": { "@azure/identity": "^4.2.0", "@azure/keyvault-secrets": "^4.8.0", diff --git a/js/cf-webapp/package.json b/js/cf-webapp/package.json index 314701500..ed3a22a32 100644 --- a/js/cf-webapp/package.json +++ b/js/cf-webapp/package.json @@ -21,7 +21,7 @@ "dependencies": { "@auth0/nextjs-auth0": "^3.3.0", "@azure/msal-node": "^3.7.3", - "@codeflash-ai/common": "^1.0.28", + "@codeflash-ai/common": "^1.0.30", "@hookform/resolvers": "^3.3.2", "@monaco-editor/react": "^4.7.0", "@prisma/client": "^6.7.0", diff --git a/js/cf-webapp/src/app/observability/page.tsx b/js/cf-webapp/src/app/observability/page.tsx index 5f6ea4318..28f8075c6 100644 --- a/js/cf-webapp/src/app/observability/page.tsx +++ b/js/cf-webapp/src/app/observability/page.tsx @@ -168,15 +168,10 @@ function TraceContent({ traceId, traceData }: { traceId: string; traceData: Trac index: index + 1, })) - function getInstrumentedPerfTests(): Array<{ code: string; index: number }> { - const features = optimizationFeatures as Record | null - const tests = features?.instrumented_perf_test as string[] | undefined - return (tests ?? []).map((code, index) => ({ - code, - index: index + 1, - })) - } - const instrumentedPerfTests = getInstrumentedPerfTests() + const instrumentedPerfTests = (optimizationFeatures?.instrumented_perf_test ?? []).map((code, index) => ({ + code, + index: index + 1, + })) const llmCalls = rawLlmCalls.sort((a, b) => { const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity diff --git a/js/common/package-lock.json b/js/common/package-lock.json index 74f13dcfc..a1c1645f4 100644 --- a/js/common/package-lock.json +++ b/js/common/package-lock.json @@ -1,12 +1,12 @@ { "name": "@codeflash-ai/common", - "version": "1.0.29", + "version": "1.0.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@codeflash-ai/common", - "version": "1.0.29", + "version": "1.0.30", "dependencies": { "@azure/identity": "^4.2.0", "@azure/keyvault-secrets": "^4.8.0", diff --git a/js/common/package.json b/js/common/package.json index 3a8b87508..470afae61 100644 --- a/js/common/package.json +++ b/js/common/package.json @@ -1,6 +1,6 @@ { "name": "@codeflash-ai/common", - "version": "1.0.29", + "version": "1.0.30", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", "repository": { From b9d318279c135403e1cdeffee8107bec380c4914 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Mon, 9 Feb 2026 01:20:59 -0500 Subject: [PATCH 059/184] feat: observability improvements and testgen prompt modernization (#2382) ## Summary - Rewrite testgen system prompts from constraint-heavy to positive-first structure with chain-of-thought instructions - Simplify LLM message structure from `[system, user, user, user]` to `[system, user]` by absorbing plan_content guidelines into system prompts - Observability UI: add search to LLM debug dialog, expand timeline view - Fix data capture: raw LLM responses, all user messages in prompt column, nested code fences, empty notes handling ## Test plan - [ ] Verify testgen produces valid test suites with the new prompt structure - [ ] Verify observability timeline displays LLM prompts/responses correctly - [ ] Check that search works in the LLM debug dialog --- django/aiservice/aiservice/llm.py | 2 +- .../aiservice/observability/database.py | 2 +- .../testgen/execute_async_system_prompt.md | 139 +++----- .../testgen/execute_async_user_prompt.md | 15 +- .../testgen/execute_system_prompt.md | 98 ++--- .../aiservice/testgen/execute_user_prompt.md | 2 +- django/aiservice/testgen/testgen.py | 69 ++-- .../components/timeline-page-view.tsx | 337 +++++++++++++++--- 8 files changed, 392 insertions(+), 272 deletions(-) diff --git a/django/aiservice/aiservice/llm.py b/django/aiservice/aiservice/llm.py index b00d1a5c6..947dbfacf 100644 --- a/django/aiservice/aiservice/llm.py +++ b/django/aiservice/aiservice/llm.py @@ -119,7 +119,7 @@ def get_llm_client(model_type: str) -> AsyncAzureOpenAI | AsyncAnthropicFoundry """ if model_type == "openai": return _create_openai_client() - elif model_type == "anthropic": + if model_type == "anthropic": return _create_anthropic_client() return None diff --git a/django/aiservice/aiservice/observability/database.py b/django/aiservice/aiservice/observability/database.py index 33e4dfcd2..c36e415a9 100644 --- a/django/aiservice/aiservice/observability/database.py +++ b/django/aiservice/aiservice/observability/database.py @@ -27,7 +27,7 @@ async def record_llm_call( """Record LLM call with result. Returns llm_call_id.""" llm_call_id = str(uuid.uuid4()) system_prompt = next((m["content"] for m in messages if m["role"] == "system"), "") - user_prompt = next((m["content"] for m in messages if m["role"] == "user"), "") + user_prompt = "\n\n".join(m["content"] for m in messages if m["role"] == "user" and m.get("content")) raw_response = None if result and hasattr(result.raw_response, "model_dump_json"): diff --git a/django/aiservice/testgen/execute_async_system_prompt.md b/django/aiservice/testgen/execute_async_system_prompt.md index c21e661ac..3227cf293 100644 --- a/django/aiservice/testgen/execute_async_system_prompt.md +++ b/django/aiservice/testgen/execute_async_system_prompt.md @@ -1,108 +1,53 @@ -**Role**: You are Codeflash, a world-class Python developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests for **asynchronous functions**. When asked to reply only with code, you write all of your code in a single markdown code block. +You are Codeflash, a world-class Python testing engineer. Your goal is to write a comprehensive, high-quality unit test suite for the **async** `{function_name}` function that fully defines its behavior — passing tests confirm correctness, and any mutation to the source code should cause at least one test to fail. -**Task** Your task is to create comprehensive, high quality test cases for the **async** {function_name} function. These test cases should encompass Basic, Edge, and Large Scale scenarios to ensure the code's robustness, reliability, and scalability. These test cases should *define* the {function_name} function, meaning that the function should pass all the tests, and a function with different external functional behavior should fail them. In other words, the test suite should fail under mutation testing of the source code. +**Think step by step before writing code.** Analyze the async function: +1. What does it do? What are its inputs, outputs, and return types? +2. What are the normal/expected usage patterns? +3. What edge cases exist (empty inputs, boundary values, type variations, error conditions)? +4. What async-specific edge cases exist (concurrent execution, coroutine handling)? +5. What large-scale or throughput scenarios should be covered? -**CRITICAL: PRESERVE ORIGINAL FUNCTION** -- You MUST use the EXACT original function signature and implementation provided -- DO NOT modify, enhance, or add parameters to the original function -- DO NOT change the function's internal logic or behavior -- The function code provided is the ONLY version you should test -- Write tests for the function AS-IS, not for an improved version +Then write tests organized into four categories: -**CRITICAL: USE REAL CLASSES - NO STUBS OR FAKES** -- NEVER define your own classes in the test file to replace real classes from the codebase -- When the context shows `from X import Y`, use that EXACT same import in your tests -- DO NOT create classes named `FakeX`, `StubX`, `MockX`, `DummyX`, `TestX`, `AsyncX`, or ANY class that mimics a real class -- DO NOT create "minimal", "helper", "placeholder", or "stub" versions of classes -- DO NOT add comments like "Minimal stub" or "Simple container" - these indicate you're doing it wrong -- If a class requires dependencies, look at the context to see how to construct it properly -- Tests that define their own classes WILL FAIL because isinstance() checks will fail +**Basic tests** — Verify fundamental functionality under normal conditions. Test that the function returns expected values when awaited, and test basic async/await behavior. -**CRITICAL: HANDLING INSTANCE METHODS** -- If the function has `self` as its first parameter, it is an **instance method** of a class -- DO NOT copy the method body into your test file and call it as a standalone function like `method_name(some_obj, ...)` -- Instead, you MUST: - 1. Import the class from its real module (e.g., `from mypackage.module import MyClass`) - 2. Create a real instance of the class (check conftest.py for fixtures that provide instances) - 3. Call the method ON the instance: `await instance.method_name(...)` for async methods -- Python passes `self` automatically when you call methods on instances - do NOT pass it manually -- Example: If testing `MyClass.process(self, data)`, write `instance = MyClass(...); result = await instance.process(data)` +**Edge tests** — Evaluate behavior under extreme or unusual conditions: empty inputs, boundary values, concurrent execution scenarios, exception handling in async context. -**CRITICAL: USE CONFTEST.PY FIXTURES WHEN PROVIDED** -- If the context includes conftest.py files, ALWAYS check for fixtures that provide the objects you need -- Pytest fixtures are defined with `@pytest.fixture` decorator and can be used simply by adding them as test function parameters -- Fixtures automatically handle setup/teardown and provide properly configured objects -- Example: If conftest.py has `@pytest.fixture async def embedding_model(): ...`, use it as `async def test_something(embedding_model): ...` -- PREFER fixtures over manual instantiation - they're pre-configured to work correctly -- Fixtures may provide Fake/Mock implementations that are designed to work with the codebase - USE THESE instead of creating your own -- Look for fixtures that return objects of the types you need (check return type hints and fixture names) +**Large-scale tests** — Assess performance and scalability with concurrent execution. Test multiple concurrent calls using `asyncio.gather()`. Use data structures up to 1000 elements and loops up to 1000 iterations. -**IMPORTANT ASYNC REQUIREMENTS:** -- The function under test is **async** and returns a coroutine that must be awaited -- All test functions must be marked with `@pytest.mark.asyncio` or be async test functions -- Use `await` when calling the async function -- Import `asyncio` when needed for running async functions -- Test concurrent execution using `asyncio.gather()` when appropriate -- Test async context managers and async iterators if applicable +**Throughput tests** — Measure performance under load and high-volume scenarios. Name these functions with `_throughput_` in the name (e.g., `test_function_throughput_high_load`). Test with varying loads (small, medium, large) and sustained execution patterns. -**1. Basic Test Cases**: -- **Objective**: To verify the fundamental functionality of the async {function_name} function under normal conditions. -- Test that the function returns expected values when awaited -- Test basic async/await behavior +**Test quality criteria:** +- Tests should be diverse — cover a wide range of inputs and async-specific scenarios +- Tests must be deterministic — always pass or fail the same way +- Sort tests by difficulty, from easiest to hardest +- Do not mock or stub the function under test or its internal calls. Mock only external dependencies (APIs, databases, network, file I/O) if absolutely necessary. +- Do not use `Mock(spec=SomeClass)` for domain classes — create real instances instead +- Include concurrent execution tests using `asyncio.gather()` to assess async performance +- Test proper async/await patterns and coroutine handling -**2. Edge Test Cases**: -- **Objective**: To evaluate the function's behavior under extreme or unusual conditions. -- Test concurrent execution scenarios -- Test exception handling in async context -- **CRITICAL**: DO NOT generate test cases that intentionally timeout or hang indefinitely. Avoid tests with `asyncio.sleep()` for long durations or infinite loops that would cause timeouts -- **FORBIDDEN**: Never create tests using `asyncio.wait_for()` with short timeouts expecting `asyncio.TimeoutError` -- **FORBIDDEN**: Never create tests that rely on timing, delays, or timeout behavior to pass -- **FORBIDDEN**: Never use extremely short timeout values (like 0.00001) to force timeouts +**Async-specific rules:** +- All test functions must use `async def` and be marked with `@pytest.mark.asyncio` +- Use `await` when calling the async function under test +- Import `asyncio` when needed +- Test concurrent execution using `asyncio.gather()` where appropriate +- Never create tests that intentionally timeout or hang. No `asyncio.wait_for()` with short timeouts expecting `TimeoutError`. No tests that rely on timing or delays. +- All throughput test functions must include `_throughput_` in their name -**3. Large Scale Test Cases**: -- **Objective**: To assess the function's performance and scalability with concurrent execution. -- Test multiple concurrent calls using `asyncio.gather()` -- Test performance under concurrent load -- Avoid loops exceeding 1000 steps, and keep data structures under 1000 elements +**Rules:** +- **Preserve the original function** — do not modify, enhance, or add parameters to the function under test. Test it exactly as provided. +- **Use real classes** — never define stub, fake, mock, dummy, or placeholder classes. Import real classes from their actual modules. Tests that define their own classes will fail `isinstance()` checks. +- **Handle instance methods correctly** — if the function has `self`, import the class, create a real instance, and call the method on the instance with `await instance.method(...)`. Do not pass `self` manually. +- **Use conftest.py fixtures when provided** — prefer fixtures over manual instantiation. Fixtures are pre-configured and handle setup/teardown. +- **Import everything you use** — every symbol must have a corresponding import. If you use `Mock`, `MagicMock`, `AsyncMock`, `patch`, etc., import each explicitly from `unittest.mock`. +- **Only import what you use** — do not add unused imports. +- **Use correct import sources** — when the dependency context shows `from X import Y`, use that exact source module. +- **Use correct constructor signatures** — only use constructor arguments shown in the provided context. Use concrete subclasses instead of abstract classes. +- **Valid Python string literals** — use ASCII quotes (`'` or `"`) as delimiters. Unicode curly quotes are not valid Python string delimiters. -**4. Throughput Test Cases**: -- **Objective**: To measure the function's performance under load and high-volume scenarios. -- Test function performance with varying loads (small, medium, large volumes of data) -- Test sustained execution patterns to assess throughput capabilities -- **IMPORTANT**: Name these test functions with `_throughput_` in the function name to make it obvious they are throughput tests (e.g., `test_function_throughput_small_load`, `test_function_throughput_high_volume`) -- Measure and validate performance characteristics where appropriate -- Focus on realistic load patterns that the function might encounter in production - -**Instructions**: -- Implement a comprehensive set of test cases following the guidelines above. -- Ensure each test case is well-documented with comments explaining the scenario it covers. -- Pay special attention to async-specific edge cases as they often reveal hidden bugs. -- For large-scale tests, focus on the function's efficiency and performance under concurrent loads. -- For throughput tests, focus on measuring performance under realistic load scenarios. -- **CRITICAL**: All throughput test functions MUST include `_throughput_` in their name to clearly identify them as throughput tests. -- Always use proper async/await syntax and test decorators. -- **IMPORTANT**: Generate tests that complete quickly and deterministically. Avoid any test patterns that could cause timeouts, hanging, or excessive delays. Keep async operations fast and bounded. -- **CRITICAL: DO NOT MOCK THE FUNCTION UNDER TEST** - Never mock, stub, or patch the {function_name} function itself or any internal functions/methods it calls. You may mock external dependencies (APIs, databases, network calls, file I/O, etc.) if necessary, but the function being tested must execute with its real implementation. -- **CRITICAL: IMPORT CLASSES FROM THEIR REAL MODULES** - When the context shows class definitions with file paths (e.g., ```python:path/to/module.py), you MUST import those classes from their actual modules instead of redefining them. For example, if you see `class Foo` in `mypackage/utils.py`, import it as `from mypackage.utils import Foo`. This is essential because the function under test uses `isinstance()` checks against the real classes, not mock versions. Never redefine classes that exist in the codebase - always import them. -- **CRITICAL: IMPORT EVERYTHING YOU USE** - Every symbol (class, function, constant) you reference in your test code MUST have a corresponding import statement. If you use `Mock`, `MagicMock`, `AsyncMock`, `PropertyMock`, `patch`, or `call`, import each one explicitly from `unittest.mock`. If you use `pytest.raises`, `pytest.mark`, or `pytest.fixture`, import `pytest`. Double-check that every name in your code is either imported, defined locally, or is a Python builtin. -- **CRITICAL: ONLY IMPORT WHAT YOU USE** - Do not add imports for classes or functions that you don't actually use in your test code. If you see a class mentioned in type hints or dependency code but don't need it for your tests, don't import it. -- **CRITICAL: USE CORRECT IMPORT SOURCES** - When you see `from X import Y` in the dependency context, and you need to import `Y` in your tests, use the SAME source module `X` that the dependency uses. Do NOT guess or assume a different module. For example, if you see `from mypackage.utils import Helper` in the dependencies, import it as `from mypackage.utils import Helper`, not from any other module. -- **CRITICAL: DO NOT USE MOCK OBJECTS FOR DOMAIN CLASSES** - Never use `Mock(spec=SomeClass)` to create instances of domain classes like Element, PreChunk, ChunkingOptions, etc. Mock objects cannot be serialized/pickled and will cause test failures. Instead, always create real instances by importing and instantiating the actual classes. This applies to ALL objects that will be passed to or used by the function being tested, including objects nested inside other objects. -- **CRITICAL: USE CORRECT CONSTRUCTOR SIGNATURES** - When instantiating classes, use only the constructor arguments shown in the provided context. Do not assume or invent parameter names. If a class appears to be abstract (marked with `abc.ABC`) or its constructor doesn't accept the arguments you need, look for and use concrete subclasses instead. For example, if `Element` is abstract and doesn't accept text, use `Text("content")` from the same module instead. - -**CRITICAL: VALID PYTHON STRING LITERALS** -- Python strings MUST be delimited with ASCII quotes: ' (U+0027) or " (U+0022) -- Unicode curly quotes (' ' " ") are NOT valid Python string delimiters -- To include special quote characters IN a string, put them inside ASCII-quoted strings: - - CORRECT: `text = "'test'"` (curly quotes as content inside ASCII double quotes) - - CORRECT: `text = '\u2018test\u2019'` (using Unicode escapes) - - WRONG: `text = ''test''` (curly quotes cannot be string delimiters) -- Always verify your string literals are properly delimited and terminated - -**Output Format Requirements**: -- Your response MUST be a single markdown code block containing valid Python code. -- Do NOT nest code blocks inside each other. -- Do NOT include markdown code fences (```) anywhere inside your code, including in string literals, comments, or docstrings. -- Do NOT include "reference code" or "module code" as string variables - just import what you need from the real modules. -- The code block MUST contain at least one async test function (e.g., `async def test_...`). +**Output format:** +- Respond with a single markdown code block containing valid Python code. +- Do not nest code blocks or include markdown fences inside code. +- Do not include "reference code" as string variables — import from real modules. +- The code block must contain at least one `async def test_...` function. - Follow the exact template structure provided in the user message. diff --git a/django/aiservice/testgen/execute_async_user_prompt.md b/django/aiservice/testgen/execute_async_user_prompt.md index 63a835ac4..fed999a83 100644 --- a/django/aiservice/testgen/execute_async_user_prompt.md +++ b/django/aiservice/testgen/execute_async_user_prompt.md @@ -1,17 +1,4 @@ -Using Python and the `{unit_test_package}` package, write a suite of unit tests for the **async** function '{function_name}', following the cases above. ONLY include assert/raise statements present in the Python library and NOT in libraries such as numpy, pandas etc. Include helpful comments to explain each line. - -**CRITICAL: DO NOT MODIFY THE FUNCTION** -- You MUST copy the function EXACTLY as provided below -- DO NOT add parameters, change logic, or enhance the function -- Test the function AS-IS with its current signature and behavior - -**CRITICAL: The function under test is ASYNC - you MUST:** -- Import `asyncio` for async functionality -- Use `@pytest.mark.asyncio` decorator on all test functions (for pytest) -- Use `async def` for all test functions -- Use `await` when calling the {function_name} function -- Test concurrent execution patterns where appropriate -- **IMPORTANT**: Name throughput test functions with `_throughput_` in the name (e.g., `test_{function_name}_throughput_high_load`) to clearly identify them as throughput tests +Using Python and the `{unit_test_package}` package, write a suite of unit tests for the **async** function '{function_name}'. ONLY include assert/raise statements present in the Python library and NOT in libraries such as numpy, pandas etc. Include helpful comments to explain each line. Reply with a single Python code block in this exact format: diff --git a/django/aiservice/testgen/execute_system_prompt.md b/django/aiservice/testgen/execute_system_prompt.md index ba1c31536..1c17adf1a 100644 --- a/django/aiservice/testgen/execute_system_prompt.md +++ b/django/aiservice/testgen/execute_system_prompt.md @@ -1,75 +1,41 @@ -**Role**: You are Codeflash, a world-class Python developer with an eagle eye for unintended bugs and edge cases. You write careful, accurate unit tests. When asked to reply only with code, you write all of your code in a single markdown code block. +You are Codeflash, a world-class Python testing engineer. Your goal is to write a comprehensive, high-quality unit test suite for the `{function_name}` function that fully defines its behavior — passing tests confirm correctness, and any mutation to the source code should cause at least one test to fail. -**Task** Your task is to create comprehensive, high quality test cases for the {function_name} function. These test cases should encompass Basic, Edge, and Large Scale scenarios to ensure the code's robustness, reliability, and scalability. These test cases should *define* the {function_name} function, meaning that the function should pass all the tests, and a function with different external functional behavior should fail them. In other words, the test suite should fail under mutation testing of the source code. +**Think step by step before writing code.** Analyze the function: +1. What does it do? What are its inputs, outputs, and return types? +2. What are the normal/expected usage patterns? +3. What edge cases exist (empty inputs, boundary values, type variations, error conditions)? +4. What large-scale or performance-relevant scenarios should be covered? -**CRITICAL: PRESERVE ORIGINAL FUNCTION** -- You MUST use the EXACT original function signature and implementation provided -- DO NOT modify, enhance, or add parameters to the original function -- DO NOT change the function's internal logic or behavior -- The function code provided is the ONLY version you should test -- Write tests for the function AS-IS, not for an improved version +Then write tests organized into three categories: -**CRITICAL: USE REAL CLASSES - NO STUBS OR FAKES** -- NEVER define your own classes in the test file to replace real classes from the codebase -- When the context shows `from X import Y`, use that EXACT same import in your tests -- DO NOT create classes named `FakeX`, `StubX`, `MockX`, `DummyX`, `TestX`, `AsyncX`, or ANY class that mimics a real class -- DO NOT create "minimal", "helper", "placeholder", or "stub" versions of classes -- DO NOT add comments like "Minimal stub" or "Simple container" - these indicate you're doing it wrong -- If a class requires dependencies, look at the context to see how to construct it properly -- Tests that define their own classes WILL FAIL because isinstance() checks will fail +**Basic tests** — Verify fundamental functionality under normal conditions with typical inputs. -**CRITICAL: HANDLING INSTANCE METHODS** -- If the function has `self` as its first parameter, it is an **instance method** of a class -- DO NOT copy the method body into your test file and call it as a standalone function like `method_name(some_obj, ...)` -- Instead, you MUST: - 1. Import the class from its real module (e.g., `from mypackage.module import MyClass`) - 2. Create a real instance of the class (check conftest.py for fixtures that provide instances) - 3. Call the method ON the instance: `instance.method_name(...)` -- Python passes `self` automatically when you call methods on instances - do NOT pass it manually -- Example: If testing `MyClass.process(self, data)`, write `instance = MyClass(...); result = instance.process(data)` +**Edge tests** — Evaluate behavior under extreme or unusual conditions: empty inputs, boundary values, special characters, None values, type edge cases. -**CRITICAL: USE CONFTEST.PY FIXTURES WHEN PROVIDED** -- If the context includes conftest.py files, ALWAYS check for fixtures that provide the objects you need -- Pytest fixtures are defined with `@pytest.fixture` decorator and can be used simply by adding them as test function parameters -- Fixtures automatically handle setup/teardown and provide properly configured objects -- Example: If conftest.py has `@pytest.fixture def embedding_model(): ...`, use it as `def test_something(embedding_model): ...` -- PREFER fixtures over manual instantiation - they're pre-configured to work correctly -- Fixtures may provide Fake/Mock implementations that are designed to work with the codebase - USE THESE instead of creating your own -- Look for fixtures that return objects of the types you need (check return type hints and fixture names) +**Large-scale tests** — Assess performance and scalability. Use data structures up to 1000 elements and loops up to 1000 iterations. -**1. Basic Test Cases**: -- **Objective**: To verify the fundamental functionality of the {function_name} function under normal conditions. - **2. Edge Test Cases**: -- **Objective**: To evaluate the function's behavior under extreme or unusual conditions. - **3. Large Scale Test Cases**: -- **Objective**: To assess the function's performance and scalability with large data samples. +**Test quality criteria:** +- Tests should be diverse — cover a wide range of inputs and scenarios +- Tests must be deterministic — always pass or fail the same way +- Sort tests by difficulty, from easiest to hardest +- Do not mock or stub the function under test or its internal calls. Mock only external dependencies (APIs, databases, network, file I/O) if absolutely necessary. +- Do not use `Mock(spec=SomeClass)` for domain classes — create real instances instead +- Include large-scale test cases to assess performance with realistic data volumes - **Instructions**: -- Implement a comprehensive set of test cases following the guidelines above. -- Ensure each test case is well-documented with comments explaining the scenario it covers. -- Pay special attention to edge cases as they often reveal hidden bugs. -- For large-scale tests, focus on the function's efficiency and performance under heavy loads. Avoid loops exceeding 1000 steps, and keep data structures under 1000 elements. -- **CRITICAL: DO NOT MOCK THE FUNCTION UNDER TEST** - Never mock, stub, or patch the {function_name} function itself or any internal functions/methods it calls. You may mock external dependencies (APIs, databases, network calls, file I/O, etc.) if necessary, but the function being tested must execute with its real implementation. -- **CRITICAL: IMPORT CLASSES FROM THEIR REAL MODULES** - When the context shows class definitions with file paths (e.g., ```python:path/to/module.py), you MUST import those classes from their actual modules instead of redefining them. For example, if you see `class Foo` in `mypackage/utils.py`, import it as `from mypackage.utils import Foo`. This is essential because the function under test uses `isinstance()` checks against the real classes, not mock versions. Never redefine classes that exist in the codebase - always import them. -- **CRITICAL: IMPORT EVERYTHING YOU USE** - Every symbol (class, function, constant) you reference in your test code MUST have a corresponding import statement. If you use `Mock`, `MagicMock`, `AsyncMock`, `PropertyMock`, `patch`, or `call`, import each one explicitly from `unittest.mock`. If you use `pytest.raises`, `pytest.mark`, or `pytest.fixture`, import `pytest`. Double-check that every name in your code is either imported, defined locally, or is a Python builtin. -- **CRITICAL: ONLY IMPORT WHAT YOU USE** - Do not add imports for classes or functions that you don't actually use in your test code. If you see a class mentioned in type hints or dependency code but don't need it for your tests, don't import it. -- **CRITICAL: USE CORRECT IMPORT SOURCES** - When you see `from X import Y` in the dependency context, and you need to import `Y` in your tests, use the SAME source module `X` that the dependency uses. Do NOT guess or assume a different module. For example, if you see `from mypackage.utils import Helper` in the dependencies, import it as `from mypackage.utils import Helper`, not from any other module. -- **CRITICAL: DO NOT USE MOCK OBJECTS FOR DOMAIN CLASSES** - Never use `Mock(spec=SomeClass)` to create instances of domain classes like Element, PreChunk, ChunkingOptions, etc. Mock objects cannot be serialized/pickled and will cause test failures. Instead, always create real instances by importing and instantiating the actual classes. This applies to ALL objects that will be passed to or used by the function being tested, including objects nested inside other objects. -- **CRITICAL: USE CORRECT CONSTRUCTOR SIGNATURES** - When instantiating classes, use only the constructor arguments shown in the provided context. Do not assume or invent parameter names. If a class appears to be abstract (marked with `abc.ABC`) or its constructor doesn't accept the arguments you need, look for and use concrete subclasses instead. For example, if `Element` is abstract and doesn't accept text, use `Text("content")` from the same module instead. +**Rules:** +- **Preserve the original function** — do not modify, enhance, or add parameters to the function under test. Test it exactly as provided. +- **Use real classes** — never define stub, fake, mock, dummy, or placeholder classes. Import real classes from their actual modules. Tests that define their own classes will fail `isinstance()` checks. +- **Handle instance methods correctly** — if the function has `self`, import the class, create a real instance, and call the method on the instance. Do not pass `self` manually. +- **Use conftest.py fixtures when provided** — prefer fixtures over manual instantiation. Fixtures are pre-configured and handle setup/teardown. +- **Import everything you use** — every symbol must have a corresponding import. If you use `Mock`, `MagicMock`, `patch`, etc., import each explicitly from `unittest.mock`. +- **Only import what you use** — do not add unused imports. +- **Use correct import sources** — when the dependency context shows `from X import Y`, use that exact source module. +- **Use correct constructor signatures** — only use constructor arguments shown in the provided context. Use concrete subclasses instead of abstract classes. +- **Valid Python string literals** — use ASCII quotes (`'` or `"`) as delimiters. Unicode curly quotes are not valid Python string delimiters. -**CRITICAL: VALID PYTHON STRING LITERALS** -- Python strings MUST be delimited with ASCII quotes: ' (U+0027) or " (U+0022) -- Unicode curly quotes (' ' " ") are NOT valid Python string delimiters -- To include special quote characters IN a string, put them inside ASCII-quoted strings: - - CORRECT: `text = "'test'"` (curly quotes as content inside ASCII double quotes) - - CORRECT: `text = '\u2018test\u2019'` (using Unicode escapes) - - WRONG: `text = ''test''` (curly quotes cannot be string delimiters) -- Always verify your string literals are properly delimited and terminated - -**Output Format Requirements**: -- Your response MUST be a single markdown code block containing valid Python code. -- Do NOT nest code blocks inside each other. -- Do NOT include markdown code fences (```) anywhere inside your code, including in string literals, comments, or docstrings. -- Do NOT include "reference code" or "module code" as string variables - just import what you need from the real modules. -- The code block MUST contain at least one test function (e.g., `def test_...`). +**Output format:** +- Respond with a single markdown code block containing valid Python code. +- Do not nest code blocks or include markdown fences inside code. +- Do not include "reference code" as string variables — import from real modules. +- The code block must contain at least one `def test_...` function. - Follow the exact template structure provided in the user message. diff --git a/django/aiservice/testgen/execute_user_prompt.md b/django/aiservice/testgen/execute_user_prompt.md index 0fd6aaf37..bfa2c302e 100644 --- a/django/aiservice/testgen/execute_user_prompt.md +++ b/django/aiservice/testgen/execute_user_prompt.md @@ -1,4 +1,4 @@ -Using Python and the `{unit_test_package}` package, write a suite of unit tests for the function '{function_name}', following the cases above. ONLY include assert/raise statements present in the Python library and NOT in libraries such as numpy, pandas etc. Include helpful comments to explain each line. +Using Python and the `{unit_test_package}` package, write a suite of unit tests for the function '{function_name}'. ONLY include assert/raise statements present in the Python library and NOT in libraries such as numpy, pandas etc. Include helpful comments to explain each line. Reply with a single Python code block in this exact format: diff --git a/django/aiservice/testgen/testgen.py b/django/aiservice/testgen/testgen.py index eea95c21f..eb3377f2d 100644 --- a/django/aiservice/testgen/testgen.py +++ b/django/aiservice/testgen/testgen.py @@ -83,65 +83,36 @@ def build_prompt( if is_async: execute_system_prompt = EXECUTE_ASYNC_SYSTEM_PROMPT execute_user_prompt = EXECUTE_ASYNC_USER_PROMPT - plan_content = f"""A good unit test suite for an ASYNC function should aim to: -- Test the async function's behavior for a wide range of possible inputs -- Test edge cases that the author may not have foreseen, including async-specific edge cases -- Take advantage of the features of `{unit_test_package}` to make async tests easy to write and maintain -- Be easy to read and understand, with clean async code and descriptive names -- Be deterministic, so that the async tests always pass or fail in the same way -- Have tests sorted by difficulty, from easiest to hardest -- Should try not to mock or stub any dependencies, so that the async testing environment is as close to production -- Include concurrent execution test cases to assess the function's async performance and behavior -- Include throughput test cases to measure the function's performance under load and high-volume scenarios -- Test proper async/await patterns and coroutine handling - -To help unit test the ASYNC function above, list diverse scenarios that the async function should be able to handle (and under each scenario, include a few examples as sub-bullets).""" posthog_event_suffix = "async-" error_context = "async " else: execute_system_prompt = EXECUTE_SYSTEM_PROMPT execute_user_prompt = EXECUTE_USER_PROMPT - plan_content = f"""A good unit test suite should aim to: -- Test the function's behavior for a wide range of possible inputs -- Test edge cases that the author may not have foreseen -- Take advantage of the features of `{unit_test_package}` to make the tests easy to write and maintain -- Be easy to read and understand, with clean code and descriptive names -- Be deterministic, so that the tests always pass or fail in the same way -- Have tests sorted by difficulty, from easiest to hardest -- Should try not to mock or stub any dependencies by using `{unit_test_package}`.mock or any other similar mocking or stubbing module, so that the testing environment is as close to the production environment as possible -- Include Large Scale Test Cases to assess the function's performance and scalability with large data samples. - -To help unit test the function above, list diverse scenarios that the function should be able to handle (and under each scenario, include a few examples as sub-bullets).""" posthog_event_suffix = "" error_context = "" - plan_user_message = {"role": "user", "content": plan_content} - package_comment = "" - # if unit_test_package == "pytest": - # package_comment = "# below, each test case is represented by a tuple passed to the @pytest.mark.parametrize decorator" system_prompt = execute_system_prompt.format(function_name=ctx.data.qualified_name) if is_numerical_code: system_prompt += f"\n{JIT_INSTRUCTIONS}\n" execute_system_message = {"role": "system", "content": system_prompt} - execute_messages = [execute_system_message, plan_user_message] - + # Build a single user message combining notes and the code template + user_parts = [] all_notes = ctx.generate_notes_markdown() - note_message = {"role": "user", "content": all_notes} + if all_notes: + user_parts.append(all_notes) - execute_messages += [note_message] - - execute_user_message = { - "role": "user", - "content": execute_user_prompt.format( + user_parts.append( + execute_user_prompt.format( unit_test_package=unit_test_package, function_name=function_name, function_code=ctx.data.source_code_being_tested, - package_comment=package_comment, - ), - } + package_comment="", + ) + ) - execute_messages += [execute_user_message] + execute_user_message = {"role": "user", "content": "\n\n".join(user_parts)} + execute_messages = [execute_system_message, execute_user_message] return execute_messages, posthog_event_suffix, error_context @@ -199,6 +170,7 @@ def parse_and_validate_llm_output( "No Python code block found in the LLM response.", raw_llm_output=response_content ) + # TODO: move replace_definition_with_import to postprocessing_testgen_pipeline for separation of concerns if function_to_optimize is not None and module_path is not None: try: code = replace_definition_with_import(parse_module(code), function_to_optimize, module_path).code @@ -243,7 +215,7 @@ async def generate_and_validate_test_code( test_module_path: str | None = None, helper_function_names: list[str] | None = None, is_async: bool = False, -) -> str: +) -> tuple[str, str]: obs_context: dict | None = ( { "call_sequence": call_sequence, @@ -289,9 +261,11 @@ async def generate_and_validate_test_code( properties={"model": execute_model.name, "usage": response.raw_response.usage.model_dump_json()}, ) + raw_llm_content = response.content + # Parse and validate validated_code = parse_and_validate_llm_output( - response_content=response.content, + response_content=raw_llm_content, ctx=ctx, python_version=python_version, error_context=error_context, @@ -299,7 +273,7 @@ async def generate_and_validate_test_code( module_path=module_path, ) - return validated_code + return validated_code, raw_llm_content @stamina.retry(on=TestGenerationFailedError, attempts=2) @@ -314,7 +288,7 @@ async def generate_regression_tests_from_function( is_async: bool = False, # noqa: FBT001, FBT002 trace_id: str = "", call_sequence: int | None = None, -) -> tuple[str, str | None, str | None]: +) -> tuple[str, str | None, str | None, str]: execute_messages, posthog_event_suffix, error_context = build_prompt( ctx=ctx, function_name=function_name, @@ -325,7 +299,7 @@ async def generate_regression_tests_from_function( cost_tracker = [] try: - validated_code = await generate_and_validate_test_code( + validated_code, raw_llm_content = await generate_and_validate_test_code( messages=execute_messages, model=execute_model, ctx=ctx, @@ -393,7 +367,7 @@ async def generate_regression_tests_from_function( "validation_error": "No test functions found after postprocessing", }, ) - return generated_test_source, instrumented_behavior_tests, instrumented_perf_tests # noqa: TRY300 + return generated_test_source, instrumented_behavior_tests, instrumented_perf_tests, raw_llm_content # noqa: TRY300 except CodeValidationError as e: total_llm_cost = sum(cost_tracker) await update_optimization_cost(trace_id=trace_id, cost=total_llm_cost, user_id=user_id) @@ -536,6 +510,7 @@ async def testgen_python( generated_test_source, instrumented_behavior_tests, instrumented_perf_tests, + raw_llm_content, ) = await generate_regression_tests_from_function( ctx=ctx, user_id=request.user, @@ -555,7 +530,7 @@ async def testgen_python( await log_features( trace_id=data.trace_id, user_id=request.user, - generated_tests=[generated_test_source], + generated_tests=[raw_llm_content], instrumented_generated_tests=[instrumented_behavior_tests], instrumented_perf_tests=[instrumented_perf_tests], test_framework=data.test_framework, diff --git a/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx b/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx index d2a69a57e..8f8c0ca57 100644 --- a/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx +++ b/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx @@ -8,6 +8,7 @@ import { Box, RefreshCw, ChevronDown, + ChevronUp, FileText, Code, GitCompare, @@ -16,6 +17,8 @@ import { AlertCircle, BarChart3, Bug, + Search, + X, } from "lucide-react" import { Dialog, @@ -135,28 +138,57 @@ function findMatchingFile( return files[0] || null } -/** Renders prompt content with syntax-highlighted code blocks */ +/** Renders prompt content with syntax-highlighted code blocks. + * Uses line-by-line parsing so nested fences (e.g. ```python:path inside + * an outer ```python block) are kept as code content instead of breaking + * the block. A code block is only closed by a standalone ``` line. */ const PromptContent = memo(function PromptContent({ content }: { content: string }) { const parts = useMemo(() => { const result: { type: "text" | "code"; content: string; language?: string }[] = [] - const codeBlockRegex = /```(\w+)?\n?([\s\S]*?)```/g - let lastIndex = 0 - let match + const lines = content.split("\n") + let inCode = false + let codeLang = "python" + let codeLines: string[] = [] + let textLines: string[] = [] + let depth = 0 - while ((match = codeBlockRegex.exec(content)) !== null) { - if (match.index > lastIndex) { - result.push({ type: "text", content: content.slice(lastIndex, match.index) }) + for (const line of lines) { + if (!inCode) { + const fence = line.match(/^```(\w+)/) + if (fence) { + if (textLines.length > 0) { + result.push({ type: "text", content: textLines.join("\n") }) + textLines = [] + } + inCode = true + depth = 0 + codeLang = fence[1] || "python" + codeLines = [] + } else { + textLines.push(line) + } + } else if (line.match(/^```\w/)) { + depth++ + codeLines.push(line) + } else if (line.trim() === "```") { + if (depth > 0) { + depth-- + codeLines.push(line) + } else { + result.push({ type: "code", content: codeLines.join("\n").trim(), language: codeLang }) + codeLines = [] + inCode = false + } + } else { + codeLines.push(line) } - result.push({ - type: "code", - content: match[2].trim(), - language: match[1] || "python", - }) - lastIndex = match.index + match[0].length } - if (lastIndex < content.length) { - result.push({ type: "text", content: content.slice(lastIndex) }) + if (inCode && codeLines.length > 0) { + result.push({ type: "code", content: codeLines.join("\n").trim(), language: codeLang }) + } + if (textLines.length > 0) { + result.push({ type: "text", content: textLines.join("\n") }) } return result.length > 0 ? result : [{ type: "text" as const, content }] @@ -186,6 +218,90 @@ const PromptContent = memo(function PromptContent({ content }: { content: string ) }) +function clearSearchHighlights(container: HTMLElement) { + const marks = container.querySelectorAll("mark[data-search-highlight]") + marks.forEach(mark => { + const parent = mark.parentNode + if (parent) { + parent.replaceChild(document.createTextNode(mark.textContent || ""), mark) + parent.normalize() + } + }) +} + +function applySearchHighlights(container: HTMLElement, query: string): number { + clearSearchHighlights(container) + if (!query.trim()) return 0 + + const lowerQuery = query.toLowerCase() + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT) + const nodesToProcess: { node: Text; matches: { start: number; end: number }[] }[] = [] + + let textNode: Text | null + while ((textNode = walker.nextNode() as Text | null)) { + const text = textNode.textContent || "" + const lowerText = text.toLowerCase() + const matches: { start: number; end: number }[] = [] + let searchFrom = 0 + + while (true) { + const index = lowerText.indexOf(lowerQuery, searchFrom) + if (index === -1) break + matches.push({ start: index, end: index + query.length }) + searchFrom = index + query.length + } + + if (matches.length > 0) { + nodesToProcess.push({ node: textNode, matches }) + } + } + + let totalMatches = 0 + + for (const { node, matches } of nodesToProcess) { + const text = node.textContent || "" + const fragment = document.createDocumentFragment() + let lastIndex = 0 + + for (const match of matches) { + if (match.start > lastIndex) { + fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.start))) + } + + const mark = document.createElement("mark") + mark.setAttribute("data-search-highlight", "") + mark.setAttribute("data-match-index", String(totalMatches)) + mark.className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]" + mark.textContent = text.slice(match.start, match.end) + fragment.appendChild(mark) + + totalMatches++ + lastIndex = match.end + } + + if (lastIndex < text.length) { + fragment.appendChild(document.createTextNode(text.slice(lastIndex))) + } + + node.parentNode?.replaceChild(fragment, node) + } + + return totalMatches +} + +function scrollToSearchMatch(container: HTMLElement, index: number) { + const marks = container.querySelectorAll("mark[data-search-highlight]") + marks.forEach(m => { + (m as HTMLElement).className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]" + }) + + const target = marks[index] as HTMLElement | undefined + if (target) { + target.className = "bg-orange-300 dark:bg-orange-600/70 text-inherit rounded-[2px] ring-2 ring-orange-400 dark:ring-orange-500" + target.scrollIntoView({ behavior: "smooth", block: "center" }) + } +} + interface LLMCallDebugDialogProps { debugData: LLMCallDebugData title: string @@ -201,6 +317,12 @@ const LLMCallDebugDialog = memo(function LLMCallDebugDialog({ const [activeTab, setActiveTab] = useState<"user" | "system">("user") const [showResponse, setShowResponse] = useState(false) const [contentReady, setContentReady] = useState(false) + const [searchOpen, setSearchOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [matchCount, setMatchCount] = useState(0) + const [currentMatch, setCurrentMatch] = useState(-1) + const contentRef = useRef(null) + const searchInputRef = useRef(null) useEffect(() => { if (open) { @@ -209,13 +331,125 @@ const LLMCallDebugDialog = memo(function LLMCallDebugDialog({ } else { setContentReady(false) setShowResponse(false) + setSearchOpen(false) + setSearchQuery("") + setMatchCount(0) + setCurrentMatch(-1) } }, [open]) + useEffect(() => { + if (searchOpen) { + requestAnimationFrame(() => searchInputRef.current?.focus()) + } + }, [searchOpen]) + + useEffect(() => { + const container = contentRef.current + if (!container || !contentReady) return + + const timer = setTimeout(() => { + clearSearchHighlights(container) + if (!searchQuery.trim()) { + setMatchCount(0) + setCurrentMatch(-1) + return + } + const count = applySearchHighlights(container, searchQuery) + setMatchCount(count) + const next = count > 0 ? 0 : -1 + setCurrentMatch(next) + if (count > 0) { + scrollToSearchMatch(container, 0) + } + }, 150) + + return () => clearTimeout(timer) + }, [searchQuery, activeTab, showResponse, contentReady]) + + function navigateMatch(direction: "next" | "prev") { + if (matchCount === 0) return + const next = direction === "next" + ? (currentMatch + 1) % matchCount + : (currentMatch - 1 + matchCount) % matchCount + setCurrentMatch(next) + if (contentRef.current) scrollToSearchMatch(contentRef.current, next) + } + + function closeSearch() { + setSearchOpen(false) + setSearchQuery("") + if (contentRef.current) clearSearchHighlights(contentRef.current) + } + const hasContent = debugData.systemPrompt || debugData.userPrompt || debugData.rawResponse if (!hasContent) return null + const searchBar = searchOpen ? ( +
    + + setSearchQuery(e.target.value)} + onKeyDown={e => { + if (e.key === "Enter") { + e.preventDefault() + navigateMatch(e.shiftKey ? "prev" : "next") + } else if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + closeSearch() + } + }} + placeholder={`Search ${activeTab === "user" ? "user" : "system"} prompt...`} + className="flex-1 bg-transparent text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none min-w-0" + /> + {searchQuery && ( + + {matchCount > 0 ? `${currentMatch + 1} of ${matchCount}` : "No matches"} + + )} +
    + + + +
    +
    + ) : ( +
    + +
    + ) + return ( @@ -226,7 +460,15 @@ const LLMCallDebugDialog = memo(function LLMCallDebugDialog({ - + { + if ((e.metaKey || e.ctrlKey) && e.key === "f") { + e.preventDefault() + setSearchOpen(true) + } + }} + >
    @@ -261,14 +503,16 @@ const LLMCallDebugDialog = memo(function LLMCallDebugDialog({ {showResponse ? ( /* Raw Response View */ -
    - {debugData.rawResponse ? ( -
    -                {debugData.rawResponse}
    -              
    - ) : ( - No response - )} +
    +
    + {debugData.rawResponse ? ( +
    +                  {debugData.rawResponse}
    +                
    + ) : ( + No response + )} +
    ) : ( /* Prompts View */ @@ -288,28 +532,31 @@ const LLMCallDebugDialog = memo(function LLMCallDebugDialog({ -
    - {!contentReady ? ( -
    -
    -
    -
    -
    - ) : activeTab === "user" ? ( - debugData.userPrompt ? ( - +
    + {searchBar} +
    + {!contentReady ? ( +
    +
    +
    +
    +
    + ) : activeTab === "user" ? ( + debugData.userPrompt ? ( + + ) : ( + No user prompt + ) ) : ( - No user prompt - ) - ) : ( - debugData.systemPrompt ? ( -
    -                    {debugData.systemPrompt}
    -                  
    - ) : ( - No system prompt - ) - )} + debugData.systemPrompt ? ( +
    +                      {debugData.systemPrompt}
    +                    
    + ) : ( + No system prompt + ) + )} +
    )} From 968946d62d757bbc44c6bd41a57cee24d2bee777 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Mon, 9 Feb 2026 04:21:41 -0500 Subject: [PATCH 060/184] feat: split diff view and LLM export for observability (#2384) ## Summary - Add split (side-by-side) diff view to the observability timeline for comparing original vs optimized code - Fix scroll handler not updating active section + expand container for candidates - Add LLM export route that returns plain text markdown of the full trace, accessible via button next to search bar ## Test plan - [ ] Load a trace in observability and verify the split diff view renders correctly - [ ] Verify the "LLM Export" button appears next to Search when results are loaded - [ ] Click the button and verify the new tab returns raw markdown text (no HTML chrome) - [ ] Verify all sections are present: function info, original code, tests, candidates, ranking, errors, summary, and prompts --- .../components/format-llm-export.ts | 182 +++++++++++++++++ .../components/timeline-page-view.tsx | 183 +++++++++++++++++- .../observability/components/trace-search.tsx | 14 +- .../app/observability/lib/get-trace-data.ts | 29 +++ .../src/app/observability/llm-export/route.ts | 171 ++++++++++++++++ js/cf-webapp/src/app/observability/page.tsx | 37 +--- 6 files changed, 577 insertions(+), 39 deletions(-) create mode 100644 js/cf-webapp/src/app/observability/components/format-llm-export.ts create mode 100644 js/cf-webapp/src/app/observability/lib/get-trace-data.ts create mode 100644 js/cf-webapp/src/app/observability/llm-export/route.ts diff --git a/js/cf-webapp/src/app/observability/components/format-llm-export.ts b/js/cf-webapp/src/app/observability/components/format-llm-export.ts new file mode 100644 index 000000000..dd6cfebc0 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/format-llm-export.ts @@ -0,0 +1,182 @@ +import type { TimelineSection } from "./timeline-types" + +interface LLMExportInput { + traceId: string + functionName: string | null + filePath: string | null + originalCode: string | null + sections: TimelineSection[] + errors: Array<{ + error_type: string | null + severity: string | null + error_message: string | null + context: { + test_name?: string + failure_reason?: string + test_output?: string + expected?: string + actual?: string + } | null + created_at: Date + }> + totalDuration: number +} + +function formatDuration(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms` + return `${(ms / 1000).toFixed(1)}s` +} + +function formatCost(cost: number | null | undefined): string { + if (cost == null || cost === 0) return "$0.00" + if (cost < 0.01) return `$${cost.toFixed(4)}` + return `$${cost.toFixed(2)}` +} + +export function formatTimelineForLLM(input: LLMExportInput): string { + const { traceId, functionName, filePath, originalCode, sections, errors, totalDuration } = input + const lines: string[] = [] + + lines.push(`# Optimization Trace: ${traceId}`) + if (functionName) lines.push(`## Function: ${functionName}`) + if (filePath) lines.push(`File: ${filePath}`) + lines.push("") + + if (originalCode) { + lines.push("### Original Code") + lines.push("```python") + lines.push(originalCode) + lines.push("```") + lines.push("") + } + + lines.push("---") + lines.push("## Timeline") + lines.push("") + + let sectionNum = 0 + for (const section of sections) { + sectionNum++ + lines.push(`### ${sectionNum}. ${section.title}`) + + const meta: string[] = [] + if (section.model) meta.push(`Model: ${section.model}`) + if (section.cost != null) meta.push(`Cost: ${formatCost(section.cost)}`) + if (section.tokens != null) meta.push(`Tokens: ${section.tokens}`) + if (section.duration != null) meta.push(`Duration: ${formatDuration(section.duration)}`) + meta.push(`Status: ${section.status}`) + if (meta.length > 0) lines.push(meta.join(" | ")) + lines.push("") + + const content = section.content + + if (content.type === "tests") { + if (content.testFramework) { + lines.push(`Test framework: ${content.testFramework}`) + lines.push("") + } + for (const group of content.testGroups) { + if (group.generated) { + lines.push(`#### Generated Test ${group.index}`) + lines.push("```python") + lines.push(group.generated.code) + lines.push("```") + lines.push("") + } + } + } else if (content.type === "candidate" || content.type === "refinement") { + if (content.rank != null) { + lines.push(`Rank: #${content.rank}${content.isBest ? " (Best)" : ""}`) + lines.push("") + } + if (content.explanation) { + lines.push("#### Explanation") + lines.push(content.explanation) + lines.push("") + } + lines.push("#### Optimized Code") + lines.push("```python") + lines.push(content.code) + lines.push("```") + lines.push("") + } else if (content.type === "ranking") { + if (content.explanation) { + lines.push("#### Ranking Explanation") + lines.push(content.explanation) + lines.push("") + } + for (const r of content.rankings) { + lines.push(`${r.rank}. ${r.label}${r.isBest ? " (Best)" : ""}`) + } + if (content.usedForPr) { + lines.push("") + lines.push("*Used for pull request*") + } + lines.push("") + } else if (content.type === "summary") { + lines.push(`- Total Cost: ${formatCost(content.metrics.totalCost)}`) + lines.push(`- Total Tokens: ${content.metrics.totalTokens}`) + lines.push(`- Total Duration: ${formatDuration(content.metrics.totalDuration)}`) + lines.push(`- Candidates: ${content.metrics.candidatesCount}`) + lines.push("") + } + + if (section.debugData) { + const { systemPrompt, userPrompt } = section.debugData + if (systemPrompt) { + lines.push("#### System Prompt") + lines.push("```") + lines.push(systemPrompt) + lines.push("```") + lines.push("") + } + if (userPrompt) { + lines.push("#### User Prompt") + lines.push("```") + lines.push(userPrompt) + lines.push("```") + lines.push("") + } + } + } + + if (errors.length > 0) { + lines.push("---") + lines.push("## Errors") + lines.push("") + for (const error of errors) { + lines.push(`### ${error.error_type || "Error"}${error.severity ? ` (${error.severity})` : ""}`) + if (error.error_message) lines.push(error.error_message) + if (error.context) { + const ctx = error.context + if (ctx.test_name) lines.push(`Test: ${ctx.test_name}`) + if (ctx.failure_reason) lines.push(`Reason: ${ctx.failure_reason}`) + if (ctx.test_output) { + lines.push("```") + lines.push(ctx.test_output) + lines.push("```") + } + if (ctx.expected) lines.push(`Expected: ${ctx.expected}`) + if (ctx.actual) lines.push(`Actual: ${ctx.actual}`) + } + lines.push("") + } + } + + const totalCost = sections.reduce((sum, s) => sum + (s.cost ?? 0), 0) + const totalTokens = sections.reduce((sum, s) => sum + (s.tokens ?? 0), 0) + const candidatesCount = sections.filter( + s => s.type === "optimization" || s.type === "line_profiler" || s.type === "refinement", + ).length + + lines.push("---") + lines.push("## Summary") + lines.push(`- Total Cost: ${formatCost(totalCost)}`) + lines.push(`- Total Tokens: ${totalTokens}`) + lines.push(`- Total Duration: ${formatDuration(totalDuration)}`) + lines.push(`- Candidates: ${candidatesCount}`) + if (errors.length > 0) lines.push(`- Errors: ${errors.length}`) + lines.push("") + + return lines.join("\n") +} diff --git a/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx b/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx index 8f8c0ca57..6239173db 100644 --- a/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx +++ b/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx @@ -19,6 +19,7 @@ import { Bug, Search, X, + Columns2, } from "lucide-react" import { Dialog, @@ -658,6 +659,150 @@ const DiffView = memo(function DiffView({ diff }: { diff: string }) { ) }) +type SideBySideRow = { + leftLine: string | null + leftNum: number | null + leftType: "removed" | "context" | "empty" + rightLine: string | null + rightNum: number | null + rightType: "added" | "context" | "empty" +} + +const SideBySideDiffView = memo(function SideBySideDiffView({ + originalCode, + candidateCode, +}: { + originalCode: string + candidateCode: string + language: string +}) { + const [rows, setRows] = useState(null) + + useEffect(() => { + import("diff").then(({ diffLines }) => { + const changes = diffLines(originalCode, candidateCode) + const result: SideBySideRow[] = [] + let leftNum = 1 + let rightNum = 1 + + for (let i = 0; i < changes.length; i++) { + const change = changes[i] + const lines = change.value.replace(/\n$/, "").split("\n") + + if (!change.added && !change.removed) { + // Context lines + for (const line of lines) { + result.push({ + leftLine: line, leftNum: leftNum++, leftType: "context", + rightLine: line, rightNum: rightNum++, rightType: "context", + }) + } + } else if (change.removed) { + // Check if next change is an addition (paired change) + const next = i + 1 < changes.length ? changes[i + 1] : null + if (next && next.added) { + const removedLines = lines + const addedLines = next.value.replace(/\n$/, "").split("\n") + const maxLen = Math.max(removedLines.length, addedLines.length) + + for (let j = 0; j < maxLen; j++) { + result.push({ + leftLine: j < removedLines.length ? removedLines[j] : null, + leftNum: j < removedLines.length ? leftNum++ : null, + leftType: j < removedLines.length ? "removed" : "empty", + rightLine: j < addedLines.length ? addedLines[j] : null, + rightNum: j < addedLines.length ? rightNum++ : null, + rightType: j < addedLines.length ? "added" : "empty", + }) + } + i++ // Skip the next (added) change + } else { + // Pure removal + for (const line of lines) { + result.push({ + leftLine: line, leftNum: leftNum++, leftType: "removed", + rightLine: null, rightNum: null, rightType: "empty", + }) + } + } + } else if (change.added) { + // Pure addition (not paired with a removal) + for (const line of lines) { + result.push({ + leftLine: null, leftNum: null, leftType: "empty", + rightLine: line, rightNum: rightNum++, rightType: "added", + }) + } + } + } + + setRows(result) + }) + }, [originalCode, candidateCode]) + + if (!rows) { + return ( +
    +
    +
    +
    +
    + ) + } + + function getCellStyle(type: "removed" | "added" | "context" | "empty") { + switch (type) { + case "removed": + return "bg-red-900/30 text-red-300" + case "added": + return "bg-green-900/30 text-green-300" + case "empty": + return "bg-zinc-800/30" + default: + return "text-zinc-300" + } + } + + return ( +
    +
    +
    + Original +
    +
    + {rows.map((row, i) => ( +
    + + {row.leftNum ?? ""} + +
    +                {row.leftLine ?? " "}
    +              
    +
    + ))} +
    +
    +
    +
    + Optimized +
    +
    + {rows.map((row, i) => ( +
    + + {row.rightNum ?? ""} + +
    +                {row.rightLine ?? " "}
    +              
    +
    + ))} +
    +
    +
    + ) +}) + const TestContent = memo(function TestContent({ content }: { content: Extract }) { const [showDetails, setShowDetails] = useState(false) const [expandedTest, setExpandedTest] = useState(null) @@ -834,7 +979,7 @@ const CandidateContent = memo(function CandidateContent({ content: Extract isActive: boolean }) { - const [viewMode, setViewMode] = useState<"code" | "diff">("diff") + const [viewMode, setViewMode] = useState<"code" | "diff" | "side-by-side">("diff") const [selectedFileIndex, setSelectedFileIndex] = useState(0) const [unifiedDiff, setUnifiedDiff] = useState(null) const [diffLoading, setDiffLoading] = useState(false) @@ -887,7 +1032,7 @@ const CandidateContent = memo(function CandidateContent({ const hasMultipleFiles = candidateFiles.length > 1 const codeContainerStyle = useMemo( - () => ({ maxHeight: isActive ? "70vh" : "200px" }), + () => ({ maxHeight: isActive ? "80vh" : "200px" }), [isActive] ) @@ -937,6 +1082,17 @@ const CandidateContent = memo(function CandidateContent({ Diff +
    )} @@ -986,6 +1142,23 @@ const CandidateContent = memo(function CandidateContent({ No code available
    ) + ) : viewMode === "side-by-side" ? ( + matchingOriginalFile && selectedCandidateFile ? ( +
    + +
    + ) : ( +
    + No original code available for comparison +
    + ) ) : diffLoading ? (
    @@ -1235,6 +1408,7 @@ export const TimelinePageView = memo(function TimelinePageView({ window.removeEventListener("scroll", handleScroll) if (rafId.current !== null) { cancelAnimationFrame(rafId.current) + rafId.current = null } } }, [sections.length]) @@ -1247,6 +1421,9 @@ export const TimelinePageView = memo(function TimelinePageView({ ) } + const activeSection = sections[activeIndex] + const shouldExpandContainer = activeSection?.content.type === "candidate" || activeSection?.content.type === "refinement" + return (
    @@ -1279,7 +1456,7 @@ export const TimelinePageView = memo(function TimelinePageView({
    -
    +
    diff --git a/js/cf-webapp/src/app/observability/components/trace-search.tsx b/js/cf-webapp/src/app/observability/components/trace-search.tsx index ea2ec9106..834f74bd5 100644 --- a/js/cf-webapp/src/app/observability/components/trace-search.tsx +++ b/js/cf-webapp/src/app/observability/components/trace-search.tsx @@ -1,7 +1,7 @@ "use client" import { useState, useCallback, type ChangeEvent } from "react" -import { Search, Loader2, CheckCircle } from "lucide-react" +import { Search, Loader2, CheckCircle, FileText } from "lucide-react" import { useRouter } from "next/navigation" interface TraceSearchProps { @@ -75,6 +75,18 @@ export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults "Search" )} + {hasResults && ( + + + LLM Export + + )}
    {!hasResults && (

    diff --git a/js/cf-webapp/src/app/observability/lib/get-trace-data.ts b/js/cf-webapp/src/app/observability/lib/get-trace-data.ts new file mode 100644 index 000000000..57a17e522 --- /dev/null +++ b/js/cf-webapp/src/app/observability/lib/get-trace-data.ts @@ -0,0 +1,29 @@ +import { unstable_cache } from "next/cache" +import { prisma } from "@/lib/prisma" + +export const getTraceData = unstable_cache( + async (tracePrefix: string) => { + const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([ + prisma.llm_calls.findMany({ + where: { trace_id: { startsWith: tracePrefix } }, + orderBy: { created_at: "asc" }, + }), + prisma.optimization_errors.findMany({ + where: { trace_id: { startsWith: tracePrefix } }, + orderBy: { created_at: "asc" }, + }), + prisma.optimization_features.findFirst({ + where: { trace_id: { startsWith: tracePrefix } }, + }), + prisma.optimization_events.findFirst({ + where: { trace_id: { startsWith: tracePrefix } }, + select: { event_type: true, function_name: true, file_path: true }, + }), + ]) + return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } + }, + ["observability-trace-detail"], + { revalidate: 60 }, +) + +export type TraceData = Awaited> diff --git a/js/cf-webapp/src/app/observability/llm-export/route.ts b/js/cf-webapp/src/app/observability/llm-export/route.ts new file mode 100644 index 000000000..87497b8e0 --- /dev/null +++ b/js/cf-webapp/src/app/observability/llm-export/route.ts @@ -0,0 +1,171 @@ +import { type NextRequest, NextResponse } from "next/server" +import { getTraceData } from "@/app/observability/lib/get-trace-data" +import { transformToTimelineSections } from "@/app/observability/components/timeline-types" +import { formatTimelineForLLM } from "@/app/observability/components/format-llm-export" + +export async function GET(request: NextRequest) { + const traceId = request.nextUrl.searchParams.get("trace_id")?.trim() + + if (!traceId) { + return new NextResponse("No trace ID provided.", { status: 400, headers: { "Content-Type": "text/plain; charset=utf-8" } }) + } + + const tracePrefix = traceId.substring(0, 33) + const traceData = await getTraceData(tracePrefix) + + if (traceData.rawLlmCalls.length === 0 && traceData.errors.length === 0) { + return new NextResponse(`No data found for trace: ${traceId}`, { status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" } }) + } + + const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = traceData + + const optimizationsOrigin = + (optimizationFeatures?.optimizations_origin as Record< + string, + { source: string; model?: string; call_sequence?: number; parent?: string } + >) || {} + + const candidateExplanations = + (optimizationFeatures?.explanations_post as Record) || {} + + const allCandidates = optimizationFeatures?.optimizations_post + ? Object.entries(optimizationFeatures.optimizations_post as Record).map( + ([id, code]) => ({ + id, + code: typeof code === "string" ? code : "", + source: optimizationsOrigin[id]?.source || "OPTIMIZE", + model: optimizationsOrigin[id]?.model, + callSequence: optimizationsOrigin[id]?.call_sequence, + explanation: candidateExplanations[id], + }), + ) + : [] + + const optimizationCandidates = allCandidates + .filter(c => c.source === "OPTIMIZE") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ ...c, index: index + 1 })) + + const lineProfilerCandidates = allCandidates + .filter(c => c.source === "OPTIMIZE_LP") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ ...c, index: index + 1 })) + + const refinementCandidates = allCandidates + .filter(c => c.source === "REFINE") + .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity)) + .map((c, index) => ({ + ...c, + index: index + 1, + parentId: optimizationsOrigin[c.id]?.parent || null, + })) + + const rankingData = optimizationFeatures?.ranking as + | { ranking?: string[]; explanation?: string } + | null + const bestCandidateId = rankingData?.ranking?.[0] ?? null + + const pullRequestRaw = optimizationFeatures?.pull_request + const usedForPr = Boolean( + pullRequestRaw != null && + typeof pullRequestRaw === "object" && + !Array.isArray(pullRequestRaw) && + Object.keys(pullRequestRaw as Record).length > 0, + ) + + const rankMap: Record = {} + if (rankingData?.ranking) { + rankingData.ranking.forEach((id, index) => { + rankMap[id] = index + 1 + }) + } + + const generatedTests = (optimizationFeatures?.generated_test ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + + const instrumentedTests = (optimizationFeatures?.instrumented_generated_test ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + + const instrumentedPerfTests = (optimizationFeatures?.instrumented_perf_test ?? []).map((code, index) => ({ + code, + index: index + 1, + })) + + const llmCalls = rawLlmCalls.sort((a, b) => { + const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity + const seqB = (b.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity + if (seqA !== seqB) return seqA - seqB + return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + }) + + const transformedCalls = llmCalls.map(call => ({ + id: call.id, + call_type: call.call_type, + model_name: call.model_name, + status: call.status, + latency_ms: call.latency_ms, + llm_cost: call.llm_cost, + total_tokens: call.total_tokens, + created_at: call.created_at, + context: call.context as { call_sequence?: number } | null, + system_prompt: call.system_prompt, + user_prompt: call.user_prompt, + raw_response: call.raw_response, + })) + + const { sections, totalDuration } = transformToTimelineSections({ + calls: transformedCalls, + optimizationCandidates, + lineProfilerCandidates, + refinementCandidates, + generatedTests, + instrumentedTests, + instrumentedPerfTests, + originalCode: optimizationFeatures?.original_code ?? null, + testFramework: optimizationFeatures?.test_framework ?? null, + candidateRankMap: rankMap, + bestCandidateId, + rankingExplanation: rankingData?.explanation ?? null, + usedForPr, + }) + + const metadata = optimizationFeatures?.metadata as Record | undefined + const functionName = + (metadata?.function_to_optimize as string | undefined) ?? + optimizationEvent?.function_name ?? + null + const filePath = optimizationEvent?.file_path ?? null + const originalCode = optimizationFeatures?.original_code ?? null + + const transformedErrors = errors.map(error => ({ + error_type: error.error_type, + severity: error.severity, + error_message: error.error_message, + context: error.context as { + test_name?: string + failure_reason?: string + test_output?: string + expected?: string + actual?: string + } | null, + created_at: error.created_at, + })) + + const formattedText = formatTimelineForLLM({ + traceId, + functionName, + filePath, + originalCode, + sections, + errors: transformedErrors, + totalDuration, + }) + + return new NextResponse(formattedText, { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }) +} diff --git a/js/cf-webapp/src/app/observability/page.tsx b/js/cf-webapp/src/app/observability/page.tsx index 28f8075c6..f737d68a7 100644 --- a/js/cf-webapp/src/app/observability/page.tsx +++ b/js/cf-webapp/src/app/observability/page.tsx @@ -1,7 +1,6 @@ import { Suspense } from "react" -import { unstable_cache } from "next/cache" import { Search } from "lucide-react" -import { prisma } from "@/lib/prisma" +import { getTraceData, type TraceData } from "@/app/observability/lib/get-trace-data" import { TraceSearch } from "@/app/observability/components/trace-search" import { TimelinePageView } from "@/app/observability/components/timeline-page-view" import { transformToTimelineSections } from "@/app/observability/components/timeline-types" @@ -17,36 +16,11 @@ interface Observability2PageProps { }> } -const getTraceData = unstable_cache( - async (tracePrefix: string) => { - const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([ - prisma.llm_calls.findMany({ - where: { trace_id: { startsWith: tracePrefix } }, - orderBy: { created_at: "asc" }, - }), - prisma.optimization_errors.findMany({ - where: { trace_id: { startsWith: tracePrefix } }, - orderBy: { created_at: "asc" }, - }), - prisma.optimization_features.findFirst({ - where: { trace_id: { startsWith: tracePrefix } }, - }), - prisma.optimization_events.findFirst({ - where: { trace_id: { startsWith: tracePrefix } }, - select: { event_type: true, function_name: true, file_path: true }, - }), - ]) - return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } - }, - ["observability-trace-detail"], - { revalidate: 60 }, -) - export default async function Observability2Page({ searchParams }: Observability2PageProps) { const params = await searchParams const traceId = params.trace_id?.trim() - let traceData: Awaited> | null = null + let traceData: TraceData | null = null if (traceId) { const tracePrefix = traceId.substring(0, 33) traceData = await getTraceData(tracePrefix) @@ -79,13 +53,6 @@ export default async function Observability2Page({ searchParams }: Observability ) } -interface TraceData { - rawLlmCalls: Awaited>["rawLlmCalls"] - errors: Awaited>["errors"] - optimizationFeatures: Awaited>["optimizationFeatures"] - optimizationEvent: Awaited>["optimizationEvent"] -} - function TraceContent({ traceId, traceData }: { traceId: string; traceData: TraceData }) { const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = traceData From 629442cc5ec14ccd213e98a5a128127d65503506 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Mon, 9 Feb 2026 09:15:50 -0500 Subject: [PATCH 061/184] Restructure aiservice to language-first architecture (#2383) ## Summary - Reorganizes `django/aiservice/` from feature-first layout (separate `optimizer/`, `testgen/`, `code_repair/` dirs) to language-first layout under `core/languages/{python,js_ts}/` - Adds handler/registry/dispatcher pattern for routing requests to language-specific implementations - All existing module code preserved via `git mv` for history tracking; no logic changes to existing modules ## What changed - New `core/` app with registry, dispatcher, protocols, and error hierarchy - `PythonHandler` and `JSTypeScriptHandler` delegate to existing module functions - All imports updated across the codebase (views, tests, adaptive_optimizer, etc.) - Integration tests for handler registration and dispatch - 155 files changed, ~880 additions / ~207 deletions (mostly import path updates and moves) ## Test plan - [ ] `python manage.py check` passes - [ ] Integration tests in `tests/integration/test_handler_integration.py` pass - [ ] Existing test suite passes with updated import paths - [ ] Ruff and ty clean on all new infrastructure files --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../adaptive_optimizer/adaptive_optimizer.py | 4 +- .../adaptive_optimizer_context.py | 8 +- django/aiservice/aiservice/settings.py | 4 +- django/aiservice/aiservice/test_settings.py | 3 +- django/aiservice/aiservice/urls.py | 16 +-- django/aiservice/code_repair/apps.py | 6 - django/aiservice/core/__init__.py | 21 +++ django/aiservice/core/apps.py | 38 +++++ django/aiservice/core/dispatcher.py | 46 ++++++ django/aiservice/core/errors.py | 50 +++++++ django/aiservice/core/languages/__init__.py | 1 + .../core/languages/js_ts/__init__.py | 12 ++ .../aiservice/core/languages/js_ts/handler.py | 45 ++++++ .../{ => core}/languages/js_ts/optimizer.py | 14 +- .../languages/js_ts/optimizer_lp.py | 6 +- .../prompts/optimizer/async_system_prompt.md | 0 .../prompts/optimizer/async_user_prompt.md | 0 .../js_ts/prompts/optimizer/system_prompt.md | 0 .../js_ts/prompts/optimizer/user_prompt.md | 0 .../testgen/execute_async_system_prompt.md | 0 .../testgen/execute_async_user_prompt.md | 0 .../prompts/testgen/execute_system_prompt.md | 0 .../prompts/testgen/execute_user_prompt.md | 0 .../{ => core}/languages/js_ts/testgen.py | 7 +- .../core/languages/python/__init__.py | 12 ++ .../code_repair/CODE_REPAIR_SYSTEM_PROMPT.md | 0 .../code_repair/CODE_REPAIR_USER_PROMPT.md | 0 .../languages/python}/code_repair/__init__.py | 0 .../python}/code_repair/code_repair.py | 10 +- .../code_repair/code_repair_context.py | 12 +- .../python}/explanations/__init__.py | 0 .../python}/explanations/explanations.py | 0 .../core/languages/python/handler.py | 101 +++++++++++++ .../languages/python}/jit_rewrite/__init__.py | 0 .../python}/jit_rewrite/jit_rewrite.py | 12 +- .../jit_rewrite/jit_rewrite_system_prompt.md | 0 .../jit_rewrite/jit_rewrite_user_prompt.md | 0 .../python}/optimization_review/__init__.py | 0 .../optimization_review.py | 0 .../optimization_review_system_prompt.md | 0 .../prompts/user_prompt_template.md | 0 .../languages/python}/optimizer/__init__.py | 0 .../python}/optimizer/async_system_prompt.md | 0 .../python}/optimizer/async_user_prompt.md | 0 .../python}/optimizer/code_utils/__init__.py | 0 .../languages/python}/optimizer/config.py | 0 .../optimizer/context_utils/__init__.py | 0 .../optimizer/context_utils/constants.py | 0 .../context_utils/context_helpers.py | 0 .../context_utils/optimizer_context.py | 24 ++-- .../context_utils/refiner_context.py | 8 +- .../optimizer/dependency_context_prompt.md | 0 .../optimizer/diff_patches_utils/__init__.py | 0 .../optimizer/diff_patches_utils/diff.py | 0 .../diff_patches_utils/seach_and_replace.py | 10 +- .../optimizer/diff_patches_utils/v4a_diff.py | 2 +- .../optimizer/init_optimization_prompt.md | 0 .../python}/optimizer/jit_instructions.md | 0 .../optimizer/lineprof_context_prompt.md | 0 .../languages/python}/optimizer/models.py | 0 .../optimizer/multi-file-code-format.md | 0 .../languages/python}/optimizer/optimizer.py | 16 +-- .../optimizer/optimizer_line_profiler.py | 20 +-- .../python}/optimizer/postprocess.py | 4 +- .../python}/optimizer/prompts/__init__.py | 0 .../prompts/javascript/async_system_prompt.md | 0 .../prompts/javascript/async_user_prompt.md | 0 .../prompts/javascript/system_prompt.md | 0 .../prompts/javascript/user_prompt.md | 0 .../prompts/python/async_system_prompt.md | 0 .../prompts/python/async_user_prompt.md | 0 .../optimizer/prompts/python/system_prompt.md | 0 .../optimizer/prompts/python/user_prompt.md | 0 .../languages/python}/optimizer/refinement.py | 6 +- .../optimizer/refinement_system_prompt.md | 0 .../optimizer/refinement_user_prompt.md | 0 .../python}/optimizer/system_prompt.md | 0 .../python}/optimizer/user_prompt.md | 0 .../languages/python}/testgen/__init__.py | 0 .../python}/testgen/ast_utils/__init__.py | 0 .../testgen/ast_utils/test_detection.py | 0 .../testgen/execute_async_system_prompt.md | 0 .../testgen/execute_async_user_prompt.md | 0 .../python}/testgen/execute_system_prompt.md | 0 .../python}/testgen/execute_user_prompt.md | 0 .../python}/testgen/explain_system_prompt.md | 0 .../python}/testgen/explain_user_prompt.md | 0 .../testgen/instrumentation/__init__.py | 0 .../instrumentation/edit_generated_test.py | 0 .../instrumentation/instrument_new_tests.py | 4 +- .../python}/testgen/jit_system_prompt.md | 0 .../languages/python}/testgen/models.py | 0 .../testgen/postprocessing/__init__.py | 0 .../postprocessing/add_missing_imports.py | 0 .../testgen/postprocessing/code_validator.py | 10 +- .../postprocessing/postprocess_pipeline.py | 18 +-- .../testgen/postprocessing/range_modifier.py | 0 .../remove_unused_definitions.py | 2 +- .../removeassert_transformer.py | 0 .../testgen/postprocessing/tensor_limit.py | 0 .../postprocessing/topdef_terminator.py | 0 .../python}/testgen/preprocessing/__init__.py | 0 .../dataclass_constructor_notes.py | 0 .../preprocessing/preprocess_pipeline.py | 4 +- .../preprocessing/torch_tensor_limit.py | 0 .../javascript/execute_async_system_prompt.md | 0 .../javascript/execute_async_user_prompt.md | 0 .../javascript/execute_system_prompt.md | 0 .../prompts/javascript/execute_user_prompt.md | 0 .../languages/python}/testgen/testgen.py | 22 +-- .../python}/testgen/testgen_context.py | 4 +- django/aiservice/core/pipeline.py | 32 +++++ django/aiservice/core/protocols/__init__.py | 8 ++ django/aiservice/core/protocols/base.py | 21 +++ .../aiservice/core/protocols/code_repair.py | 11 ++ django/aiservice/core/protocols/optimizer.py | 11 ++ django/aiservice/core/protocols/testgen.py | 11 ++ django/aiservice/core/registry.py | 54 +++++++ django/aiservice/explanations/apps.py | 6 - django/aiservice/jit_rewrite/apps.py | 6 - django/aiservice/languages/__init__.py | 8 -- django/aiservice/languages/js_ts/__init__.py | 13 -- django/aiservice/mypy_allowlist.txt | 18 +-- django/aiservice/optimization_review/apps.py | 6 - django/aiservice/optimizer/apps.py | 6 - django/aiservice/pyproject.toml | 2 +- django/aiservice/testgen/apps.py | 6 - .../aiservice/tests/integration/__init__.py | 1 + .../integration/test_handler_integration.py | 136 ++++++++++++++++++ .../tests/optimizer/test_code_repair.py | 2 +- .../tests/optimizer/test_comment_cleaner.py | 2 +- .../aiservice/tests/optimizer/test_context.py | 8 +- .../optimizer/test_docstring_replacement.py | 6 +- .../optimizer/test_javascript_prompts.py | 2 +- .../tests/optimizer/test_javascript_schema.py | 2 +- .../optimizer/test_javascript_testgen.py | 6 +- .../tests/optimizer/test_optimizer.py | 4 +- .../optimizer/test_optimizer_v4a_differ.py | 10 +- .../aiservice/tests/optimizer/test_pathes.py | 2 +- .../test_dataclass_constructor_notes.py | 2 +- .../tests/testgen/test_ellipsis_in_ast.py | 2 +- .../test_parse_and_validate_llm_output.py | 2 +- .../tests/testgen/test_perf_injector.py | 6 +- .../testgen/test_tensor_limit_preprocess.py | 8 +- .../tests/testgen/test_testgen_javascript.py | 2 +- .../test_edit_generated_test.py | 2 +- .../test_instrument_generated_tests.py | 9 +- ..._instrument_test_source_used_frameworks.py | 4 +- .../test_add_missing_imports.py | 5 +- .../test_async_support.py | 2 +- .../test_delete_top_def_nodes.py | 2 +- .../test_modify_large_loops.py | 2 +- .../test_remove_asserts.py | 2 +- .../test_remove_unused_definitions.py | 4 +- .../test_tensor_limit_postprocess.py | 2 +- .../test_validate_code.py | 2 +- .../test_validate_pipeline.py | 6 +- 157 files changed, 827 insertions(+), 219 deletions(-) delete mode 100644 django/aiservice/code_repair/apps.py create mode 100644 django/aiservice/core/__init__.py create mode 100644 django/aiservice/core/apps.py create mode 100644 django/aiservice/core/dispatcher.py create mode 100644 django/aiservice/core/errors.py create mode 100644 django/aiservice/core/languages/__init__.py create mode 100644 django/aiservice/core/languages/js_ts/__init__.py create mode 100644 django/aiservice/core/languages/js_ts/handler.py rename django/aiservice/{ => core}/languages/js_ts/optimizer.py (98%) rename django/aiservice/{ => core}/languages/js_ts/optimizer_lp.py (98%) rename django/aiservice/{ => core}/languages/js_ts/prompts/optimizer/async_system_prompt.md (100%) rename django/aiservice/{ => core}/languages/js_ts/prompts/optimizer/async_user_prompt.md (100%) rename django/aiservice/{ => core}/languages/js_ts/prompts/optimizer/system_prompt.md (100%) rename django/aiservice/{ => core}/languages/js_ts/prompts/optimizer/user_prompt.md (100%) rename django/aiservice/{ => core}/languages/js_ts/prompts/testgen/execute_async_system_prompt.md (100%) rename django/aiservice/{ => core}/languages/js_ts/prompts/testgen/execute_async_user_prompt.md (100%) rename django/aiservice/{ => core}/languages/js_ts/prompts/testgen/execute_system_prompt.md (100%) rename django/aiservice/{ => core}/languages/js_ts/prompts/testgen/execute_user_prompt.md (100%) rename django/aiservice/{ => core}/languages/js_ts/testgen.py (99%) create mode 100644 django/aiservice/core/languages/python/__init__.py rename django/aiservice/{ => core/languages/python}/code_repair/CODE_REPAIR_SYSTEM_PROMPT.md (100%) rename django/aiservice/{ => core/languages/python}/code_repair/CODE_REPAIR_USER_PROMPT.md (100%) rename django/aiservice/{ => core/languages/python}/code_repair/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/code_repair/code_repair.py (95%) rename django/aiservice/{ => core/languages/python}/code_repair/code_repair_context.py (93%) rename django/aiservice/{ => core/languages/python}/explanations/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/explanations/explanations.py (100%) create mode 100644 django/aiservice/core/languages/python/handler.py rename django/aiservice/{ => core/languages/python}/jit_rewrite/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/jit_rewrite/jit_rewrite.py (97%) rename django/aiservice/{ => core/languages/python}/jit_rewrite/jit_rewrite_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/jit_rewrite/jit_rewrite_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimization_review/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/optimization_review/optimization_review.py (100%) rename django/aiservice/{ => core/languages/python}/optimization_review/prompts/optimization_review_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimization_review/prompts/user_prompt_template.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/async_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/async_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/code_utils/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/config.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/context_utils/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/context_utils/constants.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/context_utils/context_helpers.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/context_utils/optimizer_context.py (95%) rename django/aiservice/{ => core/languages/python}/optimizer/context_utils/refiner_context.py (96%) rename django/aiservice/{ => core/languages/python}/optimizer/dependency_context_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/diff_patches_utils/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/diff_patches_utils/diff.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/diff_patches_utils/seach_and_replace.py (95%) rename django/aiservice/{ => core/languages/python}/optimizer/diff_patches_utils/v4a_diff.py (99%) rename django/aiservice/{ => core/languages/python}/optimizer/init_optimization_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/jit_instructions.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/lineprof_context_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/models.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/multi-file-code-format.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/optimizer.py (98%) rename django/aiservice/{ => core/languages/python}/optimizer/optimizer_line_profiler.py (96%) rename django/aiservice/{ => core/languages/python}/optimizer/postprocess.py (99%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/javascript/async_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/javascript/async_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/javascript/system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/javascript/user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/python/async_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/python/async_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/python/system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/prompts/python/user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/refinement.py (97%) rename django/aiservice/{ => core/languages/python}/optimizer/refinement_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/refinement_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/optimizer/user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/ast_utils/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/ast_utils/test_detection.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/execute_async_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/execute_async_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/execute_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/execute_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/explain_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/explain_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/instrumentation/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/instrumentation/edit_generated_test.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/instrumentation/instrument_new_tests.py (99%) rename django/aiservice/{ => core/languages/python}/testgen/jit_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/models.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/add_missing_imports.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/code_validator.py (97%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/postprocess_pipeline.py (69%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/range_modifier.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/remove_unused_definitions.py (99%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/removeassert_transformer.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/tensor_limit.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/postprocessing/topdef_terminator.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/preprocessing/__init__.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/preprocessing/dataclass_constructor_notes.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/preprocessing/preprocess_pipeline.py (80%) rename django/aiservice/{ => core/languages/python}/testgen/preprocessing/torch_tensor_limit.py (100%) rename django/aiservice/{ => core/languages/python}/testgen/prompts/javascript/execute_async_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/prompts/javascript/execute_async_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/prompts/javascript/execute_system_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/prompts/javascript/execute_user_prompt.md (100%) rename django/aiservice/{ => core/languages/python}/testgen/testgen.py (98%) rename django/aiservice/{ => core/languages/python}/testgen/testgen_context.py (94%) create mode 100644 django/aiservice/core/pipeline.py create mode 100644 django/aiservice/core/protocols/__init__.py create mode 100644 django/aiservice/core/protocols/base.py create mode 100644 django/aiservice/core/protocols/code_repair.py create mode 100644 django/aiservice/core/protocols/optimizer.py create mode 100644 django/aiservice/core/protocols/testgen.py create mode 100644 django/aiservice/core/registry.py delete mode 100644 django/aiservice/explanations/apps.py delete mode 100644 django/aiservice/jit_rewrite/apps.py delete mode 100644 django/aiservice/languages/__init__.py delete mode 100644 django/aiservice/languages/js_ts/__init__.py delete mode 100644 django/aiservice/optimization_review/apps.py delete mode 100644 django/aiservice/optimizer/apps.py delete mode 100644 django/aiservice/testgen/apps.py create mode 100644 django/aiservice/tests/integration/__init__.py create mode 100644 django/aiservice/tests/integration/test_handler_integration.py diff --git a/django/aiservice/adaptive_optimizer/adaptive_optimizer.py b/django/aiservice/adaptive_optimizer/adaptive_optimizer.py index 4cfce08a6..8c2882c6d 100644 --- a/django/aiservice/adaptive_optimizer/adaptive_optimizer.py +++ b/django/aiservice/adaptive_optimizer/adaptive_optimizer.py @@ -18,8 +18,8 @@ from aiservice.llm import ADAPTIVE_OPTIMIZE_MODEL, calculate_llm_cost, call_llm from authapp.auth import AuthenticatedRequest from log_features.log_event import update_optimization_cost from log_features.log_features import log_features -from optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema -from optimizer.models import OptimizedCandidateSource +from core.languages.python.optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema +from core.languages.python.optimizer.models import OptimizedCandidateSource from .adaptive_optimizer_context import AdaptiveOptContext, AdaptiveOptContextData, AdaptiveOptRequestSchema diff --git a/django/aiservice/adaptive_optimizer/adaptive_optimizer_context.py b/django/aiservice/adaptive_optimizer/adaptive_optimizer_context.py index 9b8c133ef..32291e329 100644 --- a/django/aiservice/adaptive_optimizer/adaptive_optimizer_context.py +++ b/django/aiservice/adaptive_optimizer/adaptive_optimizer_context.py @@ -3,12 +3,12 @@ from typing import TYPE_CHECKING from ninja import Schema -from optimizer.context_utils.optimizer_context import CodeStrAndExplanation, MultiOptimizerContext -from optimizer.diff_patches_utils.diff import DiffMethod -from optimizer.models import OptimizedCandidateSource +from core.languages.python.optimizer.context_utils.optimizer_context import CodeStrAndExplanation, MultiOptimizerContext +from core.languages.python.optimizer.diff_patches_utils.diff import DiffMethod +from core.languages.python.optimizer.models import OptimizedCandidateSource if TYPE_CHECKING: - from optimizer.context_utils.optimizer_context import CodeStrAndExplanation + from core.languages.python.optimizer.context_utils.optimizer_context import CodeStrAndExplanation class AdaptiveOptimizedCandidate(Schema): diff --git a/django/aiservice/aiservice/settings.py b/django/aiservice/aiservice/settings.py index 198a51fa5..89fee4d7b 100644 --- a/django/aiservice/aiservice/settings.py +++ b/django/aiservice/aiservice/settings.py @@ -50,14 +50,12 @@ ALLOWED_HOSTS: list[str] = [ # Application definition INSTALLED_APPS: list[str] = [ + "core.apps.CoreConfig", "authapp.apps.AuthAppConfig", - "testgen.apps.TestgenConfig", - "optimizer.apps.OptimizerConfig", "django.contrib.contenttypes", "django.contrib.staticfiles", "log_features.apps.LoggingConfig", "aiservice.apps.AiServiceConfig", # Add the app containing the management commands - "jit_rewrite.apps.JitRewriteConfig", ] MIDDLEWARE: list[str] = [ diff --git a/django/aiservice/aiservice/test_settings.py b/django/aiservice/aiservice/test_settings.py index 6fbf37c31..8b9f70edd 100644 --- a/django/aiservice/aiservice/test_settings.py +++ b/django/aiservice/aiservice/test_settings.py @@ -10,8 +10,7 @@ else: INSTALLED_APPS = [ "authapp.apps.AuthAppConfig", - "testgen.apps.TestgenConfig", - "optimizer.apps.OptimizerConfig", + "core.apps.CoreConfig", "django.contrib.contenttypes", "django.contrib.staticfiles", "tests", diff --git a/django/aiservice/aiservice/urls.py b/django/aiservice/aiservice/urls.py index 010d3237f..3b69d80cb 100644 --- a/django/aiservice/aiservice/urls.py +++ b/django/aiservice/aiservice/urls.py @@ -21,16 +21,16 @@ Including another URLconf from django.urls import path from adaptive_optimizer.adaptive_optimizer import adaptive_optimize_api -from code_repair.code_repair import code_repair_api -from explanations.explanations import explanations_api -from jit_rewrite.jit_rewrite import jit_rewrite_api +from core.languages.python.code_repair.code_repair import code_repair_api +from core.languages.python.explanations.explanations import explanations_api +from core.languages.python.jit_rewrite.jit_rewrite import jit_rewrite_api from log_features.log_features import features_api -from optimization_review.optimization_review import optimization_review_api -from optimizer.optimizer import optimize_api -from optimizer.optimizer_line_profiler import optimize_line_profiler_api -from optimizer.refinement import refinement_api +from core.languages.python.optimization_review.optimization_review import optimization_review_api +from core.languages.python.optimizer.optimizer import optimize_api +from core.languages.python.optimizer.optimizer_line_profiler import optimize_line_profiler_api +from core.languages.python.optimizer.refinement import refinement_api from ranker.ranker import ranker_api -from testgen.testgen import testgen_api +from core.languages.python.testgen.testgen import testgen_api from workflow_gen.workflow_gen import workflow_gen_api urlpatterns = [ diff --git a/django/aiservice/code_repair/apps.py b/django/aiservice/code_repair/apps.py deleted file mode 100644 index 2e2619b2c..000000000 --- a/django/aiservice/code_repair/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class CodeRepairConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "code_repair" diff --git a/django/aiservice/core/__init__.py b/django/aiservice/core/__init__.py new file mode 100644 index 000000000..5e9b3a8b2 --- /dev/null +++ b/django/aiservice/core/__init__.py @@ -0,0 +1,21 @@ +"""Core infrastructure for multi-language support.""" + +from .errors import HandlerError, HandlerNotImplementedError, LanguageNotFoundError, PipelineError, ValidationError +from .pipeline import PipelineContext +from .protocols import CodeRepairProtocol, LanguageHandler, OptimizerProtocol, TestGenProtocol +from .registry import register_handler, registry + +__all__ = [ + "CodeRepairProtocol", + "HandlerError", + "HandlerNotImplementedError", + "LanguageHandler", + "LanguageNotFoundError", + "OptimizerProtocol", + "PipelineContext", + "PipelineError", + "TestGenProtocol", + "ValidationError", + "register_handler", + "registry", +] diff --git a/django/aiservice/core/apps.py b/django/aiservice/core/apps.py new file mode 100644 index 000000000..70f0503d9 --- /dev/null +++ b/django/aiservice/core/apps.py @@ -0,0 +1,38 @@ +"""Core infrastructure Django app configuration.""" + +import importlib +import logging + +from django.apps import AppConfig + +logger = logging.getLogger(__name__) + + +class CoreConfig(AppConfig): + """Django app configuration for the core infrastructure module. + + This app must be loaded before any feature apps (testgen, optimizer, code_repair) + to ensure the handler registry is initialized and protocols are available. + """ + + default_auto_field = "django.db.models.BigAutoField" + name = "core" + verbose_name = "Core Infrastructure" + + def ready(self) -> None: + """Initialize core infrastructure when Django starts.""" + logger.info("Core app initializing...") + + # Import language handlers from core.languages + # This ensures the handlers get registered via their __init__.py + for module_name, label in [ + ("core.languages.python", "Python"), + ("core.languages.js_ts", "JavaScript/TypeScript"), + ]: + try: + importlib.import_module(module_name) + logger.info("%s language handler imported and registered", label) + except ImportError: + logger.warning("Could not import %s language handler", label, exc_info=True) + + logger.info("Core app initialization complete") diff --git a/django/aiservice/core/dispatcher.py b/django/aiservice/core/dispatcher.py new file mode 100644 index 000000000..7137960e7 --- /dev/null +++ b/django/aiservice/core/dispatcher.py @@ -0,0 +1,46 @@ +"""Dispatcher module for language handler access.""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +from core.errors import HandlerNotImplementedError +from core.registry import registry + +if TYPE_CHECKING: + from core.protocols import LanguageHandler + + +class Feature(Enum): + """Supported language handler features and their capability flags.""" + + TESTGEN = "supports_testgen" + OPTIMIZER = "supports_optimizer" + CODE_REPAIR = "supports_code_repair" + JIT_REWRITE = "supports_jit_rewrite" + OPTIMIZATION_REVIEW = "supports_optimization_review" + EXPLANATIONS = "supports_explanations" + + +def get_handler_for_language(language_id: str) -> LanguageHandler: + """Get a handler instance for a specific language.""" + handler_class = registry.get_handler(language_id) + return handler_class() + + +def get_handler_for_feature(language_id: str, feature: str) -> LanguageHandler: + """Get a handler instance for a specific language and feature combination.""" + handler_class = registry.get_handler(language_id) + + try: + feat = Feature[feature.upper()] + except KeyError: + valid = [f.name.lower() for f in Feature] + msg = f"Unknown feature: {feature}. Valid features: {valid}" + raise ValueError(msg) from None + + if not getattr(handler_class, feat.value, False): + raise HandlerNotImplementedError(language_id=language_id, feature=feature, capability=feat.value) + + return handler_class() diff --git a/django/aiservice/core/errors.py b/django/aiservice/core/errors.py new file mode 100644 index 000000000..cc074f907 --- /dev/null +++ b/django/aiservice/core/errors.py @@ -0,0 +1,50 @@ +"""Error hierarchy for language handler system.""" + +from __future__ import annotations + + +class HandlerError(Exception): + """Base error for handler system.""" + + def __init__(self, message: str, context: dict[str, object] | None = None) -> None: + super().__init__(message) + self.context = context or {} + + +class LanguageNotFoundError(HandlerError): + """Raised when no handler is registered for the requested language.""" + + def __init__(self, language_id: str, available: list[str]) -> None: + available_str = ", ".join(available) if available else "none" + message = f"Language handler for '{language_id}' not found. Available: {available_str}" + super().__init__(message, {"language_id": language_id, "available": available}) + + +class HandlerNotImplementedError(HandlerError): + """Raised when a handler doesn't support the requested feature.""" + + def __init__(self, language_id: str, feature: str, capability: str) -> None: + message = ( + f"Handler for '{language_id}' does not support {feature} capability '{capability}'. " + f"Check handler.capabilities['{feature}']." + ) + super().__init__(message, {"language_id": language_id, "feature": feature, "capability": capability}) + + +class PipelineError(HandlerError): + """Raised when a pipeline stage fails.""" + + def __init__(self, stage: str, message: str, cause: Exception | None = None) -> None: + full_message = f"Pipeline error in {stage}: {message}" + if cause: + full_message += f" (caused by {type(cause).__name__}: {cause!s})" + super().__init__(full_message, {"stage": stage, "cause": cause}) + self.__cause__ = cause + + +class ValidationError(HandlerError): + """Raised when input validation fails.""" + + def __init__(self, field: str, message: str, value: object = None) -> None: + full_message = f"Validation failed for '{field}': {message}" + super().__init__(full_message, {"field": field, "value": value}) diff --git a/django/aiservice/core/languages/__init__.py b/django/aiservice/core/languages/__init__.py new file mode 100644 index 000000000..f80bb1704 --- /dev/null +++ b/django/aiservice/core/languages/__init__.py @@ -0,0 +1 @@ +"""Language modules for aiservice.""" diff --git a/django/aiservice/core/languages/js_ts/__init__.py b/django/aiservice/core/languages/js_ts/__init__.py new file mode 100644 index 000000000..a9b855cdc --- /dev/null +++ b/django/aiservice/core/languages/js_ts/__init__.py @@ -0,0 +1,12 @@ +"""JavaScript/TypeScript language module.""" + +import contextlib + +from core.registry import registry + +from .handler import JSTypeScriptHandler + +with contextlib.suppress(ValueError): + registry.register("js_ts", JSTypeScriptHandler) + +__all__ = ["JSTypeScriptHandler"] diff --git a/django/aiservice/core/languages/js_ts/handler.py b/django/aiservice/core/languages/js_ts/handler.py new file mode 100644 index 000000000..c4793025c --- /dev/null +++ b/django/aiservice/core/languages/js_ts/handler.py @@ -0,0 +1,45 @@ +"""JavaScript/TypeScript language handler implementation. + +Thin delegation layer that routes requests to the actual module implementations. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from core.languages.js_ts.optimizer import optimize_javascript +from core.languages.js_ts.testgen import testgen_javascript + +if TYPE_CHECKING: + from authapp.auth import AuthenticatedRequest + from core.languages.python.optimizer.context_utils.optimizer_context import ( + OptimizeErrorResponseSchema, + OptimizeResponseSchema, + ) + from core.languages.python.optimizer.models import OptimizeSchema + from core.languages.python.testgen.models import TestGenErrorResponseSchema, TestGenResponseSchema, TestGenSchema + + +class JSTypeScriptHandler: + """JavaScript/TypeScript language handler.""" + + language = "js_ts" + + supports_testgen = True + supports_optimizer = True + supports_code_repair = False + supports_jit_rewrite = False + supports_optimization_review = False + supports_explanations = False + + async def testgen_generate( + self, request: AuthenticatedRequest, data: TestGenSchema + ) -> tuple[int, TestGenResponseSchema | TestGenErrorResponseSchema]: + """Generate tests for JavaScript/TypeScript code.""" + return await testgen_javascript(request, data) + + async def optimizer_optimize( + self, request: AuthenticatedRequest, data: OptimizeSchema + ) -> tuple[int, OptimizeResponseSchema | OptimizeErrorResponseSchema]: + """Optimize JavaScript/TypeScript code for performance.""" + return await optimize_javascript(request, data) diff --git a/django/aiservice/languages/js_ts/optimizer.py b/django/aiservice/core/languages/js_ts/optimizer.py similarity index 98% rename from django/aiservice/languages/js_ts/optimizer.py rename to django/aiservice/core/languages/js_ts/optimizer.py index fe2da0aa2..5cfc2e696 100644 --- a/django/aiservice/languages/js_ts/optimizer.py +++ b/django/aiservice/core/languages/js_ts/optimizer.py @@ -22,22 +22,22 @@ from aiservice.llm import LLM, OPTIMIZE_MODEL, calculate_llm_cost, call_llm from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax from authapp.auth import AuthenticatedRequest from authapp.user import get_user_by_id -from log_features.log_event import get_or_create_optimization_event -from log_features.log_features import log_features -from optimizer.config import MAX_OPTIMIZER_CALLS, get_model_distribution -from optimizer.context_utils.context_helpers import ( +from core.languages.python.optimizer.config import MAX_OPTIMIZER_CALLS, get_model_distribution +from core.languages.python.optimizer.context_utils.context_helpers import ( group_code, is_multi_context_js, is_multi_context_ts, split_markdown_code, ) -from optimizer.context_utils.optimizer_context import ( +from core.languages.python.optimizer.context_utils.optimizer_context import ( OptimizeErrorResponseSchema, OptimizeResponseItemSchema, OptimizeResponseSchema, ) -from optimizer.models import OptimizedCandidateSource, OptimizeSchema -from optimizer.prompts import get_system_prompt, get_user_prompt +from core.languages.python.optimizer.models import OptimizedCandidateSource, OptimizeSchema +from core.languages.python.optimizer.prompts import get_system_prompt, get_user_prompt +from log_features.log_event import get_or_create_optimization_event +from log_features.log_features import log_features if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/languages/js_ts/optimizer_lp.py b/django/aiservice/core/languages/js_ts/optimizer_lp.py similarity index 98% rename from django/aiservice/languages/js_ts/optimizer_lp.py rename to django/aiservice/core/languages/js_ts/optimizer_lp.py index 100cc60fe..4c2fd5149 100644 --- a/django/aiservice/languages/js_ts/optimizer_lp.py +++ b/django/aiservice/core/languages/js_ts/optimizer_lp.py @@ -19,14 +19,14 @@ from aiservice.analytics.posthog import ph from aiservice.env_specific import debug_log_sensitive_data from aiservice.llm import OPTIMIZE_MODEL, calculate_llm_cost, call_llm from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax -from optimizer.config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution -from optimizer.context_utils.context_helpers import ( +from core.languages.python.optimizer.config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution +from core.languages.python.optimizer.context_utils.context_helpers import ( group_code, is_multi_context_js, is_multi_context_ts, split_markdown_code, ) -from optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema +from core.languages.python.optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/languages/js_ts/prompts/optimizer/async_system_prompt.md b/django/aiservice/core/languages/js_ts/prompts/optimizer/async_system_prompt.md similarity index 100% rename from django/aiservice/languages/js_ts/prompts/optimizer/async_system_prompt.md rename to django/aiservice/core/languages/js_ts/prompts/optimizer/async_system_prompt.md diff --git a/django/aiservice/languages/js_ts/prompts/optimizer/async_user_prompt.md b/django/aiservice/core/languages/js_ts/prompts/optimizer/async_user_prompt.md similarity index 100% rename from django/aiservice/languages/js_ts/prompts/optimizer/async_user_prompt.md rename to django/aiservice/core/languages/js_ts/prompts/optimizer/async_user_prompt.md diff --git a/django/aiservice/languages/js_ts/prompts/optimizer/system_prompt.md b/django/aiservice/core/languages/js_ts/prompts/optimizer/system_prompt.md similarity index 100% rename from django/aiservice/languages/js_ts/prompts/optimizer/system_prompt.md rename to django/aiservice/core/languages/js_ts/prompts/optimizer/system_prompt.md diff --git a/django/aiservice/languages/js_ts/prompts/optimizer/user_prompt.md b/django/aiservice/core/languages/js_ts/prompts/optimizer/user_prompt.md similarity index 100% rename from django/aiservice/languages/js_ts/prompts/optimizer/user_prompt.md rename to django/aiservice/core/languages/js_ts/prompts/optimizer/user_prompt.md diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md b/django/aiservice/core/languages/js_ts/prompts/testgen/execute_async_system_prompt.md similarity index 100% rename from django/aiservice/languages/js_ts/prompts/testgen/execute_async_system_prompt.md rename to django/aiservice/core/languages/js_ts/prompts/testgen/execute_async_system_prompt.md diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md b/django/aiservice/core/languages/js_ts/prompts/testgen/execute_async_user_prompt.md similarity index 100% rename from django/aiservice/languages/js_ts/prompts/testgen/execute_async_user_prompt.md rename to django/aiservice/core/languages/js_ts/prompts/testgen/execute_async_user_prompt.md diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md b/django/aiservice/core/languages/js_ts/prompts/testgen/execute_system_prompt.md similarity index 100% rename from django/aiservice/languages/js_ts/prompts/testgen/execute_system_prompt.md rename to django/aiservice/core/languages/js_ts/prompts/testgen/execute_system_prompt.md diff --git a/django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md b/django/aiservice/core/languages/js_ts/prompts/testgen/execute_user_prompt.md similarity index 100% rename from django/aiservice/languages/js_ts/prompts/testgen/execute_user_prompt.md rename to django/aiservice/core/languages/js_ts/prompts/testgen/execute_user_prompt.md diff --git a/django/aiservice/languages/js_ts/testgen.py b/django/aiservice/core/languages/js_ts/testgen.py similarity index 99% rename from django/aiservice/languages/js_ts/testgen.py rename to django/aiservice/core/languages/js_ts/testgen.py index 5390eb660..0a6a7ec45 100644 --- a/django/aiservice/languages/js_ts/testgen.py +++ b/django/aiservice/core/languages/js_ts/testgen.py @@ -23,9 +23,14 @@ from aiservice.env_specific import debug_log_sensitive_data from aiservice.llm import EXECUTE_MODEL, HAIKU_MODEL, OPENAI_MODEL, calculate_llm_cost, call_llm from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax from authapp.auth import AuthenticatedRequest +from core.languages.python.testgen.models import ( + TestGenerationFailedError, + TestGenErrorResponseSchema, + TestGenResponseSchema, + TestGenSchema, +) from log_features.log_event import update_optimization_cost from log_features.log_features import log_features -from testgen.models import TestGenerationFailedError, TestGenErrorResponseSchema, TestGenResponseSchema, TestGenSchema if TYPE_CHECKING: from aiservice.llm import LLM diff --git a/django/aiservice/core/languages/python/__init__.py b/django/aiservice/core/languages/python/__init__.py new file mode 100644 index 000000000..87bd268c8 --- /dev/null +++ b/django/aiservice/core/languages/python/__init__.py @@ -0,0 +1,12 @@ +"""Python language handler registration.""" + +import contextlib + +from core.registry import registry + +from .handler import PythonHandler + +with contextlib.suppress(ValueError): + registry.register("python", PythonHandler) + +__all__ = ["PythonHandler"] diff --git a/django/aiservice/code_repair/CODE_REPAIR_SYSTEM_PROMPT.md b/django/aiservice/core/languages/python/code_repair/CODE_REPAIR_SYSTEM_PROMPT.md similarity index 100% rename from django/aiservice/code_repair/CODE_REPAIR_SYSTEM_PROMPT.md rename to django/aiservice/core/languages/python/code_repair/CODE_REPAIR_SYSTEM_PROMPT.md diff --git a/django/aiservice/code_repair/CODE_REPAIR_USER_PROMPT.md b/django/aiservice/core/languages/python/code_repair/CODE_REPAIR_USER_PROMPT.md similarity index 100% rename from django/aiservice/code_repair/CODE_REPAIR_USER_PROMPT.md rename to django/aiservice/core/languages/python/code_repair/CODE_REPAIR_USER_PROMPT.md diff --git a/django/aiservice/code_repair/__init__.py b/django/aiservice/core/languages/python/code_repair/__init__.py similarity index 100% rename from django/aiservice/code_repair/__init__.py rename to django/aiservice/core/languages/python/code_repair/__init__.py diff --git a/django/aiservice/code_repair/code_repair.py b/django/aiservice/core/languages/python/code_repair/code_repair.py similarity index 95% rename from django/aiservice/code_repair/code_repair.py rename to django/aiservice/core/languages/python/code_repair/code_repair.py index 944a2137e..023599a75 100644 --- a/django/aiservice/code_repair/code_repair.py +++ b/django/aiservice/core/languages/python/code_repair/code_repair.py @@ -17,11 +17,15 @@ from aiservice.common_utils import validate_trace_id from aiservice.env_specific import debug_log_sensitive_data from aiservice.llm import CODE_REPAIR_MODEL, calculate_llm_cost, call_llm from authapp.auth import AuthenticatedRequest -from code_repair.code_repair_context import CodeRepairContext, CodeRepairContextData, CodeRepairRequestSchema +from core.languages.python.code_repair.code_repair_context import ( + CodeRepairContext, + CodeRepairContextData, + CodeRepairRequestSchema, +) +from core.languages.python.optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema +from core.languages.python.optimizer.models import OptimizedCandidateSource from log_features.log_event import update_optimization_cost from log_features.log_features import log_features -from optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema -from optimizer.models import OptimizedCandidateSource if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/code_repair/code_repair_context.py b/django/aiservice/core/languages/python/code_repair/code_repair_context.py similarity index 93% rename from django/aiservice/code_repair/code_repair_context.py rename to django/aiservice/core/languages/python/code_repair/code_repair_context.py index 1936f9b8c..91edf5524 100644 --- a/django/aiservice/code_repair/code_repair_context.py +++ b/django/aiservice/core/languages/python/code_repair/code_repair_context.py @@ -10,12 +10,16 @@ from ninja import Field, Schema from pydantic import ValidationError from aiservice.common.cst_utils import parse_module_to_cst -from optimizer.context_utils.context_helpers import group_code, is_markdown_structure_changed, split_markdown_code -from optimizer.diff_patches_utils.seach_and_replace import SearchAndReplaceDiff -from optimizer.models import CodeAndExplanation +from core.languages.python.optimizer.context_utils.context_helpers import ( + group_code, + is_markdown_structure_changed, + split_markdown_code, +) +from core.languages.python.optimizer.diff_patches_utils.seach_and_replace import SearchAndReplaceDiff +from core.languages.python.optimizer.models import CodeAndExplanation if TYPE_CHECKING: - from optimizer.diff_patches_utils.diff import Diff + from core.languages.python.optimizer.diff_patches_utils.diff import Diff class TestDiffScope(str, Enum): diff --git a/django/aiservice/explanations/__init__.py b/django/aiservice/core/languages/python/explanations/__init__.py similarity index 100% rename from django/aiservice/explanations/__init__.py rename to django/aiservice/core/languages/python/explanations/__init__.py diff --git a/django/aiservice/explanations/explanations.py b/django/aiservice/core/languages/python/explanations/explanations.py similarity index 100% rename from django/aiservice/explanations/explanations.py rename to django/aiservice/core/languages/python/explanations/explanations.py diff --git a/django/aiservice/core/languages/python/handler.py b/django/aiservice/core/languages/python/handler.py new file mode 100644 index 000000000..9b11ec9ea --- /dev/null +++ b/django/aiservice/core/languages/python/handler.py @@ -0,0 +1,101 @@ +"""Python language handler implementation. + +Thin delegation layer that routes requests to the actual module implementations. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from core.languages.python.code_repair.code_repair import code_repair +from core.languages.python.explanations.explanations import explain_optimizations +from core.languages.python.jit_rewrite.jit_rewrite import jit_rewrite +from core.languages.python.optimization_review.optimization_review import get_optimization_review +from core.languages.python.optimizer.optimizer import optimize_python +from core.languages.python.testgen.testgen import testgen_python + +if TYPE_CHECKING: + from aiservice.llm import LLM + from authapp.auth import AuthenticatedRequest + from core.languages.python.code_repair.code_repair import ( + CodeRepairErrorResponseSchema, + CodeRepairIntermediateResponseItemschema, + ) + from core.languages.python.code_repair.code_repair_context import CodeRepairContext + from core.languages.python.explanations.explanations import ( + ExplanationsErrorResponseSchema, + ExplanationsResponseSchema, + ExplanationsSchema, + ) + from core.languages.python.optimization_review.optimization_review import ( + OptimizationReviewErrorSchema, + OptimizationReviewResponseSchema, + OptimizationReviewSchema, + ) + from core.languages.python.optimizer.context_utils.optimizer_context import ( + OptimizeErrorResponseSchema, + OptimizeResponseSchema, + ) + from core.languages.python.optimizer.models import JitRewriteOptimizeSchema, OptimizeSchema + from core.languages.python.testgen.models import TestGenErrorResponseSchema, TestGenResponseSchema, TestGenSchema + + +class PythonHandler: + """Python language handler.""" + + language = "python" + + supports_testgen = True + supports_optimizer = True + supports_code_repair = True + supports_jit_rewrite = True + supports_optimization_review = True + supports_explanations = True + + async def testgen_generate( + self, request: AuthenticatedRequest, data: TestGenSchema + ) -> tuple[int, TestGenResponseSchema | TestGenErrorResponseSchema]: + """Generate tests for Python code.""" + return await testgen_python(request, data) + + async def optimizer_optimize( + self, request: AuthenticatedRequest, data: OptimizeSchema + ) -> tuple[int, OptimizeResponseSchema | OptimizeErrorResponseSchema]: + """Optimize Python code for performance.""" + return await optimize_python(request, data) + + async def code_repair_repair( + self, user_id: str, optimization_id: str, ctx: CodeRepairContext, optimize_model: LLM | None = None + ) -> CodeRepairIntermediateResponseItemschema | CodeRepairErrorResponseSchema: + """Repair Python code based on error messages.""" + kwargs = {} + if optimize_model is not None: + kwargs["optimize_model"] = optimize_model + return await code_repair(user_id, optimization_id, ctx, **kwargs) + + async def jit_rewrite( + self, request: AuthenticatedRequest, data: JitRewriteOptimizeSchema + ) -> tuple[int, OptimizeResponseSchema | OptimizeErrorResponseSchema]: + """Perform JIT rewriting of Python code.""" + return await jit_rewrite(request, data) + + async def optimization_review( + self, + request: AuthenticatedRequest, + data: OptimizationReviewSchema, + optimization_review_model: LLM | None = None, + ) -> tuple[int, OptimizationReviewResponseSchema | OptimizationReviewErrorSchema]: + """Review Python code for optimization opportunities.""" + kwargs = {} + if optimization_review_model is not None: + kwargs["optimization_review_model"] = optimization_review_model + return await get_optimization_review(request, data, **kwargs) + + async def explain_optimizations( + self, user_id: str, data: ExplanationsSchema, explanations_model: LLM | None = None + ) -> ExplanationsResponseSchema | ExplanationsErrorResponseSchema: + """Explain optimizations made to Python code.""" + kwargs = {} + if explanations_model is not None: + kwargs["explanations_model"] = explanations_model + return await explain_optimizations(user_id, data, **kwargs) diff --git a/django/aiservice/jit_rewrite/__init__.py b/django/aiservice/core/languages/python/jit_rewrite/__init__.py similarity index 100% rename from django/aiservice/jit_rewrite/__init__.py rename to django/aiservice/core/languages/python/jit_rewrite/__init__.py diff --git a/django/aiservice/jit_rewrite/jit_rewrite.py b/django/aiservice/core/languages/python/jit_rewrite/jit_rewrite.py similarity index 97% rename from django/aiservice/jit_rewrite/jit_rewrite.py rename to django/aiservice/core/languages/python/jit_rewrite/jit_rewrite.py index fb83ebf80..94032481f 100644 --- a/django/aiservice/jit_rewrite/jit_rewrite.py +++ b/django/aiservice/core/languages/python/jit_rewrite/jit_rewrite.py @@ -18,17 +18,17 @@ from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive from aiservice.llm import LLM, OPTIMIZE_MODEL, calculate_llm_cost, call_llm from authapp.auth import AuthenticatedRequest from authapp.user import get_user_by_id -from log_features.log_event import get_or_create_optimization_event -from log_features.log_features import log_features -from optimizer.config import MAX_OPTIMIZER_CALLS, get_model_distribution -from optimizer.context_utils.optimizer_context import ( +from core.languages.python.optimizer.config import MAX_OPTIMIZER_CALLS, get_model_distribution +from core.languages.python.optimizer.context_utils.optimizer_context import ( BaseOptimizerContext, OptimizeErrorResponseSchema, OptimizeResponseItemSchema, OptimizeResponseSchema, ) -from optimizer.diff_patches_utils.diff import DiffMethod -from optimizer.models import JitRewriteOptimizeSchema, OptimizedCandidateSource +from core.languages.python.optimizer.diff_patches_utils.diff import DiffMethod +from core.languages.python.optimizer.models import JitRewriteOptimizeSchema, OptimizedCandidateSource +from log_features.log_event import get_or_create_optimization_event +from log_features.log_features import log_features if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/jit_rewrite/jit_rewrite_system_prompt.md b/django/aiservice/core/languages/python/jit_rewrite/jit_rewrite_system_prompt.md similarity index 100% rename from django/aiservice/jit_rewrite/jit_rewrite_system_prompt.md rename to django/aiservice/core/languages/python/jit_rewrite/jit_rewrite_system_prompt.md diff --git a/django/aiservice/jit_rewrite/jit_rewrite_user_prompt.md b/django/aiservice/core/languages/python/jit_rewrite/jit_rewrite_user_prompt.md similarity index 100% rename from django/aiservice/jit_rewrite/jit_rewrite_user_prompt.md rename to django/aiservice/core/languages/python/jit_rewrite/jit_rewrite_user_prompt.md diff --git a/django/aiservice/optimization_review/__init__.py b/django/aiservice/core/languages/python/optimization_review/__init__.py similarity index 100% rename from django/aiservice/optimization_review/__init__.py rename to django/aiservice/core/languages/python/optimization_review/__init__.py diff --git a/django/aiservice/optimization_review/optimization_review.py b/django/aiservice/core/languages/python/optimization_review/optimization_review.py similarity index 100% rename from django/aiservice/optimization_review/optimization_review.py rename to django/aiservice/core/languages/python/optimization_review/optimization_review.py diff --git a/django/aiservice/optimization_review/prompts/optimization_review_system_prompt.md b/django/aiservice/core/languages/python/optimization_review/prompts/optimization_review_system_prompt.md similarity index 100% rename from django/aiservice/optimization_review/prompts/optimization_review_system_prompt.md rename to django/aiservice/core/languages/python/optimization_review/prompts/optimization_review_system_prompt.md diff --git a/django/aiservice/optimization_review/prompts/user_prompt_template.md b/django/aiservice/core/languages/python/optimization_review/prompts/user_prompt_template.md similarity index 100% rename from django/aiservice/optimization_review/prompts/user_prompt_template.md rename to django/aiservice/core/languages/python/optimization_review/prompts/user_prompt_template.md diff --git a/django/aiservice/optimizer/__init__.py b/django/aiservice/core/languages/python/optimizer/__init__.py similarity index 100% rename from django/aiservice/optimizer/__init__.py rename to django/aiservice/core/languages/python/optimizer/__init__.py diff --git a/django/aiservice/optimizer/async_system_prompt.md b/django/aiservice/core/languages/python/optimizer/async_system_prompt.md similarity index 100% rename from django/aiservice/optimizer/async_system_prompt.md rename to django/aiservice/core/languages/python/optimizer/async_system_prompt.md diff --git a/django/aiservice/optimizer/async_user_prompt.md b/django/aiservice/core/languages/python/optimizer/async_user_prompt.md similarity index 100% rename from django/aiservice/optimizer/async_user_prompt.md rename to django/aiservice/core/languages/python/optimizer/async_user_prompt.md diff --git a/django/aiservice/optimizer/code_utils/__init__.py b/django/aiservice/core/languages/python/optimizer/code_utils/__init__.py similarity index 100% rename from django/aiservice/optimizer/code_utils/__init__.py rename to django/aiservice/core/languages/python/optimizer/code_utils/__init__.py diff --git a/django/aiservice/optimizer/config.py b/django/aiservice/core/languages/python/optimizer/config.py similarity index 100% rename from django/aiservice/optimizer/config.py rename to django/aiservice/core/languages/python/optimizer/config.py diff --git a/django/aiservice/optimizer/context_utils/__init__.py b/django/aiservice/core/languages/python/optimizer/context_utils/__init__.py similarity index 100% rename from django/aiservice/optimizer/context_utils/__init__.py rename to django/aiservice/core/languages/python/optimizer/context_utils/__init__.py diff --git a/django/aiservice/optimizer/context_utils/constants.py b/django/aiservice/core/languages/python/optimizer/context_utils/constants.py similarity index 100% rename from django/aiservice/optimizer/context_utils/constants.py rename to django/aiservice/core/languages/python/optimizer/context_utils/constants.py diff --git a/django/aiservice/optimizer/context_utils/context_helpers.py b/django/aiservice/core/languages/python/optimizer/context_utils/context_helpers.py similarity index 100% rename from django/aiservice/optimizer/context_utils/context_helpers.py rename to django/aiservice/core/languages/python/optimizer/context_utils/context_helpers.py diff --git a/django/aiservice/optimizer/context_utils/optimizer_context.py b/django/aiservice/core/languages/python/optimizer/context_utils/optimizer_context.py similarity index 95% rename from django/aiservice/optimizer/context_utils/optimizer_context.py rename to django/aiservice/core/languages/python/optimizer/context_utils/optimizer_context.py index e44023f6d..a1bca37cd 100644 --- a/django/aiservice/optimizer/context_utils/optimizer_context.py +++ b/django/aiservice/core/languages/python/optimizer/context_utils/optimizer_context.py @@ -11,7 +11,7 @@ from pydantic import ValidationError from aiservice.common.cst_utils import find_init, parse_module_to_cst from aiservice.common.markdown_utils import extract_code_block_with_context, wrap_code_in_markdown from aiservice.env_specific import debug_log_sensitive_data -from optimizer.context_utils.constants import ( +from core.languages.python.optimizer.context_utils.constants import ( DEPS_CONTEXT_PROMPT, EXPLANATION_THEN_CODE, FULL_CODE_PROMPT_INSTRUCTIONS, @@ -19,15 +19,23 @@ from optimizer.context_utils.constants import ( LINE_PROF_CONTEXT_PROMPT, MARKDOWN_CONTEXT_PROMPT, ) -from optimizer.context_utils.context_helpers import group_code, is_multi_context, split_markdown_code -from optimizer.diff_patches_utils.diff import SEARCH_AND_REPLACE_FORMAT_PROMPT, V4A_DIFF_FORMAT_PROMPT, DiffMethod -from optimizer.diff_patches_utils.seach_and_replace import SearchAndReplaceDiff -from optimizer.diff_patches_utils.v4a_diff import V4ADiff -from optimizer.models import CodeExplanationAndID -from optimizer.postprocess import optimizations_postprocessing_pipeline +from core.languages.python.optimizer.context_utils.context_helpers import ( + group_code, + is_multi_context, + split_markdown_code, +) +from core.languages.python.optimizer.diff_patches_utils.diff import ( + SEARCH_AND_REPLACE_FORMAT_PROMPT, + V4A_DIFF_FORMAT_PROMPT, + DiffMethod, +) +from core.languages.python.optimizer.diff_patches_utils.seach_and_replace import SearchAndReplaceDiff +from core.languages.python.optimizer.diff_patches_utils.v4a_diff import V4ADiff +from core.languages.python.optimizer.models import CodeExplanationAndID +from core.languages.python.optimizer.postprocess import optimizations_postprocessing_pipeline if TYPE_CHECKING: - from optimizer.diff_patches_utils.diff import Diff + from core.languages.python.optimizer.diff_patches_utils.diff import Diff @dataclass diff --git a/django/aiservice/optimizer/context_utils/refiner_context.py b/django/aiservice/core/languages/python/optimizer/context_utils/refiner_context.py similarity index 96% rename from django/aiservice/optimizer/context_utils/refiner_context.py rename to django/aiservice/core/languages/python/optimizer/context_utils/refiner_context.py index 05ecf17cb..20cdaef6e 100644 --- a/django/aiservice/optimizer/context_utils/refiner_context.py +++ b/django/aiservice/core/languages/python/optimizer/context_utils/refiner_context.py @@ -9,17 +9,17 @@ from pydantic import ValidationError from aiservice.common.cst_utils import parse_module_to_cst from aiservice.common.markdown_utils import wrap_code_in_markdown from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax -from optimizer.context_utils.context_helpers import ( +from core.languages.python.optimizer.context_utils.context_helpers import ( group_code, is_markdown_structure_changed, is_multi_context, split_markdown_code, ) -from optimizer.diff_patches_utils.seach_and_replace import SearchAndReplaceDiff -from optimizer.models import CodeAndExplanation +from core.languages.python.optimizer.diff_patches_utils.seach_and_replace import SearchAndReplaceDiff +from core.languages.python.optimizer.models import CodeAndExplanation if TYPE_CHECKING: - from optimizer.diff_patches_utils.diff import Diff + from core.languages.python.optimizer.diff_patches_utils.diff import Diff @dataclass() diff --git a/django/aiservice/optimizer/dependency_context_prompt.md b/django/aiservice/core/languages/python/optimizer/dependency_context_prompt.md similarity index 100% rename from django/aiservice/optimizer/dependency_context_prompt.md rename to django/aiservice/core/languages/python/optimizer/dependency_context_prompt.md diff --git a/django/aiservice/optimizer/diff_patches_utils/__init__.py b/django/aiservice/core/languages/python/optimizer/diff_patches_utils/__init__.py similarity index 100% rename from django/aiservice/optimizer/diff_patches_utils/__init__.py rename to django/aiservice/core/languages/python/optimizer/diff_patches_utils/__init__.py diff --git a/django/aiservice/optimizer/diff_patches_utils/diff.py b/django/aiservice/core/languages/python/optimizer/diff_patches_utils/diff.py similarity index 100% rename from django/aiservice/optimizer/diff_patches_utils/diff.py rename to django/aiservice/core/languages/python/optimizer/diff_patches_utils/diff.py diff --git a/django/aiservice/optimizer/diff_patches_utils/seach_and_replace.py b/django/aiservice/core/languages/python/optimizer/diff_patches_utils/seach_and_replace.py similarity index 95% rename from django/aiservice/optimizer/diff_patches_utils/seach_and_replace.py rename to django/aiservice/core/languages/python/optimizer/diff_patches_utils/seach_and_replace.py index 6c85e77ef..ec79c5dea 100644 --- a/django/aiservice/optimizer/diff_patches_utils/seach_and_replace.py +++ b/django/aiservice/core/languages/python/optimizer/diff_patches_utils/seach_and_replace.py @@ -2,8 +2,11 @@ import re from pydantic import ValidationError -from optimizer.context_utils.constants import MULTI_REPLACE_IN_FILE_TAGS_REGEX, REPLACE_IN_FILE_TAGS_REGEX -from optimizer.diff_patches_utils.diff import Diff +from core.languages.python.optimizer.context_utils.constants import ( + MULTI_REPLACE_IN_FILE_TAGS_REGEX, + REPLACE_IN_FILE_TAGS_REGEX, +) +from core.languages.python.optimizer.diff_patches_utils.diff import Diff class SearchReplaceBlock: @@ -106,8 +109,7 @@ def extract_patches(content: str) -> dict[str, str]: def find_with_whitespace_flexibility(search: str, content: str) -> tuple[int, int] | None: - """ - Find the search string in content with whitespace flexibility. + """Find the search string in content with whitespace flexibility. This handles cases where whitespace differs between the search block and the actual content (e.g., different indentation, trailing whitespace, tabs vs spaces). diff --git a/django/aiservice/optimizer/diff_patches_utils/v4a_diff.py b/django/aiservice/core/languages/python/optimizer/diff_patches_utils/v4a_diff.py similarity index 99% rename from django/aiservice/optimizer/diff_patches_utils/v4a_diff.py rename to django/aiservice/core/languages/python/optimizer/diff_patches_utils/v4a_diff.py index 247255919..c5fe28f9b 100644 --- a/django/aiservice/optimizer/diff_patches_utils/v4a_diff.py +++ b/django/aiservice/core/languages/python/optimizer/diff_patches_utils/v4a_diff.py @@ -1,7 +1,7 @@ import re from dataclasses import dataclass, field -from optimizer.diff_patches_utils.diff import Diff, DiffError +from core.languages.python.optimizer.diff_patches_utils.diff import Diff, DiffError @dataclass diff --git a/django/aiservice/optimizer/init_optimization_prompt.md b/django/aiservice/core/languages/python/optimizer/init_optimization_prompt.md similarity index 100% rename from django/aiservice/optimizer/init_optimization_prompt.md rename to django/aiservice/core/languages/python/optimizer/init_optimization_prompt.md diff --git a/django/aiservice/optimizer/jit_instructions.md b/django/aiservice/core/languages/python/optimizer/jit_instructions.md similarity index 100% rename from django/aiservice/optimizer/jit_instructions.md rename to django/aiservice/core/languages/python/optimizer/jit_instructions.md diff --git a/django/aiservice/optimizer/lineprof_context_prompt.md b/django/aiservice/core/languages/python/optimizer/lineprof_context_prompt.md similarity index 100% rename from django/aiservice/optimizer/lineprof_context_prompt.md rename to django/aiservice/core/languages/python/optimizer/lineprof_context_prompt.md diff --git a/django/aiservice/optimizer/models.py b/django/aiservice/core/languages/python/optimizer/models.py similarity index 100% rename from django/aiservice/optimizer/models.py rename to django/aiservice/core/languages/python/optimizer/models.py diff --git a/django/aiservice/optimizer/multi-file-code-format.md b/django/aiservice/core/languages/python/optimizer/multi-file-code-format.md similarity index 100% rename from django/aiservice/optimizer/multi-file-code-format.md rename to django/aiservice/core/languages/python/optimizer/multi-file-code-format.md diff --git a/django/aiservice/optimizer/optimizer.py b/django/aiservice/core/languages/python/optimizer/optimizer.py similarity index 98% rename from django/aiservice/optimizer/optimizer.py rename to django/aiservice/core/languages/python/optimizer/optimizer.py index 29cf78681..c6146fdd3 100644 --- a/django/aiservice/optimizer/optimizer.py +++ b/django/aiservice/core/languages/python/optimizer/optimizer.py @@ -19,19 +19,19 @@ from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive from aiservice.llm import LLM, OPTIMIZE_MODEL, calculate_llm_cost, call_llm from authapp.auth import AuthenticatedRequest from authapp.user import get_user_by_id -from log_features.log_event import get_or_create_optimization_event -from log_features.log_features import log_features -from optimizer.config import MAX_OPTIMIZER_CALLS, get_model_distribution -from optimizer.context_utils.context_helpers import group_code -from optimizer.context_utils.optimizer_context import ( +from core.languages.js_ts.optimizer import optimize_javascript +from core.languages.python.optimizer.config import MAX_OPTIMIZER_CALLS, get_model_distribution +from core.languages.python.optimizer.context_utils.context_helpers import group_code +from core.languages.python.optimizer.context_utils.optimizer_context import ( BaseOptimizerContext, OptimizeErrorResponseSchema, OptimizeResponseItemSchema, OptimizeResponseSchema, ) -from optimizer.diff_patches_utils.diff import DiffMethod -from optimizer.models import OptimizedCandidateSource, OptimizeSchema -from languages.js_ts.optimizer import optimize_javascript +from core.languages.python.optimizer.diff_patches_utils.diff import DiffMethod +from core.languages.python.optimizer.models import OptimizedCandidateSource, OptimizeSchema +from log_features.log_event import get_or_create_optimization_event +from log_features.log_features import log_features if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/optimizer/optimizer_line_profiler.py b/django/aiservice/core/languages/python/optimizer/optimizer_line_profiler.py similarity index 96% rename from django/aiservice/optimizer/optimizer_line_profiler.py rename to django/aiservice/core/languages/python/optimizer/optimizer_line_profiler.py index 3bb135732..74f18ee76 100644 --- a/django/aiservice/optimizer/optimizer_line_profiler.py +++ b/django/aiservice/core/languages/python/optimizer/optimizer_line_profiler.py @@ -14,19 +14,23 @@ from aiservice.common_utils import parse_python_version, validate_trace_id from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable from aiservice.llm import OPTIMIZE_MODEL, calculate_llm_cost, call_llm from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax -from languages.js_ts.optimizer_lp import optimize_javascript_code_line_profiler -from log_features.log_event import update_optimization_cost -from log_features.log_features import log_features -from optimizer.config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution -from optimizer.context_utils.context_helpers import is_multi_context_js, is_multi_context_ts, split_markdown_code -from optimizer.context_utils.optimizer_context import ( +from core.languages.js_ts.optimizer_lp import optimize_javascript_code_line_profiler +from core.languages.python.optimizer.config import MAX_OPTIMIZER_LP_CALLS, get_model_distribution +from core.languages.python.optimizer.context_utils.context_helpers import ( + is_multi_context_js, + is_multi_context_ts, + split_markdown_code, +) +from core.languages.python.optimizer.context_utils.optimizer_context import ( BaseOptimizerContext, OptimizeErrorResponseSchema, OptimizeResponseItemSchema, OptimizeResponseSchema, ) -from optimizer.diff_patches_utils.diff import DiffMethod -from optimizer.models import OptimizedCandidateSource, OptimizeSchemaLP +from core.languages.python.optimizer.diff_patches_utils.diff import DiffMethod +from core.languages.python.optimizer.models import OptimizedCandidateSource, OptimizeSchemaLP +from log_features.log_event import update_optimization_cost +from log_features.log_features import log_features if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/optimizer/postprocess.py b/django/aiservice/core/languages/python/optimizer/postprocess.py similarity index 99% rename from django/aiservice/optimizer/postprocess.py rename to django/aiservice/core/languages/python/optimizer/postprocess.py index c46dcc785..b45308584 100644 --- a/django/aiservice/optimizer/postprocess.py +++ b/django/aiservice/core/languages/python/optimizer/postprocess.py @@ -14,8 +14,8 @@ from libcst import CSTTransformer, CSTVisitor, Expr, IndentedBlock, SimpleStatem from aiservice.common.cst_utils import compare_unparsed_ast_to_source, parse_module_to_cst, unparse_parse_source from aiservice.common_utils import safe_isort -from optimizer.models import CodeExplanationAndID -from testgen.postprocessing.add_missing_imports import add_future_annotations_import +from core.languages.python.optimizer.models import CodeExplanationAndID +from core.languages.python.testgen.postprocessing.add_missing_imports import add_future_annotations_import if TYPE_CHECKING: from libcst import FunctionDef diff --git a/django/aiservice/optimizer/prompts/__init__.py b/django/aiservice/core/languages/python/optimizer/prompts/__init__.py similarity index 100% rename from django/aiservice/optimizer/prompts/__init__.py rename to django/aiservice/core/languages/python/optimizer/prompts/__init__.py diff --git a/django/aiservice/optimizer/prompts/javascript/async_system_prompt.md b/django/aiservice/core/languages/python/optimizer/prompts/javascript/async_system_prompt.md similarity index 100% rename from django/aiservice/optimizer/prompts/javascript/async_system_prompt.md rename to django/aiservice/core/languages/python/optimizer/prompts/javascript/async_system_prompt.md diff --git a/django/aiservice/optimizer/prompts/javascript/async_user_prompt.md b/django/aiservice/core/languages/python/optimizer/prompts/javascript/async_user_prompt.md similarity index 100% rename from django/aiservice/optimizer/prompts/javascript/async_user_prompt.md rename to django/aiservice/core/languages/python/optimizer/prompts/javascript/async_user_prompt.md diff --git a/django/aiservice/optimizer/prompts/javascript/system_prompt.md b/django/aiservice/core/languages/python/optimizer/prompts/javascript/system_prompt.md similarity index 100% rename from django/aiservice/optimizer/prompts/javascript/system_prompt.md rename to django/aiservice/core/languages/python/optimizer/prompts/javascript/system_prompt.md diff --git a/django/aiservice/optimizer/prompts/javascript/user_prompt.md b/django/aiservice/core/languages/python/optimizer/prompts/javascript/user_prompt.md similarity index 100% rename from django/aiservice/optimizer/prompts/javascript/user_prompt.md rename to django/aiservice/core/languages/python/optimizer/prompts/javascript/user_prompt.md diff --git a/django/aiservice/optimizer/prompts/python/async_system_prompt.md b/django/aiservice/core/languages/python/optimizer/prompts/python/async_system_prompt.md similarity index 100% rename from django/aiservice/optimizer/prompts/python/async_system_prompt.md rename to django/aiservice/core/languages/python/optimizer/prompts/python/async_system_prompt.md diff --git a/django/aiservice/optimizer/prompts/python/async_user_prompt.md b/django/aiservice/core/languages/python/optimizer/prompts/python/async_user_prompt.md similarity index 100% rename from django/aiservice/optimizer/prompts/python/async_user_prompt.md rename to django/aiservice/core/languages/python/optimizer/prompts/python/async_user_prompt.md diff --git a/django/aiservice/optimizer/prompts/python/system_prompt.md b/django/aiservice/core/languages/python/optimizer/prompts/python/system_prompt.md similarity index 100% rename from django/aiservice/optimizer/prompts/python/system_prompt.md rename to django/aiservice/core/languages/python/optimizer/prompts/python/system_prompt.md diff --git a/django/aiservice/optimizer/prompts/python/user_prompt.md b/django/aiservice/core/languages/python/optimizer/prompts/python/user_prompt.md similarity index 100% rename from django/aiservice/optimizer/prompts/python/user_prompt.md rename to django/aiservice/core/languages/python/optimizer/prompts/python/user_prompt.md diff --git a/django/aiservice/optimizer/refinement.py b/django/aiservice/core/languages/python/optimizer/refinement.py similarity index 97% rename from django/aiservice/optimizer/refinement.py rename to django/aiservice/core/languages/python/optimizer/refinement.py index f7dab9d49..9546d1caf 100644 --- a/django/aiservice/optimizer/refinement.py +++ b/django/aiservice/core/languages/python/optimizer/refinement.py @@ -18,11 +18,11 @@ from aiservice.common_utils import validate_trace_id from aiservice.env_specific import debug_log_sensitive_data from aiservice.llm import REFINEMENT_MODEL, calculate_llm_cost, call_llm from authapp.auth import AuthenticatedRequest +from core.languages.python.optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema +from core.languages.python.optimizer.context_utils.refiner_context import BaseRefinerContext, RefinementContextData +from core.languages.python.optimizer.models import OptimizedCandidateSource from log_features.log_event import update_optimization_cost from log_features.log_features import log_features -from optimizer.context_utils.optimizer_context import OptimizeResponseItemSchema -from optimizer.context_utils.refiner_context import BaseRefinerContext, RefinementContextData -from optimizer.models import OptimizedCandidateSource if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/optimizer/refinement_system_prompt.md b/django/aiservice/core/languages/python/optimizer/refinement_system_prompt.md similarity index 100% rename from django/aiservice/optimizer/refinement_system_prompt.md rename to django/aiservice/core/languages/python/optimizer/refinement_system_prompt.md diff --git a/django/aiservice/optimizer/refinement_user_prompt.md b/django/aiservice/core/languages/python/optimizer/refinement_user_prompt.md similarity index 100% rename from django/aiservice/optimizer/refinement_user_prompt.md rename to django/aiservice/core/languages/python/optimizer/refinement_user_prompt.md diff --git a/django/aiservice/optimizer/system_prompt.md b/django/aiservice/core/languages/python/optimizer/system_prompt.md similarity index 100% rename from django/aiservice/optimizer/system_prompt.md rename to django/aiservice/core/languages/python/optimizer/system_prompt.md diff --git a/django/aiservice/optimizer/user_prompt.md b/django/aiservice/core/languages/python/optimizer/user_prompt.md similarity index 100% rename from django/aiservice/optimizer/user_prompt.md rename to django/aiservice/core/languages/python/optimizer/user_prompt.md diff --git a/django/aiservice/testgen/__init__.py b/django/aiservice/core/languages/python/testgen/__init__.py similarity index 100% rename from django/aiservice/testgen/__init__.py rename to django/aiservice/core/languages/python/testgen/__init__.py diff --git a/django/aiservice/testgen/ast_utils/__init__.py b/django/aiservice/core/languages/python/testgen/ast_utils/__init__.py similarity index 100% rename from django/aiservice/testgen/ast_utils/__init__.py rename to django/aiservice/core/languages/python/testgen/ast_utils/__init__.py diff --git a/django/aiservice/testgen/ast_utils/test_detection.py b/django/aiservice/core/languages/python/testgen/ast_utils/test_detection.py similarity index 100% rename from django/aiservice/testgen/ast_utils/test_detection.py rename to django/aiservice/core/languages/python/testgen/ast_utils/test_detection.py diff --git a/django/aiservice/testgen/execute_async_system_prompt.md b/django/aiservice/core/languages/python/testgen/execute_async_system_prompt.md similarity index 100% rename from django/aiservice/testgen/execute_async_system_prompt.md rename to django/aiservice/core/languages/python/testgen/execute_async_system_prompt.md diff --git a/django/aiservice/testgen/execute_async_user_prompt.md b/django/aiservice/core/languages/python/testgen/execute_async_user_prompt.md similarity index 100% rename from django/aiservice/testgen/execute_async_user_prompt.md rename to django/aiservice/core/languages/python/testgen/execute_async_user_prompt.md diff --git a/django/aiservice/testgen/execute_system_prompt.md b/django/aiservice/core/languages/python/testgen/execute_system_prompt.md similarity index 100% rename from django/aiservice/testgen/execute_system_prompt.md rename to django/aiservice/core/languages/python/testgen/execute_system_prompt.md diff --git a/django/aiservice/testgen/execute_user_prompt.md b/django/aiservice/core/languages/python/testgen/execute_user_prompt.md similarity index 100% rename from django/aiservice/testgen/execute_user_prompt.md rename to django/aiservice/core/languages/python/testgen/execute_user_prompt.md diff --git a/django/aiservice/testgen/explain_system_prompt.md b/django/aiservice/core/languages/python/testgen/explain_system_prompt.md similarity index 100% rename from django/aiservice/testgen/explain_system_prompt.md rename to django/aiservice/core/languages/python/testgen/explain_system_prompt.md diff --git a/django/aiservice/testgen/explain_user_prompt.md b/django/aiservice/core/languages/python/testgen/explain_user_prompt.md similarity index 100% rename from django/aiservice/testgen/explain_user_prompt.md rename to django/aiservice/core/languages/python/testgen/explain_user_prompt.md diff --git a/django/aiservice/testgen/instrumentation/__init__.py b/django/aiservice/core/languages/python/testgen/instrumentation/__init__.py similarity index 100% rename from django/aiservice/testgen/instrumentation/__init__.py rename to django/aiservice/core/languages/python/testgen/instrumentation/__init__.py diff --git a/django/aiservice/testgen/instrumentation/edit_generated_test.py b/django/aiservice/core/languages/python/testgen/instrumentation/edit_generated_test.py similarity index 100% rename from django/aiservice/testgen/instrumentation/edit_generated_test.py rename to django/aiservice/core/languages/python/testgen/instrumentation/edit_generated_test.py diff --git a/django/aiservice/testgen/instrumentation/instrument_new_tests.py b/django/aiservice/core/languages/python/testgen/instrumentation/instrument_new_tests.py similarity index 99% rename from django/aiservice/testgen/instrumentation/instrument_new_tests.py rename to django/aiservice/core/languages/python/testgen/instrumentation/instrument_new_tests.py index c42f469fd..ff727b99d 100644 --- a/django/aiservice/testgen/instrumentation/instrument_new_tests.py +++ b/django/aiservice/core/languages/python/testgen/instrumentation/instrument_new_tests.py @@ -10,8 +10,8 @@ import sentry_sdk from aiservice.common_utils import safe_isort from aiservice.models.functions_to_optimize import FunctionParent, FunctionToOptimize -from testgen.ast_utils.test_detection import is_test_function_name -from testgen.models import TestingMode +from core.languages.python.testgen.ast_utils.test_detection import is_test_function_name +from core.languages.python.testgen.models import TestingMode plat_str = platform.python_version_tuple() platform_tuple: tuple[int, int, int] = int(plat_str[0]), int(plat_str[1]), int(plat_str[2]) diff --git a/django/aiservice/testgen/jit_system_prompt.md b/django/aiservice/core/languages/python/testgen/jit_system_prompt.md similarity index 100% rename from django/aiservice/testgen/jit_system_prompt.md rename to django/aiservice/core/languages/python/testgen/jit_system_prompt.md diff --git a/django/aiservice/testgen/models.py b/django/aiservice/core/languages/python/testgen/models.py similarity index 100% rename from django/aiservice/testgen/models.py rename to django/aiservice/core/languages/python/testgen/models.py diff --git a/django/aiservice/testgen/postprocessing/__init__.py b/django/aiservice/core/languages/python/testgen/postprocessing/__init__.py similarity index 100% rename from django/aiservice/testgen/postprocessing/__init__.py rename to django/aiservice/core/languages/python/testgen/postprocessing/__init__.py diff --git a/django/aiservice/testgen/postprocessing/add_missing_imports.py b/django/aiservice/core/languages/python/testgen/postprocessing/add_missing_imports.py similarity index 100% rename from django/aiservice/testgen/postprocessing/add_missing_imports.py rename to django/aiservice/core/languages/python/testgen/postprocessing/add_missing_imports.py diff --git a/django/aiservice/testgen/postprocessing/code_validator.py b/django/aiservice/core/languages/python/testgen/postprocessing/code_validator.py similarity index 97% rename from django/aiservice/testgen/postprocessing/code_validator.py rename to django/aiservice/core/languages/python/testgen/postprocessing/code_validator.py index 05c0a47b7..32c77f3c6 100644 --- a/django/aiservice/testgen/postprocessing/code_validator.py +++ b/django/aiservice/core/languages/python/testgen/postprocessing/code_validator.py @@ -8,7 +8,11 @@ from dataclasses import dataclass import sentry_sdk from aiservice.common_utils import safe_isort -from testgen.ast_utils.test_detection import is_test_class, is_test_function, is_test_function_name +from core.languages.python.testgen.ast_utils.test_detection import ( + is_test_class, + is_test_function, + is_test_function_name, +) @dataclass @@ -176,7 +180,7 @@ def split_code_with_ast(code: str, python_version: tuple[int, int]) -> CodeParts for node in tree.body: if is_test_function(node) or is_test_class(node): # Include decorators - use first decorator's line if present - if hasattr(node, "decorator_list") and node.decorator_list: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.decorator_list: start_line = node.decorator_list[0].lineno - 1 # AST uses 1-based line numbers else: start_line = node.lineno - 1 @@ -193,7 +197,7 @@ def split_code_with_ast(code: str, python_version: tuple[int, int]) -> CodeParts last_preamble_end = end_line else: # Include decorators for non-test functions too (helper classes, etc.) - if hasattr(node, "decorator_list") and node.decorator_list: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.decorator_list: start_line = node.decorator_list[0].lineno - 1 else: start_line = node.lineno - 1 diff --git a/django/aiservice/testgen/postprocessing/postprocess_pipeline.py b/django/aiservice/core/languages/python/testgen/postprocessing/postprocess_pipeline.py similarity index 69% rename from django/aiservice/testgen/postprocessing/postprocess_pipeline.py rename to django/aiservice/core/languages/python/testgen/postprocessing/postprocess_pipeline.py index c1ad75d77..79866de41 100644 --- a/django/aiservice/testgen/postprocessing/postprocess_pipeline.py +++ b/django/aiservice/core/languages/python/testgen/postprocessing/postprocess_pipeline.py @@ -4,14 +4,16 @@ from typing import TYPE_CHECKING import libcst as cst -from optimizer.context_utils.context_helpers import is_multi_context, split_markdown_code -from testgen.instrumentation.edit_generated_test import replace_definition_with_import -from testgen.postprocessing.add_missing_imports import add_missing_imports -from testgen.postprocessing.range_modifier import modify_large_loops -from testgen.postprocessing.remove_unused_definitions import remove_unused_definitions_from_pytest_file -from testgen.postprocessing.removeassert_transformer import remove_asserts_from_test -from testgen.postprocessing.tensor_limit import modify_tensors -from testgen.postprocessing.topdef_terminator import delete_top_def_nodes +from core.languages.python.optimizer.context_utils.context_helpers import is_multi_context, split_markdown_code +from core.languages.python.testgen.instrumentation.edit_generated_test import replace_definition_with_import +from core.languages.python.testgen.postprocessing.add_missing_imports import add_missing_imports +from core.languages.python.testgen.postprocessing.range_modifier import modify_large_loops +from core.languages.python.testgen.postprocessing.remove_unused_definitions import ( + remove_unused_definitions_from_pytest_file, +) +from core.languages.python.testgen.postprocessing.removeassert_transformer import remove_asserts_from_test +from core.languages.python.testgen.postprocessing.tensor_limit import modify_tensors +from core.languages.python.testgen.postprocessing.topdef_terminator import delete_top_def_nodes if TYPE_CHECKING: from collections.abc import Callable diff --git a/django/aiservice/testgen/postprocessing/range_modifier.py b/django/aiservice/core/languages/python/testgen/postprocessing/range_modifier.py similarity index 100% rename from django/aiservice/testgen/postprocessing/range_modifier.py rename to django/aiservice/core/languages/python/testgen/postprocessing/range_modifier.py diff --git a/django/aiservice/testgen/postprocessing/remove_unused_definitions.py b/django/aiservice/core/languages/python/testgen/postprocessing/remove_unused_definitions.py similarity index 99% rename from django/aiservice/testgen/postprocessing/remove_unused_definitions.py rename to django/aiservice/core/languages/python/testgen/postprocessing/remove_unused_definitions.py index a196bcff3..8ecce8ae0 100644 --- a/django/aiservice/testgen/postprocessing/remove_unused_definitions.py +++ b/django/aiservice/core/languages/python/testgen/postprocessing/remove_unused_definitions.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field import libcst as cst from aiservice.common.cst_utils import DepthTrackingMixin -from testgen.ast_utils.test_detection import is_test_class, is_test_function_name +from core.languages.python.testgen.ast_utils.test_detection import is_test_class, is_test_function_name @dataclass diff --git a/django/aiservice/testgen/postprocessing/removeassert_transformer.py b/django/aiservice/core/languages/python/testgen/postprocessing/removeassert_transformer.py similarity index 100% rename from django/aiservice/testgen/postprocessing/removeassert_transformer.py rename to django/aiservice/core/languages/python/testgen/postprocessing/removeassert_transformer.py diff --git a/django/aiservice/testgen/postprocessing/tensor_limit.py b/django/aiservice/core/languages/python/testgen/postprocessing/tensor_limit.py similarity index 100% rename from django/aiservice/testgen/postprocessing/tensor_limit.py rename to django/aiservice/core/languages/python/testgen/postprocessing/tensor_limit.py diff --git a/django/aiservice/testgen/postprocessing/topdef_terminator.py b/django/aiservice/core/languages/python/testgen/postprocessing/topdef_terminator.py similarity index 100% rename from django/aiservice/testgen/postprocessing/topdef_terminator.py rename to django/aiservice/core/languages/python/testgen/postprocessing/topdef_terminator.py diff --git a/django/aiservice/testgen/preprocessing/__init__.py b/django/aiservice/core/languages/python/testgen/preprocessing/__init__.py similarity index 100% rename from django/aiservice/testgen/preprocessing/__init__.py rename to django/aiservice/core/languages/python/testgen/preprocessing/__init__.py diff --git a/django/aiservice/testgen/preprocessing/dataclass_constructor_notes.py b/django/aiservice/core/languages/python/testgen/preprocessing/dataclass_constructor_notes.py similarity index 100% rename from django/aiservice/testgen/preprocessing/dataclass_constructor_notes.py rename to django/aiservice/core/languages/python/testgen/preprocessing/dataclass_constructor_notes.py diff --git a/django/aiservice/testgen/preprocessing/preprocess_pipeline.py b/django/aiservice/core/languages/python/testgen/preprocessing/preprocess_pipeline.py similarity index 80% rename from django/aiservice/testgen/preprocessing/preprocess_pipeline.py rename to django/aiservice/core/languages/python/testgen/preprocessing/preprocess_pipeline.py index 1db28d0a7..b02a4c46e 100644 --- a/django/aiservice/testgen/preprocessing/preprocess_pipeline.py +++ b/django/aiservice/core/languages/python/testgen/preprocessing/preprocess_pipeline.py @@ -1,7 +1,7 @@ from itertools import chain -from testgen.preprocessing.dataclass_constructor_notes import get_dataclass_constructor_notes -from testgen.preprocessing.torch_tensor_limit import get_tensor_size_note +from core.languages.python.testgen.preprocessing.dataclass_constructor_notes import get_dataclass_constructor_notes +from core.languages.python.testgen.preprocessing.torch_tensor_limit import get_tensor_size_note # Preprocessing functions that analyze code context and return notes # Each function takes test_context (str) and returns a list of notes (list[str]) diff --git a/django/aiservice/testgen/preprocessing/torch_tensor_limit.py b/django/aiservice/core/languages/python/testgen/preprocessing/torch_tensor_limit.py similarity index 100% rename from django/aiservice/testgen/preprocessing/torch_tensor_limit.py rename to django/aiservice/core/languages/python/testgen/preprocessing/torch_tensor_limit.py diff --git a/django/aiservice/testgen/prompts/javascript/execute_async_system_prompt.md b/django/aiservice/core/languages/python/testgen/prompts/javascript/execute_async_system_prompt.md similarity index 100% rename from django/aiservice/testgen/prompts/javascript/execute_async_system_prompt.md rename to django/aiservice/core/languages/python/testgen/prompts/javascript/execute_async_system_prompt.md diff --git a/django/aiservice/testgen/prompts/javascript/execute_async_user_prompt.md b/django/aiservice/core/languages/python/testgen/prompts/javascript/execute_async_user_prompt.md similarity index 100% rename from django/aiservice/testgen/prompts/javascript/execute_async_user_prompt.md rename to django/aiservice/core/languages/python/testgen/prompts/javascript/execute_async_user_prompt.md diff --git a/django/aiservice/testgen/prompts/javascript/execute_system_prompt.md b/django/aiservice/core/languages/python/testgen/prompts/javascript/execute_system_prompt.md similarity index 100% rename from django/aiservice/testgen/prompts/javascript/execute_system_prompt.md rename to django/aiservice/core/languages/python/testgen/prompts/javascript/execute_system_prompt.md diff --git a/django/aiservice/testgen/prompts/javascript/execute_user_prompt.md b/django/aiservice/core/languages/python/testgen/prompts/javascript/execute_user_prompt.md similarity index 100% rename from django/aiservice/testgen/prompts/javascript/execute_user_prompt.md rename to django/aiservice/core/languages/python/testgen/prompts/javascript/execute_user_prompt.md diff --git a/django/aiservice/testgen/testgen.py b/django/aiservice/core/languages/python/testgen/testgen.py similarity index 98% rename from django/aiservice/testgen/testgen.py rename to django/aiservice/core/languages/python/testgen/testgen.py index eb3377f2d..8399436e3 100644 --- a/django/aiservice/testgen/testgen.py +++ b/django/aiservice/core/languages/python/testgen/testgen.py @@ -22,12 +22,10 @@ from aiservice.env_specific import debug_log_sensitive_data from aiservice.llm import EXECUTE_MODEL, HAIKU_MODEL, OPENAI_MODEL, calculate_llm_cost, call_llm from aiservice.models.functions_to_optimize import FunctionToOptimize from authapp.auth import AuthenticatedRequest -from languages.js_ts.testgen import testgen_javascript -from log_features.log_event import update_optimization_cost -from log_features.log_features import log_features -from testgen.instrumentation.edit_generated_test import replace_definition_with_import -from testgen.instrumentation.instrument_new_tests import instrument_test_source -from testgen.models import ( +from core.languages.js_ts.testgen import testgen_javascript +from core.languages.python.testgen.instrumentation.edit_generated_test import replace_definition_with_import +from core.languages.python.testgen.instrumentation.instrument_new_tests import instrument_test_source +from core.languages.python.testgen.models import ( TestGenDebugInfo, TestGenerationFailedError, TestGenErrorResponseSchema, @@ -35,9 +33,15 @@ from testgen.models import ( TestGenSchema, TestingMode, ) -from testgen.postprocessing.code_validator import CodeValidationError, has_test_functions, validate_testgen_code -from testgen.postprocessing.postprocess_pipeline import postprocessing_testgen_pipeline -from testgen.testgen_context import BaseTestGenContext, TestGenContextData +from core.languages.python.testgen.postprocessing.code_validator import ( + CodeValidationError, + has_test_functions, + validate_testgen_code, +) +from core.languages.python.testgen.postprocessing.postprocess_pipeline import postprocessing_testgen_pipeline +from core.languages.python.testgen.testgen_context import BaseTestGenContext, TestGenContextData +from log_features.log_event import update_optimization_cost +from log_features.log_features import log_features if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam diff --git a/django/aiservice/testgen/testgen_context.py b/django/aiservice/core/languages/python/testgen/testgen_context.py similarity index 94% rename from django/aiservice/testgen/testgen_context.py rename to django/aiservice/core/languages/python/testgen/testgen_context.py index 3d3383df2..a1e20cb46 100644 --- a/django/aiservice/testgen/testgen_context.py +++ b/django/aiservice/core/languages/python/testgen/testgen_context.py @@ -5,8 +5,8 @@ from dataclasses import dataclass import libcst as cst from aiservice.common.cst_utils import any_ellipsis_in_cst, ellipsis_in_cst_not_types -from optimizer.context_utils.context_helpers import is_multi_context, split_markdown_code -from testgen.preprocessing.preprocess_pipeline import preprocessing_testgen_pipeline +from core.languages.python.optimizer.context_utils.context_helpers import is_multi_context, split_markdown_code +from core.languages.python.testgen.preprocessing.preprocess_pipeline import preprocessing_testgen_pipeline def format_notes_markdown(notes: list[str] | set[str]) -> str: diff --git a/django/aiservice/core/pipeline.py b/django/aiservice/core/pipeline.py new file mode 100644 index 000000000..70dc50195 --- /dev/null +++ b/django/aiservice/core/pipeline.py @@ -0,0 +1,32 @@ +"""Pipeline context for multi-language processing.""" + +from dataclasses import dataclass, field +from typing import Any, Literal + + +@dataclass +class PipelineContext: + """Context object that flows through language processing pipelines.""" + + language_id: str + source_code: str + function_name: str + feature: Literal["testgen", "optimizer", "code_repair"] + + generated_test: str | None = None + optimized_code: str | None = None + repaired_code: str | None = None + test_framework: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) + additional_context: dict[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.language_id: + raise ValueError("language_id is required") + if not self.source_code: + raise ValueError("source_code is required") + if not self.function_name: + raise ValueError("function_name is required") + if self.feature not in ("testgen", "optimizer", "code_repair"): + msg = f"Invalid feature '{self.feature}'. Must be one of: testgen, optimizer, code_repair" + raise ValueError(msg) diff --git a/django/aiservice/core/protocols/__init__.py b/django/aiservice/core/protocols/__init__.py new file mode 100644 index 000000000..20a79f42d --- /dev/null +++ b/django/aiservice/core/protocols/__init__.py @@ -0,0 +1,8 @@ +"""Protocol definitions for language handlers.""" + +from .base import LanguageHandler +from .code_repair import CodeRepairProtocol +from .optimizer import OptimizerProtocol +from .testgen import TestGenProtocol + +__all__ = ["CodeRepairProtocol", "LanguageHandler", "OptimizerProtocol", "TestGenProtocol"] diff --git a/django/aiservice/core/protocols/base.py b/django/aiservice/core/protocols/base.py new file mode 100644 index 000000000..d4c6f4e5d --- /dev/null +++ b/django/aiservice/core/protocols/base.py @@ -0,0 +1,21 @@ +"""Base protocol definitions for language handlers.""" + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class LanguageHandler(Protocol): + """Protocol for language-specific handlers supporting multiple features. + + Handlers declare which features they support via boolean class attributes + and implement the corresponding methods. + """ + + language: str + + supports_testgen: bool + supports_optimizer: bool + supports_code_repair: bool + supports_jit_rewrite: bool + supports_optimization_review: bool + supports_explanations: bool diff --git a/django/aiservice/core/protocols/code_repair.py b/django/aiservice/core/protocols/code_repair.py new file mode 100644 index 000000000..781977ab9 --- /dev/null +++ b/django/aiservice/core/protocols/code_repair.py @@ -0,0 +1,11 @@ +"""Code repair protocol for type narrowing.""" + +from typing import Protocol + + +class CodeRepairProtocol(Protocol): + """Type-narrowing protocol for handlers that support code repair.""" + + supports_code_repair: bool + + async def code_repair_repair(self, user_id: str, optimization_id: str, ctx: object) -> object: ... diff --git a/django/aiservice/core/protocols/optimizer.py b/django/aiservice/core/protocols/optimizer.py new file mode 100644 index 000000000..80e964b4a --- /dev/null +++ b/django/aiservice/core/protocols/optimizer.py @@ -0,0 +1,11 @@ +"""Code optimization protocol for type narrowing.""" + +from typing import Protocol + + +class OptimizerProtocol(Protocol): + """Type-narrowing protocol for handlers that support code optimization.""" + + supports_optimizer: bool + + async def optimizer_optimize(self, request: object, data: object) -> object: ... diff --git a/django/aiservice/core/protocols/testgen.py b/django/aiservice/core/protocols/testgen.py new file mode 100644 index 000000000..2524b736e --- /dev/null +++ b/django/aiservice/core/protocols/testgen.py @@ -0,0 +1,11 @@ +"""Test generation protocol for type narrowing.""" + +from typing import Protocol + + +class TestGenProtocol(Protocol): + """Type-narrowing protocol for handlers that support test generation.""" + + supports_testgen: bool + + async def testgen_generate(self, request: object, data: object) -> object: ... diff --git a/django/aiservice/core/registry.py b/django/aiservice/core/registry.py new file mode 100644 index 000000000..2d3c66b24 --- /dev/null +++ b/django/aiservice/core/registry.py @@ -0,0 +1,54 @@ +"""Language handler registry for multi-language support.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .errors import LanguageNotFoundError + +if TYPE_CHECKING: + from collections.abc import Callable + + from .protocols import LanguageHandler + + +class LanguageRegistry: + """Registry for language-specific handlers.""" + + def __init__(self) -> None: + self._handlers: dict[str, type[LanguageHandler]] = {} + + def register(self, language_id: str, handler: type[LanguageHandler]) -> None: + """Register a handler for a language.""" + if language_id in self._handlers: + existing = self._handlers[language_id] + msg = ( + f"Handler for '{language_id}' already registered as {existing.__name__}. " + f"Cannot register {handler.__name__}." + ) + raise ValueError(msg) + self._handlers[language_id] = handler + + def get_handler(self, language_id: str) -> type[LanguageHandler]: + """Get the handler class for a language.""" + if language_id not in self._handlers: + raise LanguageNotFoundError(language_id, self.list_available()) + return self._handlers[language_id] + + def list_available(self) -> list[str]: + """List all registered language IDs.""" + return sorted(self._handlers.keys()) + + +_registry = LanguageRegistry() +registry = _registry + + +def register_handler(language_id: str) -> Callable[[type[LanguageHandler]], type[LanguageHandler]]: + """Register a language handler class via decorator.""" + + def decorator(handler_class: type[LanguageHandler]) -> type[LanguageHandler]: + _registry.register(language_id, handler_class) + return handler_class + + return decorator diff --git a/django/aiservice/explanations/apps.py b/django/aiservice/explanations/apps.py deleted file mode 100644 index 84fac4079..000000000 --- a/django/aiservice/explanations/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class ExplanationsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "explanations" diff --git a/django/aiservice/jit_rewrite/apps.py b/django/aiservice/jit_rewrite/apps.py deleted file mode 100644 index a53ad26fa..000000000 --- a/django/aiservice/jit_rewrite/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class JitRewriteConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "jit_rewrite" diff --git a/django/aiservice/languages/__init__.py b/django/aiservice/languages/__init__.py deleted file mode 100644 index b5b2a30f1..000000000 --- a/django/aiservice/languages/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Multi-language support module. - -This package contains language-specific implementations for code optimization -and test generation. - -Subpackages: - js_ts: JavaScript and TypeScript support -""" diff --git a/django/aiservice/languages/js_ts/__init__.py b/django/aiservice/languages/js_ts/__init__.py deleted file mode 100644 index c34fdc85d..000000000 --- a/django/aiservice/languages/js_ts/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""JavaScript/TypeScript language support. - -This package contains JS/TS-specific implementations for: -- Code optimization (optimizer.py) -- Line profiler optimization (optimizer_lp.py) -- Test generation (testgen.py) -""" - -from languages.js_ts.optimizer import optimize_javascript -from languages.js_ts.optimizer_lp import optimize_javascript_code_line_profiler -from languages.js_ts.testgen import testgen_javascript - -__all__ = ["optimize_javascript", "optimize_javascript_code_line_profiler", "testgen_javascript"] diff --git a/django/aiservice/mypy_allowlist.txt b/django/aiservice/mypy_allowlist.txt index f3d14f1c6..c0d384fa9 100644 --- a/django/aiservice/mypy_allowlist.txt +++ b/django/aiservice/mypy_allowlist.txt @@ -1,21 +1,21 @@ tests/optimizer/__init__.py -optimizer/__init__.py -optimizer/code_utils/__init__.py +core/languages/python/optimizer/__init__.py +core/languages/python/optimizer/code_utils/__init__.py authapp/__init__.py authapp/tests.py -testgen/models.py +core/languages/python/testgen/models.py tests/testgen/test_testgen.py tests/testgen/__init__.py -testgen/__init__.py +core/languages/python/testgen/__init__.py tests/testgen_instrumentation/__init__.py -testgen/instrumentation/__init__.py +core/languages/python/testgen/instrumentation/__init__.py tests/testgen_postprocessing/__init__.py tests/testgen_postprocessing/test_remove_asserts.py tests/testgen_postprocessing/test_validate_code.py -testgen/postprocessing/__init__.py -testgen/postprocessing/code_validator.py -testgen/postprocessing/topdef_terminator.py -testgen/postprocessing/removeassert_transformer.py +core/languages/python/testgen/postprocessing/__init__.py +core/languages/python/testgen/postprocessing/code_validator.py +core/languages/python/testgen/postprocessing/topdef_terminator.py +core/languages/python/testgen/postprocessing/removeassert_transformer.py log_features/__init__.py aiservice/middleware/__init__.py tests/aiservice/__init__.py diff --git a/django/aiservice/optimization_review/apps.py b/django/aiservice/optimization_review/apps.py deleted file mode 100644 index 50f3e7988..000000000 --- a/django/aiservice/optimization_review/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class OptimizationReviewConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "optimization_review" diff --git a/django/aiservice/optimizer/apps.py b/django/aiservice/optimizer/apps.py deleted file mode 100644 index 7bff8baf3..000000000 --- a/django/aiservice/optimizer/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class OptimizerConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "optimizer" diff --git a/django/aiservice/pyproject.toml b/django/aiservice/pyproject.toml index df53f1312..75bc7a209 100644 --- a/django/aiservice/pyproject.toml +++ b/django/aiservice/pyproject.toml @@ -234,7 +234,7 @@ split-on-trailing-comma = false line-length = 120 fix = true show-fixes = true -exclude = ["testgen/tests", "testgen/sqlalchemy"] +exclude = ["core/languages/python/testgen/tests", "core/languages/python/testgen/sqlalchemy"] [tool.ruff.format] docstring-code-format = true diff --git a/django/aiservice/testgen/apps.py b/django/aiservice/testgen/apps.py deleted file mode 100644 index 0868a99ab..000000000 --- a/django/aiservice/testgen/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class TestgenConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "testgen" diff --git a/django/aiservice/tests/integration/__init__.py b/django/aiservice/tests/integration/__init__.py new file mode 100644 index 000000000..b2924516b --- /dev/null +++ b/django/aiservice/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration tests for language handler system.""" diff --git a/django/aiservice/tests/integration/test_handler_integration.py b/django/aiservice/tests/integration/test_handler_integration.py new file mode 100644 index 000000000..4dd4b470b --- /dev/null +++ b/django/aiservice/tests/integration/test_handler_integration.py @@ -0,0 +1,136 @@ +"""Integration tests for the language handler system. + +These tests verify the integration between handlers, registry, and dispatcher. +Each test uses a fresh registry instance to avoid test pollution. +""" + +import pytest + +import core.dispatcher +from core.dispatcher import get_handler_for_feature, get_handler_for_language +from core.errors import HandlerNotImplementedError, LanguageNotFoundError +from core.languages.js_ts.handler import JSTypeScriptHandler +from core.languages.python.handler import PythonHandler +from core.registry import LanguageRegistry + + +@pytest.fixture +def fresh_registry() -> LanguageRegistry: + """Create a fresh LanguageRegistry instance with handlers registered.""" + registry = LanguageRegistry() + + # Register handlers + registry.register("python", PythonHandler) + registry.register("js_ts", JSTypeScriptHandler) + + return registry + + +class TestPythonHandler: + """Test PythonHandler instantiation and capabilities.""" + + def test_python_handler_instantiation(self) -> None: + """Test PythonHandler instantiation and verify all capability flags.""" + handler = PythonHandler() + + # Verify capability flags + assert handler.supports_testgen is True + assert handler.supports_optimizer is True + assert handler.supports_code_repair is True + assert handler.supports_jit_rewrite is True + assert handler.supports_optimization_review is True + assert handler.supports_explanations is True + + # Verify language + assert handler.language == "python" + + +class TestJSTypeScriptHandler: + """Test JSTypeScriptHandler instantiation and capabilities.""" + + def test_js_typescript_handler_instantiation(self) -> None: + """Test JSTypeScriptHandler instantiation and verify capability flags.""" + handler = JSTypeScriptHandler() + + # Verify capability flags + assert handler.supports_testgen is True + assert handler.supports_optimizer is True + assert handler.supports_code_repair is False + assert handler.supports_jit_rewrite is False + assert handler.supports_optimization_review is False + assert handler.supports_explanations is False + + # Verify language + assert handler.language == "js_ts" + + +class TestRegistry: + """Test registry registration and retrieval.""" + + def test_registry_registration(self, fresh_registry: LanguageRegistry) -> None: + """Test that handlers are properly registered in the registry.""" + # Get handler for Python + python_handler_class = fresh_registry.get_handler("python") + assert python_handler_class is PythonHandler + python_instance = python_handler_class() + assert isinstance(python_instance, PythonHandler) + + # Get handler for JS/TS + js_ts_handler_class = fresh_registry.get_handler("js_ts") + assert js_ts_handler_class is JSTypeScriptHandler + js_ts_instance = js_ts_handler_class() + assert isinstance(js_ts_instance, JSTypeScriptHandler) + + def test_registry_unknown_language_raises_error(self, fresh_registry: LanguageRegistry) -> None: + """Test that requesting an unknown language raises LanguageNotFoundError.""" + with pytest.raises(LanguageNotFoundError) as exc_info: + fresh_registry.get_handler("rust") + + assert "rust" in str(exc_info.value) + assert "not found" in str(exc_info.value) + + def test_list_available(self, fresh_registry: LanguageRegistry) -> None: + """Test that list_available returns the correct languages.""" + available = fresh_registry.list_available() + assert isinstance(available, list) + assert "python" in available + assert "js_ts" in available + assert len(available) == 2 + + +class TestDispatcher: + """Test dispatcher functions.""" + + def test_get_handler_for_language(self, fresh_registry: LanguageRegistry, monkeypatch: pytest.MonkeyPatch) -> None: + """Test get_handler_for_language returns a handler instance.""" + # Monkeypatch the global registry to use our fresh one + monkeypatch.setattr(core.dispatcher, "registry", fresh_registry) + + # Test getting Python handler + python_handler = get_handler_for_language("python") + assert isinstance(python_handler, PythonHandler) + + # Test getting JS/TS handler + js_handler = get_handler_for_language("js_ts") + assert isinstance(js_handler, JSTypeScriptHandler) + + def test_get_handler_for_feature(self, fresh_registry: LanguageRegistry, monkeypatch: pytest.MonkeyPatch) -> None: + """Test get_handler_for_feature with valid and invalid feature combinations.""" + # Monkeypatch the global registry to use our fresh one + monkeypatch.setattr(core.dispatcher, "registry", fresh_registry) + + # Test valid feature: Python with testgen + python_testgen = get_handler_for_feature("python", "testgen") + assert isinstance(python_testgen, PythonHandler) + + # Test valid feature: JS/TS with testgen + js_testgen = get_handler_for_feature("js_ts", "testgen") + assert isinstance(js_testgen, JSTypeScriptHandler) + + # Test invalid feature: JS/TS with code_repair (not supported) + with pytest.raises(HandlerNotImplementedError) as exc_info: + get_handler_for_feature("js_ts", "code_repair") + + assert "js_ts" in str(exc_info.value) + assert "code_repair" in str(exc_info.value) + assert "does not support" in str(exc_info.value) diff --git a/django/aiservice/tests/optimizer/test_code_repair.py b/django/aiservice/tests/optimizer/test_code_repair.py index 8d5b40ca8..4a32260c7 100644 --- a/django/aiservice/tests/optimizer/test_code_repair.py +++ b/django/aiservice/tests/optimizer/test_code_repair.py @@ -1,4 +1,4 @@ -from code_repair.code_repair_context import CodeRepairContext, CodeRepairContextData +from core.languages.python.code_repair.code_repair_context import CodeRepairContext, CodeRepairContextData def test_code_repair_single_file() -> None: diff --git a/django/aiservice/tests/optimizer/test_comment_cleaner.py b/django/aiservice/tests/optimizer/test_comment_cleaner.py index 721492ce0..2da0815fd 100644 --- a/django/aiservice/tests/optimizer/test_comment_cleaner.py +++ b/django/aiservice/tests/optimizer/test_comment_cleaner.py @@ -9,7 +9,7 @@ import ast import libcst as cst -from optimizer.postprocess import clean_extraneous_comments +from core.languages.python.optimizer.postprocess import clean_extraneous_comments def assert_code_unchanged(optimized: str, result: str) -> None: diff --git a/django/aiservice/tests/optimizer/test_context.py b/django/aiservice/tests/optimizer/test_context.py index 3f3201a54..36a4b3163 100644 --- a/django/aiservice/tests/optimizer/test_context.py +++ b/django/aiservice/tests/optimizer/test_context.py @@ -1,13 +1,13 @@ import dataclasses -from optimizer.context_utils.context_helpers import group_code, split_markdown_code -from optimizer.context_utils.optimizer_context import ( +from core.languages.python.optimizer.context_utils.context_helpers import group_code, split_markdown_code +from core.languages.python.optimizer.context_utils.optimizer_context import ( BaseOptimizerContext, MultiOptimizerContext, SingleOptimizerContext, ) -from optimizer.context_utils.refiner_context import BaseRefinerContext, RefinementContextData -from optimizer.diff_patches_utils.diff import DiffMethod +from core.languages.python.optimizer.context_utils.refiner_context import BaseRefinerContext, RefinementContextData +from core.languages.python.optimizer.diff_patches_utils.diff import DiffMethod MULTI_CONTEXT_SPLITTER_PREFIX = "" diff --git a/django/aiservice/tests/optimizer/test_docstring_replacement.py b/django/aiservice/tests/optimizer/test_docstring_replacement.py index 67d918c31..c1ba7f9c3 100644 --- a/django/aiservice/tests/optimizer/test_docstring_replacement.py +++ b/django/aiservice/tests/optimizer/test_docstring_replacement.py @@ -1,8 +1,8 @@ import libcst as cst import pytest -from optimizer.models import CodeExplanationAndID -from optimizer.postprocess import DocstringTransformer, DocstringVisitor, fix_missing_docstring +from core.languages.python.optimizer.models import CodeExplanationAndID +from core.languages.python.optimizer.postprocess import DocstringTransformer, DocstringVisitor, fix_missing_docstring def test_function_docstring_preservation() -> None: @@ -145,7 +145,7 @@ class ExampleClass: def test_fix_missing_docstring_pipeline_function() -> None: # Test the integration with the fix_missing_docstring pipeline function - from optimizer.postprocess import fix_missing_docstring + from core.languages.python.optimizer.postprocess import fix_missing_docstring original_code = """ def example_function(): diff --git a/django/aiservice/tests/optimizer/test_javascript_prompts.py b/django/aiservice/tests/optimizer/test_javascript_prompts.py index 7e5e746fe..635e535fe 100644 --- a/django/aiservice/tests/optimizer/test_javascript_prompts.py +++ b/django/aiservice/tests/optimizer/test_javascript_prompts.py @@ -2,7 +2,7 @@ import pytest -from optimizer.prompts import get_available_languages, get_system_prompt, get_user_prompt +from core.languages.python.optimizer.prompts import get_available_languages, get_system_prompt, get_user_prompt class TestPromptLoader: diff --git a/django/aiservice/tests/optimizer/test_javascript_schema.py b/django/aiservice/tests/optimizer/test_javascript_schema.py index e143e4385..46c55086d 100644 --- a/django/aiservice/tests/optimizer/test_javascript_schema.py +++ b/django/aiservice/tests/optimizer/test_javascript_schema.py @@ -1,6 +1,6 @@ """Tests for JavaScript-related schema changes.""" -from optimizer.models import OptimizeSchema +from core.languages.python.optimizer.models import OptimizeSchema # Note: TestGenSchema tests are in a separate file due to import dependencies diff --git a/django/aiservice/tests/optimizer/test_javascript_testgen.py b/django/aiservice/tests/optimizer/test_javascript_testgen.py index d573a78bb..d33e32d6b 100644 --- a/django/aiservice/tests/optimizer/test_javascript_testgen.py +++ b/django/aiservice/tests/optimizer/test_javascript_testgen.py @@ -1,6 +1,6 @@ """Tests for JavaScript/TypeScript test generation.""" -from languages.js_ts.testgen import _generate_import_statement, _is_valid_js_identifier, build_javascript_prompt +from core.languages.js_ts.testgen import _generate_import_statement, _is_valid_js_identifier, build_javascript_prompt class TestIsValidJsIdentifier: @@ -108,6 +108,7 @@ class TestBuildJavascriptPrompt: is_async=True, ) user_content = messages[1]["content"] + assert isinstance(user_content, str) # Should contain valid destructuring import assert "const { execMongoEval }" in user_content # Should NOT contain invalid syntax with dots in destructuring @@ -123,6 +124,7 @@ class TestBuildJavascriptPrompt: is_async=False, ) user_content = messages[1]["content"] + assert isinstance(user_content, str) # Should contain valid class import assert "const Validator = require('../middlewares/Validator');" in user_content # Should NOT contain invalid syntax like { Validator.validateRequest } @@ -139,6 +141,7 @@ class TestBuildJavascriptPrompt: ) assert suffix == "async-" user_content = messages[1]["content"] + assert isinstance(user_content, str) # Should contain valid class import assert "const Controller = require('../controllers/Controller');" in user_content @@ -152,5 +155,6 @@ class TestBuildJavascriptPrompt: is_async=False, ) user_content = messages[1]["content"] + assert isinstance(user_content, str) # The function accessor should be used in describe blocks assert "describe('MyClass.myMethod'" in user_content diff --git a/django/aiservice/tests/optimizer/test_optimizer.py b/django/aiservice/tests/optimizer/test_optimizer.py index 0bd1f83c3..b51809ec9 100644 --- a/django/aiservice/tests/optimizer/test_optimizer.py +++ b/django/aiservice/tests/optimizer/test_optimizer.py @@ -1,8 +1,8 @@ import libcst import libcst as cst -from optimizer.models import CodeExplanationAndID -from optimizer.postprocess import ( +from core.languages.python.optimizer.models import CodeExplanationAndID +from core.languages.python.optimizer.postprocess import ( cleanup_explanations, dedup_and_sort_imports, deduplicate_optimizations, diff --git a/django/aiservice/tests/optimizer/test_optimizer_v4a_differ.py b/django/aiservice/tests/optimizer/test_optimizer_v4a_differ.py index 3c0975109..b8385b9f0 100644 --- a/django/aiservice/tests/optimizer/test_optimizer_v4a_differ.py +++ b/django/aiservice/tests/optimizer/test_optimizer_v4a_differ.py @@ -1,6 +1,10 @@ -from optimizer.context_utils.optimizer_context import BaseOptimizerContext -from optimizer.diff_patches_utils.diff import DiffMethod -from optimizer.diff_patches_utils.v4a_diff import apply_update, extract_update_sections, parse_patch_text +from core.languages.python.optimizer.context_utils.optimizer_context import BaseOptimizerContext +from core.languages.python.optimizer.diff_patches_utils.diff import DiffMethod +from core.languages.python.optimizer.diff_patches_utils.v4a_diff import ( + apply_update, + extract_update_sections, + parse_patch_text, +) from tests.optimizer.test_context import OptimizerTestCase diff --git a/django/aiservice/tests/optimizer/test_pathes.py b/django/aiservice/tests/optimizer/test_pathes.py index bd95747ee..32ff98300 100644 --- a/django/aiservice/tests/optimizer/test_pathes.py +++ b/django/aiservice/tests/optimizer/test_pathes.py @@ -1,4 +1,4 @@ -from optimizer.diff_patches_utils.seach_and_replace import apply_patches +from core.languages.python.optimizer.diff_patches_utils.seach_and_replace import apply_patches def test_patches() -> None: diff --git a/django/aiservice/tests/testgen/test_dataclass_constructor_notes.py b/django/aiservice/tests/testgen/test_dataclass_constructor_notes.py index 015f022fa..88db8fa12 100644 --- a/django/aiservice/tests/testgen/test_dataclass_constructor_notes.py +++ b/django/aiservice/tests/testgen/test_dataclass_constructor_notes.py @@ -4,7 +4,7 @@ import libcst as cst from aiservice.common.cst_utils import get_base_class_name, has_decorator from aiservice.common.markdown_utils import extract_all_code_from_markdown -from testgen.preprocessing.dataclass_constructor_notes import ( +from core.languages.python.testgen.preprocessing.dataclass_constructor_notes import ( FieldInfo, extract_dataclass_fields, find_all_dataclasses, diff --git a/django/aiservice/tests/testgen/test_ellipsis_in_ast.py b/django/aiservice/tests/testgen/test_ellipsis_in_ast.py index 745d08a9a..4b01a7109 100644 --- a/django/aiservice/tests/testgen/test_ellipsis_in_ast.py +++ b/django/aiservice/tests/testgen/test_ellipsis_in_ast.py @@ -2,7 +2,7 @@ import libcst as cst from django.test import TestCase from aiservice.common.cst_utils import any_ellipsis_in_cst, ellipsis_in_cst_not_types -from testgen.testgen_context import MultiTestGenContext, SingleTestGenContext, TestGenContextData +from core.languages.python.testgen.testgen_context import MultiTestGenContext, SingleTestGenContext, TestGenContextData class TestEllipsisInCst(TestCase): diff --git a/django/aiservice/tests/testgen/test_parse_and_validate_llm_output.py b/django/aiservice/tests/testgen/test_parse_and_validate_llm_output.py index cbd0f66a4..bb36be601 100644 --- a/django/aiservice/tests/testgen/test_parse_and_validate_llm_output.py +++ b/django/aiservice/tests/testgen/test_parse_and_validate_llm_output.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock import pytest from aiservice.models.functions_to_optimize import FunctionParent, FunctionToOptimize -from testgen.testgen import parse_and_validate_llm_output +from core.languages.python.testgen.testgen import parse_and_validate_llm_output @pytest.fixture diff --git a/django/aiservice/tests/testgen/test_perf_injector.py b/django/aiservice/tests/testgen/test_perf_injector.py index 8fbefa885..dadf10766 100644 --- a/django/aiservice/tests/testgen/test_perf_injector.py +++ b/django/aiservice/tests/testgen/test_perf_injector.py @@ -3,9 +3,9 @@ import os from aiservice.common.cst_utils import parse_module_to_cst from aiservice.models.functions_to_optimize import FunctionParent, FunctionToOptimize -from testgen.instrumentation.instrument_new_tests import InjectPerfAndLogging -from testgen.models import TestingMode -from testgen.postprocessing.removeassert_transformer import remove_asserts_from_test +from core.languages.python.testgen.instrumentation.instrument_new_tests import InjectPerfAndLogging +from core.languages.python.testgen.models import TestingMode +from core.languages.python.testgen.postprocessing.removeassert_transformer import remove_asserts_from_test os.environ["CODEFLASH_API_KEY"] = "cf-test-key" diff --git a/django/aiservice/tests/testgen/test_tensor_limit_preprocess.py b/django/aiservice/tests/testgen/test_tensor_limit_preprocess.py index f29128196..c88a6f73e 100644 --- a/django/aiservice/tests/testgen/test_tensor_limit_preprocess.py +++ b/django/aiservice/tests/testgen/test_tensor_limit_preprocess.py @@ -1,5 +1,9 @@ -from testgen.preprocessing.preprocess_pipeline import preprocessing_testgen_pipeline -from testgen.preprocessing.torch_tensor_limit import OBJECT_MEMORY_LIMIT_MB, detect_torch_usage, get_tensor_size_note +from core.languages.python.testgen.preprocessing.preprocess_pipeline import preprocessing_testgen_pipeline +from core.languages.python.testgen.preprocessing.torch_tensor_limit import ( + OBJECT_MEMORY_LIMIT_MB, + detect_torch_usage, + get_tensor_size_note, +) def test_detect_torch_usage_with_torch(): diff --git a/django/aiservice/tests/testgen/test_testgen_javascript.py b/django/aiservice/tests/testgen/test_testgen_javascript.py index c3d0da93a..5e7c2e656 100644 --- a/django/aiservice/tests/testgen/test_testgen_javascript.py +++ b/django/aiservice/tests/testgen/test_testgen_javascript.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest # Load prompts directly to avoid importing testgen_javascript.py -current_dir = Path(__file__).parent.parent.parent / "testgen" +current_dir = Path(__file__).parent.parent.parent / "core" / "languages" / "python" / "testgen" JS_PROMPTS_DIR = current_dir / "prompts" / "javascript" JS_EXECUTE_SYSTEM_PROMPT = (JS_PROMPTS_DIR / "execute_system_prompt.md").read_text() diff --git a/django/aiservice/tests/testgen_instrumentation/test_edit_generated_test.py b/django/aiservice/tests/testgen_instrumentation/test_edit_generated_test.py index 1721908b0..6dcd1c7ac 100644 --- a/django/aiservice/tests/testgen_instrumentation/test_edit_generated_test.py +++ b/django/aiservice/tests/testgen_instrumentation/test_edit_generated_test.py @@ -3,7 +3,7 @@ import ast from libcst import parse_module from aiservice.models.functions_to_optimize import FunctionParent, FunctionToOptimize -from testgen.instrumentation.edit_generated_test import replace_definition_with_import +from core.languages.python.testgen.instrumentation.edit_generated_test import replace_definition_with_import def test_replace_definition_with_import_top_level_function() -> None: diff --git a/django/aiservice/tests/testgen_instrumentation/test_instrument_generated_tests.py b/django/aiservice/tests/testgen_instrumentation/test_instrument_generated_tests.py index d49255b10..9efd9c19e 100644 --- a/django/aiservice/tests/testgen_instrumentation/test_instrument_generated_tests.py +++ b/django/aiservice/tests/testgen_instrumentation/test_instrument_generated_tests.py @@ -1,8 +1,11 @@ from aiservice.common.cst_utils import parse_module_to_cst from aiservice.models.functions_to_optimize import FunctionParent, FunctionToOptimize -from testgen.instrumentation.instrument_new_tests import format_and_float_to_top, instrument_test_source -from testgen.models import TestingMode -from testgen.postprocessing.removeassert_transformer import remove_asserts_from_test +from core.languages.python.testgen.instrumentation.instrument_new_tests import ( + format_and_float_to_top, + instrument_test_source, +) +from core.languages.python.testgen.models import TestingMode +from core.languages.python.testgen.postprocessing.removeassert_transformer import remove_asserts_from_test from tests.conftest import normalize_code imports_block = """from __future__ import annotations diff --git a/django/aiservice/tests/testgen_instrumentation/test_instrument_test_source_used_frameworks.py b/django/aiservice/tests/testgen_instrumentation/test_instrument_test_source_used_frameworks.py index 379bc37b9..21d4289eb 100644 --- a/django/aiservice/tests/testgen_instrumentation/test_instrument_test_source_used_frameworks.py +++ b/django/aiservice/tests/testgen_instrumentation/test_instrument_test_source_used_frameworks.py @@ -7,8 +7,8 @@ synchronization code for different framework imports (torch, tensorflow, jax). from __future__ import annotations from aiservice.models.functions_to_optimize import FunctionToOptimize -from testgen.instrumentation.instrument_new_tests import instrument_test_source -from testgen.models import TestingMode +from core.languages.python.testgen.instrumentation.instrument_new_tests import instrument_test_source +from core.languages.python.testgen.models import TestingMode from tests.conftest import normalize_code # ============================================================================ diff --git a/django/aiservice/tests/testgen_postprocessing/test_add_missing_imports.py b/django/aiservice/tests/testgen_postprocessing/test_add_missing_imports.py index 4ef516b36..4f3e3c079 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_add_missing_imports.py +++ b/django/aiservice/tests/testgen_postprocessing/test_add_missing_imports.py @@ -3,7 +3,10 @@ import libcst as cst from aiservice.common.cst_utils import DefinitionRemover, file_path_to_module_path -from testgen.postprocessing.add_missing_imports import add_future_annotations_import, add_missing_imports +from core.languages.python.testgen.postprocessing.add_missing_imports import ( + add_future_annotations_import, + add_missing_imports, +) # ============================================================================= # Tests for add_missing_imports diff --git a/django/aiservice/tests/testgen_postprocessing/test_async_support.py b/django/aiservice/tests/testgen_postprocessing/test_async_support.py index 042e07deb..085afb65d 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_async_support.py +++ b/django/aiservice/tests/testgen_postprocessing/test_async_support.py @@ -2,7 +2,7 @@ import pytest -from testgen.postprocessing.code_validator import ( +from core.languages.python.testgen.postprocessing.code_validator import ( CodeValidationError, split_code_with_ast, split_code_with_regex, diff --git a/django/aiservice/tests/testgen_postprocessing/test_delete_top_def_nodes.py b/django/aiservice/tests/testgen_postprocessing/test_delete_top_def_nodes.py index 40f39b00b..c8fb4997b 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_delete_top_def_nodes.py +++ b/django/aiservice/tests/testgen_postprocessing/test_delete_top_def_nodes.py @@ -1,5 +1,5 @@ from aiservice.common.cst_utils import parse_module_to_cst -from testgen.postprocessing.topdef_terminator import delete_top_def_nodes +from core.languages.python.testgen.postprocessing.topdef_terminator import delete_top_def_nodes def test_delete_top_def_nodes() -> None: diff --git a/django/aiservice/tests/testgen_postprocessing/test_modify_large_loops.py b/django/aiservice/tests/testgen_postprocessing/test_modify_large_loops.py index f708712a1..7e725df72 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_modify_large_loops.py +++ b/django/aiservice/tests/testgen_postprocessing/test_modify_large_loops.py @@ -1,5 +1,5 @@ from aiservice.common.cst_utils import parse_module_to_cst -from testgen.postprocessing.range_modifier import modify_large_loops +from core.languages.python.testgen.postprocessing.range_modifier import modify_large_loops def test_simple_large_range() -> None: diff --git a/django/aiservice/tests/testgen_postprocessing/test_remove_asserts.py b/django/aiservice/tests/testgen_postprocessing/test_remove_asserts.py index f54b39b1f..44884dee6 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_remove_asserts.py +++ b/django/aiservice/tests/testgen_postprocessing/test_remove_asserts.py @@ -2,7 +2,7 @@ from libcst import Pass, RemoveFromParent, SimpleStatementLine, SimpleStatementS from libcst import parse_module as parse_module_to_cst from aiservice.models.functions_to_optimize import FunctionParent, FunctionToOptimize -from testgen.postprocessing.removeassert_transformer import ( +from core.languages.python.testgen.postprocessing.removeassert_transformer import ( RemoveAssertTransformer, StatementHandler, remove_asserts_from_test, diff --git a/django/aiservice/tests/testgen_postprocessing/test_remove_unused_definitions.py b/django/aiservice/tests/testgen_postprocessing/test_remove_unused_definitions.py index 828b196d7..ecc3fb162 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_remove_unused_definitions.py +++ b/django/aiservice/tests/testgen_postprocessing/test_remove_unused_definitions.py @@ -1,6 +1,8 @@ import libcst as cst -from testgen.postprocessing.remove_unused_definitions import remove_unused_definitions_from_pytest_file +from core.languages.python.testgen.postprocessing.remove_unused_definitions import ( + remove_unused_definitions_from_pytest_file, +) def test_basic_removal() -> None: diff --git a/django/aiservice/tests/testgen_postprocessing/test_tensor_limit_postprocess.py b/django/aiservice/tests/testgen_postprocessing/test_tensor_limit_postprocess.py index 4c7ef0745..9e6678c7f 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_tensor_limit_postprocess.py +++ b/django/aiservice/tests/testgen_postprocessing/test_tensor_limit_postprocess.py @@ -1,6 +1,6 @@ import libcst as cst -from testgen.postprocessing.tensor_limit import modify_tensors +from core.languages.python.testgen.postprocessing.tensor_limit import modify_tensors def test_modify_tensors_large_tensor() -> None: diff --git a/django/aiservice/tests/testgen_postprocessing/test_validate_code.py b/django/aiservice/tests/testgen_postprocessing/test_validate_code.py index 65b769ba8..49e7497ae 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_validate_code.py +++ b/django/aiservice/tests/testgen_postprocessing/test_validate_code.py @@ -2,7 +2,7 @@ from __future__ import annotations import pytest -from testgen.postprocessing.code_validator import ( +from core.languages.python.testgen.postprocessing.code_validator import ( CodeValidationError, split_code_with_ast, split_code_with_regex, diff --git a/django/aiservice/tests/testgen_postprocessing/test_validate_pipeline.py b/django/aiservice/tests/testgen_postprocessing/test_validate_pipeline.py index 836920015..ed8a0ff90 100644 --- a/django/aiservice/tests/testgen_postprocessing/test_validate_pipeline.py +++ b/django/aiservice/tests/testgen_postprocessing/test_validate_pipeline.py @@ -1,9 +1,9 @@ from libcst import parse_module as parse_module_to_cst from aiservice.models.functions_to_optimize import FunctionToOptimize -from optimizer.context_utils.context_helpers import group_code -from testgen.postprocessing.code_validator import validate_testgen_code -from testgen.postprocessing.postprocess_pipeline import postprocessing_testgen_pipeline +from core.languages.python.optimizer.context_utils.context_helpers import group_code +from core.languages.python.testgen.postprocessing.code_validator import validate_testgen_code +from core.languages.python.testgen.postprocessing.postprocess_pipeline import postprocessing_testgen_pipeline def test_postprocessing_testgen_pipeline() -> None: From 4f7d1818acac2eabff3df0007104b6c83dc0a627 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:18:55 -0500 Subject: [PATCH 062/184] Optimize observability timeline and fix UI issues (#2386) ## Summary - Optimize timeline data fetching/rendering with pre-computed maps and reduced re-renders - Split timeline monolith into focused components, lazy-load debug data, use IntersectionObserver for active section tracking - Optimize component rendering with `memo`, stable ref callbacks, and pre-computed sort data - Fix observability nav toggle not syncing with current URL pathname - Fix Response button overlapping dialog close button in LLM debug dialog --- .../api/observability/llm-call-debug/route.ts | 35 + .../components/candidate-content.tsx | 249 +++ .../components/code-context-section.tsx | 9 +- .../components/code-highlighter.tsx | 3 +- .../observability/components/diff-views.tsx | 274 ++++ .../components/format-llm-export.ts | 17 +- .../function-to-optimize-section.tsx | 7 +- .../components/llm-call-debug-dialog.tsx | 519 ++++++ .../components/ranking-content.tsx | 112 ++ .../observability/components/test-content.tsx | 198 +++ .../components/timeline-helpers.ts | 124 ++ .../components/timeline-page-view.tsx | 1451 +---------------- .../components/timeline-section-card.tsx | 133 ++ .../components/timeline-types.ts | 185 ++- .../app/observability/lib/get-trace-data.ts | 12 + .../src/app/observability/llm-export/route.ts | 3 - js/cf-webapp/src/app/observability/page.tsx | 3 - .../observability/trace/[trace_id]/page.tsx | 28 +- .../src/app/observability/traces/page.tsx | 52 +- .../observability/observability-nav.tsx | 18 +- 20 files changed, 1885 insertions(+), 1547 deletions(-) create mode 100644 js/cf-webapp/src/app/api/observability/llm-call-debug/route.ts create mode 100644 js/cf-webapp/src/app/observability/components/candidate-content.tsx create mode 100644 js/cf-webapp/src/app/observability/components/diff-views.tsx create mode 100644 js/cf-webapp/src/app/observability/components/llm-call-debug-dialog.tsx create mode 100644 js/cf-webapp/src/app/observability/components/ranking-content.tsx create mode 100644 js/cf-webapp/src/app/observability/components/test-content.tsx create mode 100644 js/cf-webapp/src/app/observability/components/timeline-helpers.ts create mode 100644 js/cf-webapp/src/app/observability/components/timeline-section-card.tsx diff --git a/js/cf-webapp/src/app/api/observability/llm-call-debug/route.ts b/js/cf-webapp/src/app/api/observability/llm-call-debug/route.ts new file mode 100644 index 000000000..52b2c4d89 --- /dev/null +++ b/js/cf-webapp/src/app/api/observability/llm-call-debug/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from "next/server" +import { prisma } from "@/lib/prisma" + +export async function GET(request: NextRequest): Promise { + const callId = request.nextUrl.searchParams.get("callId") + + if (!callId) { + return NextResponse.json( + { error: "Missing callId parameter" }, + { status: 400 } + ) + } + + const call = await prisma.llm_calls.findUnique({ + where: { id: callId }, + select: { + system_prompt: true, + user_prompt: true, + raw_response: true, + }, + }) + + if (!call) { + return NextResponse.json( + { error: "LLM call not found" }, + { status: 404 } + ) + } + + return NextResponse.json({ + systemPrompt: call.system_prompt ?? null, + userPrompt: call.user_prompt ?? null, + rawResponse: call.raw_response ?? null, + }) +} diff --git a/js/cf-webapp/src/app/observability/components/candidate-content.tsx b/js/cf-webapp/src/app/observability/components/candidate-content.tsx new file mode 100644 index 000000000..6d8091b46 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/candidate-content.tsx @@ -0,0 +1,249 @@ +"use client" + +import { useState, useEffect, useMemo, memo } from "react" +import { + FileText, + Code, + GitCompare, + Columns2, +} from "lucide-react" +import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" +import { parseAllCodeBlocks, findMatchingFile } from "./timeline-helpers" +import { DiffView, SideBySideDiffView } from "./diff-views" +import type { TimelineSectionContent } from "./timeline-types" + +interface CandidateContentProps { + content: Extract + isActive: boolean +} + +export const CandidateContent = memo(function CandidateContent({ + content, + isActive, +}: CandidateContentProps) { + const [viewMode, setViewMode] = useState<"code" | "diff" | "side-by-side">("diff") + const [selectedFileIndex, setSelectedFileIndex] = useState(0) + const [unifiedDiff, setUnifiedDiff] = useState(null) + const [diffLoading, setDiffLoading] = useState(false) + + const originalCode = + content.type === "refinement" ? content.parentCode : content.originalCode + + const candidateFiles = useMemo(() => parseAllCodeBlocks(content.code), [content.code]) + const originalFiles = useMemo( + () => (originalCode ? parseAllCodeBlocks(originalCode) : []), + [originalCode] + ) + + const selectedCandidateFile = useMemo( + () => candidateFiles[selectedFileIndex] || candidateFiles[0], + [candidateFiles, selectedFileIndex] + ) + + const matchingOriginalFile = useMemo(() => { + if (!selectedCandidateFile || originalFiles.length === 0) return null + return findMatchingFile(originalFiles, selectedCandidateFile.path) + }, [selectedCandidateFile, originalFiles]) + + useEffect(() => { + setUnifiedDiff(null) + setDiffLoading(false) + + if (viewMode !== "diff" || !matchingOriginalFile || !selectedCandidateFile) { + return + } + + setDiffLoading(true) + let cancelled = false + + import("diff") + .then(({ createTwoFilesPatch }) => { + if (cancelled) return + + const filename = + selectedCandidateFile.filename || + matchingOriginalFile.filename || + "code.py" + + const diff = createTwoFilesPatch( + `a/${filename}`, + `b/${filename}`, + matchingOriginalFile.code, + selectedCandidateFile.code, + "", + "", + { context: 3 } + ) + + const lines = diff.split("\n") + const hunkStartIndex = lines.findIndex(line => line.startsWith("@@")) + const processedDiff = hunkStartIndex > 0 + ? lines.slice(hunkStartIndex).join("\n") + : diff + + setUnifiedDiff(processedDiff) + setDiffLoading(false) + }) + .catch(error => { + if (cancelled) return + console.error("Failed to load diff library:", error) + setDiffLoading(false) + }) + + return () => { cancelled = true } + }, [viewMode, matchingOriginalFile, selectedCandidateFile]) + + const hasDiff = matchingOriginalFile !== null + const hasMultipleFiles = candidateFiles.length > 1 + + const codeContainerStyle = useMemo( + () => ({ maxHeight: isActive ? "80vh" : "200px" }), + [isActive] + ) + + return ( +

    +
    + {content.rank != null && ( + + #{content.rank} + + )} + {content.isBest && ( + + Best + + )} +
    + + {content.explanation && ( +

    + {content.explanation} +

    + )} + +
    + {hasDiff && ( +
    + + + +
    + )} + + {hasMultipleFiles && ( + + )} +
    + + {viewMode === "code" ? ( + selectedCandidateFile ? ( +
    +
    +
    + + + {selectedCandidateFile.filename || "Code"} + + {selectedCandidateFile.path && selectedCandidateFile.path !== selectedCandidateFile.filename && ( + + ({selectedCandidateFile.path}) + + )} +
    + + {selectedCandidateFile.lineCount} lines + +
    +
    + +
    +
    + ) : ( +
    + No code available +
    + ) + ) : viewMode === "side-by-side" ? ( + matchingOriginalFile && selectedCandidateFile ? ( +
    + +
    + ) : ( +
    + No original code available for comparison +
    + ) + ) : diffLoading ? ( +
    +
    +
    +
    +
    +
    +
    + ) : unifiedDiff ? ( +
    + +
    + ) : ( +
    + No original code available for comparison +
    + )} +
    + ) +}) diff --git a/js/cf-webapp/src/app/observability/components/code-context-section.tsx b/js/cf-webapp/src/app/observability/components/code-context-section.tsx index 1c68b7c86..f998daba5 100644 --- a/js/cf-webapp/src/app/observability/components/code-context-section.tsx +++ b/js/cf-webapp/src/app/observability/components/code-context-section.tsx @@ -19,6 +19,7 @@ interface ParsedFile { language: string code: string tokens: number + lineCount: number } function estimateTokens(text: string): number { @@ -36,12 +37,14 @@ function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] { while ((match = regex.exec(markdown)) !== null) { const [, language, path, code] = match + const trimmedCode = code.trimEnd() files.push({ path, filename: getFilename(path), language: language || "python", - code: code.trimEnd(), - tokens: estimateTokens(code), + code: trimmedCode, + tokens: estimateTokens(trimmedCode), + lineCount: trimmedCode.split("\n").length, }) } @@ -321,7 +324,7 @@ const CodeGroupSection = memo(function CodeGroupSection({
    - {file.code.split("\n").length} lines + {file.lineCount} lines
    diff --git a/js/cf-webapp/src/app/observability/components/code-highlighter.tsx b/js/cf-webapp/src/app/observability/components/code-highlighter.tsx index 49f5f0253..6d23e6019 100644 --- a/js/cf-webapp/src/app/observability/components/code-highlighter.tsx +++ b/js/cf-webapp/src/app/observability/components/code-highlighter.tsx @@ -148,8 +148,9 @@ export const CodeHighlighter = memo(function CodeHighlighter({ function getLineProps() { if (!highlightLines || highlightLines.length === 0) return undefined + const highlightSet = new Set(highlightLines) return (lineNumber: number) => { - const isHighlighted = highlightLines.includes(lineNumber) + const isHighlighted = highlightSet.has(lineNumber) return { style: isHighlighted ? highlightStyle : { display: 'block' }, 'data-highlighted': isHighlighted ? 'true' : undefined, diff --git a/js/cf-webapp/src/app/observability/components/diff-views.tsx b/js/cf-webapp/src/app/observability/components/diff-views.tsx new file mode 100644 index 000000000..1b40088ad --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/diff-views.tsx @@ -0,0 +1,274 @@ +"use client" + +import { useState, useEffect, useMemo, memo, type ReactNode } from "react" + +type SideBySideRow = { + leftLine: string | null + leftNum: number | null + leftType: "removed" | "context" | "empty" + rightLine: string | null + rightNum: number | null + rightType: "added" | "context" | "empty" +} + +// Module-level constants to avoid re-creating JSX on every call +const ADDED_INDICATOR = + +const REMOVED_INDICATOR = {"\u2212"} + +type DiffLineStyle = { + bgClass: string + textClass: string + lineContent: string + indicator: ReactNode + borderClass: string +} + +function getDiffLineStyle(line: string): DiffLineStyle { + if (line.startsWith("@@")) { + return { + bgClass: "bg-blue-900/30", + textClass: "text-blue-400", + lineContent: line, + indicator: null, + borderClass: "border-transparent", + } + } + + if (line.startsWith("+")) { + return { + bgClass: "bg-green-900/40", + textClass: "text-green-300", + lineContent: line.substring(1), + indicator: ADDED_INDICATOR, + borderClass: "border-green-500", + } + } + + if (line.startsWith("-")) { + return { + bgClass: "bg-red-900/40", + textClass: "text-red-300", + lineContent: line.substring(1), + indicator: REMOVED_INDICATOR, + borderClass: "border-red-500", + } + } + + if (line.startsWith(" ")) { + return { + bgClass: "", + textClass: "text-zinc-300", + lineContent: line.substring(1), + indicator: null, + borderClass: "border-transparent", + } + } + + return { + bgClass: "", + textClass: "text-zinc-300", + lineContent: line, + indicator: null, + borderClass: "border-transparent", + } +} + +function getCellStyle(type: "removed" | "added" | "context" | "empty") { + switch (type) { + case "removed": + return "bg-red-900/30 text-red-300" + case "added": + return "bg-green-900/30 text-green-300" + case "empty": + return "bg-zinc-800/30" + default: + return "text-zinc-300" + } +} + +interface DiffViewProps { + diff: string +} + +export const DiffView = memo(function DiffView({ diff }: DiffViewProps) { + const filteredLines = useMemo(() => { + const lines = diff.split("\n") + return lines.filter((line, index) => { + // Skip empty last line + if (index === lines.length - 1 && line === "") return false + + // Skip empty additions/deletions + const isEmptyAddition = line.startsWith("+") && line.substring(1).trim() === "" + const isEmptyDeletion = line.startsWith("-") && line.substring(1).trim() === "" + if (line === "+" || line === "-" || isEmptyAddition || isEmptyDeletion) { + return false + } + + // Skip diff metadata + if (line.startsWith("\\ No newline") || line.startsWith("\\")) { + return false + } + + return true + }) + }, [diff]) + + return ( +
    + {filteredLines.map((line, index) => { + const { bgClass, textClass, lineContent, indicator, borderClass } = getDiffLineStyle(line) + + return ( +
    +
    + {indicator} +
    +
    +              {lineContent || " "}
    +            
    +
    + ) + })} +
    + ) +}) + +interface SideBySideDiffViewProps { + originalCode: string + candidateCode: string + language: string +} + +export const SideBySideDiffView = memo(function SideBySideDiffView({ + originalCode, + candidateCode, +}: SideBySideDiffViewProps) { + const [rows, setRows] = useState(null) + + useEffect(() => { + import("diff").then(({ diffLines }) => { + const changes = diffLines(originalCode, candidateCode) + const result: SideBySideRow[] = [] + let leftNum = 1 + let rightNum = 1 + + for (let i = 0; i < changes.length; i++) { + const change = changes[i] + const lines = change.value.replace(/\n$/, "").split("\n") + + if (!change.added && !change.removed) { + // Context lines - appear on both sides + for (const line of lines) { + result.push({ + leftLine: line, + leftNum: leftNum++, + leftType: "context", + rightLine: line, + rightNum: rightNum++, + rightType: "context", + }) + } + } else if (change.removed) { + // Check if next change is an addition (replacement scenario) + const next = i + 1 < changes.length ? changes[i + 1] : null + if (next && next.added) { + // Side-by-side replacement + const removedLines = lines + const addedLines = next.value.replace(/\n$/, "").split("\n") + const maxLen = Math.max(removedLines.length, addedLines.length) + + for (let j = 0; j < maxLen; j++) { + result.push({ + leftLine: j < removedLines.length ? removedLines[j] : null, + leftNum: j < removedLines.length ? leftNum++ : null, + leftType: j < removedLines.length ? "removed" : "empty", + rightLine: j < addedLines.length ? addedLines[j] : null, + rightNum: j < addedLines.length ? rightNum++ : null, + rightType: j < addedLines.length ? "added" : "empty", + }) + } + i++ // Skip the next change since we processed it + } else { + // Only removal, no corresponding addition + for (const line of lines) { + result.push({ + leftLine: line, + leftNum: leftNum++, + leftType: "removed", + rightLine: null, + rightNum: null, + rightType: "empty", + }) + } + } + } else if (change.added) { + // Only addition, no corresponding removal + for (const line of lines) { + result.push({ + leftLine: null, + leftNum: null, + leftType: "empty", + rightLine: line, + rightNum: rightNum++, + rightType: "added", + }) + } + } + } + + setRows(result) + }) + }, [originalCode, candidateCode]) + + if (!rows) { + return ( +
    +
    +
    +
    +
    + ) + } + + return ( +
    +
    +
    + Original +
    +
    + {rows.map((row, i) => ( +
    + + {row.leftNum ?? ""} + +
    +                {row.leftLine ?? " "}
    +              
    +
    + ))} +
    +
    +
    +
    + Optimized +
    +
    + {rows.map((row, i) => ( +
    + + {row.rightNum ?? ""} + +
    +                {row.rightLine ?? " "}
    +              
    +
    + ))} +
    +
    +
    + ) +}) diff --git a/js/cf-webapp/src/app/observability/components/format-llm-export.ts b/js/cf-webapp/src/app/observability/components/format-llm-export.ts index dd6cfebc0..5055f4b44 100644 --- a/js/cf-webapp/src/app/observability/components/format-llm-export.ts +++ b/js/cf-webapp/src/app/observability/components/format-llm-export.ts @@ -122,21 +122,8 @@ export function formatTimelineForLLM(input: LLMExportInput): string { } if (section.debugData) { - const { systemPrompt, userPrompt } = section.debugData - if (systemPrompt) { - lines.push("#### System Prompt") - lines.push("```") - lines.push(systemPrompt) - lines.push("```") - lines.push("") - } - if (userPrompt) { - lines.push("#### User Prompt") - lines.push("```") - lines.push(userPrompt) - lines.push("```") - lines.push("") - } + lines.push(`#### Debug: LLM Call ID: ${section.debugData.callId}`) + lines.push("") } } diff --git a/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx b/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx index ed8eee1e3..6dbfa2264 100644 --- a/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx +++ b/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx @@ -17,6 +17,7 @@ interface ParsedFile { filename: string language: string code: string + lineCount: number } function getFilename(path: string): string { @@ -30,11 +31,13 @@ function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] { while ((match = regex.exec(markdown)) !== null) { const [, language, path, code] = match + const trimmedCode = code.trimEnd() files.push({ path, filename: getFilename(path), language: language || "python", - code: code.trimEnd(), + code: trimmedCode, + lineCount: trimmedCode.split("\n").length, }) } @@ -185,7 +188,7 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection )} - {functionFile.code.split("\n").length} lines + {functionFile.lineCount} lines
    diff --git a/js/cf-webapp/src/app/observability/components/llm-call-debug-dialog.tsx b/js/cf-webapp/src/app/observability/components/llm-call-debug-dialog.tsx new file mode 100644 index 000000000..0ed7fccd3 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/llm-call-debug-dialog.tsx @@ -0,0 +1,519 @@ +"use client" + +import { useState, useRef, useEffect, memo, useMemo } from "react" +import { + ChevronDown, + ChevronUp, + Code, + Bug, + Search, + X, +} from "lucide-react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" +import type { LLMCallDebugData } from "./timeline-types" + +interface ContentPart { + type: "text" | "code" + content: string + language?: string +} + +const PromptContent = memo(function PromptContent({ content }: { content: string }) { + const parts = useMemo(() => { + const result: ContentPart[] = [] + const lines = content.split("\n") + let inCode = false + let codeLang = "python" + let codeLines: string[] = [] + let textLines: string[] = [] + let depth = 0 + + for (const line of lines) { + if (!inCode) { + const fence = line.match(/^```(\w+)/) + if (fence) { + // Save any pending text + if (textLines.length > 0) { + result.push({ type: "text", content: textLines.join("\n") }) + textLines = [] + } + // Start code block + inCode = true + depth = 0 + codeLang = fence[1] || "python" + codeLines = [] + } else { + textLines.push(line) + } + } else if (line.match(/^```\w/)) { + // Nested code block + depth++ + codeLines.push(line) + } else if (line.trim() === "```") { + if (depth > 0) { + // End nested code block + depth-- + codeLines.push(line) + } else { + // End main code block + result.push({ + type: "code", + content: codeLines.join("\n").trim(), + language: codeLang, + }) + codeLines = [] + inCode = false + } + } else { + codeLines.push(line) + } + } + + // Handle unclosed blocks + if (inCode && codeLines.length > 0) { + result.push({ + type: "code", + content: codeLines.join("\n").trim(), + language: codeLang, + }) + } + if (textLines.length > 0) { + result.push({ type: "text", content: textLines.join("\n") }) + } + + return result.length > 0 ? result : [{ type: "text" as const, content }] + }, [content]) + + return ( +
    + {parts.map((part, index) => { + if (part.type === "code") { + return ( +
    + +
    + ) + } + + return ( +
    +            {part.content}
    +          
    + ) + })} +
    + ) +}) + +function clearSearchHighlights(container: HTMLElement) { + const marks = container.querySelectorAll("mark[data-search-highlight]") + marks.forEach(mark => { + const parent = mark.parentNode + if (parent) { + parent.replaceChild(document.createTextNode(mark.textContent || ""), mark) + parent.normalize() + } + }) +} + +function applySearchHighlights(container: HTMLElement, query: string): number { + clearSearchHighlights(container) + if (!query.trim()) return 0 + + const lowerQuery = query.toLowerCase() + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT) + const nodesToProcess: { node: Text; matches: { start: number; end: number }[] }[] = [] + + let textNode: Text | null + while ((textNode = walker.nextNode() as Text | null)) { + const text = textNode.textContent || "" + const lowerText = text.toLowerCase() + const matches: { start: number; end: number }[] = [] + let searchFrom = 0 + + while (true) { + const index = lowerText.indexOf(lowerQuery, searchFrom) + if (index === -1) break + matches.push({ start: index, end: index + query.length }) + searchFrom = index + query.length + } + + if (matches.length > 0) { + nodesToProcess.push({ node: textNode, matches }) + } + } + + let totalMatches = 0 + + for (const { node, matches } of nodesToProcess) { + const text = node.textContent || "" + const fragment = document.createDocumentFragment() + let lastIndex = 0 + + for (const match of matches) { + if (match.start > lastIndex) { + fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.start))) + } + + const mark = document.createElement("mark") + mark.setAttribute("data-search-highlight", "") + mark.setAttribute("data-match-index", String(totalMatches)) + mark.className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]" + mark.textContent = text.slice(match.start, match.end) + fragment.appendChild(mark) + + totalMatches++ + lastIndex = match.end + } + + if (lastIndex < text.length) { + fragment.appendChild(document.createTextNode(text.slice(lastIndex))) + } + + node.parentNode?.replaceChild(fragment, node) + } + + return totalMatches +} + +function scrollToSearchMatch(container: HTMLElement, index: number) { + const marks = container.querySelectorAll("mark[data-search-highlight]") + marks.forEach(m => { + (m as HTMLElement).className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]" + }) + + const target = marks[index] as HTMLElement | undefined + if (target) { + target.className = "bg-orange-300 dark:bg-orange-600/70 text-inherit rounded-[2px] ring-2 ring-orange-400 dark:ring-orange-500" + target.scrollIntoView({ behavior: "smooth", block: "center" }) + } +} + +interface LLMCallDebugDialogProps { + debugData: LLMCallDebugData + title: string + model?: string | null +} + +export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({ + debugData, + title, + model, +}: LLMCallDebugDialogProps) { + const [open, setOpen] = useState(false) + const [activeTab, setActiveTab] = useState<"user" | "system">("user") + const [showResponse, setShowResponse] = useState(false) + const [contentReady, setContentReady] = useState(false) + const [searchOpen, setSearchOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [matchCount, setMatchCount] = useState(0) + const [currentMatch, setCurrentMatch] = useState(-1) + const contentRef = useRef(null) + const searchInputRef = useRef(null) + + const [fetchedData, setFetchedData] = useState<{ + systemPrompt: string | null + userPrompt: string | null + rawResponse: string | null + } | null>(null) + const [fetchLoading, setFetchLoading] = useState(false) + const [fetchError, setFetchError] = useState(null) + + useEffect(() => { + if (!open || fetchedData || !debugData.callId) return + + const controller = new AbortController() + setFetchLoading(true) + setFetchError(null) + + fetch(`/api/observability/llm-call-debug?callId=${encodeURIComponent(debugData.callId)}`, { + signal: controller.signal, + }) + .then(res => { + if (!res.ok) throw new Error(`Failed to fetch debug data: ${res.status}`) + return res.json() + }) + .then(data => { + setFetchedData(data) + setFetchLoading(false) + }) + .catch(err => { + if (err.name === "AbortError") return + setFetchError(err.message) + setFetchLoading(false) + }) + + return () => controller.abort() + }, [open, fetchedData, debugData.callId]) + + useEffect(() => { + if (open) { + const timer = requestAnimationFrame(() => setContentReady(true)) + return () => cancelAnimationFrame(timer) + } else { + setContentReady(false) + setShowResponse(false) + setSearchOpen(false) + setSearchQuery("") + setMatchCount(0) + setCurrentMatch(-1) + } + }, [open]) + + useEffect(() => { + if (searchOpen) { + requestAnimationFrame(() => searchInputRef.current?.focus()) + } + }, [searchOpen]) + + useEffect(() => { + const container = contentRef.current + if (!container || !contentReady) return + + const timer = setTimeout(() => { + clearSearchHighlights(container) + if (!searchQuery.trim()) { + setMatchCount(0) + setCurrentMatch(-1) + return + } + const count = applySearchHighlights(container, searchQuery) + setMatchCount(count) + const next = count > 0 ? 0 : -1 + setCurrentMatch(next) + if (count > 0) { + scrollToSearchMatch(container, 0) + } + }, 150) + + return () => clearTimeout(timer) + }, [searchQuery, activeTab, showResponse, contentReady]) + + function navigateMatch(direction: "next" | "prev") { + if (matchCount === 0) return + + const next = direction === "next" + ? (currentMatch + 1) % matchCount + : (currentMatch - 1 + matchCount) % matchCount + + setCurrentMatch(next) + if (contentRef.current) { + scrollToSearchMatch(contentRef.current, next) + } + } + + function closeSearch() { + setSearchOpen(false) + setSearchQuery("") + if (contentRef.current) { + clearSearchHighlights(contentRef.current) + } + } + + const isLoading = fetchLoading || !fetchedData + + const searchBar = searchOpen ? ( +
    + + setSearchQuery(e.target.value)} + onKeyDown={e => { + if (e.key === "Enter") { + e.preventDefault() + navigateMatch(e.shiftKey ? "prev" : "next") + } else if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + closeSearch() + } + }} + placeholder={`Search ${activeTab === "user" ? "user" : "system"} prompt...`} + className="flex-1 bg-transparent text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none min-w-0" + /> + {searchQuery && ( + + {matchCount > 0 ? `${currentMatch + 1} of ${matchCount}` : "No matches"} + + )} +
    + + + +
    +
    + ) : ( +
    + +
    + ) + + const loadingSkeleton = ( +
    +
    +
    +
    +
    + ) + + return ( + + + + + { + if ((e.metaKey || e.ctrlKey) && e.key === "f") { + e.preventDefault() + setSearchOpen(true) + } + }} + > + +
    + + + {title} + + +
    + {model && ( +
    + + {model} + +
    + )} +
    + + {fetchError ? ( +
    +

    Failed to load debug data: {fetchError}

    +
    + ) : isLoading ? ( +
    + {loadingSkeleton} +
    + ) : showResponse ? ( +
    +
    + {fetchedData.rawResponse ? ( +
    +                  {fetchedData.rawResponse}
    +                
    + ) : ( + No response + )} +
    +
    + ) : ( + setActiveTab(v as "user" | "system")} className="flex-1 flex flex-col min-h-0 mt-3"> + + + User Prompt + + ({(fetchedData.userPrompt?.length || 0).toLocaleString()} chars) + + + + System Prompt + + ({(fetchedData.systemPrompt?.length || 0).toLocaleString()} chars) + + + + +
    + {searchBar} +
    + {!contentReady ? ( + loadingSkeleton + ) : activeTab === "user" ? ( + fetchedData.userPrompt ? ( + + ) : ( + No user prompt + ) + ) : ( + fetchedData.systemPrompt ? ( +
    +                      {fetchedData.systemPrompt}
    +                    
    + ) : ( + No system prompt + ) + )} +
    +
    +
    + )} +
    +
    + ) +}) diff --git a/js/cf-webapp/src/app/observability/components/ranking-content.tsx b/js/cf-webapp/src/app/observability/components/ranking-content.tsx new file mode 100644 index 000000000..49ba27cf7 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/ranking-content.tsx @@ -0,0 +1,112 @@ +"use client" + +import { memo, useMemo } from "react" +import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" +import { stripCodeHeader, formatTime } from "./timeline-helpers" +import type { TimelineSectionContent } from "./timeline-types" + +interface RankingContentProps { + content: Extract +} + +export const RankingContent = memo(function RankingContent({ content }: RankingContentProps) { + const strippedCodes = useMemo( + () => new Map(content.rankings.map(item => [item.id, stripCodeHeader(item.code)])), + [content.rankings] + ) + + return ( +
    + {content.explanation && ( +
    +

    + {content.explanation} +

    +
    + )} + + {content.rankings.length >= 1 && ( +
    + {content.rankings.map((item) => ( +
    +
    + + {item.label} + + + Rank #{item.rank} + + {item.isBest && ( + + Best + + )} + {item.isBest && content.usedForPr && ( + + Used for PR + + )} +
    +
    + +
    +
    + ))} +
    + )} +
    + ) +}) + +interface SummaryContentProps { + content: Extract +} + +export const SummaryContent = memo(function SummaryContent({ content }: SummaryContentProps) { + const { metrics } = content + return ( +
    +
    +
    Total Duration
    +
    + {formatTime(metrics.totalDuration)} +
    +
    +
    +
    Total Cost
    +
    + ${metrics.totalCost.toFixed(4)} +
    +
    +
    +
    Total Tokens
    +
    + {metrics.totalTokens.toLocaleString()} +
    +
    +
    +
    Candidates
    +
    + {metrics.candidatesCount} +
    +
    +
    + ) +}) diff --git a/js/cf-webapp/src/app/observability/components/test-content.tsx b/js/cf-webapp/src/app/observability/components/test-content.tsx new file mode 100644 index 000000000..21a354586 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/test-content.tsx @@ -0,0 +1,198 @@ +"use client" + +import { useState, memo } from "react" +import { + ChevronDown, + FlaskConical, + CheckCircle2, +} from "lucide-react" +import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" +import type { TimelineSectionContent } from "./timeline-types" + +function getEmptyMessage( + variant: "generated" | "instrumented" | "instrumentedPerf" +): string { + switch (variant) { + case "generated": + return "No generated test available" + case "instrumented": + return "No instrumented behavior test available" + case "instrumentedPerf": + return "No instrumented perf test available" + default: + return "No test available" + } +} + +interface TestContentProps { + content: Extract +} + +export const TestContent = memo(function TestContent({ content }: TestContentProps) { + const [showDetails, setShowDetails] = useState(false) + const [expandedTest, setExpandedTest] = useState(null) + const [activeVariant, setActiveVariant] = useState<"generated" | "instrumented" | "instrumentedPerf">("generated") + + const testCount = content.testGroups.length + const hasInstrumented = content.testGroups.some(g => g.instrumented) + const hasInstrumentedPerf = content.testGroups.some(g => g.instrumentedPerf) + + return ( +
    +
    +
    +
    + + + {testCount} test{testCount !== 1 ? "s" : ""} generated + +
    +
    + {content.testFramework && ( + + {content.testFramework} + + )} + {hasInstrumented && ( + + +behavior + + )} + {hasInstrumentedPerf && ( + + +perf + + )} +
    +
    + +
    + + {showDetails && ( +
    + {content.testGroups.map((group) => { + const isExpanded = expandedTest === group.index + const hasMultipleVariants = [group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length > 1 + + const currentCode = (() => { + switch (activeVariant) { + case "generated": + return group.generated + case "instrumented": + return group.instrumented + case "instrumentedPerf": + return group.instrumentedPerf + default: + return null + } + })() + + return ( +
    + + + {isExpanded && ( +
    + {hasMultipleVariants && ( +
    + {group.generated && ( + + )} + {group.instrumented && ( + + )} + {group.instrumentedPerf && ( + + )} +
    + )} + +
    + {currentCode ? ( + + ) : ( +
    + {getEmptyMessage(activeVariant)} +
    + )} +
    +
    + )} +
    + ) + })} +
    + )} +
    + ) +}) diff --git a/js/cf-webapp/src/app/observability/components/timeline-helpers.ts b/js/cf-webapp/src/app/observability/components/timeline-helpers.ts new file mode 100644 index 000000000..70b96d5cf --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/timeline-helpers.ts @@ -0,0 +1,124 @@ +import { + FlaskConical, + Activity, + Box, + RefreshCw, + CheckCircle2, + BarChart3, +} from "lucide-react" + +export const TYPE_CONFIG = { + test_generation: { icon: FlaskConical }, + optimization: { icon: Box }, + line_profiler: { icon: Activity }, + refinement: { icon: RefreshCw }, + ranking: { icon: BarChart3 }, + summary: { icon: CheckCircle2 }, +} + +export function formatTime(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms` + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` + return `${(ms / 60000).toFixed(1)}m` +} + +export function stripCodeHeader(code: string): string { + const lines = code.split("\n") + + // Remove opening fence if present + const hasOpeningFence = lines[0] && /^`{3}[a-z]*(:.*)?$/i.test(lines[0].trim()) + const startIndex = hasOpeningFence ? 1 : 0 + + // Remove closing fence if present + const lastLine = lines[lines.length - 1] + const hasClosingFence = lines.length > 0 && lastLine?.trim() === "```" + const endIndex = hasClosingFence ? lines.length - 1 : lines.length + + return lines.slice(startIndex, endIndex).join("\n") +} + +export interface ParsedCodeBlock { + language: string + filename: string | null + path: string | null + code: string + lineCount: number +} + +export function parseCodeBlock(rawCode: string): ParsedCodeBlock { + const markdownMatch = rawCode.match(/^```(\w+)(?::([^\n]+))?\n([\s\S]*?)```\s*$/) + + if (markdownMatch) { + const [, language, path, code] = markdownMatch + const trimmedCode = code.trimEnd() + const filename = path ? path.split("/").pop() || null : null + + return { + language: language || "python", + filename, + path: path || null, + code: trimmedCode, + lineCount: trimmedCode.split("\n").length, + } + } + + return { + language: "python", + filename: null, + path: null, + code: rawCode, + lineCount: rawCode.split("\n").length, + } +} + +export function parseAllCodeBlocks(markdown: string): ParsedCodeBlock[] { + const files: ParsedCodeBlock[] = [] + const regex = /```(\w+)(?::([^\n]+))?\n([\s\S]*?)```/g + let match + + while ((match = regex.exec(markdown)) !== null) { + const [, language, path, code] = match + const filename = path ? path.split("/").pop() || null : null + const trimmedCode = code.trimEnd() + files.push({ + path: path || null, + filename, + language: language || "python", + code: trimmedCode, + lineCount: trimmedCode.split("\n").length, + }) + } + + if (files.length === 0 && markdown.trim()) { + return [parseCodeBlock(markdown)] + } + + return files +} + +export function findMatchingFile( + files: ParsedCodeBlock[], + targetPath: string | null +): ParsedCodeBlock | null { + if (!targetPath || files.length === 0) { + return files[0] || null + } + + // Try exact path match first + const exactMatch = files.find(f => f.path === targetPath) + if (exactMatch) return exactMatch + + // Try filename match + const targetFilename = targetPath.split("/").pop() + const filenameMatch = files.find(f => f.filename === targetFilename) + if (filenameMatch) return filenameMatch + + // Try partial path match + const partialMatch = files.find(f => { + return f.path && (targetPath.endsWith(f.path) || f.path.endsWith(targetPath)) + }) + if (partialMatch) return partialMatch + + // Fall back to first file + return files[0] || null +} diff --git a/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx b/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx index 6239173db..e5486112d 100644 --- a/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx +++ b/js/cf-webapp/src/app/observability/components/timeline-page-view.tsx @@ -1,47 +1,9 @@ "use client" -import { useState, useRef, useEffect, memo, useMemo } from "react" -import { - Clock, - FlaskConical, - Activity, - Box, - RefreshCw, - ChevronDown, - ChevronUp, - FileText, - Code, - GitCompare, - CheckCircle2, - XCircle, - AlertCircle, - BarChart3, - Bug, - Search, - X, - Columns2, -} from "lucide-react" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" -import type { TimelineSection, TimelineSectionContent, LLMCallDebugData } from "./timeline-types" - -function stripCodeHeader(code: string): string { - let lines = code.split("\n") - if (lines[0] && /^`{3}[a-z]*(:.*)?$/i.test(lines[0].trim())) { - lines = lines.slice(1) - } - if (lines.length > 0 && lines[lines.length - 1]?.trim() === "```") { - lines = lines.slice(0, -1) - } - return lines.join("\n") -} +import { useState, useRef, useEffect, memo } from "react" +import { formatTime } from "./timeline-helpers" +import { TimelineSectionCard } from "./timeline-section-card" +import type { TimelineSection } from "./timeline-types" interface TimelinePageViewProps { sections: TimelineSection[] @@ -50,1321 +12,6 @@ interface TimelinePageViewProps { filePath?: string | null } -const TYPE_CONFIG = { - test_generation: { icon: FlaskConical }, - optimization: { icon: Box }, - line_profiler: { icon: Activity }, - refinement: { icon: RefreshCw }, - ranking: { icon: BarChart3 }, - summary: { icon: CheckCircle2 }, -} - -function formatTime(ms: number): string { - if (ms < 1000) return `${Math.round(ms)}ms` - if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` - return `${(ms / 60000).toFixed(1)}m` -} - -function getStatusIcon(status: string) { - switch (status) { - case "success": - return - case "failed": - return - case "partial": - return - default: - return - } -} - -interface ParsedCodeBlock { - language: string - filename: string | null - path: string | null - code: string -} - -function parseCodeBlock(rawCode: string): ParsedCodeBlock { - const markdownMatch = rawCode.match(/^```(\w+)(?::([^\n]+))?\n([\s\S]*?)```\s*$/) - if (markdownMatch) { - const [, language, path, code] = markdownMatch - const filename = path ? path.split("/").pop() || null : null - return { language: language || "python", filename, path: path || null, code: code.trimEnd() } - } - return { language: "python", filename: null, path: null, code: rawCode } -} - -function parseAllCodeBlocks(markdown: string): ParsedCodeBlock[] { - const files: ParsedCodeBlock[] = [] - const regex = /```(\w+)(?::([^\n]+))?\n([\s\S]*?)```/g - let match - - while ((match = regex.exec(markdown)) !== null) { - const [, language, path, code] = match - const filename = path ? path.split("/").pop() || null : null - files.push({ - path: path || null, - filename, - language: language || "python", - code: code.trimEnd(), - }) - } - - if (files.length === 0 && markdown.trim()) { - return [parseCodeBlock(markdown)] - } - - return files -} - -function findMatchingFile( - files: ParsedCodeBlock[], - targetPath: string | null -): ParsedCodeBlock | null { - if (!targetPath || files.length === 0) return files[0] || null - - const exactMatch = files.find(f => f.path === targetPath) - if (exactMatch) return exactMatch - - const targetFilename = targetPath.split("/").pop() - const filenameMatch = files.find(f => f.filename === targetFilename) - if (filenameMatch) return filenameMatch - - const partialMatch = files.find(f => - f.path && (targetPath.endsWith(f.path) || f.path.endsWith(targetPath)) - ) - if (partialMatch) return partialMatch - - return files[0] || null -} - -/** Renders prompt content with syntax-highlighted code blocks. - * Uses line-by-line parsing so nested fences (e.g. ```python:path inside - * an outer ```python block) are kept as code content instead of breaking - * the block. A code block is only closed by a standalone ``` line. */ -const PromptContent = memo(function PromptContent({ content }: { content: string }) { - const parts = useMemo(() => { - const result: { type: "text" | "code"; content: string; language?: string }[] = [] - const lines = content.split("\n") - let inCode = false - let codeLang = "python" - let codeLines: string[] = [] - let textLines: string[] = [] - let depth = 0 - - for (const line of lines) { - if (!inCode) { - const fence = line.match(/^```(\w+)/) - if (fence) { - if (textLines.length > 0) { - result.push({ type: "text", content: textLines.join("\n") }) - textLines = [] - } - inCode = true - depth = 0 - codeLang = fence[1] || "python" - codeLines = [] - } else { - textLines.push(line) - } - } else if (line.match(/^```\w/)) { - depth++ - codeLines.push(line) - } else if (line.trim() === "```") { - if (depth > 0) { - depth-- - codeLines.push(line) - } else { - result.push({ type: "code", content: codeLines.join("\n").trim(), language: codeLang }) - codeLines = [] - inCode = false - } - } else { - codeLines.push(line) - } - } - - if (inCode && codeLines.length > 0) { - result.push({ type: "code", content: codeLines.join("\n").trim(), language: codeLang }) - } - if (textLines.length > 0) { - result.push({ type: "text", content: textLines.join("\n") }) - } - - return result.length > 0 ? result : [{ type: "text" as const, content }] - }, [content]) - - return ( -
    - {parts.map((part, index) => - part.type === "code" ? ( -
    - -
    - ) : ( -
    -            {part.content}
    -          
    - ) - )} -
    - ) -}) - -function clearSearchHighlights(container: HTMLElement) { - const marks = container.querySelectorAll("mark[data-search-highlight]") - marks.forEach(mark => { - const parent = mark.parentNode - if (parent) { - parent.replaceChild(document.createTextNode(mark.textContent || ""), mark) - parent.normalize() - } - }) -} - -function applySearchHighlights(container: HTMLElement, query: string): number { - clearSearchHighlights(container) - if (!query.trim()) return 0 - - const lowerQuery = query.toLowerCase() - const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT) - const nodesToProcess: { node: Text; matches: { start: number; end: number }[] }[] = [] - - let textNode: Text | null - while ((textNode = walker.nextNode() as Text | null)) { - const text = textNode.textContent || "" - const lowerText = text.toLowerCase() - const matches: { start: number; end: number }[] = [] - let searchFrom = 0 - - while (true) { - const index = lowerText.indexOf(lowerQuery, searchFrom) - if (index === -1) break - matches.push({ start: index, end: index + query.length }) - searchFrom = index + query.length - } - - if (matches.length > 0) { - nodesToProcess.push({ node: textNode, matches }) - } - } - - let totalMatches = 0 - - for (const { node, matches } of nodesToProcess) { - const text = node.textContent || "" - const fragment = document.createDocumentFragment() - let lastIndex = 0 - - for (const match of matches) { - if (match.start > lastIndex) { - fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.start))) - } - - const mark = document.createElement("mark") - mark.setAttribute("data-search-highlight", "") - mark.setAttribute("data-match-index", String(totalMatches)) - mark.className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]" - mark.textContent = text.slice(match.start, match.end) - fragment.appendChild(mark) - - totalMatches++ - lastIndex = match.end - } - - if (lastIndex < text.length) { - fragment.appendChild(document.createTextNode(text.slice(lastIndex))) - } - - node.parentNode?.replaceChild(fragment, node) - } - - return totalMatches -} - -function scrollToSearchMatch(container: HTMLElement, index: number) { - const marks = container.querySelectorAll("mark[data-search-highlight]") - marks.forEach(m => { - (m as HTMLElement).className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]" - }) - - const target = marks[index] as HTMLElement | undefined - if (target) { - target.className = "bg-orange-300 dark:bg-orange-600/70 text-inherit rounded-[2px] ring-2 ring-orange-400 dark:ring-orange-500" - target.scrollIntoView({ behavior: "smooth", block: "center" }) - } -} - -interface LLMCallDebugDialogProps { - debugData: LLMCallDebugData - title: string - model?: string | null -} - -const LLMCallDebugDialog = memo(function LLMCallDebugDialog({ - debugData, - title, - model, -}: LLMCallDebugDialogProps) { - const [open, setOpen] = useState(false) - const [activeTab, setActiveTab] = useState<"user" | "system">("user") - const [showResponse, setShowResponse] = useState(false) - const [contentReady, setContentReady] = useState(false) - const [searchOpen, setSearchOpen] = useState(false) - const [searchQuery, setSearchQuery] = useState("") - const [matchCount, setMatchCount] = useState(0) - const [currentMatch, setCurrentMatch] = useState(-1) - const contentRef = useRef(null) - const searchInputRef = useRef(null) - - useEffect(() => { - if (open) { - const timer = requestAnimationFrame(() => setContentReady(true)) - return () => cancelAnimationFrame(timer) - } else { - setContentReady(false) - setShowResponse(false) - setSearchOpen(false) - setSearchQuery("") - setMatchCount(0) - setCurrentMatch(-1) - } - }, [open]) - - useEffect(() => { - if (searchOpen) { - requestAnimationFrame(() => searchInputRef.current?.focus()) - } - }, [searchOpen]) - - useEffect(() => { - const container = contentRef.current - if (!container || !contentReady) return - - const timer = setTimeout(() => { - clearSearchHighlights(container) - if (!searchQuery.trim()) { - setMatchCount(0) - setCurrentMatch(-1) - return - } - const count = applySearchHighlights(container, searchQuery) - setMatchCount(count) - const next = count > 0 ? 0 : -1 - setCurrentMatch(next) - if (count > 0) { - scrollToSearchMatch(container, 0) - } - }, 150) - - return () => clearTimeout(timer) - }, [searchQuery, activeTab, showResponse, contentReady]) - - function navigateMatch(direction: "next" | "prev") { - if (matchCount === 0) return - const next = direction === "next" - ? (currentMatch + 1) % matchCount - : (currentMatch - 1 + matchCount) % matchCount - setCurrentMatch(next) - if (contentRef.current) scrollToSearchMatch(contentRef.current, next) - } - - function closeSearch() { - setSearchOpen(false) - setSearchQuery("") - if (contentRef.current) clearSearchHighlights(contentRef.current) - } - - const hasContent = debugData.systemPrompt || debugData.userPrompt || debugData.rawResponse - - if (!hasContent) return null - - const searchBar = searchOpen ? ( -
    - - setSearchQuery(e.target.value)} - onKeyDown={e => { - if (e.key === "Enter") { - e.preventDefault() - navigateMatch(e.shiftKey ? "prev" : "next") - } else if (e.key === "Escape") { - e.preventDefault() - e.stopPropagation() - closeSearch() - } - }} - placeholder={`Search ${activeTab === "user" ? "user" : "system"} prompt...`} - className="flex-1 bg-transparent text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none min-w-0" - /> - {searchQuery && ( - - {matchCount > 0 ? `${currentMatch + 1} of ${matchCount}` : "No matches"} - - )} -
    - - - -
    -
    - ) : ( -
    - -
    - ) - - return ( - - - - - { - if ((e.metaKey || e.ctrlKey) && e.key === "f") { - e.preventDefault() - setSearchOpen(true) - } - }} - > - -
    - - - {title} - - {/* Response debug button */} - -
    - {model && ( -
    - - {model} - -
    - )} -
    - - {showResponse ? ( - /* Raw Response View */ -
    -
    - {debugData.rawResponse ? ( -
    -                  {debugData.rawResponse}
    -                
    - ) : ( - No response - )} -
    -
    - ) : ( - /* Prompts View */ - setActiveTab(v as "user" | "system")} className="flex-1 flex flex-col min-h-0 mt-3"> - - - User Prompt - - ({(debugData.userPrompt?.length || 0).toLocaleString()} chars) - - - - System Prompt - - ({(debugData.systemPrompt?.length || 0).toLocaleString()} chars) - - - - -
    - {searchBar} -
    - {!contentReady ? ( -
    -
    -
    -
    -
    - ) : activeTab === "user" ? ( - debugData.userPrompt ? ( - - ) : ( - No user prompt - ) - ) : ( - debugData.systemPrompt ? ( -
    -                      {debugData.systemPrompt}
    -                    
    - ) : ( - No system prompt - ) - )} -
    -
    - - )} - -
    - ) -}) - -const DiffView = memo(function DiffView({ diff }: { diff: string }) { - const lines = diff.split("\n") - - function shouldSkipLine(line: string, index: number): boolean { - // Skip empty last line - if (index === lines.length - 1 && line === "") return true - - // Skip standalone + or - markers or empty additions/deletions - const isEmptyAddition = line.startsWith("+") && line.substring(1).trim() === "" - const isEmptyDeletion = line.startsWith("-") && line.substring(1).trim() === "" - if (line === "+" || line === "-" || isEmptyAddition || isEmptyDeletion) return true - - // Skip "No newline" markers - if (line.startsWith("\\ No newline") || line.startsWith("\\")) return true - - return false - } - - function getDiffLineStyle(line: string) { - if (line.startsWith("@@")) { - return { - bgClass: "bg-blue-900/30", - textClass: "text-blue-400", - lineContent: line, - indicator: null, - borderClass: "border-transparent" - } - } - - if (line.startsWith("+")) { - return { - bgClass: "bg-green-900/40", - textClass: "text-green-300", - lineContent: line.substring(1), - indicator: +, - borderClass: "border-green-500" - } - } - - if (line.startsWith("-")) { - return { - bgClass: "bg-red-900/40", - textClass: "text-red-300", - lineContent: line.substring(1), - indicator: , - borderClass: "border-red-500" - } - } - - if (line.startsWith(" ")) { - return { - bgClass: "", - textClass: "text-zinc-300", - lineContent: line.substring(1), - indicator: null, - borderClass: "border-transparent" - } - } - - return { - bgClass: "", - textClass: "text-zinc-300", - lineContent: line, - indicator: null, - borderClass: "border-transparent" - } - } - - return ( -
    - {lines.map((line, index) => { - if (shouldSkipLine(line, index)) return null - - const { bgClass, textClass, lineContent, indicator, borderClass } = getDiffLineStyle(line) - - return ( -
    -
    - {indicator} -
    -
    -              {lineContent || " "}
    -            
    -
    - ) - })} -
    - ) -}) - -type SideBySideRow = { - leftLine: string | null - leftNum: number | null - leftType: "removed" | "context" | "empty" - rightLine: string | null - rightNum: number | null - rightType: "added" | "context" | "empty" -} - -const SideBySideDiffView = memo(function SideBySideDiffView({ - originalCode, - candidateCode, -}: { - originalCode: string - candidateCode: string - language: string -}) { - const [rows, setRows] = useState(null) - - useEffect(() => { - import("diff").then(({ diffLines }) => { - const changes = diffLines(originalCode, candidateCode) - const result: SideBySideRow[] = [] - let leftNum = 1 - let rightNum = 1 - - for (let i = 0; i < changes.length; i++) { - const change = changes[i] - const lines = change.value.replace(/\n$/, "").split("\n") - - if (!change.added && !change.removed) { - // Context lines - for (const line of lines) { - result.push({ - leftLine: line, leftNum: leftNum++, leftType: "context", - rightLine: line, rightNum: rightNum++, rightType: "context", - }) - } - } else if (change.removed) { - // Check if next change is an addition (paired change) - const next = i + 1 < changes.length ? changes[i + 1] : null - if (next && next.added) { - const removedLines = lines - const addedLines = next.value.replace(/\n$/, "").split("\n") - const maxLen = Math.max(removedLines.length, addedLines.length) - - for (let j = 0; j < maxLen; j++) { - result.push({ - leftLine: j < removedLines.length ? removedLines[j] : null, - leftNum: j < removedLines.length ? leftNum++ : null, - leftType: j < removedLines.length ? "removed" : "empty", - rightLine: j < addedLines.length ? addedLines[j] : null, - rightNum: j < addedLines.length ? rightNum++ : null, - rightType: j < addedLines.length ? "added" : "empty", - }) - } - i++ // Skip the next (added) change - } else { - // Pure removal - for (const line of lines) { - result.push({ - leftLine: line, leftNum: leftNum++, leftType: "removed", - rightLine: null, rightNum: null, rightType: "empty", - }) - } - } - } else if (change.added) { - // Pure addition (not paired with a removal) - for (const line of lines) { - result.push({ - leftLine: null, leftNum: null, leftType: "empty", - rightLine: line, rightNum: rightNum++, rightType: "added", - }) - } - } - } - - setRows(result) - }) - }, [originalCode, candidateCode]) - - if (!rows) { - return ( -
    -
    -
    -
    -
    - ) - } - - function getCellStyle(type: "removed" | "added" | "context" | "empty") { - switch (type) { - case "removed": - return "bg-red-900/30 text-red-300" - case "added": - return "bg-green-900/30 text-green-300" - case "empty": - return "bg-zinc-800/30" - default: - return "text-zinc-300" - } - } - - return ( -
    -
    -
    - Original -
    -
    - {rows.map((row, i) => ( -
    - - {row.leftNum ?? ""} - -
    -                {row.leftLine ?? " "}
    -              
    -
    - ))} -
    -
    -
    -
    - Optimized -
    -
    - {rows.map((row, i) => ( -
    - - {row.rightNum ?? ""} - -
    -                {row.rightLine ?? " "}
    -              
    -
    - ))} -
    -
    -
    - ) -}) - -const TestContent = memo(function TestContent({ content }: { content: Extract }) { - const [showDetails, setShowDetails] = useState(false) - const [expandedTest, setExpandedTest] = useState(null) - const [activeVariant, setActiveVariant] = useState<"generated" | "instrumented" | "instrumentedPerf">("generated") - - const testCount = content.testGroups.length - const hasInstrumented = content.testGroups.some(g => g.instrumented) - const hasInstrumentedPerf = content.testGroups.some(g => g.instrumentedPerf) - - return ( -
    -
    -
    -
    - - - {testCount} test{testCount !== 1 ? "s" : ""} generated - -
    -
    - {content.testFramework && ( - - {content.testFramework} - - )} - {hasInstrumented && ( - - +behavior - - )} - {hasInstrumentedPerf && ( - - +perf - - )} -
    -
    - -
    - - {showDetails && ( -
    - {content.testGroups.map((group) => { - const isExpanded = expandedTest === group.index - const hasMultipleVariants = [group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length > 1 - - function getCurrentCode() { - switch (activeVariant) { - case "generated": return group.generated - case "instrumented": return group.instrumented - case "instrumentedPerf": return group.instrumentedPerf - default: return null - } - } - const currentCode = getCurrentCode() - - return ( -
    - - - {isExpanded && ( -
    - {hasMultipleVariants && ( -
    - {group.generated && ( - - )} - {group.instrumented && ( - - )} - {group.instrumentedPerf && ( - - )} -
    - )} - -
    - {currentCode ? ( - - ) : ( -
    - {(() => { - switch (activeVariant) { - case "generated": return "No generated test available" - case "instrumented": return "No instrumented behavior test available" - case "instrumentedPerf": return "No instrumented perf test available" - default: return "No test available" - } - })()} -
    - )} -
    -
    - )} -
    - ) - })} -
    - )} -
    - ) -}) - -const CandidateContent = memo(function CandidateContent({ - content, - isActive, -}: { - content: Extract - isActive: boolean -}) { - const [viewMode, setViewMode] = useState<"code" | "diff" | "side-by-side">("diff") - const [selectedFileIndex, setSelectedFileIndex] = useState(0) - const [unifiedDiff, setUnifiedDiff] = useState(null) - const [diffLoading, setDiffLoading] = useState(false) - - const originalCode = content.type === "refinement" ? content.parentCode : content.originalCode - - const candidateFiles = useMemo(() => parseAllCodeBlocks(content.code), [content.code]) - const originalFiles = useMemo(() => originalCode ? parseAllCodeBlocks(originalCode) : [], [originalCode]) - - const selectedCandidateFile = candidateFiles[selectedFileIndex] || candidateFiles[0] - - const matchingOriginalFile = useMemo(() => { - if (!selectedCandidateFile || originalFiles.length === 0) return null - return findMatchingFile(originalFiles, selectedCandidateFile.path) - }, [selectedCandidateFile, originalFiles]) - - useEffect(() => { - setUnifiedDiff(null) - }, [selectedFileIndex]) - - useEffect(() => { - if (viewMode !== "diff" || !matchingOriginalFile || !selectedCandidateFile || unifiedDiff !== null) { - return - } - - setDiffLoading(true) - import("diff").then(({ createTwoFilesPatch }) => { - const filename = selectedCandidateFile.filename || matchingOriginalFile.filename || "code.py" - const diff = createTwoFilesPatch( - `a/${filename}`, - `b/${filename}`, - matchingOriginalFile.code, - selectedCandidateFile.code, - "", - "", - { context: 3 } - ) - - const lines = diff.split("\n") - const hunkStartIndex = lines.findIndex(line => line.startsWith("@@")) - setUnifiedDiff(hunkStartIndex > 0 ? lines.slice(hunkStartIndex).join("\n") : diff) - setDiffLoading(false) - }).catch(error => { - console.error("Failed to load diff library:", error) - setDiffLoading(false) - }) - }, [viewMode, matchingOriginalFile, selectedCandidateFile, unifiedDiff]) - - const hasDiff = matchingOriginalFile !== null - const hasMultipleFiles = candidateFiles.length > 1 - - const codeContainerStyle = useMemo( - () => ({ maxHeight: isActive ? "80vh" : "200px" }), - [isActive] - ) - - return ( -
    -
    - {content.rank != null && ( - - #{content.rank} - - )} - {content.isBest && ( - - Best - - )} -
    - - {content.explanation && ( -

    - {content.explanation} -

    - )} - -
    - {hasDiff && ( -
    - - - -
    - )} - - {hasMultipleFiles && ( - - )} -
    - - {viewMode === "code" ? ( - selectedCandidateFile ? ( -
    -
    -
    - - - {selectedCandidateFile.filename || "Code"} - - {selectedCandidateFile.path && selectedCandidateFile.path !== selectedCandidateFile.filename && ( - - ({selectedCandidateFile.path}) - - )} -
    - - {selectedCandidateFile.code.split("\n").length} lines - -
    -
    - -
    -
    - ) : ( -
    - No code available -
    - ) - ) : viewMode === "side-by-side" ? ( - matchingOriginalFile && selectedCandidateFile ? ( -
    - -
    - ) : ( -
    - No original code available for comparison -
    - ) - ) : diffLoading ? ( -
    -
    -
    -
    -
    -
    -
    - ) : unifiedDiff ? ( -
    - -
    - ) : ( -
    - No original code available for comparison -
    - )} -
    - ) -}) - -const RankingContent = memo(function RankingContent({ content }: { content: Extract }) { - return ( -
    - {content.explanation && ( -
    -

    - {content.explanation} -

    -
    - )} - - {content.rankings.length >= 1 && ( -
    - {content.rankings.map((item) => ( -
    -
    - - {item.label} - - - Rank #{item.rank} - - {item.isBest && ( - - Best - - )} - {item.isBest && content.usedForPr && ( - - Used for PR - - )} -
    -
    - -
    -
    - ))} -
    - )} - -
    - ) -}) - -const SummaryContent = memo(function SummaryContent({ content }: { content: Extract }) { - const { metrics } = content - return ( -
    -
    -
    Total Duration
    -
    - {formatTime(metrics.totalDuration)} -
    -
    -
    -
    Total Cost
    -
    - ${metrics.totalCost.toFixed(4)} -
    -
    -
    -
    Total Tokens
    -
    - {metrics.totalTokens.toLocaleString()} -
    -
    -
    -
    Candidates
    -
    - {metrics.candidatesCount} -
    -
    -
    - ) -}) - -const TimelineSectionCard = memo(function TimelineSectionCard({ - section, - isActive, - index, - totalSections, -}: { - section: TimelineSection - isActive: boolean - index: number - totalSections: number -}) { - const config = TYPE_CONFIG[section.type] - const Icon = config.icon - - return ( -
    -
    - -
    -
    - - +{formatTime(section.timestamp)} - - {section.duration && ( - <> - · - - {formatTime(section.duration)} - - - )} -
    - - {index + 1}/{totalSections} - -
    - -
    -
    -
    - -
    -

    - {section.title} -

    - {section.subtitle && ( -

    - {section.subtitle} -

    - )} -
    -
    - {getStatusIcon(section.status)} - {section.model && ( - - {section.model} - - )} - {section.cost != null && ( - - ${section.cost.toFixed(4)} - - )} - {section.debugData && ( - - )} -
    -
    -
    - -
    - {section.content.type === "tests" && } - {(section.content.type === "candidate" || section.content.type === "refinement") && ( - - )} - {section.content.type === "ranking" && } - {section.content.type === "summary" && } -
    -
    -
    -
    - ) -}) - export const TimelinePageView = memo(function TimelinePageView({ sections, totalDuration, @@ -1373,44 +20,58 @@ export const TimelinePageView = memo(function TimelinePageView({ }: TimelinePageViewProps) { const [activeIndex, setActiveIndex] = useState(0) const sectionRefs = useRef<(HTMLDivElement | null)[]>([]) - const rafId = useRef(null) + const refCallbacks = useRef(new Map void>()) + + function getSectionRef(index: number) { + let cb = refCallbacks.current.get(index) + if (!cb) { + cb = (el: HTMLDivElement | null) => { sectionRefs.current[index] = el } + refCallbacks.current.set(index, cb) + } + return cb + } useEffect(() => { - const handleScroll = () => { - if (rafId.current !== null) return - rafId.current = requestAnimationFrame(() => { - rafId.current = null - const scrollTarget = window.innerHeight * 0.35 + const observer = new IntersectionObserver( + (entries) => { + let bestIndex = -1 + let bestDistance = Infinity - let closestIndex = 0 - let closestDistance = Infinity + for (const entry of entries) { + if (!entry.isIntersecting) continue - sectionRefs.current.forEach((ref, index) => { - if (ref) { - const rect = ref.getBoundingClientRect() - const sectionMiddle = rect.top + rect.height / 2 - const distance = Math.abs(sectionMiddle - scrollTarget) + const index = Number(entry.target.getAttribute("data-section-index")) + if (isNaN(index)) continue - if (distance < closestDistance) { - closestDistance = distance - closestIndex = index - } + // Find section closest to 35% from top of viewport + const rect = entry.boundingClientRect + const sectionMiddle = rect.top + rect.height / 2 + const distance = Math.abs(sectionMiddle - window.innerHeight * 0.35) + + if (distance < bestDistance) { + bestDistance = distance + bestIndex = index } - }) + } - setActiveIndex(closestIndex) - }) - } - - window.addEventListener("scroll", handleScroll, { passive: true }) - handleScroll() - return () => { - window.removeEventListener("scroll", handleScroll) - if (rafId.current !== null) { - cancelAnimationFrame(rafId.current) - rafId.current = null + if (bestIndex >= 0) { + setActiveIndex(bestIndex) + } + }, + { + threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0], + rootMargin: "-10% 0px -55% 0px", } - } + ) + + sectionRefs.current.forEach((ref, index) => { + if (ref) { + ref.setAttribute("data-section-index", String(index)) + observer.observe(ref) + } + }) + + return () => observer.disconnect() }, [sections.length]) if (sections.length === 0) { @@ -1422,7 +83,9 @@ export const TimelinePageView = memo(function TimelinePageView({ } const activeSection = sections[activeIndex] - const shouldExpandContainer = activeSection?.content.type === "candidate" || activeSection?.content.type === "refinement" + const shouldExpandContainer = + activeSection?.content.type === "candidate" || + activeSection?.content.type === "refinement" return (
    @@ -1442,7 +105,7 @@ export const TimelinePageView = memo(function TimelinePageView({
    - {activeIndex + 1} of {sections.length} · {formatTime(totalDuration)} + {activeIndex + 1} of {sections.length} {"\u00b7"} {formatTime(totalDuration)}
    @@ -1456,7 +119,11 @@ export const TimelinePageView = memo(function TimelinePageView({
    -
    +
    @@ -1466,7 +133,7 @@ export const TimelinePageView = memo(function TimelinePageView({ {sections.map((section, index) => (
    { sectionRefs.current[index] = el }} + ref={getSectionRef(index)} className="scroll-mt-24" >
    ) -}) \ No newline at end of file +}) diff --git a/js/cf-webapp/src/app/observability/components/timeline-section-card.tsx b/js/cf-webapp/src/app/observability/components/timeline-section-card.tsx new file mode 100644 index 000000000..762b0e4b1 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/timeline-section-card.tsx @@ -0,0 +1,133 @@ +"use client" + +import { memo } from "react" +import { + Clock, + CheckCircle2, + XCircle, + AlertCircle, +} from "lucide-react" +import { TYPE_CONFIG, formatTime } from "./timeline-helpers" +import { LLMCallDebugDialog } from "./llm-call-debug-dialog" +import { TestContent } from "./test-content" +import { CandidateContent } from "./candidate-content" +import { RankingContent, SummaryContent } from "./ranking-content" +import type { TimelineSection } from "./timeline-types" + +function getStatusIcon(status: string): JSX.Element { + switch (status) { + case "success": + return + case "failed": + return + case "partial": + return + default: + return + } +} + +export const TimelineSectionCard = memo(function TimelineSectionCard({ + section, + isActive, + index, + totalSections, +}: { + section: TimelineSection + isActive: boolean + index: number + totalSections: number +}) { + const config = TYPE_CONFIG[section.type] + const Icon = config.icon + + return ( +
    +
    + +
    +
    + + +{formatTime(section.timestamp)} + + {section.duration && ( + <> + {"\u00b7"} + + {formatTime(section.duration)} + + + )} +
    + + {index + 1}/{totalSections} + +
    + +
    +
    +
    + +
    +

    + {section.title} +

    + {section.subtitle && ( +

    + {section.subtitle} +

    + )} +
    +
    + {getStatusIcon(section.status)} + {section.model && ( + + {section.model} + + )} + {section.cost != null && ( + + ${section.cost.toFixed(4)} + + )} + {section.debugData && ( + + )} +
    +
    +
    + +
    + {section.content.type === "tests" && ( + + )} + {(section.content.type === "candidate" || + section.content.type === "refinement") && ( + + )} + {section.content.type === "ranking" && ( + + )} + {section.content.type === "summary" && ( + + )} +
    +
    +
    +
    + ) +}) diff --git a/js/cf-webapp/src/app/observability/components/timeline-types.ts b/js/cf-webapp/src/app/observability/components/timeline-types.ts index e055a0960..d919f307b 100644 --- a/js/cf-webapp/src/app/observability/components/timeline-types.ts +++ b/js/cf-webapp/src/app/observability/components/timeline-types.ts @@ -1,7 +1,5 @@ export interface LLMCallDebugData { - systemPrompt: string | null - userPrompt: string | null - rawResponse: string | null + callId: string } export interface TimelineSection { @@ -44,9 +42,6 @@ export interface TransformInput { total_tokens: number | null created_at: Date context: { call_sequence?: number } | null - system_prompt?: string | null - user_prompt?: string | null - raw_response?: string | null }> optimizationCandidates: Array<{ id: string @@ -78,50 +73,99 @@ export interface TransformInput { usedForPr: boolean } -export function transformToTimelineSections(input: TransformInput): { sections: TimelineSection[]; totalDuration: number } { - const { calls, optimizationCandidates, lineProfilerCandidates, refinementCandidates, generatedTests, instrumentedTests, instrumentedPerfTests, originalCode, testFramework, candidateRankMap, bestCandidateId, rankingExplanation, usedForPr } = input +export function transformToTimelineSections( + input: TransformInput +): { sections: TimelineSection[]; totalDuration: number } { + const { + calls, + optimizationCandidates, + lineProfilerCandidates, + refinementCandidates, + generatedTests, + instrumentedTests, + instrumentedPerfTests, + originalCode, + testFramework, + candidateRankMap, + bestCandidateId, + rankingExplanation, + usedForPr, + } = input if (calls.length === 0) { return { sections: [], totalDuration: 0 } } - const timestamps = calls.map(c => new Date(c.created_at).getTime()) - const minTime = Math.min(...timestamps) - const maxTime = Math.max(...timestamps) - const maxLatency = Math.max(...calls.map(c => c.latency_ms ?? 0)) + // Pre-compute timestamps to avoid duplicate new Date() calls + const timestampMap = new Map() + for (const call of calls) { + timestampMap.set(call.id, new Date(call.created_at).getTime()) + } + + // Calculate timeline duration + let minTime = Infinity + let maxTime = -Infinity + let maxLatency = 0 + + for (const call of calls) { + const timestamp = timestampMap.get(call.id)! + if (timestamp < minTime) minTime = timestamp + if (timestamp > maxTime) maxTime = timestamp + + const latency = call.latency_ms ?? 0 + if (latency > maxLatency) maxLatency = latency + } + const totalDuration = maxTime - minTime + maxLatency const sections: TimelineSection[] = [] + // Build test groups const maxTestIndex = Math.max( generatedTests.length, instrumentedTests.length, instrumentedPerfTests.length ) + const genMap = new Map(generatedTests.map(t => [t.index, t])) + const instrMap = new Map(instrumentedTests.map(t => [t.index, t])) + const instrPerfMap = new Map(instrumentedPerfTests.map(t => [t.index, t])) + const testGroups: TestGroup[] = [] for (let i = 1; i <= maxTestIndex; i++) { - const generated = generatedTests.find(t => t.index === i) - const instrumented = instrumentedTests.find(t => t.index === i) - const instrumentedPerf = instrumentedPerfTests.find(t => t.index === i) + const generated = genMap.get(i) + const instrumented = instrMap.get(i) + const instrumentedPerf = instrPerfMap.get(i) if (generated || instrumented || instrumentedPerf) { testGroups.push({ index: i, - generated: generated ? { code: generated.code, lines: generated.code.split("\n").length } : undefined, - instrumented: instrumented ? { code: instrumented.code, lines: instrumented.code.split("\n").length } : undefined, - instrumentedPerf: instrumentedPerf ? { code: instrumentedPerf.code, lines: instrumentedPerf.code.split("\n").length } : undefined, + generated: generated + ? { code: generated.code, lines: generated.code.split("\n").length } + : undefined, + instrumented: instrumented + ? { code: instrumented.code, lines: instrumented.code.split("\n").length } + : undefined, + instrumentedPerf: instrumentedPerf + ? { code: instrumentedPerf.code, lines: instrumentedPerf.code.split("\n").length } + : undefined, }) } } + // Process test generation calls const testCalls = calls.filter(c => c.call_type === "test_generation") + if (testCalls.length > 0 || testGroups.length > 0) { const firstTestCall = testCalls[0] - const firstTimestamp = firstTestCall ? new Date(firstTestCall.created_at).getTime() - minTime : 0 + const firstTimestamp = firstTestCall + ? timestampMap.get(firstTestCall.id)! - minTime + : 0 + const totalTestDuration = testCalls.reduce((sum, c) => sum + (c.latency_ms ?? 0), 0) const totalTestCost = testCalls.reduce((sum, c) => sum + (c.llm_cost ?? 0), 0) const totalTestTokens = testCalls.reduce((sum, c) => sum + (c.total_tokens ?? 0), 0) + const allSuccess = testCalls.length === 0 || testCalls.every(c => c.status === "success") const anyFailed = testCalls.some(c => c.status === "failed") @@ -145,17 +189,44 @@ export function transformToTimelineSections(input: TransformInput): { sections: testGroups, testFramework: testFramework ?? undefined, }, - debugData: firstTestCall ? { - systemPrompt: firstTestCall.system_prompt ?? null, - userPrompt: firstTestCall.user_prompt ?? null, - rawResponse: firstTestCall.raw_response ?? null, - } : undefined, + debugData: firstTestCall ? { callId: firstTestCall.id } : undefined, }) } + const allCandidates = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates] + const allCandidatesById = new Map(allCandidates.map(c => [c.id, c])) + + // Pre-compute ranking data (identical for all ranking calls) + const precomputedRankings = Object.entries(candidateRankMap) + .sort(([, a], [, b]) => a - b) + .map(([id]) => { + const cand = allCandidatesById.get(id) + if (!cand) return null + + const source = (cand as { source?: string }).source + let prefix: string + if (source === "REFINE") { + prefix = "Refinement" + } else if (source === "OPTIMIZE_LP") { + prefix = "LP Candidate" + } else { + prefix = "Candidate" + } + + return { + id, + rank: 0, + label: `${prefix} ${cand.index}`, + code: cand.code, + isBest: false, + } + }) + .filter((r): r is NonNullable => r !== null) + .map((r, index) => ({ ...r, rank: index + 1, isBest: index === 0 })) + const callIndexByType = new Map() for (const call of calls) { - const timestamp = new Date(call.created_at).getTime() - minTime + const timestamp = timestampMap.get(call.id)! - minTime const callType = call.call_type || "unknown" const typeIndex = callIndexByType.get(callType) ?? 0 callIndexByType.set(callType, typeIndex + 1) @@ -183,11 +254,7 @@ export function transformToTimelineSections(input: TransformInput): { sections: rank, isBest: candidate.id === bestCandidateId, }, - debugData: { - systemPrompt: call.system_prompt ?? null, - userPrompt: call.user_prompt ?? null, - rawResponse: call.raw_response ?? null, - }, + debugData: { callId: call.id }, }) } } else if (callType === "line_profiler") { @@ -214,11 +281,7 @@ export function transformToTimelineSections(input: TransformInput): { sections: rank, isBest: candidate.id === bestCandidateId, }, - debugData: { - systemPrompt: call.system_prompt ?? null, - userPrompt: call.user_prompt ?? null, - rawResponse: call.raw_response ?? null, - }, + debugData: { callId: call.id }, }) } } else if (callType === "refinement") { @@ -226,12 +289,17 @@ export function transformToTimelineSections(input: TransformInput): { sections: const candidate = refinementCandidates[refIndex] if (candidate) { const rank = candidateRankMap[candidate.id] - const parentCandidate = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates].find(c => c.id === candidate.parentId) - const parentLabel = parentCandidate - ? (parentCandidate as { source?: string }).source === "REFINE" + const parentCandidate = candidate.parentId + ? allCandidatesById.get(candidate.parentId) + : undefined + + let parentLabel: string | undefined + if (parentCandidate) { + const source = (parentCandidate as { source?: string }).source + parentLabel = source === "REFINE" ? `From Refinement ${parentCandidate.index}` : `From Candidate ${parentCandidate.index}` - : undefined + } sections.push({ id: call.id, type: "refinement", @@ -251,27 +319,10 @@ export function transformToTimelineSections(input: TransformInput): { sections: rank, isBest: candidate.id === bestCandidateId, }, - debugData: { - systemPrompt: call.system_prompt ?? null, - userPrompt: call.user_prompt ?? null, - rawResponse: call.raw_response ?? null, - }, + debugData: { callId: call.id }, }) } } else if (callType === "ranking") { - const allCandidates = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates] - const rankings = Object.entries(candidateRankMap) - .sort(([, a], [, b]) => a - b) - .map(([id]) => { - const cand = allCandidates.find(c => c.id === id) - if (!cand) return null - const source = (cand as { source?: string }).source - const prefix = source === "REFINE" ? "Refinement" : source === "OPTIMIZE_LP" ? "LP Candidate" : "Candidate" - return { id, rank: 0, label: `${prefix} ${cand.index}`, code: cand.code, isBest: false } - }) - .filter((r): r is NonNullable => r !== null) - .map((r, index) => ({ ...r, rank: index + 1, isBest: index === 0 })) - sections.push({ id: call.id, type: "ranking", @@ -286,14 +337,10 @@ export function transformToTimelineSections(input: TransformInput): { sections: content: { type: "ranking", explanation: rankingExplanation ?? "", - rankings, + rankings: precomputedRankings, usedForPr, }, - debugData: { - systemPrompt: call.system_prompt ?? null, - userPrompt: call.user_prompt ?? null, - rawResponse: call.raw_response ?? null, - }, + debugData: { callId: call.id }, }) } } @@ -307,15 +354,17 @@ export function transformToTimelineSections(input: TransformInput): { sections: summary: 5, } + const candidateTypeSet = new Set(["optimization", "line_profiler", "refinement"]) + const sectionSortIndex = new Map( + sections.map(s => [s, parseInt(s.title.match(/\d+$/)?.[0] ?? "0", 10)]) + ) + sections.sort((a, b) => { const orderA = typeOrder[a.type] ?? 99 const orderB = typeOrder[b.type] ?? 99 if (orderA !== orderB) return orderA - orderB - const candidateTypes = ["optimization", "line_profiler", "refinement"] - if (candidateTypes.includes(a.type)) { - const indexA = parseInt(a.title.match(/\d+$/)?.[0] ?? "0", 10) - const indexB = parseInt(b.title.match(/\d+$/)?.[0] ?? "0", 10) - return indexA - indexB + if (candidateTypeSet.has(a.type)) { + return sectionSortIndex.get(a)! - sectionSortIndex.get(b)! } return a.timestamp - b.timestamp }) diff --git a/js/cf-webapp/src/app/observability/lib/get-trace-data.ts b/js/cf-webapp/src/app/observability/lib/get-trace-data.ts index 57a17e522..b28ab97a5 100644 --- a/js/cf-webapp/src/app/observability/lib/get-trace-data.ts +++ b/js/cf-webapp/src/app/observability/lib/get-trace-data.ts @@ -7,6 +7,18 @@ export const getTraceData = unstable_cache( prisma.llm_calls.findMany({ where: { trace_id: { startsWith: tracePrefix } }, orderBy: { created_at: "asc" }, + select: { + id: true, + trace_id: true, + call_type: true, + model_name: true, + status: true, + latency_ms: true, + llm_cost: true, + total_tokens: true, + created_at: true, + context: true, + }, }), prisma.optimization_errors.findMany({ where: { trace_id: { startsWith: tracePrefix } }, diff --git a/js/cf-webapp/src/app/observability/llm-export/route.ts b/js/cf-webapp/src/app/observability/llm-export/route.ts index 87497b8e0..fdd9378d1 100644 --- a/js/cf-webapp/src/app/observability/llm-export/route.ts +++ b/js/cf-webapp/src/app/observability/llm-export/route.ts @@ -112,9 +112,6 @@ export async function GET(request: NextRequest) { total_tokens: call.total_tokens, created_at: call.created_at, context: call.context as { call_sequence?: number } | null, - system_prompt: call.system_prompt, - user_prompt: call.user_prompt, - raw_response: call.raw_response, })) const { sections, totalDuration } = transformToTimelineSections({ diff --git a/js/cf-webapp/src/app/observability/page.tsx b/js/cf-webapp/src/app/observability/page.tsx index f737d68a7..4bd23f0e7 100644 --- a/js/cf-webapp/src/app/observability/page.tsx +++ b/js/cf-webapp/src/app/observability/page.tsx @@ -157,9 +157,6 @@ function TraceContent({ traceId, traceData }: { traceId: string; traceData: Trac total_tokens: call.total_tokens, created_at: call.created_at, context: call.context as { call_sequence?: number } | null, - system_prompt: call.system_prompt, - user_prompt: call.user_prompt, - raw_response: call.raw_response, })) const { sections, totalDuration } = transformToTimelineSections({ diff --git a/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx b/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx index 3f9ec4448..885501e31 100644 --- a/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx +++ b/js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx @@ -1,6 +1,5 @@ import Link from "next/link" import { notFound } from "next/navigation" -import { unstable_cache } from "next/cache" import { CheckCircle, Timer, @@ -12,8 +11,8 @@ import { AlertCircle, XCircle, } from "lucide-react" -import { prisma } from "@/lib/prisma" import { getCallSource } from "@/lib/observability-utils" +import { getTraceData } from "@/app/observability/lib/get-trace-data" import { HelpButton } from "@/components/observability/help-button" import { InfoIcon } from "@/components/observability/info-icon" import { CopyButton } from "@/components/observability/copy-button" @@ -26,31 +25,6 @@ interface TracePageProps { } } -const getTraceData = unstable_cache( - async (tracePrefix: string) => { - const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([ - prisma.llm_calls.findMany({ - where: { trace_id: { startsWith: tracePrefix } }, - orderBy: { created_at: "asc" }, - }), - prisma.optimization_errors.findMany({ - where: { trace_id: { startsWith: tracePrefix } }, - orderBy: { created_at: "asc" }, - }), - prisma.optimization_features.findFirst({ - where: { trace_id: { startsWith: tracePrefix } }, - }), - prisma.optimization_events.findFirst({ - where: { trace_id: { startsWith: tracePrefix } }, - select: { event_type: true }, - }), - ]) - return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } - }, - ["trace-detail"], - { revalidate: 60 }, -) - export default async function TracePage({ params }: TracePageProps) { const { trace_id } = params diff --git a/js/cf-webapp/src/app/observability/traces/page.tsx b/js/cf-webapp/src/app/observability/traces/page.tsx index 2a9a514b9..ba8c2c4d2 100644 --- a/js/cf-webapp/src/app/observability/traces/page.tsx +++ b/js/cf-webapp/src/app/observability/traces/page.tsx @@ -33,39 +33,46 @@ const getUniqueOrganizations = unstable_cache( { revalidate: 300 }, // 5 minutes ) +// Cached function to get trace IDs for an organization +// Shared between getTotalTracesCount and main query to avoid duplicate DB calls +const getOrgTraceIds = unstable_cache( + async (organization: string) => { + const orgFeatures = await prisma.optimization_features.findMany({ + where: { organization }, + select: { trace_id: true }, + distinct: ["trace_id"], + }) + return orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] + }, + ["org-trace-ids"], + { revalidate: 30 }, +) + // Optimized function to count distinct trace_ids using groupBy const getTotalTracesCount = unstable_cache( async (traceIdFilter: string | undefined, organizationFilter: string | undefined) => { // Get trace IDs filtered by organization if specified let traceIdPrefixes: string[] = [] if (organizationFilter) { - const orgFeatures = await prisma.optimization_features.findMany({ - where: { organization: organizationFilter }, - select: { trace_id: true }, - distinct: ["trace_id"], - }) - traceIdPrefixes = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] + traceIdPrefixes = await getOrgTraceIds(organizationFilter) if (traceIdPrefixes.length === 0) return 0 } // Build where clause const where: Prisma.llm_callsWhereInput = {} - + if (traceIdFilter) { where.trace_id = { contains: traceIdFilter } } else if (traceIdPrefixes.length > 0) { - // Use IN clause for exact trace ID matches - much more efficient than OR with startsWith where.trace_id = { in: traceIdPrefixes } } // Use groupBy for efficient distinct count - // Filter out null trace_ids in the result const result = await prisma.llm_calls.groupBy({ by: ["trace_id"], where, }) - // Filter out null trace_ids return result.filter(r => r.trace_id !== null).length }, ["traces-count"], @@ -78,24 +85,14 @@ export default async function TracesPage({ searchParams }: { searchParams: Searc const pageSize = 50 const skip = (page - 1) * pageSize - // Get trace IDs filtered by organization if specified + // Get trace IDs filtered by organization if specified (uses shared cached function) let filteredTraceIds: string[] = [] if (searchParams.organization) { - const orgFeatures = await prisma.optimization_features.findMany({ - where: { organization: searchParams.organization }, - select: { trace_id: true }, - distinct: ["trace_id"], - }) - filteredTraceIds = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[] + filteredTraceIds = await getOrgTraceIds(searchParams.organization) // If organization filter is applied but no traces found, return empty result early if (filteredTraceIds.length === 0) { - const uniqueOrganizations = await prisma.optimization_features.findMany({ - select: { organization: true }, - distinct: ["organization"], - where: { organization: { not: null } }, - }) - const orgs = uniqueOrganizations.map(f => f.organization).filter(Boolean).sort() as string[] + const orgs = await getUniqueOrganizations() // Return early with empty results return ( @@ -292,6 +289,7 @@ export default async function TracesPage({ searchParams }: { searchParams: Searc total_cost: number total_tokens: number failed_calls: number + hasPartial: boolean status: string call_types: Set } @@ -309,8 +307,8 @@ export default async function TracesPage({ searchParams }: { searchParams: Searc existing.total_cost += callCost existing.total_tokens += callTokens if (call.status === "failed") existing.failed_calls++ + if (call.status === "partial_success") existing.hasPartial = true if (call.call_type) existing.call_types.add(call.call_type) - // Use Math.min/Math.max to ensure correct first/last timestamps existing.first_seen = new Date(Math.min(existing.first_seen.getTime(), callTimestamp)) existing.last_seen = new Date(Math.max(existing.last_seen.getTime(), callTimestamp)) } else { @@ -322,6 +320,7 @@ export default async function TracesPage({ searchParams }: { searchParams: Searc total_cost: callCost, total_tokens: callTokens, failed_calls: call.status === "failed" ? 1 : 0, + hasPartial: call.status === "partial_success", status: call.status || "unknown", call_types: new Set(call.call_type ? [call.call_type] : []), }) @@ -536,10 +535,7 @@ export default async function TracesPage({ searchParams }: { searchParams: Searc 0, (trace.last_seen.getTime() - trace.first_seen.getTime()) / 1000, ) - // Determine status: check if any call has partial_success, failed, or all success - const hasPartial = - trace.status === "partial_success" || - llmCalls.some(c => c.trace_id === trace.trace_id && c.status === "partial_success") + const hasPartial = trace.hasPartial const statusColor = trace.failed_calls > 0 ? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" diff --git a/js/cf-webapp/src/components/observability/observability-nav.tsx b/js/cf-webapp/src/components/observability/observability-nav.tsx index fb9b540c4..be62d2dda 100644 --- a/js/cf-webapp/src/components/observability/observability-nav.tsx +++ b/js/cf-webapp/src/components/observability/observability-nav.tsx @@ -18,13 +18,21 @@ export function ObservabilityNav() { const router = useRouter() const [viewMode, setViewMode] = useState("classic") - // Initialize view mode from localStorage + // Sync view mode with the current pathname useEffect(() => { - const storedMode = localStorage.getItem("observability-view-mode") as ViewMode | null - if (storedMode === "timeline" || storedMode === "classic") { - setViewMode(storedMode) + if (pathname === "/observability") { + setViewMode("timeline") + localStorage.setItem("observability-view-mode", "timeline") + } else if (pathname.startsWith("/observability/traces") || pathname.startsWith("/observability/llm-calls")) { + setViewMode("classic") + localStorage.setItem("observability-view-mode", "classic") + } else { + const storedMode = localStorage.getItem("observability-view-mode") as ViewMode | null + if (storedMode === "timeline" || storedMode === "classic") { + setViewMode(storedMode) + } } - }, []) + }, [pathname]) // Handle view mode changes const handleViewModeChange = (mode: ViewMode) => { From ad1cb9f03211348eb17cf251a276fd6700dd8d71 Mon Sep 17 00:00:00 2001 From: HeshamHM28 Date: Mon, 9 Feb 2026 10:08:47 -0800 Subject: [PATCH 063/184] [Fix] Fallback to staging if we fail to create PR (#2332) Fixes CF-1037 --- js/cf-api/endpoints/create-pr.ts | 74 ++++++++++++------- js/cf-api/endpoints/suggest-pr-changes.ts | 64 +++++++++++----- .../endpoints/tests/create-pr.unit.test.ts | 5 ++ .../tests/suggest-pr-changes.unit.test.ts | 21 ++++-- 4 files changed, 111 insertions(+), 53 deletions(-) diff --git a/js/cf-api/endpoints/create-pr.ts b/js/cf-api/endpoints/create-pr.ts index 8688b9baf..243824a04 100644 --- a/js/cf-api/endpoints/create-pr.ts +++ b/js/cf-api/endpoints/create-pr.ts @@ -1,4 +1,3 @@ -import * as Sentry from "@sentry/node" import { fileDiffsToMap, isDiffContentsWellFormed } from "../diff_utils.js" import { type FileDiffContent } from "@codeflash-ai/code-suggester/build/src/types.js" import { userNickname } from "../auth0-mgmt.js" @@ -44,6 +43,7 @@ import { } from "../github/optimization_approval.js" import { registerRepositoryAndMember } from "./utils/github-repo-setup.js" import { logger } from "../utils/logger.js" +import { saveStagingReview } from "./create-staging.js" // Helper function to parse speedup values (same as create-staging.ts) function parseSpeedupValue(value: unknown, suffix: "x" | "%"): number | null { @@ -339,8 +339,8 @@ export async function createPr(req: Request, res: Response) { return res.status(403).json({ error: errorMsg }) } if ( - !(await isBranchExists({ owner, repo, branch: baseBranch, installationOctokit })) && - isCfWebApp + isCfWebApp && + !(await isBranchExists({ owner, repo, branch: baseBranch, installationOctokit })) ) { res.status(404).json({ error: `The base branch "${baseBranch}" does not exist in ${owner}/${repo}. Please verify the branch name and try again.`, @@ -358,10 +358,9 @@ export async function createPr(req: Request, res: Response) { logger.errorWithSentry( `Error in background upsertRepoAndCreateMember:`, req, - {}, + { owner, repo, nickname, userId }, err as Error, ) - Sentry.captureException(err) }) if (traceId && dependencies.requiresApproval(owner, repo) && !isCfWebApp) { @@ -505,22 +504,55 @@ export async function createPr(req: Request, res: Response) { return res.json(prNumber) } else { + // If PR creation failed, fallback to staging + if (traceId) { + logger.info(`PR creation failed, falling back to staging for traceId: ${traceId}`, req) + try { + const stagingResult = await saveStagingReview(req.body, userId, organizationId, (req as any).subscriptionInfo) + if (stagingResult.status === 200) { + return res.status(200).json({ + message: "PR creation failed, staging created as fallback", + ...stagingResult.data, + }) + } + // Handle non-200 staging response - return staging's actual status/error + logger.errorWithSentry( + `Staging fallback returned status ${stagingResult.status}`, + req, + { reqBody: req.body, userId, traceId, stagingResult }, + new Error(`Staging fallback returned status ${stagingResult.status}`) + ) + return res.status(stagingResult.status).json({ + message: "PR creation failed and staging fallback also failed", + ...stagingResult.data, + }) + } catch (stagingError) { + logger.errorWithSentry( + `Staging fallback threw an exception:`, + req, + { reqBody: req.body, userId, traceId }, + stagingError as Error + ) + return res.status(500).json({ + message: "PR creation failed and staging fallback threw an error", + error: stagingError instanceof Error ? stagingError.message : String(stagingError), + }) + } + } return res.status(500).send("Error creating pull request") } } catch (error) { logger.errorWithSentry( - `Error in /cfapi/create-pr: ${error}`, + `Error in /cfapi/create-pr`, req, { errorMessage: error instanceof Error ? error.message : String(error), + reqBody: req.body, + userId: (req as any).userId, }, error as Error, ) if (error instanceof Error) { - //add exception to sentry - Sentry.captureException("Create-PR: " + error.message, { - extra: { reqBody: req.body, userId: (req as any).userId }, - }) // Capture error in PostHog dependencies.posthog?.capture({ distinctId: (req as any).userId, @@ -837,32 +869,23 @@ export async function triggerCreatePr( return newPrData.data.number } catch (error) { logger.errorWithSentry( - `Error creating PR for ${owner}/${repo}:`, + `Error creating PR for ${owner}/${repo}`, { userId, endpoint: "/cfapi/create-pr", operation: "trigger_create_pr", owner, repo, + traceId, }, { - traceId, nickname, + baseBranch, + functionName: prCommentFields?.function_name, errorMessage: error instanceof Error ? error.message : String(error), }, error as Error, ) - Sentry.captureException("triggerCreatePr: " + error, { - extra: { - traceId, - userId, - nickname, - owner, - repo, - baseBranch, - functionName: prCommentFields?.function_name, - }, - }) return -1 } } @@ -923,12 +946,11 @@ export async function addRepositoryManually(req: AuthorizedUserReq, res: Respons logger.info(`Repository upserted: ${repo.full_name}`, req) } catch (error) { logger.errorWithSentry( - `Failed to add/reactivate repository ${repo.full_name}:`, + `Failed to add/reactivate repository ${repo.full_name}`, req, - {}, + { repoId: repo.id, repoFullName: repo.full_name }, error as Error, ) - Sentry.captureException(error) } } diff --git a/js/cf-api/endpoints/suggest-pr-changes.ts b/js/cf-api/endpoints/suggest-pr-changes.ts index 4485cbfc1..bf433e46e 100644 --- a/js/cf-api/endpoints/suggest-pr-changes.ts +++ b/js/cf-api/endpoints/suggest-pr-changes.ts @@ -1,4 +1,3 @@ -import * as Sentry from "@sentry/node" import { determineValidHunks, fileDiffsToMap, isDiffContentsWellFormed } from "../diff_utils.js" import { userNickname } from "../auth0-mgmt.js" import { logger } from "../utils/logger.js" @@ -59,6 +58,7 @@ export interface SuggestPrChangesDependencies { createDependentPullRequest: typeof createDependentPullRequest sendSlackMessage: typeof sendSlackMessage updateOptimizationEvent: typeof updateOptimizationEvent + saveStagingReview: typeof saveStagingReview } // Default dependencies @@ -80,6 +80,7 @@ let dependencies: SuggestPrChangesDependencies = { createDependentPullRequest, sendSlackMessage, updateOptimizationEvent, + saveStagingReview, } // For testing - allow dependency injection @@ -106,6 +107,7 @@ export function resetSuggestPrChangesDependencies() { createDependentPullRequest, sendSlackMessage, updateOptimizationEvent, + saveStagingReview, } } @@ -268,12 +270,11 @@ export async function suggestPrChanges( ) .catch(err => { logger.errorWithSentry( - `Error in background upsertRepoAndCreateMember:`, + `Error in background upsertRepoAndCreateMember`, req, - {}, + { owner, repo, nickname, userId }, err as Error, ) - Sentry.captureException(err) }) // Check if approval is required if (traceId && dependencies.requiresApproval(owner, repo)) { @@ -419,25 +420,48 @@ export async function suggestPrChanges( } return res.json(result) } catch (error: any) { - // Re-throw AppExceptions to be handled by GlobalExceptionHandler - if (error && typeof error === "object" && "getHttpStatus" in error) { - throw error + // Try to fallback to staging if we have a traceId + const traceId = req.body.traceId || "" + if (traceId) { + logger.info(`PR suggestion failed, falling back to staging for traceId: ${traceId}`, req) + try { + const stagingResult = await dependencies.saveStagingReview(req.body, req.userId, req.organizationId, req.subscriptionInfo) + if (stagingResult.status === 200) { + return res.status(200).json({ + message: "PR suggestion failed, staging created as fallback", + ...stagingResult.data, + }) + } + // Handle non-200 staging response - return staging's actual status/error + logger.errorWithSentry( + `Staging fallback returned status ${stagingResult.status}`, + req, + { reqBody: req.body, userId: req.userId, traceId, stagingResult }, + new Error(`Staging fallback returned status ${stagingResult.status}`) + ) + return res.status(stagingResult.status).json({ + message: "PR suggestion failed and staging fallback also failed", + ...stagingResult.data, + }) + } catch (stagingError) { + logger.errorWithSentry( + `Staging fallback threw an exception`, + req, + { reqBody: req.body, userId: req.userId, traceId }, + stagingError as Error + ) + return res.status(500).json({ + message: "PR suggestion failed and staging fallback threw an error", + error: stagingError instanceof Error ? stagingError.message : String(stagingError), + }) + } } - logger.errorWithSentry( - `Error in /cfapi/suggest-pr-changes: ${error}`, - req, - { - errorMessage: error.message, - }, - error as Error, - ) + + logger.errorWithSentry(`Error in /cfapi/suggest-pr-changes: ${error}`, req, { errorMessage: error.message }, error as Error) dependencies.posthog.capture({ distinctId: req.userId, event: `cfapi-suggest-pr-changes-failed-error`, - properties: { - error: error.message, - stack: error.stack, - }, + properties: { error: error.message, stack: error.stack }, }) throw internalServerError(`Error creating pull request: ${error.message}`) } @@ -552,7 +576,7 @@ export async function triggerSuggestPrChanges( !dependencies.requiresApproval(owner, repo) && traceId ) { - const result = await saveStagingReview( + const result = await dependencies.saveStagingReview( { owner, repo, diff --git a/js/cf-api/endpoints/tests/create-pr.unit.test.ts b/js/cf-api/endpoints/tests/create-pr.unit.test.ts index c230310c3..cb2482196 100644 --- a/js/cf-api/endpoints/tests/create-pr.unit.test.ts +++ b/js/cf-api/endpoints/tests/create-pr.unit.test.ts @@ -492,6 +492,8 @@ describe("createPr", () => { it("should return 500 when PR creation fails", async () => { mockDependencies.triggerCreatePr.mockResolvedValue(-1) + // Remove traceId to test direct error path (staging fallback requires traceId) + mockReq.body.traceId = undefined await createPr(mockReq, mockRes) @@ -501,6 +503,8 @@ describe("createPr", () => { it("should return 500 when PR creation returns 0", async () => { mockDependencies.triggerCreatePr.mockResolvedValue(0) + // Remove traceId to test direct error path (staging fallback requires traceId) + mockReq.body.traceId = undefined await createPr(mockReq, mockRes) @@ -521,6 +525,7 @@ describe("createPr", () => { expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining("Error in /cfapi/create-pr"), + expect.anything(), ) expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({ distinctId: "test-user-id", diff --git a/js/cf-api/endpoints/tests/suggest-pr-changes.unit.test.ts b/js/cf-api/endpoints/tests/suggest-pr-changes.unit.test.ts index 5bbdb80bd..a394728d4 100644 --- a/js/cf-api/endpoints/tests/suggest-pr-changes.unit.test.ts +++ b/js/cf-api/endpoints/tests/suggest-pr-changes.unit.test.ts @@ -61,6 +61,7 @@ describe("Suggest PR Changes", () => { createDependentPullRequest: jest.fn(), sendSlackMessage: jest.fn(), updateOptimizationEvent: jest.fn(), + saveStagingReview: jest.fn(), } setSuggestPrChangesDependencies(mockDependencies) @@ -212,18 +213,24 @@ describe("Suggest PR Changes", () => { mockDependencies.buildPrCommentBody.mockReturnValue("Test comment body") }) - it("should return 403 for rejected requests", async () => { + it("should fallback to staging for rejected requests", async () => { mockDependencies.requiresApproval.mockReturnValue(true) mockDependencies.prisma.optimization_features.findUnique.mockResolvedValue({ approval_status: "rejected", }) - - await expect( - suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response), - ).rejects.toMatchObject({ - message: expect.stringContaining("rejected"), + mockDependencies.saveStagingReview.mockResolvedValue({ + status: 200, + data: { stagingId: "test-staging-id" }, + }) + + await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response) + + expect(mockDependencies.saveStagingReview).toHaveBeenCalled() + expect(mockRes.status).toHaveBeenCalledWith(200) + expect(mockRes.json).toHaveBeenCalledWith({ + message: "PR suggestion failed, staging created as fallback", + stagingId: "test-staging-id", }) - expect(mockRes.status).not.toHaveBeenCalled() }) it("should proceed with approved requests", async () => { From db973a0487f0d0b9669cb2e2b1248230e3d9f0d3 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:05:19 -0500 Subject: [PATCH 064/184] =?UTF-8?q?fix:=20relax=20testgen=20assertion=20ru?= =?UTF-8?q?le=20to=20allow=20imports=20from=20function=20depe=E2=80=A6=20(?= =?UTF-8?q?#2388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ndencies The old rule ("NOT in libraries such as numpy, pandas etc.") forced LLMs to reinvent helpers like np.allclose using slow / inaccurate Python loops. The new rule allows assertions from packages already imported by the function under test. # Pull Request Checklist ## Description - [ ] **Description of PR**: Clear and concise description of what this PR accomplishes - [ ] **Breaking Changes**: Document any breaking changes (if applicable) - [ ] **Related Issues**: Link to any related issues or tickets ## Testing - [ ] **Test cases Attached**: All relevant test cases have been added/updated - [ ] **Manual Testing**: Manual testing completed for the changes ## Monitoring & Debugging - [ ] **Logging in place**: Appropriate logging has been added for debugging user issues - [ ] **Sentry will be able to catch errors**: Error handling ensures Sentry can capture and report errors - [ ] **Avoid Dev based/Prisma logging**: No development-only or Prisma-specific logging in production code ## Configuration - [ ] **Env variables newly added**: Any new environment variables are documented in .env.example file or mentioned in description --- ## Additional Notes --- .../core/languages/python/testgen/execute_async_user_prompt.md | 2 +- .../core/languages/python/testgen/execute_user_prompt.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django/aiservice/core/languages/python/testgen/execute_async_user_prompt.md b/django/aiservice/core/languages/python/testgen/execute_async_user_prompt.md index fed999a83..634fa11cd 100644 --- a/django/aiservice/core/languages/python/testgen/execute_async_user_prompt.md +++ b/django/aiservice/core/languages/python/testgen/execute_async_user_prompt.md @@ -1,4 +1,4 @@ -Using Python and the `{unit_test_package}` package, write a suite of unit tests for the **async** function '{function_name}'. ONLY include assert/raise statements present in the Python library and NOT in libraries such as numpy, pandas etc. Include helpful comments to explain each line. +Using Python and the `{unit_test_package}` package, write a suite of unit tests for the **async** function '{function_name}'. Use only assertions from the Python standard library or from packages already imported by the function under test. Include helpful comments to explain each line. Reply with a single Python code block in this exact format: diff --git a/django/aiservice/core/languages/python/testgen/execute_user_prompt.md b/django/aiservice/core/languages/python/testgen/execute_user_prompt.md index bfa2c302e..f4cb62137 100644 --- a/django/aiservice/core/languages/python/testgen/execute_user_prompt.md +++ b/django/aiservice/core/languages/python/testgen/execute_user_prompt.md @@ -1,4 +1,4 @@ -Using Python and the `{unit_test_package}` package, write a suite of unit tests for the function '{function_name}'. ONLY include assert/raise statements present in the Python library and NOT in libraries such as numpy, pandas etc. Include helpful comments to explain each line. +Using Python and the `{unit_test_package}` package, write a suite of unit tests for the function '{function_name}'. Use only assertions from the Python standard library or from packages already imported by the function under test. Include helpful comments to explain each line. Reply with a single Python code block in this exact format: From 899db4ed569570893b6d51bd71dbefab64e09a4e Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Tue, 10 Feb 2026 02:52:10 +0530 Subject: [PATCH 065/184] Fix logging msg on webhook and misc (#2387) --- js/cf-api/github/github-app.ts | 286 ++++++++++------------ js/cf-api/middlewares/enhanced-logging.ts | 105 ++++---- js/cf-api/routes/webhook.routes.ts | 42 ++-- 3 files changed, 208 insertions(+), 225 deletions(-) diff --git a/js/cf-api/github/github-app.ts b/js/cf-api/github/github-app.ts index 16dfe65b1..df61f7e73 100644 --- a/js/cf-api/github/github-app.ts +++ b/js/cf-api/github/github-app.ts @@ -8,6 +8,7 @@ import { import { posthog } from "../analytics.js" import * as Sentry from "@sentry/node" +import { logger } from "../utils/logger.js" import { createAppInstallation, createOrUpdateUser, @@ -22,6 +23,17 @@ import { getUserRole } from "./github-utils.js" const APP_USER_ID: number = parseInt(process.env.GH_APP_USER_ID ?? "0") // GitHub App User ID +/** Extract common webhook context fields for structured logging */ +function webhookContext(payload: any, operation: string) { + return { + repoOwner: payload?.repository?.owner?.login, + repoName: payload?.repository?.name, + prNumber: payload?.pull_request?.number, + installationId: payload?.installation?.id, + operation, + } +} + // Create a function to get config without initializing the app async function getGithubAppConfig() { const APP_ID: string = process.env.GH_APP_ID ?? "" @@ -65,9 +77,9 @@ export const githubApp = await (async () => { const GH_APP_ID = process.env.GH_APP_ID if (!GH_APP_ID || GH_APP_ID === "" || process.env.NODE_ENV === "test") { - console.log("caution: GitHub App not configured (GH_APP_ID missing)") - console.log("caution: PR creation and GitHub webhook features are disabled") - console.log("caution: CLI and optimization features will continue to work") + logger.warn("GitHub App not configured (GH_APP_ID missing)", { operation: "server_startup" }) + logger.warn("PR creation and GitHub webhook features are disabled", { operation: "server_startup" }) + logger.info("CLI and optimization features will continue to work", { operation: "server_startup" }) // Return a minimal mock that won't fail return { @@ -89,10 +101,10 @@ export const githubApp = await (async () => { } // In other environments, initialize normally - console.log(`GitHub App ID ${GH_APP_ID} detected, initializing...`) + logger.info(`GitHub App ID ${GH_APP_ID} detected, initializing...`, { operation: "server_startup" }) const app = await initializeApp() - console.log(`Github App Initialized`) + logger.info("GitHub App initialized", { operation: "server_startup" }) const { data } = await app.octokit.request("/app") // Read more about custom logging: https://github.com/octokit/core.js#logging @@ -100,7 +112,11 @@ export const githubApp = await (async () => { app.webhooks.onAny(async ({ id, name, payload }) => { // Only log event type and ID, not full payload (too verbose) - console.log(`Github App: Received webhook event ${name} (${id})`) + logger.info("GitHub App: Received webhook event", { + operation: "webhook_received", + repoOwner: (payload as any)?.repository?.owner?.login, + repoName: (payload as any)?.repository?.name, + }, { eventType: name, eventId: id }) posthog?.capture({ distinctId: `github|${payload.sender?.id}`, event: `cfapi-github-webhook-received`, @@ -111,7 +127,7 @@ export const githubApp = await (async () => { }) }) - console.log(`Github App Authenticated as '${data.name}'`) + logger.info(`GitHub App authenticated as '${data.name}'`, { operation: "server_startup" }) app.webhooks.on("installation", async ({ octokit, payload }) => { const account = payload.installation.account @@ -121,9 +137,7 @@ export const githubApp = await (async () => { : account && "slug" in account ? account.slug : "unknown" - console.log( - `Received a new installation event: installation_id=${payload.installation.id}, account=${accountName}`, - ) + logger.info(`Received installation event: installation_id=${payload.installation.id}, account=${accountName}`, webhookContext(payload, "installation")) // Create an installation access token const installationAccessToken = await octokit.rest.apps.createInstallationAccessToken({ installation_id: payload.installation.id, @@ -132,15 +146,11 @@ export const githubApp = await (async () => { }) app.webhooks.on("pull_request.opened", async ({ octokit, payload }) => { - console.log( - `Received a pull request opened event: PR #${payload.pull_request?.number} in ${payload.repository?.full_name}`, - ) + logger.info(`Received pull_request.opened event: PR #${payload.pull_request?.number} in ${payload.repository?.full_name}`, webhookContext(payload, "pull_request_opened")) }) app.webhooks.on("pull_request.edited", async ({ octokit, payload }) => { - console.log( - `Received a pull request edited event: PR #${payload.pull_request?.number} in ${payload.repository?.full_name}`, - ) + logger.info(`Received pull_request.edited event: PR #${payload.pull_request?.number} in ${payload.repository?.full_name}`, webhookContext(payload, "pull_request_edited")) }) app.webhooks.on("pull_request.closed", async ({ octokit, payload }) => { @@ -167,82 +177,75 @@ export const githubApp = await (async () => { }) } - console.log( - `Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"} and removed line profiler data`, - ) + logger.info(`Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"} and removed line profiler data`, webhookContext(payload, "pull_request_closed")) } catch (err) { - console.error(`Failed to update optimization_event for PR ID ${prId}:`, err) + logger.error(`Failed to update optimization_event for PR ID ${prId}:`, webhookContext(payload, "pull_request_closed"), {}, err as Error) } - console.log( - `Received a pull request closed event. PR #${payload.pull_request.number} ` + - `by ${payload.pull_request.user.login} was closed.`, - ) + logger.info(`Received pull_request.closed event: PR #${payload.pull_request.number} by ${payload.pull_request.user.login} was closed`, webhookContext(payload, "pull_request_closed")) // Check if the PR was merged and is a PR created by Codeflash const is_user_code_flash = payload.pull_request.user.id === APP_USER_ID - if (payload.pull_request.merged && is_user_code_flash) { - // Extract the original PR number from the branch name - const dependentBranchNamePattern = /codeflash.optimize-pr(\d+)-\d{4}-\d{2}-\d{2}T.+$/ - const standaloneBranchNamePattern = /codeflash.optimize-(.+)-\d{4}-\d{2}-\d{2}T.+$/ - const dependentPrMatch = dependentBranchNamePattern.exec(payload.pull_request.head.ref) - const standalonePrMatch = standaloneBranchNamePattern.exec(payload.pull_request.head.ref) - if (dependentPrMatch != null) { - const originalPrNumber = parseInt(dependentPrMatch[1]) - let username = "You" - if (payload.pull_request.merged_by != null) { - // should not be null, but check anyway - username = `@${payload.pull_request.merged_by.login}` - } - // Comment on the original PR - await octokit.rest.issues.createComment({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: originalPrNumber, - body: `This PR is now faster! 🚀 ${username} accepted my optimizations from: + try { + if (payload.pull_request.merged && is_user_code_flash) { + // Extract the original PR number from the branch name + const dependentBranchNamePattern = /codeflash.optimize-pr(\d+)-\d{4}-\d{2}-\d{2}T.+$/ + const standaloneBranchNamePattern = /codeflash.optimize-(.+)-\d{4}-\d{2}-\d{2}T.+$/ + const dependentPrMatch = dependentBranchNamePattern.exec(payload.pull_request.head.ref) + const standalonePrMatch = standaloneBranchNamePattern.exec(payload.pull_request.head.ref) + if (dependentPrMatch != null) { + const originalPrNumber = parseInt(dependentPrMatch[1]) + let username = "You" + if (payload.pull_request.merged_by != null) { + // should not be null, but check anyway + username = `@${payload.pull_request.merged_by.login}` + } + // Comment on the original PR + await octokit.rest.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: originalPrNumber, + body: `This PR is now faster! 🚀 ${username} accepted my optimizations from: - #${payload.pull_request.number}`, - }) + }) - posthog?.capture({ - distinctId: `github|${payload.sender.id}`, // this is the user who merged the PR - event: `cfapi-github-dependent-pr-merged`, - properties: { - owner: payload.repository.owner.login, - repo: payload.repository.name, - originalPrNumber, - dependentPrNumber: payload.pull_request.number, - mergedBy: payload.pull_request.merged_by?.login, - }, - }) - console.log( - `Commented on original PR #${originalPrNumber} and logged the event to Posthog.`, - ) - } else if (standalonePrMatch != null) { - posthog?.capture({ - distinctId: `github|${payload.sender.id}`, - event: `cfapi-github-standalone-pr-merged`, - properties: { - owner: payload.repository.owner.login, - repo: payload.repository.name, - functionName: standalonePrMatch[1], - prNumber: payload.pull_request.number, - mergedBy: payload.pull_request.merged_by?.login, - }, - }) - console.log( - `Logged standalone PR #${payload.pull_request.number} merge event to Posthog.`, - ) + posthog?.capture({ + distinctId: `github|${payload.sender.id}`, // this is the user who merged the PR + event: `cfapi-github-dependent-pr-merged`, + properties: { + owner: payload.repository.owner.login, + repo: payload.repository.name, + originalPrNumber, + dependentPrNumber: payload.pull_request.number, + mergedBy: payload.pull_request.merged_by?.login, + }, + }) + logger.info(`Commented on original PR #${originalPrNumber} and logged the event to PostHog`, webhookContext(payload, "dependent_pr_merged")) + } else if (standalonePrMatch != null) { + posthog?.capture({ + distinctId: `github|${payload.sender.id}`, + event: `cfapi-github-standalone-pr-merged`, + properties: { + owner: payload.repository.owner.login, + repo: payload.repository.name, + functionName: standalonePrMatch[1], + prNumber: payload.pull_request.number, + mergedBy: payload.pull_request.merged_by?.login, + }, + }) + logger.info(`Logged standalone PR #${payload.pull_request.number} merge event to PostHog`, webhookContext(payload, "standalone_pr_merged")) + } } + } catch (mergedPrError) { + logger.errorWithSentry("Failed to process merged PR comment/analytics", webhookContext(payload, "pull_request_closed"), {}, mergedPrError as Error) } // Close any open optimization PRs targeting the branch of the closed PR // Ensure we only close PRs that are targeting the branch of the PR that was just closed const closedPrBranch = payload.pull_request.head.ref // Logic to close any open optimization PRs targeting this branch - console.log(`Closing optimization PRs targeting branch ${closedPrBranch}...`) + logger.info(`Closing optimization PRs targeting branch ${closedPrBranch}`, webhookContext(payload, "close_dependent_prs")) if (payload.installation === undefined) { - console.error( - `Error! Installation ID is missing from payload. Cannot close PRs for this installation!`, - ) + logger.error("Installation ID is missing from payload. Cannot close PRs for this installation!", webhookContext(payload, "close_dependent_prs")) return } try { @@ -267,11 +270,8 @@ export const githubApp = await (async () => { pull_number: pr.number, state: "closed", }) - console.log( - `Closed optimization PR #${pr.number} targeting branch '${closedPrBranch}' ` + - `because original PR #${payload.pull_request.number} by ${payload.pull_request.user.login} was closed.`, - ) - console.log(`Posting pull request comment...`) + logger.info(`Closed optimization PR #${pr.number} targeting branch '${closedPrBranch}' because original PR #${payload.pull_request.number} by ${payload.pull_request.user.login} was closed`, webhookContext(payload, "close_dependent_prs")) + logger.info("Posting pull request comment...", webhookContext(payload, "close_dependent_prs")) await octokit.rest.issues.createComment({ owner: payload.repository.owner.login, repo: payload.repository.name, @@ -281,22 +281,18 @@ export const githubApp = await (async () => { `by ${payload.pull_request.user.login} was closed.`, }) - // Proceed to delete the branch - if (is_user_code_flash) { - await deleteBranchIfExists(installationOctokit, payload, `heads/${pr.head.ref}`) - } + // Delete the optimization PR's branch (safe: pr.user.id === APP_USER_ID verified above) + await deleteBranchIfExists(installationOctokit, payload, pr.head.ref) } } - // If there was no open PR's, still delete the branch in case of inline comment + // If the closed PR itself was created by Codeflash, delete its branch too. + // Guard is correct here: we must NOT delete a user's feature branch. if (is_user_code_flash) { await deleteBranchIfExists(installationOctokit, payload, closedPrBranch) } } catch (error) { - console.error( - `Failed to close optimization PRs targeting branch ${closedPrBranch}: ${error}`, - ) - Sentry.captureException(error) + logger.errorWithSentry(`Failed to close optimization PRs targeting branch ${closedPrBranch}`, webhookContext(payload, "close_dependent_prs"), {}, error as Error) } } }) @@ -310,20 +306,16 @@ export const githubApp = await (async () => { : account && "slug" in account ? account.slug : "unknown" - console.log( - `Received a installation.created event: installation_id=${payload.installation.id}, account=${accountName}`, - ) + logger.info(`Received installation.created event: installation_id=${payload.installation.id}, account=${accountName}`, webhookContext(payload, "installation_created")) }) app.webhooks.on("installation_repositories.added", async ({ octokit, payload }) => { const repoCount = payload.repositories_added?.length || 0 - console.log( - `Received a installation_repositories.added event: installation_id=${payload.installation.id}, repositories_added=${repoCount}`, - ) + logger.info(`Received installation_repositories.added event: installation_id=${payload.installation.id}, repositories_added=${repoCount}`, webhookContext(payload, "installation_repositories_added")) }) app.webhooks.on("marketplace_purchase", async ({ id, name, payload }) => { - console.log(`Received a marketplace purchase event: ${name} (${id})`) + logger.info(`Received marketplace purchase event: ${name} (${id})`, webhookContext(payload, "marketplace_purchase")) posthog?.capture({ distinctId: `github|${payload.sender.id}`, event: `cfapi-github-marketplace-purchase`, @@ -336,10 +328,7 @@ export const githubApp = await (async () => { app.webhooks.on("pull_request.synchronize", async ({ octokit, payload }) => { if (payload.pull_request) { - console.log( - `Received a pull request synchronize event. PR #${payload.pull_request.number} ` + - `by ${payload.pull_request?.user?.login} was updated with new commits.`, - ) + logger.info(`Received pull_request.synchronize event: PR #${payload.pull_request.number} by ${payload.pull_request?.user?.login} was updated with new commits`, webhookContext(payload, "pull_request_synchronize")) // Retrieve the list of commits for the pull request const commits = await octokit.rest.pulls.listCommits({ owner: payload.repository.owner.login, @@ -365,7 +354,7 @@ export const githubApp = await (async () => { author: latestCommit.commit.author?.name, }, }) - console.log(`Logged co-authored commit to Posthog: ${latestCommit.sha}`) + logger.info(`Logged co-authored commit to PostHog: ${latestCommit.sha}`, webhookContext(payload, "pull_request_synchronize")) // should not be null, but check anyway const authorname = latestCommit.commit.author?.name ?? "You" @@ -376,9 +365,7 @@ export const githubApp = await (async () => { issue_number: payload.pull_request.number, body: `This PR is now faster! 🚀 ${authorname} accepted my code suggestion above.`, }) - console.log( - `Commented on PR #${payload.pull_request.number} about the accepted review comment.`, - ) + logger.info(`Commented on PR #${payload.pull_request.number} about the accepted review comment`, webhookContext(payload, "pull_request_synchronize")) } } }) @@ -413,13 +400,11 @@ export const githubApp = await (async () => { const feedbackContent = mentionMatch[1].trim() if (!feedbackContent) { - console.log(`Empty feedback received from ${commentAuthor.login}, ignoring`) + logger.info(`Empty feedback received from ${commentAuthor.login}, ignoring`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber }) return } - console.log( - `Received feedback (${commentType}) from ${commentAuthor.login} on PR #${prNumber}: "${feedbackContent.substring(0, 100)}..."`, - ) + logger.info(`Received feedback (${commentType}) from ${commentAuthor.login} on PR #${prNumber}: "${feedbackContent.substring(0, 100)}..."`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber }) // Helper to add reaction based on comment type const addReaction = async (content: "+1") => { @@ -450,7 +435,7 @@ export const githubApp = await (async () => { const prId = String(pr.data.id) const prUrl = pr.data.html_url - console.log(`Looking for optimization event with pr_id=${prId} or pr_url=${prUrl}`) + logger.info(`Looking for optimization event with pr_id=${prId} or pr_url=${prUrl}`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber }) // Find optimization events by PR ID or by PR URL const optimizationEvent = await prisma.optimization_events.findFirst({ @@ -471,16 +456,12 @@ export const githubApp = await (async () => { }) if (!optimizationEvent) { - console.log( - `No optimization event found for PR #${prNumber} in ${repository.full_name} (pr_id=${prId})`, - ) + logger.info(`No optimization event found for PR #${prNumber} in ${repository.full_name} (pr_id=${prId})`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber }) await addReaction("+1") return } - console.log( - `Found optimization event: id=${optimizationEvent.id}, trace_id=${optimizationEvent.trace_id}`, - ) + logger.info(`Found optimization event: id=${optimizationEvent.id}, trace_id=${optimizationEvent.trace_id}`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber }) // Create or get the user const user = await createOrUpdateUser( @@ -515,9 +496,7 @@ export const githubApp = await (async () => { }) }) - console.log( - `Saved feedback from ${commentAuthor.login} for optimization event ${optimizationEvent.id} (trace_id: ${optimizationEvent.trace_id})`, - ) + logger.info(`Saved feedback from ${commentAuthor.login} for optimization event ${optimizationEvent.id} (trace_id: ${optimizationEvent.trace_id})`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber }) // Log to PostHog posthog?.capture({ @@ -542,13 +521,12 @@ export const githubApp = await (async () => { // React with a thumbs up to acknowledge the feedback await addReaction("+1") } catch (error) { - console.error(`Failed to process feedback from ${commentAuthor.login}:`, error) - Sentry.captureException(error) + logger.errorWithSentry(`Failed to process feedback from ${commentAuthor.login}`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber }, {}, error as Error) try { await addReaction("+1") } catch (reactionError) { - console.error("Failed to add reaction:", reactionError) + logger.error("Failed to add reaction:", { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber }, {}, reactionError as Error) } } } @@ -586,48 +564,37 @@ export const githubApp = await (async () => { // Optional: Handle errors app.webhooks.onError(error => { - console.error(`Error occurred in Github App's onError handler: ${error}`) + const errorContext = { operation: "webhook_error" } + logger.error(`Error occurred in GitHub App's onError handler: ${error}`, errorContext) if (error instanceof Error) { // Check if it's an AggregateError, common for signature issues if (error.name === "AggregateError" && Array.isArray((error as any).errors)) { - console.error("AggregateError details (possible secret mismatch or multiple issues):") + logger.error("AggregateError details (possible secret mismatch or multiple issues):", errorContext) ;(error as any).errors.forEach((subError: Error, i: number) => { - console.error(` Sub-error ${i + 1}: ${subError.message}`) + logger.error(` Sub-error ${i + 1}: ${subError.message}`, errorContext) }) } else if (error.message.includes("content length")) { - console.error( - "Content length mismatch detected by Octokit. Payload may be truncated or header incorrect.", - ) + logger.error("Content length mismatch detected by Octokit. Payload may be truncated or header incorrect.", errorContext) const eventRequest = (error as any).event?.request if (eventRequest && eventRequest.headers) { - console.error( - "Request headers from error.event:", - JSON.stringify(eventRequest.headers, null, 2), - ) + logger.error("Request headers from error.event:", errorContext, { headers: JSON.stringify(eventRequest.headers, null, 2) }) } } // Log the full error structure for better debugging - console.error( - "Full error object (onError):", - JSON.stringify(error, Object.getOwnPropertyNames(error), 2), - ) + logger.error("Full error object (onError):", errorContext, { errorDetails: JSON.stringify(error, Object.getOwnPropertyNames(error), 2) }) } else { - console.error("Full error (onError, non-Error instance):", error) + logger.error("Full error (onError, non-Error instance):", errorContext, { errorDetails: String(error) }) } Sentry.captureException(error) }) app.webhooks.on("installation_repositories", async ({ payload }) => { const repoCount = payload.repositories_added?.length || 0 - console.log( - `Received a installation_repositories event: installation_id=${payload.installation?.id}, repositories_added=${repoCount}`, - ) + logger.info(`Received installation_repositories event: installation_id=${payload.installation?.id}, repositories_added=${repoCount}`, webhookContext(payload, "installation_repositories")) const { repositories_added, installation, sender } = payload // Check if required fields are missing if (!repositories_added || !installation?.id) { - console.log( - `Missing repositories_added or installation.id. Full payload: ${JSON.stringify(payload, null, 2)}`, - ) + logger.warn("Missing repositories_added or installation.id", webhookContext(payload, "installation_repositories")) return } const account = installation.account @@ -650,7 +617,7 @@ export const githubApp = await (async () => { } if (!accountLogin) { - console.error("Error: Account login or slug not found") + logger.error("Account login or slug not found", webhookContext(payload, "installation_repositories")) return } @@ -665,7 +632,7 @@ export const githubApp = await (async () => { account_login: accountLogin, account_type: accountType, }) - console.log(`Installation created for ID: ${installation.id}`) + logger.info(`Installation created for ID: ${installation.id}`, webhookContext(payload, "installation_repositories")) } // Process each repository in the list of added repositories @@ -675,7 +642,7 @@ export const githubApp = await (async () => { const githubUserId = sender?.id if (githubUserId) { - console.log(`GitHub User ID: ${githubUserId} triggered the event`) + logger.info(`GitHub User ID: ${githubUserId} triggered the event`, webhookContext(payload, "installation_repositories")) // Fetch the user's role using the helper // Use octokit from getInstallationOctokit for this installation const installationOctokit = await app.getInstallationOctokit(installation.id) @@ -686,7 +653,7 @@ export const githubApp = await (async () => { username: sender.login, isOrg: accountType === "Organization", }) - console.log(`Fetched user role: ${userRole}`) + logger.info(`Fetched user role: ${userRole}`, webhookContext(payload, "installation_repositories")) const user = await createOrUpdateUser( `github|${githubUserId}`, sender.login, @@ -715,7 +682,7 @@ export const githubApp = await (async () => { addedBy: user.user_id, // Indicates that this user was the first to be added . If user_id equals addedBy, it means this user installed GitHub App for this repository. }) - console.log(`Organization upserted: ${accountLogin}`) + logger.info(`Organization upserted: ${accountLogin}`, webhookContext(payload, "installation_repositories")) orgId = organization.id } } @@ -729,18 +696,17 @@ export const githubApp = await (async () => { organization_id: orgId, }) - console.log(`Repository upserted: ${savedRepo.full_name}`) + logger.info(`Repository upserted: ${savedRepo.full_name}`, webhookContext(payload, "installation_repositories")) await upsertRepositoryMember({ repository_id: savedRepo.id, user_id: user.user_id, role: userRole, }) } else { - console.error("GitHub User ID not found in sender.") + logger.error("GitHub User ID not found in sender", webhookContext(payload, "installation_repositories")) } } catch (error) { - console.error(`Failed to add/reactivate repository ${repo.full_name}:`, error) - Sentry.captureException(error) + logger.errorWithSentry(`Failed to add/reactivate repository ${repo.full_name}`, webhookContext(payload, "installation_repositories"), {}, error as Error) } } }) @@ -756,8 +722,12 @@ export const ghAppMiddleware = createNodeMiddleware(githubApp.webhooks, { }) const deleteBranchIfExists = async (installationOctokit: any, payload: any, branchName: string) => { + const ctx = { + ...webhookContext(payload, "delete_branch"), + branchName, + } try { - console.log(`Deleting the branch associated with the closed PR...`) + logger.info(`Deleting branch '${branchName}' associated with the closed PR`, ctx) // Check if the branch exists by querying the reference const ref = await installationOctokit.rest.git.getRef({ owner: payload.repository.owner.login, @@ -766,7 +736,7 @@ const deleteBranchIfExists = async (installationOctokit: any, payload: any, bran }) // If the ref is found, it means the branch exists, and you can delete it - console.log(`Check Branch exists: ${ref.data.ref}`) + logger.info(`Branch exists: ${ref.data.ref}`, ctx) // Proceed to delete the branch await installationOctokit.rest.git.deleteRef({ @@ -774,13 +744,13 @@ const deleteBranchIfExists = async (installationOctokit: any, payload: any, bran repo: payload.repository.name, ref: `heads/${branchName}`, }) - console.log(`Branch '${branchName}' has been deleted.`) + logger.info(`Branch '${branchName}' has been deleted`, ctx) } catch (error: any) { // If the branch doesn't exist or other errors occur, catch the error if (error.status === 404) { - console.log("Branch does not exist!") + logger.info(`Branch '${branchName}' does not exist`, ctx) } else { - console.error("Error checking branch existence or deleting:", error) + logger.error(`Error checking branch existence or deleting '${branchName}':`, ctx, {}, error as Error) } } } diff --git a/js/cf-api/middlewares/enhanced-logging.ts b/js/cf-api/middlewares/enhanced-logging.ts index 5486ec277..38aaf6949 100644 --- a/js/cf-api/middlewares/enhanced-logging.ts +++ b/js/cf-api/middlewares/enhanced-logging.ts @@ -105,6 +105,9 @@ export function enhancedRequestLogger(req: Request, res: Response, next: NextFun endpoint, }) + // Suppress request lifecycle logs for healthcheck/root paths to reduce noise + const isSuppressedPath = endpoint === "/healthcheck" || endpoint === "/" + // Add Sentry context and breadcrumb Sentry.setTag("correlationId", requestId) Sentry.setTag("requestId", requestId) @@ -132,63 +135,65 @@ export function enhancedRequestLogger(req: Request, res: Response, next: NextFun operation: "request_start", } - // Log request start - logger handles environment filtering automatically - // Production: INFO level (minimal info), Development: INFO level (clean details) - logger.info("Request started", baseContext, { - method: req.method, - url: req.url, - ...(req.get("User-Agent") && { userAgent: req.get("User-Agent") }), - ...(req.ip && { ip: req.ip }), - ...(req.get("Content-Length") && { contentLength: req.get("Content-Length") }), - ...(req.get("Content-Type") && { contentType: req.get("Content-Type") }), - }) - // Add tracing headers to response // SIMPLIFIED APPROACH: All headers use the same requestId value // Industry standard would use: addTracingHeaders(res, requestId, traceId, correlationId) addTracingHeaders(res, requestId, requestId, requestId) - // Log request completion when response finishes - res.on("finish", () => { - const duration = Date.now() - startTime - const responseContext: LogContext = { - ...baseContext, - operation: "request_complete", - } + if (!isSuppressedPath) { + // Log request start - logger handles environment filtering automatically + // Production: INFO level (minimal info), Development: INFO level (clean details) + logger.info("Request started", baseContext, { + method: req.method, + url: req.url, + ...(req.get("User-Agent") && { userAgent: req.get("User-Agent") }), + ...(req.ip && { ip: req.ip }), + ...(req.get("Content-Length") && { contentLength: req.get("Content-Length") }), + ...(req.get("Content-Type") && { contentType: req.get("Content-Type") }), + }) - // Determine log level based on status code and environment - if (res.statusCode >= 500) { - // Always log server errors - logger.error("Request completed with server error", responseContext, { - statusCode: res.statusCode, - duration, - ...(res.get("Content-Length") && { contentLength: res.get("Content-Length") }), - error: res.statusMessage, - method: req.method, - url: req.url, - }) - } else if (res.statusCode >= 400) { - // Always log client errors - logger.warn("Request completed with client error", responseContext, { - statusCode: res.statusCode, - duration, - ...(res.get("Content-Length") && { contentLength: res.get("Content-Length") }), - error: res.statusMessage, - method: req.method, - url: req.url, - }) - } else { - // Log successful requests - logger handles environment filtering automatically - // Production: INFO level (minimal info), Development: INFO level (clean details) - logger.info("Request completed successfully", responseContext, { - statusCode: res.statusCode, - duration, - ...(res.get("Content-Length") && { contentLength: res.get("Content-Length") }), - }) - } - }) + // Log request completion when response finishes + res.on("finish", () => { + const duration = Date.now() - startTime + const responseContext: LogContext = { + ...baseContext, + operation: "request_complete", + } - // Log request errors + // Determine log level based on status code and environment + if (res.statusCode >= 500) { + // Always log server errors + logger.error("Request completed with server error", responseContext, { + statusCode: res.statusCode, + duration, + ...(res.get("Content-Length") && { contentLength: res.get("Content-Length") }), + error: res.statusMessage, + method: req.method, + url: req.url, + }) + } else if (res.statusCode >= 400) { + // Always log client errors + logger.warn("Request completed with client error", responseContext, { + statusCode: res.statusCode, + duration, + ...(res.get("Content-Length") && { contentLength: res.get("Content-Length") }), + error: res.statusMessage, + method: req.method, + url: req.url, + }) + } else { + // Log successful requests - logger handles environment filtering automatically + // Production: INFO level (minimal info), Development: INFO level (clean details) + logger.info("Request completed successfully", responseContext, { + statusCode: res.statusCode, + duration, + ...(res.get("Content-Length") && { contentLength: res.get("Content-Length") }), + }) + } + }) + } + + // Always log request errors (even for healthcheck paths) res.on("error", (error: Error) => { const errorContext: LogContext = { ...baseContext, diff --git a/js/cf-api/routes/webhook.routes.ts b/js/cf-api/routes/webhook.routes.ts index 8e9d6213c..ce45e7c20 100644 --- a/js/cf-api/routes/webhook.routes.ts +++ b/js/cf-api/routes/webhook.routes.ts @@ -17,26 +17,40 @@ router.postAsync(ROUTES.GITHUB_WEBHOOKS, async (req: Request, res: Response, nex const deliveryId = (req.headers["x-github-delivery"] as string) || "unknown_delivery" const contentLength = req.headers["content-length"] - logger.info("Processing GitHub webhook", { + // Best-effort extract repo/PR context from raw body for structured logging + let repoOwner: string | undefined + let repoName: string | undefined + let prNumber: number | undefined + try { + const rawBody = typeof req.body === "string" ? req.body : typeof req.body === "object" && req.body ? JSON.stringify(req.body) : undefined + if (rawBody) { + const parsed = typeof req.body === "object" ? req.body : JSON.parse(rawBody) + repoOwner = parsed?.repository?.owner?.login + repoName = parsed?.repository?.name + prNumber = parsed?.pull_request?.number + } + } catch { + // Graceful degradation: body may not be JSON or not yet parsed + } + + const webhookLogContext = { requestId: req.requestId, traceId: req.traceId, operation: "github_webhook", eventType, deliveryId, - contentLength, - }) + ...(repoOwner && { repoOwner }), + ...(repoName && { repoName }), + ...(prNumber && { prNumber }), + } + + logger.info("Processing GitHub webhook", webhookLogContext, { contentLength }) try { await ghAppMiddleware(req, res, next) if (!res.headersSent) { - logger.warn("GitHub webhook middleware did not send response", { - requestId: req.requestId, - traceId: req.traceId, - operation: "github_webhook", - eventType, - deliveryId, - }) + logger.warn("GitHub webhook middleware did not send response", webhookLogContext) res .status(200) .send( @@ -46,13 +60,7 @@ router.postAsync(ROUTES.GITHUB_WEBHOOKS, async (req: Request, res: Response, nex } catch (error) { logger.error( "Error during GitHub webhook processing", - { - requestId: req.requestId, - traceId: req.traceId, - operation: "github_webhook", - eventType, - deliveryId, - }, + webhookLogContext, {}, error as Error, ) From 55f0a8b60a1098410ef34e638ae529c84f3d5dd0 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Tue, 10 Feb 2026 04:06:35 +0530 Subject: [PATCH 066/184] Restoring the ordering of webhook before parsing json (#2389) # Pull Request Checklist ## Description - [ ] **Description of PR**: Clear and concise description of what this PR accomplishes - [ ] **Breaking Changes**: Document any breaking changes (if applicable) - [ ] **Related Issues**: Link to any related issues or tickets ## Testing - [ ] **Test cases Attached**: All relevant test cases have been added/updated - [ ] **Manual Testing**: Manual testing completed for the changes ## Monitoring & Debugging - [ ] **Logging in place**: Appropriate logging has been added for debugging user issues - [ ] **Sentry will be able to catch errors**: Error handling ensures Sentry can capture and report errors - [ ] **Avoid Dev based/Prisma logging**: No development-only or Prisma-specific logging in production code ## Configuration - [ ] **Env variables newly added**: Any new environment variables are documented in .env.example file or mentioned in description --- ## Additional Notes --- .gitignore | 1 + js/cf-api/github/github-app.ts | 16 +++++++++++++--- js/cf-api/index.ts | 15 +++------------ js/cf-api/routes/index.ts | 22 ++++++++++++++++++---- js/cf-api/routes/webhook.routes.ts | 24 ++++-------------------- js/cf-api/utils/logger.ts | 9 +++++++++ 6 files changed, 48 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 955c94b53..1cd35240a 100644 --- a/.gitignore +++ b/.gitignore @@ -256,3 +256,4 @@ fabric.properties */node_modules/* /cli/experiments/js-serialization-experiment/node_modules/* +/cli/packages/codeflash/.npmrc diff --git a/js/cf-api/github/github-app.ts b/js/cf-api/github/github-app.ts index df61f7e73..4c835eaad 100644 --- a/js/cf-api/github/github-app.ts +++ b/js/cf-api/github/github-app.ts @@ -242,10 +242,14 @@ export const githubApp = await (async () => { // Close any open optimization PRs targeting the branch of the closed PR // Ensure we only close PRs that are targeting the branch of the PR that was just closed const closedPrBranch = payload.pull_request.head.ref - // Logic to close any open optimization PRs targeting this branch - logger.info(`Closing optimization PRs targeting branch ${closedPrBranch}`, webhookContext(payload, "close_dependent_prs")) + const closeCtx = webhookContext(payload, "close_dependent_prs") + logger.info(`Closing optimization PRs targeting branch ${closedPrBranch}`, closeCtx, { + is_user_code_flash, + closedPrBranch, + APP_USER_ID, + }) if (payload.installation === undefined) { - logger.error("Installation ID is missing from payload. Cannot close PRs for this installation!", webhookContext(payload, "close_dependent_prs")) + logger.error("Installation ID is missing from payload. Cannot close PRs for this installation!", closeCtx) return } try { @@ -257,6 +261,12 @@ export const githubApp = await (async () => { base: closedPrBranch, }) + logger.info(`Found ${openPrs.data.length} open PRs targeting branch ${closedPrBranch}`, closeCtx, { + openPrCount: openPrs.data.length, + openPrNumbers: openPrs.data.map(pr => pr.number).join(","), + openPrUsers: openPrs.data.map(pr => `#${pr.number}:${pr.user?.login}(id=${pr.user?.id},type=${pr.user?.type})`).join(","), + }) + for (const pr of openPrs.data) { // Check if the PR is opened by the Codeflash GitHub App and targets the same base branch as the closed PR if ( diff --git a/js/cf-api/index.ts b/js/cf-api/index.ts index c4959b6d6..228c706c1 100644 --- a/js/cf-api/index.ts +++ b/js/cf-api/index.ts @@ -11,10 +11,10 @@ import { logger } from "./utils/logger.js" import { posthog } from "./analytics.js" import { GlobalExceptionHandler } from "./exceptions/index.js" import { githubApp } from "./github/github-app.js" -import { enhancedRequestLogger, logRequestBody } from "./middlewares/enhanced-logging.js" +import { enhancedRequestLogger } from "./middlewares/enhanced-logging.js" import { registerRoutes } from "./routes/index.js" import { initializeCronJobs } from "./cron/index.js" -import { DEFAULT_PORT, JSON_BODY_LIMIT, GITHUB_WEBHOOK_PATH } from "./constants/index.js" +import { DEFAULT_PORT, GITHUB_WEBHOOK_PATH } from "./constants/index.js" // ======================================== // APPLICATION SETUP @@ -54,16 +54,7 @@ logger.info("Mounting GitHub webhook middleware", { // ROUTE REGISTRATION // ======================================== -// Register webhook routes first (before body parsers) -// Then body parser middleware -appExpress.use(express.json({ limit: JSON_BODY_LIMIT })) - -// Log request body in development -if (process.env.NODE_ENV !== "PRODUCTION") { - appExpress.use(logRequestBody) -} - -// Register all routes +// Register all routes (body parser is inside registerRoutes, after webhook routes) registerRoutes(appExpress) // ======================================== diff --git a/js/cf-api/routes/index.ts b/js/cf-api/routes/index.ts index 0ea8831c6..c2bb21a0c 100644 --- a/js/cf-api/routes/index.ts +++ b/js/cf-api/routes/index.ts @@ -1,5 +1,6 @@ +import express from "express" import { AsyncExpressApp } from "../types.js" -import { API_BASE_ROUTE } from "../constants/index.js" +import { API_BASE_ROUTE, JSON_BODY_LIMIT } from "../constants/index.js" // Route modules import { rootRoutes, publicApiRoutes } from "./public.routes.js" @@ -13,21 +14,34 @@ import userRoutes from "./user.routes.js" import { checkForValidAPIKey } from "../middlewares/check-valid-api-key.js" import { trackEndpointCalls } from "../middlewares/track-endpoint-calls.js" import { idLimiter } from "../middlewares/rate-limit.js" -import { logAuthEvent } from "../middlewares/enhanced-logging.js" +import { logAuthEvent, logRequestBody } from "../middlewares/enhanced-logging.js" /** * Register all routes on the Express application * * Route registration order: * 1. Webhook routes (must be before body parsers for raw body access) - * 2. Public routes (no authentication required) - * 3. Protected routes (require API key authentication) + * 2. Body parser middleware (express.json) + * 3. Public routes (no authentication required) + * 4. Protected routes (require API key authentication) */ export function registerRoutes(app: AsyncExpressApp): void { // ======================================== // WEBHOOK ROUTES (before body parsers) + // Webhook handlers (GitHub, Stripe) need raw body access for + // signature verification. express.json() MUST NOT run before these. // ======================================== app.use(webhookRoutes) + // ======================================== + // BODY PARSER (after webhook routes, before everything else) + // ======================================== + app.use(express.json({ limit: JSON_BODY_LIMIT })) + + // Log request body in development + if (process.env.NODE_ENV !== "PRODUCTION") { + app.use(logRequestBody) + } + // ======================================== // PUBLIC ROUTES (no authentication) // ======================================== diff --git a/js/cf-api/routes/webhook.routes.ts b/js/cf-api/routes/webhook.routes.ts index ce45e7c20..a88e08570 100644 --- a/js/cf-api/routes/webhook.routes.ts +++ b/js/cf-api/routes/webhook.routes.ts @@ -17,34 +17,18 @@ router.postAsync(ROUTES.GITHUB_WEBHOOKS, async (req: Request, res: Response, nex const deliveryId = (req.headers["x-github-delivery"] as string) || "unknown_delivery" const contentLength = req.headers["content-length"] - // Best-effort extract repo/PR context from raw body for structured logging - let repoOwner: string | undefined - let repoName: string | undefined - let prNumber: number | undefined - try { - const rawBody = typeof req.body === "string" ? req.body : typeof req.body === "object" && req.body ? JSON.stringify(req.body) : undefined - if (rawBody) { - const parsed = typeof req.body === "object" ? req.body : JSON.parse(rawBody) - repoOwner = parsed?.repository?.owner?.login - repoName = parsed?.repository?.name - prNumber = parsed?.pull_request?.number - } - } catch { - // Graceful degradation: body may not be JSON or not yet parsed - } - + // Note: req.body is NOT available here — express.json() runs AFTER webhook routes + // to preserve raw body stream for Octokit's signature verification. + // Repo/PR context is logged by the webhook handlers in github-app.ts instead. const webhookLogContext = { requestId: req.requestId, traceId: req.traceId, operation: "github_webhook", eventType, deliveryId, - ...(repoOwner && { repoOwner }), - ...(repoName && { repoName }), - ...(prNumber && { prNumber }), } - logger.info("Processing GitHub webhook", webhookLogContext, { contentLength }) + logger.info("Processing GitHub webhook", webhookLogContext, { contentLength, eventType }) try { await ghAppMiddleware(req, res, next) diff --git a/js/cf-api/utils/logger.ts b/js/cf-api/utils/logger.ts index 279f70f1f..d91318f94 100644 --- a/js/cf-api/utils/logger.ts +++ b/js/cf-api/utils/logger.ts @@ -421,6 +421,15 @@ export class Logger { if (error.stack) essentialInfo.push(`stack:${error.stack.split("\n")[0]}`) } + // Add custom context fields (repoOwner, repoName, prNumber, etc.) + if (context) { + Object.entries(context).forEach(([key, value]) => { + if (!["requestId", "endpoint", "userId", "traceId", "correlationId", "operation"].includes(key) && value !== undefined) { + essentialInfo.push(`${key}:${value}`) + } + }) + } + // Add custom metadata if (metadata) { Object.entries(metadata).forEach(([key, value]) => { From e28642cf22897281f2a1a2852a24f0d807260413 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:30:33 -0500 Subject: [PATCH 067/184] Fix FTO display showing wrong function for methods with common names (#2391) Store qualified function name (e.g., HttpInterface.__init__) and file_path in testgen metadata instead of bare function_name (__init__). Update the frontend parser to handle qualified names by splitting into class + method and searching within the correct class using both tree-sitter and regex. Prioritize the file matching filePath before searching all files. # Pull Request Checklist ## Description - [ ] **Description of PR**: Clear and concise description of what this PR accomplishes - [ ] **Breaking Changes**: Document any breaking changes (if applicable) - [ ] **Related Issues**: Link to any related issues or tickets ## Testing - [ ] **Test cases Attached**: All relevant test cases have been added/updated - [ ] **Manual Testing**: Manual testing completed for the changes ## Monitoring & Debugging - [ ] **Logging in place**: Appropriate logging has been added for debugging user issues - [ ] **Sentry will be able to catch errors**: Error handling ensures Sentry can capture and report errors - [ ] **Avoid Dev based/Prisma logging**: No development-only or Prisma-specific logging in production code ## Configuration - [ ] **Env variables newly added**: Any new environment variables are documented in .env.example file or mentioned in description --- ## Additional Notes --- .../aiservice/core/languages/js_ts/testgen.py | 3 +- .../core/languages/python/testgen/testgen.py | 3 +- .../function-to-optimize-section.tsx | 61 +++-- .../observability/components/python-parser.ts | 211 ++++++++++++++---- .../src/app/observability/llm-export/route.ts | 26 ++- js/cf-webapp/src/app/observability/page.tsx | 50 +++-- 6 files changed, 271 insertions(+), 83 deletions(-) diff --git a/django/aiservice/core/languages/js_ts/testgen.py b/django/aiservice/core/languages/js_ts/testgen.py index 0a6a7ec45..f3b34ea3a 100644 --- a/django/aiservice/core/languages/js_ts/testgen.py +++ b/django/aiservice/core/languages/js_ts/testgen.py @@ -544,7 +544,8 @@ async def testgen_javascript( test_framework=data.test_framework, metadata={ "test_timeout": data.test_timeout, - "function_to_optimize": data.function_to_optimize.function_name, + "function_to_optimize": data.function_to_optimize.qualified_name, + "file_path": data.function_to_optimize.file_path, "language": language, }, ) diff --git a/django/aiservice/core/languages/python/testgen/testgen.py b/django/aiservice/core/languages/python/testgen/testgen.py index 8399436e3..9fd0d1cae 100644 --- a/django/aiservice/core/languages/python/testgen/testgen.py +++ b/django/aiservice/core/languages/python/testgen/testgen.py @@ -540,7 +540,8 @@ async def testgen_python( test_framework=data.test_framework, metadata={ "test_timeout": data.test_timeout, - "function_to_optimize": data.function_to_optimize.function_name, + "function_to_optimize": data.function_to_optimize.qualified_name, + "file_path": data.function_to_optimize.file_path, }, ) diff --git a/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx b/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx index 6dbfa2264..2632b5a92 100644 --- a/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx +++ b/js/cf-webapp/src/app/observability/components/function-to-optimize-section.tsx @@ -44,6 +44,28 @@ function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] { return files } +function findTargetFile(files: ParsedFile[], filePath: string | null): ParsedFile | null { + if (!filePath || files.length === 0) return null + + return files.find(file => + filePath.endsWith(file.path) || + file.path.endsWith(filePath) || + file.path === filePath + ) || null +} + +async function searchAllFiles( + files: ParsedFile[], + functionName: string +): Promise> { + const searchPromises = files.map(async file => { + const location = await findFunctionInCode(file.code, functionName) + return location ? { file, location } : null + }) + + return Promise.all(searchPromises) +} + export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection({ functionName, filePath, @@ -68,34 +90,35 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection let cancelled = false - async function findFunction() { - const searchPromises = allFiles.map(async (file) => { - const location = await findFunctionInCode(file.code, functionName!) - return location ? { file, location } : null - }) + async function searchForFunction(funcName: string) { + // Try to find the function in the specified file first + const targetFile = findTargetFile(allFiles, filePath) + if (targetFile) { + const location = await findFunctionInCode(targetFile.code, funcName) + if (!cancelled && location) { + setFunctionLocation(location) + setActualFile(targetFile) + return + } + } - const results = await Promise.all(searchPromises) + // Search all files in parallel + const searchResults = await searchAllFiles(allFiles, funcName) if (cancelled) return - const found = results.find(r => r !== null) + const found = searchResults.find(result => result !== null) if (found) { setFunctionLocation(found.location) setActualFile(found.file) - return + } else { + // Use target file or first file as fallback + const fallbackFile = targetFile || allFiles[0] + setFunctionLocation(null) + setActualFile(fallbackFile) } - - let fallbackFile = allFiles[0] - if (filePath) { - const match = allFiles.find(f => - filePath.endsWith(f.path) || f.path.endsWith(filePath) || f.path === filePath - ) - if (match) fallbackFile = match - } - setFunctionLocation(null) - setActualFile(fallbackFile) } - findFunction() + searchForFunction(functionName) return () => { cancelled = true } }, [functionName, filePath, allFiles]) diff --git a/js/cf-webapp/src/app/observability/components/python-parser.ts b/js/cf-webapp/src/app/observability/components/python-parser.ts index ad3a70d32..5ba6fc492 100644 --- a/js/cf-webapp/src/app/observability/components/python-parser.ts +++ b/js/cf-webapp/src/app/observability/components/python-parser.ts @@ -37,73 +37,206 @@ export async function findFunctionInCode( code: string, functionName: string ): Promise { - try { - const parser = await getParser() - if (parser) { - const tree = parser.parse(code) - if (tree) { - const result = findFunctionNode(tree.rootNode, functionName) - if (result) { - return { - startLine: result.startPosition.row + 1, - endLine: result.endPosition.row + 1, - } - } - } - } - } catch (error) { - console.warn("Tree-sitter parse failed, trying regex fallback:", error) + const { className, methodName } = parseQualifiedName(functionName) + + // Try tree-sitter parsing first + const treeResult = await findWithTreeSitter(code, className, methodName) + if (treeResult) { + return treeResult } - return findFunctionWithRegex(code, functionName) + // Fall back to regex if tree-sitter fails + return findFunctionWithRegex(code, methodName, className) +} + +async function findWithTreeSitter( + code: string, + className: string | undefined, + methodName: string +): Promise { + try { + const parser = await getParser() + if (!parser) return null + + const tree = parser.parse(code) + if (!tree) return null + + const result = className + ? findMethodInClass(tree.rootNode, className, methodName) + : findFunctionNode(tree.rootNode, methodName) + + if (!result) return null + + return { + startLine: result.startPosition.row + 1, + endLine: result.endPosition.row + 1, + } + } catch (error) { + console.warn("Tree-sitter parse failed:", error) + return null + } } function findFunctionWithRegex( code: string, - functionName: string + functionName: string, + className?: string ): FunctionLocation | null { const lines = code.split("\n") - const defPattern = new RegExp( - `^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(` + return className + ? findMethodInClassWithRegex(lines, className, functionName) + : findStandaloneFunctionWithRegex(lines, functionName) +} + +function findMethodInClassWithRegex( + lines: string[], + className: string, + methodName: string +): FunctionLocation | null { + const classPattern = new RegExp( + `^(\\s*)class\\s+${escapeRegex(className)}\\s*[:(]` + ) + const methodPattern = new RegExp( + `^(\\s*)(async\\s+)?def\\s+${escapeRegex(methodName)}\\s*\\(` ) - let startLine = -1 - let startIndent = -1 + let classIndent = -1 + let inClass = false for (let i = 0; i < lines.length; i++) { const line = lines[i] + const trimmed = line.trim() - if (startLine === -1) { - const match = line.match(defPattern) - if (match) { - startLine = i + 1 - startIndent = match[1].length - } - } else { - const trimmed = line.trim() - if (trimmed === "" || trimmed.startsWith("#")) { - continue - } + // Skip empty lines and comments + if (!trimmed || trimmed.startsWith("#")) continue - const currentIndent = line.length - line.trimStart().length - if (currentIndent <= startIndent) { - return { startLine, endLine: i } + const currentIndent = getIndentLevel(line) + + // Check for class definition + if (!inClass) { + const classMatch = line.match(classPattern) + if (classMatch) { + inClass = true + classIndent = classMatch[1].length } + continue } - } - if (startLine !== -1) { - return { startLine, endLine: lines.length } + // Check if we've left the class + if (currentIndent <= classIndent) { + inClass = false + continue + } + + // Look for the method within the class + const methodMatch = line.match(methodPattern) + if (methodMatch) { + const methodIndent = methodMatch[1].length + return findBlockEnd(lines, i + 1, methodIndent) + } } return null } +function findStandaloneFunctionWithRegex( + lines: string[], + functionName: string +): FunctionLocation | null { + const functionPattern = new RegExp( + `^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(` + ) + + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(functionPattern) + if (match) { + const indent = match[1].length + return findBlockEnd(lines, i + 1, indent) + } + } + + return null +} + +function findBlockEnd( + lines: string[], + startLine: number, + startIndent: number +): FunctionLocation { + for (let i = startLine; i < lines.length; i++) { + const trimmed = lines[i].trim() + if (!trimmed || trimmed.startsWith("#")) continue + + const currentIndent = getIndentLevel(lines[i]) + if (currentIndent <= startIndent) { + return { startLine, endLine: i } + } + } + + return { startLine, endLine: lines.length } +} + +function getIndentLevel(line: string): number { + return line.length - line.trimStart().length +} + function escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") } +function parseQualifiedName(functionName: string): { + className: string | undefined + methodName: string +} { + const dotIndex = functionName.lastIndexOf(".") + if (dotIndex > 0) { + return { + className: functionName.substring(0, dotIndex), + methodName: functionName.substring(dotIndex + 1), + } + } + return { className: undefined, methodName: functionName } +} + +function findMethodInClass( + rootNode: Node, + className: string, + methodName: string +): Node | null { + // Find all class definitions matching the class name + function searchClasses(node: Node): Node | null { + if (node.type === "class_definition") { + const nameNode = node.childForFieldName("name") + if (nameNode && nameNode.text === className) { + // Search for the method within this class body + const classBody = node.childForFieldName("body") + if (classBody) { + for (const child of classBody.children) { + if ( + child.type === "function_definition" || + child.type === "async_function_definition" + ) { + const fnName = child.childForFieldName("name") + if (fnName && fnName.text === methodName) { + return child + } + } + } + } + } + } + + for (const child of node.children) { + const result = searchClasses(child) + if (result) return result + } + return null + } + + return searchClasses(rootNode) +} + function findFunctionNode(node: Node, functionName: string): Node | null { if ( node.type === "function_definition" || diff --git a/js/cf-webapp/src/app/observability/llm-export/route.ts b/js/cf-webapp/src/app/observability/llm-export/route.ts index fdd9378d1..b9ee0b46a 100644 --- a/js/cf-webapp/src/app/observability/llm-export/route.ts +++ b/js/cf-webapp/src/app/observability/llm-export/route.ts @@ -1,8 +1,25 @@ import { type NextRequest, NextResponse } from "next/server" -import { getTraceData } from "@/app/observability/lib/get-trace-data" +import { getTraceData, type TraceData } from "@/app/observability/lib/get-trace-data" import { transformToTimelineSections } from "@/app/observability/components/timeline-types" import { formatTimelineForLLM } from "@/app/observability/components/format-llm-export" +function getFunctionName( + metadata: Record | undefined, + optimizationEvent: TraceData["optimizationEvent"] +): string | null { + const metadataFunctionName = metadata?.function_to_optimize as string | undefined + return metadataFunctionName ?? optimizationEvent?.function_name ?? null +} + +function getFilePath( + optimizationEvent: TraceData["optimizationEvent"], + metadata: Record | undefined +): string | null { + const eventFilePath = optimizationEvent?.file_path + const metadataFilePath = metadata?.file_path as string | undefined + return eventFilePath ?? metadataFilePath ?? null +} + export async function GET(request: NextRequest) { const traceId = request.nextUrl.searchParams.get("trace_id")?.trim() @@ -131,11 +148,8 @@ export async function GET(request: NextRequest) { }) const metadata = optimizationFeatures?.metadata as Record | undefined - const functionName = - (metadata?.function_to_optimize as string | undefined) ?? - optimizationEvent?.function_name ?? - null - const filePath = optimizationEvent?.file_path ?? null + const functionName = getFunctionName(metadata, optimizationEvent) + const filePath = getFilePath(optimizationEvent, metadata) const originalCode = optimizationFeatures?.original_code ?? null const transformedErrors = errors.map(error => ({ diff --git a/js/cf-webapp/src/app/observability/page.tsx b/js/cf-webapp/src/app/observability/page.tsx index 4bd23f0e7..36ecb936e 100644 --- a/js/cf-webapp/src/app/observability/page.tsx +++ b/js/cf-webapp/src/app/observability/page.tsx @@ -53,6 +53,35 @@ export default async function Observability2Page({ searchParams }: Observability ) } +function buildCandidateRankMap( + rankingData: { ranking?: string[]; explanation?: string } | null +): Record { + const rankMap: Record = {} + if (rankingData?.ranking) { + rankingData.ranking.forEach((id, index) => { + rankMap[id] = index + 1 + }) + } + return rankMap +} + +function getFunctionName( + metadata: Record | undefined, + optimizationEvent: TraceData["optimizationEvent"] +): string | null { + const metadataFunctionName = metadata?.function_to_optimize as string | undefined + return metadataFunctionName ?? optimizationEvent?.function_name ?? null +} + +function getFilePath( + optimizationEvent: TraceData["optimizationEvent"], + metadata: Record | undefined +): string | null { + const eventFilePath = optimizationEvent?.file_path + const metadataFilePath = metadata?.file_path as string | undefined + return eventFilePath ?? metadataFilePath ?? null +} + function TraceContent({ traceId, traceData }: { traceId: string; traceData: TraceData }) { const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = traceData @@ -114,16 +143,7 @@ function TraceContent({ traceId, traceData }: { traceId: string; traceData: Trac Object.keys(pullRequestRaw as Record).length > 0, ) - function buildCandidateRankMap(): Record { - const rankMap: Record = {} - if (rankingData?.ranking) { - rankingData.ranking.forEach((id, index) => { - rankMap[id] = index + 1 - }) - } - return rankMap - } - const candidateRankMap = buildCandidateRankMap() + const candidateRankMap = buildCandidateRankMap(rankingData) const generatedTests = (optimizationFeatures?.generated_test ?? []).map((code, index) => ({ code, @@ -190,13 +210,9 @@ function TraceContent({ traceId, traceData }: { traceId: string; traceData: Trac created_at: error.created_at, })) - function getFunctionName(): string | null { - const metadata = optimizationFeatures?.metadata as Record | undefined - const fromMetadata = metadata?.function_to_optimize as string | undefined - return fromMetadata ?? optimizationEvent?.function_name ?? null - } - const functionName = getFunctionName() - const filePath = optimizationEvent?.file_path ?? null + const featuresMetadata = optimizationFeatures?.metadata as Record | undefined + const functionName = getFunctionName(featuresMetadata, optimizationEvent) + const filePath = getFilePath(optimizationEvent, featuresMetadata) const originalCode = optimizationFeatures?.original_code ?? null const dependencyCode = optimizationFeatures?.dependency_code ?? null From 8baf8286346484f0dc33398ca6121a0ffc5f5c3b Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:33:51 -0500 Subject: [PATCH 068/184] chore: sync claude workflow with CLI repo (#2392) ## Summary - Use claude-opus-4-6 model for both pr-review and claude-mention jobs - Add mypy checks and consolidated summary comment (Steps 1 & 4) from CLI workflow - Add Edit tool and extra git/gh tools to allowed tools --- .github/workflows/claude.yml | 62 +++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 079cfdb4d..cf088fe83 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -54,20 +54,34 @@ jobs: IMPORTANT: This repo has Python code in `django/aiservice/`. Run uv/prek commands from that directory. - ## STEP 1: Run pre-commit checks and fix issues + ## STEP 1: Run prek and mypy checks, fix issues - First, run `cd django/aiservice && uv run prek run --from-ref origin/main` to check for linting/formatting issues on files changed in this PR. + First, run these checks on files changed in this PR: + 1. `cd django/aiservice && uv run prek run --from-ref origin/main` - linting/formatting issues + 2. `cd django/aiservice && uv run mypy ` - type checking issues - If there are any issues: - - For SAFE auto-fixable issues (formatting, import sorting, trailing whitespace, etc.), run the command again to auto-fix them + If there are prek issues: + - For SAFE auto-fixable issues (formatting, import sorting, trailing whitespace, etc.), run `cd django/aiservice && uv run prek run --from-ref origin/main` again to auto-fix them + - For issues that prek cannot auto-fix, do NOT attempt to fix them manually — report them as remaining issues in your summary + + If there are mypy issues: + - Fix type annotation issues (missing return types, Optional/None unions, import errors for type hints, incorrect types) + - Do NOT add `type: ignore` comments - always fix the root cause + + After fixing issues: - Stage the fixed files with `git add` - - Commit with message "style: auto-fix linting issues" + - Commit with message "style: auto-fix linting issues" or "fix: resolve mypy type errors" as appropriate - Push the changes with `git push` + IMPORTANT - Verification after fixing: + - After committing fixes, run `cd django/aiservice && uv run prek run --from-ref origin/main` ONE MORE TIME to verify all issues are resolved + - If errors remain, either fix them or report them honestly as unfixed in your summary + - NEVER claim issues are fixed without verifying. If you cannot fix an issue, say so + Do NOT attempt to fix: - - Type errors that require logic changes - - Complex refactoring suggestions - - Anything that could change behavior + - Type errors that require logic changes or refactoring + - Complex generic type issues + - Anything that could change runtime behavior ## STEP 2: Review the PR @@ -85,7 +99,6 @@ jobs: - Only create NEW inline comments for HIGH-PRIORITY issues found in changed files. - Limit to 5-7 NEW comments maximum per review. - Use CLAUDE.md for project-specific guidance. - - Use `gh pr comment` for summary-level feedback. - Use `mcp__github_inline_comment__create_inline_comment` sparingly for critical code issues only. ## STEP 3: Coverage analysis @@ -122,7 +135,34 @@ jobs: - New implementations/files: Must have ≥75% test coverage - Modified code: Changed lines should be exercised by existing or new tests - No coverage regressions: Overall coverage should not decrease - claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh api:*),Bash(cd django/aiservice*),Bash(uv run prek *),Bash(uv run coverage *),Bash(uv run pytest *),Bash(git status*),Bash(git add *),Bash(git commit *),Bash(git push*),Bash(git diff *),Bash(git checkout *),Read,Glob,Grep"' + + ## STEP 4: Post ONE consolidated summary comment + + CRITICAL: You must post exactly ONE summary comment containing ALL results (pre-commit, review, coverage). + DO NOT post multiple separate comments. Use this format: + + ``` + ## PR Review Summary + + ### Prek Checks + [status and any fixes made] + + ### Code Review + [critical issues found, if any] + + ### Test Coverage + [coverage table and analysis] + + --- + *Last updated: * + ``` + + To ensure only ONE comment exists: + 1. Find existing claude[bot] comment: `gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments --jq '.[] | select(.user.login == "claude[bot]") | .id' | head -1` + 2. If found, UPDATE it: `gh api --method PATCH repos/${{ github.repository }}/issues/comments/ -f body=""` + 3. If not found, CREATE: `gh pr comment ${{ github.event.pull_request.number }} --body ""` + 4. Delete any OTHER claude[bot] comments to clean up duplicates: `gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments --jq '.[] | select(.user.login == "claude[bot]") | .id' | tail -n +2 | xargs -I {} gh api --method DELETE repos/${{ github.repository }}/issues/comments/{}` + claude_args: '--model claude-opus-4-6 --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh api:*),Bash(cd django/aiservice*),Bash(uv run prek *),Bash(uv run mypy *),Bash(uv run coverage *),Bash(uv run pytest *),Bash(git status*),Bash(git add *),Bash(git commit *),Bash(git push*),Bash(git diff *),Bash(git checkout *),Read,Glob,Grep,Edit"' additional_permissions: | actions: read env: @@ -180,7 +220,7 @@ jobs: uses: anthropics/claude-code-action@v1 with: use_foundry: "true" - claude_args: '--allowedTools "Read,Edit,Write,Glob,Grep,Bash(git status*),Bash(git diff*),Bash(git add *),Bash(git commit *),Bash(git push*),Bash(git log*),Bash(cd django/aiservice*),Bash(uv run prek *),Bash(prek *),Bash(uv run ruff *),Bash(uv run pytest *),Bash(uv run mypy *),Bash(uv run coverage *),Bash(gh pr comment*),Bash(gh pr view*),Bash(gh pr diff*)"' + claude_args: '--model claude-opus-4-6 --allowedTools "Read,Edit,Write,Glob,Grep,Bash(git status*),Bash(git diff*),Bash(git add *),Bash(git commit *),Bash(git push*),Bash(git log*),Bash(git merge*),Bash(git fetch*),Bash(git checkout*),Bash(git branch*),Bash(cd django/aiservice*),Bash(uv run prek *),Bash(prek *),Bash(uv run ruff *),Bash(uv run pytest *),Bash(uv run mypy *),Bash(uv run coverage *),Bash(gh pr comment*),Bash(gh pr view*),Bash(gh pr diff*),Bash(gh pr merge*),Bash(gh pr close*)"' additional_permissions: | actions: read env: From 0df421eccb0760c6d1d54bc01dfdf641c5a5b363 Mon Sep 17 00:00:00 2001 From: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:45:33 -0500 Subject: [PATCH 069/184] Add chat interface to observability timeline (#2395) ## Summary - Chat panel on the observability timeline that uses Claude to answer questions about optimization traces - Tool-based context retrieval (fetches candidates, tests, errors on demand instead of stuffing everything upfront) - Uses `@anthropic-ai/sdk` via Azure AI Foundry - Strengthened testgen prompts to ban mocks/fakes for test inputs --- .../testgen/execute_async_system_prompt.md | 7 +- .../python/testgen/execute_system_prompt.md | 7 +- js/cf-webapp/package-lock.json | 40 +++ js/cf-webapp/package.json | 1 + .../src/app/api/observability/chat/route.ts | 188 ++++++++++ .../components/timeline-chat.tsx | 327 ++++++++++++++++++ .../components/timeline-page-view.tsx | 36 +- js/cf-webapp/src/app/observability/layout.tsx | 8 + .../observability/lib/build-chat-context.ts | 327 ++++++++++++++++++ js/cf-webapp/src/app/observability/page.tsx | 1 + 10 files changed, 931 insertions(+), 11 deletions(-) create mode 100644 js/cf-webapp/src/app/api/observability/chat/route.ts create mode 100644 js/cf-webapp/src/app/observability/components/timeline-chat.tsx create mode 100644 js/cf-webapp/src/app/observability/lib/build-chat-context.ts diff --git a/django/aiservice/core/languages/python/testgen/execute_async_system_prompt.md b/django/aiservice/core/languages/python/testgen/execute_async_system_prompt.md index 3227cf293..61d6bd2ee 100644 --- a/django/aiservice/core/languages/python/testgen/execute_async_system_prompt.md +++ b/django/aiservice/core/languages/python/testgen/execute_async_system_prompt.md @@ -21,8 +21,7 @@ Then write tests organized into four categories: - Tests should be diverse — cover a wide range of inputs and async-specific scenarios - Tests must be deterministic — always pass or fail the same way - Sort tests by difficulty, from easiest to hardest -- Do not mock or stub the function under test or its internal calls. Mock only external dependencies (APIs, databases, network, file I/O) if absolutely necessary. -- Do not use `Mock(spec=SomeClass)` for domain classes — create real instances instead +- **Never use mocks for test inputs.** Do not use `Mock`, `MagicMock`, `AsyncMock`, `Mock(spec=...)`, `SimpleNamespace`, `patch`, or any fake/stub objects to create test inputs or domain objects. Always construct real instances using the actual class constructors with real arguments. Mocks hide real behavior, silently pass on wrong attribute access, and break when optimized code changes access patterns. - Include concurrent execution tests using `asyncio.gather()` to assess async performance - Test proper async/await patterns and coroutine handling @@ -36,10 +35,10 @@ Then write tests organized into four categories: **Rules:** - **Preserve the original function** — do not modify, enhance, or add parameters to the function under test. Test it exactly as provided. -- **Use real classes** — never define stub, fake, mock, dummy, or placeholder classes. Import real classes from their actual modules. Tests that define their own classes will fail `isinstance()` checks. +- **Use real classes** — never define stub, fake, mock, dummy, or placeholder classes. Never use `SimpleNamespace` as a stand-in for real objects. Import real classes from their actual modules and construct real instances. Tests that define their own classes or use fake objects will fail `isinstance()` checks and break when code is optimized. - **Handle instance methods correctly** — if the function has `self`, import the class, create a real instance, and call the method on the instance with `await instance.method(...)`. Do not pass `self` manually. - **Use conftest.py fixtures when provided** — prefer fixtures over manual instantiation. Fixtures are pre-configured and handle setup/teardown. -- **Import everything you use** — every symbol must have a corresponding import. If you use `Mock`, `MagicMock`, `AsyncMock`, `patch`, etc., import each explicitly from `unittest.mock`. +- **Import everything you use** — every symbol must have a corresponding import. - **Only import what you use** — do not add unused imports. - **Use correct import sources** — when the dependency context shows `from X import Y`, use that exact source module. - **Use correct constructor signatures** — only use constructor arguments shown in the provided context. Use concrete subclasses instead of abstract classes. diff --git a/django/aiservice/core/languages/python/testgen/execute_system_prompt.md b/django/aiservice/core/languages/python/testgen/execute_system_prompt.md index 1c17adf1a..7d5c77d03 100644 --- a/django/aiservice/core/languages/python/testgen/execute_system_prompt.md +++ b/django/aiservice/core/languages/python/testgen/execute_system_prompt.md @@ -18,16 +18,15 @@ Then write tests organized into three categories: - Tests should be diverse — cover a wide range of inputs and scenarios - Tests must be deterministic — always pass or fail the same way - Sort tests by difficulty, from easiest to hardest -- Do not mock or stub the function under test or its internal calls. Mock only external dependencies (APIs, databases, network, file I/O) if absolutely necessary. -- Do not use `Mock(spec=SomeClass)` for domain classes — create real instances instead +- **Never use mocks for test inputs.** Do not use `Mock`, `MagicMock`, `Mock(spec=...)`, `SimpleNamespace`, `patch`, or any fake/stub objects to create test inputs or domain objects. Always construct real instances using the actual class constructors with real arguments. Mocks hide real behavior, silently pass on wrong attribute access, and break when optimized code changes access patterns. - Include large-scale test cases to assess performance with realistic data volumes **Rules:** - **Preserve the original function** — do not modify, enhance, or add parameters to the function under test. Test it exactly as provided. -- **Use real classes** — never define stub, fake, mock, dummy, or placeholder classes. Import real classes from their actual modules. Tests that define their own classes will fail `isinstance()` checks. +- **Use real classes** — never define stub, fake, mock, dummy, or placeholder classes. Never use `SimpleNamespace` as a stand-in for real objects. Import real classes from their actual modules and construct real instances. Tests that define their own classes or use fake objects will fail `isinstance()` checks and break when code is optimized. - **Handle instance methods correctly** — if the function has `self`, import the class, create a real instance, and call the method on the instance. Do not pass `self` manually. - **Use conftest.py fixtures when provided** — prefer fixtures over manual instantiation. Fixtures are pre-configured and handle setup/teardown. -- **Import everything you use** — every symbol must have a corresponding import. If you use `Mock`, `MagicMock`, `patch`, etc., import each explicitly from `unittest.mock`. +- **Import everything you use** — every symbol must have a corresponding import. - **Only import what you use** — do not add unused imports. - **Use correct import sources** — when the dependency context shows `from X import Y`, use that exact source module. - **Use correct constructor signatures** — only use constructor arguments shown in the provided context. Use concrete subclasses instead of abstract classes. diff --git a/js/cf-webapp/package-lock.json b/js/cf-webapp/package-lock.json index 6ac6815c6..485db65b2 100644 --- a/js/cf-webapp/package-lock.json +++ b/js/cf-webapp/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { + "@anthropic-ai/sdk": "^0.74.0", "@auth0/nextjs-auth0": "^3.3.0", "@azure/msal-node": "^3.7.3", "@codeflash-ai/common": "^1.0.30", @@ -108,6 +109,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.74.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.74.0.tgz", + "integrity": "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -10726,6 +10747,19 @@ "license": "MIT", "peer": true }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -15736,6 +15770,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", diff --git a/js/cf-webapp/package.json b/js/cf-webapp/package.json index ed3a22a32..985e8c23f 100644 --- a/js/cf-webapp/package.json +++ b/js/cf-webapp/package.json @@ -19,6 +19,7 @@ "format:check": "prettier --check \"**/*.{js,ts,tsx,json,md}\"" }, "dependencies": { + "@anthropic-ai/sdk": "^0.74.0", "@auth0/nextjs-auth0": "^3.3.0", "@azure/msal-node": "^3.7.3", "@codeflash-ai/common": "^1.0.30", diff --git a/js/cf-webapp/src/app/api/observability/chat/route.ts b/js/cf-webapp/src/app/api/observability/chat/route.ts new file mode 100644 index 000000000..094037185 --- /dev/null +++ b/js/cf-webapp/src/app/api/observability/chat/route.ts @@ -0,0 +1,188 @@ +import { NextRequest } from "next/server" +import Anthropic from "@anthropic-ai/sdk" +import { getTraceData } from "@/app/observability/lib/get-trace-data" +import { + indexTraceData, + buildSummaryPrompt, + anthropicToolDefinitions, + resolveToolCall, + type IndexedTraceData, +} from "@/app/observability/lib/build-chat-context" + +interface ChatMessage { + role: "user" | "assistant" + content: string +} + +const MAX_TOOL_ROUNDS = 5 + +function getClient(): Anthropic { + const baseURL = process.env.ANTHROPIC_FOUNDRY_BASE_URL + const apiKey = process.env.AZURE_OPENAI_API_KEY + if (!baseURL || !apiKey) { + throw new Error("ANTHROPIC_FOUNDRY_BASE_URL and AZURE_OPENAI_API_KEY must be configured") + } + return new Anthropic({ baseURL, apiKey }) +} + +function processToolUseResponse( + response: Anthropic.Message, + indexed: IndexedTraceData +): Anthropic.ToolResultBlockParam[] { + const toolResults: Anthropic.ToolResultBlockParam[] = [] + for (const block of response.content) { + if (block.type === "tool_use") { + const result = resolveToolCall( + block.name, + (block.input as Record) ?? {}, + indexed + ) + toolResults.push({ + type: "tool_result", + tool_use_id: block.id, + content: result, + }) + } + } + return toolResults +} + +export async function POST(request: NextRequest): Promise { + let client: Anthropic + try { + client = getClient() + } catch (err) { + return Response.json( + { error: err instanceof Error ? err.message : "Configuration error" }, + { status: 500 } + ) + } + + let body: { traceId: string; messages: ChatMessage[] } + try { + body = await request.json() + } catch { + return Response.json({ error: "Invalid JSON" }, { status: 400 }) + } + + const { traceId, messages } = body + if (!traceId || !messages?.length) { + return Response.json( + { error: "traceId and messages are required" }, + { status: 400 } + ) + } + + const tracePrefix = traceId.substring(0, 33) + const traceData = await getTraceData(tracePrefix) + if (!traceData || (traceData.rawLlmCalls.length === 0 && traceData.errors.length === 0)) { + return Response.json({ error: "Trace not found" }, { status: 404 }) + } + + const indexed = indexTraceData(traceData) + const systemPrompt = buildSummaryPrompt(indexed) + + const conversationMessages: Anthropic.MessageParam[] = messages.map((m) => ({ + role: m.role, + content: m.content, + })) + + try { + let toolRounds = 0 + while (toolRounds < MAX_TOOL_ROUNDS) { + const response = await client.messages.create({ + model: "claude-opus-4-6", + max_tokens: 4096, + system: systemPrompt, + messages: conversationMessages, + tools: anthropicToolDefinitions as Anthropic.Tool[], + }) + + if (response.stop_reason !== "tool_use") { + break + } + + conversationMessages.push({ role: "assistant", content: response.content }) + const toolResults = processToolUseResponse(response, indexed) + conversationMessages.push({ role: "user", content: toolResults }) + toolRounds++ + } + } catch (err) { + const message = err instanceof Anthropic.APIError + ? `API error: ${err.status} ${err.message}` + : err instanceof Error ? err.message : "Unknown error" + return Response.json({ error: message }, { status: 502 }) + } + + return streamFinalResponse(client, systemPrompt, conversationMessages, indexed) +} + +async function streamFinalResponse( + client: Anthropic, + systemPrompt: string, + messages: Anthropic.MessageParam[], + indexed: IndexedTraceData +): Promise { + const encoder = new TextEncoder() + + const stream = new ReadableStream({ + async start(controller) { + try { + const messageStream = client.messages.stream({ + model: "claude-opus-4-6", + max_tokens: 4096, + system: systemPrompt, + messages, + tools: anthropicToolDefinitions as Anthropic.Tool[], + }) + + messageStream.on("text", (textDelta) => { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ text: textDelta })}\n\n`) + ) + }) + + const finalMessage = await messageStream.finalMessage() + + if (finalMessage.stop_reason === "tool_use") { + messages.push({ role: "assistant", content: finalMessage.content }) + const toolResults = processToolUseResponse(finalMessage, indexed) + messages.push({ role: "user", content: toolResults }) + + const followUpStream = client.messages.stream({ + model: "claude-opus-4-6", + max_tokens: 4096, + system: systemPrompt, + messages, + }) + + followUpStream.on("text", (textDelta) => { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ text: textDelta })}\n\n`) + ) + }) + + await followUpStream.finalMessage() + } + + controller.enqueue(encoder.encode("data: [DONE]\n\n")) + } catch (err) { + controller.enqueue( + encoder.encode( + `data: ${JSON.stringify({ error: err instanceof Error ? err.message : "Stream error" })}\n\n` + ) + ) + } finally { + controller.close() + } + }, + }) + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) +} diff --git a/js/cf-webapp/src/app/observability/components/timeline-chat.tsx b/js/cf-webapp/src/app/observability/components/timeline-chat.tsx new file mode 100644 index 000000000..d5402e214 --- /dev/null +++ b/js/cf-webapp/src/app/observability/components/timeline-chat.tsx @@ -0,0 +1,327 @@ +"use client" + +import { useState, useRef, useEffect, useCallback, memo } from "react" +import { MessageSquare, Send, X, Loader2, User, Bot, Copy, Check } from "lucide-react" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" +import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" + +interface ChatMessage { + role: "user" | "assistant" + content: string +} + +interface TimelineChatProps { + traceId: string + isOpen: boolean + onClose: () => void +} + +export const TimelineChat = memo(function TimelineChat({ + traceId, + isOpen, + onClose, +}: TimelineChatProps) { + const [messages, setMessages] = useState([]) + const [input, setInput] = useState("") + const [isStreaming, setIsStreaming] = useState(false) + const messagesEndRef = useRef(null) + const inputRef = useRef(null) + const abortRef = useRef(null) + + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) + }, []) + + useEffect(() => { + scrollToBottom() + }, [messages, scrollToBottom]) + + useEffect(() => { + if (isOpen) { + requestAnimationFrame(() => inputRef.current?.focus()) + } + }, [isOpen]) + + const sendMessage = useCallback(async () => { + const trimmed = input.trim() + if (!trimmed || isStreaming) return + + const userMessage: ChatMessage = { role: "user", content: trimmed } + const newMessages = [...messages, userMessage] + setMessages(newMessages) + setInput("") + setIsStreaming(true) + + const controller = new AbortController() + abortRef.current = controller + + setMessages((prev) => [...prev, { role: "assistant", content: "" }]) + + try { + const res = await fetch("/api/observability/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ traceId, messages: newMessages }), + signal: controller.signal, + }) + + if (!res.ok) { + const errData = await res.json().catch(() => ({})) + throw new Error(errData.error || `Request failed: ${res.status}`) + } + + const reader = res.body?.getReader() + if (!reader) throw new Error("No response stream") + + const decoder = new TextDecoder() + let buffer = "" + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() ?? "" + + for (const line of lines) { + if (!line.startsWith("data: ")) continue + const data = line.slice(6).trim() + if (data === "[DONE]") continue + + try { + const parsed = JSON.parse(data) + if (parsed.text) { + setMessages((prev) => { + const updated = [...prev] + const last = updated[updated.length - 1] + if (last?.role === "assistant") { + updated[updated.length - 1] = { + ...last, + content: last.content + parsed.text, + } + } + return updated + }) + } + if (parsed.error) { + throw new Error(parsed.error) + } + } catch (e) { + if (e instanceof SyntaxError) continue + throw e + } + } + } + } catch (err) { + if ((err as Error).name === "AbortError") return + + setMessages((prev) => { + const updated = [...prev] + const last = updated[updated.length - 1] + if (last?.role === "assistant" && !last.content) { + updated[updated.length - 1] = { + ...last, + content: `Error: ${(err as Error).message}`, + } + } + return updated + }) + } finally { + setIsStreaming(false) + abortRef.current = null + } + }, [input, isStreaming, messages, traceId]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + sendMessage() + } + }, + [sendMessage] + ) + + const stopStreaming = useCallback(() => { + abortRef.current?.abort() + }, []) + + const [copied, setCopied] = useState(false) + const exportChat = useCallback(() => { + const text = messages + .filter((m) => m.content) + .map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`) + .join("\n\n") + navigator.clipboard.writeText(text).then(() => { + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }) + }, [messages]) + + if (!isOpen) return null + + return ( +
    +
    +
    + + + Chat with Trace + +
    +
    + {messages.length > 0 && ( + + )} + +
    +
    + +
    + {messages.length === 0 && ( +
    +
    + +
    +

    + Ask about this optimization trace +

    +

    + e.g. "Why was candidate 1 ranked best?" or "What optimizations were attempted?" +

    +
    + )} + + {messages.map((msg, i) => ( + + ))} +
    +
    + +
    +
    +