File size: 7,915 Bytes
71d239c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7603aa2
71d239c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
"""Central configuration: env, story styles, difficulty levels, file filters, palette.

StoryCode reads a non-coder's project and explains it as a story. The values here
are deliberately plain-language: labels a non-technical person sees should never
contain jargon unless we explain it.
"""
from __future__ import annotations

import os
from dataclasses import dataclass

from dotenv import load_dotenv

load_dotenv()

# --- Runtime / services -----------------------------------------------------
# The model runs on Modal (vLLM, OpenAI-compatible API). The Space is CPU-only.
MODAL_ENDPOINT_URL = os.getenv("MODAL_ENDPOINT_URL", "").rstrip("/")
MODAL_API_KEY = os.getenv("MODAL_API_KEY", "")
MODEL_ID = os.getenv("MODEL_ID", "openbmb/MiniCPM4.1-8B")
DB_PATH = os.getenv("STORYCODE_DB_PATH", "data/storycode.db")
USE_ZEROGPU_FALLBACK = os.getenv("USE_ZEROGPU_FALLBACK", "0") == "1"

# Generation limits (keep the Space responsive; map-reduce stays well under 32k ctx).
LLM_TIMEOUT_S = int(os.getenv("LLM_TIMEOUT_S", "90"))
MAX_FILES = int(os.getenv("STORYCODE_MAX_FILES", "60"))      # analysed per project
MAX_FILE_BYTES = int(os.getenv("STORYCODE_MAX_FILE_BYTES", str(200_000)))
MAX_TOTAL_BYTES = int(os.getenv("STORYCODE_MAX_TOTAL_BYTES", str(5_000_000)))

# --- Languages we understand (stated honestly in the UI) --------------------
# Extension -> language key. Anything else is listed but not deeply parsed.
LANG_BY_EXT: dict[str, str] = {
    ".py": "python",
    ".js": "javascript", ".jsx": "javascript",
    ".ts": "typescript", ".tsx": "typescript", ".mjs": "javascript", ".cjs": "javascript",
    ".html": "html", ".htm": "html",
    ".css": "css", ".scss": "css",
    ".json": "json",
    ".yaml": "yaml", ".yml": "yaml",
    ".md": "markdown",
}

# Directories and files we never analyse (noise / not the user's own code).
IGNORE_DIRS: frozenset[str] = frozenset({
    "node_modules", ".git", ".svn", "__pycache__", ".venv", "venv", "env",
    "dist", "build", ".next", ".nuxt", "out", "target", ".idea", ".vscode",
    "coverage", ".pytest_cache", ".mypy_cache", "site-packages", ".cache",
    "vendor", "bower_components", ".gradio",
})
IGNORE_FILES: frozenset[str] = frozenset({
    "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock",
    ".ds_store", "thumbs.db",
})
# Binary-ish extensions to skip outright.
BINARY_EXTS: frozenset[str] = frozenset({
    ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".svg", ".pdf", ".zip",
    ".gz", ".tar", ".mp3", ".mp4", ".wav", ".woff", ".woff2", ".ttf", ".otf",
    ".pyc", ".so", ".dll", ".exe", ".bin", ".db", ".sqlite", ".lock",
})

# Manifest files the dependency explainer reads.
DEP_MANIFESTS: frozenset[str] = frozenset({
    "requirements.txt", "package.json", "pyproject.toml", "pipfile",
})


# --- Roles -------------------------------------------------------------------
# A file's "role" drives the architecture-map grouping and the safe-to-edit
# verdict. These are plain-English buckets, not technical layers.
@dataclass(frozen=True)
class Role:
    key: str
    label: str
    color: str  # used in the diagram + chips


ROLES: tuple[Role, ...] = (
    Role("entry", "Front door", "#4b5bd6"),       # what runs first / users hit
    Role("frontend", "What you see", "#3a7ca5"),  # UI / pages / styles
    Role("backend", "The engine", "#6b4fb0"),     # logic / services / API calls
    Role("data", "Memory", "#3f9b6b"),            # db / storage / models / embeddings
    Role("config", "Settings", "#8a7a66"),        # config / env / manifests
    Role("test", "Safety checks", "#9aa0a6"),     # tests
    Role("util", "Helpers", "#b0793a"),           # shared utilities
    Role("other", "Other", "#9aa0a6"),
)
ROLE_KEYS: tuple[str, ...] = tuple(r.key for r in ROLES)
ROLE_LABEL = {r.key: r.label for r in ROLES}
ROLE_COLOR = {r.key: r.color for r in ROLES}

# Filename / path hints used by analyzer.graph.classify_role (first match wins).
ENTRY_NAMES: frozenset[str] = frozenset({
    "app.py", "main.py", "manage.py", "server.py", "run.py", "wsgi.py", "asgi.py",
    "index.js", "index.ts", "index.jsx", "index.tsx", "main.js", "main.ts",
    "index.html", "app.js", "app.ts",
    "train.py", "play.py", "demo.py", "infer.py", "evaluate.py", "test.py",
})
CONFIG_HINTS: tuple[str, ...] = (
    "config", "settings", ".env", "requirements", "package.json", "pyproject",
    "dockerfile", "makefile", ".yaml", ".yml", ".toml", ".ini",
)
DATA_HINTS: tuple[str, ...] = (
    "db", "database", "model", "schema", "store", "embed", "vector",
    "index", "ingest", "migration", "seed",
)
TEST_HINTS: tuple[str, ...] = ("test", "spec", "__tests__")
UTIL_HINTS: tuple[str, ...] = ("util", "helper", "common", "lib", "shared", "tools")


# --- Story styles ------------------------------------------------------------
# Simple Walkthrough is the DEFAULT (per the brief: creative styles can feel
# patronising; they're opt-in). Each carries a short voice instruction handed to
# the model, but the model only ever narrates facts we extracted.
@dataclass(frozen=True)
class Style:
    key: str
    label: str
    emoji: str
    voice: str  # instruction injected into the narration prompt


STYLES: tuple[Style, ...] = (
    Style("plain", "Simple Walkthrough", "πŸ“‹",
          "Explain plainly and warmly, step by step, no metaphors, no jargon. "
          "Like a patient friend who read the whole project for them."),
    Style("kids", "Kids Book", "🧸",
          "Tell it like a children's picture book. Each file is a friendly "
          "character with a job. Keep sentences short and gentle."),
    Style("thriller", "Thriller", "πŸ•΅οΈ",
          "Tell it like a suspense thriller β€” the request arrives, tension "
          "builds as it moves through the code β€” but every claim must stay true."),
    Style("news", "News Report", "πŸ“°",
          "Report it like a breaking-news anchor: who did what, in what order, "
          "with punchy headlines. Accurate, never sensational about facts."),
    Style("recipe", "Recipe", "🍳",
          "Explain it like a cooking recipe: ingredients (the pieces) and steps "
          "(the flow), in order."),
)
STYLE_KEYS: tuple[str, ...] = tuple(s.key for s in STYLES)
STYLE_BY_KEY = {s.key: s for s in STYLES}
DEFAULT_STYLE = "plain"


@dataclass(frozen=True)
class Difficulty:
    key: str
    label: str
    voice: str


DIFFICULTIES: tuple[Difficulty, ...] = (
    Difficulty("kid", "Explain like I'm 5",
               "Use no technical terms at all. Everyday words a child knows."),
    Difficulty("teen", "Teenager",
               "Light technical terms are fine if you explain each one in the "
               "same sentence."),
    Difficulty("adult", "Adult / curious beginner",
               "Full clarity. Technical terms are okay but define them the first "
               "time they appear."),
)
DIFFICULTY_KEYS: tuple[str, ...] = tuple(d.key for d in DIFFICULTIES)
DIFFICULTY_BY_KEY = {d.key: d for d in DIFFICULTIES}
DEFAULT_DIFFICULTY = "teen"


# --- Where did your code come from? (onboarding) ----------------------------
SOURCE_HINTS: tuple[str, ...] = (
    "Claude / Claude Code", "Cursor", "ChatGPT", "Bolt / v0 / Lovable",
    "A ZIP someone sent me", "Not sure",
)


# --- Visual palette (mirrors ui/styles.css; kept here for diagram + chips) ---
PALETTE = {
    "paper": "#f7f4ef",
    "ink": "#1d2430",
    "accent": "#4b5bd6",
    "safe": "#3f9b6b",
    "careful": "#d8932f",
    "danger": "#c2503f",
    "muted": "#6b7280",
    "line": "#e7e3da",
}

# Safe-to-edit verdicts (computed in analyzer.graph, never by the model).
SAFE = "safe"
CAREFUL = "careful"
DANGER = "danger"
SAFETY_COLOR = {SAFE: PALETTE["safe"], CAREFUL: PALETTE["careful"], DANGER: PALETTE["danger"]}
SAFETY_EMOJI = {SAFE: "🟒", CAREFUL: "🟠", DANGER: "πŸ”΄"}
SAFETY_LABEL = {SAFE: "Safe to change", CAREFUL: "Change carefully", DANGER: "Don't touch"}