Pandago commited on
Commit
a6f0611
·
verified ·
1 Parent(s): 356eea0

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +24 -0
  2. Dockerfile +56 -0
  3. README.md +559 -5
  4. __init__.py +1 -0
  5. agent/__init__.py +1 -0
  6. agent/hybrid_policy.py +245 -0
  7. agent/memory.py +178 -0
  8. agent/policy.py +340 -0
  9. agent/reflection.py +168 -0
  10. agent/train.py +508 -0
  11. assets/episode.png +0 -0
  12. assets/formulas-1.png +3 -0
  13. assets/formulas-2.png +0 -0
  14. assets/gs.png +0 -0
  15. assets/hybrid.png +3 -0
  16. assets/logo.png +0 -0
  17. assets/reflexion.png +3 -0
  18. assets/sys arch.png +3 -0
  19. baseline_agent.py +249 -0
  20. bedrock_model.py +167 -0
  21. check.sh +332 -0
  22. client.py +141 -0
  23. dashboard/README.md +327 -0
  24. dashboard/backend/main.py +473 -0
  25. dashboard/backend/requirements.txt +5 -0
  26. dashboard/frontend/index.html +13 -0
  27. dashboard/frontend/node_modules/@alloc/quick-lru/index.d.ts +128 -0
  28. dashboard/frontend/node_modules/@alloc/quick-lru/index.js +263 -0
  29. dashboard/frontend/node_modules/@alloc/quick-lru/license +9 -0
  30. dashboard/frontend/node_modules/@alloc/quick-lru/package.json +43 -0
  31. dashboard/frontend/node_modules/@alloc/quick-lru/readme.md +139 -0
  32. dashboard/frontend/node_modules/@babel/code-frame/LICENSE +22 -0
  33. dashboard/frontend/node_modules/@babel/code-frame/README.md +19 -0
  34. dashboard/frontend/node_modules/@babel/code-frame/lib/index.js +217 -0
  35. dashboard/frontend/node_modules/@babel/code-frame/lib/index.js.map +1 -0
  36. dashboard/frontend/node_modules/@babel/code-frame/package.json +32 -0
  37. dashboard/frontend/node_modules/@babel/compat-data/LICENSE +22 -0
  38. dashboard/frontend/node_modules/@babel/compat-data/README.md +19 -0
  39. dashboard/frontend/node_modules/@babel/compat-data/corejs2-built-ins.js +2 -0
  40. dashboard/frontend/node_modules/@babel/compat-data/corejs3-shipped-proposals.js +2 -0
  41. dashboard/frontend/node_modules/@babel/compat-data/data/corejs2-built-ins.json +2106 -0
  42. dashboard/frontend/node_modules/@babel/compat-data/data/corejs3-shipped-proposals.json +5 -0
  43. dashboard/frontend/node_modules/@babel/compat-data/data/native-modules.json +18 -0
  44. dashboard/frontend/node_modules/@babel/compat-data/data/overlapping-plugins.json +35 -0
  45. dashboard/frontend/node_modules/@babel/compat-data/data/plugin-bugfixes.json +203 -0
  46. dashboard/frontend/node_modules/@babel/compat-data/data/plugins.json +838 -0
  47. dashboard/frontend/node_modules/@babel/compat-data/native-modules.js +2 -0
  48. dashboard/frontend/node_modules/@babel/compat-data/overlapping-plugins.js +2 -0
  49. dashboard/frontend/node_modules/@babel/compat-data/package.json +40 -0
  50. dashboard/frontend/node_modules/@babel/compat-data/plugin-bugfixes.js +2 -0
.gitattributes CHANGED
@@ -33,3 +33,27 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/formulas-1.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/hybrid.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/reflexion.png filter=lfs diff=lfs merge=lfs -text
39
+ assets/sys[[:space:]]arch.png filter=lfs diff=lfs merge=lfs -text
40
+ dashboard/frontend/node_modules/@esbuild/linux-x64/bin/esbuild filter=lfs diff=lfs merge=lfs -text
41
+ dashboard/frontend/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node filter=lfs diff=lfs merge=lfs -text
42
+ dashboard/frontend/node_modules/esbuild/bin/esbuild filter=lfs diff=lfs merge=lfs -text
43
+ images/big.png filter=lfs diff=lfs merge=lfs -text
44
+ wheels/anyio-4.13.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
45
+ wheels/boto3-1.42.78-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
46
+ wheels/botocore-1.42.78-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
47
+ wheels/certifi-2026.2.25-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
48
+ wheels/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl filter=lfs diff=lfs merge=lfs -text
49
+ wheels/click-8.3.1-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
50
+ wheels/fastapi-0.135.2-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
51
+ wheels/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl filter=lfs diff=lfs merge=lfs -text
52
+ wheels/pydantic-2.12.5-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
53
+ wheels/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl filter=lfs diff=lfs merge=lfs -text
54
+ wheels/python_dateutil-2.9.0.post0-py2.py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
55
+ wheels/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl filter=lfs diff=lfs merge=lfs -text
56
+ wheels/urllib3-2.6.3-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
57
+ wheels/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl filter=lfs diff=lfs merge=lfs -text
58
+ wheels/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl filter=lfs diff=lfs merge=lfs -text
59
+ wheels/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── GraphStrike — OpenEnv Environment Server ─────────────────────────────────
2
+ # This Dockerfile runs the environment server (FastAPI + Gradio UI + episodes).
3
+ # No training loop, no AWS credentials required.
4
+ #
5
+ # Used by: openenv push / HuggingFace Spaces deployment
6
+ # Port: 7860 (HF Spaces default — configurable via PORT env var)
7
+ #
8
+ # For local training (Qwen3 + Reflexion + Hybrid Policy), use server/Dockerfile.
9
+ # ─────────────────────────────────────────────────────────────────────────────
10
+
11
+ FROM python:3.12-slim
12
+
13
+ WORKDIR /app
14
+
15
+ # ── Install Python dependencies (network available on HF build workers) ───────
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir \
18
+ fastapi \
19
+ "uvicorn[standard]" \
20
+ "pydantic>=2.6.0" \
21
+ requests \
22
+ "openenv-core>=0.2.0" \
23
+ "gradio>=4.0.0" \
24
+ "openai>=1.0.0"
25
+
26
+ # ── Copy source code ──────────────────────────────────────────────────────────
27
+ COPY . .
28
+
29
+ # ── Pre-generate all 150 episodes at build time (~1 second, deterministic) ───
30
+ # This bakes the episodes into the image so the server starts instantly.
31
+ RUN python server/generator.py
32
+
33
+ # ── Dirs for optional persistent data (mounted as volumes on local Docker) ───
34
+ RUN mkdir -p /app/memory /app/runs
35
+
36
+ # ── Runtime config ────────────────────────────────────────────────────────────
37
+ ENV PORT=7860
38
+ ENV AWS_DEFAULT_REGION=us-east-1
39
+
40
+ # HF Spaces expects the app on port 7860 (override via PORT env var)
41
+ EXPOSE 7860
42
+
43
+ # Server only — the training loop is NOT started here.
44
+ # Judges evaluate the environment via the API endpoints:
45
+ # GET /health → liveness check
46
+ # GET /tasks → available tasks + action schema
47
+ # POST /reset → start an episode
48
+ # POST /step → take an action
49
+ # GET /grader → get normalised score after SUBMIT
50
+ # POST /baseline → run the rule-based agent on all 3 tasks
51
+ ENV ENABLE_WEB_INTERFACE=true
52
+ CMD ["python", "-m", "uvicorn", "server.app:app", \
53
+ "--host", "0.0.0.0", \
54
+ "--port", "7860", \
55
+ "--workers", "1", \
56
+ "--log-level", "info"]
README.md CHANGED
@@ -1,10 +1,564 @@
1
  ---
2
- title: Graphstrike Model Training
3
- emoji: 🌖
4
- colorFrom: gray
5
- colorTo: red
6
  sdk: docker
 
7
  pinned: false
 
 
 
 
 
 
 
 
8
  ---
 
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: GraphStrike
3
+ emoji: 🕵️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
+ tags:
11
+ - reinforcement-learning
12
+ - social-network
13
+ - fraud-detection
14
+ - openenv
15
+ - llm-agent
16
+ base_path: /web
17
  ---
18
+ <br>
19
 
20
+ <p align="center">
21
+ <img src="assets/logo.png" width="600"/>
22
+ </p>
23
+
24
+ <br>
25
+
26
+ <p align="center">
27
+ <img src="https://img.shields.io/badge/Hugging%20Face-FFD21E?style=for-the-badge&logo=huggingface&logoColor=black"/>
28
+ <img src="https://img.shields.io/badge/HF%20Spaces-FFBF00?style=for-the-badge&logo=huggingface&logoColor=black"/>
29
+ <img src="https://img.shields.io/badge/FastAPI-009688?style=for-the-badge&logo=fastapi&logoColor=white"/>
30
+ <img src="https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white"/>
31
+ <img src="https://img.shields.io/badge/Gradio-F97316?style=for-the-badge&logo=gradio&logoColor=white"/>
32
+ <img src="https://img.shields.io/badge/OpenEnv-4B5563?style=for-the-badge&logo=envato&logoColor=white"/>
33
+ <img src="https://img.shields.io/badge/Amazon%20Bedrock-FF9900?style=for-the-badge&logo=amazonaws&logoColor=white"/>
34
+ </p>
35
+ <br>
36
+
37
+ <h1 align="center">
38
+ </h1>
39
+ <p align="center">
40
+ An OpenEnv-compatible reinforcement learning environment where an LLM agent must identify all 10 members of a coordinated fake account network hidden inside a synthetic social network. The agent learns via Reflexion and a dynamic hybrid rule/LLM policy , not via gradient updates or fine-tuning.
41
+ <br />
42
+ </p>
43
+ </p>
44
+
45
+ <br>
46
+
47
+ ## Round 2 — Platform-Adaptive Trust & Safety
48
+
49
+ Round 2 makes detection **platform-aware end-to-end**. Each episode runs on a named platform (Instagram, Snapchat, X, LinkedIn, Reddit, …); a `PlatformPolicy` is compiled offline from real transparency-report text and cached per platform; the high-signal account fields start hidden and are revealed only by explicit tool actions; and a shared evaluation runner consults the LLM at exactly two decision points per suspicious account.
50
+
51
+ ### How the policy lives end-to-end
52
+
53
+ ```
54
+ transparency reports per-episode runtime
55
+ ──────────────────── ───────────────────
56
+ Tavily search ──► Groq Llama-3.1 client.reset(task, seed)
57
+ extracts {π, fn_cost, fp_cost, │
58
+ harm_weight, primary_signal} env loads policy_cache/{platform}.json
59
+ │ │
60
+ ▼ GET_POLICY (step 0, +0.20 bonus)
61
+ sanitize_pi() clamp π to [5e-4, 0.05] │
62
+ compute_threshold: DP1 (LLM): pick tool
63
+ θ_raw = C_fn·π / [C_fn·π + C_fp·(1−π)] reverse_image_search / analyze_bio
64
+ θ* = clamp(θ_raw / harm_weight, .01, .95) / check_ip / done
65
+ fp_penalty_weight = C_fp │
66
+ │ DP2 (LLM): flag / skip
67
+ ▼ │
68
+ sanity_check_policy() warns on outliers SUBMIT
69
+ │ reward = tp − fp·C_fp − fn·0.3 + bonuses
70
+ policy_cache/{platform}.json (30-day TTL) decision_package + grader_score returned
71
+ ```
72
+
73
+ The threshold `θ*` is read by the LLM in DP1/DP2 prompts; the FP-penalty `C_fp` is paid in the terminal reward at SUBMIT. Both come from the same compile-time computation — they cannot drift apart.
74
+
75
+ ### What's new in this Round 2 cut
76
+
77
+ - **9 actions** including `get_policy`, `reverse_image_search`, `analyze_bio`, `check_ip`. `openenv.yaml` action_schema mirrors all nine.
78
+ - **Per-step reward delta** is returned on every `/step` (not only at SUBMIT), so per-action shaping like the GET_POLICY bonus and tool penalties are immediately visible.
79
+ - **`visible_accounts` is populated for every visible id at reset**, with hidden signals at `0.0 / ""` until tools reveal them.
80
+ - **`StepResponse` carries top-level `decision_package` and `grader_score`** after SUBMIT — callers no longer need to grep the message.
81
+ - **Blind FLAG is denied at flag time with `−0.15`** when the agent has neither inspected the account nor used any tool on it.
82
+ - **Generic platform support**: the policy compiler uses the platform-agnostic Tavily query `"{platform} fake account content policy enforcement 2024 2025"` and falls back to a generic policy when no hardcoded entry exists.
83
+ - **Two-decision-point eval runner** (`eval-models/_round2_runner.py`) drives episodes deterministically and exposes the LLM only at DP1 (tool pick) and DP2 (flag/skip). Six thin model shims (qwen, gemma, deepseek, llama, mistral, nvidia) plug in HF-router or Bedrock backends through a shared `_llm_adapters.py`.
84
+ - **`bash check.sh`** runs a 12-step system check covering health, all 9 actions, reward shaping, and the decision package.
85
+
86
+ Full architecture, formula audit, end-to-end policy lifecycle, sanity rules, scoring math, tool contracts, eval-runner internals, and quickstart commands live in **[`reference.md`](reference.md)** (single source of truth).
87
+
88
+ ## Theme
89
+
90
+ **SUPPORT**
91
+
92
+ ### Customer Service Agents
93
+
94
+ Complex environment where agents resolve multi-step queries using external tools and APIs.
95
+
96
+ ## Problem Statement
97
+
98
+ **The task:** A social network contains fake accounts organised into a single coordinated ring of 10. The ring behaves in a coordinated way — same posting hour, same IP subnet, stolen celebrity photos, copy-paste bios. The agent must find all 10 by navigating a limited step budget, inspecting accounts, and flagging suspects.
99
+
100
+ ## Proposed Solution
101
+
102
+ An OpenEnv-compatible reinforcement learning environment where an LLM agent must identify all 10 members of a coordinated fake account ring hidden inside a synthetic social network. The agent learns via **Reflexion** and a **dynamic hybrid rule/LLM policy** — not via gradient updates or fine-tuning.
103
+
104
+ ---
105
+ ## Novelty Highlights
106
+
107
+ - **Adaptive Hybrid Intelligence (Rules + LLM):** Unlike static ensembles, GraphStrike dynamically blends deterministic rules and LLM reasoning using a trust gate, shifting control as performance improves.
108
+ - **Learning Without Fine-Tuning:** Instead of updating model weights, the agent learns through Reflexion lessons and best-trajectory memory injected into future prompts.
109
+ - **Graph-First Detection Pipeline:** Detection is not account-by-account only; it uses cascade effects, neighbor propagation, and multi-hop graph expansion to uncover coordinated rings.
110
+ - **Math-Grounded Decision Control:** Risk composition, trust calibration, and grader alignment are formula-driven, making behavior interpretable and reproducible.
111
+ - **Adversarial Evasion Benchmarking:** Hard-mode includes timed evasion events, so success reflects robustness under disruption rather than overfitting to static patterns.
112
+ - **Safety-Net by Design:** High-confidence rule overrides prevent catastrophic LLM errors while preserving LLM flexibility for strategic exploration.
113
+ ---
114
+
115
+ ## Performance Summary
116
+
117
+ We evaluate GraphStrike's hybrid rule/LLM policy across multiple *frontier models to measure how well each model handles the investigation task. All runs use
118
+ the same inference pipeline (`inference.py`) with identical system prompts and structured logging. Each model ran: (1) seed=0 on all 3 tasks, and
119
+ (2) seeds 0-2 on all 3 tasks for variance measurement.*
120
+
121
+ **Seed=0 scores (single episode per task):**
122
+
123
+ <p align="center">
124
+ <img src="images/table1.png" alt="Model Performance Table" width="1600"/>
125
+ </p>
126
+ <br>
127
+
128
+ **3-seed variance scores (mean across seeds 0, 1, 2):**
129
+
130
+ <p align="center">
131
+ <img src="images/table2.png" alt="Model Performance Table" width="1600"/>
132
+ </p>
133
+ <br>
134
+
135
+ **Rule-Based Baseline (no LLM, deterministic)**
136
+
137
+ <p align="center">
138
+ <img src="images/table3.png" alt="Model Performance Table" width="1600"/>
139
+ </p>
140
+ <br>
141
+
142
+ ---
143
+ ## Table of Contents
144
+
145
+ 1. [What This Is](#1-what-this-is)
146
+ 2. [The Problem: How Fake Detection Actually Works](#2-the-problem-how-fake-detection-actually-works)
147
+ 3. [Synthetic Data Generation](#3-synthetic-data-generation)
148
+ 4. [Data Model](#4-data-model)
149
+ 5. [The RL Environment](#5-the-rl-environment)
150
+ 6. [Risk Scoring Mathematics](#6-risk-scoring-mathematics)
151
+ 8. [The LLM Policy (Qwen3 via Bedrock)](#8-the-llm-policy-qwen3-via-bedrock)
152
+ 9. [Reflexion — How the Agent Learns](#9-reflexion--how-the-agent-learns)
153
+ 10. [Hybrid Policy — The Novel Contribution](#10-hybrid-policy--the-novel-contribution)
154
+ 11. [Training Loop End-to-End](#11-training-loop-end-to-end)
155
+ 12. [API Reference](#12-api-reference)
156
+ 13. [Docker Deployment](#13-docker-deployment)
157
+ 14. [Submission Requirements](#14-submission-requirements)
158
+ 15. [Verification & Validation](#15-verification--validation)
159
+
160
+ ---
161
+
162
+ ## 1. What is this !?
163
+
164
+ This is an **OpenEnv hackathon** submission. OpenEnv is a framework for building RL environments with a standard microservice interface (`/reset`, `/step`, `/state`) so that any agent implementation can plug in.
165
+
166
+ **What makes this non-trivial:**
167
+
168
+ - The network is large (50–1000 accounts depending on difficulty).
169
+ - Fake accounts are mixed with innocent high-signal "decoy" accounts.
170
+ - In hard mode, the gang actively evades — dropping intra-gang follows, renaming profiles — while the agent is mid-investigation.
171
+ - The agent cannot see the full network upfront: it must explore via INSPECT and INVESTIGATE_NETWORK actions, spending steps to reveal information.
172
+
173
+ **What makes the learning novel:**
174
+
175
+ - The LLM (inference via AWS Bedrock) cannot be fine-tuned — it is a black-box API.
176
+ - The agent learns via **Reflexion**: post-episode lessons are written back into memory and injected into every future prompt.
177
+ - A **dynamic hybrid policy** (α-weighted) blends the LLM with a deterministic rule engine, with the blend weight α updating based on recent win rate. Rules dominate early; the LLM takes over as it proves itself.
178
+
179
+ ### System Architecture
180
+
181
+ ![System Architecture](assets/sys%20arch.png)
182
+
183
+ ---
184
+
185
+ ## 2. The Problem: How Fake Detection Actually Works
186
+
187
+ A real-world fake account detector does **not** read post content. Detection relies on three categories of signals computed from metadata:
188
+
189
+ ### Signal Hierarchy (Node -> Behavioral -> Graph)
190
+
191
+ ![Signal Hierarchy](assets/gs.png)
192
+
193
+ - **Node signals (offline):** content fingerprints like photo reuse, bio-template similarity, and comment repetition provide the first suspicion layer.
194
+ - **Behavioral signals (temporal/device):** coordinated posting hour, account-age clustering, and shared IP subnet add stronger gang-level evidence.
195
+ - **Graph signals (live at INSPECT):** mutual follows, flagged-neighbor growth, and cluster alignment are hardest to evade, so they carry the highest weight in risk scoring.
196
+ - **False-positive control:** high-legitimacy hubs (for example celebrities) are down-weighted through hub-legitimacy discounting.
197
+
198
+ ---
199
+
200
+ ## 3. Synthetic Data Generation
201
+
202
+ **File:** `server/generator.py`
203
+
204
+ Episodes are generated deterministically by seed. 150 episodes are pre-generated (50 per task) and cached as JSON files in `episodes/`.
205
+
206
+ ### Network Composition
207
+
208
+ | Task | Network size | Gang | Decoys | Real | Max steps |
209
+ |---|---|---|---|---|---|
210
+ | easy | 50 | 10 | 0 | 40 | 30 |
211
+ | medium | 200 | 10 | 20 | 170 | 50 |
212
+ | hard | 1000 | 10 | 50 | 940 | 80 |
213
+
214
+ - **Gang accounts:** All 10 share `base_age` (same creation week), tightly clustered `avg_post_hour`, high `photo_reuse_score`/`bio_template_score`, `comment_repeat_score` in [0.60, 0.90], `ip_cluster_id = "ip_gang_{seed}"`, and dense intra-gang follow edges (density 0.60–0.80).
215
+ - **Real accounts:** Log-normal follower distributions, unique IP clusters, low fake scores.
216
+ - **Decoy accounts** (medium/hard): Real accounts with elevated fraud scores (0.20–0.40 range) — they look suspicious but are NOT gang members and penalise reckless flagging.
217
+ - **Celebrity accounts** (2 per episode): 100k–5M followers, very low fake scores, high `hub_legitimacy_score`.
218
+ - **Zero-edge isolates** (2 per episode): No edges — test whether the agent wastes steps on disconnected nodes.
219
+
220
+ ---
221
+
222
+ ## 4. Data Model
223
+
224
+ **File:** `models.py`
225
+
226
+ ### ActionType
227
+
228
+ | Value | Cost | Effect |
229
+ |---|---|---|
230
+ | `inspect` | 1 step | Reveals full `AccountProfile` + follow list |
231
+ | `investigate_network` | 2 steps | Expands 2 hops; reveals account IDs only |
232
+ | `flag` | 0 steps | Marks account as gang member; triggers SUSPECT cascade |
233
+ | `unflag` | 0 steps | Removes flag; clears CONFIRMED_FAKE status |
234
+ | `submit` | 0 steps | Ends episode; triggers scoring |
235
+
236
+ ### AccountProfile — key fields
237
+
238
+ | Category | Fields |
239
+ |---|---|
240
+ | Raw counts | `follower_count`, `following_count`, `post_count` |
241
+ | Temporal | `avg_post_hour`, `account_age_days` |
242
+ | Content pipeline (0–1) | `photo_reuse_score`, `bio_template_score`, `comment_repeat_score` |
243
+ | IP/device | `shared_ip_count`, `ip_cluster_id` |
244
+ | Graph (live at INSPECT) | `mutual_follow_rate`, `flagged_neighbor_count`, `avg_neighbor_photo_reuse`, `post_hour_cluster_score` |
245
+ | Risk breakdown | `fake_risk_score`, `node_risk`, `behavior_risk`, `graph_risk`, `hub_legitimacy_score` |
246
+ | Evasion/status | `name_change_count`, `status` (NORMAL/SUSPECT/CONFIRMED_FAKE) |
247
+
248
+ ### FakeGangObservation — what the agent sees each step
249
+
250
+ `done`, `reward`, `visible_accounts`, `visible_account_ids`, `flagged_ids`, `inspected_ids`, `suspect_ids`, `graph_edges`, `steps_remaining`, `evasion_triggered`, `evasion_count`, `task`, `message`
251
+
252
+ ---
253
+
254
+ ## 5. The RL Environment
255
+
256
+ **File:** `server/environment.py`
257
+
258
+ ### Episode Lifecycle & Action Mechanics
259
+
260
+ ![Episode Flow](assets/episode.png)
261
+
262
+ **FLAG cascade (dual):** When FLAG(X) is called — (1) every visible account that X follows becomes SUSPECT via the follow-graph, and (2) every visible account sharing X's `ip_cluster_id` becomes SUSPECT. Gang members share `ip_gang_{seed}`; real accounts have unique IPs → zero false positives.
263
+
264
+ ### Reward Function
265
+
266
+ ```
267
+ base_reward = tp×1.0 − fp×0.5 − fn×0.3
268
+
269
+ Win condition:
270
+ easy/medium: recall ≥ 0.8 AND precision ≥ 0.7
271
+ hard: recall ≥ 0.9 AND precision ≥ 0.8
272
+
273
+ Bonuses:
274
+ +5.0 full win
275
+ +3.0 perfect recall
276
+ +2.0 partial win (high recall, low precision)
277
+ +1.0 efficiency (SUBMIT with ≥50% steps remaining)
278
+ −1.0 per evasion event (hard mode)
279
+ −2.0 forced submit (ran out of steps)
280
+ ```
281
+
282
+ ### Evasion (hard mode)
283
+
284
+ - **`unfollow_intragang`:** 30% of intra-gang edges randomly removed mid-investigation — destroys graph signal. Fires 4 times (steps 15, 30, 45, 60).
285
+ - **`rename_count`:** Random gang members get `name_change_count += 1` — a visual evasion signal.
286
+
287
+ ---
288
+
289
+ ## 6. Risk Scoring Mathematics
290
+
291
+ **File:** `server/scoring.py` — all functions are stateless and deterministic.
292
+
293
+ ![pipw](images/big.png)
294
+
295
+ ![Risk Scoring Formulas 1](assets/formulas-1.png)
296
+
297
+ ![Risk Scoring Formulas 2](assets/formulas-2.png)
298
+
299
+ ---
300
+
301
+ ## 8. The LLM Policy (Qwen3 via Bedrock)
302
+
303
+ **File:** `agent/policy.py`
304
+
305
+ **Model:** `qwen.qwen3-next-80b-a3b` via AWS Bedrock Converse API (`maxTokens=512, temperature=0.4`)
306
+
307
+ ### Prompt Structure
308
+
309
+ Every step, the policy builds a prompt from three components:
310
+
311
+ ```
312
+ [reflections from past episodes] ← grows richer every episode
313
+ [best trajectory few-shot example] ← best win ever, showing the full action log
314
+ ━━━ CURRENT CASE ━━━
315
+ [formatted observation] ← status badges, risk scores, suspect list
316
+ What is your next action?
317
+ ```
318
+
319
+ Accounts in the observation are **sorted by `fake_risk_score` descending**, with status badges prepended. `fnbr=N(!)` highlights when `flagged_neighbor_count > 0`; `[HUB?]` warns the LLM not to flag high-legitimacy accounts.
320
+
321
+ ### Required Response Format
322
+
323
+ ```xml
324
+ <thinking>
325
+ Reasoning — which account is most suspicious and why.
326
+ </thinking>
327
+ <action>
328
+ INSPECT acc_0041
329
+ </action>
330
+ ```
331
+
332
+ If parsing fails, a heuristic fallback inspects the highest-scored uninspected account. Retries use exponential backoff (1s, 2s, 4s) up to 3 attempts.
333
+
334
+ ---
335
+
336
+ ## 9. Reflexion — How the Agent Learns
337
+
338
+ **Files:** `agent/reflection.py`, `agent/memory.py`
339
+
340
+ The agent **cannot** update Qwen3's weights — Bedrock is a black-box API. Instead, it learns via **Reflexion**: post-episode lessons are written as text and injected into future prompts.
341
+
342
+ ### Reflexion Learning Loop
343
+
344
+ ![Reflexion Learning Loop](assets/reflexion.png)
345
+
346
+
347
+
348
+ ```
349
+ Episode N:
350
+ 1. LLM acts using: system_prompt + reflections[last 4] + best_trajectory
351
+ 2. Episode ends → WIN or LOSS
352
+ 3. Post-episode:
353
+ LOSS → generate_reflection(action_log, outcome) → lesson stored
354
+ WIN → save trajectory if better reward + generate_success_reflection
355
+
356
+ Episode N+1:
357
+ → last 4 reflections + best win trajectory injected into prompt
358
+ → LLM has learned from its past
359
+ ```
360
+
361
+ **Example generated reflection:**
362
+ > *"The starting accounts were all real; I wasted 8 steps inspecting low-signal nodes before pivoting. When photo_reuse and bio_template are both below 0.3 after 3 inspections, immediately use INVESTIGATE_NETWORK to jump to a different graph region."*
363
+
364
+ All memory persists in a Docker volume (`memory/`) across container restarts — reflections, best trajectories, win history, and α values per task.
365
+
366
+ ---
367
+
368
+ ## 10. Hybrid Policy — The Novel Contribution
369
+
370
+ **File:** `agent/hybrid_policy.py`
371
+
372
+ **Key insight:** A new LLM agent starts dumb but improves over time. A rule engine is always consistent but cannot adapt. The hybrid policy exploits both — rules provide a safety net early while the LLM builds its track record; once the LLM proves itself, rules step back.
373
+
374
+ ### Architecture
375
+
376
+ ![Hybrid Policy Architecture](assets/hybrid.png)
377
+
378
+ ### Alpha (α): The Trust Weight
379
+
380
+ α is a per-task value in [0.20, cap] representing current trust in the LLM:
381
+
382
+ ```
383
+ reflection_factor = min(1.0, n_reflections / 4.0)
384
+ raw = 0.20 + reflection_factor × (0.80 × recent_win_rate + 0.12)
385
+ α = clamp(raw, 0.20, cap)
386
+ ```
387
+
388
+ | Task | α cap | Rationale |
389
+ |---|---|---|
390
+ | easy | 0.50 | Rule engine alone achieves ~91% — LLM should assist, not override |
391
+ | medium | 0.70 | Decoys require some LLM judgment, but cascade must stay |
392
+ | hard | 0.85 | LLM needs latitude for evasion adaptation, but safety rules remain |
393
+
394
+ **Alpha trajectory over training (easy task, cap=0.50):**
395
+
396
+ | Episode | Win rate | Reflections | α (capped) |
397
+ |---|---|---|---|
398
+ | 1 | 0% | 0 | 0.20 |
399
+ | 5 | 20% | 4 | 0.48 |
400
+ | 10 | 50% | 9 | **0.50** |
401
+ | 20 | 80% | 19 | **0.50** |
402
+
403
+ <br>
404
+
405
+ ![System Architecture](images/plot.png)
406
+
407
+ ### Rule Confidence Levels
408
+
409
+ | Situation | Action | Confidence |
410
+ |---|---|---|
411
+ | Steps remaining = 0 | SUBMIT | 1.00 |
412
+ | Uninspected SUSPECT accounts exist | INSPECT suspects[0] | 0.95 |
413
+ | `fake_risk ≥ 0.85` | FLAG that account | 0.95 |
414
+ | `fake_risk` in [threshold, 0.85) | FLAG that account | 0.70+ |
415
+ | 10 accounts already flagged | SUBMIT | 0.85 |
416
+ | Steps remaining ≤ 3 | SUBMIT | 0.90 |
417
+ | Uninspected accounts available | INSPECT top candidate | 0.30 |
418
+
419
+ At **α=0.20** (early): rules dominate (~90% of decisions). At **α=0.50** (moderate): LLM controls exploration; rules control safety. At **α=0.85** (high): LLM controls most decisions; rules only override forced submits and uninspected suspects.
420
+
421
+ α is saved to `memory/alpha_{task}.json` and persists across Docker restarts — the agent doesn't reset to 0.20 every time.
422
+
423
+ ---
424
+
425
+ ## 11. Training Loop End-to-End
426
+
427
+ **File:** `train.py`
428
+
429
+ ### Curriculum
430
+
431
+ | Phase | Episodes | Task | Goal |
432
+ |---|---|---|---|
433
+ | 1 | 1–20 | easy | Learn basic signal thresholds, build first reflections |
434
+ | 2 | 21–35 | medium | Handle decoys, learn evasion response |
435
+ | 3 | 36–50 | hard | Feature-only detection, persistent evasion |
436
+
437
+ Seeds rotate deterministically: `seed = (episode_num + task_offset) % 50`
438
+
439
+ ### Per-Episode Flow
440
+
441
+ ```
442
+ for ep in range(n_episodes):
443
+
444
+ 1. DETERMINE TASK curriculum_task(ep) or fixed task
445
+ 2. COMPUTE ALPHA compute_alpha(win_rate, n_reflections, task)
446
+ 3. LOAD CONTEXT last 4 reflections + best win trajectory
447
+ 4. RUN EPISODE while not obs.done:
448
+ blend(rule_action, llm_action, rule_conf, α)
449
+ → obs = env.step(final)
450
+ 5. POST-EPISODE record_win → update α → generate reflection
451
+ 6. LOG task | win/loss | reward | recall | precision | α | modes
452
+ ```
453
+
454
+ Episode metrics (flushed to `runs/metrics.jsonl` every 5 episodes) include: `episode`, `task`, `won`, `reward`, `recall`, `precision`, `steps_used`, `alpha_used`, `mode_agree`, `mode_rule`, `mode_llm`, `n_reflections_used`.
455
+
456
+ You can watch the transition: early episodes have high `rule` counts; later episodes have high `agree` counts (LLM learned to make the same decisions as the rules, but also brings strategic reasoning the rules can't).
457
+
458
+ ---
459
+
460
+ ## 12. API Reference
461
+
462
+ **File:** `server/app.py`
463
+
464
+ | Endpoint | Method | Description |
465
+ |---|---|---|
466
+ | `/health` | GET | `{"status": "healthy"}` |
467
+ | `/tasks` | GET | Task list + `action_schema` + `score_range: [0.0, 1.0]` |
468
+ | `/reset` | POST | Accepts `{task, seed}` → returns initial observation |
469
+ | `/step` | POST | Accepts any `FakeGangAction` → returns updated observation |
470
+ | `/state` | GET | Current episode metadata (step count, task, score) |
471
+ | `/grader` | GET | Normalised [0.0, 1.0] score after SUBMIT |
472
+ | `/baseline` | POST | Runs rule-based agent on all 3 tasks, returns scores |
473
+
474
+ **Baseline performance:**
475
+
476
+ | Task | Seed=0 score | Win rate (50 seeds) | Mean score (50 seeds) |
477
+ |---|---|---|---|
478
+ | easy | 0.91 | 100% | ~0.91 |
479
+ | medium | 0.906 | 84% | ~0.77 |
480
+ | hard | 0.9038 | 52% | ~0.47 |
481
+
482
+ ---
483
+
484
+ ## 13. Docker Deployment
485
+
486
+ ```bash
487
+ # Build
488
+ docker build -f server/Dockerfile -t graphstrike .
489
+
490
+ # Run
491
+ docker run -it \
492
+ -e AWS_ACCESS_KEY_ID=your_key \
493
+ -e AWS_SECRET_ACCESS_KEY=your_secret \
494
+ -v $(pwd)/memory:/app/memory \
495
+ -v $(pwd)/runs:/app/runs \
496
+ -p 8000:8000 \
497
+ graphstrike
498
+ ```
499
+
500
+ The `memory/` and `runs/` volumes preserve all learning between container restarts.
501
+
502
+ ### Environment Variables
503
+
504
+ | Variable | Default | Description |
505
+ |---|---|---|
506
+ | `AWS_ACCESS_KEY_ID` | (required) | For Bedrock/Qwen3 access |
507
+ | `AWS_SECRET_ACCESS_KEY` | (required) | For Bedrock/Qwen3 access |
508
+ | `AWS_DEFAULT_REGION` | `us-east-1` | Bedrock region |
509
+ | `TRAIN_TASK` | (curriculum) | Fix to `easy`/`medium`/`hard` |
510
+ | `TRAIN_EPISODES` | `50` | Total training episodes |
511
+ | `TRAIN_TEMP` | `0.4` | LLM sampling temperature |
512
+ | `TRAIN_VERBOSE` | `0` | Set `1` for per-step action logging |
513
+ | `SERVER_PORT` | `8000` | FastAPI port |
514
+
515
+ ### Startup Sequence (`run.sh`)
516
+
517
+ ```
518
+ 1. Validate AWS credentials
519
+ 2. python server/generator.py → generates 150 episode JSON files
520
+ 3. uvicorn server.app:app → starts the environment server
521
+ 4. Health check polling → waits until /health responds
522
+ 5. python train.py → runs the full training loop
523
+ ```
524
+
525
+ ---
526
+
527
+
528
+ ### Full HTTP validation
529
+
530
+ ```bash
531
+ python3 -m uvicorn server.app:app --port 8001 &
532
+ sleep 3
533
+ python3 validate.py --url http://localhost:8001
534
+ # Expected: Results: 24/24 passed — all OK
535
+ ```
536
+
537
+ ### Deployed Endpoint Verification
538
+
539
+ ```bash
540
+ curl https://pandago-graphstrike.hf.space/health
541
+ # → {"status": "healthy"}
542
+
543
+ curl https://pandago-graphstrike.hf.space/tasks
544
+ # → {"tasks": ["easy","medium","hard"], "action_schema": {...}, "score_range": [0.0, 1.0]}
545
+
546
+ curl -X POST https://pandago-graphstrike.hf.space/baseline
547
+ # → {"scores": {"easy": 0.91, "medium": 0.906, "hard": 0.9038}, "agent": "rule_based"}
548
+ ```
549
+
550
+ ---
551
+
552
+ ![Material wave loading](https://github.com/user-attachments/assets/a08255eb-9647-471d-9881-61871332249f)
553
+
554
+ ## Developed with ❤️ by Team ComputeXOR
555
+
556
+ ### {
557
+
558
+ ### [Sai Nivedh](https://github.com/SaiNivedh26) ,
559
+
560
+ ### [Charuvarthan](https://github.com/Charuvarthan-T) ,
561
+
562
+ ### [Sajeev](https://github.com/SajeevSenthil)
563
+
564
+ ### }
__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # GraphStrike — OpenEnv environment for coordinated fake account ring detection
agent/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Agent package — LLM policy, memory, and reflection for the Fake Gang Detection env
agent/hybrid_policy.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hybrid confidence-weighted policy for the Fake Gang Detection agent.
2
+
3
+ Blends a deterministic rule-based agent with the LLM (Qwen3) agent
4
+ using a dynamic trust weight α ∈ [0.20, 1.00]:
5
+
6
+ α = 0.20 + reflection_factor × (0.80 × recent_win_rate + 0.12)
7
+
8
+ Action selection when LLM and rules DISAGREE:
9
+ • rules win if rule_confidence >= α (α low → rules trusted more)
10
+ • LLM wins if rule_confidence < α (α high → LLM trusted more)
11
+ When they AGREE the action is used as-is (mode="agree").
12
+
13
+ α dynamics:
14
+ α starts at 0.20 (rules dominate — LLM has no history yet).
15
+ As the LLM accumulates wins and reflections, α climbs toward 1.0.
16
+ Episode 1, 0 wins → α ≈ 0.20 (almost always rule-guided)
17
+ Episode 20, 70% wins, 4 reflections → α ≈ 0.76 (LLM leads, rules as safety net)
18
+ Episode 40, 90% wins, 8 reflections → α ≈ 0.92 (LLM trusted, rules intervene rarely)
19
+
20
+ Rule confidence levels:
21
+ 1.00 — forced SUBMIT (out of steps)
22
+ 0.95 — INSPECT a SUSPECT account (cascade-elevated neighbor)
23
+ 0.90 — FLAG an account with fake_risk_score ≥ 0.85
24
+ 0.80 — SUBMIT with 10 flags in place
25
+ 0.70 — FLAG an account with fake_risk_score in [threshold, 0.85)
26
+ 0.30 — exploratory INSPECT (no strong signal, just scanning)
27
+
28
+ This means:
29
+ • At α=0.20 → rules win all disagreements (confidence always ≥ 0.20)
30
+ • At α=0.50 → rules win when confidence ≥ 0.50 (suspect/high-risk actions)
31
+ • At α=0.80 → rules win only when confidence ≥ 0.80 (critical overrides only)
32
+ • At α=1.00 → rules never win (pure LLM mode)
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import sys
38
+ from pathlib import Path
39
+ from typing import Dict, List, Optional, Tuple
40
+
41
+ _ROOT = Path(__file__).parent.parent
42
+ sys.path.insert(0, str(_ROOT))
43
+ sys.path.insert(0, str(_ROOT / "server"))
44
+
45
+ from models import ActionType, FakeGangAction, FakeGangObservation
46
+ from agent.policy import get_action
47
+
48
+ # Per-task thresholds mirror inference.py
49
+ _THRESHOLDS: Dict[str, float] = {
50
+ "easy": 0.60,
51
+ "medium": 0.50,
52
+ "hard": 0.45,
53
+ }
54
+
55
+ # Bootstrap raw-feature score — same calibration as inference.py
56
+ # 0.30*photo + 0.20*bio + 0.50*comment_repeat >= 0.40
57
+ # Gang member (any task): ~0.57–0.78; decoy: ~0.25; real: ~0.07
58
+ _BOOTSTRAP_RAW_THRESHOLD = 0.40
59
+ _SHARED_IP_GANG_THRESHOLD = 5
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Rule-based single-step decision
64
+ # ---------------------------------------------------------------------------
65
+
66
+ def get_rule_action(obs: FakeGangObservation) -> Tuple[FakeGangAction, float]:
67
+ """Return the rule-based action for the current observation and its confidence.
68
+
69
+ Returns:
70
+ (action, confidence) where confidence ∈ [0.0, 1.0]
71
+ """
72
+ threshold = _THRESHOLDS.get(obs.task, 0.50)
73
+
74
+ # Priority 1 — forced end (out of steps)
75
+ if obs.steps_remaining <= 0:
76
+ return FakeGangAction(action_type=ActionType.SUBMIT), 1.00
77
+
78
+ # Priority 2 — INSPECT SUSPECT accounts (auto-cascaded from FLAG)
79
+ uninspected_suspects = [s for s in obs.suspect_ids if s not in obs.inspected_ids]
80
+ if uninspected_suspects:
81
+ return (
82
+ FakeGangAction(action_type=ActionType.INSPECT, account_id=uninspected_suspects[0]),
83
+ 0.95,
84
+ )
85
+
86
+ # Priority 3 — FLAG high-risk inspected accounts
87
+ # Two signal paths: composite fake_risk (active post-cascade) OR bootstrap raw
88
+ # node score (catches first gang members before graph signals are established).
89
+ for p in sorted(obs.visible_accounts, key=lambda x: x.fake_risk_score, reverse=True):
90
+ if p.account_id in obs.flagged_ids:
91
+ continue
92
+ if p.hub_legitimacy_score > 0.75:
93
+ continue # protect celebrities
94
+
95
+ bootstrap_raw = (
96
+ 0.30 * p.photo_reuse_score
97
+ + 0.20 * p.bio_template_score
98
+ + 0.50 * p.comment_repeat_score
99
+ )
100
+
101
+ if p.shared_ip_count >= _SHARED_IP_GANG_THRESHOLD:
102
+ return FakeGangAction(action_type=ActionType.FLAG, account_id=p.account_id), 0.97
103
+
104
+ if p.fake_risk_score >= threshold:
105
+ confidence = min(0.95, 0.70 + (p.fake_risk_score - threshold) * 0.60)
106
+ return FakeGangAction(action_type=ActionType.FLAG, account_id=p.account_id), confidence
107
+
108
+ if bootstrap_raw >= _BOOTSTRAP_RAW_THRESHOLD:
109
+ # Bootstrap confidence: how far above threshold the raw score is
110
+ confidence = min(0.88, 0.60 + (bootstrap_raw - _BOOTSTRAP_RAW_THRESHOLD) * 0.80)
111
+ return FakeGangAction(action_type=ActionType.FLAG, account_id=p.account_id), confidence
112
+
113
+ # Priority 4 — INVESTIGATE_NETWORK to chain through the gang cluster.
114
+ #
115
+ # The problem: FLAG only cascades SUSPECT to already-visible neighbors. Gang members
116
+ # follow each other but aren't visible until we expand the graph. INVESTIGATE_NETWORK
117
+ # (2-hop expansion) + the environment's re-cascade makes them SUSPECT immediately,
118
+ # so Priority 2 picks them up in the next step.
119
+ #
120
+ # How many investigations have we done this episode?
121
+ # Each INVESTIGATE_NETWORK on an already-inspected account: +2 steps, +0 inspected_ids.
122
+ # Each INSPECT: +1 step, +1 inspected_ids.
123
+ # So: n_investigate ≈ (steps_used - len(inspected_ids)) // 2
124
+ # This self-throttles: fires once per newly flagged account (one investigation per flag).
125
+ #
126
+ # Max investigations per episode is task-capped to protect the step budget:
127
+ # easy=1 (30 step budget, intra-gang density 0.80 → one expand covers all)
128
+ # medium=2 (50 steps, density 0.70 + evasion)
129
+ # hard=3 (80 steps, density 0.60 + heavy evasion)
130
+ _max_investigate = {"easy": 1, "medium": 2, "hard": 3}
131
+ _max_steps_map = {"easy": 30, "medium": 50, "hard": 80}
132
+ if obs.flagged_ids and obs.steps_remaining > 4:
133
+ _steps_used = _max_steps_map.get(obs.task, 50) - obs.steps_remaining
134
+ _n_inv = max(0, (_steps_used - len(obs.inspected_ids)) // 2)
135
+ _n_max = _max_investigate.get(obs.task, 2)
136
+ if _n_inv < min(_n_max, len(obs.flagged_ids)):
137
+ target = obs.flagged_ids[_n_inv] # cycle: 1st flag → 1st investigate, etc.
138
+ return (
139
+ FakeGangAction(action_type=ActionType.INVESTIGATE_NETWORK, account_id=target),
140
+ 0.87,
141
+ )
142
+
143
+ # Priority 5 — SUBMIT if fully confident (10 flagged or almost out of steps)
144
+ if len(obs.flagged_ids) >= 10:
145
+ return FakeGangAction(action_type=ActionType.SUBMIT), 0.85
146
+
147
+ if obs.steps_remaining <= 3:
148
+ return FakeGangAction(action_type=ActionType.SUBMIT), 0.90
149
+
150
+ # Priority 6 — INSPECT the highest-risk uninspected account (exploratory)
151
+ uninspected = [i for i in obs.visible_account_ids if i not in obs.inspected_ids]
152
+ if uninspected:
153
+ # Sort by SUSPECT status first, then by insertion order (stable sort).
154
+ # Insertion order = the order accounts entered visible_account_ids:
155
+ # starting_visible accounts come first (added at reset), then accounts
156
+ # revealed by subsequent INSPECTs. This prevents low-ID real accounts
157
+ # from perpetually cutting ahead of gang members that appear early in
158
+ # starting_visible (critical for hard task with 1000-account networks).
159
+ suspects_set = set(obs.suspect_ids)
160
+ uninspected.sort(key=lambda i: (i not in suspects_set,)) # stable → preserves insertion order
161
+ return FakeGangAction(action_type=ActionType.INSPECT, account_id=uninspected[0]), 0.30
162
+
163
+ # Fallback
164
+ return FakeGangAction(action_type=ActionType.SUBMIT), 0.75
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Alpha computation
169
+ # ---------------------------------------------------------------------------
170
+
171
+ def compute_alpha(recent_win_rate: float, n_reflections: int, task: str = "easy") -> float:
172
+ """Compute α (LLM trust weight) from recent performance.
173
+
174
+ α = 0.20 + reflection_factor × (0.80 × win_rate + 0.12)
175
+
176
+ reflection_factor ramps from 0 → 1 as reflections accumulate (0–4).
177
+ A reflection bonus of +0.12 × reflection_factor ensures α rises above 0.30
178
+ even with 0% wins after ≥4 reflections. This breaks the chicken-and-egg
179
+ deadlock on medium/hard: without any wins, α was stuck at 0.20 forever,
180
+ meaning the LLM never took exploratory INSPECT steps and could never
181
+ accumulate wins to push α higher.
182
+
183
+ Per-task caps prevent α from climbing so high that the LLM overrides
184
+ correct rule-engine decisions (Priority 2 suspect conf=0.95, FLAG conf=0.90).
185
+ """
186
+ # Per-task α ceiling: rule engine keeps authority on high-confidence actions
187
+ _alpha_cap = {"easy": 0.50, "medium": 0.70, "hard": 0.85}
188
+ cap = _alpha_cap.get(task, 0.70)
189
+
190
+ reflection_factor = min(1.0, n_reflections / 4.0)
191
+ raw = 0.20 + reflection_factor * (0.80 * recent_win_rate + 0.12)
192
+ return round(max(0.20, min(cap, raw)), 3)
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # Hybrid decision
197
+ # ---------------------------------------------------------------------------
198
+
199
+ def get_hybrid_action(
200
+ obs: FakeGangObservation,
201
+ reflections: List[str],
202
+ few_shot_example: Optional[dict] = None,
203
+ alpha: float = 0.30,
204
+ temperature: float = 0.40,
205
+ ) -> Tuple[FakeGangAction, str, str]:
206
+ """Return the blended action, the raw LLM output, and the decision mode.
207
+
208
+ Decision logic:
209
+ 1. Get rule action + confidence
210
+ 2. Get LLM action (with Reflexion context)
211
+ 3. If they agree → "agree" (unanimous)
212
+ 4. If disagree:
213
+ - rule_confidence >= alpha → rule wins ("rule_override")
214
+ - rule_confidence < alpha → LLM wins ("llm")
215
+
216
+ Returns:
217
+ (action, raw_llm_output, mode_str)
218
+
219
+ mode_str is one of:
220
+ "agree" — both said the same thing
221
+ "rule_override(c=X,α=Y)" — rule overrode LLM
222
+ "llm(c=X,α=Y)" — LLM won over rule
223
+ """
224
+ rule_action, rule_conf = get_rule_action(obs)
225
+ llm_action, raw_llm = get_action(
226
+ obs=obs,
227
+ reflections=reflections,
228
+ few_shot_example=few_shot_example,
229
+ temperature=temperature,
230
+ )
231
+
232
+ agree = (
233
+ rule_action.action_type == llm_action.action_type
234
+ and rule_action.account_id == llm_action.account_id
235
+ )
236
+
237
+ if agree:
238
+ return llm_action, raw_llm, "agree"
239
+
240
+ if rule_conf >= alpha:
241
+ mode = f"rule_override(c={rule_conf:.2f},α={alpha:.2f})"
242
+ return rule_action, raw_llm, mode
243
+ else:
244
+ mode = f"llm(c={rule_conf:.2f}<α={alpha:.2f})"
245
+ return llm_action, raw_llm, mode
agent/memory.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Persistent episodic memory for the LLM detective agent.
2
+
3
+ Stores:
4
+ - Reflections: short lessons the agent generates after each episode
5
+ - Best trajectories: full action logs from high-reward episodes (used as few-shot examples)
6
+
7
+ All data is written to disk so learning persists across container restarts
8
+ when the memory/ directory is mounted as a Docker volume.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ MEMORY_DIR = Path(__file__).parent.parent / "memory"
18
+
19
+
20
+ class AgentMemory:
21
+ """Disk-backed memory for reflections and successful trajectories."""
22
+
23
+ def __init__(self, memory_dir: Path = MEMORY_DIR) -> None:
24
+ self.memory_dir = memory_dir
25
+ self.memory_dir.mkdir(parents=True, exist_ok=True)
26
+
27
+ # ------------------------------------------------------------------
28
+ # Reflections (one JSONL file per task)
29
+ # ------------------------------------------------------------------
30
+
31
+ def _reflections_path(self, task: str) -> Path:
32
+ return self.memory_dir / f"reflections_{task}.jsonl"
33
+
34
+ def add_reflection(
35
+ self,
36
+ task: str,
37
+ text: str,
38
+ episode_num: int,
39
+ reward: float,
40
+ ) -> None:
41
+ entry = {
42
+ "episode": episode_num,
43
+ "reward": round(reward, 3),
44
+ "reflection": text.strip(),
45
+ }
46
+ with open(self._reflections_path(task), "a") as f:
47
+ f.write(json.dumps(entry) + "\n")
48
+
49
+ def get_reflections(self, task: str, n: int = 4) -> List[str]:
50
+ """Return the n most recent reflection texts for a task."""
51
+ path = self._reflections_path(task)
52
+ if not path.exists():
53
+ return []
54
+ lines = path.read_text().strip().splitlines()
55
+ entries = []
56
+ for line in lines:
57
+ try:
58
+ entries.append(json.loads(line))
59
+ except json.JSONDecodeError:
60
+ pass
61
+ # Return the last n reflections
62
+ return [e["reflection"] for e in entries[-n:]]
63
+
64
+ def reflection_count(self, task: str) -> int:
65
+ path = self._reflections_path(task)
66
+ if not path.exists():
67
+ return 0
68
+ return sum(1 for line in path.read_text().splitlines() if line.strip())
69
+
70
+ # ------------------------------------------------------------------
71
+ # Best trajectory (one JSON file per task — stores single best run)
72
+ # ------------------------------------------------------------------
73
+
74
+ def _trajectory_path(self, task: str) -> Path:
75
+ return self.memory_dir / f"best_trajectory_{task}.json"
76
+
77
+ def add_trajectory(
78
+ self,
79
+ task: str,
80
+ action_log: List[str],
81
+ final_message: str,
82
+ reward: float,
83
+ episode_num: int,
84
+ ) -> bool:
85
+ """Save trajectory if it's better than the current best. Returns True if saved."""
86
+ path = self._trajectory_path(task)
87
+ current_best_reward = -999.0
88
+ if path.exists():
89
+ try:
90
+ current_best_reward = json.loads(path.read_text()).get("reward", -999.0)
91
+ except (json.JSONDecodeError, KeyError):
92
+ pass
93
+
94
+ if reward > current_best_reward:
95
+ data = {
96
+ "task": task,
97
+ "episode": episode_num,
98
+ "reward": round(reward, 3),
99
+ "action_log": action_log,
100
+ "final_message": final_message,
101
+ }
102
+ path.write_text(json.dumps(data, indent=2))
103
+ return True
104
+ return False
105
+
106
+ def get_best_trajectory(self, task: str) -> Optional[Dict[str, Any]]:
107
+ """Return best saved trajectory for task, or None."""
108
+ path = self._trajectory_path(task)
109
+ if not path.exists():
110
+ return None
111
+ try:
112
+ return json.loads(path.read_text())
113
+ except json.JSONDecodeError:
114
+ return None
115
+
116
+ # ------------------------------------------------------------------
117
+ # Win history + alpha persistence
118
+ # ------------------------------------------------------------------
119
+
120
+ def _wins_path(self, task: str) -> Path:
121
+ return self.memory_dir / f"wins_{task}.jsonl"
122
+
123
+ def _alpha_path(self, task: str) -> Path:
124
+ return self.memory_dir / f"alpha_{task}.json"
125
+
126
+ def record_win(self, task: str, won: bool, episode_num: int) -> None:
127
+ """Append an episode outcome to the win history for this task."""
128
+ entry = {"episode": episode_num, "won": won}
129
+ with open(self._wins_path(task), "a") as f:
130
+ f.write(json.dumps(entry) + "\n")
131
+
132
+ def recent_win_rate(self, task: str, n: int = 10) -> float:
133
+ """Return win rate over the last n episodes for this task."""
134
+ path = self._wins_path(task)
135
+ if not path.exists():
136
+ return 0.0
137
+ entries = []
138
+ for line in path.read_text().strip().splitlines():
139
+ try:
140
+ entries.append(json.loads(line))
141
+ except json.JSONDecodeError:
142
+ pass
143
+ window = entries[-n:]
144
+ if not window:
145
+ return 0.0
146
+ return sum(1 for e in window if e["won"]) / len(window)
147
+
148
+ def save_alpha(self, task: str, alpha: float) -> None:
149
+ """Persist the current α (LLM trust weight) for a task."""
150
+ self._alpha_path(task).write_text(json.dumps({"alpha": round(alpha, 3)}))
151
+
152
+ def load_alpha(self, task: str, default: float = 0.20) -> float:
153
+ """Load persisted α, or return default if not saved yet."""
154
+ path = self._alpha_path(task)
155
+ if not path.exists():
156
+ return default
157
+ try:
158
+ return json.loads(path.read_text()).get("alpha", default)
159
+ except (json.JSONDecodeError, KeyError):
160
+ return default
161
+
162
+ # ------------------------------------------------------------------
163
+ # Summary
164
+ # ------------------------------------------------------------------
165
+
166
+ def summary(self) -> str:
167
+ lines = ["=== Agent Memory ==="]
168
+ for task in ["easy", "medium", "hard"]:
169
+ n_ref = self.reflection_count(task)
170
+ best = self.get_best_trajectory(task)
171
+ best_r = f"{best['reward']:+.2f}" if best else "none"
172
+ alpha = self.load_alpha(task)
173
+ wr = self.recent_win_rate(task, n=10)
174
+ lines.append(
175
+ f" {task:6s}: {n_ref:3d} reflections | best reward: {best_r} "
176
+ f"| α={alpha:.2f} | wr(last10)={wr:.0%}"
177
+ )
178
+ return "\n".join(lines)
agent/policy.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LLM policy for the Fake Gang Detection agent.
2
+
3
+ Uses Qwen3 via AWS Bedrock (configured in bedrock_model.py) to:
4
+ 1. Format the current observation into a readable detective briefing
5
+ 2. Inject past reflections and a best-trajectory few-shot example
6
+ 3. Call Qwen to reason and output the next action
7
+ 4. Parse the LLM text output into a typed FakeGangAction
8
+
9
+ Learning mechanism:
10
+ The agent does NOT fine-tune model weights (Bedrock is a black-box API).
11
+ Instead it learns via **Reflexion** — accumulated reflections and successful
12
+ episode demonstrations are injected into every prompt, causing measurably
13
+ better decisions over time.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ import sys
20
+ import time
21
+ from pathlib import Path
22
+ from typing import Dict, List, Optional, Tuple
23
+
24
+ # Resolve imports regardless of working directory
25
+ _ROOT = Path(__file__).parent.parent
26
+ sys.path.insert(0, str(_ROOT))
27
+ sys.path.insert(0, str(_ROOT / "server"))
28
+
29
+ from bedrock_model import invoke_qwen
30
+ from models import FakeGangAction, FakeGangObservation, ActionType
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # System prompt — static, sent as the "system" role to Qwen
34
+ # ---------------------------------------------------------------------------
35
+
36
+ SYSTEM_PROMPT = """\
37
+ You are an elite digital forensics detective. Your mission: identify ALL 10 members \
38
+ of a coordinated fake account gang hidden inside a large social network.
39
+
40
+ ━━━ AVAILABLE ACTIONS ━━━
41
+ GET_POLICY → retrieve platform policy + threshold (FREE — call first!)
42
+ INSPECT <account_id> → reveal full profile + follow list (costs 1 step)
43
+ REVERSE_IMAGE_SEARCH <account_id> → reveal photo_reuse_score (costs 1 step)
44
+ ANALYZE_BIO <account_id> → reveal bio_template_score (costs 1 step)
45
+ CHECK_IP <account_id> → reveal ip_cluster_id (costs 2 steps)
46
+ INVESTIGATE_NETWORK <account_id> → expand graph 2 hops, find new IDs (costs 2 steps)
47
+ FLAG <account_id> → mark account as gang member (free)
48
+ UNFLAG <account_id> → remove a flag (free)
49
+ SUBMIT → finalise your answer, end episode
50
+
51
+ ━━━ ROUND 2: PLATFORM-ADAPTIVE DETECTION ━━━
52
+ ⚠ CRITICAL: Call GET_POLICY first! Each episode runs on a specific platform (Instagram/Snapchat)
53
+ with different detection thresholds and cost models:
54
+
55
+ • Instagram: θ = 0.08 (STRICT) — FP penalty = 0.1 (HIGH precision required)
56
+ Primary signal: photo_reuse (content-focused enforcement)
57
+ Strategy: Only flag when fake_risk >= 0.08 AND primary signals confirmed
58
+
59
+ • Snapchat: θ = 0.74 (LENIENT) — FP penalty = 0.01 (HIGH recall required)
60
+ Primary signal: bio_template (behavior-focused enforcement)
61
+ Strategy: Flag aggressively when fake_risk >= 0.74, maximize recall
62
+
63
+ ━━━ HIDDEN SIGNALS (Round 2) ━━━
64
+ ⚠ photo_reuse_score, bio_template_score, ip_cluster_id start as 0.0/None!
65
+ You MUST use tool actions to reveal them:
66
+
67
+ • REVERSE_IMAGE_SEARCH → reveals photo_reuse_score (costs 1 step)
68
+ Use when: Instagram platform, or profile looks suspicious but fake_risk borderline
69
+
70
+ • ANALYZE_BIO → reveals bio_template_score (costs 1 step)
71
+ Use when: Snapchat platform, or comment_repeat_score already elevated
72
+
73
+ • CHECK_IP → reveals ip_cluster_id (costs 2 steps — expensive!)
74
+ Use when: Multiple accounts with similar patterns (shared_ip_count > 5)
75
+
76
+ Strategy: Don't blindly call tools on every account. Use tools strategically on:
77
+ 1. Accounts with status=SUSPECT (flagged neighbor cascade)
78
+ 2. Borderline accounts (fake_risk near platform threshold)
79
+ 3. Primary signal for the platform (photo_reuse for Instagram, bio_template for Snapchat)
80
+
81
+ ━━━ RISK SCORE GUIDE (platform-adaptive) ━━━
82
+ fake_risk_score computation changes based on platform's primary signal:
83
+ • If photo_reuse is primary → node_risk weighted 0.45 (vs default 0.30)
84
+ • If ip_cluster is primary → behavior_risk weighted 0.40 (vs default 0.25)
85
+
86
+ Thresholds (use platform threshold from GET_POLICY):
87
+ • fake_risk >= θ + 0.25 → CONFIRMED_FAKE status, safe to FLAG
88
+ • θ <= fake_risk < θ + 0.25 → SUSPECT status, investigate with tools first
89
+ • fake_risk < θ → NORMAL status, skip unless connected to gang cluster
90
+ • hub_legitimacy_score > 0.70 → likely celebrity, do NOT flag
91
+
92
+ ━━━ RAW SIGNAL THRESHOLDS ━━━
93
+ • photo_reuse_score > 0.5 → stealing celebrity photos (strong on Instagram)
94
+ • bio_template_score > 0.4 → copy-paste bios (strong on Snapchat)
95
+ • comment_repeat_score > 0.6 → copy-paste spam comments
96
+ • shared_ip_count > 5 → sharing IP subnet (hint: use CHECK_IP to confirm cluster)
97
+ • mutual_follow_rate > 0.6 → gang members mutually inflate each other
98
+ • avg_post_hour clustered → all posting in same narrow time window
99
+
100
+ ━━━ CORE STRATEGY (Round 2) ━━━
101
+ 1. Call GET_POLICY first → learn platform (Instagram/Snapchat) + threshold + primary signal
102
+ 2. INSPECT starting accounts → identify high fake_risk_score accounts
103
+ 3. Use tool actions strategically:
104
+ - Instagram: REVERSE_IMAGE_SEARCH on borderline suspects
105
+ - Snapchat: ANALYZE_BIO on borderline suspects
106
+ - CHECK_IP only when shared_ip_count > 5 (confirm gang cluster)
107
+ 4. INSPECT any status=SUSPECT accounts (cascade from flagged neighbors)
108
+ 5. FLAG when fake_risk >= platform threshold + confirmed signals
109
+ 6. SUBMIT when you have ~10 flagged OR steps_remaining < 5
110
+ 7. Platform bonuses:
111
+ - Instagram: +2.0 reward if precision >= 0.95 (avoid FPs!)
112
+ - Snapchat: +2.0 reward if recall >= 0.95 (catch all fakes!)
113
+
114
+ ━━━ RESPONSE FORMAT (always use this exactly) ━━━
115
+ <thinking>
116
+ [your reasoning — platform context, which signals to investigate, which account is most suspicious and why]
117
+ </thinking>
118
+ <action>
119
+ [exactly one action, e.g.: GET_POLICY or INSPECT acc_0042 or REVERSE_IMAGE_SEARCH acc_0007 or SUBMIT]
120
+ </action>\
121
+ """
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Observation formatter
125
+ # ---------------------------------------------------------------------------
126
+
127
+ _STATUS_BADGE = {
128
+ "confirmed_fake": "CONFIRMED_FAKE",
129
+ "suspect": "SUSPECT ",
130
+ "normal": "NORMAL ",
131
+ }
132
+
133
+
134
+ def _format_observation(obs: FakeGangObservation) -> str:
135
+ flagged_str = ", ".join(obs.flagged_ids) if obs.flagged_ids else "none yet"
136
+ uninspected = [i for i in obs.visible_account_ids if i not in obs.inspected_ids]
137
+ suspect_uninspected = [s for s in obs.suspect_ids if s not in obs.inspected_ids]
138
+
139
+ # Round 2: Show platform context if available
140
+ platform_info = ""
141
+ if hasattr(obs, "platform") and obs.platform:
142
+ platform_info = f" | PLATFORM: {obs.platform}"
143
+
144
+ lines = [
145
+ f"TASK: {obs.task.upper()}{platform_info} | Steps remaining: {obs.steps_remaining}",
146
+ f"Evasion triggered: {obs.evasion_triggered} (events so far: {obs.evasion_count})",
147
+ f"Currently flagged ({len(obs.flagged_ids)}/10): {flagged_str}",
148
+ f"Accounts inspected: {len(obs.inspected_ids)} | Not yet inspected: {len(uninspected)}",
149
+ ]
150
+ if suspect_uninspected:
151
+ lines.append(
152
+ f"SUSPECTS not yet inspected ({len(suspect_uninspected)}): "
153
+ + ", ".join(suspect_uninspected[:8])
154
+ + (f" +{len(suspect_uninspected)-8} more" if len(suspect_uninspected) > 8 else "")
155
+ )
156
+ lines.append("")
157
+
158
+ if obs.visible_accounts:
159
+ lines.append("PROFILED ACCOUNTS (sorted by fake_risk_score — highest first):")
160
+ lines.append(
161
+ " [status | risk | node beh graph hub | photo bio mutual | comment ip_count]"
162
+ )
163
+ scored = sorted(obs.visible_accounts, key=lambda p: p.fake_risk_score, reverse=True)
164
+ for p in scored[:18]:
165
+ status_str = getattr(p.status, "value", str(p.status)) if hasattr(p, "status") else "normal"
166
+ badge = _STATUS_BADGE.get(status_str, "NORMAL ")
167
+ flagged_marker = " ◀ FLAGGED" if p.account_id in obs.flagged_ids else ""
168
+ nc = f" name_chg={p.name_change_count}" if p.name_change_count else ""
169
+ fnbr = f" fnbr={p.flagged_neighbor_count}(!)" if p.flagged_neighbor_count else ""
170
+ hub_warn = " [HUB?]" if p.hub_legitimacy_score > 0.70 else ""
171
+ lines.append(
172
+ f" {badge} {p.account_id}{flagged_marker}: "
173
+ f"risk={p.fake_risk_score:.3f} | "
174
+ f"node={p.node_risk:.2f} beh={p.behavior_risk:.2f} "
175
+ f"graph={p.graph_risk:.2f} hub={p.hub_legitimacy_score:.2f}{hub_warn} | "
176
+ f"photo={p.photo_reuse_score:.3f} bio={p.bio_template_score:.3f} "
177
+ f"mutual={p.mutual_follow_rate:.2f}"
178
+ f"{fnbr}{nc}"
179
+ )
180
+ if len(obs.visible_accounts) > 18:
181
+ lines.append(f" … +{len(obs.visible_accounts) - 18} more inspected accounts")
182
+ else:
183
+ lines.append("No accounts profiled yet — pick one from the known IDs below and INSPECT it.")
184
+
185
+ if uninspected:
186
+ sample = uninspected[:12]
187
+ more = f" +{len(uninspected)-12} more" if len(uninspected) > 12 else ""
188
+ lines.append(f"\nKNOWN UNINSPECTED IDs: {', '.join(sample)}{more}")
189
+
190
+ lines.append(f"\nEnvironment message: {obs.message}")
191
+ return "\n".join(lines)
192
+
193
+
194
+ def _format_reflections(reflections: List[str]) -> str:
195
+ if not reflections:
196
+ return ""
197
+ parts = ["\n━━━ LESSONS FROM YOUR PAST CASES ━━━"]
198
+ for i, r in enumerate(reflections, 1):
199
+ parts.append(f"{i}. {r.strip()}")
200
+ return "\n".join(parts)
201
+
202
+
203
+ def _format_few_shot(example: Optional[Dict]) -> str:
204
+ if not example:
205
+ return ""
206
+ log = example.get("action_log", [])
207
+ preview = log[:14]
208
+ tail = f"\n … [{len(log)-14} more steps] …" if len(log) > 14 else ""
209
+ steps_str = "\n".join(f" {s}" for s in preview) + tail
210
+ return (
211
+ f"\n━━━ EXAMPLE SUCCESSFUL CASE (task={example['task']}, reward={example['reward']:+.2f}) ━━━\n"
212
+ f"{steps_str}\n"
213
+ f" → {example['final_message']}"
214
+ )
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Action parser
219
+ # ---------------------------------------------------------------------------
220
+
221
+ def _parse_action(raw: str, obs: FakeGangObservation) -> FakeGangAction:
222
+ """Extract a FakeGangAction from the LLM's raw text output."""
223
+ # Prefer content inside <action>…</action>
224
+ m = re.search(r"<action>\s*(.*?)\s*</action>", raw, re.DOTALL | re.IGNORECASE)
225
+ text = m.group(1).strip() if m else raw.strip()
226
+ upper = text.upper()
227
+
228
+ # Round 2: New tool actions (parse before INSPECT to avoid prefix collision)
229
+ if "GET_POLICY" in upper:
230
+ return FakeGangAction(action_type=ActionType.GET_POLICY)
231
+
232
+ if hit := re.search(r"REVERSE_IMAGE_SEARCH\s+(acc_\w+)", upper):
233
+ acc_id = hit.group(1).lower()
234
+ return FakeGangAction(action_type=ActionType.REVERSE_IMAGE_SEARCH, account_id=acc_id)
235
+
236
+ if hit := re.search(r"ANALYZE_BIO\s+(acc_\w+)", upper):
237
+ acc_id = hit.group(1).lower()
238
+ return FakeGangAction(action_type=ActionType.ANALYZE_BIO, account_id=acc_id)
239
+
240
+ if hit := re.search(r"CHECK_IP\s+(acc_\w+)", upper):
241
+ acc_id = hit.group(1).lower()
242
+ return FakeGangAction(action_type=ActionType.CHECK_IP, account_id=acc_id)
243
+
244
+ # INVESTIGATE_NETWORK must come before INSPECT (it's a longer prefix)
245
+ if hit := re.search(r"INVESTIGATE_NETWORK\s+(acc_\w+)", upper):
246
+ acc_id = hit.group(1).lower()
247
+ if acc_id in obs.visible_account_ids:
248
+ return FakeGangAction(action_type=ActionType.INVESTIGATE_NETWORK, account_id=acc_id)
249
+
250
+ if hit := re.search(r"INSPECT\s+(acc_\w+)", upper):
251
+ acc_id = hit.group(1).lower()
252
+ if acc_id in obs.visible_account_ids:
253
+ return FakeGangAction(action_type=ActionType.INSPECT, account_id=acc_id)
254
+
255
+ if hit := re.search(r"UNFLAG\s+(acc_\w+)", upper):
256
+ return FakeGangAction(action_type=ActionType.UNFLAG, account_id=hit.group(1).lower())
257
+
258
+ if hit := re.search(r"FLAG\s+(acc_\w+)", upper):
259
+ return FakeGangAction(action_type=ActionType.FLAG, account_id=hit.group(1).lower())
260
+
261
+ if "SUBMIT" in upper:
262
+ return FakeGangAction(action_type=ActionType.SUBMIT)
263
+
264
+ # Fallback: inspect the highest-scored uninspected account
265
+ uninspected = [i for i in obs.visible_account_ids if i not in obs.inspected_ids]
266
+ if uninspected:
267
+ return FakeGangAction(action_type=ActionType.INSPECT, account_id=uninspected[0])
268
+ return FakeGangAction(action_type=ActionType.SUBMIT)
269
+
270
+
271
+ # ---------------------------------------------------------------------------
272
+ # Public interface
273
+ # ---------------------------------------------------------------------------
274
+
275
+ def get_action(
276
+ obs: FakeGangObservation,
277
+ reflections: List[str],
278
+ few_shot_example: Optional[Dict] = None,
279
+ temperature: float = 0.4,
280
+ max_retries: int = 3,
281
+ ) -> Tuple[FakeGangAction, str]:
282
+ """
283
+ Query Qwen3 (via AWS Bedrock) for the next detective action.
284
+
285
+ Returns:
286
+ (action, raw_llm_output)
287
+
288
+ Learning signal flows in through `reflections` and `few_shot_example`:
289
+ - reflections: list of past post-episode lessons (from agent/reflection.py)
290
+ - few_shot_example: best saved trajectory (from agent/memory.py)
291
+ Both grow richer over training, causing measurably better decisions.
292
+ """
293
+ reflections_text = _format_reflections(reflections)
294
+ few_shot_text = _format_few_shot(few_shot_example)
295
+ obs_text = _format_observation(obs)
296
+
297
+ prompt = (
298
+ f"{reflections_text}"
299
+ f"{few_shot_text}"
300
+ f"\n\n━━━ CURRENT CASE ━━━\n"
301
+ f"{obs_text}"
302
+ f"\n\nWhat is your next action?"
303
+ )
304
+
305
+ last_error: Optional[Exception] = None
306
+ for attempt in range(max_retries):
307
+ try:
308
+ raw = invoke_qwen(
309
+ prompt=prompt,
310
+ system=SYSTEM_PROMPT,
311
+ max_tokens=512,
312
+ temperature=temperature,
313
+ )
314
+ action = _parse_action(raw, obs)
315
+ return action, raw
316
+ except Exception as exc:
317
+ last_error = exc
318
+ wait = 2 ** attempt
319
+ print(f" [policy] Bedrock call failed (attempt {attempt+1}): {exc} — retrying in {wait}s")
320
+ time.sleep(wait)
321
+
322
+ # All retries exhausted — fall back to heuristic
323
+ print(f" [policy] All retries exhausted: {last_error}. Using heuristic fallback.")
324
+ return _heuristic_fallback(obs), "[FALLBACK — Bedrock unavailable]"
325
+
326
+
327
+ def _heuristic_fallback(obs: FakeGangObservation) -> FakeGangAction:
328
+ """Simple heuristic used when Bedrock is unavailable."""
329
+ uninspected = [i for i in obs.visible_account_ids if i not in obs.inspected_ids]
330
+
331
+ if uninspected and obs.steps_remaining > 3:
332
+ return FakeGangAction(action_type=ActionType.INSPECT, account_id=uninspected[0])
333
+
334
+ # Flag high-signal accounts
335
+ for p in obs.visible_accounts:
336
+ if (p.photo_reuse_score > 0.5 and p.bio_template_score > 0.4
337
+ and p.account_id not in obs.flagged_ids):
338
+ return FakeGangAction(action_type=ActionType.FLAG, account_id=p.account_id)
339
+
340
+ return FakeGangAction(action_type=ActionType.SUBMIT)
agent/reflection.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Post-episode reflection generator.
2
+
3
+ After each episode the agent calls `generate_reflection()`.
4
+ Qwen3 analyses the action log and outcome, then writes a concrete lesson
5
+ that gets stored in AgentMemory and injected into future prompts.
6
+
7
+ This is the core **learning mechanism**:
8
+ Episode N fails → reflection generated → stored in memory
9
+ Episode N+1 → reflection in prompt → agent avoids past mistake
10
+ ...
11
+ Win rate rises measurably over episodes.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List
19
+
20
+ _ROOT = Path(__file__).parent.parent
21
+ sys.path.insert(0, str(_ROOT))
22
+
23
+ from bedrock_model import invoke_qwen
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Reflection prompt
27
+ # ---------------------------------------------------------------------------
28
+
29
+ _REFLECTION_SYSTEM = """\
30
+ You are a senior detective reviewing a FAKE INSTAGRAM ACCOUNT detection case debrief.
31
+ This environment detects coordinated fake social media accounts — NOT financial fraud.
32
+
33
+ Signals available in this environment (use ONLY these):
34
+ • comment_repeat_score > 0.6 → copy-paste spam comments (gang: 0.6-0.9, real: 0.0-0.08)
35
+ • shared_ip_count > 5 → shares IP subnet (all 10 gang members have count=9)
36
+ • photo_reuse_score > 0.5 → stolen profile photos
37
+ • bio_template_score > 0.4 → copy-paste bio text
38
+ • fake_risk_score > 0.75 → high-confidence gang member (composite score)
39
+ • hub_legitimacy_score > 0.70 → celebrity account, do NOT flag
40
+ • After FLAG: visible neighbors auto-become SUSPECT (priority targets)
41
+
42
+ Available actions: INSPECT (1 step, reveals profile), INVESTIGATE_NETWORK (2 steps, 2-hop expand),
43
+ FLAG, UNFLAG, SUBMIT.
44
+
45
+ CRITICAL: Write lessons about fake social media signals and INSPECT/INVESTIGATE_NETWORK strategy
46
+ ONLY. Do NOT mention transactions, financial transfers, banking, or any concepts not listed above.
47
+ Output only the lesson text — no headers, no bullet points, just 2-3 plain sentences.\
48
+ """
49
+
50
+
51
+ def generate_reflection(
52
+ task: str,
53
+ action_log: List[str],
54
+ final_message: str,
55
+ won: bool,
56
+ steps_used: int,
57
+ max_steps: int,
58
+ episode_num: int,
59
+ ) -> str:
60
+ """
61
+ Ask Qwen3 to generate a concrete lesson from one completed episode.
62
+ Returns a short string (2-3 sentences) suitable for memory storage.
63
+ """
64
+ outcome = "SUCCESS" if won else "FAILURE"
65
+ log_preview = "\n".join(f" {i+1}. {s}" for i, s in enumerate(action_log[:20]))
66
+ if len(action_log) > 20:
67
+ log_preview += f"\n … [{len(action_log) - 20} more steps]"
68
+
69
+ prompt = f"""\
70
+ FAKE INSTAGRAM ACCOUNT DETECTION — Episode {episode_num}
71
+ Task difficulty: {task.upper()}
72
+ Outcome: {outcome}
73
+ Steps used: {steps_used}/{max_steps}
74
+ Result: {final_message}
75
+
76
+ AVAILABLE SIGNALS (reference for your lesson):
77
+ comment_repeat_score > 0.6 | shared_ip_count > 5 | photo_reuse_score > 0.5
78
+ fake_risk_score > 0.75 | hub_legitimacy_score > 0.70 (celebrity, skip)
79
+ After FLAG → neighbors become SUSPECT (inspect them immediately)
80
+ INVESTIGATE_NETWORK on a flagged account reveals their 2-hop gang cluster
81
+
82
+ INVESTIGATION LOG:
83
+ {log_preview}
84
+
85
+ Write a 2-3 sentence lesson for your future self based on this case.
86
+ Focus on: which of the above signals were most diagnostic, whether using
87
+ INVESTIGATE_NETWORK after the first FLAG would have helped, and how to
88
+ better allocate the step budget. Be concrete and actionable.\
89
+ """
90
+
91
+ try:
92
+ reflection = invoke_qwen(
93
+ prompt=prompt,
94
+ system=_REFLECTION_SYSTEM,
95
+ max_tokens=180,
96
+ temperature=0.6,
97
+ )
98
+ return reflection.strip()
99
+ except Exception as exc:
100
+ # If Bedrock fails, generate a minimal rule-based reflection
101
+ return _rule_based_reflection(won, steps_used, max_steps, final_message)
102
+
103
+
104
+ def _rule_based_reflection(
105
+ won: bool, steps_used: int, max_steps: int, final_message: str
106
+ ) -> str:
107
+ """Minimal fallback reflection when Bedrock is unavailable."""
108
+ if won and steps_used < max_steps * 0.6:
109
+ return (
110
+ "Early INVESTIGATE_NETWORK calls efficiently expanded the graph to all gang members. "
111
+ "Flagging accounts with both high photo_reuse AND bio_template scores maintained precision. "
112
+ "Submitting with budget remaining earned an efficiency bonus."
113
+ )
114
+ if won:
115
+ return (
116
+ "Found all gang members but used most of the step budget. "
117
+ "Look for intra-gang follow density earlier — once you find one member, INVESTIGATE_NETWORK immediately. "
118
+ "Flag faster to leave budget for verification."
119
+ )
120
+ if "Recall=0.00" in final_message or "TP=0" in final_message:
121
+ return (
122
+ "Zero gang members found — the starting accounts were all real. "
123
+ "After inspecting 3-4 low-signal accounts, use INVESTIGATE_NETWORK to jump to a different part of the graph. "
124
+ "Gang members have photo_reuse > 0.5 and bio_template > 0.4 simultaneously."
125
+ )
126
+ return (
127
+ "Partial recall — found some gang members but missed others. "
128
+ "After flagging the first gang member, immediately use INVESTIGATE_NETWORK: gang members follow each other heavily. "
129
+ "Don't waste steps inspecting low-signal accounts one-by-one."
130
+ )
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Post-win reflection (reinforces what worked)
135
+ # ---------------------------------------------------------------------------
136
+
137
+ def generate_success_reflection(
138
+ task: str,
139
+ action_log: List[str],
140
+ final_message: str,
141
+ steps_used: int,
142
+ max_steps: int,
143
+ episode_num: int,
144
+ ) -> str:
145
+ """Generate a reinforcement reflection after a WIN to capture what worked."""
146
+ log_preview = "\n".join(f" {i+1}. {s}" for i, s in enumerate(action_log[:15]))
147
+
148
+ prompt = f"""\
149
+ SUCCESSFUL CASE — Episode {episode_num}
150
+ Task: {task.upper()} | Steps used: {steps_used}/{max_steps}
151
+ Result: {final_message}
152
+
153
+ INVESTIGATION LOG (first 15 steps):
154
+ {log_preview}
155
+
156
+ In 2-3 sentences, describe the specific strategy that led to success.
157
+ What did you do right? What should you repeat in future cases?\
158
+ """
159
+
160
+ try:
161
+ return invoke_qwen(
162
+ prompt=prompt,
163
+ system=_REFLECTION_SYSTEM,
164
+ max_tokens=150,
165
+ temperature=0.5,
166
+ ).strip()
167
+ except Exception:
168
+ return _rule_based_reflection(True, steps_used, max_steps, final_message)
agent/train.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Training script for Fake Gang Detection agent.
4
+
5
+ Supports:
6
+ - AWS Bedrock models (Qwen, Claude, Llama)
7
+ - HuggingFace router models
8
+ - Local rule-based baseline
9
+ - Platform-specific training (Instagram/Snapchat)
10
+ - Metrics tracking and visualization
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import sys
17
+ import time
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+ from typing import Dict, List, Optional, Tuple
21
+
22
+ # Add parent to path
23
+ _ROOT = Path(__file__).parent.parent
24
+ sys.path.insert(0, str(_ROOT))
25
+
26
+ from server.environment import FakeGangEnvironment
27
+ from models import FakeGangAction, FakeGangObservation, ActionType
28
+
29
+
30
+ # ============================================================================
31
+ # LLM Backends
32
+ # ============================================================================
33
+
34
+ def call_bedrock(prompt: str, system_prompt: str, model_id: str) -> str:
35
+ """Call AWS Bedrock model."""
36
+ import boto3
37
+
38
+ client = boto3.client(
39
+ service_name="bedrock-runtime",
40
+ region_name=os.getenv("AWS_DEFAULT_REGION", "us-east-1"),
41
+ aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
42
+ aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
43
+ )
44
+
45
+ # Try converse API first (boto3 >= 1.34.x)
46
+ if hasattr(client, "converse"):
47
+ resp = client.converse(
48
+ modelId=model_id,
49
+ messages=[{"role": "user", "content": [{"text": prompt}]}],
50
+ system=[{"text": system_prompt}],
51
+ inferenceConfig={"temperature": 0.3, "maxTokens": 512},
52
+ )
53
+ return resp["output"]["message"]["content"][0]["text"]
54
+ else:
55
+ # Fallback to invoke_model
56
+ body = json.dumps({
57
+ "prompt": f"{system_prompt}\n\nUser: {prompt}\n\nAssistant:",
58
+ "max_tokens": 512,
59
+ "temperature": 0.3,
60
+ })
61
+ resp = client.invoke_model(modelId=model_id, body=body)
62
+ result = json.loads(resp["body"].read())
63
+ return result.get("completion", result.get("content", [{"text": "SUBMIT"}])[0]["text"])
64
+
65
+
66
+ def call_hf(prompt: str, system_prompt: str, model_name: str, api_key: str) -> str:
67
+ """Call HuggingFace router model."""
68
+ from openai import OpenAI
69
+
70
+ client = OpenAI(
71
+ base_url=os.getenv("API_BASE_URL", "https://router.huggingface.co/v1"),
72
+ api_key=api_key,
73
+ )
74
+
75
+ resp = client.chat.completions.create(
76
+ model=model_name,
77
+ messages=[
78
+ {"role": "system", "content": system_prompt},
79
+ {"role": "user", "content": prompt},
80
+ ],
81
+ temperature=0.3,
82
+ max_tokens=512,
83
+ )
84
+
85
+ return (resp.choices[0].message.content or "").strip()
86
+
87
+
88
+ # ============================================================================
89
+ # System Prompt (Round 2)
90
+ # ============================================================================
91
+
92
+ SYSTEM_PROMPT = """You are an AI detective finding 10 coordinated fake accounts in a social network.
93
+
94
+ ━━━ ROUND 2: PLATFORM-ADAPTIVE DETECTION ━━━
95
+ Episodes run on specific platforms (Instagram/Snapchat) with different thresholds and enforcement priorities.
96
+
97
+ ACTIONS (reply with exactly ONE line):
98
+ - GET_POLICY — get platform policy (FREE, call first!)
99
+ - INSPECT acc_XXXX — reveal profile (costs 1 step)
100
+ - REVERSE_IMAGE_SEARCH acc_XXXX — reveal photo_reuse_score (costs 1 step)
101
+ - ANALYZE_BIO acc_XXXX — reveal bio_template_score (costs 1 step)
102
+ - CHECK_IP acc_XXXX — reveal ip_cluster_id (costs 2 steps, expensive!)
103
+ - FLAG acc_XXXX — mark as fake (FREE, no step cost, triggers suspect cascade)
104
+ - SUBMIT — end episode, get scored
105
+
106
+ DECISION RULES (Round 2, apply top-to-bottom):
107
+ 1. First action of episode → GET_POLICY (learn platform threshold and primary signal)
108
+ 2. If suspect_ids lists accounts you haven't inspected → INSPECT the first one
109
+ 3. If ANY profiled account has shared_ip_count >= 5 → CHECK_IP to confirm cluster, then FLAG
110
+ 4. If photo_reuse_score or bio_template_score is 0.0 on suspicious accounts → use REVERSE_IMAGE_SEARCH or ANALYZE_BIO
111
+ 5. If ANY profiled account has photo_reuse >= 0.50 AND bio_template >= 0.40 and hub < 0.70 → FLAG
112
+ 6. If fake_risk_score >= platform_threshold and hub < 0.70 → FLAG
113
+ 7. If uninspected visible accounts and steps > 3 → INSPECT the next one
114
+ 8. If you have 10 flags OR steps <= 3 → SUBMIT
115
+
116
+ PLATFORM-SPECIFIC STRATEGIES:
117
+ - Instagram (threshold ~0.08, high FP penalty): Be precise, use REVERSE_IMAGE_SEARCH on borderline cases
118
+ - Snapchat (threshold ~0.74, low FP penalty): Be aggressive, flag when fake_risk >= 0.74
119
+
120
+ IMPORTANT:
121
+ - Hidden signals (photo_reuse, bio_template, ip_cluster) start as 0.0/None — use tools to reveal!
122
+ - GET_POLICY is FREE and shows platform threshold — always call first
123
+ - FLAG is FREE (costs 0 steps) — flag aggressively when you see suspicious signals
124
+ - CHECK_IP costs 2 steps (expensive) — only use when shared_ip_count >= 5
125
+ - hub_legitimacy_score > 0.70 means celebrity — do NOT flag
126
+
127
+ Reply with EXACTLY one line, nothing else:
128
+ GET_POLICY
129
+ REVERSE_IMAGE_SEARCH acc_XXXX
130
+ ANALYZE_BIO acc_XXXX
131
+ CHECK_IP acc_XXXX
132
+ FLAG acc_XXXX
133
+ INSPECT acc_XXXX
134
+ SUBMIT"""
135
+
136
+
137
+ # ============================================================================
138
+ # Agent Policy
139
+ # ============================================================================
140
+
141
+ def format_observation(obs: FakeGangObservation) -> str:
142
+ """Format observation as text prompt for LLM."""
143
+ lines = []
144
+
145
+ # Platform context
146
+ platform_info = f" | PLATFORM: {obs.platform}" if obs.platform else ""
147
+ lines.append(f"TASK: {obs.task.upper()}{platform_info} | Steps remaining: {obs.steps_remaining}")
148
+
149
+ flagged = obs.flagged_ids
150
+ lines.append(f"Flagged ({len(flagged)}/10): {', '.join(flagged) if flagged else 'none'}")
151
+
152
+ # Suspects (high priority)
153
+ suspects = obs.suspect_ids
154
+ inspected = obs.inspected_ids
155
+ uninspected_suspects = [s for s in suspects if s not in inspected]
156
+ if uninspected_suspects:
157
+ lines.append(f"*** SUSPECTS (uninspected) → INSPECT THESE FIRST: {', '.join(uninspected_suspects)} ***")
158
+
159
+ # Accounts
160
+ if obs.visible_accounts:
161
+ unflagged_suspicious = []
162
+ flagged_accs = []
163
+ clean_accs = []
164
+
165
+ for a in sorted(obs.visible_accounts, key=lambda x: x.fake_risk_score, reverse=True):
166
+ aid = a.account_id
167
+ if aid in flagged:
168
+ flagged_accs.append(a)
169
+ elif (a.shared_ip_count >= 5 or
170
+ (a.photo_reuse_score >= 0.50 and a.bio_template_score >= 0.40)):
171
+ unflagged_suspicious.append(a)
172
+ else:
173
+ clean_accs.append(a)
174
+
175
+ if unflagged_suspicious:
176
+ lines.append(f"\n!!! ACTION NEEDED — FLAG THESE ({len(unflagged_suspicious)} suspicious):")
177
+ for a in unflagged_suspicious:
178
+ lines.append(f" → FLAG {a.account_id}: risk={a.fake_risk_score:.3f} photo={a.photo_reuse_score:.2f} bio={a.bio_template_score:.2f} ip_shared={a.shared_ip_count} hub={a.hub_legitimacy_score:.2f}")
179
+
180
+ if flagged_accs:
181
+ lines.append(f"\nALREADY FLAGGED ({len(flagged_accs)}):")
182
+ for a in flagged_accs[:5]:
183
+ lines.append(f" ✓ {a.account_id}")
184
+
185
+ if clean_accs:
186
+ lines.append(f"\nCLEAN ({len(clean_accs)}):")
187
+ for a in clean_accs[:8]:
188
+ hub_mark = " [CELEBRITY]" if a.hub_legitimacy_score > 0.70 else ""
189
+ lines.append(f" {a.account_id}: risk={a.fake_risk_score:.3f} photo={a.photo_reuse_score:.2f} bio={a.bio_template_score:.2f} hub={a.hub_legitimacy_score:.2f}{hub_mark}")
190
+
191
+ visible_ids = obs.visible_account_ids
192
+ uninspected_ids = [i for i in visible_ids if i not in inspected]
193
+ if uninspected_ids:
194
+ lines.append(f"\nUninspected IDs ({len(uninspected_ids)}): {', '.join(uninspected_ids[:10])}{'...' if len(uninspected_ids) > 10 else ''}")
195
+
196
+ lines.append(f"\nMessage: {obs.message}")
197
+ return "\n".join(lines)
198
+
199
+
200
+ def parse_action(text: str, obs: FakeGangObservation) -> FakeGangAction:
201
+ """Parse LLM response into action."""
202
+ text = text.strip().upper()
203
+
204
+ for line in text.split("\n"):
205
+ line = line.strip()
206
+ parts = line.split(maxsplit=1)
207
+ verb = parts[0]
208
+ acc = parts[1].lower() if len(parts) > 1 else None
209
+
210
+ # Round 2 actions
211
+ if verb == "GET_POLICY":
212
+ return FakeGangAction(action_type=ActionType.GET_POLICY)
213
+ if verb == "REVERSE_IMAGE_SEARCH" and acc:
214
+ return FakeGangAction(action_type=ActionType.REVERSE_IMAGE_SEARCH, account_id=acc)
215
+ if verb == "ANALYZE_BIO" and acc:
216
+ return FakeGangAction(action_type=ActionType.ANALYZE_BIO, account_id=acc)
217
+ if verb == "CHECK_IP" and acc:
218
+ return FakeGangAction(action_type=ActionType.CHECK_IP, account_id=acc)
219
+
220
+ # Round 1 actions
221
+ if verb in ("INSPECT", "FLAG", "UNFLAG", "INVESTIGATE_NETWORK"):
222
+ if acc:
223
+ return FakeGangAction(action_type=ActionType[verb], account_id=acc)
224
+ if verb == "SUBMIT":
225
+ return FakeGangAction(action_type=ActionType.SUBMIT)
226
+
227
+ # Fallback: inspect first uninspected
228
+ for s in obs.suspect_ids:
229
+ if s not in obs.inspected_ids:
230
+ return FakeGangAction(action_type=ActionType.INSPECT, account_id=s)
231
+
232
+ for v in obs.visible_account_ids:
233
+ if v not in obs.inspected_ids:
234
+ return FakeGangAction(action_type=ActionType.INSPECT, account_id=v)
235
+
236
+ return FakeGangAction(action_type=ActionType.SUBMIT)
237
+
238
+
239
+ # ============================================================================
240
+ # Episode Runner
241
+ # ============================================================================
242
+
243
+ def run_episode(
244
+ env: FakeGangEnvironment,
245
+ task: str,
246
+ seed: int,
247
+ backend: str,
248
+ model_id: str,
249
+ verbose: bool = False,
250
+ ) -> Dict:
251
+ """Run one episode and return metrics."""
252
+
253
+ obs = env.reset(task=task, seed=seed)
254
+ platform = obs.platform
255
+
256
+ actions_taken = []
257
+ tool_counts = {
258
+ "GET_POLICY": 0,
259
+ "REVERSE_IMAGE_SEARCH": 0,
260
+ "ANALYZE_BIO": 0,
261
+ "CHECK_IP": 0,
262
+ "INSPECT": 0,
263
+ "FLAG": 0,
264
+ }
265
+
266
+ start_time = time.time()
267
+
268
+ while not obs.done:
269
+ # Format observation
270
+ prompt = format_observation(obs)
271
+
272
+ # Get action from LLM or rule-based
273
+ if backend == "bedrock":
274
+ response = call_bedrock(prompt, SYSTEM_PROMPT, model_id)
275
+ action = parse_action(response, obs)
276
+ elif backend == "hf":
277
+ api_key = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
278
+ response = call_hf(prompt, SYSTEM_PROMPT, model_id, api_key)
279
+ action = parse_action(response, obs)
280
+ else: # rule-based
281
+ action = get_rule_based_action(obs)
282
+
283
+ # Track action
284
+ actions_taken.append(action.action_type.value)
285
+ if action.action_type.value.upper() in tool_counts:
286
+ tool_counts[action.action_type.value.upper()] += 1
287
+
288
+ # Step environment
289
+ obs = env.step(action)
290
+
291
+ if verbose:
292
+ print(f" [{obs.steps_remaining:2d}] {action.action_type.value:20s} {action.account_id or ''}")
293
+
294
+ elapsed = time.time() - start_time
295
+
296
+ # Get final metrics
297
+ grader = env._last_grader_score
298
+
299
+ # Count TP/FP/FN
300
+ flagged = set(env._flagged)
301
+ gang_members = set(env._ep["gang_member_ids"])
302
+ tp = len(flagged & gang_members)
303
+ fp = len(flagged - gang_members)
304
+ fn = len(gang_members - flagged)
305
+
306
+ precision = tp / max(tp + fp, 1)
307
+ recall = tp / max(tp + fn, 1)
308
+
309
+ return {
310
+ "episode": seed,
311
+ "platform": platform,
312
+ "task": task,
313
+ "reward": obs.reward or 0.0,
314
+ "grader_score": grader,
315
+ "tp": tp,
316
+ "fp": fp,
317
+ "fn": fn,
318
+ "precision": precision,
319
+ "recall": recall,
320
+ "steps_used": env._step_count,
321
+ "max_steps": env._max_steps,
322
+ "tool_counts": tool_counts,
323
+ "total_tools": sum(tool_counts.values()) - tool_counts["INSPECT"] - tool_counts["FLAG"], # Only investigation tools
324
+ "actions": actions_taken,
325
+ "elapsed_seconds": elapsed,
326
+ }
327
+
328
+
329
+ def get_rule_based_action(obs: FakeGangObservation) -> FakeGangAction:
330
+ """Simple rule-based policy for baseline."""
331
+ # Priority 1: Inspect suspects
332
+ for s in obs.suspect_ids:
333
+ if s not in obs.inspected_ids:
334
+ return FakeGangAction(action_type=ActionType.INSPECT, account_id=s)
335
+
336
+ # Priority 2: Flag high-risk accounts
337
+ for p in obs.visible_accounts:
338
+ if p.account_id in obs.flagged_ids:
339
+ continue
340
+ if p.hub_legitimacy_score > 0.75:
341
+ continue
342
+ if p.shared_ip_count >= 5 or p.fake_risk_score >= 0.60:
343
+ return FakeGangAction(action_type=ActionType.FLAG, account_id=p.account_id)
344
+
345
+ # Priority 3: Inspect uninspected
346
+ uninspected = [i for i in obs.visible_account_ids if i not in obs.inspected_ids]
347
+ if uninspected and obs.steps_remaining > 3:
348
+ return FakeGangAction(action_type=ActionType.INSPECT, account_id=uninspected[0])
349
+
350
+ return FakeGangAction(action_type=ActionType.SUBMIT)
351
+
352
+
353
+ # ============================================================================
354
+ # Training Loop
355
+ # ============================================================================
356
+
357
+ def run_training(
358
+ episodes: int,
359
+ task: str,
360
+ backend: str,
361
+ model_id: str,
362
+ output_file: Optional[str],
363
+ verbose: bool,
364
+ ) -> List[Dict]:
365
+ """Run training loop and collect metrics."""
366
+
367
+ env = FakeGangEnvironment()
368
+ results = []
369
+
370
+ print(f"Starting training: {episodes} episodes, task={task}, backend={backend}, model={model_id}")
371
+ print("=" * 80)
372
+
373
+ for ep in range(episodes):
374
+ try:
375
+ result = run_episode(env, task, seed=ep, backend=backend, model_id=model_id, verbose=verbose)
376
+ results.append(result)
377
+
378
+ # Print summary
379
+ print(f"Episode {ep:3d} ({result['platform']:9s}): "
380
+ f"reward={result['reward']:+.3f} | "
381
+ f"grader={result['grader_score']:.3f} | "
382
+ f"TP={result['tp']:2d} FP={result['fp']:2d} FN={result['fn']:2d} | "
383
+ f"P={result['precision']:.2f} R={result['recall']:.2f} | "
384
+ f"tools={result['total_tools']} | "
385
+ f"{result['elapsed_seconds']:.1f}s")
386
+
387
+ except Exception as e:
388
+ print(f"Episode {ep:3d} FAILED: {e}")
389
+ continue
390
+
391
+ print("=" * 80)
392
+
393
+ # Aggregate metrics
394
+ instagram_results = [r for r in results if r["platform"] == "Instagram"]
395
+ snapchat_results = [r for r in results if r["platform"] == "Snapchat"]
396
+
397
+ def avg(lst, key):
398
+ vals = [r[key] for r in lst]
399
+ return sum(vals) / len(vals) if vals else 0.0
400
+
401
+ print(f"\n=== Training Summary ===")
402
+ print(f"Total Episodes: {len(results)}/{episodes}")
403
+ print(f"\nInstagram ({len(instagram_results)} episodes):")
404
+ print(f" Avg Reward: {avg(instagram_results, 'reward'):+.3f}")
405
+ print(f" Avg Grader: {avg(instagram_results, 'grader_score'):.3f}")
406
+ print(f" Avg Precision: {avg(instagram_results, 'precision'):.3f}")
407
+ print(f" Avg Recall: {avg(instagram_results, 'recall'):.3f}")
408
+ print(f" Win Rate: {sum(1 for r in instagram_results if r['grader_score'] >= 0.815) / len(instagram_results) * 100:.1f}%")
409
+ print(f" Avg Tools: {avg(instagram_results, 'total_tools'):.1f}")
410
+
411
+ print(f"\nSnapchat ({len(snapchat_results)} episodes):")
412
+ print(f" Avg Reward: {avg(snapchat_results, 'reward'):+.3f}")
413
+ print(f" Avg Grader: {avg(snapchat_results, 'grader_score'):.3f}")
414
+ print(f" Avg Precision: {avg(snapchat_results, 'precision'):.3f}")
415
+ print(f" Avg Recall: {avg(snapchat_results, 'recall'):.3f}")
416
+ print(f" Win Rate: {sum(1 for r in snapchat_results if r['grader_score'] >= 0.815) / len(snapchat_results) * 100:.1f}%")
417
+ print(f" Avg Tools: {avg(snapchat_results, 'total_tools'):.1f}")
418
+
419
+ # Tool usage breakdown
420
+ all_tools = {}
421
+ for r in results:
422
+ for tool, count in r["tool_counts"].items():
423
+ all_tools[tool] = all_tools.get(tool, 0) + count
424
+
425
+ print(f"\nTool Usage (total):")
426
+ for tool, count in sorted(all_tools.items(), key=lambda x: -x[1]):
427
+ print(f" {tool:25s}: {count:4d} calls")
428
+
429
+ # Save results
430
+ if output_file:
431
+ output_path = Path(output_file)
432
+ output_path.parent.mkdir(parents=True, exist_ok=True)
433
+
434
+ output_data = {
435
+ "metadata": {
436
+ "episodes": episodes,
437
+ "task": task,
438
+ "backend": backend,
439
+ "model_id": model_id,
440
+ "timestamp": datetime.now().isoformat(),
441
+ },
442
+ "results": results,
443
+ "summary": {
444
+ "instagram": {
445
+ "count": len(instagram_results),
446
+ "avg_reward": avg(instagram_results, "reward"),
447
+ "avg_grader": avg(instagram_results, "grader_score"),
448
+ "avg_precision": avg(instagram_results, "precision"),
449
+ "avg_recall": avg(instagram_results, "recall"),
450
+ "win_rate": sum(1 for r in instagram_results if r["grader_score"] >= 0.815) / len(instagram_results) if instagram_results else 0,
451
+ "avg_tools": avg(instagram_results, "total_tools"),
452
+ },
453
+ "snapchat": {
454
+ "count": len(snapchat_results),
455
+ "avg_reward": avg(snapchat_results, "reward"),
456
+ "avg_grader": avg(snapchat_results, "grader_score"),
457
+ "avg_precision": avg(snapchat_results, "precision"),
458
+ "avg_recall": avg(snapchat_results, "recall"),
459
+ "win_rate": sum(1 for r in snapchat_results if r["grader_score"] >= 0.815) / len(snapchat_results) if snapchat_results else 0,
460
+ "avg_tools": avg(snapchat_results, "total_tools"),
461
+ },
462
+ "tool_usage": all_tools,
463
+ },
464
+ }
465
+
466
+ output_path.write_text(json.dumps(output_data, indent=2))
467
+ print(f"\n✓ Results saved to {output_file}")
468
+
469
+ return results
470
+
471
+
472
+ # ============================================================================
473
+ # CLI
474
+ # ============================================================================
475
+
476
+ def main():
477
+ parser = argparse.ArgumentParser(description="Train Fake Gang Detection agent")
478
+
479
+ # Training params
480
+ parser.add_argument("--episodes", type=int, default=50, help="Number of episodes")
481
+ parser.add_argument("--task", choices=["easy", "medium", "hard"], default="easy", help="Task difficulty")
482
+
483
+ # Model selection
484
+ parser.add_argument("--backend", choices=["bedrock", "hf", "rule"], default="rule",
485
+ help="LLM backend (bedrock=AWS, hf=HuggingFace, rule=baseline)")
486
+ parser.add_argument("--model-id", default="qwen.qwen3-next-80b-a3b",
487
+ help="Model ID (Bedrock: qwen.qwen3-next-80b-a3b, HF: Qwen/Qwen2.5-72B-Instruct)")
488
+
489
+ # Output
490
+ parser.add_argument("--output", "-o", default="results/training_results.json",
491
+ help="Output JSON file for metrics")
492
+ parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed action log")
493
+
494
+ args = parser.parse_args()
495
+
496
+ # Run training
497
+ run_training(
498
+ episodes=args.episodes,
499
+ task=args.task,
500
+ backend=args.backend,
501
+ model_id=args.model_id,
502
+ output_file=args.output,
503
+ verbose=args.verbose,
504
+ )
505
+
506
+
507
+ if __name__ == "__main__":
508
+ main()
assets/episode.png ADDED
assets/formulas-1.png ADDED

Git LFS Details

  • SHA256: e0efb65c597f87da46edf10f7295a775568a6de84e190b424777b7120d141ae4
  • Pointer size: 131 Bytes
  • Size of remote file: 230 kB
assets/formulas-2.png ADDED
assets/gs.png ADDED
assets/hybrid.png ADDED

Git LFS Details

  • SHA256: bdc17cf51111efce3a756c4c9fad6179d2dcbe54431702f778c805435484cb8e
  • Pointer size: 131 Bytes
  • Size of remote file: 135 kB
assets/logo.png ADDED
assets/reflexion.png ADDED

Git LFS Details

  • SHA256: 16a89e586131c4c1484edb70b37a363bce1ad84d2009b4d930d43ab62f179c35
  • Pointer size: 131 Bytes
  • Size of remote file: 124 kB
assets/sys arch.png ADDED

Git LFS Details

  • SHA256: 02aa4f9f4f0abfaf934b4c2774c3adc1a3e0dc5486955daea6291e154f79f22c
  • Pointer size: 131 Bytes
  • Size of remote file: 205 kB
baseline_agent.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Baseline agents for all three tasks of the Fake Gang Detection environment.
2
+
3
+ Run directly:
4
+ python baseline_agent.py --task easy --episodes 10
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+
14
+ sys.path.insert(0, str(Path(__file__).parent))
15
+
16
+ from client import FakeGangEnvClient, StepResult
17
+ from models import AccountProfile, FakeGangObservation, ActionType
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Scoring helper
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def _gang_score(profile: AccountProfile, task: str) -> float:
25
+ """Simple composite fake-likelihood score (0–1)."""
26
+ # Normalise account age: newer = more suspicious (fakes created recently)
27
+ age_score = max(0.0, 1.0 - profile.account_age_days / 500.0)
28
+ # Posting hour clustering: peak hour 14 is suspicious
29
+ hour_score = max(0.0, 1.0 - abs(profile.avg_post_hour - 14.0) / 12.0)
30
+ return (
31
+ profile.photo_reuse_score * 0.40
32
+ + profile.bio_template_score * 0.30
33
+ + age_score * 0.20
34
+ + hour_score * 0.10
35
+ )
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Easy baseline: signal scanner
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def easy_agent(env: FakeGangEnvClient, seed: int = 0) -> StepResult:
43
+ result = env.reset(task="easy", seed=seed)
44
+ obs = result.observation
45
+
46
+ PHOTO_THRESH = 0.50
47
+ BIO_THRESH = 0.40
48
+
49
+ while not obs.done:
50
+ # Pick an uninspected account to inspect
51
+ uninspected = [
52
+ i for i in obs.visible_account_ids if i not in obs.inspected_ids
53
+ ]
54
+
55
+ if uninspected:
56
+ target = uninspected[0]
57
+ result = env.inspect(target)
58
+ obs = result.observation
59
+
60
+ # Flag if it matches both signal thresholds
61
+ profile = next((p for p in obs.visible_accounts if p.account_id == target), None)
62
+ if profile and (
63
+ profile.photo_reuse_score > PHOTO_THRESH
64
+ and profile.bio_template_score > BIO_THRESH
65
+ ):
66
+ result = env.flag(target)
67
+ obs = result.observation
68
+
69
+ # Explore its network if we don't have enough flags yet
70
+ if len(obs.flagged_ids) < 10:
71
+ result = env.investigate_network(target)
72
+ obs = result.observation
73
+ else:
74
+ # Lower thresholds if we're running low on steps
75
+ if obs.steps_remaining < 10:
76
+ PHOTO_THRESH = max(0.20, PHOTO_THRESH - 0.10)
77
+ BIO_THRESH = max(0.20, BIO_THRESH - 0.10)
78
+ # Re-evaluate all inspected accounts with looser thresholds
79
+ for p in obs.visible_accounts:
80
+ if p.account_id not in obs.flagged_ids:
81
+ if (p.photo_reuse_score > PHOTO_THRESH
82
+ and p.bio_template_score > BIO_THRESH):
83
+ result = env.flag(p.account_id)
84
+ obs = result.observation
85
+
86
+ result = env.submit()
87
+ obs = result.observation
88
+
89
+ if obs.steps_remaining <= 0 or obs.done:
90
+ if not obs.done:
91
+ result = env.submit()
92
+ obs = result.observation
93
+ break
94
+
95
+ return result
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Medium baseline: time-aware scanner
100
+ # ---------------------------------------------------------------------------
101
+
102
+ def medium_agent(env: FakeGangEnvClient, seed: int = 0) -> StepResult:
103
+ result = env.reset(task="medium", seed=seed)
104
+ obs = result.observation
105
+
106
+ max_steps = 50
107
+ evasion_step = 20
108
+
109
+ while not obs.done:
110
+ steps_used = max_steps - obs.steps_remaining
111
+
112
+ uninspected = [i for i in obs.visible_account_ids if i not in obs.inspected_ids]
113
+
114
+ # Phase 1: race against evasion — use graph traversal
115
+ if steps_used < evasion_step - 5 and uninspected:
116
+ target = uninspected[0]
117
+ result = env.inspect(target)
118
+ obs = result.observation
119
+ profile = next((p for p in obs.visible_accounts if p.account_id == target), None)
120
+ if profile and profile.photo_reuse_score > 0.35:
121
+ result = env.flag(target)
122
+ obs = result.observation
123
+ result = env.investigate_network(target)
124
+ obs = result.observation
125
+
126
+ # Phase 2: after evasion — rely on features only
127
+ elif uninspected:
128
+ target = uninspected[0]
129
+ result = env.inspect(target)
130
+ obs = result.observation
131
+ profile = next((p for p in obs.visible_accounts if p.account_id == target), None)
132
+ if profile:
133
+ score = _gang_score(profile, "medium")
134
+ if score > 0.45:
135
+ result = env.flag(target)
136
+ obs = result.observation
137
+
138
+ else:
139
+ # No more to inspect — submit
140
+ result = env.submit()
141
+ obs = result.observation
142
+ break
143
+
144
+ if obs.steps_remaining <= 2 or obs.done:
145
+ if not obs.done:
146
+ result = env.submit()
147
+ obs = result.observation
148
+ break
149
+
150
+ return result
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Hard baseline: feature-only detective
155
+ # ---------------------------------------------------------------------------
156
+
157
+ def hard_agent(env: FakeGangEnvClient, seed: int = 0) -> StepResult:
158
+ result = env.reset(task="hard", seed=seed)
159
+ obs = result.observation
160
+
161
+ max_steps = 80
162
+ submit_by_step = 60 # submit before too many evasion events
163
+
164
+ while not obs.done:
165
+ steps_used = max_steps - obs.steps_remaining
166
+ uninspected = [i for i in obs.visible_account_ids if i not in obs.inspected_ids]
167
+
168
+ # Inspect until we have a dataset or time's up
169
+ if uninspected and steps_used < submit_by_step:
170
+ target = uninspected[0]
171
+ result = env.inspect(target)
172
+ obs = result.observation
173
+
174
+ # Discover more IDs via network expansion (spend the extra step)
175
+ if len(obs.visible_account_ids) < 100 and obs.steps_remaining > 15:
176
+ result = env.investigate_network(target)
177
+ obs = result.observation
178
+
179
+ else:
180
+ # Score all inspected accounts and flag top-12
181
+ scored = sorted(
182
+ obs.visible_accounts,
183
+ key=lambda p: _gang_score(p, "hard") + 0.3 * p.name_change_count,
184
+ reverse=True,
185
+ )
186
+ # Unflag everything first
187
+ for fid in list(obs.flagged_ids):
188
+ result = env.unflag(fid)
189
+ obs = result.observation
190
+
191
+ # Flag top 12 (a bit generous to improve recall)
192
+ for profile in scored[:12]:
193
+ result = env.flag(profile.account_id)
194
+ obs = result.observation
195
+
196
+ result = env.submit()
197
+ obs = result.observation
198
+ break
199
+
200
+ if obs.steps_remaining <= 2 or obs.done:
201
+ if not obs.done:
202
+ result = env.submit()
203
+ obs = result.observation
204
+ break
205
+
206
+ return result
207
+
208
+
209
+ # ---------------------------------------------------------------------------
210
+ # Evaluation runner
211
+ # ---------------------------------------------------------------------------
212
+
213
+ AGENTS = {
214
+ "easy": easy_agent,
215
+ "medium": medium_agent,
216
+ "hard": hard_agent,
217
+ }
218
+
219
+
220
+ def evaluate(task: str, episodes: int = 10, base_url: str = "http://localhost:8000") -> None:
221
+ agent_fn = AGENTS[task]
222
+ wins = 0
223
+ total_reward = 0.0
224
+
225
+ with FakeGangEnvClient(base_url=base_url) as env:
226
+ for seed in range(episodes):
227
+ result = agent_fn(env, seed=seed)
228
+ msg = result.message
229
+ reward = result.reward or 0.0
230
+ total_reward += reward
231
+ won = "[WIN]" in msg
232
+ if won:
233
+ wins += 1
234
+ print(f"Episode {seed:3d} | {'WIN ' if won else 'LOSS'} | reward={reward:+.2f} | {msg}")
235
+
236
+ print(f"\n{task.upper()} — wins: {wins}/{episodes} ({100*wins/episodes:.0f}%) | avg reward: {total_reward/episodes:.2f}")
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # CLI
241
+ # ---------------------------------------------------------------------------
242
+
243
+ if __name__ == "__main__":
244
+ parser = argparse.ArgumentParser(description="Run baseline agent against the Fake Gang env.")
245
+ parser.add_argument("--task", choices=["easy", "medium", "hard"], default="easy")
246
+ parser.add_argument("--episodes", type=int, default=10)
247
+ parser.add_argument("--url", default="http://localhost:8000")
248
+ args = parser.parse_args()
249
+ evaluate(args.task, args.episodes, args.url)
bedrock_model.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import boto3
2
+ import json
3
+ import os
4
+ from botocore.exceptions import ClientError
5
+ from typing import Any, List, Dict, Optional, Union
6
+
7
+ # --- Credentials setup ---
8
+ AWS_ACCESS_KEY = os.environ.get("AWS_ACCESS_KEY_ID")
9
+ AWS_SECRET_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
10
+
11
+ # Bedrock Marketplace model endpoint ARN (replace with your own)
12
+ MODEL_ID = "qwen.qwen3-next-80b-a3b"
13
+
14
+ # Build the Bedrock runtime client
15
+ client = boto3.client(
16
+ service_name="bedrock-runtime",
17
+ region_name="us-east-1",
18
+ aws_access_key_id=AWS_ACCESS_KEY,
19
+ aws_secret_access_key=AWS_SECRET_KEY,
20
+ )
21
+
22
+
23
+ def invoke_qwen(
24
+ prompt: str,
25
+ system: str = None,
26
+ max_tokens: int = 1024,
27
+ temperature: float = 0.3,
28
+ images: Optional[List[Dict[str, Union[str, bytes]]]] = None
29
+ ) -> str:
30
+ """
31
+ Invoke the Qwen VL model via Bedrock Converse API with optional image input.
32
+
33
+ Args:
34
+ prompt: Text prompt for the model.
35
+ system: Optional system prompt.
36
+ max_tokens: Maximum tokens to generate.
37
+ images: Optional list of image dictionaries. Each dict must contain:
38
+ - "bytes": raw image bytes (e.g., from open(file, "rb").read())
39
+ OR
40
+ - "path": local file path to an image (will be read as raw bytes).
41
+ - "format": image format string ("jpeg", "png", "gif", or "webp").
42
+
43
+ Returns:
44
+ Generated text response.
45
+ """
46
+ # Start building the content blocks for the user message
47
+ content_blocks: List[Dict] = [{"text": prompt}]
48
+
49
+ # Add image blocks if any are supplied
50
+ if images:
51
+ for img in images:
52
+ # Determine raw bytes from either "bytes" or "path"
53
+ if "bytes" in img:
54
+ img_bytes = img["bytes"]
55
+ elif "path" in img:
56
+ with open(img["path"], "rb") as f:
57
+ img_bytes = f.read()
58
+ else:
59
+ raise ValueError(
60
+ "Each image dict must include either 'bytes' (raw bytes) "
61
+ "or 'path' (file path) plus a 'format' key."
62
+ )
63
+
64
+ if "format" not in img:
65
+ raise ValueError("Each image dict must include a 'format' (e.g., 'jpeg').")
66
+
67
+ # Build the image block as required by the Converse API
68
+ image_block = {
69
+ "image": {
70
+ "format": img["format"],
71
+ "source": {"bytes": img_bytes} # raw bytes, NOT base64‑encoded
72
+ }
73
+ }
74
+ content_blocks.append(image_block)
75
+
76
+ # Assemble the final message
77
+ messages = [{"role": "user", "content": content_blocks}]
78
+
79
+ # Prepare the Converse API call
80
+ kwargs = {
81
+ "modelId": MODEL_ID, # Marketplace endpoint ARN
82
+ "messages": messages,
83
+ "inferenceConfig": {
84
+ "maxTokens": max_tokens,
85
+ "temperature": temperature,
86
+ }
87
+ }
88
+ if system:
89
+ kwargs["system"] = [{"text": system}]
90
+
91
+ try:
92
+ response = client.converse(**kwargs)
93
+ output = response["output"]["message"]["content"][0]["text"]
94
+ usage = response["usage"]
95
+ print(f"[Tokens] in={usage['inputTokens']} out={usage['outputTokens']}")
96
+ return output
97
+ except ClientError as e:
98
+ raise RuntimeError(f"Bedrock error: {e.response['Error']['Message']}")
99
+
100
+
101
+ def _parse_score_response(text: str) -> Dict[str, Any]:
102
+ """Parse JSON object with score, issues, summary from model output."""
103
+ raw = (text or "").strip()
104
+ start, end = raw.find("{"), raw.rfind("}")
105
+ if start >= 0 and end > start:
106
+ raw = raw[start : end + 1]
107
+ try:
108
+ d = json.loads(raw)
109
+ except json.JSONDecodeError:
110
+ return {"score": 0, "issues": ["Could not parse model JSON"], "summary": text[:400] if text else ""}
111
+ score = d.get("score", 0)
112
+ try:
113
+ score_i = int(score)
114
+ except (TypeError, ValueError):
115
+ score_i = 0
116
+ score_i = max(0, min(100, score_i))
117
+ issues = d.get("issues")
118
+ if issues is None:
119
+ issues = []
120
+ if isinstance(issues, str):
121
+ issues = [issues]
122
+ if not isinstance(issues, list):
123
+ issues = []
124
+ issues = [str(x).strip() for x in issues if str(x).strip()]
125
+ summary = str(d.get("summary", "") or "").strip()
126
+ return {"score": score_i, "issues": issues[:12], "summary": summary}
127
+
128
+
129
+ def score_design_against_spec(image_bytes: bytes, spec: Optional[str]) -> Dict[str, Any]:
130
+ """
131
+ Score a UI screenshot (PNG bytes) against the product spec via the vision model.
132
+ Returns dict: score (0-100), issues (list of str), summary (str).
133
+ """
134
+ system = (
135
+ "You are a product design QA assistant. Compare the screenshot to the product spec. "
136
+ "Return ONLY a single JSON object, no markdown, with keys: "
137
+ 'score (integer 0-100), issues (array of short strings, max 8 items), '
138
+ 'summary (one sentence). Be strict about spec mismatches.'
139
+ )
140
+ spec_block = (spec or "").strip() or (
141
+ "(No spec was provided — score clarity, hierarchy, visual polish, and common UX patterns.)"
142
+ )
143
+ prompt = (
144
+ "Evaluate this design for the following product spec.\n\n"
145
+ f"SPEC:\n{spec_block}\n\n"
146
+ "Output JSON only."
147
+ )
148
+ text = invoke_qwen(
149
+ prompt=prompt,
150
+ system=system,
151
+ max_tokens=1024,
152
+ temperature=0.2,
153
+ images=[{"bytes": image_bytes, "format": "png"}],
154
+ )
155
+ return _parse_score_response(text)
156
+
157
+
158
+ # --- Example usage ---
159
+ if __name__ == "__main__":
160
+ # 1️⃣ Text‑only call (same as before)
161
+ text_result = invoke_qwen(
162
+ prompt="Hello",
163
+ # system="You are a content extractor. Return content as JSON list.",
164
+ )
165
+ print("\nText‑only result:")
166
+ print(text_result)
167
+
check.sh ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # GraphStrike Round 2 — Full System Check
3
+ # Run: bash check_round2.sh
4
+ # Assumes server is running at localhost:7860
5
+
6
+ BASE="http://localhost:7860"
7
+ echo "========================================"
8
+ echo "GRAPHSTRIKE ROUND 2 — SYSTEM CHECK"
9
+ echo "========================================"
10
+
11
+ # -------------------------------------------------------
12
+ # CHECK 1 — Server is alive
13
+ # -------------------------------------------------------
14
+ echo ""
15
+ echo "CHECK 1: Server health"
16
+ curl -s "$BASE/" | python3 -m json.tool 2>/dev/null || \
17
+ curl -s "$BASE/health" | python3 -m json.tool 2>/dev/null || \
18
+ echo " (no health endpoint — checking /tasks instead)"
19
+
20
+ # -------------------------------------------------------
21
+ # CHECK 2 — Tasks endpoint shows Round 2 actions
22
+ # -------------------------------------------------------
23
+ echo ""
24
+ echo "CHECK 2: /tasks — must show all Round 2 action types"
25
+ curl -s "$BASE/tasks" | python3 -m json.tool
26
+
27
+ # -------------------------------------------------------
28
+ # CHECK 3 — Reset episode (instagram)
29
+ # -------------------------------------------------------
30
+ echo ""
31
+ echo "CHECK 3: /reset — instagram, easy task"
32
+ RESET=$(curl -s -X POST "$BASE/reset" \
33
+ -H "Content-Type: application/json" \
34
+ -d '{"task": "easy"}')
35
+ echo $RESET | python3 -m json.tool
36
+
37
+ # Extract session or episode info if present
38
+ EPISODE_ID=$(echo $RESET | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('episode_id',''))" 2>/dev/null)
39
+ echo " Episode ID: $EPISODE_ID"
40
+
41
+ # -------------------------------------------------------
42
+ # CHECK 4 — GET_POLICY (lowercase — correct form)
43
+ # -------------------------------------------------------
44
+ echo ""
45
+ echo "CHECK 4: step GET_POLICY (lowercase) — must return threshold"
46
+ curl -s -X POST "$BASE/step" \
47
+ -H "Content-Type: application/json" \
48
+ -d '{"action_type": "get_policy"}' | python3 -m json.tool
49
+
50
+ # Extract a real account_id from the reset observation BEFORE the first inspect.
51
+ ACCOUNT_ID=$(echo $RESET | python3 -c "
52
+ import sys, json
53
+ d = json.load(sys.stdin)
54
+ obs = d.get('observation', d)
55
+ accounts = obs.get('visible_account_ids') or obs.get('visible_accounts') or []
56
+ if accounts:
57
+ print(accounts[0] if isinstance(accounts[0], str) else accounts[0].get('account_id','acc_000'))
58
+ else:
59
+ print('acc_000')
60
+ " 2>/dev/null)
61
+ echo " Using account_id: $ACCOUNT_ID"
62
+
63
+ # -------------------------------------------------------
64
+ # CHECK 5 — INSPECT first visible account
65
+ # -------------------------------------------------------
66
+ echo ""
67
+ echo "CHECK 5: step inspect — first visible account ($ACCOUNT_ID)"
68
+ INSPECT=$(curl -s -X POST "$BASE/step" \
69
+ -H "Content-Type: application/json" \
70
+ -d "{\"action_type\": \"inspect\", \"account_id\": \"$ACCOUNT_ID\"}")
71
+ echo $INSPECT | python3 -m json.tool
72
+
73
+ # -------------------------------------------------------
74
+ # CHECK 6 — REVERSE_IMAGE_SEARCH
75
+ # photo_reuse_score must be None before, filled after
76
+ # -------------------------------------------------------
77
+ echo ""
78
+ echo "CHECK 6: reverse_image_search — must populate photo_reuse_score"
79
+ echo " Before call — check photo_reuse_score is hidden:"
80
+ curl -s -X POST "$BASE/step" \
81
+ -H "Content-Type: application/json" \
82
+ -d "{\"action_type\": \"inspect\", \"account_id\": \"$ACCOUNT_ID\"}" | \
83
+ ACCOUNT_ID="$ACCOUNT_ID" python3 -c "
84
+ import sys, json, os
85
+ d = json.load(sys.stdin)
86
+ obs = d.get('observation', {})
87
+ target = os.environ['ACCOUNT_ID']
88
+ profile = next((a for a in obs.get('visible_accounts', []) if a.get('account_id') == target), {})
89
+ val = profile.get('photo_reuse_score', 'KEY_MISSING')
90
+ print(f' photo_reuse_score = {val}')
91
+ print(' PASS — hidden before tool call' if (val in (None, 0.0)) else
92
+ f' WARN — already visible (={val})' if val != 'KEY_MISSING' else
93
+ ' WARN — field not found in response')
94
+ "
95
+
96
+ echo " After reverse_image_search:"
97
+ curl -s -X POST "$BASE/step" \
98
+ -H "Content-Type: application/json" \
99
+ -d "{\"action_type\": \"reverse_image_search\", \"account_id\": \"$ACCOUNT_ID\"}" | \
100
+ ACCOUNT_ID="$ACCOUNT_ID" python3 -c "
101
+ import sys, json, os
102
+ d = json.load(sys.stdin)
103
+ obs = d.get('observation', {})
104
+ target = os.environ['ACCOUNT_ID']
105
+ profile = next((a for a in obs.get('visible_accounts', []) if a.get('account_id') == target), {})
106
+ val = profile.get('photo_reuse_score', 'KEY_MISSING')
107
+ reward = d.get('reward', 'N/A')
108
+ print(f' photo_reuse_score = {val}')
109
+ print(f' reward = {reward}')
110
+ print(' PASS — signal revealed' if val not in (None, 'KEY_MISSING') else
111
+ ' FAIL — signal still hidden after tool call')
112
+ "
113
+
114
+ # -------------------------------------------------------
115
+ # CHECK 7 — ANALYZE_BIO
116
+ # -------------------------------------------------------
117
+ echo ""
118
+ echo "CHECK 7: analyze_bio — must populate bio_template_score"
119
+ curl -s -X POST "$BASE/step" \
120
+ -H "Content-Type: application/json" \
121
+ -d "{\"action_type\": \"analyze_bio\", \"account_id\": \"$ACCOUNT_ID\"}" | \
122
+ ACCOUNT_ID="$ACCOUNT_ID" python3 -c "
123
+ import sys, json, os
124
+ d = json.load(sys.stdin)
125
+ obs = d.get('observation', {})
126
+ target = os.environ['ACCOUNT_ID']
127
+ profile = next((a for a in obs.get('visible_accounts', []) if a.get('account_id') == target), {})
128
+ val = profile.get('bio_template_score', 'KEY_MISSING')
129
+ reward = d.get('reward', 'N/A')
130
+ print(f' bio_template_score = {val}')
131
+ print(f' reward = {reward}')
132
+ print(' PASS' if val not in (None, 0.0, 'KEY_MISSING') else ' FAIL')
133
+ "
134
+
135
+ # -------------------------------------------------------
136
+ # CHECK 8 — CHECK_IP
137
+ # -------------------------------------------------------
138
+ echo ""
139
+ echo "CHECK 8: check_ip — must reveal ip_cluster, costs 2 steps"
140
+ curl -s -X POST "$BASE/step" \
141
+ -H "Content-Type: application/json" \
142
+ -d "{\"action_type\": \"check_ip\", \"account_id\": \"$ACCOUNT_ID\"}" | \
143
+ ACCOUNT_ID="$ACCOUNT_ID" python3 -c "
144
+ import sys, json, os
145
+ d = json.load(sys.stdin)
146
+ obs = d.get('observation', {})
147
+ target = os.environ['ACCOUNT_ID']
148
+ profile = next((a for a in obs.get('visible_accounts', []) if a.get('account_id') == target), {})
149
+ # server exposes the cluster id via the message and shared_ip_count via the profile.
150
+ shared = profile.get('shared_ip_count', 'KEY_MISSING')
151
+ msg = d.get('message','')
152
+ reward = d.get('reward', 'N/A')
153
+ print(f' shared_ip_count = {shared}')
154
+ print(f' message excerpt: {msg[:120]}')
155
+ print(f' reward = {reward}')
156
+ print(' PASS' if 'cluster' in msg.lower() or shared not in (None,'KEY_MISSING') else ' FAIL')
157
+ "
158
+
159
+ # -------------------------------------------------------
160
+ # CHECK 9 — GET_POLICY first-step bonus
161
+ # -------------------------------------------------------
162
+ echo ""
163
+ echo "CHECK 9: GET_POLICY at step 0 must give +0.20 reward"
164
+ echo " Resetting fresh episode..."
165
+ curl -s -X POST "$BASE/reset" \
166
+ -H "Content-Type: application/json" \
167
+ -d '{"task": "easy"}' > /dev/null
168
+
169
+ curl -s -X POST "$BASE/step" \
170
+ -H "Content-Type: application/json" \
171
+ -d '{"action_type": "get_policy"}' | \
172
+ python3 -c "
173
+ import sys, json
174
+ d = json.load(sys.stdin)
175
+ reward = d.get('reward', None)
176
+ msg = d.get('message', '')
177
+ threshold = None
178
+ try:
179
+ import re
180
+ m = re.search(r'threshold[=:\s]+([\d.]+)', str(d))
181
+ if m: threshold = m.group(1)
182
+ except: pass
183
+ print(f' reward = {reward}')
184
+ print(f' threshold found = {threshold}')
185
+ print(' PASS — +0.20 bonus received' if reward and float(reward) >= 0.15 else
186
+ f' WARN — reward={reward}, expected ~0.20')
187
+ "
188
+
189
+ # -------------------------------------------------------
190
+ # CHECK 10 — Redundant tool call penalty
191
+ # -------------------------------------------------------
192
+ echo ""
193
+ echo "CHECK 10: Calling reverse_image_search twice must give -0.05 penalty"
194
+ curl -s -X POST "$BASE/reset" \
195
+ -H "Content-Type: application/json" \
196
+ -d '{"task": "easy"}' > /dev/null
197
+
198
+ # First call — should give normal reward
199
+ R1=$(curl -s -X POST "$BASE/step" \
200
+ -H "Content-Type: application/json" \
201
+ -d "{\"action_type\": \"reverse_image_search\", \"account_id\": \"$ACCOUNT_ID\"}" | \
202
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('reward',0))")
203
+
204
+ # Second call on same account — should give -0.05
205
+ R2=$(curl -s -X POST "$BASE/step" \
206
+ -H "Content-Type: application/json" \
207
+ -d "{\"action_type\": \"reverse_image_search\", \"account_id\": \"$ACCOUNT_ID\"}" | \
208
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('reward',0))")
209
+
210
+ echo " First call reward: $R1"
211
+ echo " Second call reward: $R2"
212
+ python3 -c "
213
+ r1, r2 = $R1, $R2
214
+ print(' PASS — penalty applied on redundant call' if r2 < r1 else
215
+ 'FAIL — no penalty on redundant call (should be -0.05)')
216
+ "
217
+
218
+ # -------------------------------------------------------
219
+ # CHECK 11 — FLAG without revealed signals gives penalty
220
+ # -------------------------------------------------------
221
+ echo ""
222
+ echo "CHECK 11: FLAG without any revealed signals must give -0.15 penalty"
223
+ curl -s -X POST "$BASE/reset" \
224
+ -H "Content-Type: application/json" \
225
+ -d '{"task": "easy"}' > /dev/null
226
+
227
+ curl -s -X POST "$BASE/step" \
228
+ -H "Content-Type: application/json" \
229
+ -d "{\"action_type\": \"flag\", \"account_id\": \"$ACCOUNT_ID\"}" | \
230
+ python3 -c "
231
+ import sys, json
232
+ d = json.load(sys.stdin)
233
+ reward = d.get('reward', None)
234
+ print(f' reward = {reward}')
235
+ print(' PASS — penalty applied for flagging without evidence' if reward and float(reward) <= -0.10 else
236
+ f' WARN — reward={reward}, expected -0.15 penalty')
237
+ "
238
+
239
+ # -------------------------------------------------------
240
+ # CHECK 12 — Full episode end to end
241
+ # -------------------------------------------------------
242
+ echo ""
243
+ echo "CHECK 12: Full episode — reset, get_policy, tools, flag, submit"
244
+ python3 - <<'PYEOF'
245
+ import requests, json
246
+
247
+ BASE = "http://localhost:7860"
248
+
249
+ # Reset
250
+ r = requests.post(f"{BASE}/reset", json={"task": "easy"})
251
+ resp = r.json()
252
+ obs = resp.get("observation", resp)
253
+ accounts = obs.get("visible_account_ids") or obs.get("visible_accounts") or []
254
+ if accounts:
255
+ acc = accounts[0] if isinstance(accounts[0], str) else accounts[0].get("account_id")
256
+ else:
257
+ acc = "acc_000"
258
+ print(f" Episode started. First account: {acc}")
259
+
260
+ steps = []
261
+
262
+ # Step 1: get_policy
263
+ r = requests.post(f"{BASE}/step", json={"action_type": "get_policy"})
264
+ d = r.json()
265
+ steps.append(("get_policy", d.get("reward")))
266
+
267
+ # Step 2: reverse_image_search
268
+ r = requests.post(f"{BASE}/step", json={"action_type": "reverse_image_search", "account_id": acc})
269
+ d = r.json()
270
+ steps.append(("reverse_image_search", d.get("reward")))
271
+
272
+ # Step 3: analyze_bio
273
+ r = requests.post(f"{BASE}/step", json={"action_type": "analyze_bio", "account_id": acc})
274
+ d = r.json()
275
+ steps.append(("analyze_bio", d.get("reward")))
276
+
277
+ # Step 4: flag
278
+ r = requests.post(f"{BASE}/step", json={"action_type": "flag", "account_id": acc})
279
+ d = r.json()
280
+ steps.append(("flag", d.get("reward")))
281
+
282
+ # Step 5: submit
283
+ r = requests.post(f"{BASE}/step", json={"action_type": "submit"})
284
+ d = r.json()
285
+ steps.append(("submit", d.get("reward")))
286
+ msg = d.get("message", "")
287
+ done = d.get("done", False)
288
+
289
+ print("\n Action log:")
290
+ for action, reward in steps:
291
+ print(f" {action:<25} reward={reward}")
292
+
293
+ print(f"\n Episode done: {done}")
294
+
295
+ # Check decision package in submit message
296
+ for keyword in ["Decision:", "policy_rationale", "evidence_summary", "flagged_accounts"]:
297
+ found = keyword.lower() in msg.lower()
298
+ print(f" Decision package [{keyword}]: {'PASS' if found else 'MISSING'}")
299
+
300
+ # Check grader score
301
+ grader = d.get("grader_score") or d.get("score")
302
+ print(f" Grader score: {grader}")
303
+
304
+ # Check structured decision_package field
305
+ dp = d.get("decision_package")
306
+ if dp:
307
+ print(f" decision_package keys: {sorted(dp.keys())}")
308
+ else:
309
+ print(" decision_package: missing (expected after submit)")
310
+ PYEOF
311
+
312
+ # -------------------------------------------------------
313
+ # SUMMARY
314
+ # -------------------------------------------------------
315
+ echo ""
316
+ echo "========================================"
317
+ echo "CHECK COMPLETE"
318
+ echo ""
319
+ echo "Expected results:"
320
+ echo " CHECK 1-3: Server healthy, tasks listed, reset works"
321
+ echo " CHECK 4: get_policy returns threshold from compiled policy"
322
+ echo " CHECK 5: inspect works"
323
+ echo " CHECK 6: reverse_image_search reveals photo_reuse_score"
324
+ echo " CHECK 7: analyze_bio reveals bio_template_score"
325
+ echo " CHECK 8: check_ip reveals ip_cluster_signal"
326
+ echo " CHECK 9: get_policy at step 0 gives +0.20 reward"
327
+ echo " CHECK 10: redundant tool call gives -0.05 penalty"
328
+ echo " CHECK 11: flag without evidence gives -0.15 penalty"
329
+ echo " CHECK 12: full episode runs, decision package in submit"
330
+ echo ""
331
+ echo "If any check fails, fix that component before running eval scripts."
332
+ echo "========================================"
client.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Python client for the Fake Gang Detection OpenEnv environment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Any, Dict, Optional
8
+
9
+ try:
10
+ import requests
11
+ except ImportError:
12
+ requests = None # type: ignore
13
+
14
+ from models import (
15
+ AccountProfile,
16
+ FakeGangAction,
17
+ FakeGangObservation,
18
+ FakeGangState,
19
+ ActionType,
20
+ )
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Result container
25
+ # ---------------------------------------------------------------------------
26
+
27
+ @dataclass
28
+ class StepResult:
29
+ observation: FakeGangObservation
30
+ done: bool
31
+ reward: Optional[float]
32
+ message: str
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Sync HTTP client
37
+ # ---------------------------------------------------------------------------
38
+
39
+ class FakeGangEnvClient:
40
+ """Synchronous HTTP client for the Fake Gang Detection environment."""
41
+
42
+ def __init__(self, base_url: str = "http://localhost:8000") -> None:
43
+ if requests is None:
44
+ raise ImportError("Install 'requests' to use FakeGangEnvClient.")
45
+ self.base_url = base_url.rstrip("/")
46
+ self._session = requests.Session()
47
+
48
+ # ------------------------------------------------------------------
49
+ # Public API
50
+ # ------------------------------------------------------------------
51
+
52
+ def reset(
53
+ self,
54
+ task: str = "easy",
55
+ seed: Optional[int] = None,
56
+ episode_id: Optional[str] = None,
57
+ ) -> StepResult:
58
+ payload = {"task": task}
59
+ if seed is not None:
60
+ payload["seed"] = seed
61
+ if episode_id is not None:
62
+ payload["episode_id"] = episode_id
63
+ resp = self._post("/reset", payload)
64
+ return self._parse_result(resp)
65
+
66
+ def step(self, action: FakeGangAction) -> StepResult:
67
+ resp = self._post("/step", action.model_dump())
68
+ return self._parse_result(resp)
69
+
70
+ def state(self) -> FakeGangState:
71
+ resp = self._session.get(f"{self.base_url}/state")
72
+ resp.raise_for_status()
73
+ return FakeGangState(**resp.json())
74
+
75
+ def health(self) -> Dict[str, str]:
76
+ resp = self._session.get(f"{self.base_url}/health")
77
+ resp.raise_for_status()
78
+ return resp.json()
79
+
80
+ # ------------------------------------------------------------------
81
+ # Convenience shortcuts
82
+ # ------------------------------------------------------------------
83
+
84
+ def inspect(self, account_id: str) -> StepResult:
85
+ return self.step(FakeGangAction(action_type=ActionType.INSPECT, account_id=account_id))
86
+
87
+ def investigate_network(self, account_id: str) -> StepResult:
88
+ return self.step(FakeGangAction(action_type=ActionType.INVESTIGATE_NETWORK, account_id=account_id))
89
+
90
+ def flag(self, account_id: str) -> StepResult:
91
+ return self.step(FakeGangAction(action_type=ActionType.FLAG, account_id=account_id))
92
+
93
+ def unflag(self, account_id: str) -> StepResult:
94
+ return self.step(FakeGangAction(action_type=ActionType.UNFLAG, account_id=account_id))
95
+
96
+ def submit(self) -> StepResult:
97
+ return self.step(FakeGangAction(action_type=ActionType.SUBMIT))
98
+
99
+ # ------------------------------------------------------------------
100
+ # Helpers
101
+ # ------------------------------------------------------------------
102
+
103
+ def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
104
+ resp = self._session.post(f"{self.base_url}{path}", json=payload)
105
+ resp.raise_for_status()
106
+ return resp.json()
107
+
108
+ def _parse_result(self, payload: Dict[str, Any]) -> StepResult:
109
+ obs_data = payload["observation"]
110
+ profiles = [AccountProfile(**p) for p in obs_data.get("visible_accounts", [])]
111
+ obs = FakeGangObservation(
112
+ done=obs_data.get("done", False),
113
+ reward=obs_data.get("reward"),
114
+ visible_accounts=profiles,
115
+ visible_account_ids=obs_data.get("visible_account_ids", []),
116
+ flagged_ids=obs_data.get("flagged_ids", []),
117
+ inspected_ids=obs_data.get("inspected_ids", []),
118
+ graph_edges=obs_data.get("graph_edges", {}),
119
+ steps_remaining=obs_data.get("steps_remaining", 0),
120
+ evasion_triggered=obs_data.get("evasion_triggered", False),
121
+ evasion_count=obs_data.get("evasion_count", 0),
122
+ task=obs_data.get("task", "easy"),
123
+ message=obs_data.get("message", ""),
124
+ suspect_ids=obs_data.get("suspect_ids", []),
125
+ )
126
+ return StepResult(
127
+ observation=obs,
128
+ done=payload.get("done", False),
129
+ reward=payload.get("reward"),
130
+ message=payload.get("message", ""),
131
+ )
132
+
133
+ # ------------------------------------------------------------------
134
+ # Context manager support
135
+ # ------------------------------------------------------------------
136
+
137
+ def __enter__(self) -> "FakeGangEnvClient":
138
+ return self
139
+
140
+ def __exit__(self, *args: Any) -> None:
141
+ self._session.close()
dashboard/README.md ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fake Gang Detection Dashboard
2
+
3
+ Interactive visualization dashboard for Round 2 platform-adaptive detection.
4
+
5
+ **Tech Stack**: React 18 + TypeScript + D3.js + FastAPI + WebSockets
6
+
7
+ ---
8
+
9
+ ## Setup
10
+
11
+ ### Backend (FastAPI)
12
+
13
+ ```bash
14
+ cd dashboard/backend
15
+ pip install -r requirements.txt
16
+ python main.py
17
+ ```
18
+
19
+ Backend runs on `http://localhost:8000`
20
+
21
+ API docs: `http://localhost:8000/docs`
22
+
23
+ ### Frontend (React + Vite)
24
+
25
+ ```bash
26
+ cd dashboard/frontend
27
+ npm install
28
+ npm run dev
29
+ ```
30
+
31
+ Frontend runs on `http://localhost:5173`
32
+
33
+ ---
34
+
35
+ ## Features
36
+
37
+ ### 1. Policy Compiler Panel
38
+ - Select platform (Instagram/Snapchat)
39
+ - Click "Compile Policy" to run Bayesian threshold calculation
40
+ - Streaming progress via WebSocket
41
+ - Policy summary card with:
42
+ - Threshold (θ*) with STRICT/LENIENT badge
43
+ - Base rate (π)
44
+ - FP penalty weight with HIGH/LOW badge
45
+ - Primary enforcement signal
46
+ - FN/FP cost signals
47
+ - Confidence score
48
+ - Compilation timestamp
49
+
50
+ ### 2. Network Graph Panel
51
+ - D3.js force-directed layout
52
+ - Node colors:
53
+ - 🔴 Red: Gang members (confirmed fakes)
54
+ - 🟢 Green: Real accounts
55
+ - 🟣 Purple: Celebrities (high hub legitimacy)
56
+ - Edge styles:
57
+ - Solid: Follows relationship
58
+ - Dashed: Mutual follows
59
+ - Interactive:
60
+ - Drag nodes to reposition
61
+ - Zoom and pan
62
+ - Hover for tooltips (account ID, risk, hub score)
63
+ - Auto-refreshes every 2 seconds during episode
64
+
65
+ ### 3. Training Panel
66
+ - Platform comparison (Instagram vs Snapchat)
67
+ - Metrics:
68
+ - Episode count
69
+ - Average score
70
+ - Average precision
71
+ - Average recall
72
+ - Recent episodes list with:
73
+ - Episode number and platform
74
+ - Score (color-coded: green >0.85, yellow <0.85)
75
+ - TP/FP/FN breakdown
76
+
77
+ ### 4. Control Panel
78
+ - **Single Episode**:
79
+ - Platform selector
80
+ - Task difficulty (easy/medium/hard)
81
+ - Seed input
82
+ - Start/Stop buttons
83
+ - **Batch Training**:
84
+ - Episode count input
85
+ - Auto-split between Instagram/Snapchat
86
+ - Run training button
87
+
88
+ ---
89
+
90
+ ## API Endpoints
91
+
92
+ ### WebSocket
93
+
94
+ - `WS /ws/compile_policy/{platform}` - Policy compilation progress
95
+ - `WS /ws/episode/{episode_id}` - Episode state updates
96
+ - `WS /ws/training/{training_id}` - Training progress
97
+
98
+ ### REST
99
+
100
+ - `POST /api/compile_policy` - Compile policy (non-streaming)
101
+ - `GET /api/policy/{platform}` - Get cached policy
102
+ - `POST /api/episode/start` - Start new episode
103
+ - `POST /api/episode/{episode_id}/step` - Execute action
104
+ - `GET /api/episode/{episode_id}/state` - Get episode state
105
+ - `POST /api/training/start` - Start training loop
106
+ - `GET /api/training/{training_id}/metrics` - Get training metrics
107
+ - `GET /health` - Health check
108
+
109
+ ---
110
+
111
+ ## Development
112
+
113
+ ### Backend Development
114
+
115
+ ```bash
116
+ cd dashboard/backend
117
+
118
+ # Run with auto-reload
119
+ uvicorn main:app --reload --host 0.0.0.0 --port 8000
120
+
121
+ # Test policy endpoint
122
+ curl http://localhost:8000/api/policy/Instagram
123
+
124
+ # Test episode start
125
+ curl -X POST http://localhost:8000/api/episode/start \
126
+ -H "Content-Type: application/json" \
127
+ -d '{"platform": "Instagram", "task": "easy", "seed": 0}'
128
+ ```
129
+
130
+ ### Frontend Development
131
+
132
+ ```bash
133
+ cd dashboard/frontend
134
+
135
+ # Install dependencies
136
+ npm install
137
+
138
+ # Run dev server with hot reload
139
+ npm run dev
140
+
141
+ # Build for production
142
+ npm run build
143
+
144
+ # Preview production build
145
+ npm run preview
146
+
147
+ # Lint code
148
+ npm run lint
149
+ ```
150
+
151
+ ### Adding New Components
152
+
153
+ 1. Create component file in `src/components/`
154
+ 2. Import in `App.tsx`
155
+ 3. Add to panels grid or control panel
156
+
157
+ Example:
158
+ ```typescript
159
+ // src/components/MetricsChart.tsx
160
+ import { useEffect, useRef } from 'react'
161
+ import * as d3 from 'd3'
162
+
163
+ export default function MetricsChart() {
164
+ const svgRef = useRef<SVGSVGElement>(null)
165
+
166
+ useEffect(() => {
167
+ // D3.js rendering logic
168
+ }, [])
169
+
170
+ return <svg ref={svgRef} />
171
+ }
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Architecture
177
+
178
+ ```
179
+ ┌─────────────────────────────────────────────────────────────┐
180
+ │ React Frontend │
181
+ │ ┌───────────────┐ ┌────────────────┐ ┌────────────────┐ │
182
+ │ │ PolicyPanel │ │ NetworkGraph │ │ TrainingPanel │ │
183
+ │ │ (streaming) │ │ (D3.js force) │ │ (metrics) │ │
184
+ │ └───────────────┘ └────────────────┘ └────────────────┘ │
185
+ │ │ │ │ │
186
+ │ └─────────────────┴──────────────────┘ │
187
+ │ │ │
188
+ │ WebSocket + REST API │
189
+ └─────────────────────────────��┬────────────────────────────────┘
190
+
191
+ ┌──────────────────────────────┴────────────────────────────────┐
192
+ │ FastAPI Backend │
193
+ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
194
+ │ │ Policy Compiler│ │ Environment │ │ Episode State │ │
195
+ │ │ (WebSocket) │ │ (OpenEnv) │ │ Management │ │
196
+ │ └────────────────┘ └────────────────┘ └────────────────┘ │
197
+ └───────────────────────────────────────────────────────────────┘
198
+ ```
199
+
200
+ ### Data Flow
201
+
202
+ **Policy Compilation**:
203
+ 1. User selects platform and clicks "Compile Policy"
204
+ 2. Frontend opens WebSocket to `/ws/compile_policy/{platform}`
205
+ 3. Backend calls `policy_compiler.get_policy(platform)`
206
+ 4. Progress streamed via WebSocket: `{type: "progress", message: "..."}`
207
+ 5. Complete policy sent: `{type: "complete", policy: {...}}`
208
+ 6. Frontend renders PolicyCard with metrics
209
+
210
+ **Episode Execution**:
211
+ 1. User sets parameters and clicks "Start Episode"
212
+ 2. Frontend POSTs to `/api/episode/start`
213
+ 3. Backend creates `FakeGangEnvironment`, calls `reset()`
214
+ 4. Returns `{episode_id, platform, observation, policy}`
215
+ 5. Frontend polls `/api/episode/{episode_id}/state` every 2s
216
+ 6. Returns `{observation, graph_data, metrics}`
217
+ 7. NetworkGraph updates with new nodes/edges
218
+
219
+ **Training Loop** (TODO: integrate with agent/train.py):
220
+ 1. User sets episode count and clicks "Run Training"
221
+ 2. Frontend POSTs to `/api/training/start`
222
+ 3. Backend starts training loop in background
223
+ 4. For each episode, sends WebSocket update: `{type: "episode_complete", ...}`
224
+ 5. Frontend updates TrainingPanel metrics in real-time
225
+
226
+ ---
227
+
228
+ ## Next Steps
229
+
230
+ ### Phase 1: Core Functionality ✅
231
+ - [x] PolicyPanel with WebSocket streaming
232
+ - [x] NetworkGraphPanel with D3.js force layout
233
+ - [x] TrainingPanel with metrics display
234
+ - [x] ControlPanel for episode/training control
235
+ - [x] FastAPI backend with REST + WebSocket
236
+
237
+ ### Phase 2: Integration 📋
238
+ - [ ] Connect training loop to `agent/train.py`
239
+ - [ ] Implement real-time episode stepping (manual mode)
240
+ - [ ] Add WebSocket reconnection logic
241
+ - [ ] Store training metrics in backend state
242
+
243
+ ### Phase 3: Enhancements 📋
244
+ - [ ] Training curve line chart (D3.js)
245
+ - [ ] Tool usage breakdown bar chart
246
+ - [ ] Precision/Recall scatter plot
247
+ - [ ] Export metrics to JSON/CSV
248
+ - [ ] Dark/light theme toggle
249
+ - [ ] Responsive design for mobile
250
+
251
+ ### Phase 4: Advanced Features 📋
252
+ - [ ] Multi-model comparison (Qwen vs Claude vs Llama)
253
+ - [ ] Episode replay (step-by-step visualization)
254
+ - [ ] Gang registry visualization
255
+ - [ ] Custom policy editor
256
+ - [ ] A/B testing framework
257
+
258
+ ---
259
+
260
+ ## Troubleshooting
261
+
262
+ **Backend fails to start**:
263
+ - Check Python version (3.10+)
264
+ - Install requirements: `pip install -r requirements.txt`
265
+ - Check port 8000 is available: `lsof -i :8000`
266
+
267
+ **Frontend fails to build**:
268
+ - Check Node version (18+)
269
+ - Delete `node_modules` and reinstall: `rm -rf node_modules && npm install`
270
+ - Clear cache: `rm -rf .vite`
271
+
272
+ **WebSocket connection fails**:
273
+ - Check backend is running on port 8000
274
+ - Check CORS settings in `main.py`
275
+ - Open browser console for errors
276
+
277
+ **Graph not rendering**:
278
+ - Check D3.js version: `npm list d3`
279
+ - Check browser console for errors
280
+ - Verify `graphData` is populated: `console.log(graphData)`
281
+
282
+ **Policy not loading**:
283
+ - Check `policy_cache/` directory exists
284
+ - Verify Tavily/Groq API keys (or use fallback)
285
+ - Check backend logs for compilation errors
286
+
287
+ ---
288
+
289
+ ## Production Deployment
290
+
291
+ ### Backend
292
+
293
+ ```bash
294
+ # Install production dependencies
295
+ pip install gunicorn
296
+
297
+ # Run with Gunicorn (production WSGI server)
298
+ gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:8000
299
+ ```
300
+
301
+ ### Frontend
302
+
303
+ ```bash
304
+ # Build production bundle
305
+ npm run build
306
+
307
+ # Serve with nginx or host on Vercel/Netlify
308
+ # Output in dist/ directory
309
+ ```
310
+
311
+ ### Docker (TODO)
312
+
313
+ ```dockerfile
314
+ # Dockerfile
315
+ FROM python:3.10-slim
316
+ WORKDIR /app
317
+ COPY requirements.txt .
318
+ RUN pip install -r requirements.txt
319
+ COPY . .
320
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
321
+ ```
322
+
323
+ ---
324
+
325
+ **Dashboard Status**: ✅ Core implementation complete, ready for integration testing
326
+
327
+ **Next**: Connect training loop and test end-to-end flow
dashboard/backend/main.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI Backend for Fake Gang Detection Dashboard
3
+
4
+ Provides:
5
+ - WebSocket streaming for policy compilation, episodes, and training
6
+ - REST endpoints for episode control and metrics
7
+ - CORS support for React frontend
8
+ """
9
+
10
+ import asyncio
11
+ import json
12
+ import uuid
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional
16
+
17
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from pydantic import BaseModel
20
+ import sys
21
+
22
+ # Add parent to path
23
+ _ROOT = Path(__file__).parent.parent.parent
24
+ sys.path.insert(0, str(_ROOT))
25
+
26
+ from server.environment import FakeGangEnvironment
27
+ from server.policy_compiler import compile_policy, get_policy
28
+ from models import FakeGangAction, ActionType
29
+
30
+ # ============================================================================
31
+ # Lifespan Context Manager
32
+ # ============================================================================
33
+
34
+ from contextlib import asynccontextmanager
35
+
36
+ @asynccontextmanager
37
+ async def lifespan(app_instance: FastAPI):
38
+ """Lifespan context manager for startup/shutdown."""
39
+ # Startup
40
+ print("🚀 Starting Fake Gang Detection Dashboard API")
41
+ print("📍 API: http://localhost:8000")
42
+ print("📖 Docs: http://localhost:8000/docs")
43
+
44
+ # Pre-load policies
45
+ for platform in ["Instagram", "Snapchat"]:
46
+ try:
47
+ policy = get_policy(platform)
48
+ policy_cache[platform] = policy.model_dump()
49
+ print(f"✓ Loaded {platform} policy: θ={policy.threshold:.3f}")
50
+ except Exception as e:
51
+ print(f"⚠ Failed to load {platform} policy: {e}")
52
+
53
+ yield
54
+
55
+ # Shutdown
56
+ print("👋 Shutting down Dashboard API")
57
+
58
+ # ============================================================================
59
+ # FastAPI App
60
+ # ============================================================================
61
+
62
+ app = FastAPI(title="Fake Gang Detection Dashboard API", lifespan=lifespan)
63
+
64
+ # CORS for React frontend
65
+ app.add_middleware(
66
+ CORSMiddleware,
67
+ allow_origins=["http://localhost:5173", "http://localhost:3000"], # Vite default, CRA default
68
+ allow_credentials=True,
69
+ allow_methods=["*"],
70
+ allow_headers=["*"],
71
+ )
72
+
73
+ # ============================================================================
74
+ # Global State
75
+ # ============================================================================
76
+
77
+ active_episodes: Dict[str, Dict] = {} # episode_id -> {env, obs, task, seed, platform}
78
+ active_trainings: Dict[str, Dict] = {} # training_id -> {status, results, ...}
79
+ policy_cache: Dict[str, Dict] = {} # platform -> policy
80
+
81
+
82
+ # ============================================================================
83
+ # Request/Response Models
84
+ # ============================================================================
85
+
86
+ class CompilePolicyRequest(BaseModel):
87
+ platform: str # "Instagram" or "Snapchat"
88
+ use_cache: bool = True
89
+
90
+
91
+ class StartEpisodeRequest(BaseModel):
92
+ platform: Optional[str] = None # Auto-assign by seed if None
93
+ task: str = "easy"
94
+ seed: int = 0
95
+
96
+
97
+ class StepActionRequest(BaseModel):
98
+ action_type: str
99
+ account_id: Optional[str] = None
100
+
101
+
102
+ class StartTrainingRequest(BaseModel):
103
+ episodes: int = 50
104
+ task: str = "easy"
105
+ platforms: Optional[List[str]] = None # ["Instagram", "Snapchat"] or None for auto
106
+
107
+
108
+ # ============================================================================
109
+ # WebSocket: Policy Compilation
110
+ # ============================================================================
111
+
112
+ @app.websocket("/ws/compile_policy/{platform}")
113
+ async def websocket_compile_policy(websocket: WebSocket, platform: str):
114
+ """Stream policy compilation progress."""
115
+ await websocket.accept()
116
+
117
+ try:
118
+ # Send progress updates
119
+ await websocket.send_json({
120
+ "type": "progress",
121
+ "message": f"🔍 Starting policy compilation for {platform}..."
122
+ })
123
+
124
+ await asyncio.sleep(0.5)
125
+
126
+ await websocket.send_json({
127
+ "type": "progress",
128
+ "message": "🌐 Fetching transparency reports from Tavily..."
129
+ })
130
+
131
+ await asyncio.sleep(1)
132
+
133
+ await websocket.send_json({
134
+ "type": "progress",
135
+ "message": "🤖 Extracting parameters with Groq LLM..."
136
+ })
137
+
138
+ await asyncio.sleep(1.5)
139
+
140
+ await websocket.send_json({
141
+ "type": "progress",
142
+ "message": "📊 Computing Bayesian threshold..."
143
+ })
144
+
145
+ # Actually compile policy (uses cache if available)
146
+ policy = get_policy(platform)
147
+
148
+ await websocket.send_json({
149
+ "type": "progress",
150
+ "message": f"✅ Policy compiled: θ={policy.threshold:.3f}"
151
+ })
152
+
153
+ await asyncio.sleep(0.5)
154
+
155
+ # Send complete policy
156
+ await websocket.send_json({
157
+ "type": "complete",
158
+ "policy": policy.model_dump(),
159
+ })
160
+
161
+ except WebSocketDisconnect:
162
+ pass
163
+ except Exception as e:
164
+ await websocket.send_json({
165
+ "type": "error",
166
+ "message": str(e),
167
+ })
168
+
169
+
170
+ # ============================================================================
171
+ # WebSocket: Episode Execution
172
+ # ============================================================================
173
+
174
+ @app.websocket("/ws/episode/{episode_id}")
175
+ async def websocket_episode(websocket: WebSocket, episode_id: str):
176
+ """Stream episode state updates."""
177
+ await websocket.accept()
178
+
179
+ if episode_id not in active_episodes:
180
+ await websocket.send_json({
181
+ "type": "error",
182
+ "message": f"Episode {episode_id} not found"
183
+ })
184
+ return
185
+
186
+ ep_data = active_episodes[episode_id]
187
+ env = ep_data["env"]
188
+ obs = ep_data["obs"]
189
+
190
+ try:
191
+ # Send initial state
192
+ await websocket.send_json({
193
+ "type": "observation",
194
+ "data": obs.model_dump(),
195
+ })
196
+
197
+ # Wait for actions from client (if manual mode)
198
+ # For now, we'll just send the current state
199
+ while True:
200
+ await asyncio.sleep(1)
201
+ # In a real implementation, this would wait for step actions
202
+
203
+ except WebSocketDisconnect:
204
+ pass
205
+
206
+
207
+ # ============================================================================
208
+ # WebSocket: Training Loop
209
+ # ============================================================================
210
+
211
+ @app.websocket("/ws/training/{training_id}")
212
+ async def websocket_training(websocket: WebSocket, training_id: str):
213
+ """Stream training progress."""
214
+ await websocket.accept()
215
+
216
+ if training_id not in active_trainings:
217
+ await websocket.send_json({
218
+ "type": "error",
219
+ "message": f"Training {training_id} not found"
220
+ })
221
+ return
222
+
223
+ training_data = active_trainings[training_id]
224
+
225
+ try:
226
+ # Stream training progress (simulated here)
227
+ # In real implementation, this would run the actual training loop
228
+ await websocket.send_json({
229
+ "type": "started",
230
+ "episodes": training_data["episodes"],
231
+ "task": training_data["task"],
232
+ })
233
+
234
+ # Simulate training episodes
235
+ # TODO: Replace with actual training loop from agent/train.py
236
+ for ep in range(training_data["episodes"]):
237
+ await asyncio.sleep(2) # Simulate episode execution
238
+
239
+ platform = "Instagram" if ep % 2 == 0 else "Snapchat"
240
+ score = 0.85 + (ep * 0.001) # Simulated improving score
241
+
242
+ await websocket.send_json({
243
+ "type": "episode_complete",
244
+ "episode": ep,
245
+ "platform": platform,
246
+ "score": score,
247
+ "tp": 9,
248
+ "fp": 1,
249
+ "fn": 1,
250
+ })
251
+
252
+ await websocket.send_json({
253
+ "type": "training_complete",
254
+ "avg_score": 0.87,
255
+ })
256
+
257
+ except WebSocketDisconnect:
258
+ pass
259
+
260
+
261
+ # ============================================================================
262
+ # REST: Policy Endpoints
263
+ # ============================================================================
264
+
265
+ @app.post("/api/compile_policy")
266
+ async def compile_policy_rest(request: CompilePolicyRequest):
267
+ """Compile policy (REST endpoint, non-streaming)."""
268
+ policy = get_policy(request.platform) if request.use_cache else compile_policy(request.platform, use_cache=False)
269
+ policy_cache[request.platform] = policy.model_dump()
270
+
271
+ return {
272
+ "policy": policy.model_dump(),
273
+ "sources": policy.sources,
274
+ }
275
+
276
+
277
+ @app.get("/api/policy/{platform}")
278
+ async def get_policy_rest(platform: str):
279
+ """Get cached policy."""
280
+ if platform in policy_cache:
281
+ return {"policy": policy_cache[platform]}
282
+
283
+ policy = get_policy(platform)
284
+ policy_cache[platform] = policy.model_dump()
285
+ return {"policy": policy.model_dump()}
286
+
287
+
288
+ # ============================================================================
289
+ # REST: Episode Endpoints
290
+ # ============================================================================
291
+
292
+ @app.post("/api/episode/start")
293
+ async def start_episode(request: StartEpisodeRequest):
294
+ """Start a new episode."""
295
+ episode_id = str(uuid.uuid4())
296
+
297
+ env = FakeGangEnvironment()
298
+ obs = env.reset(task=request.task, seed=request.seed)
299
+
300
+ # Load policy
301
+ policy = get_policy(obs.platform)
302
+
303
+ # Store episode state
304
+ active_episodes[episode_id] = {
305
+ "env": env,
306
+ "obs": obs,
307
+ "task": request.task,
308
+ "seed": request.seed,
309
+ "platform": obs.platform,
310
+ "policy": policy.model_dump(),
311
+ "created_at": datetime.now().isoformat(),
312
+ }
313
+
314
+ return {
315
+ "episode_id": episode_id,
316
+ "platform": obs.platform,
317
+ "observation": obs.model_dump(),
318
+ "policy": policy.model_dump(),
319
+ }
320
+
321
+
322
+ @app.post("/api/episode/{episode_id}/step")
323
+ async def step_episode(episode_id: str, request: StepActionRequest):
324
+ """Execute one step in episode."""
325
+ if episode_id not in active_episodes:
326
+ raise HTTPException(status_code=404, detail="Episode not found")
327
+
328
+ ep_data = active_episodes[episode_id]
329
+ env = ep_data["env"]
330
+
331
+ # Create action
332
+ action = FakeGangAction(
333
+ action_type=ActionType[request.action_type.upper()],
334
+ account_id=request.account_id,
335
+ )
336
+
337
+ # Step environment
338
+ obs = env.step(action)
339
+ ep_data["obs"] = obs
340
+
341
+ # Build graph data
342
+ graph_data = {
343
+ "nodes": [
344
+ {
345
+ "id": p.account_id,
346
+ "fake_risk_score": p.fake_risk_score,
347
+ "hub_legitimacy_score": p.hub_legitimacy_score,
348
+ "is_fake": p.account_id in env._ep["gang_member_ids"],
349
+ }
350
+ for p in obs.visible_accounts
351
+ ],
352
+ "edges": [
353
+ {
354
+ "source": acc_id,
355
+ "target": target_id,
356
+ "is_mutual": target_id in obs.graph_edges.get(target_id, []) and acc_id in obs.graph_edges[target_id],
357
+ }
358
+ for acc_id, targets in obs.graph_edges.items()
359
+ for target_id in targets
360
+ ],
361
+ }
362
+
363
+ return {
364
+ "observation": obs.model_dump(),
365
+ "graph_data": graph_data,
366
+ "done": obs.done,
367
+ "reward": obs.reward,
368
+ }
369
+
370
+
371
+ @app.get("/api/episode/{episode_id}/state")
372
+ async def get_episode_state(episode_id: str):
373
+ """Get current episode state."""
374
+ if episode_id not in active_episodes:
375
+ raise HTTPException(status_code=404, detail="Episode not found")
376
+
377
+ ep_data = active_episodes[episode_id]
378
+ obs = ep_data["obs"]
379
+ env = ep_data["env"]
380
+
381
+ # Build graph data
382
+ graph_data = {
383
+ "nodes": [
384
+ {
385
+ "id": p.account_id,
386
+ "fake_risk_score": p.fake_risk_score,
387
+ "hub_legitimacy_score": p.hub_legitimacy_score,
388
+ "is_fake": p.account_id in env._ep["gang_member_ids"],
389
+ }
390
+ for p in obs.visible_accounts
391
+ ],
392
+ "edges": [
393
+ {
394
+ "source": acc_id,
395
+ "target": target_id,
396
+ "is_mutual": target_id in obs.graph_edges.get(target_id, []) and acc_id in obs.graph_edges[target_id],
397
+ }
398
+ for acc_id, targets in obs.graph_edges.items()
399
+ for target_id in targets
400
+ ],
401
+ }
402
+
403
+ return {
404
+ "episode_id": episode_id,
405
+ "platform": ep_data["platform"],
406
+ "policy": ep_data["policy"],
407
+ "observation": obs.model_dump(),
408
+ "graph_data": graph_data,
409
+ "metrics": {
410
+ "steps_used": env._step_count,
411
+ "max_steps": env._max_steps,
412
+ "flagged_count": len(obs.flagged_ids),
413
+ "grader_score": env._last_grader_score,
414
+ },
415
+ }
416
+
417
+
418
+ # ============================================================================
419
+ # REST: Training Endpoints
420
+ # ============================================================================
421
+
422
+ @app.post("/api/training/start")
423
+ async def start_training(request: StartTrainingRequest):
424
+ """Start training loop."""
425
+ training_id = str(uuid.uuid4())
426
+
427
+ active_trainings[training_id] = {
428
+ "status": "running",
429
+ "episodes": request.episodes,
430
+ "task": request.task,
431
+ "platforms": request.platforms or ["Instagram", "Snapchat"],
432
+ "results": [],
433
+ "created_at": datetime.now().isoformat(),
434
+ }
435
+
436
+ return {
437
+ "training_id": training_id,
438
+ "status": "running",
439
+ }
440
+
441
+
442
+ @app.get("/api/training/{training_id}/metrics")
443
+ async def get_training_metrics(training_id: str):
444
+ """Get training metrics."""
445
+ if training_id not in active_trainings:
446
+ raise HTTPException(status_code=404, detail="Training not found")
447
+
448
+ return {
449
+ "training_id": training_id,
450
+ "metrics": active_trainings[training_id].get("results", []),
451
+ }
452
+
453
+
454
+ # ============================================================================
455
+ # Health Check
456
+ # ============================================================================
457
+
458
+ @app.get("/health")
459
+ async def health_check():
460
+ """Health check endpoint."""
461
+ return {
462
+ "status": "healthy",
463
+ "active_episodes": len(active_episodes),
464
+ "active_trainings": len(active_trainings),
465
+ "timestamp": datetime.now().isoformat(),
466
+ }
467
+
468
+
469
+
470
+
471
+ if __name__ == "__main__":
472
+ import uvicorn
473
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
dashboard/backend/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.109.0
2
+ uvicorn[standard]==0.27.0
3
+ websockets==12.0
4
+ python-multipart==0.0.6
5
+ pydantic==2.5.3
dashboard/frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Fake Gang Detection Dashboard</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
dashboard/frontend/node_modules/@alloc/quick-lru/index.d.ts ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare namespace QuickLRU {
2
+ interface Options<KeyType, ValueType> {
3
+ /**
4
+ The maximum number of milliseconds an item should remain in the cache.
5
+
6
+ @default Infinity
7
+
8
+ By default, `maxAge` will be `Infinity`, which means that items will never expire.
9
+ Lazy expiration upon the next write or read call.
10
+
11
+ Individual expiration of an item can be specified by the `set(key, value, maxAge)` method.
12
+ */
13
+ readonly maxAge?: number;
14
+
15
+ /**
16
+ The maximum number of items before evicting the least recently used items.
17
+ */
18
+ readonly maxSize: number;
19
+
20
+ /**
21
+ Called right before an item is evicted from the cache.
22
+
23
+ Useful for side effects or for items like object URLs that need explicit cleanup (`revokeObjectURL`).
24
+ */
25
+ onEviction?: (key: KeyType, value: ValueType) => void;
26
+ }
27
+ }
28
+
29
+ declare class QuickLRU<KeyType, ValueType>
30
+ implements Iterable<[KeyType, ValueType]> {
31
+ /**
32
+ The stored item count.
33
+ */
34
+ readonly size: number;
35
+
36
+ /**
37
+ Simple ["Least Recently Used" (LRU) cache](https://en.m.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_.28LRU.29).
38
+
39
+ The instance is [`iterable`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Iteration_protocols) so you can use it directly in a [`for…of`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/for...of) loop.
40
+
41
+ @example
42
+ ```
43
+ import QuickLRU = require('quick-lru');
44
+
45
+ const lru = new QuickLRU({maxSize: 1000});
46
+
47
+ lru.set('🦄', '🌈');
48
+
49
+ lru.has('🦄');
50
+ //=> true
51
+
52
+ lru.get('🦄');
53
+ //=> '🌈'
54
+ ```
55
+ */
56
+ constructor(options: QuickLRU.Options<KeyType, ValueType>);
57
+
58
+ [Symbol.iterator](): IterableIterator<[KeyType, ValueType]>;
59
+
60
+ /**
61
+ Set an item. Returns the instance.
62
+
63
+ Individual expiration of an item can be specified with the `maxAge` option. If not specified, the global `maxAge` value will be used in case it is specified in the constructor, otherwise the item will never expire.
64
+
65
+ @returns The list instance.
66
+ */
67
+ set(key: KeyType, value: ValueType, options?: {maxAge?: number}): this;
68
+
69
+ /**
70
+ Get an item.
71
+
72
+ @returns The stored item or `undefined`.
73
+ */
74
+ get(key: KeyType): ValueType | undefined;
75
+
76
+ /**
77
+ Check if an item exists.
78
+ */
79
+ has(key: KeyType): boolean;
80
+
81
+ /**
82
+ Get an item without marking it as recently used.
83
+
84
+ @returns The stored item or `undefined`.
85
+ */
86
+ peek(key: KeyType): ValueType | undefined;
87
+
88
+ /**
89
+ Delete an item.
90
+
91
+ @returns `true` if the item is removed or `false` if the item doesn't exist.
92
+ */
93
+ delete(key: KeyType): boolean;
94
+
95
+ /**
96
+ Delete all items.
97
+ */
98
+ clear(): void;
99
+
100
+ /**
101
+ Update the `maxSize` in-place, discarding items as necessary. Insertion order is mostly preserved, though this is not a strong guarantee.
102
+
103
+ Useful for on-the-fly tuning of cache sizes in live systems.
104
+ */
105
+ resize(maxSize: number): void;
106
+
107
+ /**
108
+ Iterable for all the keys.
109
+ */
110
+ keys(): IterableIterator<KeyType>;
111
+
112
+ /**
113
+ Iterable for all the values.
114
+ */
115
+ values(): IterableIterator<ValueType>;
116
+
117
+ /**
118
+ Iterable for all entries, starting with the oldest (ascending in recency).
119
+ */
120
+ entriesAscending(): IterableIterator<[KeyType, ValueType]>;
121
+
122
+ /**
123
+ Iterable for all entries, starting with the newest (descending in recency).
124
+ */
125
+ entriesDescending(): IterableIterator<[KeyType, ValueType]>;
126
+ }
127
+
128
+ export = QuickLRU;
dashboard/frontend/node_modules/@alloc/quick-lru/index.js ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ class QuickLRU {
4
+ constructor(options = {}) {
5
+ if (!(options.maxSize && options.maxSize > 0)) {
6
+ throw new TypeError('`maxSize` must be a number greater than 0');
7
+ }
8
+
9
+ if (typeof options.maxAge === 'number' && options.maxAge === 0) {
10
+ throw new TypeError('`maxAge` must be a number greater than 0');
11
+ }
12
+
13
+ this.maxSize = options.maxSize;
14
+ this.maxAge = options.maxAge || Infinity;
15
+ this.onEviction = options.onEviction;
16
+ this.cache = new Map();
17
+ this.oldCache = new Map();
18
+ this._size = 0;
19
+ }
20
+
21
+ _emitEvictions(cache) {
22
+ if (typeof this.onEviction !== 'function') {
23
+ return;
24
+ }
25
+
26
+ for (const [key, item] of cache) {
27
+ this.onEviction(key, item.value);
28
+ }
29
+ }
30
+
31
+ _deleteIfExpired(key, item) {
32
+ if (typeof item.expiry === 'number' && item.expiry <= Date.now()) {
33
+ if (typeof this.onEviction === 'function') {
34
+ this.onEviction(key, item.value);
35
+ }
36
+
37
+ return this.delete(key);
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ _getOrDeleteIfExpired(key, item) {
44
+ const deleted = this._deleteIfExpired(key, item);
45
+ if (deleted === false) {
46
+ return item.value;
47
+ }
48
+ }
49
+
50
+ _getItemValue(key, item) {
51
+ return item.expiry ? this._getOrDeleteIfExpired(key, item) : item.value;
52
+ }
53
+
54
+ _peek(key, cache) {
55
+ const item = cache.get(key);
56
+
57
+ return this._getItemValue(key, item);
58
+ }
59
+
60
+ _set(key, value) {
61
+ this.cache.set(key, value);
62
+ this._size++;
63
+
64
+ if (this._size >= this.maxSize) {
65
+ this._size = 0;
66
+ this._emitEvictions(this.oldCache);
67
+ this.oldCache = this.cache;
68
+ this.cache = new Map();
69
+ }
70
+ }
71
+
72
+ _moveToRecent(key, item) {
73
+ this.oldCache.delete(key);
74
+ this._set(key, item);
75
+ }
76
+
77
+ * _entriesAscending() {
78
+ for (const item of this.oldCache) {
79
+ const [key, value] = item;
80
+ if (!this.cache.has(key)) {
81
+ const deleted = this._deleteIfExpired(key, value);
82
+ if (deleted === false) {
83
+ yield item;
84
+ }
85
+ }
86
+ }
87
+
88
+ for (const item of this.cache) {
89
+ const [key, value] = item;
90
+ const deleted = this._deleteIfExpired(key, value);
91
+ if (deleted === false) {
92
+ yield item;
93
+ }
94
+ }
95
+ }
96
+
97
+ get(key) {
98
+ if (this.cache.has(key)) {
99
+ const item = this.cache.get(key);
100
+
101
+ return this._getItemValue(key, item);
102
+ }
103
+
104
+ if (this.oldCache.has(key)) {
105
+ const item = this.oldCache.get(key);
106
+ if (this._deleteIfExpired(key, item) === false) {
107
+ this._moveToRecent(key, item);
108
+ return item.value;
109
+ }
110
+ }
111
+ }
112
+
113
+ set(key, value, {maxAge = this.maxAge === Infinity ? undefined : Date.now() + this.maxAge} = {}) {
114
+ if (this.cache.has(key)) {
115
+ this.cache.set(key, {
116
+ value,
117
+ maxAge
118
+ });
119
+ } else {
120
+ this._set(key, {value, expiry: maxAge});
121
+ }
122
+ }
123
+
124
+ has(key) {
125
+ if (this.cache.has(key)) {
126
+ return !this._deleteIfExpired(key, this.cache.get(key));
127
+ }
128
+
129
+ if (this.oldCache.has(key)) {
130
+ return !this._deleteIfExpired(key, this.oldCache.get(key));
131
+ }
132
+
133
+ return false;
134
+ }
135
+
136
+ peek(key) {
137
+ if (this.cache.has(key)) {
138
+ return this._peek(key, this.cache);
139
+ }
140
+
141
+ if (this.oldCache.has(key)) {
142
+ return this._peek(key, this.oldCache);
143
+ }
144
+ }
145
+
146
+ delete(key) {
147
+ const deleted = this.cache.delete(key);
148
+ if (deleted) {
149
+ this._size--;
150
+ }
151
+
152
+ return this.oldCache.delete(key) || deleted;
153
+ }
154
+
155
+ clear() {
156
+ this.cache.clear();
157
+ this.oldCache.clear();
158
+ this._size = 0;
159
+ }
160
+
161
+ resize(newSize) {
162
+ if (!(newSize && newSize > 0)) {
163
+ throw new TypeError('`maxSize` must be a number greater than 0');
164
+ }
165
+
166
+ const items = [...this._entriesAscending()];
167
+ const removeCount = items.length - newSize;
168
+ if (removeCount < 0) {
169
+ this.cache = new Map(items);
170
+ this.oldCache = new Map();
171
+ this._size = items.length;
172
+ } else {
173
+ if (removeCount > 0) {
174
+ this._emitEvictions(items.slice(0, removeCount));
175
+ }
176
+
177
+ this.oldCache = new Map(items.slice(removeCount));
178
+ this.cache = new Map();
179
+ this._size = 0;
180
+ }
181
+
182
+ this.maxSize = newSize;
183
+ }
184
+
185
+ * keys() {
186
+ for (const [key] of this) {
187
+ yield key;
188
+ }
189
+ }
190
+
191
+ * values() {
192
+ for (const [, value] of this) {
193
+ yield value;
194
+ }
195
+ }
196
+
197
+ * [Symbol.iterator]() {
198
+ for (const item of this.cache) {
199
+ const [key, value] = item;
200
+ const deleted = this._deleteIfExpired(key, value);
201
+ if (deleted === false) {
202
+ yield [key, value.value];
203
+ }
204
+ }
205
+
206
+ for (const item of this.oldCache) {
207
+ const [key, value] = item;
208
+ if (!this.cache.has(key)) {
209
+ const deleted = this._deleteIfExpired(key, value);
210
+ if (deleted === false) {
211
+ yield [key, value.value];
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ * entriesDescending() {
218
+ let items = [...this.cache];
219
+ for (let i = items.length - 1; i >= 0; --i) {
220
+ const item = items[i];
221
+ const [key, value] = item;
222
+ const deleted = this._deleteIfExpired(key, value);
223
+ if (deleted === false) {
224
+ yield [key, value.value];
225
+ }
226
+ }
227
+
228
+ items = [...this.oldCache];
229
+ for (let i = items.length - 1; i >= 0; --i) {
230
+ const item = items[i];
231
+ const [key, value] = item;
232
+ if (!this.cache.has(key)) {
233
+ const deleted = this._deleteIfExpired(key, value);
234
+ if (deleted === false) {
235
+ yield [key, value.value];
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ * entriesAscending() {
242
+ for (const [key, value] of this._entriesAscending()) {
243
+ yield [key, value.value];
244
+ }
245
+ }
246
+
247
+ get size() {
248
+ if (!this._size) {
249
+ return this.oldCache.size;
250
+ }
251
+
252
+ let oldCacheSize = 0;
253
+ for (const key of this.oldCache.keys()) {
254
+ if (!this.cache.has(key)) {
255
+ oldCacheSize++;
256
+ }
257
+ }
258
+
259
+ return Math.min(this._size + oldCacheSize, this.maxSize);
260
+ }
261
+ }
262
+
263
+ module.exports = QuickLRU;
dashboard/frontend/node_modules/@alloc/quick-lru/license ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dashboard/frontend/node_modules/@alloc/quick-lru/package.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@alloc/quick-lru",
3
+ "version": "5.2.0",
4
+ "description": "Simple “Least Recently Used” (LRU) cache",
5
+ "license": "MIT",
6
+ "repository": "sindresorhus/quick-lru",
7
+ "funding": "https://github.com/sponsors/sindresorhus",
8
+ "author": {
9
+ "name": "Sindre Sorhus",
10
+ "email": "sindresorhus@gmail.com",
11
+ "url": "https://sindresorhus.com"
12
+ },
13
+ "engines": {
14
+ "node": ">=10"
15
+ },
16
+ "scripts": {
17
+ "test": "xo && nyc ava && tsd"
18
+ },
19
+ "files": [
20
+ "index.js",
21
+ "index.d.ts"
22
+ ],
23
+ "keywords": [
24
+ "lru",
25
+ "quick",
26
+ "cache",
27
+ "caching",
28
+ "least",
29
+ "recently",
30
+ "used",
31
+ "fast",
32
+ "map",
33
+ "hash",
34
+ "buffer"
35
+ ],
36
+ "devDependencies": {
37
+ "ava": "^2.0.0",
38
+ "coveralls": "^3.0.3",
39
+ "nyc": "^15.0.0",
40
+ "tsd": "^0.11.0",
41
+ "xo": "^0.26.0"
42
+ }
43
+ }
dashboard/frontend/node_modules/@alloc/quick-lru/readme.md ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # quick-lru [![Build Status](https://travis-ci.org/sindresorhus/quick-lru.svg?branch=master)](https://travis-ci.org/sindresorhus/quick-lru) [![Coverage Status](https://coveralls.io/repos/github/sindresorhus/quick-lru/badge.svg?branch=master)](https://coveralls.io/github/sindresorhus/quick-lru?branch=master)
2
+
3
+ > Simple [“Least Recently Used” (LRU) cache](https://en.m.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_.28LRU.29)
4
+
5
+ Useful when you need to cache something and limit memory usage.
6
+
7
+ Inspired by the [`hashlru` algorithm](https://github.com/dominictarr/hashlru#algorithm), but instead uses [`Map`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map) to support keys of any type, not just strings, and values can be `undefined`.
8
+
9
+ ## Install
10
+
11
+ ```
12
+ $ npm install quick-lru
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```js
18
+ const QuickLRU = require('quick-lru');
19
+
20
+ const lru = new QuickLRU({maxSize: 1000});
21
+
22
+ lru.set('🦄', '🌈');
23
+
24
+ lru.has('🦄');
25
+ //=> true
26
+
27
+ lru.get('🦄');
28
+ //=> '🌈'
29
+ ```
30
+
31
+ ## API
32
+
33
+ ### new QuickLRU(options?)
34
+
35
+ Returns a new instance.
36
+
37
+ ### options
38
+
39
+ Type: `object`
40
+
41
+ #### maxSize
42
+
43
+ *Required*\
44
+ Type: `number`
45
+
46
+ The maximum number of items before evicting the least recently used items.
47
+
48
+ #### maxAge
49
+
50
+ Type: `number`\
51
+ Default: `Infinity`
52
+
53
+ The maximum number of milliseconds an item should remain in cache.
54
+ By default maxAge will be Infinity, which means that items will never expire.
55
+
56
+ Lazy expiration happens upon the next `write` or `read` call.
57
+
58
+ Individual expiration of an item can be specified by the `set(key, value, options)` method.
59
+
60
+ #### onEviction
61
+
62
+ *Optional*\
63
+ Type: `(key, value) => void`
64
+
65
+ Called right before an item is evicted from the cache.
66
+
67
+ Useful for side effects or for items like object URLs that need explicit cleanup (`revokeObjectURL`).
68
+
69
+ ### Instance
70
+
71
+ The instance is [`iterable`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Iteration_protocols) so you can use it directly in a [`for…of`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/for...of) loop.
72
+
73
+ Both `key` and `value` can be of any type.
74
+
75
+ #### .set(key, value, options?)
76
+
77
+ Set an item. Returns the instance.
78
+
79
+ Individual expiration of an item can be specified with the `maxAge` option. If not specified, the global `maxAge` value will be used in case it is specified on the constructor, otherwise the item will never expire.
80
+
81
+ #### .get(key)
82
+
83
+ Get an item.
84
+
85
+ #### .has(key)
86
+
87
+ Check if an item exists.
88
+
89
+ #### .peek(key)
90
+
91
+ Get an item without marking it as recently used.
92
+
93
+ #### .delete(key)
94
+
95
+ Delete an item.
96
+
97
+ Returns `true` if the item is removed or `false` if the item doesn't exist.
98
+
99
+ #### .clear()
100
+
101
+ Delete all items.
102
+
103
+ #### .resize(maxSize)
104
+
105
+ Update the `maxSize`, discarding items as necessary. Insertion order is mostly preserved, though this is not a strong guarantee.
106
+
107
+ Useful for on-the-fly tuning of cache sizes in live systems.
108
+
109
+ #### .keys()
110
+
111
+ Iterable for all the keys.
112
+
113
+ #### .values()
114
+
115
+ Iterable for all the values.
116
+
117
+ #### .entriesAscending()
118
+
119
+ Iterable for all entries, starting with the oldest (ascending in recency).
120
+
121
+ #### .entriesDescending()
122
+
123
+ Iterable for all entries, starting with the newest (descending in recency).
124
+
125
+ #### .size
126
+
127
+ The stored item count.
128
+
129
+ ---
130
+
131
+ <div align="center">
132
+ <b>
133
+ <a href="https://tidelift.com/subscription/pkg/npm-quick-lru?utm_source=npm-quick-lru&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
134
+ </b>
135
+ <br>
136
+ <sub>
137
+ Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
138
+ </sub>
139
+ </div>
dashboard/frontend/node_modules/@babel/code-frame/LICENSE ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2014-present Sebastian McKenzie and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dashboard/frontend/node_modules/@babel/code-frame/README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @babel/code-frame
2
+
3
+ > Generate errors that contain a code frame that point to source locations.
4
+
5
+ See our website [@babel/code-frame](https://babeljs.io/docs/babel-code-frame) for more information.
6
+
7
+ ## Install
8
+
9
+ Using npm:
10
+
11
+ ```sh
12
+ npm install --save-dev @babel/code-frame
13
+ ```
14
+
15
+ or using yarn:
16
+
17
+ ```sh
18
+ yarn add @babel/code-frame --dev
19
+ ```
dashboard/frontend/node_modules/@babel/code-frame/lib/index.js ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var picocolors = require('picocolors');
6
+ var jsTokens = require('js-tokens');
7
+ var helperValidatorIdentifier = require('@babel/helper-validator-identifier');
8
+
9
+ function isColorSupported() {
10
+ return (typeof process === "object" && (process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false") ? false : picocolors.isColorSupported
11
+ );
12
+ }
13
+ const compose = (f, g) => v => f(g(v));
14
+ function buildDefs(colors) {
15
+ return {
16
+ keyword: colors.cyan,
17
+ capitalized: colors.yellow,
18
+ jsxIdentifier: colors.yellow,
19
+ punctuator: colors.yellow,
20
+ number: colors.magenta,
21
+ string: colors.green,
22
+ regex: colors.magenta,
23
+ comment: colors.gray,
24
+ invalid: compose(compose(colors.white, colors.bgRed), colors.bold),
25
+ gutter: colors.gray,
26
+ marker: compose(colors.red, colors.bold),
27
+ message: compose(colors.red, colors.bold),
28
+ reset: colors.reset
29
+ };
30
+ }
31
+ const defsOn = buildDefs(picocolors.createColors(true));
32
+ const defsOff = buildDefs(picocolors.createColors(false));
33
+ function getDefs(enabled) {
34
+ return enabled ? defsOn : defsOff;
35
+ }
36
+
37
+ const sometimesKeywords = new Set(["as", "async", "from", "get", "of", "set"]);
38
+ const NEWLINE$1 = /\r\n|[\n\r\u2028\u2029]/;
39
+ const BRACKET = /^[()[\]{}]$/;
40
+ let tokenize;
41
+ const JSX_TAG = /^[a-z][\w-]*$/i;
42
+ const getTokenType = function (token, offset, text) {
43
+ if (token.type === "name") {
44
+ const tokenValue = token.value;
45
+ if (helperValidatorIdentifier.isKeyword(tokenValue) || helperValidatorIdentifier.isStrictReservedWord(tokenValue, true) || sometimesKeywords.has(tokenValue)) {
46
+ return "keyword";
47
+ }
48
+ if (JSX_TAG.test(tokenValue) && (text[offset - 1] === "<" || text.slice(offset - 2, offset) === "</")) {
49
+ return "jsxIdentifier";
50
+ }
51
+ const firstChar = String.fromCodePoint(tokenValue.codePointAt(0));
52
+ if (firstChar !== firstChar.toLowerCase()) {
53
+ return "capitalized";
54
+ }
55
+ }
56
+ if (token.type === "punctuator" && BRACKET.test(token.value)) {
57
+ return "bracket";
58
+ }
59
+ if (token.type === "invalid" && (token.value === "@" || token.value === "#")) {
60
+ return "punctuator";
61
+ }
62
+ return token.type;
63
+ };
64
+ tokenize = function* (text) {
65
+ let match;
66
+ while (match = jsTokens.default.exec(text)) {
67
+ const token = jsTokens.matchToToken(match);
68
+ yield {
69
+ type: getTokenType(token, match.index, text),
70
+ value: token.value
71
+ };
72
+ }
73
+ };
74
+ function highlight(text) {
75
+ if (text === "") return "";
76
+ const defs = getDefs(true);
77
+ let highlighted = "";
78
+ for (const {
79
+ type,
80
+ value
81
+ } of tokenize(text)) {
82
+ if (type in defs) {
83
+ highlighted += value.split(NEWLINE$1).map(str => defs[type](str)).join("\n");
84
+ } else {
85
+ highlighted += value;
86
+ }
87
+ }
88
+ return highlighted;
89
+ }
90
+
91
+ let deprecationWarningShown = false;
92
+ const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
93
+ function getMarkerLines(loc, source, opts, startLineBaseZero) {
94
+ const startLoc = Object.assign({
95
+ column: 0,
96
+ line: -1
97
+ }, loc.start);
98
+ const endLoc = Object.assign({}, startLoc, loc.end);
99
+ const {
100
+ linesAbove = 2,
101
+ linesBelow = 3
102
+ } = opts || {};
103
+ const startLine = startLoc.line - startLineBaseZero;
104
+ const startColumn = startLoc.column;
105
+ const endLine = endLoc.line - startLineBaseZero;
106
+ const endColumn = endLoc.column;
107
+ let start = Math.max(startLine - (linesAbove + 1), 0);
108
+ let end = Math.min(source.length, endLine + linesBelow);
109
+ if (startLine === -1) {
110
+ start = 0;
111
+ }
112
+ if (endLine === -1) {
113
+ end = source.length;
114
+ }
115
+ const lineDiff = endLine - startLine;
116
+ const markerLines = {};
117
+ if (lineDiff) {
118
+ for (let i = 0; i <= lineDiff; i++) {
119
+ const lineNumber = i + startLine;
120
+ if (!startColumn) {
121
+ markerLines[lineNumber] = true;
122
+ } else if (i === 0) {
123
+ const sourceLength = source[lineNumber - 1].length;
124
+ markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
125
+ } else if (i === lineDiff) {
126
+ markerLines[lineNumber] = [0, endColumn];
127
+ } else {
128
+ const sourceLength = source[lineNumber - i].length;
129
+ markerLines[lineNumber] = [0, sourceLength];
130
+ }
131
+ }
132
+ } else {
133
+ if (startColumn === endColumn) {
134
+ if (startColumn) {
135
+ markerLines[startLine] = [startColumn, 0];
136
+ } else {
137
+ markerLines[startLine] = true;
138
+ }
139
+ } else {
140
+ markerLines[startLine] = [startColumn, endColumn - startColumn];
141
+ }
142
+ }
143
+ return {
144
+ start,
145
+ end,
146
+ markerLines
147
+ };
148
+ }
149
+ function codeFrameColumns(rawLines, loc, opts = {}) {
150
+ const shouldHighlight = opts.forceColor || isColorSupported() && opts.highlightCode;
151
+ const startLineBaseZero = (opts.startLine || 1) - 1;
152
+ const defs = getDefs(shouldHighlight);
153
+ const lines = rawLines.split(NEWLINE);
154
+ const {
155
+ start,
156
+ end,
157
+ markerLines
158
+ } = getMarkerLines(loc, lines, opts, startLineBaseZero);
159
+ const hasColumns = loc.start && typeof loc.start.column === "number";
160
+ const numberMaxWidth = String(end + startLineBaseZero).length;
161
+ const highlightedLines = shouldHighlight ? highlight(rawLines) : rawLines;
162
+ let frame = highlightedLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
163
+ const number = start + 1 + index;
164
+ const paddedNumber = ` ${number + startLineBaseZero}`.slice(-numberMaxWidth);
165
+ const gutter = ` ${paddedNumber} |`;
166
+ const hasMarker = markerLines[number];
167
+ const lastMarkerLine = !markerLines[number + 1];
168
+ if (hasMarker) {
169
+ let markerLine = "";
170
+ if (Array.isArray(hasMarker)) {
171
+ const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
172
+ const numberOfMarkers = hasMarker[1] || 1;
173
+ markerLine = ["\n ", defs.gutter(gutter.replace(/\d/g, " ")), " ", markerSpacing, defs.marker("^").repeat(numberOfMarkers)].join("");
174
+ if (lastMarkerLine && opts.message) {
175
+ markerLine += " " + defs.message(opts.message);
176
+ }
177
+ }
178
+ return [defs.marker(">"), defs.gutter(gutter), line.length > 0 ? ` ${line}` : "", markerLine].join("");
179
+ } else {
180
+ return ` ${defs.gutter(gutter)}${line.length > 0 ? ` ${line}` : ""}`;
181
+ }
182
+ }).join("\n");
183
+ if (opts.message && !hasColumns) {
184
+ frame = `${" ".repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`;
185
+ }
186
+ if (shouldHighlight) {
187
+ return defs.reset(frame);
188
+ } else {
189
+ return frame;
190
+ }
191
+ }
192
+ function index (rawLines, lineNumber, colNumber, opts = {}) {
193
+ if (!deprecationWarningShown) {
194
+ deprecationWarningShown = true;
195
+ const message = "Passing lineNumber and colNumber is deprecated to @babel/code-frame. Please use `codeFrameColumns`.";
196
+ if (process.emitWarning) {
197
+ process.emitWarning(message, "DeprecationWarning");
198
+ } else {
199
+ const deprecationError = new Error(message);
200
+ deprecationError.name = "DeprecationWarning";
201
+ console.warn(new Error(message));
202
+ }
203
+ }
204
+ colNumber = Math.max(colNumber, 0);
205
+ const location = {
206
+ start: {
207
+ column: colNumber,
208
+ line: lineNumber
209
+ }
210
+ };
211
+ return codeFrameColumns(rawLines, location, opts);
212
+ }
213
+
214
+ exports.codeFrameColumns = codeFrameColumns;
215
+ exports.default = index;
216
+ exports.highlight = highlight;
217
+ //# sourceMappingURL=index.js.map
dashboard/frontend/node_modules/@babel/code-frame/lib/index.js.map ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version":3,"file":"index.js","sources":["../src/defs.ts","../src/highlight.ts","../src/index.ts"],"sourcesContent":["import picocolors, { createColors } from \"picocolors\";\nimport type { Colors, Formatter } from \"picocolors/types\";\n\nexport function isColorSupported() {\n return (\n // See https://github.com/alexeyraspopov/picocolors/issues/62\n typeof process === \"object\" &&\n (process.env.FORCE_COLOR === \"0\" || process.env.FORCE_COLOR === \"false\")\n ? false\n : picocolors.isColorSupported\n );\n}\n\nexport type InternalTokenType =\n | \"keyword\"\n | \"capitalized\"\n | \"jsxIdentifier\"\n | \"punctuator\"\n | \"number\"\n | \"string\"\n | \"regex\"\n | \"comment\"\n | \"invalid\";\n\ntype UITokens = \"gutter\" | \"marker\" | \"message\";\n\nexport type Defs = Record<InternalTokenType | UITokens | \"reset\", Formatter>;\n\nconst compose: <T, U, V>(f: (gv: U) => V, g: (v: T) => U) => (v: T) => V =\n (f, g) => v =>\n f(g(v));\n\n/**\n * Styles for token types.\n */\nfunction buildDefs(colors: Colors): Defs {\n return {\n keyword: colors.cyan,\n capitalized: colors.yellow,\n jsxIdentifier: colors.yellow,\n punctuator: colors.yellow,\n number: colors.magenta,\n string: colors.green,\n regex: colors.magenta,\n comment: colors.gray,\n invalid: compose(compose(colors.white, colors.bgRed), colors.bold),\n\n gutter: colors.gray,\n marker: compose(colors.red, colors.bold),\n message: compose(colors.red, colors.bold),\n\n reset: colors.reset,\n };\n}\n\nconst defsOn = buildDefs(createColors(true));\nconst defsOff = buildDefs(createColors(false));\n\nexport function getDefs(enabled: boolean): Defs {\n return enabled ? defsOn : defsOff;\n}\n","import type { Token as JSToken, JSXToken } from \"js-tokens\";\nimport jsTokens from \"js-tokens\";\n// We inline this package\n// eslint-disable-next-line import/no-extraneous-dependencies\nimport * as charCodes from \"charcodes\";\n\nimport {\n isStrictReservedWord,\n isKeyword,\n} from \"@babel/helper-validator-identifier\";\n\nimport { getDefs, type InternalTokenType } from \"./defs.ts\";\n\n/**\n * Names that are always allowed as identifiers, but also appear as keywords\n * within certain syntactic productions.\n *\n * https://tc39.es/ecma262/#sec-keywords-and-reserved-words\n *\n * `target` has been omitted since it is very likely going to be a false\n * positive.\n */\nconst sometimesKeywords = new Set([\"as\", \"async\", \"from\", \"get\", \"of\", \"set\"]);\n\ntype Token = {\n type: InternalTokenType | \"uncolored\";\n value: string;\n};\n\n/**\n * RegExp to test for newlines in terminal.\n */\nconst NEWLINE = /\\r\\n|[\\n\\r\\u2028\\u2029]/;\n\n/**\n * RegExp to test for the three types of brackets.\n */\nconst BRACKET = /^[()[\\]{}]$/;\n\nlet tokenize: (\n text: string,\n) => Generator<{ type: InternalTokenType | \"uncolored\"; value: string }>;\n\nif (process.env.BABEL_8_BREAKING) {\n /**\n * Get the type of token, specifying punctuator type.\n */\n const getTokenType = function (\n token: JSToken | JSXToken,\n ): InternalTokenType | \"uncolored\" {\n if (token.type === \"IdentifierName\") {\n const tokenValue = token.value;\n if (\n isKeyword(tokenValue) ||\n isStrictReservedWord(tokenValue, true) ||\n sometimesKeywords.has(tokenValue)\n ) {\n return \"keyword\";\n }\n\n const firstChar = tokenValue.charCodeAt(0);\n if (firstChar < 128) {\n // ASCII characters\n if (\n firstChar >= charCodes.uppercaseA &&\n firstChar <= charCodes.uppercaseZ\n ) {\n return \"capitalized\";\n }\n } else {\n const firstChar = String.fromCodePoint(tokenValue.codePointAt(0));\n if (firstChar !== firstChar.toLowerCase()) {\n return \"capitalized\";\n }\n }\n }\n\n if (token.type === \"Punctuator\" && BRACKET.test(token.value)) {\n return \"uncolored\";\n }\n\n if (token.type === \"Invalid\" && token.value === \"@\") {\n return \"punctuator\";\n }\n\n switch (token.type) {\n case \"NumericLiteral\":\n return \"number\";\n\n case \"StringLiteral\":\n case \"JSXString\":\n case \"NoSubstitutionTemplate\":\n return \"string\";\n\n case \"RegularExpressionLiteral\":\n return \"regex\";\n\n case \"Punctuator\":\n case \"JSXPunctuator\":\n return \"punctuator\";\n\n case \"MultiLineComment\":\n case \"SingleLineComment\":\n return \"comment\";\n\n case \"Invalid\":\n case \"JSXInvalid\":\n return \"invalid\";\n\n case \"JSXIdentifier\":\n return \"jsxIdentifier\";\n\n default:\n return \"uncolored\";\n }\n };\n\n /**\n * Turn a string of JS into an array of objects.\n */\n tokenize = function* (text: string): Generator<Token> {\n for (const token of jsTokens(text, { jsx: true })) {\n switch (token.type) {\n case \"TemplateHead\":\n yield { type: \"string\", value: token.value.slice(0, -2) };\n yield { type: \"punctuator\", value: \"${\" };\n break;\n\n case \"TemplateMiddle\":\n yield { type: \"punctuator\", value: \"}\" };\n yield { type: \"string\", value: token.value.slice(1, -2) };\n yield { type: \"punctuator\", value: \"${\" };\n break;\n\n case \"TemplateTail\":\n yield { type: \"punctuator\", value: \"}\" };\n yield { type: \"string\", value: token.value.slice(1) };\n break;\n\n default:\n yield {\n type: getTokenType(token),\n value: token.value,\n };\n }\n }\n };\n} else {\n /**\n * RegExp to test for what seems to be a JSX tag name.\n */\n const JSX_TAG = /^[a-z][\\w-]*$/i;\n\n // The token here is defined in js-tokens@4. However we don't bother\n // typing it since the whole block will be removed in Babel 8\n const getTokenType = function (token: any, offset: number, text: string) {\n if (token.type === \"name\") {\n const tokenValue = token.value;\n if (\n isKeyword(tokenValue) ||\n isStrictReservedWord(tokenValue, true) ||\n sometimesKeywords.has(tokenValue)\n ) {\n return \"keyword\";\n }\n\n if (\n JSX_TAG.test(tokenValue) &&\n (text[offset - 1] === \"<\" || text.slice(offset - 2, offset) === \"</\")\n ) {\n return \"jsxIdentifier\";\n }\n\n const firstChar = String.fromCodePoint(tokenValue.codePointAt(0));\n if (firstChar !== firstChar.toLowerCase()) {\n return \"capitalized\";\n }\n }\n\n if (token.type === \"punctuator\" && BRACKET.test(token.value)) {\n return \"bracket\";\n }\n\n if (\n token.type === \"invalid\" &&\n (token.value === \"@\" || token.value === \"#\")\n ) {\n return \"punctuator\";\n }\n\n return token.type;\n };\n\n tokenize = function* (text: string) {\n let match;\n while ((match = (jsTokens as any).default.exec(text))) {\n const token = (jsTokens as any).matchToToken(match);\n\n yield {\n type: getTokenType(token, match.index, text),\n value: token.value,\n };\n }\n };\n}\n\nexport function highlight(text: string) {\n if (text === \"\") return \"\";\n\n const defs = getDefs(true);\n\n let highlighted = \"\";\n\n for (const { type, value } of tokenize(text)) {\n if (type in defs) {\n highlighted += value\n .split(NEWLINE)\n .map(str => defs[type as InternalTokenType](str))\n .join(\"\\n\");\n } else {\n highlighted += value;\n }\n }\n\n return highlighted;\n}\n","import { getDefs, isColorSupported } from \"./defs.ts\";\nimport { highlight } from \"./highlight.ts\";\n\nexport { highlight };\n\nlet deprecationWarningShown = false;\n\ntype Location = {\n column: number;\n line: number;\n};\n\ntype NodeLocation = {\n end?: Location;\n start: Location;\n};\n\nexport interface Options {\n /** Syntax highlight the code as JavaScript for terminals. default: false */\n highlightCode?: boolean;\n /** The number of lines to show above the error. default: 2 */\n linesAbove?: number;\n /** The number of lines to show below the error. default: 3 */\n linesBelow?: number;\n /** The line number corresponding to the first line in `rawLines`. default: 1 */\n startLine?: number;\n /**\n * Forcibly syntax highlight the code as JavaScript (for non-terminals);\n * overrides highlightCode.\n * default: false\n */\n forceColor?: boolean;\n /**\n * Pass in a string to be displayed inline (if possible) next to the\n * highlighted location in the code. If it can't be positioned inline,\n * it will be placed above the code frame.\n * default: nothing\n */\n message?: string;\n}\n\n/**\n * RegExp to test for newlines in terminal.\n */\n\nconst NEWLINE = /\\r\\n|[\\n\\r\\u2028\\u2029]/;\n\n/**\n * Extract what lines should be marked and highlighted.\n */\n\ntype MarkerLines = Record<number, true | [number, number]>;\n\nfunction getMarkerLines(\n loc: NodeLocation,\n source: string[],\n opts: Options,\n startLineBaseZero: number,\n): {\n start: number;\n end: number;\n markerLines: MarkerLines;\n} {\n const startLoc: Location = {\n column: 0,\n line: -1,\n ...loc.start,\n };\n const endLoc: Location = {\n ...startLoc,\n ...loc.end,\n };\n const { linesAbove = 2, linesBelow = 3 } = opts || {};\n const startLine = startLoc.line - startLineBaseZero;\n const startColumn = startLoc.column;\n const endLine = endLoc.line - startLineBaseZero;\n const endColumn = endLoc.column;\n\n let start = Math.max(startLine - (linesAbove + 1), 0);\n let end = Math.min(source.length, endLine + linesBelow);\n\n if (startLine === -1) {\n start = 0;\n }\n\n if (endLine === -1) {\n end = source.length;\n }\n\n const lineDiff = endLine - startLine;\n const markerLines: MarkerLines = {};\n\n if (lineDiff) {\n for (let i = 0; i <= lineDiff; i++) {\n const lineNumber = i + startLine;\n\n if (!startColumn) {\n markerLines[lineNumber] = true;\n } else if (i === 0) {\n const sourceLength = source[lineNumber - 1].length;\n\n markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];\n } else if (i === lineDiff) {\n markerLines[lineNumber] = [0, endColumn];\n } else {\n const sourceLength = source[lineNumber - i].length;\n\n markerLines[lineNumber] = [0, sourceLength];\n }\n }\n } else {\n if (startColumn === endColumn) {\n if (startColumn) {\n markerLines[startLine] = [startColumn, 0];\n } else {\n markerLines[startLine] = true;\n }\n } else {\n markerLines[startLine] = [startColumn, endColumn - startColumn];\n }\n }\n\n return { start, end, markerLines };\n}\n\nexport function codeFrameColumns(\n rawLines: string,\n loc: NodeLocation,\n opts: Options = {},\n): string {\n const shouldHighlight =\n opts.forceColor || (isColorSupported() && opts.highlightCode);\n const startLineBaseZero = (opts.startLine || 1) - 1;\n const defs = getDefs(shouldHighlight);\n\n const lines = rawLines.split(NEWLINE);\n const { start, end, markerLines } = getMarkerLines(\n loc,\n lines,\n opts,\n startLineBaseZero,\n );\n const hasColumns = loc.start && typeof loc.start.column === \"number\";\n\n const numberMaxWidth = String(end + startLineBaseZero).length;\n\n const highlightedLines = shouldHighlight ? highlight(rawLines) : rawLines;\n\n let frame = highlightedLines\n .split(NEWLINE, end)\n .slice(start, end)\n .map((line, index) => {\n const number = start + 1 + index;\n const paddedNumber = ` ${number + startLineBaseZero}`.slice(\n -numberMaxWidth,\n );\n const gutter = ` ${paddedNumber} |`;\n const hasMarker = markerLines[number];\n const lastMarkerLine = !markerLines[number + 1];\n if (hasMarker) {\n let markerLine = \"\";\n if (Array.isArray(hasMarker)) {\n const markerSpacing = line\n .slice(0, Math.max(hasMarker[0] - 1, 0))\n .replace(/[^\\t]/g, \" \");\n const numberOfMarkers = hasMarker[1] || 1;\n\n markerLine = [\n \"\\n \",\n defs.gutter(gutter.replace(/\\d/g, \" \")),\n \" \",\n markerSpacing,\n defs.marker(\"^\").repeat(numberOfMarkers),\n ].join(\"\");\n\n if (lastMarkerLine && opts.message) {\n markerLine += \" \" + defs.message(opts.message);\n }\n }\n return [\n defs.marker(\">\"),\n defs.gutter(gutter),\n line.length > 0 ? ` ${line}` : \"\",\n markerLine,\n ].join(\"\");\n } else {\n return ` ${defs.gutter(gutter)}${line.length > 0 ? ` ${line}` : \"\"}`;\n }\n })\n .join(\"\\n\");\n\n if (opts.message && !hasColumns) {\n frame = `${\" \".repeat(numberMaxWidth + 1)}${opts.message}\\n${frame}`;\n }\n\n if (shouldHighlight) {\n return defs.reset(frame);\n } else {\n return frame;\n }\n}\n\n/**\n * Create a code frame, adding line numbers, code highlighting, and pointing to a given position.\n */\n\nexport default function (\n rawLines: string,\n lineNumber: number,\n colNumber?: number | null,\n opts: Options = {},\n): string {\n if (!deprecationWarningShown) {\n deprecationWarningShown = true;\n\n const message =\n \"Passing lineNumber and colNumber is deprecated to @babel/code-frame. Please use `codeFrameColumns`.\";\n\n if (process.emitWarning) {\n // A string is directly supplied to emitWarning, because when supplying an\n // Error object node throws in the tests because of different contexts\n process.emitWarning(message, \"DeprecationWarning\");\n } else {\n const deprecationError = new Error(message);\n deprecationError.name = \"DeprecationWarning\";\n console.warn(new Error(message));\n }\n }\n\n colNumber = Math.max(colNumber, 0);\n\n const location: NodeLocation = {\n start: { column: colNumber, line: lineNumber },\n };\n\n return codeFrameColumns(rawLines, location, opts);\n}\n"],"names":["isColorSupported","process","env","FORCE_COLOR","picocolors","compose","f","g","v","buildDefs","colors","keyword","cyan","capitalized","yellow","jsxIdentifier","punctuator","number","magenta","string","green","regex","comment","gray","invalid","white","bgRed","bold","gutter","marker","red","message","reset","defsOn","createColors","defsOff","getDefs","enabled","sometimesKeywords","Set","NEWLINE","BRACKET","tokenize","JSX_TAG","getTokenType","token","offset","text","type","tokenValue","value","isKeyword","isStrictReservedWord","has","test","slice","firstChar","String","fromCodePoint","codePointAt","toLowerCase","match","jsTokens","default","exec","matchToToken","index","highlight","defs","highlighted","split","map","str","join","deprecationWarningShown","getMarkerLines","loc","source","opts","startLineBaseZero","startLoc","Object","assign","column","line","start","endLoc","end","linesAbove","linesBelow","startLine","startColumn","endLine","endColumn","Math","max","min","length","lineDiff","markerLines","i","lineNumber","sourceLength","codeFrameColumns","rawLines","shouldHighlight","forceColor","highlightCode","lines","hasColumns","numberMaxWidth","highlightedLines","frame","paddedNumber","hasMarker","lastMarkerLine","markerLine","Array","isArray","markerSpacing","replace","numberOfMarkers","repeat","colNumber","emitWarning","deprecationError","Error","name","console","warn","location"],"mappings":";;;;;;;;AAGO,SAASA,gBAAgBA,GAAG;EACjC,QAEE,OAAOC,OAAO,KAAK,QAAQ,KACxBA,OAAO,CAACC,GAAG,CAACC,WAAW,KAAK,GAAG,IAAIF,OAAO,CAACC,GAAG,CAACC,WAAW,KAAK,OAAO,CAAC,GACtE,KAAK,GACLC,UAAU,CAACJ,gBAAAA;AAAgB,IAAA;AAEnC,CAAA;AAiBA,MAAMK,OAAkE,GACtEA,CAACC,CAAC,EAAEC,CAAC,KAAKC,CAAC,IACTF,CAAC,CAACC,CAAC,CAACC,CAAC,CAAC,CAAC,CAAA;AAKX,SAASC,SAASA,CAACC,MAAc,EAAQ;EACvC,OAAO;IACLC,OAAO,EAAED,MAAM,CAACE,IAAI;IACpBC,WAAW,EAAEH,MAAM,CAACI,MAAM;IAC1BC,aAAa,EAAEL,MAAM,CAACI,MAAM;IAC5BE,UAAU,EAAEN,MAAM,CAACI,MAAM;IACzBG,MAAM,EAAEP,MAAM,CAACQ,OAAO;IACtBC,MAAM,EAAET,MAAM,CAACU,KAAK;IACpBC,KAAK,EAAEX,MAAM,CAACQ,OAAO;IACrBI,OAAO,EAAEZ,MAAM,CAACa,IAAI;AACpBC,IAAAA,OAAO,EAAEnB,OAAO,CAACA,OAAO,CAACK,MAAM,CAACe,KAAK,EAAEf,MAAM,CAACgB,KAAK,CAAC,EAAEhB,MAAM,CAACiB,IAAI,CAAC;IAElEC,MAAM,EAAElB,MAAM,CAACa,IAAI;IACnBM,MAAM,EAAExB,OAAO,CAACK,MAAM,CAACoB,GAAG,EAAEpB,MAAM,CAACiB,IAAI,CAAC;IACxCI,OAAO,EAAE1B,OAAO,CAACK,MAAM,CAACoB,GAAG,EAAEpB,MAAM,CAACiB,IAAI,CAAC;IAEzCK,KAAK,EAAEtB,MAAM,CAACsB,KAAAA;GACf,CAAA;AACH,CAAA;AAEA,MAAMC,MAAM,GAAGxB,SAAS,CAACyB,uBAAY,CAAC,IAAI,CAAC,CAAC,CAAA;AAC5C,MAAMC,OAAO,GAAG1B,SAAS,CAACyB,uBAAY,CAAC,KAAK,CAAC,CAAC,CAAA;AAEvC,SAASE,OAAOA,CAACC,OAAgB,EAAQ;AAC9C,EAAA,OAAOA,OAAO,GAAGJ,MAAM,GAAGE,OAAO,CAAA;AACnC;;ACtCA,MAAMG,iBAAiB,GAAG,IAAIC,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;AAU9E,MAAMC,SAAO,GAAG,yBAAyB,CAAA;AAKzC,MAAMC,OAAO,GAAG,aAAa,CAAA;AAE7B,IAAIC,QAEoE,CAAA;AA8GtE,MAAMC,OAAO,GAAG,gBAAgB,CAAA;AAIhC,MAAMC,YAAY,GAAG,UAAUC,KAAU,EAAEC,MAAc,EAAEC,IAAY,EAAE;AACvE,EAAA,IAAIF,KAAK,CAACG,IAAI,KAAK,MAAM,EAAE;AACzB,IAAA,MAAMC,UAAU,GAAGJ,KAAK,CAACK,KAAK,CAAA;AAC9B,IAAA,IACEC,mCAAS,CAACF,UAAU,CAAC,IACrBG,8CAAoB,CAACH,UAAU,EAAE,IAAI,CAAC,IACtCX,iBAAiB,CAACe,GAAG,CAACJ,UAAU,CAAC,EACjC;AACA,MAAA,OAAO,SAAS,CAAA;AAClB,KAAA;AAEA,IAAA,IACEN,OAAO,CAACW,IAAI,CAACL,UAAU,CAAC,KACvBF,IAAI,CAACD,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,IAAIC,IAAI,CAACQ,KAAK,CAACT,MAAM,GAAG,CAAC,EAAEA,MAAM,CAAC,KAAK,IAAI,CAAC,EACrE;AACA,MAAA,OAAO,eAAe,CAAA;AACxB,KAAA;AAEA,IAAA,MAAMU,SAAS,GAAGC,MAAM,CAACC,aAAa,CAACT,UAAU,CAACU,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AACjE,IAAA,IAAIH,SAAS,KAAKA,SAAS,CAACI,WAAW,EAAE,EAAE;AACzC,MAAA,OAAO,aAAa,CAAA;AACtB,KAAA;AACF,GAAA;AAEA,EAAA,IAAIf,KAAK,CAACG,IAAI,KAAK,YAAY,IAAIP,OAAO,CAACa,IAAI,CAACT,KAAK,CAACK,KAAK,CAAC,EAAE;AAC5D,IAAA,OAAO,SAAS,CAAA;AAClB,GAAA;AAEA,EAAA,IACEL,KAAK,CAACG,IAAI,KAAK,SAAS,KACvBH,KAAK,CAACK,KAAK,KAAK,GAAG,IAAIL,KAAK,CAACK,KAAK,KAAK,GAAG,CAAC,EAC5C;AACA,IAAA,OAAO,YAAY,CAAA;AACrB,GAAA;EAEA,OAAOL,KAAK,CAACG,IAAI,CAAA;AACnB,CAAC,CAAA;AAEDN,QAAQ,GAAG,WAAWK,IAAY,EAAE;AAClC,EAAA,IAAIc,KAAK,CAAA;EACT,OAAQA,KAAK,GAAIC,QAAQ,CAASC,OAAO,CAACC,IAAI,CAACjB,IAAI,CAAC,EAAG;AACrD,IAAA,MAAMF,KAAK,GAAIiB,QAAQ,CAASG,YAAY,CAACJ,KAAK,CAAC,CAAA;IAEnD,MAAM;MACJb,IAAI,EAAEJ,YAAY,CAACC,KAAK,EAAEgB,KAAK,CAACK,KAAK,EAAEnB,IAAI,CAAC;MAC5CG,KAAK,EAAEL,KAAK,CAACK,KAAAA;KACd,CAAA;AACH,GAAA;AACF,CAAC,CAAA;AAGI,SAASiB,SAASA,CAACpB,IAAY,EAAE;AACtC,EAAA,IAAIA,IAAI,KAAK,EAAE,EAAE,OAAO,EAAE,CAAA;AAE1B,EAAA,MAAMqB,IAAI,GAAGhC,OAAO,CAAC,IAAI,CAAC,CAAA;EAE1B,IAAIiC,WAAW,GAAG,EAAE,CAAA;AAEpB,EAAA,KAAK,MAAM;IAAErB,IAAI;AAAEE,IAAAA,KAAAA;AAAM,GAAC,IAAIR,QAAQ,CAACK,IAAI,CAAC,EAAE;IAC5C,IAAIC,IAAI,IAAIoB,IAAI,EAAE;MAChBC,WAAW,IAAInB,KAAK,CACjBoB,KAAK,CAAC9B,SAAO,CAAC,CACd+B,GAAG,CAACC,GAAG,IAAIJ,IAAI,CAACpB,IAAI,CAAsB,CAACwB,GAAG,CAAC,CAAC,CAChDC,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,KAAC,MAAM;AACLJ,MAAAA,WAAW,IAAInB,KAAK,CAAA;AACtB,KAAA;AACF,GAAA;AAEA,EAAA,OAAOmB,WAAW,CAAA;AACpB;;AC5NA,IAAIK,uBAAuB,GAAG,KAAK,CAAA;AAwCnC,MAAMlC,OAAO,GAAG,yBAAyB,CAAA;AAQzC,SAASmC,cAAcA,CACrBC,GAAiB,EACjBC,MAAgB,EAChBC,IAAa,EACbC,iBAAyB,EAKzB;AACA,EAAA,MAAMC,QAAkB,GAAAC,MAAA,CAAAC,MAAA,CAAA;AACtBC,IAAAA,MAAM,EAAE,CAAC;AACTC,IAAAA,IAAI,EAAE,CAAC,CAAA;GACJR,EAAAA,GAAG,CAACS,KAAK,CACb,CAAA;EACD,MAAMC,MAAgB,GAAAL,MAAA,CAAAC,MAAA,CACjBF,EAAAA,EAAAA,QAAQ,EACRJ,GAAG,CAACW,GAAG,CACX,CAAA;EACD,MAAM;AAAEC,IAAAA,UAAU,GAAG,CAAC;AAAEC,IAAAA,UAAU,GAAG,CAAA;AAAE,GAAC,GAAGX,IAAI,IAAI,EAAE,CAAA;AACrD,EAAA,MAAMY,SAAS,GAAGV,QAAQ,CAACI,IAAI,GAAGL,iBAAiB,CAAA;AACnD,EAAA,MAAMY,WAAW,GAAGX,QAAQ,CAACG,MAAM,CAAA;AACnC,EAAA,MAAMS,OAAO,GAAGN,MAAM,CAACF,IAAI,GAAGL,iBAAiB,CAAA;AAC/C,EAAA,MAAMc,SAAS,GAAGP,MAAM,CAACH,MAAM,CAAA;AAE/B,EAAA,IAAIE,KAAK,GAAGS,IAAI,CAACC,GAAG,CAACL,SAAS,IAAIF,UAAU,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AACrD,EAAA,IAAID,GAAG,GAAGO,IAAI,CAACE,GAAG,CAACnB,MAAM,CAACoB,MAAM,EAAEL,OAAO,GAAGH,UAAU,CAAC,CAAA;AAEvD,EAAA,IAAIC,SAAS,KAAK,CAAC,CAAC,EAAE;AACpBL,IAAAA,KAAK,GAAG,CAAC,CAAA;AACX,GAAA;AAEA,EAAA,IAAIO,OAAO,KAAK,CAAC,CAAC,EAAE;IAClBL,GAAG,GAAGV,MAAM,CAACoB,MAAM,CAAA;AACrB,GAAA;AAEA,EAAA,MAAMC,QAAQ,GAAGN,OAAO,GAAGF,SAAS,CAAA;EACpC,MAAMS,WAAwB,GAAG,EAAE,CAAA;AAEnC,EAAA,IAAID,QAAQ,EAAE;IACZ,KAAK,IAAIE,CAAC,GAAG,CAAC,EAAEA,CAAC,IAAIF,QAAQ,EAAEE,CAAC,EAAE,EAAE;AAClC,MAAA,MAAMC,UAAU,GAAGD,CAAC,GAAGV,SAAS,CAAA;MAEhC,IAAI,CAACC,WAAW,EAAE;AAChBQ,QAAAA,WAAW,CAACE,UAAU,CAAC,GAAG,IAAI,CAAA;AAChC,OAAC,MAAM,IAAID,CAAC,KAAK,CAAC,EAAE;QAClB,MAAME,YAAY,GAAGzB,MAAM,CAACwB,UAAU,GAAG,CAAC,CAAC,CAACJ,MAAM,CAAA;AAElDE,QAAAA,WAAW,CAACE,UAAU,CAAC,GAAG,CAACV,WAAW,EAAEW,YAAY,GAAGX,WAAW,GAAG,CAAC,CAAC,CAAA;AACzE,OAAC,MAAM,IAAIS,CAAC,KAAKF,QAAQ,EAAE;QACzBC,WAAW,CAACE,UAAU,CAAC,GAAG,CAAC,CAAC,EAAER,SAAS,CAAC,CAAA;AAC1C,OAAC,MAAM;QACL,MAAMS,YAAY,GAAGzB,MAAM,CAACwB,UAAU,GAAGD,CAAC,CAAC,CAACH,MAAM,CAAA;QAElDE,WAAW,CAACE,UAAU,CAAC,GAAG,CAAC,CAAC,EAAEC,YAAY,CAAC,CAAA;AAC7C,OAAA;AACF,KAAA;AACF,GAAC,MAAM;IACL,IAAIX,WAAW,KAAKE,SAAS,EAAE;AAC7B,MAAA,IAAIF,WAAW,EAAE;QACfQ,WAAW,CAACT,SAAS,CAAC,GAAG,CAACC,WAAW,EAAE,CAAC,CAAC,CAAA;AAC3C,OAAC,MAAM;AACLQ,QAAAA,WAAW,CAACT,SAAS,CAAC,GAAG,IAAI,CAAA;AAC/B,OAAA;AACF,KAAC,MAAM;MACLS,WAAW,CAACT,SAAS,CAAC,GAAG,CAACC,WAAW,EAAEE,SAAS,GAAGF,WAAW,CAAC,CAAA;AACjE,KAAA;AACF,GAAA;EAEA,OAAO;IAAEN,KAAK;IAAEE,GAAG;AAAEY,IAAAA,WAAAA;GAAa,CAAA;AACpC,CAAA;AAEO,SAASI,gBAAgBA,CAC9BC,QAAgB,EAChB5B,GAAiB,EACjBE,IAAa,GAAG,EAAE,EACV;AACR,EAAA,MAAM2B,eAAe,GACnB3B,IAAI,CAAC4B,UAAU,IAAK1G,gBAAgB,EAAE,IAAI8E,IAAI,CAAC6B,aAAc,CAAA;EAC/D,MAAM5B,iBAAiB,GAAG,CAACD,IAAI,CAACY,SAAS,IAAI,CAAC,IAAI,CAAC,CAAA;AACnD,EAAA,MAAMtB,IAAI,GAAGhC,OAAO,CAACqE,eAAe,CAAC,CAAA;AAErC,EAAA,MAAMG,KAAK,GAAGJ,QAAQ,CAAClC,KAAK,CAAC9B,OAAO,CAAC,CAAA;EACrC,MAAM;IAAE6C,KAAK;IAAEE,GAAG;AAAEY,IAAAA,WAAAA;GAAa,GAAGxB,cAAc,CAChDC,GAAG,EACHgC,KAAK,EACL9B,IAAI,EACJC,iBACF,CAAC,CAAA;AACD,EAAA,MAAM8B,UAAU,GAAGjC,GAAG,CAACS,KAAK,IAAI,OAAOT,GAAG,CAACS,KAAK,CAACF,MAAM,KAAK,QAAQ,CAAA;EAEpE,MAAM2B,cAAc,GAAGrD,MAAM,CAAC8B,GAAG,GAAGR,iBAAiB,CAAC,CAACkB,MAAM,CAAA;EAE7D,MAAMc,gBAAgB,GAAGN,eAAe,GAAGtC,SAAS,CAACqC,QAAQ,CAAC,GAAGA,QAAQ,CAAA;EAEzE,IAAIQ,KAAK,GAAGD,gBAAgB,CACzBzC,KAAK,CAAC9B,OAAO,EAAE+C,GAAG,CAAC,CACnBhC,KAAK,CAAC8B,KAAK,EAAEE,GAAG,CAAC,CACjBhB,GAAG,CAAC,CAACa,IAAI,EAAElB,KAAK,KAAK;AACpB,IAAA,MAAMjD,MAAM,GAAGoE,KAAK,GAAG,CAAC,GAAGnB,KAAK,CAAA;AAChC,IAAA,MAAM+C,YAAY,GAAG,CAAIhG,CAAAA,EAAAA,MAAM,GAAG8D,iBAAiB,CAAE,CAAA,CAACxB,KAAK,CACzD,CAACuD,cACH,CAAC,CAAA;AACD,IAAA,MAAMlF,MAAM,GAAG,CAAIqF,CAAAA,EAAAA,YAAY,CAAI,EAAA,CAAA,CAAA;AACnC,IAAA,MAAMC,SAAS,GAAGf,WAAW,CAAClF,MAAM,CAAC,CAAA;IACrC,MAAMkG,cAAc,GAAG,CAAChB,WAAW,CAAClF,MAAM,GAAG,CAAC,CAAC,CAAA;AAC/C,IAAA,IAAIiG,SAAS,EAAE;MACb,IAAIE,UAAU,GAAG,EAAE,CAAA;AACnB,MAAA,IAAIC,KAAK,CAACC,OAAO,CAACJ,SAAS,CAAC,EAAE;AAC5B,QAAA,MAAMK,aAAa,GAAGnC,IAAI,CACvB7B,KAAK,CAAC,CAAC,EAAEuC,IAAI,CAACC,GAAG,CAACmB,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CACvCM,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACzB,QAAA,MAAMC,eAAe,GAAGP,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;AAEzCE,QAAAA,UAAU,GAAG,CACX,KAAK,EACLhD,IAAI,CAACxC,MAAM,CAACA,MAAM,CAAC4F,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,EACvC,GAAG,EACHD,aAAa,EACbnD,IAAI,CAACvC,MAAM,CAAC,GAAG,CAAC,CAAC6F,MAAM,CAACD,eAAe,CAAC,CACzC,CAAChD,IAAI,CAAC,EAAE,CAAC,CAAA;AAEV,QAAA,IAAI0C,cAAc,IAAIrC,IAAI,CAAC/C,OAAO,EAAE;UAClCqF,UAAU,IAAI,GAAG,GAAGhD,IAAI,CAACrC,OAAO,CAAC+C,IAAI,CAAC/C,OAAO,CAAC,CAAA;AAChD,SAAA;AACF,OAAA;AACA,MAAA,OAAO,CACLqC,IAAI,CAACvC,MAAM,CAAC,GAAG,CAAC,EAChBuC,IAAI,CAACxC,MAAM,CAACA,MAAM,CAAC,EACnBwD,IAAI,CAACa,MAAM,GAAG,CAAC,GAAG,CAAA,CAAA,EAAIb,IAAI,CAAE,CAAA,GAAG,EAAE,EACjCgC,UAAU,CACX,CAAC3C,IAAI,CAAC,EAAE,CAAC,CAAA;AACZ,KAAC,MAAM;AACL,MAAA,OAAO,IAAIL,IAAI,CAACxC,MAAM,CAACA,MAAM,CAAC,CAAGwD,EAAAA,IAAI,CAACa,MAAM,GAAG,CAAC,GAAG,CAAA,CAAA,EAAIb,IAAI,CAAE,CAAA,GAAG,EAAE,CAAE,CAAA,CAAA;AACtE,KAAA;AACF,GAAC,CAAC,CACDX,IAAI,CAAC,IAAI,CAAC,CAAA;AAEb,EAAA,IAAIK,IAAI,CAAC/C,OAAO,IAAI,CAAC8E,UAAU,EAAE;AAC/BG,IAAAA,KAAK,GAAG,CAAG,EAAA,GAAG,CAACU,MAAM,CAACZ,cAAc,GAAG,CAAC,CAAC,GAAGhC,IAAI,CAAC/C,OAAO,CAAA,EAAA,EAAKiF,KAAK,CAAE,CAAA,CAAA;AACtE,GAAA;AAEA,EAAA,IAAIP,eAAe,EAAE;AACnB,IAAA,OAAOrC,IAAI,CAACpC,KAAK,CAACgF,KAAK,CAAC,CAAA;AAC1B,GAAC,MAAM;AACL,IAAA,OAAOA,KAAK,CAAA;AACd,GAAA;AACF,CAAA;AAMe,cAAA,EACbR,QAAgB,EAChBH,UAAkB,EAClBsB,SAAyB,EACzB7C,IAAa,GAAG,EAAE,EACV;EACR,IAAI,CAACJ,uBAAuB,EAAE;AAC5BA,IAAAA,uBAAuB,GAAG,IAAI,CAAA;IAE9B,MAAM3C,OAAO,GACX,qGAAqG,CAAA;IAEvG,IAAI9B,OAAO,CAAC2H,WAAW,EAAE;AAGvB3H,MAAAA,OAAO,CAAC2H,WAAW,CAAC7F,OAAO,EAAE,oBAAoB,CAAC,CAAA;AACpD,KAAC,MAAM;AACL,MAAA,MAAM8F,gBAAgB,GAAG,IAAIC,KAAK,CAAC/F,OAAO,CAAC,CAAA;MAC3C8F,gBAAgB,CAACE,IAAI,GAAG,oBAAoB,CAAA;MAC5CC,OAAO,CAACC,IAAI,CAAC,IAAIH,KAAK,CAAC/F,OAAO,CAAC,CAAC,CAAA;AAClC,KAAA;AACF,GAAA;EAEA4F,SAAS,GAAG7B,IAAI,CAACC,GAAG,CAAC4B,SAAS,EAAE,CAAC,CAAC,CAAA;AAElC,EAAA,MAAMO,QAAsB,GAAG;AAC7B7C,IAAAA,KAAK,EAAE;AAAEF,MAAAA,MAAM,EAAEwC,SAAS;AAAEvC,MAAAA,IAAI,EAAEiB,UAAAA;AAAW,KAAA;GAC9C,CAAA;AAED,EAAA,OAAOE,gBAAgB,CAACC,QAAQ,EAAE0B,QAAQ,EAAEpD,IAAI,CAAC,CAAA;AACnD;;;;;;"}
dashboard/frontend/node_modules/@babel/code-frame/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@babel/code-frame",
3
+ "version": "7.29.0",
4
+ "description": "Generate errors that contain a code frame that point to source locations.",
5
+ "author": "The Babel Team (https://babel.dev/team)",
6
+ "homepage": "https://babel.dev/docs/en/next/babel-code-frame",
7
+ "bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen",
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/babel/babel.git",
15
+ "directory": "packages/babel-code-frame"
16
+ },
17
+ "main": "./lib/index.js",
18
+ "dependencies": {
19
+ "@babel/helper-validator-identifier": "^7.28.5",
20
+ "js-tokens": "^4.0.0",
21
+ "picocolors": "^1.1.1"
22
+ },
23
+ "devDependencies": {
24
+ "charcodes": "^0.2.0",
25
+ "import-meta-resolve": "^4.1.0",
26
+ "strip-ansi": "^4.0.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=6.9.0"
30
+ },
31
+ "type": "commonjs"
32
+ }
dashboard/frontend/node_modules/@babel/compat-data/LICENSE ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2014-present Sebastian McKenzie and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dashboard/frontend/node_modules/@babel/compat-data/README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @babel/compat-data
2
+
3
+ > The compat-data to determine required Babel plugins
4
+
5
+ See our website [@babel/compat-data](https://babeljs.io/docs/babel-compat-data) for more information.
6
+
7
+ ## Install
8
+
9
+ Using npm:
10
+
11
+ ```sh
12
+ npm install --save @babel/compat-data
13
+ ```
14
+
15
+ or using yarn:
16
+
17
+ ```sh
18
+ yarn add @babel/compat-data
19
+ ```
dashboard/frontend/node_modules/@babel/compat-data/corejs2-built-ins.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Todo (Babel 8): remove this file as Babel 8 drop support of core-js 2
2
+ module.exports = require("./data/corejs2-built-ins.json");
dashboard/frontend/node_modules/@babel/compat-data/corejs3-shipped-proposals.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Todo (Babel 8): remove this file now that it is included in babel-plugin-polyfill-corejs3
2
+ module.exports = require("./data/corejs3-shipped-proposals.json");
dashboard/frontend/node_modules/@babel/compat-data/data/corejs2-built-ins.json ADDED
@@ -0,0 +1,2106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "es6.array.copy-within": {
3
+ "chrome": "45",
4
+ "opera": "32",
5
+ "edge": "12",
6
+ "firefox": "32",
7
+ "safari": "9",
8
+ "node": "4",
9
+ "deno": "1",
10
+ "ios": "9",
11
+ "samsung": "5",
12
+ "rhino": "1.7.13",
13
+ "opera_mobile": "32",
14
+ "electron": "0.31"
15
+ },
16
+ "es6.array.every": {
17
+ "chrome": "5",
18
+ "opera": "10.10",
19
+ "edge": "12",
20
+ "firefox": "2",
21
+ "safari": "3.1",
22
+ "node": "0.4",
23
+ "deno": "1",
24
+ "ie": "9",
25
+ "android": "4",
26
+ "ios": "6",
27
+ "phantom": "1.9",
28
+ "samsung": "1",
29
+ "rhino": "1.7.13",
30
+ "opera_mobile": "10.1",
31
+ "electron": "0.20"
32
+ },
33
+ "es6.array.fill": {
34
+ "chrome": "45",
35
+ "opera": "32",
36
+ "edge": "12",
37
+ "firefox": "31",
38
+ "safari": "7.1",
39
+ "node": "4",
40
+ "deno": "1",
41
+ "ios": "8",
42
+ "samsung": "5",
43
+ "rhino": "1.7.13",
44
+ "opera_mobile": "32",
45
+ "electron": "0.31"
46
+ },
47
+ "es6.array.filter": {
48
+ "chrome": "51",
49
+ "opera": "38",
50
+ "edge": "13",
51
+ "firefox": "48",
52
+ "safari": "10",
53
+ "node": "6.5",
54
+ "deno": "1",
55
+ "ios": "10",
56
+ "samsung": "5",
57
+ "opera_mobile": "41",
58
+ "electron": "1.2"
59
+ },
60
+ "es6.array.find": {
61
+ "chrome": "45",
62
+ "opera": "32",
63
+ "edge": "12",
64
+ "firefox": "25",
65
+ "safari": "7.1",
66
+ "node": "4",
67
+ "deno": "1",
68
+ "ios": "8",
69
+ "samsung": "5",
70
+ "rhino": "1.7.13",
71
+ "opera_mobile": "32",
72
+ "electron": "0.31"
73
+ },
74
+ "es6.array.find-index": {
75
+ "chrome": "45",
76
+ "opera": "32",
77
+ "edge": "12",
78
+ "firefox": "25",
79
+ "safari": "7.1",
80
+ "node": "4",
81
+ "deno": "1",
82
+ "ios": "8",
83
+ "samsung": "5",
84
+ "rhino": "1.7.13",
85
+ "opera_mobile": "32",
86
+ "electron": "0.31"
87
+ },
88
+ "es7.array.flat-map": {
89
+ "chrome": "69",
90
+ "opera": "56",
91
+ "edge": "79",
92
+ "firefox": "62",
93
+ "safari": "12",
94
+ "node": "11",
95
+ "deno": "1",
96
+ "ios": "12",
97
+ "samsung": "10",
98
+ "rhino": "1.7.15",
99
+ "opera_mobile": "48",
100
+ "electron": "4.0"
101
+ },
102
+ "es6.array.for-each": {
103
+ "chrome": "5",
104
+ "opera": "10.10",
105
+ "edge": "12",
106
+ "firefox": "2",
107
+ "safari": "3.1",
108
+ "node": "0.4",
109
+ "deno": "1",
110
+ "ie": "9",
111
+ "android": "4",
112
+ "ios": "6",
113
+ "phantom": "1.9",
114
+ "samsung": "1",
115
+ "rhino": "1.7.13",
116
+ "opera_mobile": "10.1",
117
+ "electron": "0.20"
118
+ },
119
+ "es6.array.from": {
120
+ "chrome": "51",
121
+ "opera": "38",
122
+ "edge": "15",
123
+ "firefox": "36",
124
+ "safari": "10",
125
+ "node": "6.5",
126
+ "deno": "1",
127
+ "ios": "10",
128
+ "samsung": "5",
129
+ "rhino": "1.7.15",
130
+ "opera_mobile": "41",
131
+ "electron": "1.2"
132
+ },
133
+ "es7.array.includes": {
134
+ "chrome": "47",
135
+ "opera": "34",
136
+ "edge": "14",
137
+ "firefox": "102",
138
+ "safari": "10",
139
+ "node": "6",
140
+ "deno": "1",
141
+ "ios": "10",
142
+ "samsung": "5",
143
+ "rhino": "1.8",
144
+ "opera_mobile": "34",
145
+ "electron": "0.36"
146
+ },
147
+ "es6.array.index-of": {
148
+ "chrome": "5",
149
+ "opera": "10.10",
150
+ "edge": "12",
151
+ "firefox": "2",
152
+ "safari": "3.1",
153
+ "node": "0.4",
154
+ "deno": "1",
155
+ "ie": "9",
156
+ "android": "4",
157
+ "ios": "6",
158
+ "phantom": "1.9",
159
+ "samsung": "1",
160
+ "rhino": "1.7.13",
161
+ "opera_mobile": "10.1",
162
+ "electron": "0.20"
163
+ },
164
+ "es6.array.is-array": {
165
+ "chrome": "5",
166
+ "opera": "10.50",
167
+ "edge": "12",
168
+ "firefox": "4",
169
+ "safari": "4",
170
+ "node": "0.4",
171
+ "deno": "1",
172
+ "ie": "9",
173
+ "android": "4",
174
+ "ios": "6",
175
+ "phantom": "1.9",
176
+ "samsung": "1",
177
+ "rhino": "1.7.13",
178
+ "opera_mobile": "10.1",
179
+ "electron": "0.20"
180
+ },
181
+ "es6.array.iterator": {
182
+ "chrome": "66",
183
+ "opera": "53",
184
+ "edge": "12",
185
+ "firefox": "60",
186
+ "safari": "9",
187
+ "node": "10",
188
+ "deno": "1",
189
+ "ios": "9",
190
+ "samsung": "9",
191
+ "rhino": "1.7.13",
192
+ "opera_mobile": "47",
193
+ "electron": "3.0"
194
+ },
195
+ "es6.array.last-index-of": {
196
+ "chrome": "5",
197
+ "opera": "10.10",
198
+ "edge": "12",
199
+ "firefox": "2",
200
+ "safari": "3.1",
201
+ "node": "0.4",
202
+ "deno": "1",
203
+ "ie": "9",
204
+ "android": "4",
205
+ "ios": "6",
206
+ "phantom": "1.9",
207
+ "samsung": "1",
208
+ "rhino": "1.7.13",
209
+ "opera_mobile": "10.1",
210
+ "electron": "0.20"
211
+ },
212
+ "es6.array.map": {
213
+ "chrome": "51",
214
+ "opera": "38",
215
+ "edge": "13",
216
+ "firefox": "48",
217
+ "safari": "10",
218
+ "node": "6.5",
219
+ "deno": "1",
220
+ "ios": "10",
221
+ "samsung": "5",
222
+ "opera_mobile": "41",
223
+ "electron": "1.2"
224
+ },
225
+ "es6.array.of": {
226
+ "chrome": "45",
227
+ "opera": "32",
228
+ "edge": "12",
229
+ "firefox": "25",
230
+ "safari": "9",
231
+ "node": "4",
232
+ "deno": "1",
233
+ "ios": "9",
234
+ "samsung": "5",
235
+ "rhino": "1.7.13",
236
+ "opera_mobile": "32",
237
+ "electron": "0.31"
238
+ },
239
+ "es6.array.reduce": {
240
+ "chrome": "5",
241
+ "opera": "10.50",
242
+ "edge": "12",
243
+ "firefox": "3",
244
+ "safari": "4",
245
+ "node": "0.4",
246
+ "deno": "1",
247
+ "ie": "9",
248
+ "android": "4",
249
+ "ios": "6",
250
+ "phantom": "1.9",
251
+ "samsung": "1",
252
+ "rhino": "1.7.13",
253
+ "opera_mobile": "10.1",
254
+ "electron": "0.20"
255
+ },
256
+ "es6.array.reduce-right": {
257
+ "chrome": "5",
258
+ "opera": "10.50",
259
+ "edge": "12",
260
+ "firefox": "3",
261
+ "safari": "4",
262
+ "node": "0.4",
263
+ "deno": "1",
264
+ "ie": "9",
265
+ "android": "4",
266
+ "ios": "6",
267
+ "phantom": "1.9",
268
+ "samsung": "1",
269
+ "rhino": "1.7.13",
270
+ "opera_mobile": "10.1",
271
+ "electron": "0.20"
272
+ },
273
+ "es6.array.slice": {
274
+ "chrome": "51",
275
+ "opera": "38",
276
+ "edge": "13",
277
+ "firefox": "48",
278
+ "safari": "10",
279
+ "node": "6.5",
280
+ "deno": "1",
281
+ "ios": "10",
282
+ "samsung": "5",
283
+ "opera_mobile": "41",
284
+ "electron": "1.2"
285
+ },
286
+ "es6.array.some": {
287
+ "chrome": "5",
288
+ "opera": "10.10",
289
+ "edge": "12",
290
+ "firefox": "2",
291
+ "safari": "3.1",
292
+ "node": "0.4",
293
+ "deno": "1",
294
+ "ie": "9",
295
+ "android": "4",
296
+ "ios": "6",
297
+ "phantom": "1.9",
298
+ "samsung": "1",
299
+ "rhino": "1.7.13",
300
+ "opera_mobile": "10.1",
301
+ "electron": "0.20"
302
+ },
303
+ "es6.array.sort": {
304
+ "chrome": "63",
305
+ "opera": "50",
306
+ "edge": "12",
307
+ "firefox": "5",
308
+ "safari": "12",
309
+ "node": "10",
310
+ "deno": "1",
311
+ "ie": "9",
312
+ "ios": "12",
313
+ "samsung": "8",
314
+ "rhino": "1.7.13",
315
+ "opera_mobile": "46",
316
+ "electron": "3.0"
317
+ },
318
+ "es6.array.species": {
319
+ "chrome": "51",
320
+ "opera": "38",
321
+ "edge": "13",
322
+ "firefox": "48",
323
+ "safari": "10",
324
+ "node": "6.5",
325
+ "deno": "1",
326
+ "ios": "10",
327
+ "samsung": "5",
328
+ "rhino": "1.7.15",
329
+ "opera_mobile": "41",
330
+ "electron": "1.2"
331
+ },
332
+ "es6.date.now": {
333
+ "chrome": "5",
334
+ "opera": "10.50",
335
+ "edge": "12",
336
+ "firefox": "2",
337
+ "safari": "4",
338
+ "node": "0.4",
339
+ "deno": "1",
340
+ "ie": "9",
341
+ "android": "4",
342
+ "ios": "6",
343
+ "phantom": "1.9",
344
+ "samsung": "1",
345
+ "rhino": "1.7.13",
346
+ "opera_mobile": "10.1",
347
+ "electron": "0.20"
348
+ },
349
+ "es6.date.to-iso-string": {
350
+ "chrome": "5",
351
+ "opera": "10.50",
352
+ "edge": "12",
353
+ "firefox": "3.5",
354
+ "safari": "4",
355
+ "node": "0.4",
356
+ "deno": "1",
357
+ "ie": "9",
358
+ "android": "4",
359
+ "ios": "6",
360
+ "phantom": "1.9",
361
+ "samsung": "1",
362
+ "rhino": "1.7.13",
363
+ "opera_mobile": "10.1",
364
+ "electron": "0.20"
365
+ },
366
+ "es6.date.to-json": {
367
+ "chrome": "5",
368
+ "opera": "12.10",
369
+ "edge": "12",
370
+ "firefox": "4",
371
+ "safari": "10",
372
+ "node": "0.4",
373
+ "deno": "1",
374
+ "ie": "9",
375
+ "android": "4",
376
+ "ios": "10",
377
+ "samsung": "1",
378
+ "rhino": "1.7.13",
379
+ "opera_mobile": "12.1",
380
+ "electron": "0.20"
381
+ },
382
+ "es6.date.to-primitive": {
383
+ "chrome": "47",
384
+ "opera": "34",
385
+ "edge": "15",
386
+ "firefox": "44",
387
+ "safari": "10",
388
+ "node": "6",
389
+ "deno": "1",
390
+ "ios": "10",
391
+ "samsung": "5",
392
+ "rhino": "1.8",
393
+ "opera_mobile": "34",
394
+ "electron": "0.36"
395
+ },
396
+ "es6.date.to-string": {
397
+ "chrome": "5",
398
+ "opera": "10.50",
399
+ "edge": "12",
400
+ "firefox": "2",
401
+ "safari": "3.1",
402
+ "node": "0.4",
403
+ "deno": "1",
404
+ "ie": "10",
405
+ "android": "4",
406
+ "ios": "6",
407
+ "phantom": "1.9",
408
+ "samsung": "1",
409
+ "rhino": "1.7.13",
410
+ "opera_mobile": "10.1",
411
+ "electron": "0.20"
412
+ },
413
+ "es6.function.bind": {
414
+ "chrome": "7",
415
+ "opera": "12",
416
+ "edge": "12",
417
+ "firefox": "4",
418
+ "safari": "5.1",
419
+ "node": "0.4",
420
+ "deno": "1",
421
+ "ie": "9",
422
+ "android": "4",
423
+ "ios": "6",
424
+ "phantom": "1.9",
425
+ "samsung": "1",
426
+ "rhino": "1.7.13",
427
+ "opera_mobile": "12",
428
+ "electron": "0.20"
429
+ },
430
+ "es6.function.has-instance": {
431
+ "chrome": "51",
432
+ "opera": "38",
433
+ "edge": "15",
434
+ "firefox": "50",
435
+ "safari": "10",
436
+ "node": "6.5",
437
+ "deno": "1",
438
+ "ios": "10",
439
+ "samsung": "5",
440
+ "opera_mobile": "41",
441
+ "electron": "1.2"
442
+ },
443
+ "es6.function.name": {
444
+ "chrome": "5",
445
+ "opera": "10.50",
446
+ "edge": "14",
447
+ "firefox": "2",
448
+ "safari": "4",
449
+ "node": "0.4",
450
+ "deno": "1",
451
+ "android": "4",
452
+ "ios": "6",
453
+ "phantom": "1.9",
454
+ "samsung": "1",
455
+ "rhino": "1.7.13",
456
+ "opera_mobile": "10.1",
457
+ "electron": "0.20"
458
+ },
459
+ "es6.map": {
460
+ "chrome": "51",
461
+ "opera": "38",
462
+ "edge": "15",
463
+ "firefox": "53",
464
+ "safari": "10",
465
+ "node": "6.5",
466
+ "deno": "1",
467
+ "ios": "10",
468
+ "samsung": "5",
469
+ "opera_mobile": "41",
470
+ "electron": "1.2"
471
+ },
472
+ "es6.math.acosh": {
473
+ "chrome": "38",
474
+ "opera": "25",
475
+ "edge": "12",
476
+ "firefox": "25",
477
+ "safari": "7.1",
478
+ "node": "0.12",
479
+ "deno": "1",
480
+ "ios": "8",
481
+ "samsung": "3",
482
+ "rhino": "1.7.13",
483
+ "opera_mobile": "25",
484
+ "electron": "0.20"
485
+ },
486
+ "es6.math.asinh": {
487
+ "chrome": "38",
488
+ "opera": "25",
489
+ "edge": "12",
490
+ "firefox": "25",
491
+ "safari": "7.1",
492
+ "node": "0.12",
493
+ "deno": "1",
494
+ "ios": "8",
495
+ "samsung": "3",
496
+ "rhino": "1.7.13",
497
+ "opera_mobile": "25",
498
+ "electron": "0.20"
499
+ },
500
+ "es6.math.atanh": {
501
+ "chrome": "38",
502
+ "opera": "25",
503
+ "edge": "12",
504
+ "firefox": "25",
505
+ "safari": "7.1",
506
+ "node": "0.12",
507
+ "deno": "1",
508
+ "ios": "8",
509
+ "samsung": "3",
510
+ "rhino": "1.7.13",
511
+ "opera_mobile": "25",
512
+ "electron": "0.20"
513
+ },
514
+ "es6.math.cbrt": {
515
+ "chrome": "38",
516
+ "opera": "25",
517
+ "edge": "12",
518
+ "firefox": "25",
519
+ "safari": "7.1",
520
+ "node": "0.12",
521
+ "deno": "1",
522
+ "ios": "8",
523
+ "samsung": "3",
524
+ "rhino": "1.7.13",
525
+ "opera_mobile": "25",
526
+ "electron": "0.20"
527
+ },
528
+ "es6.math.clz32": {
529
+ "chrome": "38",
530
+ "opera": "25",
531
+ "edge": "12",
532
+ "firefox": "31",
533
+ "safari": "9",
534
+ "node": "0.12",
535
+ "deno": "1",
536
+ "ios": "9",
537
+ "samsung": "3",
538
+ "rhino": "1.7.13",
539
+ "opera_mobile": "25",
540
+ "electron": "0.20"
541
+ },
542
+ "es6.math.cosh": {
543
+ "chrome": "38",
544
+ "opera": "25",
545
+ "edge": "12",
546
+ "firefox": "25",
547
+ "safari": "7.1",
548
+ "node": "0.12",
549
+ "deno": "1",
550
+ "ios": "8",
551
+ "samsung": "3",
552
+ "rhino": "1.7.13",
553
+ "opera_mobile": "25",
554
+ "electron": "0.20"
555
+ },
556
+ "es6.math.expm1": {
557
+ "chrome": "38",
558
+ "opera": "25",
559
+ "edge": "12",
560
+ "firefox": "25",
561
+ "safari": "7.1",
562
+ "node": "0.12",
563
+ "deno": "1",
564
+ "ios": "8",
565
+ "samsung": "3",
566
+ "rhino": "1.7.13",
567
+ "opera_mobile": "25",
568
+ "electron": "0.20"
569
+ },
570
+ "es6.math.fround": {
571
+ "chrome": "38",
572
+ "opera": "25",
573
+ "edge": "12",
574
+ "firefox": "26",
575
+ "safari": "7.1",
576
+ "node": "0.12",
577
+ "deno": "1",
578
+ "ios": "8",
579
+ "samsung": "3",
580
+ "rhino": "1.7.13",
581
+ "opera_mobile": "25",
582
+ "electron": "0.20"
583
+ },
584
+ "es6.math.hypot": {
585
+ "chrome": "38",
586
+ "opera": "25",
587
+ "edge": "12",
588
+ "firefox": "27",
589
+ "safari": "7.1",
590
+ "node": "0.12",
591
+ "deno": "1",
592
+ "ios": "8",
593
+ "samsung": "3",
594
+ "rhino": "1.7.13",
595
+ "opera_mobile": "25",
596
+ "electron": "0.20"
597
+ },
598
+ "es6.math.imul": {
599
+ "chrome": "30",
600
+ "opera": "17",
601
+ "edge": "12",
602
+ "firefox": "23",
603
+ "safari": "7",
604
+ "node": "0.12",
605
+ "deno": "1",
606
+ "android": "4.4",
607
+ "ios": "7",
608
+ "samsung": "2",
609
+ "rhino": "1.7.13",
610
+ "opera_mobile": "18",
611
+ "electron": "0.20"
612
+ },
613
+ "es6.math.log1p": {
614
+ "chrome": "38",
615
+ "opera": "25",
616
+ "edge": "12",
617
+ "firefox": "25",
618
+ "safari": "7.1",
619
+ "node": "0.12",
620
+ "deno": "1",
621
+ "ios": "8",
622
+ "samsung": "3",
623
+ "rhino": "1.7.13",
624
+ "opera_mobile": "25",
625
+ "electron": "0.20"
626
+ },
627
+ "es6.math.log10": {
628
+ "chrome": "38",
629
+ "opera": "25",
630
+ "edge": "12",
631
+ "firefox": "25",
632
+ "safari": "7.1",
633
+ "node": "0.12",
634
+ "deno": "1",
635
+ "ios": "8",
636
+ "samsung": "3",
637
+ "rhino": "1.7.13",
638
+ "opera_mobile": "25",
639
+ "electron": "0.20"
640
+ },
641
+ "es6.math.log2": {
642
+ "chrome": "38",
643
+ "opera": "25",
644
+ "edge": "12",
645
+ "firefox": "25",
646
+ "safari": "7.1",
647
+ "node": "0.12",
648
+ "deno": "1",
649
+ "ios": "8",
650
+ "samsung": "3",
651
+ "rhino": "1.7.13",
652
+ "opera_mobile": "25",
653
+ "electron": "0.20"
654
+ },
655
+ "es6.math.sign": {
656
+ "chrome": "38",
657
+ "opera": "25",
658
+ "edge": "12",
659
+ "firefox": "25",
660
+ "safari": "9",
661
+ "node": "0.12",
662
+ "deno": "1",
663
+ "ios": "9",
664
+ "samsung": "3",
665
+ "rhino": "1.7.13",
666
+ "opera_mobile": "25",
667
+ "electron": "0.20"
668
+ },
669
+ "es6.math.sinh": {
670
+ "chrome": "38",
671
+ "opera": "25",
672
+ "edge": "12",
673
+ "firefox": "25",
674
+ "safari": "7.1",
675
+ "node": "0.12",
676
+ "deno": "1",
677
+ "ios": "8",
678
+ "samsung": "3",
679
+ "rhino": "1.7.13",
680
+ "opera_mobile": "25",
681
+ "electron": "0.20"
682
+ },
683
+ "es6.math.tanh": {
684
+ "chrome": "38",
685
+ "opera": "25",
686
+ "edge": "12",
687
+ "firefox": "25",
688
+ "safari": "7.1",
689
+ "node": "0.12",
690
+ "deno": "1",
691
+ "ios": "8",
692
+ "samsung": "3",
693
+ "rhino": "1.7.13",
694
+ "opera_mobile": "25",
695
+ "electron": "0.20"
696
+ },
697
+ "es6.math.trunc": {
698
+ "chrome": "38",
699
+ "opera": "25",
700
+ "edge": "12",
701
+ "firefox": "25",
702
+ "safari": "7.1",
703
+ "node": "0.12",
704
+ "deno": "1",
705
+ "ios": "8",
706
+ "samsung": "3",
707
+ "rhino": "1.7.13",
708
+ "opera_mobile": "25",
709
+ "electron": "0.20"
710
+ },
711
+ "es6.number.constructor": {
712
+ "chrome": "41",
713
+ "opera": "28",
714
+ "edge": "12",
715
+ "firefox": "36",
716
+ "safari": "9",
717
+ "node": "4",
718
+ "deno": "1",
719
+ "ios": "9",
720
+ "samsung": "3.4",
721
+ "rhino": "1.7.13",
722
+ "opera_mobile": "28",
723
+ "electron": "0.21"
724
+ },
725
+ "es6.number.epsilon": {
726
+ "chrome": "34",
727
+ "opera": "21",
728
+ "edge": "12",
729
+ "firefox": "25",
730
+ "safari": "9",
731
+ "node": "0.12",
732
+ "deno": "1",
733
+ "ios": "9",
734
+ "samsung": "2",
735
+ "rhino": "1.7.14",
736
+ "opera_mobile": "21",
737
+ "electron": "0.20"
738
+ },
739
+ "es6.number.is-finite": {
740
+ "chrome": "19",
741
+ "opera": "15",
742
+ "edge": "12",
743
+ "firefox": "16",
744
+ "safari": "9",
745
+ "node": "0.8",
746
+ "deno": "1",
747
+ "android": "4.1",
748
+ "ios": "9",
749
+ "samsung": "1.5",
750
+ "rhino": "1.7.13",
751
+ "opera_mobile": "14",
752
+ "electron": "0.20"
753
+ },
754
+ "es6.number.is-integer": {
755
+ "chrome": "34",
756
+ "opera": "21",
757
+ "edge": "12",
758
+ "firefox": "16",
759
+ "safari": "9",
760
+ "node": "0.12",
761
+ "deno": "1",
762
+ "ios": "9",
763
+ "samsung": "2",
764
+ "rhino": "1.7.13",
765
+ "opera_mobile": "21",
766
+ "electron": "0.20"
767
+ },
768
+ "es6.number.is-nan": {
769
+ "chrome": "19",
770
+ "opera": "15",
771
+ "edge": "12",
772
+ "firefox": "15",
773
+ "safari": "9",
774
+ "node": "0.8",
775
+ "deno": "1",
776
+ "android": "4.1",
777
+ "ios": "9",
778
+ "samsung": "1.5",
779
+ "rhino": "1.7.13",
780
+ "opera_mobile": "14",
781
+ "electron": "0.20"
782
+ },
783
+ "es6.number.is-safe-integer": {
784
+ "chrome": "34",
785
+ "opera": "21",
786
+ "edge": "12",
787
+ "firefox": "32",
788
+ "safari": "9",
789
+ "node": "0.12",
790
+ "deno": "1",
791
+ "ios": "9",
792
+ "samsung": "2",
793
+ "rhino": "1.7.13",
794
+ "opera_mobile": "21",
795
+ "electron": "0.20"
796
+ },
797
+ "es6.number.max-safe-integer": {
798
+ "chrome": "34",
799
+ "opera": "21",
800
+ "edge": "12",
801
+ "firefox": "31",
802
+ "safari": "9",
803
+ "node": "0.12",
804
+ "deno": "1",
805
+ "ios": "9",
806
+ "samsung": "2",
807
+ "rhino": "1.7.13",
808
+ "opera_mobile": "21",
809
+ "electron": "0.20"
810
+ },
811
+ "es6.number.min-safe-integer": {
812
+ "chrome": "34",
813
+ "opera": "21",
814
+ "edge": "12",
815
+ "firefox": "31",
816
+ "safari": "9",
817
+ "node": "0.12",
818
+ "deno": "1",
819
+ "ios": "9",
820
+ "samsung": "2",
821
+ "rhino": "1.7.13",
822
+ "opera_mobile": "21",
823
+ "electron": "0.20"
824
+ },
825
+ "es6.number.parse-float": {
826
+ "chrome": "34",
827
+ "opera": "21",
828
+ "edge": "12",
829
+ "firefox": "25",
830
+ "safari": "9",
831
+ "node": "0.12",
832
+ "deno": "1",
833
+ "ios": "9",
834
+ "samsung": "2",
835
+ "rhino": "1.7.14",
836
+ "opera_mobile": "21",
837
+ "electron": "0.20"
838
+ },
839
+ "es6.number.parse-int": {
840
+ "chrome": "34",
841
+ "opera": "21",
842
+ "edge": "12",
843
+ "firefox": "25",
844
+ "safari": "9",
845
+ "node": "0.12",
846
+ "deno": "1",
847
+ "ios": "9",
848
+ "samsung": "2",
849
+ "rhino": "1.7.14",
850
+ "opera_mobile": "21",
851
+ "electron": "0.20"
852
+ },
853
+ "es6.object.assign": {
854
+ "chrome": "49",
855
+ "opera": "36",
856
+ "edge": "13",
857
+ "firefox": "36",
858
+ "safari": "10",
859
+ "node": "6",
860
+ "deno": "1",
861
+ "ios": "10",
862
+ "samsung": "5",
863
+ "rhino": "1.8",
864
+ "opera_mobile": "36",
865
+ "electron": "0.37"
866
+ },
867
+ "es6.object.create": {
868
+ "chrome": "5",
869
+ "opera": "12",
870
+ "edge": "12",
871
+ "firefox": "4",
872
+ "safari": "4",
873
+ "node": "0.4",
874
+ "deno": "1",
875
+ "ie": "9",
876
+ "android": "4",
877
+ "ios": "6",
878
+ "phantom": "1.9",
879
+ "samsung": "1",
880
+ "rhino": "1.7.13",
881
+ "opera_mobile": "12",
882
+ "electron": "0.20"
883
+ },
884
+ "es7.object.define-getter": {
885
+ "chrome": "62",
886
+ "opera": "49",
887
+ "edge": "16",
888
+ "firefox": "48",
889
+ "safari": "9",
890
+ "node": "8.10",
891
+ "deno": "1",
892
+ "ios": "9",
893
+ "samsung": "8",
894
+ "opera_mobile": "46",
895
+ "electron": "3.0"
896
+ },
897
+ "es7.object.define-setter": {
898
+ "chrome": "62",
899
+ "opera": "49",
900
+ "edge": "16",
901
+ "firefox": "48",
902
+ "safari": "9",
903
+ "node": "8.10",
904
+ "deno": "1",
905
+ "ios": "9",
906
+ "samsung": "8",
907
+ "opera_mobile": "46",
908
+ "electron": "3.0"
909
+ },
910
+ "es6.object.define-property": {
911
+ "chrome": "5",
912
+ "opera": "12",
913
+ "edge": "12",
914
+ "firefox": "4",
915
+ "safari": "5.1",
916
+ "node": "0.4",
917
+ "deno": "1",
918
+ "ie": "9",
919
+ "android": "4",
920
+ "ios": "6",
921
+ "phantom": "1.9",
922
+ "samsung": "1",
923
+ "rhino": "1.7.13",
924
+ "opera_mobile": "12",
925
+ "electron": "0.20"
926
+ },
927
+ "es6.object.define-properties": {
928
+ "chrome": "5",
929
+ "opera": "12",
930
+ "edge": "12",
931
+ "firefox": "4",
932
+ "safari": "4",
933
+ "node": "0.4",
934
+ "deno": "1",
935
+ "ie": "9",
936
+ "android": "4",
937
+ "ios": "6",
938
+ "phantom": "1.9",
939
+ "samsung": "1",
940
+ "rhino": "1.7.13",
941
+ "opera_mobile": "12",
942
+ "electron": "0.20"
943
+ },
944
+ "es7.object.entries": {
945
+ "chrome": "54",
946
+ "opera": "41",
947
+ "edge": "14",
948
+ "firefox": "47",
949
+ "safari": "10.1",
950
+ "node": "7",
951
+ "deno": "1",
952
+ "ios": "10.3",
953
+ "samsung": "6",
954
+ "rhino": "1.7.14",
955
+ "opera_mobile": "41",
956
+ "electron": "1.4"
957
+ },
958
+ "es6.object.freeze": {
959
+ "chrome": "44",
960
+ "opera": "31",
961
+ "edge": "12",
962
+ "firefox": "35",
963
+ "safari": "9",
964
+ "node": "4",
965
+ "deno": "1",
966
+ "ios": "9",
967
+ "samsung": "4",
968
+ "rhino": "1.7.13",
969
+ "opera_mobile": "32",
970
+ "electron": "0.30"
971
+ },
972
+ "es6.object.get-own-property-descriptor": {
973
+ "chrome": "44",
974
+ "opera": "31",
975
+ "edge": "12",
976
+ "firefox": "35",
977
+ "safari": "9",
978
+ "node": "4",
979
+ "deno": "1",
980
+ "ios": "9",
981
+ "samsung": "4",
982
+ "rhino": "1.7.13",
983
+ "opera_mobile": "32",
984
+ "electron": "0.30"
985
+ },
986
+ "es7.object.get-own-property-descriptors": {
987
+ "chrome": "54",
988
+ "opera": "41",
989
+ "edge": "15",
990
+ "firefox": "50",
991
+ "safari": "10.1",
992
+ "node": "7",
993
+ "deno": "1",
994
+ "ios": "10.3",
995
+ "samsung": "6",
996
+ "rhino": "1.8",
997
+ "opera_mobile": "41",
998
+ "electron": "1.4"
999
+ },
1000
+ "es6.object.get-own-property-names": {
1001
+ "chrome": "40",
1002
+ "opera": "27",
1003
+ "edge": "12",
1004
+ "firefox": "33",
1005
+ "safari": "9",
1006
+ "node": "4",
1007
+ "deno": "1",
1008
+ "ios": "9",
1009
+ "samsung": "3.4",
1010
+ "rhino": "1.7.13",
1011
+ "opera_mobile": "27",
1012
+ "electron": "0.21"
1013
+ },
1014
+ "es6.object.get-prototype-of": {
1015
+ "chrome": "44",
1016
+ "opera": "31",
1017
+ "edge": "12",
1018
+ "firefox": "35",
1019
+ "safari": "9",
1020
+ "node": "4",
1021
+ "deno": "1",
1022
+ "ios": "9",
1023
+ "samsung": "4",
1024
+ "rhino": "1.7.13",
1025
+ "opera_mobile": "32",
1026
+ "electron": "0.30"
1027
+ },
1028
+ "es7.object.lookup-getter": {
1029
+ "chrome": "62",
1030
+ "opera": "49",
1031
+ "edge": "79",
1032
+ "firefox": "36",
1033
+ "safari": "9",
1034
+ "node": "8.10",
1035
+ "deno": "1",
1036
+ "ios": "9",
1037
+ "samsung": "8",
1038
+ "opera_mobile": "46",
1039
+ "electron": "3.0"
1040
+ },
1041
+ "es7.object.lookup-setter": {
1042
+ "chrome": "62",
1043
+ "opera": "49",
1044
+ "edge": "79",
1045
+ "firefox": "36",
1046
+ "safari": "9",
1047
+ "node": "8.10",
1048
+ "deno": "1",
1049
+ "ios": "9",
1050
+ "samsung": "8",
1051
+ "opera_mobile": "46",
1052
+ "electron": "3.0"
1053
+ },
1054
+ "es6.object.prevent-extensions": {
1055
+ "chrome": "44",
1056
+ "opera": "31",
1057
+ "edge": "12",
1058
+ "firefox": "35",
1059
+ "safari": "9",
1060
+ "node": "4",
1061
+ "deno": "1",
1062
+ "ios": "9",
1063
+ "samsung": "4",
1064
+ "rhino": "1.7.13",
1065
+ "opera_mobile": "32",
1066
+ "electron": "0.30"
1067
+ },
1068
+ "es6.object.to-string": {
1069
+ "chrome": "57",
1070
+ "opera": "44",
1071
+ "edge": "15",
1072
+ "firefox": "51",
1073
+ "safari": "10",
1074
+ "node": "8",
1075
+ "deno": "1",
1076
+ "ios": "10",
1077
+ "samsung": "7",
1078
+ "opera_mobile": "43",
1079
+ "electron": "1.7"
1080
+ },
1081
+ "es6.object.is": {
1082
+ "chrome": "19",
1083
+ "opera": "15",
1084
+ "edge": "12",
1085
+ "firefox": "22",
1086
+ "safari": "9",
1087
+ "node": "0.8",
1088
+ "deno": "1",
1089
+ "android": "4.1",
1090
+ "ios": "9",
1091
+ "samsung": "1.5",
1092
+ "rhino": "1.7.13",
1093
+ "opera_mobile": "14",
1094
+ "electron": "0.20"
1095
+ },
1096
+ "es6.object.is-frozen": {
1097
+ "chrome": "44",
1098
+ "opera": "31",
1099
+ "edge": "12",
1100
+ "firefox": "35",
1101
+ "safari": "9",
1102
+ "node": "4",
1103
+ "deno": "1",
1104
+ "ios": "9",
1105
+ "samsung": "4",
1106
+ "rhino": "1.7.13",
1107
+ "opera_mobile": "32",
1108
+ "electron": "0.30"
1109
+ },
1110
+ "es6.object.is-sealed": {
1111
+ "chrome": "44",
1112
+ "opera": "31",
1113
+ "edge": "12",
1114
+ "firefox": "35",
1115
+ "safari": "9",
1116
+ "node": "4",
1117
+ "deno": "1",
1118
+ "ios": "9",
1119
+ "samsung": "4",
1120
+ "rhino": "1.7.13",
1121
+ "opera_mobile": "32",
1122
+ "electron": "0.30"
1123
+ },
1124
+ "es6.object.is-extensible": {
1125
+ "chrome": "44",
1126
+ "opera": "31",
1127
+ "edge": "12",
1128
+ "firefox": "35",
1129
+ "safari": "9",
1130
+ "node": "4",
1131
+ "deno": "1",
1132
+ "ios": "9",
1133
+ "samsung": "4",
1134
+ "rhino": "1.7.13",
1135
+ "opera_mobile": "32",
1136
+ "electron": "0.30"
1137
+ },
1138
+ "es6.object.keys": {
1139
+ "chrome": "40",
1140
+ "opera": "27",
1141
+ "edge": "12",
1142
+ "firefox": "35",
1143
+ "safari": "9",
1144
+ "node": "4",
1145
+ "deno": "1",
1146
+ "ios": "9",
1147
+ "samsung": "3.4",
1148
+ "rhino": "1.7.13",
1149
+ "opera_mobile": "27",
1150
+ "electron": "0.21"
1151
+ },
1152
+ "es6.object.seal": {
1153
+ "chrome": "44",
1154
+ "opera": "31",
1155
+ "edge": "12",
1156
+ "firefox": "35",
1157
+ "safari": "9",
1158
+ "node": "4",
1159
+ "deno": "1",
1160
+ "ios": "9",
1161
+ "samsung": "4",
1162
+ "rhino": "1.7.13",
1163
+ "opera_mobile": "32",
1164
+ "electron": "0.30"
1165
+ },
1166
+ "es6.object.set-prototype-of": {
1167
+ "chrome": "34",
1168
+ "opera": "21",
1169
+ "edge": "12",
1170
+ "firefox": "31",
1171
+ "safari": "9",
1172
+ "node": "0.12",
1173
+ "deno": "1",
1174
+ "ie": "11",
1175
+ "ios": "9",
1176
+ "samsung": "2",
1177
+ "rhino": "1.7.13",
1178
+ "opera_mobile": "21",
1179
+ "electron": "0.20"
1180
+ },
1181
+ "es7.object.values": {
1182
+ "chrome": "54",
1183
+ "opera": "41",
1184
+ "edge": "14",
1185
+ "firefox": "47",
1186
+ "safari": "10.1",
1187
+ "node": "7",
1188
+ "deno": "1",
1189
+ "ios": "10.3",
1190
+ "samsung": "6",
1191
+ "rhino": "1.7.14",
1192
+ "opera_mobile": "41",
1193
+ "electron": "1.4"
1194
+ },
1195
+ "es6.promise": {
1196
+ "chrome": "51",
1197
+ "opera": "38",
1198
+ "edge": "14",
1199
+ "firefox": "45",
1200
+ "safari": "10",
1201
+ "node": "6.5",
1202
+ "deno": "1",
1203
+ "ios": "10",
1204
+ "samsung": "5",
1205
+ "rhino": "1.7.15",
1206
+ "opera_mobile": "41",
1207
+ "electron": "1.2"
1208
+ },
1209
+ "es7.promise.finally": {
1210
+ "chrome": "63",
1211
+ "opera": "50",
1212
+ "edge": "18",
1213
+ "firefox": "58",
1214
+ "safari": "11.1",
1215
+ "node": "10",
1216
+ "deno": "1",
1217
+ "ios": "11.3",
1218
+ "samsung": "8",
1219
+ "rhino": "1.7.15",
1220
+ "opera_mobile": "46",
1221
+ "electron": "3.0"
1222
+ },
1223
+ "es6.reflect.apply": {
1224
+ "chrome": "49",
1225
+ "opera": "36",
1226
+ "edge": "12",
1227
+ "firefox": "42",
1228
+ "safari": "10",
1229
+ "node": "6",
1230
+ "deno": "1",
1231
+ "ios": "10",
1232
+ "samsung": "5",
1233
+ "rhino": "1.8",
1234
+ "opera_mobile": "36",
1235
+ "electron": "0.37"
1236
+ },
1237
+ "es6.reflect.construct": {
1238
+ "chrome": "49",
1239
+ "opera": "36",
1240
+ "edge": "13",
1241
+ "firefox": "49",
1242
+ "safari": "10",
1243
+ "node": "6",
1244
+ "deno": "1",
1245
+ "ios": "10",
1246
+ "samsung": "5",
1247
+ "opera_mobile": "36",
1248
+ "electron": "0.37"
1249
+ },
1250
+ "es6.reflect.define-property": {
1251
+ "chrome": "49",
1252
+ "opera": "36",
1253
+ "edge": "13",
1254
+ "firefox": "42",
1255
+ "safari": "10",
1256
+ "node": "6",
1257
+ "deno": "1",
1258
+ "ios": "10",
1259
+ "samsung": "5",
1260
+ "rhino": "1.8",
1261
+ "opera_mobile": "36",
1262
+ "electron": "0.37"
1263
+ },
1264
+ "es6.reflect.delete-property": {
1265
+ "chrome": "49",
1266
+ "opera": "36",
1267
+ "edge": "12",
1268
+ "firefox": "42",
1269
+ "safari": "10",
1270
+ "node": "6",
1271
+ "deno": "1",
1272
+ "ios": "10",
1273
+ "samsung": "5",
1274
+ "rhino": "1.8",
1275
+ "opera_mobile": "36",
1276
+ "electron": "0.37"
1277
+ },
1278
+ "es6.reflect.get": {
1279
+ "chrome": "49",
1280
+ "opera": "36",
1281
+ "edge": "12",
1282
+ "firefox": "42",
1283
+ "safari": "10",
1284
+ "node": "6",
1285
+ "deno": "1",
1286
+ "ios": "10",
1287
+ "samsung": "5",
1288
+ "rhino": "1.8",
1289
+ "opera_mobile": "36",
1290
+ "electron": "0.37"
1291
+ },
1292
+ "es6.reflect.get-own-property-descriptor": {
1293
+ "chrome": "49",
1294
+ "opera": "36",
1295
+ "edge": "12",
1296
+ "firefox": "42",
1297
+ "safari": "10",
1298
+ "node": "6",
1299
+ "deno": "1",
1300
+ "ios": "10",
1301
+ "samsung": "5",
1302
+ "rhino": "1.8",
1303
+ "opera_mobile": "36",
1304
+ "electron": "0.37"
1305
+ },
1306
+ "es6.reflect.get-prototype-of": {
1307
+ "chrome": "49",
1308
+ "opera": "36",
1309
+ "edge": "12",
1310
+ "firefox": "42",
1311
+ "safari": "10",
1312
+ "node": "6",
1313
+ "deno": "1",
1314
+ "ios": "10",
1315
+ "samsung": "5",
1316
+ "rhino": "1.8",
1317
+ "opera_mobile": "36",
1318
+ "electron": "0.37"
1319
+ },
1320
+ "es6.reflect.has": {
1321
+ "chrome": "49",
1322
+ "opera": "36",
1323
+ "edge": "12",
1324
+ "firefox": "42",
1325
+ "safari": "10",
1326
+ "node": "6",
1327
+ "deno": "1",
1328
+ "ios": "10",
1329
+ "samsung": "5",
1330
+ "rhino": "1.8",
1331
+ "opera_mobile": "36",
1332
+ "electron": "0.37"
1333
+ },
1334
+ "es6.reflect.is-extensible": {
1335
+ "chrome": "49",
1336
+ "opera": "36",
1337
+ "edge": "12",
1338
+ "firefox": "42",
1339
+ "safari": "10",
1340
+ "node": "6",
1341
+ "deno": "1",
1342
+ "ios": "10",
1343
+ "samsung": "5",
1344
+ "rhino": "1.8",
1345
+ "opera_mobile": "36",
1346
+ "electron": "0.37"
1347
+ },
1348
+ "es6.reflect.own-keys": {
1349
+ "chrome": "49",
1350
+ "opera": "36",
1351
+ "edge": "12",
1352
+ "firefox": "42",
1353
+ "safari": "10",
1354
+ "node": "6",
1355
+ "deno": "1",
1356
+ "ios": "10",
1357
+ "samsung": "5",
1358
+ "rhino": "1.8",
1359
+ "opera_mobile": "36",
1360
+ "electron": "0.37"
1361
+ },
1362
+ "es6.reflect.prevent-extensions": {
1363
+ "chrome": "49",
1364
+ "opera": "36",
1365
+ "edge": "12",
1366
+ "firefox": "42",
1367
+ "safari": "10",
1368
+ "node": "6",
1369
+ "deno": "1",
1370
+ "ios": "10",
1371
+ "samsung": "5",
1372
+ "rhino": "1.8",
1373
+ "opera_mobile": "36",
1374
+ "electron": "0.37"
1375
+ },
1376
+ "es6.reflect.set": {
1377
+ "chrome": "49",
1378
+ "opera": "36",
1379
+ "edge": "12",
1380
+ "firefox": "42",
1381
+ "safari": "10",
1382
+ "node": "6",
1383
+ "deno": "1",
1384
+ "ios": "10",
1385
+ "samsung": "5",
1386
+ "rhino": "1.8",
1387
+ "opera_mobile": "36",
1388
+ "electron": "0.37"
1389
+ },
1390
+ "es6.reflect.set-prototype-of": {
1391
+ "chrome": "49",
1392
+ "opera": "36",
1393
+ "edge": "12",
1394
+ "firefox": "42",
1395
+ "safari": "10",
1396
+ "node": "6",
1397
+ "deno": "1",
1398
+ "ios": "10",
1399
+ "samsung": "5",
1400
+ "rhino": "1.8",
1401
+ "opera_mobile": "36",
1402
+ "electron": "0.37"
1403
+ },
1404
+ "es6.regexp.constructor": {
1405
+ "chrome": "50",
1406
+ "opera": "37",
1407
+ "edge": "79",
1408
+ "firefox": "40",
1409
+ "safari": "10",
1410
+ "node": "6",
1411
+ "deno": "1",
1412
+ "ios": "10",
1413
+ "samsung": "5",
1414
+ "opera_mobile": "37",
1415
+ "electron": "1.1"
1416
+ },
1417
+ "es6.regexp.flags": {
1418
+ "chrome": "49",
1419
+ "opera": "36",
1420
+ "edge": "79",
1421
+ "firefox": "37",
1422
+ "safari": "9",
1423
+ "node": "6",
1424
+ "deno": "1",
1425
+ "ios": "9",
1426
+ "samsung": "5",
1427
+ "rhino": "1.7.15",
1428
+ "opera_mobile": "36",
1429
+ "electron": "0.37"
1430
+ },
1431
+ "es6.regexp.match": {
1432
+ "chrome": "50",
1433
+ "opera": "37",
1434
+ "edge": "79",
1435
+ "firefox": "49",
1436
+ "safari": "10",
1437
+ "node": "6",
1438
+ "deno": "1",
1439
+ "ios": "10",
1440
+ "samsung": "5",
1441
+ "rhino": "1.7.13",
1442
+ "opera_mobile": "37",
1443
+ "electron": "1.1"
1444
+ },
1445
+ "es6.regexp.replace": {
1446
+ "chrome": "50",
1447
+ "opera": "37",
1448
+ "edge": "79",
1449
+ "firefox": "49",
1450
+ "safari": "10",
1451
+ "node": "6",
1452
+ "deno": "1",
1453
+ "ios": "10",
1454
+ "samsung": "5",
1455
+ "opera_mobile": "37",
1456
+ "electron": "1.1"
1457
+ },
1458
+ "es6.regexp.split": {
1459
+ "chrome": "50",
1460
+ "opera": "37",
1461
+ "edge": "79",
1462
+ "firefox": "49",
1463
+ "safari": "10",
1464
+ "node": "6",
1465
+ "deno": "1",
1466
+ "ios": "10",
1467
+ "samsung": "5",
1468
+ "opera_mobile": "37",
1469
+ "electron": "1.1"
1470
+ },
1471
+ "es6.regexp.search": {
1472
+ "chrome": "50",
1473
+ "opera": "37",
1474
+ "edge": "79",
1475
+ "firefox": "49",
1476
+ "safari": "10",
1477
+ "node": "6",
1478
+ "deno": "1",
1479
+ "ios": "10",
1480
+ "samsung": "5",
1481
+ "rhino": "1.7.13",
1482
+ "opera_mobile": "37",
1483
+ "electron": "1.1"
1484
+ },
1485
+ "es6.regexp.to-string": {
1486
+ "chrome": "50",
1487
+ "opera": "37",
1488
+ "edge": "79",
1489
+ "firefox": "39",
1490
+ "safari": "10",
1491
+ "node": "6",
1492
+ "deno": "1",
1493
+ "ios": "10",
1494
+ "samsung": "5",
1495
+ "rhino": "1.7.15",
1496
+ "opera_mobile": "37",
1497
+ "electron": "1.1"
1498
+ },
1499
+ "es6.set": {
1500
+ "chrome": "51",
1501
+ "opera": "38",
1502
+ "edge": "15",
1503
+ "firefox": "53",
1504
+ "safari": "10",
1505
+ "node": "6.5",
1506
+ "deno": "1",
1507
+ "ios": "10",
1508
+ "samsung": "5",
1509
+ "opera_mobile": "41",
1510
+ "electron": "1.2"
1511
+ },
1512
+ "es6.symbol": {
1513
+ "chrome": "51",
1514
+ "opera": "38",
1515
+ "edge": "79",
1516
+ "firefox": "51",
1517
+ "safari": "10",
1518
+ "node": "6.5",
1519
+ "deno": "1",
1520
+ "ios": "10",
1521
+ "samsung": "5",
1522
+ "opera_mobile": "41",
1523
+ "electron": "1.2"
1524
+ },
1525
+ "es7.symbol.async-iterator": {
1526
+ "chrome": "63",
1527
+ "opera": "50",
1528
+ "edge": "79",
1529
+ "firefox": "57",
1530
+ "safari": "12",
1531
+ "node": "10",
1532
+ "deno": "1",
1533
+ "ios": "12",
1534
+ "samsung": "8",
1535
+ "opera_mobile": "46",
1536
+ "electron": "3.0"
1537
+ },
1538
+ "es6.string.anchor": {
1539
+ "chrome": "5",
1540
+ "opera": "15",
1541
+ "edge": "12",
1542
+ "firefox": "17",
1543
+ "safari": "6",
1544
+ "node": "0.4",
1545
+ "deno": "1",
1546
+ "android": "4",
1547
+ "ios": "7",
1548
+ "phantom": "1.9",
1549
+ "samsung": "1",
1550
+ "rhino": "1.7.14",
1551
+ "opera_mobile": "14",
1552
+ "electron": "0.20"
1553
+ },
1554
+ "es6.string.big": {
1555
+ "chrome": "5",
1556
+ "opera": "15",
1557
+ "edge": "12",
1558
+ "firefox": "17",
1559
+ "safari": "6",
1560
+ "node": "0.4",
1561
+ "deno": "1",
1562
+ "android": "4",
1563
+ "ios": "7",
1564
+ "phantom": "1.9",
1565
+ "samsung": "1",
1566
+ "rhino": "1.7.14",
1567
+ "opera_mobile": "14",
1568
+ "electron": "0.20"
1569
+ },
1570
+ "es6.string.blink": {
1571
+ "chrome": "5",
1572
+ "opera": "15",
1573
+ "edge": "12",
1574
+ "firefox": "17",
1575
+ "safari": "6",
1576
+ "node": "0.4",
1577
+ "deno": "1",
1578
+ "android": "4",
1579
+ "ios": "7",
1580
+ "phantom": "1.9",
1581
+ "samsung": "1",
1582
+ "rhino": "1.7.14",
1583
+ "opera_mobile": "14",
1584
+ "electron": "0.20"
1585
+ },
1586
+ "es6.string.bold": {
1587
+ "chrome": "5",
1588
+ "opera": "15",
1589
+ "edge": "12",
1590
+ "firefox": "17",
1591
+ "safari": "6",
1592
+ "node": "0.4",
1593
+ "deno": "1",
1594
+ "android": "4",
1595
+ "ios": "7",
1596
+ "phantom": "1.9",
1597
+ "samsung": "1",
1598
+ "rhino": "1.7.14",
1599
+ "opera_mobile": "14",
1600
+ "electron": "0.20"
1601
+ },
1602
+ "es6.string.code-point-at": {
1603
+ "chrome": "41",
1604
+ "opera": "28",
1605
+ "edge": "12",
1606
+ "firefox": "29",
1607
+ "safari": "9",
1608
+ "node": "4",
1609
+ "deno": "1",
1610
+ "ios": "9",
1611
+ "samsung": "3.4",
1612
+ "rhino": "1.7.13",
1613
+ "opera_mobile": "28",
1614
+ "electron": "0.21"
1615
+ },
1616
+ "es6.string.ends-with": {
1617
+ "chrome": "41",
1618
+ "opera": "28",
1619
+ "edge": "12",
1620
+ "firefox": "29",
1621
+ "safari": "9",
1622
+ "node": "4",
1623
+ "deno": "1",
1624
+ "ios": "9",
1625
+ "samsung": "3.4",
1626
+ "rhino": "1.7.13",
1627
+ "opera_mobile": "28",
1628
+ "electron": "0.21"
1629
+ },
1630
+ "es6.string.fixed": {
1631
+ "chrome": "5",
1632
+ "opera": "15",
1633
+ "edge": "12",
1634
+ "firefox": "17",
1635
+ "safari": "6",
1636
+ "node": "0.4",
1637
+ "deno": "1",
1638
+ "android": "4",
1639
+ "ios": "7",
1640
+ "phantom": "1.9",
1641
+ "samsung": "1",
1642
+ "rhino": "1.7.14",
1643
+ "opera_mobile": "14",
1644
+ "electron": "0.20"
1645
+ },
1646
+ "es6.string.fontcolor": {
1647
+ "chrome": "5",
1648
+ "opera": "15",
1649
+ "edge": "12",
1650
+ "firefox": "17",
1651
+ "safari": "6",
1652
+ "node": "0.4",
1653
+ "deno": "1",
1654
+ "android": "4",
1655
+ "ios": "7",
1656
+ "phantom": "1.9",
1657
+ "samsung": "1",
1658
+ "rhino": "1.7.14",
1659
+ "opera_mobile": "14",
1660
+ "electron": "0.20"
1661
+ },
1662
+ "es6.string.fontsize": {
1663
+ "chrome": "5",
1664
+ "opera": "15",
1665
+ "edge": "12",
1666
+ "firefox": "17",
1667
+ "safari": "6",
1668
+ "node": "0.4",
1669
+ "deno": "1",
1670
+ "android": "4",
1671
+ "ios": "7",
1672
+ "phantom": "1.9",
1673
+ "samsung": "1",
1674
+ "rhino": "1.7.14",
1675
+ "opera_mobile": "14",
1676
+ "electron": "0.20"
1677
+ },
1678
+ "es6.string.from-code-point": {
1679
+ "chrome": "41",
1680
+ "opera": "28",
1681
+ "edge": "12",
1682
+ "firefox": "29",
1683
+ "safari": "9",
1684
+ "node": "4",
1685
+ "deno": "1",
1686
+ "ios": "9",
1687
+ "samsung": "3.4",
1688
+ "rhino": "1.7.13",
1689
+ "opera_mobile": "28",
1690
+ "electron": "0.21"
1691
+ },
1692
+ "es6.string.includes": {
1693
+ "chrome": "41",
1694
+ "opera": "28",
1695
+ "edge": "12",
1696
+ "firefox": "40",
1697
+ "safari": "9",
1698
+ "node": "4",
1699
+ "deno": "1",
1700
+ "ios": "9",
1701
+ "samsung": "3.4",
1702
+ "rhino": "1.7.13",
1703
+ "opera_mobile": "28",
1704
+ "electron": "0.21"
1705
+ },
1706
+ "es6.string.italics": {
1707
+ "chrome": "5",
1708
+ "opera": "15",
1709
+ "edge": "12",
1710
+ "firefox": "17",
1711
+ "safari": "6",
1712
+ "node": "0.4",
1713
+ "deno": "1",
1714
+ "android": "4",
1715
+ "ios": "7",
1716
+ "phantom": "1.9",
1717
+ "samsung": "1",
1718
+ "rhino": "1.7.14",
1719
+ "opera_mobile": "14",
1720
+ "electron": "0.20"
1721
+ },
1722
+ "es6.string.iterator": {
1723
+ "chrome": "38",
1724
+ "opera": "25",
1725
+ "edge": "12",
1726
+ "firefox": "36",
1727
+ "safari": "9",
1728
+ "node": "0.12",
1729
+ "deno": "1",
1730
+ "ios": "9",
1731
+ "samsung": "3",
1732
+ "rhino": "1.7.13",
1733
+ "opera_mobile": "25",
1734
+ "electron": "0.20"
1735
+ },
1736
+ "es6.string.link": {
1737
+ "chrome": "5",
1738
+ "opera": "15",
1739
+ "edge": "12",
1740
+ "firefox": "17",
1741
+ "safari": "6",
1742
+ "node": "0.4",
1743
+ "deno": "1",
1744
+ "android": "4",
1745
+ "ios": "7",
1746
+ "phantom": "1.9",
1747
+ "samsung": "1",
1748
+ "rhino": "1.7.14",
1749
+ "opera_mobile": "14",
1750
+ "electron": "0.20"
1751
+ },
1752
+ "es7.string.pad-start": {
1753
+ "chrome": "57",
1754
+ "opera": "44",
1755
+ "edge": "15",
1756
+ "firefox": "48",
1757
+ "safari": "10",
1758
+ "node": "8",
1759
+ "deno": "1",
1760
+ "ios": "10",
1761
+ "samsung": "7",
1762
+ "rhino": "1.7.13",
1763
+ "opera_mobile": "43",
1764
+ "electron": "1.7"
1765
+ },
1766
+ "es7.string.pad-end": {
1767
+ "chrome": "57",
1768
+ "opera": "44",
1769
+ "edge": "15",
1770
+ "firefox": "48",
1771
+ "safari": "10",
1772
+ "node": "8",
1773
+ "deno": "1",
1774
+ "ios": "10",
1775
+ "samsung": "7",
1776
+ "rhino": "1.7.13",
1777
+ "opera_mobile": "43",
1778
+ "electron": "1.7"
1779
+ },
1780
+ "es6.string.raw": {
1781
+ "chrome": "41",
1782
+ "opera": "28",
1783
+ "edge": "12",
1784
+ "firefox": "34",
1785
+ "safari": "9",
1786
+ "node": "4",
1787
+ "deno": "1",
1788
+ "ios": "9",
1789
+ "samsung": "3.4",
1790
+ "rhino": "1.7.14",
1791
+ "opera_mobile": "28",
1792
+ "electron": "0.21"
1793
+ },
1794
+ "es6.string.repeat": {
1795
+ "chrome": "41",
1796
+ "opera": "28",
1797
+ "edge": "12",
1798
+ "firefox": "24",
1799
+ "safari": "9",
1800
+ "node": "4",
1801
+ "deno": "1",
1802
+ "ios": "9",
1803
+ "samsung": "3.4",
1804
+ "rhino": "1.7.13",
1805
+ "opera_mobile": "28",
1806
+ "electron": "0.21"
1807
+ },
1808
+ "es6.string.small": {
1809
+ "chrome": "5",
1810
+ "opera": "15",
1811
+ "edge": "12",
1812
+ "firefox": "17",
1813
+ "safari": "6",
1814
+ "node": "0.4",
1815
+ "deno": "1",
1816
+ "android": "4",
1817
+ "ios": "7",
1818
+ "phantom": "1.9",
1819
+ "samsung": "1",
1820
+ "rhino": "1.7.14",
1821
+ "opera_mobile": "14",
1822
+ "electron": "0.20"
1823
+ },
1824
+ "es6.string.starts-with": {
1825
+ "chrome": "41",
1826
+ "opera": "28",
1827
+ "edge": "12",
1828
+ "firefox": "29",
1829
+ "safari": "9",
1830
+ "node": "4",
1831
+ "deno": "1",
1832
+ "ios": "9",
1833
+ "samsung": "3.4",
1834
+ "rhino": "1.7.13",
1835
+ "opera_mobile": "28",
1836
+ "electron": "0.21"
1837
+ },
1838
+ "es6.string.strike": {
1839
+ "chrome": "5",
1840
+ "opera": "15",
1841
+ "edge": "12",
1842
+ "firefox": "17",
1843
+ "safari": "6",
1844
+ "node": "0.4",
1845
+ "deno": "1",
1846
+ "android": "4",
1847
+ "ios": "7",
1848
+ "phantom": "1.9",
1849
+ "samsung": "1",
1850
+ "rhino": "1.7.14",
1851
+ "opera_mobile": "14",
1852
+ "electron": "0.20"
1853
+ },
1854
+ "es6.string.sub": {
1855
+ "chrome": "5",
1856
+ "opera": "15",
1857
+ "edge": "12",
1858
+ "firefox": "17",
1859
+ "safari": "6",
1860
+ "node": "0.4",
1861
+ "deno": "1",
1862
+ "android": "4",
1863
+ "ios": "7",
1864
+ "phantom": "1.9",
1865
+ "samsung": "1",
1866
+ "rhino": "1.7.14",
1867
+ "opera_mobile": "14",
1868
+ "electron": "0.20"
1869
+ },
1870
+ "es6.string.sup": {
1871
+ "chrome": "5",
1872
+ "opera": "15",
1873
+ "edge": "12",
1874
+ "firefox": "17",
1875
+ "safari": "6",
1876
+ "node": "0.4",
1877
+ "deno": "1",
1878
+ "android": "4",
1879
+ "ios": "7",
1880
+ "phantom": "1.9",
1881
+ "samsung": "1",
1882
+ "rhino": "1.7.14",
1883
+ "opera_mobile": "14",
1884
+ "electron": "0.20"
1885
+ },
1886
+ "es6.string.trim": {
1887
+ "chrome": "5",
1888
+ "opera": "10.50",
1889
+ "edge": "12",
1890
+ "firefox": "3.5",
1891
+ "safari": "4",
1892
+ "node": "0.4",
1893
+ "deno": "1",
1894
+ "ie": "9",
1895
+ "android": "4",
1896
+ "ios": "6",
1897
+ "phantom": "1.9",
1898
+ "samsung": "1",
1899
+ "rhino": "1.7.13",
1900
+ "opera_mobile": "10.1",
1901
+ "electron": "0.20"
1902
+ },
1903
+ "es7.string.trim-left": {
1904
+ "chrome": "66",
1905
+ "opera": "53",
1906
+ "edge": "79",
1907
+ "firefox": "61",
1908
+ "safari": "12",
1909
+ "node": "10",
1910
+ "deno": "1",
1911
+ "ios": "12",
1912
+ "samsung": "9",
1913
+ "rhino": "1.7.13",
1914
+ "opera_mobile": "47",
1915
+ "electron": "3.0"
1916
+ },
1917
+ "es7.string.trim-right": {
1918
+ "chrome": "66",
1919
+ "opera": "53",
1920
+ "edge": "79",
1921
+ "firefox": "61",
1922
+ "safari": "12",
1923
+ "node": "10",
1924
+ "deno": "1",
1925
+ "ios": "12",
1926
+ "samsung": "9",
1927
+ "rhino": "1.7.13",
1928
+ "opera_mobile": "47",
1929
+ "electron": "3.0"
1930
+ },
1931
+ "es6.typed.array-buffer": {
1932
+ "chrome": "51",
1933
+ "opera": "38",
1934
+ "edge": "13",
1935
+ "firefox": "48",
1936
+ "safari": "10",
1937
+ "node": "6.5",
1938
+ "deno": "1",
1939
+ "ios": "10",
1940
+ "samsung": "5",
1941
+ "opera_mobile": "41",
1942
+ "electron": "1.2"
1943
+ },
1944
+ "es6.typed.data-view": {
1945
+ "chrome": "5",
1946
+ "opera": "12",
1947
+ "edge": "12",
1948
+ "firefox": "15",
1949
+ "safari": "5.1",
1950
+ "node": "0.4",
1951
+ "deno": "1",
1952
+ "ie": "10",
1953
+ "android": "4",
1954
+ "ios": "6",
1955
+ "phantom": "1.9",
1956
+ "samsung": "1",
1957
+ "rhino": "1.7.13",
1958
+ "opera_mobile": "12",
1959
+ "electron": "0.20"
1960
+ },
1961
+ "es6.typed.int8-array": {
1962
+ "chrome": "51",
1963
+ "opera": "38",
1964
+ "edge": "13",
1965
+ "firefox": "48",
1966
+ "safari": "10",
1967
+ "node": "6.5",
1968
+ "deno": "1",
1969
+ "ios": "10",
1970
+ "samsung": "5",
1971
+ "opera_mobile": "41",
1972
+ "electron": "1.2"
1973
+ },
1974
+ "es6.typed.uint8-array": {
1975
+ "chrome": "51",
1976
+ "opera": "38",
1977
+ "edge": "13",
1978
+ "firefox": "48",
1979
+ "safari": "10",
1980
+ "node": "6.5",
1981
+ "deno": "1",
1982
+ "ios": "10",
1983
+ "samsung": "5",
1984
+ "opera_mobile": "41",
1985
+ "electron": "1.2"
1986
+ },
1987
+ "es6.typed.uint8-clamped-array": {
1988
+ "chrome": "51",
1989
+ "opera": "38",
1990
+ "edge": "13",
1991
+ "firefox": "48",
1992
+ "safari": "10",
1993
+ "node": "6.5",
1994
+ "deno": "1",
1995
+ "ios": "10",
1996
+ "samsung": "5",
1997
+ "opera_mobile": "41",
1998
+ "electron": "1.2"
1999
+ },
2000
+ "es6.typed.int16-array": {
2001
+ "chrome": "51",
2002
+ "opera": "38",
2003
+ "edge": "13",
2004
+ "firefox": "48",
2005
+ "safari": "10",
2006
+ "node": "6.5",
2007
+ "deno": "1",
2008
+ "ios": "10",
2009
+ "samsung": "5",
2010
+ "opera_mobile": "41",
2011
+ "electron": "1.2"
2012
+ },
2013
+ "es6.typed.uint16-array": {
2014
+ "chrome": "51",
2015
+ "opera": "38",
2016
+ "edge": "13",
2017
+ "firefox": "48",
2018
+ "safari": "10",
2019
+ "node": "6.5",
2020
+ "deno": "1",
2021
+ "ios": "10",
2022
+ "samsung": "5",
2023
+ "opera_mobile": "41",
2024
+ "electron": "1.2"
2025
+ },
2026
+ "es6.typed.int32-array": {
2027
+ "chrome": "51",
2028
+ "opera": "38",
2029
+ "edge": "13",
2030
+ "firefox": "48",
2031
+ "safari": "10",
2032
+ "node": "6.5",
2033
+ "deno": "1",
2034
+ "ios": "10",
2035
+ "samsung": "5",
2036
+ "opera_mobile": "41",
2037
+ "electron": "1.2"
2038
+ },
2039
+ "es6.typed.uint32-array": {
2040
+ "chrome": "51",
2041
+ "opera": "38",
2042
+ "edge": "13",
2043
+ "firefox": "48",
2044
+ "safari": "10",
2045
+ "node": "6.5",
2046
+ "deno": "1",
2047
+ "ios": "10",
2048
+ "samsung": "5",
2049
+ "opera_mobile": "41",
2050
+ "electron": "1.2"
2051
+ },
2052
+ "es6.typed.float32-array": {
2053
+ "chrome": "51",
2054
+ "opera": "38",
2055
+ "edge": "13",
2056
+ "firefox": "48",
2057
+ "safari": "10",
2058
+ "node": "6.5",
2059
+ "deno": "1",
2060
+ "ios": "10",
2061
+ "samsung": "5",
2062
+ "opera_mobile": "41",
2063
+ "electron": "1.2"
2064
+ },
2065
+ "es6.typed.float64-array": {
2066
+ "chrome": "51",
2067
+ "opera": "38",
2068
+ "edge": "13",
2069
+ "firefox": "48",
2070
+ "safari": "10",
2071
+ "node": "6.5",
2072
+ "deno": "1",
2073
+ "ios": "10",
2074
+ "samsung": "5",
2075
+ "opera_mobile": "41",
2076
+ "electron": "1.2"
2077
+ },
2078
+ "es6.weak-map": {
2079
+ "chrome": "51",
2080
+ "opera": "38",
2081
+ "edge": "15",
2082
+ "firefox": "53",
2083
+ "safari": "9",
2084
+ "node": "6.5",
2085
+ "deno": "1",
2086
+ "ios": "9",
2087
+ "samsung": "5",
2088
+ "rhino": "1.7.15",
2089
+ "opera_mobile": "41",
2090
+ "electron": "1.2"
2091
+ },
2092
+ "es6.weak-set": {
2093
+ "chrome": "51",
2094
+ "opera": "38",
2095
+ "edge": "15",
2096
+ "firefox": "53",
2097
+ "safari": "9",
2098
+ "node": "6.5",
2099
+ "deno": "1",
2100
+ "ios": "9",
2101
+ "samsung": "5",
2102
+ "rhino": "1.7.15",
2103
+ "opera_mobile": "41",
2104
+ "electron": "1.2"
2105
+ }
2106
+ }
dashboard/frontend/node_modules/@babel/compat-data/data/corejs3-shipped-proposals.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ [
2
+ "esnext.promise.all-settled",
3
+ "esnext.string.match-all",
4
+ "esnext.global-this"
5
+ ]
dashboard/frontend/node_modules/@babel/compat-data/data/native-modules.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "es6.module": {
3
+ "chrome": "61",
4
+ "and_chr": "61",
5
+ "edge": "16",
6
+ "firefox": "60",
7
+ "and_ff": "60",
8
+ "node": "13.2.0",
9
+ "opera": "48",
10
+ "op_mob": "45",
11
+ "safari": "10.1",
12
+ "ios": "10.3",
13
+ "samsung": "8.2",
14
+ "android": "61",
15
+ "electron": "2.0",
16
+ "ios_saf": "10.3"
17
+ }
18
+ }
dashboard/frontend/node_modules/@babel/compat-data/data/overlapping-plugins.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transform-async-to-generator": [
3
+ "bugfix/transform-async-arrows-in-class"
4
+ ],
5
+ "transform-parameters": [
6
+ "bugfix/transform-edge-default-parameters",
7
+ "bugfix/transform-safari-id-destructuring-collision-in-function-expression"
8
+ ],
9
+ "transform-function-name": [
10
+ "bugfix/transform-edge-function-name"
11
+ ],
12
+ "transform-block-scoping": [
13
+ "bugfix/transform-safari-block-shadowing",
14
+ "bugfix/transform-safari-for-shadowing"
15
+ ],
16
+ "transform-template-literals": [
17
+ "bugfix/transform-tagged-template-caching"
18
+ ],
19
+ "transform-optional-chaining": [
20
+ "bugfix/transform-v8-spread-parameters-in-optional-chaining"
21
+ ],
22
+ "proposal-optional-chaining": [
23
+ "bugfix/transform-v8-spread-parameters-in-optional-chaining"
24
+ ],
25
+ "transform-class-properties": [
26
+ "bugfix/transform-v8-static-class-fields-redefine-readonly",
27
+ "bugfix/transform-firefox-class-in-computed-class-key",
28
+ "bugfix/transform-safari-class-field-initializer-scope"
29
+ ],
30
+ "proposal-class-properties": [
31
+ "bugfix/transform-v8-static-class-fields-redefine-readonly",
32
+ "bugfix/transform-firefox-class-in-computed-class-key",
33
+ "bugfix/transform-safari-class-field-initializer-scope"
34
+ ]
35
+ }
dashboard/frontend/node_modules/@babel/compat-data/data/plugin-bugfixes.json ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bugfix/transform-async-arrows-in-class": {
3
+ "chrome": "55",
4
+ "opera": "42",
5
+ "edge": "15",
6
+ "firefox": "52",
7
+ "safari": "11",
8
+ "node": "7.6",
9
+ "deno": "1",
10
+ "ios": "11",
11
+ "samsung": "6",
12
+ "opera_mobile": "42",
13
+ "electron": "1.6"
14
+ },
15
+ "bugfix/transform-edge-default-parameters": {
16
+ "chrome": "49",
17
+ "opera": "36",
18
+ "edge": "18",
19
+ "firefox": "52",
20
+ "safari": "10",
21
+ "node": "6",
22
+ "deno": "1",
23
+ "ios": "10",
24
+ "samsung": "5",
25
+ "opera_mobile": "36",
26
+ "electron": "0.37"
27
+ },
28
+ "bugfix/transform-edge-function-name": {
29
+ "chrome": "51",
30
+ "opera": "38",
31
+ "edge": "79",
32
+ "firefox": "53",
33
+ "safari": "10",
34
+ "node": "6.5",
35
+ "deno": "1",
36
+ "ios": "10",
37
+ "samsung": "5",
38
+ "opera_mobile": "41",
39
+ "electron": "1.2"
40
+ },
41
+ "bugfix/transform-safari-block-shadowing": {
42
+ "chrome": "49",
43
+ "opera": "36",
44
+ "edge": "12",
45
+ "firefox": "44",
46
+ "safari": "11",
47
+ "node": "6",
48
+ "deno": "1",
49
+ "ie": "11",
50
+ "ios": "11",
51
+ "samsung": "5",
52
+ "opera_mobile": "36",
53
+ "electron": "0.37"
54
+ },
55
+ "bugfix/transform-safari-for-shadowing": {
56
+ "chrome": "49",
57
+ "opera": "36",
58
+ "edge": "12",
59
+ "firefox": "4",
60
+ "safari": "11",
61
+ "node": "6",
62
+ "deno": "1",
63
+ "ie": "11",
64
+ "ios": "11",
65
+ "samsung": "5",
66
+ "rhino": "1.7.13",
67
+ "opera_mobile": "36",
68
+ "electron": "0.37"
69
+ },
70
+ "bugfix/transform-safari-id-destructuring-collision-in-function-expression": {
71
+ "chrome": "49",
72
+ "opera": "36",
73
+ "edge": "14",
74
+ "firefox": "2",
75
+ "safari": "16.3",
76
+ "node": "6",
77
+ "deno": "1",
78
+ "ios": "16.3",
79
+ "samsung": "5",
80
+ "opera_mobile": "36",
81
+ "electron": "0.37"
82
+ },
83
+ "bugfix/transform-tagged-template-caching": {
84
+ "chrome": "41",
85
+ "opera": "28",
86
+ "edge": "12",
87
+ "firefox": "34",
88
+ "safari": "13",
89
+ "node": "4",
90
+ "deno": "1",
91
+ "ios": "13",
92
+ "samsung": "3.4",
93
+ "rhino": "1.7.14",
94
+ "opera_mobile": "28",
95
+ "electron": "0.21"
96
+ },
97
+ "bugfix/transform-v8-spread-parameters-in-optional-chaining": {
98
+ "chrome": "91",
99
+ "opera": "77",
100
+ "edge": "91",
101
+ "firefox": "74",
102
+ "safari": "13.1",
103
+ "node": "16.9",
104
+ "deno": "1.9",
105
+ "ios": "13.4",
106
+ "samsung": "16",
107
+ "opera_mobile": "64",
108
+ "electron": "13.0"
109
+ },
110
+ "transform-optional-chaining": {
111
+ "chrome": "80",
112
+ "opera": "67",
113
+ "edge": "80",
114
+ "firefox": "74",
115
+ "safari": "13.1",
116
+ "node": "14",
117
+ "deno": "1",
118
+ "ios": "13.4",
119
+ "samsung": "13",
120
+ "rhino": "1.8",
121
+ "opera_mobile": "57",
122
+ "electron": "8.0"
123
+ },
124
+ "proposal-optional-chaining": {
125
+ "chrome": "80",
126
+ "opera": "67",
127
+ "edge": "80",
128
+ "firefox": "74",
129
+ "safari": "13.1",
130
+ "node": "14",
131
+ "deno": "1",
132
+ "ios": "13.4",
133
+ "samsung": "13",
134
+ "rhino": "1.8",
135
+ "opera_mobile": "57",
136
+ "electron": "8.0"
137
+ },
138
+ "transform-parameters": {
139
+ "chrome": "49",
140
+ "opera": "36",
141
+ "edge": "15",
142
+ "firefox": "52",
143
+ "safari": "10",
144
+ "node": "6",
145
+ "deno": "1",
146
+ "ios": "10",
147
+ "samsung": "5",
148
+ "opera_mobile": "36",
149
+ "electron": "0.37"
150
+ },
151
+ "transform-async-to-generator": {
152
+ "chrome": "55",
153
+ "opera": "42",
154
+ "edge": "15",
155
+ "firefox": "52",
156
+ "safari": "10.1",
157
+ "node": "7.6",
158
+ "deno": "1",
159
+ "ios": "10.3",
160
+ "samsung": "6",
161
+ "opera_mobile": "42",
162
+ "electron": "1.6"
163
+ },
164
+ "transform-template-literals": {
165
+ "chrome": "41",
166
+ "opera": "28",
167
+ "edge": "13",
168
+ "firefox": "34",
169
+ "safari": "9",
170
+ "node": "4",
171
+ "deno": "1",
172
+ "ios": "9",
173
+ "samsung": "3.4",
174
+ "opera_mobile": "28",
175
+ "electron": "0.21"
176
+ },
177
+ "transform-function-name": {
178
+ "chrome": "51",
179
+ "opera": "38",
180
+ "edge": "14",
181
+ "firefox": "53",
182
+ "safari": "10",
183
+ "node": "6.5",
184
+ "deno": "1",
185
+ "ios": "10",
186
+ "samsung": "5",
187
+ "opera_mobile": "41",
188
+ "electron": "1.2"
189
+ },
190
+ "transform-block-scoping": {
191
+ "chrome": "50",
192
+ "opera": "37",
193
+ "edge": "14",
194
+ "firefox": "53",
195
+ "safari": "10",
196
+ "node": "6",
197
+ "deno": "1",
198
+ "ios": "10",
199
+ "samsung": "5",
200
+ "opera_mobile": "37",
201
+ "electron": "1.1"
202
+ }
203
+ }
dashboard/frontend/node_modules/@babel/compat-data/data/plugins.json ADDED
@@ -0,0 +1,838 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transform-explicit-resource-management": {
3
+ "chrome": "134",
4
+ "edge": "134",
5
+ "firefox": "141",
6
+ "node": "24",
7
+ "electron": "35.0"
8
+ },
9
+ "transform-duplicate-named-capturing-groups-regex": {
10
+ "chrome": "126",
11
+ "opera": "112",
12
+ "edge": "126",
13
+ "firefox": "129",
14
+ "safari": "17.4",
15
+ "node": "23",
16
+ "ios": "17.4",
17
+ "electron": "31.0"
18
+ },
19
+ "transform-regexp-modifiers": {
20
+ "chrome": "125",
21
+ "opera": "111",
22
+ "edge": "125",
23
+ "firefox": "132",
24
+ "node": "23",
25
+ "samsung": "27",
26
+ "electron": "31.0"
27
+ },
28
+ "transform-unicode-sets-regex": {
29
+ "chrome": "112",
30
+ "opera": "98",
31
+ "edge": "112",
32
+ "firefox": "116",
33
+ "safari": "17",
34
+ "node": "20",
35
+ "deno": "1.32",
36
+ "ios": "17",
37
+ "samsung": "23",
38
+ "opera_mobile": "75",
39
+ "electron": "24.0"
40
+ },
41
+ "bugfix/transform-v8-static-class-fields-redefine-readonly": {
42
+ "chrome": "98",
43
+ "opera": "84",
44
+ "edge": "98",
45
+ "firefox": "75",
46
+ "safari": "15",
47
+ "node": "12",
48
+ "deno": "1.18",
49
+ "ios": "15",
50
+ "samsung": "11",
51
+ "opera_mobile": "52",
52
+ "electron": "17.0"
53
+ },
54
+ "bugfix/transform-firefox-class-in-computed-class-key": {
55
+ "chrome": "74",
56
+ "opera": "62",
57
+ "edge": "79",
58
+ "firefox": "126",
59
+ "safari": "16",
60
+ "node": "12",
61
+ "deno": "1",
62
+ "ios": "16",
63
+ "samsung": "11",
64
+ "opera_mobile": "53",
65
+ "electron": "6.0"
66
+ },
67
+ "bugfix/transform-safari-class-field-initializer-scope": {
68
+ "chrome": "74",
69
+ "opera": "62",
70
+ "edge": "79",
71
+ "firefox": "69",
72
+ "safari": "16",
73
+ "node": "12",
74
+ "deno": "1",
75
+ "ios": "16",
76
+ "samsung": "11",
77
+ "opera_mobile": "53",
78
+ "electron": "6.0"
79
+ },
80
+ "transform-class-static-block": {
81
+ "chrome": "94",
82
+ "opera": "80",
83
+ "edge": "94",
84
+ "firefox": "93",
85
+ "safari": "16.4",
86
+ "node": "16.11",
87
+ "deno": "1.14",
88
+ "ios": "16.4",
89
+ "samsung": "17",
90
+ "opera_mobile": "66",
91
+ "electron": "15.0"
92
+ },
93
+ "proposal-class-static-block": {
94
+ "chrome": "94",
95
+ "opera": "80",
96
+ "edge": "94",
97
+ "firefox": "93",
98
+ "safari": "16.4",
99
+ "node": "16.11",
100
+ "deno": "1.14",
101
+ "ios": "16.4",
102
+ "samsung": "17",
103
+ "opera_mobile": "66",
104
+ "electron": "15.0"
105
+ },
106
+ "transform-private-property-in-object": {
107
+ "chrome": "91",
108
+ "opera": "77",
109
+ "edge": "91",
110
+ "firefox": "90",
111
+ "safari": "15",
112
+ "node": "16.9",
113
+ "deno": "1.9",
114
+ "ios": "15",
115
+ "samsung": "16",
116
+ "opera_mobile": "64",
117
+ "electron": "13.0"
118
+ },
119
+ "proposal-private-property-in-object": {
120
+ "chrome": "91",
121
+ "opera": "77",
122
+ "edge": "91",
123
+ "firefox": "90",
124
+ "safari": "15",
125
+ "node": "16.9",
126
+ "deno": "1.9",
127
+ "ios": "15",
128
+ "samsung": "16",
129
+ "opera_mobile": "64",
130
+ "electron": "13.0"
131
+ },
132
+ "transform-class-properties": {
133
+ "chrome": "74",
134
+ "opera": "62",
135
+ "edge": "79",
136
+ "firefox": "90",
137
+ "safari": "14.1",
138
+ "node": "12",
139
+ "deno": "1",
140
+ "ios": "14.5",
141
+ "samsung": "11",
142
+ "opera_mobile": "53",
143
+ "electron": "6.0"
144
+ },
145
+ "proposal-class-properties": {
146
+ "chrome": "74",
147
+ "opera": "62",
148
+ "edge": "79",
149
+ "firefox": "90",
150
+ "safari": "14.1",
151
+ "node": "12",
152
+ "deno": "1",
153
+ "ios": "14.5",
154
+ "samsung": "11",
155
+ "opera_mobile": "53",
156
+ "electron": "6.0"
157
+ },
158
+ "transform-private-methods": {
159
+ "chrome": "84",
160
+ "opera": "70",
161
+ "edge": "84",
162
+ "firefox": "90",
163
+ "safari": "15",
164
+ "node": "14.6",
165
+ "deno": "1",
166
+ "ios": "15",
167
+ "samsung": "14",
168
+ "opera_mobile": "60",
169
+ "electron": "10.0"
170
+ },
171
+ "proposal-private-methods": {
172
+ "chrome": "84",
173
+ "opera": "70",
174
+ "edge": "84",
175
+ "firefox": "90",
176
+ "safari": "15",
177
+ "node": "14.6",
178
+ "deno": "1",
179
+ "ios": "15",
180
+ "samsung": "14",
181
+ "opera_mobile": "60",
182
+ "electron": "10.0"
183
+ },
184
+ "transform-numeric-separator": {
185
+ "chrome": "75",
186
+ "opera": "62",
187
+ "edge": "79",
188
+ "firefox": "70",
189
+ "safari": "13",
190
+ "node": "12.5",
191
+ "deno": "1",
192
+ "ios": "13",
193
+ "samsung": "11",
194
+ "rhino": "1.7.14",
195
+ "opera_mobile": "54",
196
+ "electron": "6.0"
197
+ },
198
+ "proposal-numeric-separator": {
199
+ "chrome": "75",
200
+ "opera": "62",
201
+ "edge": "79",
202
+ "firefox": "70",
203
+ "safari": "13",
204
+ "node": "12.5",
205
+ "deno": "1",
206
+ "ios": "13",
207
+ "samsung": "11",
208
+ "rhino": "1.7.14",
209
+ "opera_mobile": "54",
210
+ "electron": "6.0"
211
+ },
212
+ "transform-logical-assignment-operators": {
213
+ "chrome": "85",
214
+ "opera": "71",
215
+ "edge": "85",
216
+ "firefox": "79",
217
+ "safari": "14",
218
+ "node": "15",
219
+ "deno": "1.2",
220
+ "ios": "14",
221
+ "samsung": "14",
222
+ "opera_mobile": "60",
223
+ "electron": "10.0"
224
+ },
225
+ "proposal-logical-assignment-operators": {
226
+ "chrome": "85",
227
+ "opera": "71",
228
+ "edge": "85",
229
+ "firefox": "79",
230
+ "safari": "14",
231
+ "node": "15",
232
+ "deno": "1.2",
233
+ "ios": "14",
234
+ "samsung": "14",
235
+ "opera_mobile": "60",
236
+ "electron": "10.0"
237
+ },
238
+ "transform-nullish-coalescing-operator": {
239
+ "chrome": "80",
240
+ "opera": "67",
241
+ "edge": "80",
242
+ "firefox": "72",
243
+ "safari": "13.1",
244
+ "node": "14",
245
+ "deno": "1",
246
+ "ios": "13.4",
247
+ "samsung": "13",
248
+ "rhino": "1.8",
249
+ "opera_mobile": "57",
250
+ "electron": "8.0"
251
+ },
252
+ "proposal-nullish-coalescing-operator": {
253
+ "chrome": "80",
254
+ "opera": "67",
255
+ "edge": "80",
256
+ "firefox": "72",
257
+ "safari": "13.1",
258
+ "node": "14",
259
+ "deno": "1",
260
+ "ios": "13.4",
261
+ "samsung": "13",
262
+ "rhino": "1.8",
263
+ "opera_mobile": "57",
264
+ "electron": "8.0"
265
+ },
266
+ "transform-optional-chaining": {
267
+ "chrome": "91",
268
+ "opera": "77",
269
+ "edge": "91",
270
+ "firefox": "74",
271
+ "safari": "13.1",
272
+ "node": "16.9",
273
+ "deno": "1.9",
274
+ "ios": "13.4",
275
+ "samsung": "16",
276
+ "opera_mobile": "64",
277
+ "electron": "13.0"
278
+ },
279
+ "proposal-optional-chaining": {
280
+ "chrome": "91",
281
+ "opera": "77",
282
+ "edge": "91",
283
+ "firefox": "74",
284
+ "safari": "13.1",
285
+ "node": "16.9",
286
+ "deno": "1.9",
287
+ "ios": "13.4",
288
+ "samsung": "16",
289
+ "opera_mobile": "64",
290
+ "electron": "13.0"
291
+ },
292
+ "transform-json-strings": {
293
+ "chrome": "66",
294
+ "opera": "53",
295
+ "edge": "79",
296
+ "firefox": "62",
297
+ "safari": "12",
298
+ "node": "10",
299
+ "deno": "1",
300
+ "ios": "12",
301
+ "samsung": "9",
302
+ "rhino": "1.7.14",
303
+ "opera_mobile": "47",
304
+ "electron": "3.0"
305
+ },
306
+ "proposal-json-strings": {
307
+ "chrome": "66",
308
+ "opera": "53",
309
+ "edge": "79",
310
+ "firefox": "62",
311
+ "safari": "12",
312
+ "node": "10",
313
+ "deno": "1",
314
+ "ios": "12",
315
+ "samsung": "9",
316
+ "rhino": "1.7.14",
317
+ "opera_mobile": "47",
318
+ "electron": "3.0"
319
+ },
320
+ "transform-optional-catch-binding": {
321
+ "chrome": "66",
322
+ "opera": "53",
323
+ "edge": "79",
324
+ "firefox": "58",
325
+ "safari": "11.1",
326
+ "node": "10",
327
+ "deno": "1",
328
+ "ios": "11.3",
329
+ "samsung": "9",
330
+ "opera_mobile": "47",
331
+ "electron": "3.0"
332
+ },
333
+ "proposal-optional-catch-binding": {
334
+ "chrome": "66",
335
+ "opera": "53",
336
+ "edge": "79",
337
+ "firefox": "58",
338
+ "safari": "11.1",
339
+ "node": "10",
340
+ "deno": "1",
341
+ "ios": "11.3",
342
+ "samsung": "9",
343
+ "opera_mobile": "47",
344
+ "electron": "3.0"
345
+ },
346
+ "transform-parameters": {
347
+ "chrome": "49",
348
+ "opera": "36",
349
+ "edge": "18",
350
+ "firefox": "52",
351
+ "safari": "16.3",
352
+ "node": "6",
353
+ "deno": "1",
354
+ "ios": "16.3",
355
+ "samsung": "5",
356
+ "opera_mobile": "36",
357
+ "electron": "0.37"
358
+ },
359
+ "transform-async-generator-functions": {
360
+ "chrome": "63",
361
+ "opera": "50",
362
+ "edge": "79",
363
+ "firefox": "57",
364
+ "safari": "12",
365
+ "node": "10",
366
+ "deno": "1",
367
+ "ios": "12",
368
+ "samsung": "8",
369
+ "opera_mobile": "46",
370
+ "electron": "3.0"
371
+ },
372
+ "proposal-async-generator-functions": {
373
+ "chrome": "63",
374
+ "opera": "50",
375
+ "edge": "79",
376
+ "firefox": "57",
377
+ "safari": "12",
378
+ "node": "10",
379
+ "deno": "1",
380
+ "ios": "12",
381
+ "samsung": "8",
382
+ "opera_mobile": "46",
383
+ "electron": "3.0"
384
+ },
385
+ "transform-object-rest-spread": {
386
+ "chrome": "60",
387
+ "opera": "47",
388
+ "edge": "79",
389
+ "firefox": "55",
390
+ "safari": "11.1",
391
+ "node": "8.3",
392
+ "deno": "1",
393
+ "ios": "11.3",
394
+ "samsung": "8",
395
+ "opera_mobile": "44",
396
+ "electron": "2.0"
397
+ },
398
+ "proposal-object-rest-spread": {
399
+ "chrome": "60",
400
+ "opera": "47",
401
+ "edge": "79",
402
+ "firefox": "55",
403
+ "safari": "11.1",
404
+ "node": "8.3",
405
+ "deno": "1",
406
+ "ios": "11.3",
407
+ "samsung": "8",
408
+ "opera_mobile": "44",
409
+ "electron": "2.0"
410
+ },
411
+ "transform-dotall-regex": {
412
+ "chrome": "62",
413
+ "opera": "49",
414
+ "edge": "79",
415
+ "firefox": "78",
416
+ "safari": "11.1",
417
+ "node": "8.10",
418
+ "deno": "1",
419
+ "ios": "11.3",
420
+ "samsung": "8",
421
+ "rhino": "1.7.15",
422
+ "opera_mobile": "46",
423
+ "electron": "3.0"
424
+ },
425
+ "transform-unicode-property-regex": {
426
+ "chrome": "64",
427
+ "opera": "51",
428
+ "edge": "79",
429
+ "firefox": "78",
430
+ "safari": "11.1",
431
+ "node": "10",
432
+ "deno": "1",
433
+ "ios": "11.3",
434
+ "samsung": "9",
435
+ "opera_mobile": "47",
436
+ "electron": "3.0"
437
+ },
438
+ "proposal-unicode-property-regex": {
439
+ "chrome": "64",
440
+ "opera": "51",
441
+ "edge": "79",
442
+ "firefox": "78",
443
+ "safari": "11.1",
444
+ "node": "10",
445
+ "deno": "1",
446
+ "ios": "11.3",
447
+ "samsung": "9",
448
+ "opera_mobile": "47",
449
+ "electron": "3.0"
450
+ },
451
+ "transform-named-capturing-groups-regex": {
452
+ "chrome": "64",
453
+ "opera": "51",
454
+ "edge": "79",
455
+ "firefox": "78",
456
+ "safari": "11.1",
457
+ "node": "10",
458
+ "deno": "1",
459
+ "ios": "11.3",
460
+ "samsung": "9",
461
+ "opera_mobile": "47",
462
+ "electron": "3.0"
463
+ },
464
+ "transform-async-to-generator": {
465
+ "chrome": "55",
466
+ "opera": "42",
467
+ "edge": "15",
468
+ "firefox": "52",
469
+ "safari": "11",
470
+ "node": "7.6",
471
+ "deno": "1",
472
+ "ios": "11",
473
+ "samsung": "6",
474
+ "opera_mobile": "42",
475
+ "electron": "1.6"
476
+ },
477
+ "transform-exponentiation-operator": {
478
+ "chrome": "52",
479
+ "opera": "39",
480
+ "edge": "14",
481
+ "firefox": "52",
482
+ "safari": "10.1",
483
+ "node": "7",
484
+ "deno": "1",
485
+ "ios": "10.3",
486
+ "samsung": "6",
487
+ "rhino": "1.7.14",
488
+ "opera_mobile": "41",
489
+ "electron": "1.3"
490
+ },
491
+ "transform-template-literals": {
492
+ "chrome": "41",
493
+ "opera": "28",
494
+ "edge": "13",
495
+ "firefox": "34",
496
+ "safari": "13",
497
+ "node": "4",
498
+ "deno": "1",
499
+ "ios": "13",
500
+ "samsung": "3.4",
501
+ "opera_mobile": "28",
502
+ "electron": "0.21"
503
+ },
504
+ "transform-literals": {
505
+ "chrome": "44",
506
+ "opera": "31",
507
+ "edge": "12",
508
+ "firefox": "53",
509
+ "safari": "9",
510
+ "node": "4",
511
+ "deno": "1",
512
+ "ios": "9",
513
+ "samsung": "4",
514
+ "rhino": "1.7.15",
515
+ "opera_mobile": "32",
516
+ "electron": "0.30"
517
+ },
518
+ "transform-function-name": {
519
+ "chrome": "51",
520
+ "opera": "38",
521
+ "edge": "79",
522
+ "firefox": "53",
523
+ "safari": "10",
524
+ "node": "6.5",
525
+ "deno": "1",
526
+ "ios": "10",
527
+ "samsung": "5",
528
+ "opera_mobile": "41",
529
+ "electron": "1.2"
530
+ },
531
+ "transform-arrow-functions": {
532
+ "chrome": "47",
533
+ "opera": "34",
534
+ "edge": "13",
535
+ "firefox": "43",
536
+ "safari": "10",
537
+ "node": "6",
538
+ "deno": "1",
539
+ "ios": "10",
540
+ "samsung": "5",
541
+ "rhino": "1.7.13",
542
+ "opera_mobile": "34",
543
+ "electron": "0.36"
544
+ },
545
+ "transform-block-scoped-functions": {
546
+ "chrome": "41",
547
+ "opera": "28",
548
+ "edge": "12",
549
+ "firefox": "46",
550
+ "safari": "10",
551
+ "node": "4",
552
+ "deno": "1",
553
+ "ie": "11",
554
+ "ios": "10",
555
+ "samsung": "3.4",
556
+ "opera_mobile": "28",
557
+ "electron": "0.21"
558
+ },
559
+ "transform-classes": {
560
+ "chrome": "46",
561
+ "opera": "33",
562
+ "edge": "13",
563
+ "firefox": "45",
564
+ "safari": "10",
565
+ "node": "5",
566
+ "deno": "1",
567
+ "ios": "10",
568
+ "samsung": "5",
569
+ "opera_mobile": "33",
570
+ "electron": "0.36"
571
+ },
572
+ "transform-object-super": {
573
+ "chrome": "46",
574
+ "opera": "33",
575
+ "edge": "13",
576
+ "firefox": "45",
577
+ "safari": "10",
578
+ "node": "5",
579
+ "deno": "1",
580
+ "ios": "10",
581
+ "samsung": "5",
582
+ "opera_mobile": "33",
583
+ "electron": "0.36"
584
+ },
585
+ "transform-shorthand-properties": {
586
+ "chrome": "43",
587
+ "opera": "30",
588
+ "edge": "12",
589
+ "firefox": "33",
590
+ "safari": "9",
591
+ "node": "4",
592
+ "deno": "1",
593
+ "ios": "9",
594
+ "samsung": "4",
595
+ "rhino": "1.7.14",
596
+ "opera_mobile": "30",
597
+ "electron": "0.27"
598
+ },
599
+ "transform-duplicate-keys": {
600
+ "chrome": "42",
601
+ "opera": "29",
602
+ "edge": "12",
603
+ "firefox": "34",
604
+ "safari": "9",
605
+ "node": "4",
606
+ "deno": "1",
607
+ "ios": "9",
608
+ "samsung": "3.4",
609
+ "opera_mobile": "29",
610
+ "electron": "0.25"
611
+ },
612
+ "transform-computed-properties": {
613
+ "chrome": "44",
614
+ "opera": "31",
615
+ "edge": "12",
616
+ "firefox": "34",
617
+ "safari": "7.1",
618
+ "node": "4",
619
+ "deno": "1",
620
+ "ios": "8",
621
+ "samsung": "4",
622
+ "rhino": "1.8",
623
+ "opera_mobile": "32",
624
+ "electron": "0.30"
625
+ },
626
+ "transform-for-of": {
627
+ "chrome": "51",
628
+ "opera": "38",
629
+ "edge": "15",
630
+ "firefox": "53",
631
+ "safari": "10",
632
+ "node": "6.5",
633
+ "deno": "1",
634
+ "ios": "10",
635
+ "samsung": "5",
636
+ "opera_mobile": "41",
637
+ "electron": "1.2"
638
+ },
639
+ "transform-sticky-regex": {
640
+ "chrome": "49",
641
+ "opera": "36",
642
+ "edge": "13",
643
+ "firefox": "3",
644
+ "safari": "10",
645
+ "node": "6",
646
+ "deno": "1",
647
+ "ios": "10",
648
+ "samsung": "5",
649
+ "rhino": "1.7.15",
650
+ "opera_mobile": "36",
651
+ "electron": "0.37"
652
+ },
653
+ "transform-unicode-escapes": {
654
+ "chrome": "44",
655
+ "opera": "31",
656
+ "edge": "12",
657
+ "firefox": "53",
658
+ "safari": "9",
659
+ "node": "4",
660
+ "deno": "1",
661
+ "ios": "9",
662
+ "samsung": "4",
663
+ "rhino": "1.7.15",
664
+ "opera_mobile": "32",
665
+ "electron": "0.30"
666
+ },
667
+ "transform-unicode-regex": {
668
+ "chrome": "50",
669
+ "opera": "37",
670
+ "edge": "13",
671
+ "firefox": "46",
672
+ "safari": "12",
673
+ "node": "6",
674
+ "deno": "1",
675
+ "ios": "12",
676
+ "samsung": "5",
677
+ "opera_mobile": "37",
678
+ "electron": "1.1"
679
+ },
680
+ "transform-spread": {
681
+ "chrome": "46",
682
+ "opera": "33",
683
+ "edge": "13",
684
+ "firefox": "45",
685
+ "safari": "10",
686
+ "node": "5",
687
+ "deno": "1",
688
+ "ios": "10",
689
+ "samsung": "5",
690
+ "opera_mobile": "33",
691
+ "electron": "0.36"
692
+ },
693
+ "transform-destructuring": {
694
+ "chrome": "51",
695
+ "opera": "38",
696
+ "edge": "15",
697
+ "firefox": "53",
698
+ "safari": "10",
699
+ "node": "6.5",
700
+ "deno": "1",
701
+ "ios": "10",
702
+ "samsung": "5",
703
+ "opera_mobile": "41",
704
+ "electron": "1.2"
705
+ },
706
+ "transform-block-scoping": {
707
+ "chrome": "50",
708
+ "opera": "37",
709
+ "edge": "14",
710
+ "firefox": "53",
711
+ "safari": "11",
712
+ "node": "6",
713
+ "deno": "1",
714
+ "ios": "11",
715
+ "samsung": "5",
716
+ "opera_mobile": "37",
717
+ "electron": "1.1"
718
+ },
719
+ "transform-typeof-symbol": {
720
+ "chrome": "48",
721
+ "opera": "35",
722
+ "edge": "12",
723
+ "firefox": "36",
724
+ "safari": "9",
725
+ "node": "6",
726
+ "deno": "1",
727
+ "ios": "9",
728
+ "samsung": "5",
729
+ "rhino": "1.8",
730
+ "opera_mobile": "35",
731
+ "electron": "0.37"
732
+ },
733
+ "transform-new-target": {
734
+ "chrome": "46",
735
+ "opera": "33",
736
+ "edge": "14",
737
+ "firefox": "41",
738
+ "safari": "10",
739
+ "node": "5",
740
+ "deno": "1",
741
+ "ios": "10",
742
+ "samsung": "5",
743
+ "opera_mobile": "33",
744
+ "electron": "0.36"
745
+ },
746
+ "transform-regenerator": {
747
+ "chrome": "50",
748
+ "opera": "37",
749
+ "edge": "13",
750
+ "firefox": "53",
751
+ "safari": "10",
752
+ "node": "6",
753
+ "deno": "1",
754
+ "ios": "10",
755
+ "samsung": "5",
756
+ "opera_mobile": "37",
757
+ "electron": "1.1"
758
+ },
759
+ "transform-member-expression-literals": {
760
+ "chrome": "7",
761
+ "opera": "12",
762
+ "edge": "12",
763
+ "firefox": "2",
764
+ "safari": "5.1",
765
+ "node": "0.4",
766
+ "deno": "1",
767
+ "ie": "9",
768
+ "android": "4",
769
+ "ios": "6",
770
+ "phantom": "1.9",
771
+ "samsung": "1",
772
+ "rhino": "1.7.13",
773
+ "opera_mobile": "12",
774
+ "electron": "0.20"
775
+ },
776
+ "transform-property-literals": {
777
+ "chrome": "7",
778
+ "opera": "12",
779
+ "edge": "12",
780
+ "firefox": "2",
781
+ "safari": "5.1",
782
+ "node": "0.4",
783
+ "deno": "1",
784
+ "ie": "9",
785
+ "android": "4",
786
+ "ios": "6",
787
+ "phantom": "1.9",
788
+ "samsung": "1",
789
+ "rhino": "1.7.13",
790
+ "opera_mobile": "12",
791
+ "electron": "0.20"
792
+ },
793
+ "transform-reserved-words": {
794
+ "chrome": "13",
795
+ "opera": "10.50",
796
+ "edge": "12",
797
+ "firefox": "2",
798
+ "safari": "3.1",
799
+ "node": "0.6",
800
+ "deno": "1",
801
+ "ie": "9",
802
+ "android": "4.4",
803
+ "ios": "6",
804
+ "phantom": "1.9",
805
+ "samsung": "1",
806
+ "rhino": "1.7.13",
807
+ "opera_mobile": "10.1",
808
+ "electron": "0.20"
809
+ },
810
+ "transform-export-namespace-from": {
811
+ "chrome": "72",
812
+ "deno": "1.0",
813
+ "edge": "79",
814
+ "firefox": "80",
815
+ "node": "13.2.0",
816
+ "opera": "60",
817
+ "opera_mobile": "51",
818
+ "safari": "14.1",
819
+ "ios": "14.5",
820
+ "samsung": "11.0",
821
+ "android": "72",
822
+ "electron": "5.0"
823
+ },
824
+ "proposal-export-namespace-from": {
825
+ "chrome": "72",
826
+ "deno": "1.0",
827
+ "edge": "79",
828
+ "firefox": "80",
829
+ "node": "13.2.0",
830
+ "opera": "60",
831
+ "opera_mobile": "51",
832
+ "safari": "14.1",
833
+ "ios": "14.5",
834
+ "samsung": "11.0",
835
+ "android": "72",
836
+ "electron": "5.0"
837
+ }
838
+ }
dashboard/frontend/node_modules/@babel/compat-data/native-modules.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
2
+ module.exports = require("./data/native-modules.json");
dashboard/frontend/node_modules/@babel/compat-data/overlapping-plugins.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
2
+ module.exports = require("./data/overlapping-plugins.json");
dashboard/frontend/node_modules/@babel/compat-data/package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@babel/compat-data",
3
+ "version": "7.29.0",
4
+ "author": "The Babel Team (https://babel.dev/team)",
5
+ "license": "MIT",
6
+ "description": "The compat-data to determine required Babel plugins",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/babel/babel.git",
10
+ "directory": "packages/babel-compat-data"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "exports": {
16
+ "./plugins": "./plugins.js",
17
+ "./native-modules": "./native-modules.js",
18
+ "./corejs2-built-ins": "./corejs2-built-ins.js",
19
+ "./corejs3-shipped-proposals": "./corejs3-shipped-proposals.js",
20
+ "./overlapping-plugins": "./overlapping-plugins.js",
21
+ "./plugin-bugfixes": "./plugin-bugfixes.js"
22
+ },
23
+ "scripts": {
24
+ "build-data": "./scripts/download-compat-table.sh && node ./scripts/build-data.mjs && node ./scripts/build-modules-support.mjs && node ./scripts/build-bugfixes-targets.mjs"
25
+ },
26
+ "keywords": [
27
+ "babel",
28
+ "compat-table",
29
+ "compat-data"
30
+ ],
31
+ "devDependencies": {
32
+ "@mdn/browser-compat-data": "^6.0.8",
33
+ "core-js-compat": "^3.48.0",
34
+ "electron-to-chromium": "^1.5.278"
35
+ },
36
+ "engines": {
37
+ "node": ">=6.9.0"
38
+ },
39
+ "type": "commonjs"
40
+ }
dashboard/frontend/node_modules/@babel/compat-data/plugin-bugfixes.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
2
+ module.exports = require("./data/plugin-bugfixes.json");