| """The static-analysis engine: ingested files -> a factual ProjectModel. |
| |
| This is StoryCode's source of truth. It dispatches each file to the right parser |
| (Python `ast`, JS/TS tree-sitter, or the lightweight generic pass), reads the |
| dependency manifests, then builds the project graph (edges, fan-in/out, roles, |
| entry points, safe-to-edit). The language model is never involved here. |
| """ |
| from __future__ import annotations |
|
|
| import posixpath |
|
|
| import config |
| from ingest import Ingested |
| from schema import FileInfo, ProjectModel |
|
|
| from analyzer import deps, generic, graph, js_treesitter, python_ast |
|
|
|
|
| def _loc(text: str) -> int: |
| return sum(1 for line in text.splitlines() if line.strip()) |
|
|
|
|
| def _parse_one(path: str, lang: str, text: str): |
| if lang == "python": |
| return python_ast.parse(text) |
| if lang in ("javascript", "typescript"): |
| return js_treesitter.parse(text, lang) |
| if lang in ("html", "css"): |
| return generic.parse(text, lang) |
| return [], [] |
|
|
|
|
| def analyze_project(ingested: Ingested) -> ProjectModel: |
| """Turn ingested source files into a fully-populated ProjectModel.""" |
| files: list[FileInfo] = [] |
| dependencies = [] |
| langs: set[str] = set() |
|
|
| for sf in ingested.files: |
| base = posixpath.basename(sf.path).lower() |
| if base in config.DEP_MANIFESTS: |
| dependencies.extend(deps.parse_manifest(base, sf.text)) |
| |
| symbols, imports = _parse_one(sf.path, sf.lang, sf.text) |
| if sf.lang in config.LANG_BY_EXT.values(): |
| langs.add(sf.lang) |
| files.append(FileInfo( |
| path=sf.path, lang=sf.lang, loc=_loc(sf.text), |
| symbols=symbols, imports=imports, |
| role=graph.classify_role(sf.path, sf.lang), |
| )) |
|
|
| graph.resolve_edges(files) |
| graph.compute_metrics(files) |
|
|
| |
| seen: set[tuple[str, str]] = set() |
| uniq_deps = [] |
| for d in dependencies: |
| key = (d.name.lower(), d.manifest) |
| if key not in seen: |
| seen.add(key) |
| uniq_deps.append(d) |
|
|
| return ProjectModel( |
| name=ingested.name, |
| files=files, |
| deps=uniq_deps, |
| entry_points=graph.detect_entry_points(files), |
| languages=sorted(langs), |
| skipped=ingested.skipped, |
| secrets_found=ingested.secrets_found, |
| note=ingested.note, |
| ) |
|
|