sourcecode 0.33.0__py3-none-any.whl → 0.34.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sourcecode/__init__.py +1 -1
- sourcecode/ast_extractor.py +48 -3
- sourcecode/cli.py +28 -9
- sourcecode/relevance_scorer.py +40 -6
- sourcecode/serializer.py +288 -53
- {sourcecode-0.33.0.dist-info → sourcecode-0.34.0.dist-info}/METADATA +1 -1
- {sourcecode-0.33.0.dist-info → sourcecode-0.34.0.dist-info}/RECORD +10 -10
- {sourcecode-0.33.0.dist-info → sourcecode-0.34.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.33.0.dist-info → sourcecode-0.34.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-0.33.0.dist-info → sourcecode-0.34.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/ast_extractor.py
CHANGED
|
@@ -13,6 +13,7 @@ Install tree-sitter for best TS/JS results:
|
|
|
13
13
|
|
|
14
14
|
import ast
|
|
15
15
|
import re
|
|
16
|
+
import sys
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any, Iterator, Optional
|
|
18
19
|
|
|
@@ -31,6 +32,45 @@ from sourcecode.contract_model import (
|
|
|
31
32
|
|
|
32
33
|
_MAX_FILE_SIZE = 200_000 # bytes — skip files larger than this
|
|
33
34
|
|
|
35
|
+
# Python stdlib module names — used to filter noise from import lists.
|
|
36
|
+
# sys.stdlib_module_names is available in Python 3.10+; fall back to a
|
|
37
|
+
# curated set for 3.9 compatibility.
|
|
38
|
+
if hasattr(sys, "stdlib_module_names"):
|
|
39
|
+
_PY_STDLIB: frozenset[str] = sys.stdlib_module_names # type: ignore[attr-defined]
|
|
40
|
+
else:
|
|
41
|
+
_PY_STDLIB: frozenset[str] = frozenset({ # type: ignore[no-redef]
|
|
42
|
+
"__future__", "_thread", "abc", "aifc", "argparse", "array", "ast",
|
|
43
|
+
"asynchat", "asyncio", "asyncore", "atexit", "audioop", "base64",
|
|
44
|
+
"bdb", "binascii", "binhex", "bisect", "builtins", "bz2", "calendar",
|
|
45
|
+
"cgi", "cgitb", "chunk", "cmath", "cmd", "code", "codecs", "codeop",
|
|
46
|
+
"collections", "colorsys", "compileall", "concurrent", "configparser",
|
|
47
|
+
"contextlib", "contextvars", "copy", "copyreg", "cProfile", "csv",
|
|
48
|
+
"ctypes", "curses", "dataclasses", "datetime", "dbm", "decimal",
|
|
49
|
+
"difflib", "dis", "doctest", "email", "encodings", "enum", "errno",
|
|
50
|
+
"faulthandler", "fcntl", "filecmp", "fileinput", "fnmatch", "fractions",
|
|
51
|
+
"ftplib", "functools", "gc", "getopt", "getpass", "gettext", "glob",
|
|
52
|
+
"grp", "gzip", "hashlib", "heapq", "hmac", "html", "http", "idlelib",
|
|
53
|
+
"imaplib", "importlib", "inspect", "io", "ipaddress", "itertools",
|
|
54
|
+
"json", "keyword", "lib2to3", "linecache", "locale", "logging", "lzma",
|
|
55
|
+
"mailbox", "marshal", "math", "mimetypes", "mmap", "modulefinder",
|
|
56
|
+
"multiprocessing", "netrc", "nntplib", "numbers", "operator", "optparse",
|
|
57
|
+
"os", "pathlib", "pdb", "pickle", "pickletools", "pipes", "pkgutil",
|
|
58
|
+
"platform", "plistlib", "poplib", "posix", "posixpath", "pprint",
|
|
59
|
+
"profile", "pstats", "pty", "pwd", "py_compile", "pyclbr", "pydoc",
|
|
60
|
+
"queue", "quopri", "random", "re", "readline", "reprlib", "resource",
|
|
61
|
+
"rlcompleter", "runpy", "sched", "secrets", "select", "selectors",
|
|
62
|
+
"shelve", "shlex", "shutil", "signal", "site", "smtpd", "smtplib",
|
|
63
|
+
"sndhdr", "socket", "socketserver", "sqlite3", "ssl", "stat",
|
|
64
|
+
"statistics", "string", "stringprep", "struct", "subprocess", "sunau",
|
|
65
|
+
"symtable", "sys", "sysconfig", "syslog", "tabnanny", "tarfile",
|
|
66
|
+
"tempfile", "termios", "test", "textwrap", "threading", "time",
|
|
67
|
+
"timeit", "tkinter", "token", "tokenize", "tomllib", "trace",
|
|
68
|
+
"traceback", "tracemalloc", "tty", "types", "typing", "unicodedata",
|
|
69
|
+
"unittest", "urllib", "uuid", "venv", "warnings", "wave", "weakref",
|
|
70
|
+
"webbrowser", "wsgiref", "xml", "xmlrpc", "zipapp", "zipfile",
|
|
71
|
+
"zipimport", "zlib", "zoneinfo",
|
|
72
|
+
})
|
|
73
|
+
|
|
34
74
|
_LANGUAGE_MAP: dict[str, str] = {
|
|
35
75
|
".py": "python",
|
|
36
76
|
".ts": "typescript",
|
|
@@ -729,9 +769,10 @@ def _py_signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
|
|
729
769
|
sig += f" -> {ast.unparse(node.returns)}"
|
|
730
770
|
except Exception:
|
|
731
771
|
pass
|
|
732
|
-
#
|
|
733
|
-
|
|
734
|
-
|
|
772
|
+
# Keep full signature — serializer applies per-mode compression.
|
|
773
|
+
# Hard cap at 2000 to prevent pathological cases.
|
|
774
|
+
if len(sig) > 2000:
|
|
775
|
+
sig = sig[:1997] + "..."
|
|
735
776
|
return sig
|
|
736
777
|
|
|
737
778
|
|
|
@@ -840,6 +881,10 @@ def _extract_python(path: str, source: str) -> FileContract:
|
|
|
840
881
|
if exported or name in all_names:
|
|
841
882
|
exports.append(ExportRecord(name=name, kind="class"))
|
|
842
883
|
|
|
884
|
+
# Filter stdlib from imports — they add noise without signal for agents
|
|
885
|
+
_stdlib_roots = {m.split(".")[0] for m in _PY_STDLIB}
|
|
886
|
+
imports = [i for i in imports if i.source.split(".")[0] not in _stdlib_roots]
|
|
887
|
+
|
|
843
888
|
deps = sorted({
|
|
844
889
|
imp.source.split(".")[0]
|
|
845
890
|
for imp in imports
|
sourcecode/cli.py
CHANGED
|
@@ -516,11 +516,13 @@ def main(
|
|
|
516
516
|
"contract",
|
|
517
517
|
"--mode",
|
|
518
518
|
help=(
|
|
519
|
-
"Output mode: contract (default) | hybrid | raw. "
|
|
520
|
-
"contract: per-file
|
|
519
|
+
"Output mode: contract|minimal (default) | standard | deep | hybrid | raw. "
|
|
520
|
+
"contract/minimal: minimal per-file contracts — exports, signatures, deps. Smallest output. "
|
|
521
|
+
"standard: full per-file detail with imports, relevance scores, extraction method. "
|
|
522
|
+
"deep: standard + optional analysis sections (deps, env, git). "
|
|
521
523
|
"hybrid: contracts + compact bodies for top-ranked files. "
|
|
522
524
|
"raw: legacy project-level analysis (stacks, entry points, dependencies). "
|
|
523
|
-
"contract
|
|
525
|
+
"contract/minimal is the recommended default for AI coding agents."
|
|
524
526
|
),
|
|
525
527
|
),
|
|
526
528
|
max_symbols: Optional[int] = typer.Option(
|
|
@@ -587,7 +589,7 @@ def main(
|
|
|
587
589
|
_t0 = time.monotonic()
|
|
588
590
|
|
|
589
591
|
# Validate new flag choices
|
|
590
|
-
_MODE_CHOICES = ("contract", "hybrid", "raw")
|
|
592
|
+
_MODE_CHOICES = ("contract", "minimal", "standard", "deep", "hybrid", "raw")
|
|
591
593
|
if mode not in _MODE_CHOICES:
|
|
592
594
|
typer.echo(
|
|
593
595
|
f"Error: invalid value '{mode}' for --mode. Valid options: {', '.join(_MODE_CHOICES)}",
|
|
@@ -631,6 +633,13 @@ def main(
|
|
|
631
633
|
typer.echo(f"Error: '{target}' is not a directory.", err=True)
|
|
632
634
|
raise typer.Exit(code=1)
|
|
633
635
|
|
|
636
|
+
# Normalize mode aliases
|
|
637
|
+
_CONTRACT_MODES = frozenset({"contract", "minimal", "standard", "deep", "hybrid"})
|
|
638
|
+
if mode == "minimal":
|
|
639
|
+
mode = "contract" # minimal is the canonical default contract rendering
|
|
640
|
+
elif mode not in _CONTRACT_MODES and mode != "raw":
|
|
641
|
+
mode = "contract" # unknown → safe default
|
|
642
|
+
|
|
634
643
|
# Legacy flags imply raw mode unless --mode was explicitly overridden.
|
|
635
644
|
# These flags produce standard_view-only output sections not in contract_view.
|
|
636
645
|
# Preserves backward compat: callers using any legacy flag get their previous format.
|
|
@@ -639,9 +648,17 @@ def main(
|
|
|
639
648
|
compact or agent or tree or format == "yaml" or trace_pipeline
|
|
640
649
|
or docs or semantics or graph_modules or full_metrics or architecture
|
|
641
650
|
)
|
|
642
|
-
if mode
|
|
651
|
+
if mode in ("contract", "standard", "deep") and _legacy_flags_active:
|
|
643
652
|
mode = "raw"
|
|
644
653
|
|
|
654
|
+
# Map mode to contract_view depth
|
|
655
|
+
_CONTRACT_DEPTH = {
|
|
656
|
+
"contract": "minimal",
|
|
657
|
+
"standard": "standard",
|
|
658
|
+
"deep": "deep",
|
|
659
|
+
"hybrid": "minimal", # hybrid adds bodies via pipeline, minimal header
|
|
660
|
+
}
|
|
661
|
+
|
|
645
662
|
# --- Import analysis modules ---
|
|
646
663
|
from dataclasses import asdict, replace
|
|
647
664
|
|
|
@@ -1226,8 +1243,9 @@ def main(
|
|
|
1226
1243
|
))
|
|
1227
1244
|
sm = _replace(sm, pipeline_trace=_trace.build_trace())
|
|
1228
1245
|
|
|
1229
|
-
# Contract pipeline — runs for mode=contract|hybrid (skip for raw)
|
|
1230
|
-
|
|
1246
|
+
# Contract pipeline — runs for mode=contract|standard|deep|hybrid (skip for raw)
|
|
1247
|
+
_is_contract_mode = mode in ("contract", "standard", "deep", "hybrid")
|
|
1248
|
+
if _is_contract_mode:
|
|
1231
1249
|
from sourcecode.contract_pipeline import ContractPipeline
|
|
1232
1250
|
_cp = ContractPipeline()
|
|
1233
1251
|
_contracts, _contract_summary = _cp.run(
|
|
@@ -1249,9 +1267,10 @@ def main(
|
|
|
1249
1267
|
typer.echo(f"[contract] {len(_contracts)} files extracted ({_contract_summary.method_breakdown})", err=True)
|
|
1250
1268
|
|
|
1251
1269
|
# 4. Serialize
|
|
1252
|
-
if
|
|
1270
|
+
if _is_contract_mode:
|
|
1253
1271
|
from sourcecode.serializer import contract_view as _contract_view
|
|
1254
|
-
|
|
1272
|
+
_depth = _CONTRACT_DEPTH.get(mode, "minimal")
|
|
1273
|
+
data = _contract_view(sm, emit_graph=emit_graph, depth=_depth)
|
|
1255
1274
|
if not no_redact:
|
|
1256
1275
|
data = redact_dict(data)
|
|
1257
1276
|
content = json.dumps(data, indent=2, ensure_ascii=False)
|
sourcecode/relevance_scorer.py
CHANGED
|
@@ -82,8 +82,30 @@ _AUXILIARY_DIR_PATTERNS: list[re.Pattern[str]] = [
|
|
|
82
82
|
re.compile(r"(?:^|/)scripts?(?:/|$)"),
|
|
83
83
|
re.compile(r"(?:^|/)tools?(?:/|$)"),
|
|
84
84
|
re.compile(r"(?:^|/)ci(?:/|$)"),
|
|
85
|
+
re.compile(r"(?:^|/)migrations?(?:/|$)"),
|
|
86
|
+
re.compile(r"(?:^|/)generated?(?:/|$)"),
|
|
87
|
+
re.compile(r"(?:^|/)storybook(?:/|$)"),
|
|
88
|
+
re.compile(r"(?:^|/)stories(?:/|$)"),
|
|
85
89
|
]
|
|
86
90
|
|
|
91
|
+
# Test file patterns — scored low, excluded from default contract output
|
|
92
|
+
_TEST_FILE_PATTERNS: tuple[str, ...] = (
|
|
93
|
+
"_test.", ".test.", ".spec.", "test_", "conftest", "_spec.",
|
|
94
|
+
)
|
|
95
|
+
_TEST_DIR_MARKERS: frozenset[str] = frozenset({
|
|
96
|
+
"/test/", "/tests/", "/spec/", "/specs/", "/__tests__/", "/__mocks__/",
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
# Config/tooling filenames that are low runtime-relevance
|
|
100
|
+
_LOW_RUNTIME_STEMS: frozenset[str] = frozenset({
|
|
101
|
+
"setup", "setup.cfg", "pyproject", "package", "package-lock",
|
|
102
|
+
"yarn.lock", "pnpm-lock", "composer", "gemfile", "podfile",
|
|
103
|
+
"dockerfile", "docker-compose", "makefile", "rakefile",
|
|
104
|
+
"gruntfile", "gulpfile", "webpack.config", "vite.config",
|
|
105
|
+
"rollup.config", "babel.config", "jest.config", "vitest.config",
|
|
106
|
+
"tsconfig", "jsconfig", ".eslintrc", ".prettierrc", ".editorconfig",
|
|
107
|
+
})
|
|
108
|
+
|
|
87
109
|
_HIGH_VALUE_SUFFIXES: frozenset[str] = frozenset({
|
|
88
110
|
".py", ".ts", ".tsx", ".js", ".jsx", ".mjs",
|
|
89
111
|
".go", ".java", ".kt", ".rs", ".rb", ".cs",
|
|
@@ -114,7 +136,7 @@ class RelevanceScorer:
|
|
|
114
136
|
|
|
115
137
|
base = 0.3
|
|
116
138
|
|
|
117
|
-
# Package role boost
|
|
139
|
+
# Package role boost — runtime code scores high, tooling/docs low
|
|
118
140
|
role = self._package_role(norm)
|
|
119
141
|
role_boost = {
|
|
120
142
|
"runtime_core": 0.4,
|
|
@@ -124,10 +146,10 @@ class RelevanceScorer:
|
|
|
124
146
|
"composition_layer": 0.2,
|
|
125
147
|
"plugin_package": 0.15,
|
|
126
148
|
"infrastructure_layer": 0.15,
|
|
127
|
-
"tooling_layer": -0.
|
|
128
|
-
"docs_layer": -0.
|
|
129
|
-
"test_layer": 0.
|
|
130
|
-
"benchmark_layer": -0.
|
|
149
|
+
"tooling_layer": -0.15,
|
|
150
|
+
"docs_layer": -0.25,
|
|
151
|
+
"test_layer": -0.1,
|
|
152
|
+
"benchmark_layer": -0.25,
|
|
131
153
|
}.get(role, 0.0)
|
|
132
154
|
base += role_boost
|
|
133
155
|
|
|
@@ -141,7 +163,19 @@ class RelevanceScorer:
|
|
|
141
163
|
if stem in _ENTRYPOINT_STEMS:
|
|
142
164
|
base += 0.15
|
|
143
165
|
|
|
144
|
-
#
|
|
166
|
+
# Test file penalty — tests are useful for coverage but not for
|
|
167
|
+
# understanding architecture or editing production code
|
|
168
|
+
fname = Path(norm).name.lower()
|
|
169
|
+
if (any(m in f"/{norm}/" for m in _TEST_DIR_MARKERS)
|
|
170
|
+
or any(fname.startswith(p.strip(".")) or p in fname
|
|
171
|
+
for p in _TEST_FILE_PATTERNS)):
|
|
172
|
+
base -= 0.25
|
|
173
|
+
|
|
174
|
+
# Config/tooling filename penalty
|
|
175
|
+
if stem.lower() in _LOW_RUNTIME_STEMS:
|
|
176
|
+
base -= 0.2
|
|
177
|
+
|
|
178
|
+
# Auxiliary dir penalty
|
|
145
179
|
if self._is_auxiliary(norm):
|
|
146
180
|
base -= 0.2
|
|
147
181
|
|
sourcecode/serializer.py
CHANGED
|
@@ -868,26 +868,266 @@ def contract_view(
|
|
|
868
868
|
sm: SourceMap,
|
|
869
869
|
*,
|
|
870
870
|
emit_graph: bool = False,
|
|
871
|
+
depth: str = "minimal",
|
|
871
872
|
) -> dict[str, Any]:
|
|
872
|
-
"""Contract-mode output: project
|
|
873
|
+
"""Contract-mode output: project header + per-file semantic contracts.
|
|
873
874
|
|
|
874
|
-
|
|
875
|
-
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
875
|
+
depth="minimal" (default): compact header, filtered imports, no ranking
|
|
876
|
+
metadata, no per-file method/limitations. Smallest token footprint.
|
|
877
|
+
depth="standard": full per-file detail — imports, relevance scores,
|
|
878
|
+
fan metrics, extraction method. Current v0.33 behavior.
|
|
879
|
+
depth="deep": standard + optional analysis sections (deps, env, git).
|
|
879
880
|
|
|
880
|
-
Never includes:
|
|
881
|
-
|
|
881
|
+
Never includes: file bodies, function implementations, comments, or
|
|
882
|
+
low-signal metadata regardless of depth.
|
|
882
883
|
"""
|
|
883
|
-
|
|
884
|
+
contracts = sm.file_contracts or []
|
|
885
|
+
|
|
886
|
+
if depth == "minimal":
|
|
887
|
+
return _contract_view_minimal(sm, contracts, emit_graph=emit_graph)
|
|
888
|
+
if depth in ("standard", "deep"):
|
|
889
|
+
return _contract_view_standard(sm, contracts, emit_graph=emit_graph,
|
|
890
|
+
include_optional=(depth == "deep"))
|
|
891
|
+
return _contract_view_minimal(sm, contracts, emit_graph=emit_graph)
|
|
892
|
+
|
|
884
893
|
|
|
885
|
-
|
|
894
|
+
# ---------------------------------------------------------------------------
|
|
895
|
+
# Minimal contract renderer — smallest token footprint
|
|
896
|
+
# ---------------------------------------------------------------------------
|
|
897
|
+
|
|
898
|
+
def _contract_view_minimal(
|
|
899
|
+
sm: SourceMap,
|
|
900
|
+
contracts: list[Any],
|
|
901
|
+
*,
|
|
902
|
+
emit_graph: bool = False,
|
|
903
|
+
) -> dict[str, Any]:
|
|
904
|
+
"""Minimal contract: project header + stripped per-file contracts."""
|
|
886
905
|
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
906
|
+
|
|
907
|
+
# Entry point paths only (production)
|
|
908
|
+
ep_paths = sorted({
|
|
909
|
+
ep.path.replace("\\", "/")
|
|
910
|
+
for ep in sm.entry_points
|
|
911
|
+
if is_production_entry_point(ep)
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
project: dict[str, Any] = {"type": sm.project_type}
|
|
915
|
+
if primary:
|
|
916
|
+
project["stack"] = primary.stack
|
|
917
|
+
if primary.frameworks:
|
|
918
|
+
project["frameworks"] = [f.name for f in primary.frameworks]
|
|
919
|
+
if ep_paths:
|
|
920
|
+
project["entry_points"] = ep_paths
|
|
921
|
+
if sm.project_summary:
|
|
922
|
+
project["summary"] = sm.project_summary
|
|
923
|
+
|
|
924
|
+
result: dict[str, Any] = {
|
|
925
|
+
"schema_version": sm.metadata.schema_version,
|
|
926
|
+
"mode": "minimal",
|
|
927
|
+
"project": project,
|
|
890
928
|
}
|
|
929
|
+
|
|
930
|
+
# Per-file contracts
|
|
931
|
+
if contracts:
|
|
932
|
+
serialized: list[dict[str, Any]] = []
|
|
933
|
+
for c in contracts:
|
|
934
|
+
item = _serialize_contract_minimal(c)
|
|
935
|
+
serialized.append(item)
|
|
936
|
+
result["contracts"] = serialized
|
|
937
|
+
|
|
938
|
+
# Optional analysis sections — included when the analyzer explicitly ran
|
|
939
|
+
# (user passed --dependencies, --env-map, --code-notes, --git-context)
|
|
940
|
+
if sm.dependency_summary is not None and sm.dependency_summary.requested:
|
|
941
|
+
dep_dict = asdict(sm.dependency_summary)
|
|
942
|
+
dep_dict.pop("dependencies", None)
|
|
943
|
+
result["dependency_summary"] = dep_dict
|
|
944
|
+
result["key_dependencies"] = [
|
|
945
|
+
{k: v for k, v in asdict(d).items() if v is not None and k != "parent"}
|
|
946
|
+
for d in sm.key_dependencies
|
|
947
|
+
if (d.role or "unknown") in _PRODUCTION_DEP_ROLES and d.scope not in {"dev"}
|
|
948
|
+
]
|
|
949
|
+
|
|
950
|
+
if sm.env_summary is not None and sm.env_summary.requested:
|
|
951
|
+
result["env_summary"] = asdict(sm.env_summary)
|
|
952
|
+
|
|
953
|
+
if sm.code_notes_summary is not None and sm.code_notes_summary.requested:
|
|
954
|
+
result["code_notes_summary"] = asdict(sm.code_notes_summary)
|
|
955
|
+
|
|
956
|
+
if sm.git_context is not None and sm.git_context.requested:
|
|
957
|
+
result["git_context"] = asdict(sm.git_context)
|
|
958
|
+
|
|
959
|
+
# Optional graph (--emit-graph)
|
|
960
|
+
if emit_graph and contracts:
|
|
961
|
+
from sourcecode.contract_pipeline import build_dependency_graph
|
|
962
|
+
result["dependency_graph"] = build_dependency_graph(contracts)
|
|
963
|
+
|
|
964
|
+
# Compact summary
|
|
965
|
+
if sm.contract_summary is not None:
|
|
966
|
+
cs = sm.contract_summary
|
|
967
|
+
degraded = bool(cs.method_breakdown.get("heuristic", 0))
|
|
968
|
+
summary: dict[str, Any] = {
|
|
969
|
+
"files": cs.extracted_files,
|
|
970
|
+
"total": cs.total_files,
|
|
971
|
+
}
|
|
972
|
+
if cs.method_breakdown:
|
|
973
|
+
summary["methods"] = cs.method_breakdown
|
|
974
|
+
if degraded:
|
|
975
|
+
summary["degraded"] = True
|
|
976
|
+
summary["degraded_hint"] = "install sourcecode[ast] for full TS/JS extraction"
|
|
977
|
+
result["summary"] = summary
|
|
978
|
+
|
|
979
|
+
return result
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _split_params(param_str: str) -> list[str]:
|
|
983
|
+
"""Split parameter string at top-level commas."""
|
|
984
|
+
params: list[str] = []
|
|
985
|
+
depth = 0
|
|
986
|
+
current: list[str] = []
|
|
987
|
+
for ch in param_str:
|
|
988
|
+
if ch in "([{":
|
|
989
|
+
depth += 1
|
|
990
|
+
current.append(ch)
|
|
991
|
+
elif ch in ")]}":
|
|
992
|
+
depth -= 1
|
|
993
|
+
current.append(ch)
|
|
994
|
+
elif ch == "," and depth == 0:
|
|
995
|
+
p = "".join(current).strip()
|
|
996
|
+
if p:
|
|
997
|
+
params.append(p)
|
|
998
|
+
current = []
|
|
999
|
+
else:
|
|
1000
|
+
current.append(ch)
|
|
1001
|
+
if current:
|
|
1002
|
+
p = "".join(current).strip()
|
|
1003
|
+
if p:
|
|
1004
|
+
params.append(p)
|
|
1005
|
+
return params
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _strip_param_default(param: str) -> str:
|
|
1009
|
+
"""Remove '= <default>' from a single parameter, keeping type annotation."""
|
|
1010
|
+
depth = 0
|
|
1011
|
+
for i, ch in enumerate(param):
|
|
1012
|
+
if ch in "([{":
|
|
1013
|
+
depth += 1
|
|
1014
|
+
elif ch in ")]}":
|
|
1015
|
+
depth -= 1
|
|
1016
|
+
elif ch == "=" and depth == 0:
|
|
1017
|
+
return param[:i].rstrip()
|
|
1018
|
+
return param
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _compress_sig(name: str, sig: str, max_len: int = 100) -> str:
|
|
1022
|
+
"""Compress a function signature — strip defaults, preserve type annotations."""
|
|
1023
|
+
paren_start = sig.find("(")
|
|
1024
|
+
if paren_start < 0:
|
|
1025
|
+
full = f"{name}{sig}"
|
|
1026
|
+
return full[:max_len - 3] + "..." if len(full) > max_len else full
|
|
1027
|
+
|
|
1028
|
+
# Find matching close paren
|
|
1029
|
+
depth = 0
|
|
1030
|
+
paren_end = -1
|
|
1031
|
+
for i, ch in enumerate(sig[paren_start:], paren_start):
|
|
1032
|
+
if ch == "(":
|
|
1033
|
+
depth += 1
|
|
1034
|
+
elif ch == ")":
|
|
1035
|
+
depth -= 1
|
|
1036
|
+
if depth == 0:
|
|
1037
|
+
paren_end = i
|
|
1038
|
+
break
|
|
1039
|
+
|
|
1040
|
+
if paren_end >= 0:
|
|
1041
|
+
param_str = sig[paren_start + 1:paren_end]
|
|
1042
|
+
ret_str = sig[paren_end + 1:]
|
|
1043
|
+
clean_params = [_strip_param_default(p) for p in _split_params(param_str)]
|
|
1044
|
+
full = f"{name}({', '.join(clean_params)}){ret_str}"
|
|
1045
|
+
else:
|
|
1046
|
+
# Truncated signature (e.g. 2000-char cap hit) — best-effort strip of visible params
|
|
1047
|
+
visible = sig[paren_start + 1:]
|
|
1048
|
+
partial = _split_params(visible)
|
|
1049
|
+
clean_params = [_strip_param_default(p) for p in partial]
|
|
1050
|
+
full = f"{name}({', '.join(clean_params)}"
|
|
1051
|
+
|
|
1052
|
+
if len(full) > max_len:
|
|
1053
|
+
full = full[:max_len - 3] + "..."
|
|
1054
|
+
return full
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _serialize_contract_minimal(c: Any) -> dict[str, Any]:
|
|
1058
|
+
"""Serialize one FileContract to minimal format."""
|
|
1059
|
+
item: dict[str, Any] = {"path": c.path, "role": c.role}
|
|
1060
|
+
|
|
1061
|
+
if c.is_changed:
|
|
1062
|
+
item["changed"] = True
|
|
1063
|
+
|
|
1064
|
+
# Exports: flat string for functions/unknown, {name,k} for others
|
|
1065
|
+
# When all exports are same non-function kind, group them
|
|
1066
|
+
if c.exports:
|
|
1067
|
+
exs: list[Any] = []
|
|
1068
|
+
kinds = {e.kind for e in c.exports}
|
|
1069
|
+
if len(kinds) == 1 and "function" not in kinds and "unknown" not in kinds:
|
|
1070
|
+
# All same non-function kind — compact: {"k": "class", "names": [...]}
|
|
1071
|
+
only_kind = next(iter(kinds))
|
|
1072
|
+
exs = [{"k": only_kind, "names": sorted(e.name for e in c.exports)}]
|
|
1073
|
+
else:
|
|
1074
|
+
for e in sorted(c.exports, key=lambda e: e.name):
|
|
1075
|
+
if e.kind in ("function", "unknown"):
|
|
1076
|
+
exs.append(e.name)
|
|
1077
|
+
else:
|
|
1078
|
+
exs.append({"name": e.name, "k": e.kind})
|
|
1079
|
+
item["exports"] = exs
|
|
1080
|
+
|
|
1081
|
+
# External deps (non-stdlib already filtered in extractor)
|
|
1082
|
+
if c.dependencies:
|
|
1083
|
+
item["deps"] = sorted(c.dependencies)
|
|
1084
|
+
|
|
1085
|
+
# Exported function signatures — compressed
|
|
1086
|
+
exported_names = {e.name for e in c.exports}
|
|
1087
|
+
if c.functions:
|
|
1088
|
+
fns = []
|
|
1089
|
+
for f in sorted(c.functions, key=lambda f: f.name):
|
|
1090
|
+
if not (f.exported or f.name in exported_names):
|
|
1091
|
+
continue
|
|
1092
|
+
fns.append(_compress_sig(f.name, f.signature))
|
|
1093
|
+
if fns:
|
|
1094
|
+
item["fn"] = fns
|
|
1095
|
+
|
|
1096
|
+
# Types: skip if fully covered by exports (avoids duplication in model files)
|
|
1097
|
+
if c.types:
|
|
1098
|
+
export_names_set = {e.name for e in c.exports}
|
|
1099
|
+
non_redundant = [t for t in c.types if t.name not in export_names_set]
|
|
1100
|
+
if non_redundant:
|
|
1101
|
+
item["types"] = [
|
|
1102
|
+
{"name": t.name, "k": t.kind} if t.kind not in ("interface", "class") else t.name
|
|
1103
|
+
for t in sorted(non_redundant, key=lambda t: t.name)
|
|
1104
|
+
]
|
|
1105
|
+
|
|
1106
|
+
# Hooks (TSX/JSX — usually short list)
|
|
1107
|
+
if c.hooks_used:
|
|
1108
|
+
item["hooks"] = c.hooks_used
|
|
1109
|
+
|
|
1110
|
+
return item
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
# ---------------------------------------------------------------------------
|
|
1114
|
+
# Standard contract renderer — full per-file detail (v0.33 behavior)
|
|
1115
|
+
# ---------------------------------------------------------------------------
|
|
1116
|
+
|
|
1117
|
+
def _contract_view_standard(
|
|
1118
|
+
sm: SourceMap,
|
|
1119
|
+
contracts: list[Any],
|
|
1120
|
+
*,
|
|
1121
|
+
emit_graph: bool = False,
|
|
1122
|
+
include_optional: bool = False,
|
|
1123
|
+
) -> dict[str, Any]:
|
|
1124
|
+
"""Standard contract: full per-file detail — mirrors v0.33 output."""
|
|
1125
|
+
from dataclasses import asdict as _asdict
|
|
1126
|
+
|
|
1127
|
+
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
1128
|
+
project: dict[str, Any] = {"type": sm.project_type}
|
|
1129
|
+
if sm.project_summary:
|
|
1130
|
+
project["summary"] = sm.project_summary
|
|
891
1131
|
if primary:
|
|
892
1132
|
project["primary_stack"] = primary.stack
|
|
893
1133
|
if primary.frameworks:
|
|
@@ -898,20 +1138,19 @@ def contract_view(
|
|
|
898
1138
|
ep_groups = _entry_point_groups(sm.entry_points)
|
|
899
1139
|
|
|
900
1140
|
result: dict[str, Any] = {
|
|
901
|
-
# Keep metadata and top-level fields for backward compat with callers
|
|
902
|
-
# that inspect schema_version, stacks, project_type, project_summary.
|
|
903
|
-
"metadata": asdict(sm.metadata),
|
|
904
1141
|
"schema_version": sm.metadata.schema_version,
|
|
905
|
-
"mode": "
|
|
906
|
-
"project_type": sm.project_type,
|
|
907
|
-
"project_summary": sm.project_summary,
|
|
908
|
-
"architecture_summary": sm.architecture_summary,
|
|
1142
|
+
"mode": "standard",
|
|
909
1143
|
"project": project,
|
|
910
|
-
"stacks": [
|
|
1144
|
+
"stacks": [
|
|
1145
|
+
{"stack": s.stack, "primary": s.primary,
|
|
1146
|
+
"frameworks": [f.name for f in (s.frameworks or [])],
|
|
1147
|
+
"package_manager": s.package_manager}
|
|
1148
|
+
for s in sm.stacks
|
|
1149
|
+
],
|
|
911
1150
|
"entry_points": ep_groups["production"],
|
|
912
1151
|
}
|
|
913
|
-
|
|
914
|
-
|
|
1152
|
+
if ep_groups["development"]:
|
|
1153
|
+
result["development_entry_points"] = ep_groups["development"]
|
|
915
1154
|
|
|
916
1155
|
if sm.confidence_summary is not None:
|
|
917
1156
|
result["confidence"] = {
|
|
@@ -919,8 +1158,7 @@ def contract_view(
|
|
|
919
1158
|
"stack": sm.confidence_summary.stack_confidence,
|
|
920
1159
|
}
|
|
921
1160
|
|
|
922
|
-
#
|
|
923
|
-
contracts = sm.file_contracts # list[FileContract]
|
|
1161
|
+
# Per-file contracts (full detail)
|
|
924
1162
|
if contracts:
|
|
925
1163
|
serialized: list[dict[str, Any]] = []
|
|
926
1164
|
for c in contracts:
|
|
@@ -939,22 +1177,26 @@ def contract_view(
|
|
|
939
1177
|
item["is_changed"] = True
|
|
940
1178
|
if c.exports:
|
|
941
1179
|
item["exports"] = [
|
|
942
|
-
{k: v for k, v in _asdict(e).items()
|
|
1180
|
+
{k: v for k, v in _asdict(e).items()
|
|
1181
|
+
if v is not None and v is not False and v != "unknown"}
|
|
943
1182
|
for e in c.exports
|
|
944
1183
|
]
|
|
945
1184
|
if c.imports:
|
|
946
1185
|
item["imports"] = [
|
|
947
|
-
{
|
|
1186
|
+
{"source": i.source, "symbols": i.symbols}
|
|
1187
|
+
if i.symbols else {"source": i.source}
|
|
948
1188
|
for i in c.imports
|
|
949
1189
|
]
|
|
950
1190
|
if c.functions:
|
|
951
1191
|
item["functions"] = [
|
|
952
|
-
{k: v for k, v in _asdict(f).items()
|
|
1192
|
+
{k: v for k, v in _asdict(f).items()
|
|
1193
|
+
if v is not None and v is not False and v != []}
|
|
953
1194
|
for f in c.functions
|
|
954
1195
|
]
|
|
955
1196
|
if c.types:
|
|
956
1197
|
item["types"] = [
|
|
957
|
-
{k: v for k, v in _asdict(t).items()
|
|
1198
|
+
{k: v for k, v in _asdict(t).items()
|
|
1199
|
+
if v is not None and v != [] and v != "unknown"}
|
|
958
1200
|
for t in c.types
|
|
959
1201
|
]
|
|
960
1202
|
if c.hooks_used:
|
|
@@ -967,35 +1209,28 @@ def contract_view(
|
|
|
967
1209
|
serialized.append(item)
|
|
968
1210
|
result["file_contracts"] = serialized
|
|
969
1211
|
|
|
970
|
-
#
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
if sm.code_notes_summary is not None and sm.code_notes_summary.requested:
|
|
988
|
-
result["code_notes_summary"] = asdict(sm.code_notes_summary)
|
|
989
|
-
|
|
990
|
-
if sm.git_context is not None and sm.git_context.requested:
|
|
991
|
-
result["git_context"] = asdict(sm.git_context)
|
|
1212
|
+
# Optional analysis sections (deep mode or when analyzers ran)
|
|
1213
|
+
if include_optional:
|
|
1214
|
+
if sm.dependency_summary is not None and sm.dependency_summary.requested:
|
|
1215
|
+
dep_dict = asdict(sm.dependency_summary)
|
|
1216
|
+
dep_dict.pop("dependencies", None)
|
|
1217
|
+
result["dependency_summary"] = dep_dict
|
|
1218
|
+
result["key_dependencies"] = [
|
|
1219
|
+
{k: v for k, v in asdict(d).items() if v is not None and k != "parent"}
|
|
1220
|
+
for d in sm.key_dependencies
|
|
1221
|
+
if (d.role or "unknown") in _PRODUCTION_DEP_ROLES and d.scope not in {"dev"}
|
|
1222
|
+
]
|
|
1223
|
+
if sm.env_summary is not None and sm.env_summary.requested:
|
|
1224
|
+
result["env_summary"] = asdict(sm.env_summary)
|
|
1225
|
+
if sm.code_notes_summary is not None and sm.code_notes_summary.requested:
|
|
1226
|
+
result["code_notes_summary"] = asdict(sm.code_notes_summary)
|
|
1227
|
+
if sm.git_context is not None and sm.git_context.requested:
|
|
1228
|
+
result["git_context"] = asdict(sm.git_context)
|
|
992
1229
|
|
|
993
|
-
# ── Dependency graph ─────────────────────────────────────────────────────
|
|
994
1230
|
if emit_graph and contracts:
|
|
995
1231
|
from sourcecode.contract_pipeline import build_dependency_graph
|
|
996
1232
|
result["dependency_graph"] = build_dependency_graph(contracts)
|
|
997
1233
|
|
|
998
|
-
# ── Contract summary ─────────────────────────────────────────────────────
|
|
999
1234
|
if sm.contract_summary is not None:
|
|
1000
1235
|
cs = sm.contract_summary
|
|
1001
1236
|
result["contract_summary"] = {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=fYdoUeHpMCvt6KgjSCq7EwaVElY_1iqG4Ugp2NKdgxU,103
|
|
2
2
|
sourcecode/architecture_analyzer.py,sha256=H6noGgVArUJ25z1qC0fFA0KvJJeHZYyhKvKSkOyWHUk,23096
|
|
3
3
|
sourcecode/architecture_summary.py,sha256=rSY5MRiaz4N1YdG0pqDTDuFjSN7PO_Zplx-dtNzv2Yo,19985
|
|
4
|
-
sourcecode/ast_extractor.py,sha256=
|
|
4
|
+
sourcecode/ast_extractor.py,sha256=qiYWdXXdKpTVqTt2fRIHrkZJUTnpHPvPJKYchV_dnWE,39219
|
|
5
5
|
sourcecode/classifier.py,sha256=GKTMN8qKZX7ponSwDJfN08RrasI4CVpq1_gFBgEopps,7093
|
|
6
|
-
sourcecode/cli.py,sha256=
|
|
6
|
+
sourcecode/cli.py,sha256=O1ObfcxvhMYMXjd6otx6G0fE9ethIAX4qDUpUUjOxgY,63167
|
|
7
7
|
sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
|
|
8
8
|
sourcecode/confidence_analyzer.py,sha256=HxJMPLI5ulqtkncnv98W4iVO6yMbpQo87VuxiuNbDmY,12167
|
|
9
9
|
sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0FlQ,6503
|
|
@@ -20,12 +20,12 @@ sourcecode/graph_analyzer.py,sha256=hMOsLLz9B0UnQ4xwbHdgr3bFvqpw0bQ8kN-xmEn3Krk,
|
|
|
20
20
|
sourcecode/metrics_analyzer.py,sha256=4uh11v-Q0gdrN87BOxuFWUym3N3AOkOuy21K5N8peB8,20126
|
|
21
21
|
sourcecode/prepare_context.py,sha256=vxEzr8czS3MFbdTx4hBJQlJLrl9cuvbHdL3ZokxFkvo,31384
|
|
22
22
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
23
|
-
sourcecode/relevance_scorer.py,sha256=
|
|
23
|
+
sourcecode/relevance_scorer.py,sha256=w-vOYcTWB-c2OAi8F5-y16ZB1QMPPtvxUNJnMrzGKCo,7470
|
|
24
24
|
sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
|
|
25
25
|
sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
|
|
26
26
|
sourcecode/schema.py,sha256=AShu_bcP30TYaw4Dl1nYy8aFnBCKxrUli3LhU3MZTjs,20739
|
|
27
27
|
sourcecode/semantic_analyzer.py,sha256=asQfJf-EhzYaOTA-iMuZsrVXtbW7SV2WEKCxgsxa88Y,79413
|
|
28
|
-
sourcecode/serializer.py,sha256=
|
|
28
|
+
sourcecode/serializer.py,sha256=en11Au-YhpO1hYPugbCCPNvHkqBiTSRuGVoiLxDAKeQ,49888
|
|
29
29
|
sourcecode/summarizer.py,sha256=ZuzIdm3t8A-d5MuQL0TSNLrd-L0IQIuguIxeNXMNJf8,16070
|
|
30
30
|
sourcecode/tree_utils.py,sha256=Fj9OIuUksBvgibNd3feog0sMDjVypJzPexp5lvMoYWI,1424
|
|
31
31
|
sourcecode/workspace.py,sha256=fQlVoNx8S-fSHpKoJ0JBvEHCFkxszH0KZVJed1i3TRk,6845
|
|
@@ -56,8 +56,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
56
56
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
57
57
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
58
58
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
59
|
-
sourcecode-0.
|
|
60
|
-
sourcecode-0.
|
|
61
|
-
sourcecode-0.
|
|
62
|
-
sourcecode-0.
|
|
63
|
-
sourcecode-0.
|
|
59
|
+
sourcecode-0.34.0.dist-info/METADATA,sha256=Xg-K2zPoLVH0BfwsjhgqyFyVamkBsPfp6o8mxZdl-HY,25209
|
|
60
|
+
sourcecode-0.34.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
61
|
+
sourcecode-0.34.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
62
|
+
sourcecode-0.34.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
63
|
+
sourcecode-0.34.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|