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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "0.33.0"
3
+ __version__ = "0.34.0"
@@ -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
- # Truncate runaway signatures (e.g. typer.Option() defaults)
733
- if len(sig) > 300:
734
- sig = sig[:297] + "..."
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 semantic contracts — exports, signatures, types, imports. No bodies. "
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 mode is the recommended default for AI coding agents."
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 == "contract" and _legacy_flags_active:
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
- if mode in ("contract", "hybrid"):
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 mode in ("contract", "hybrid"):
1270
+ if _is_contract_mode:
1253
1271
  from sourcecode.serializer import contract_view as _contract_view
1254
- data = _contract_view(sm, emit_graph=emit_graph)
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)
@@ -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.1,
128
- "docs_layer": -0.15,
129
- "test_layer": 0.05,
130
- "benchmark_layer": -0.2,
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
- # Penalize auxiliary dirs
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 context + per-file semantic contracts.
873
+ """Contract-mode output: project header + per-file semantic contracts.
873
874
 
874
- Emits:
875
- - Project identity (stacks, entry_points, summary)
876
- - Per-file FileContracts ranked by relevance
877
- - Optional dependency graph (--emit-graph)
878
- - ContractSummary
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: raw file contents, function bodies, implementation logic,
881
- comments, formatting, or low-signal metadata.
881
+ Never includes: file bodies, function implementations, comments, or
882
+ low-signal metadata regardless of depth.
882
883
  """
883
- from dataclasses import asdict as _asdict
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
- # ── Project context (compact) ────────────────────────────────────────────
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
- project: dict[str, Any] = {
888
- "type": sm.project_type,
889
- "summary": sm.project_summary,
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": "contract",
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": [asdict(s) for s in sm.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
- result["development_entry_points"] = ep_groups["development"]
914
- result["auxiliary_entry_points"] = ep_groups["auxiliary"]
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
- # ── File contracts ───────────────────────────────────────────────────────
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() if v is not None and v is not False and v != "unknown"}
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
- {k: v for k, v in _asdict(i).items() if v is not None and v != [] and k != "kind" or v not in ("named", "side_effect")}
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() if v is not None and v is not False and v != []}
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() if v is not None and v != [] and v != "unknown"}
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
- # ── Optional analysis sections (pass-through when analyzers ran) ─────────
971
- # Include summary-level data from optional analyzers so that callers using
972
- # --dependencies, --env-map, --code-notes alongside --mode contract
973
- # still receive the expected keys in the output.
974
- if sm.dependency_summary is not None and sm.dependency_summary.requested:
975
- dep_dict = asdict(sm.dependency_summary)
976
- dep_dict.pop("dependencies", None)
977
- result["dependency_summary"] = dep_dict
978
- result["key_dependencies"] = [
979
- {k: v for k, v in asdict(d).items() if v is not None and k != "parent"}
980
- for d in sm.key_dependencies
981
- if (d.role or "unknown") in _PRODUCTION_DEP_ROLES and d.scope not in {"dev"}
982
- ]
983
-
984
- if sm.env_summary is not None and sm.env_summary.requested:
985
- result["env_summary"] = asdict(sm.env_summary)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.33.0
3
+ Version: 0.34.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -1,9 +1,9 @@
1
- sourcecode/__init__.py,sha256=WPrr-Lw7JGDFbBhRr-n0AhpquPRsep-3Xx2oy1zTpQE,103
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=KrxFpegjS_rWdBrdckx7smDly1UYtxxQtzoxkUt1DuE,36293
4
+ sourcecode/ast_extractor.py,sha256=qiYWdXXdKpTVqTt2fRIHrkZJUTnpHPvPJKYchV_dnWE,39219
5
5
  sourcecode/classifier.py,sha256=GKTMN8qKZX7ponSwDJfN08RrasI4CVpq1_gFBgEopps,7093
6
- sourcecode/cli.py,sha256=C2Ix8dtV63eRP670nJWiafTj4PASyDBFzDzoAxLTV2I,62186
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=2yvxDFnz9YGrHEJubgx9soiVIDZHKv_pntOtTARtKow,5928
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=55Prw0BhSC3CD0IAuhpvhEHaTuvLu38Rb1iRf89g0wU,42169
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.33.0.dist-info/METADATA,sha256=5XU8eGZ_tBs3f4xl5PW9FUU4ciT1ZovvbVjDnf7Q-50,25209
60
- sourcecode-0.33.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
61
- sourcecode-0.33.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
62
- sourcecode-0.33.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
63
- sourcecode-0.33.0.dist-info/RECORD,,
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,,