seam-code 0.3.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.
Files changed (109) hide show
  1. seam/__init__.py +3 -0
  2. seam/_data/schema.sql +225 -0
  3. seam/_web/assets/index-BL_tqprR.js +216 -0
  4. seam/_web/assets/index-GTKUhVyD.css +1 -0
  5. seam/_web/index.html +13 -0
  6. seam/analysis/__init__.py +14 -0
  7. seam/analysis/affected.py +254 -0
  8. seam/analysis/builtins.py +966 -0
  9. seam/analysis/byte_budget.py +217 -0
  10. seam/analysis/changes.py +709 -0
  11. seam/analysis/cluster_naming.py +260 -0
  12. seam/analysis/clustering.py +216 -0
  13. seam/analysis/confidence.py +699 -0
  14. seam/analysis/embeddings.py +195 -0
  15. seam/analysis/flows.py +708 -0
  16. seam/analysis/impact.py +444 -0
  17. seam/analysis/imports.py +994 -0
  18. seam/analysis/imports_ext.py +780 -0
  19. seam/analysis/imports_resolve.py +176 -0
  20. seam/analysis/processes.py +453 -0
  21. seam/analysis/relevance.py +155 -0
  22. seam/analysis/rwr.py +129 -0
  23. seam/analysis/staleness.py +328 -0
  24. seam/analysis/steer.py +282 -0
  25. seam/analysis/synthesis.py +253 -0
  26. seam/analysis/synthesis_channels.py +433 -0
  27. seam/analysis/testpaths.py +103 -0
  28. seam/analysis/traversal.py +470 -0
  29. seam/cli/__init__.py +0 -0
  30. seam/cli/install.py +232 -0
  31. seam/cli/main.py +2602 -0
  32. seam/cli/output.py +137 -0
  33. seam/cli/read.py +244 -0
  34. seam/cli/serve.py +145 -0
  35. seam/config.py +551 -0
  36. seam/indexer/__init__.py +0 -0
  37. seam/indexer/cluster_index.py +425 -0
  38. seam/indexer/db.py +496 -0
  39. seam/indexer/embedding_index.py +183 -0
  40. seam/indexer/field_access.py +536 -0
  41. seam/indexer/field_access_c_cpp.py +643 -0
  42. seam/indexer/field_access_ext.py +708 -0
  43. seam/indexer/field_access_ext2.py +408 -0
  44. seam/indexer/field_access_go_rust.py +737 -0
  45. seam/indexer/field_access_php_swift.py +888 -0
  46. seam/indexer/field_access_ts.py +626 -0
  47. seam/indexer/graph.py +321 -0
  48. seam/indexer/graph_c.py +562 -0
  49. seam/indexer/graph_c_cpp.py +39 -0
  50. seam/indexer/graph_common.py +644 -0
  51. seam/indexer/graph_cpp.py +615 -0
  52. seam/indexer/graph_csharp.py +651 -0
  53. seam/indexer/graph_go.py +723 -0
  54. seam/indexer/graph_go_rust.py +39 -0
  55. seam/indexer/graph_java.py +689 -0
  56. seam/indexer/graph_java_csharp.py +38 -0
  57. seam/indexer/graph_php.py +914 -0
  58. seam/indexer/graph_python.py +628 -0
  59. seam/indexer/graph_ruby.py +748 -0
  60. seam/indexer/graph_rust.py +653 -0
  61. seam/indexer/graph_scope_infer.py +902 -0
  62. seam/indexer/graph_scope_infer_ext.py +723 -0
  63. seam/indexer/graph_scope_infer_ext2.py +992 -0
  64. seam/indexer/graph_swift.py +1014 -0
  65. seam/indexer/graph_swift_infer.py +515 -0
  66. seam/indexer/graph_typescript.py +663 -0
  67. seam/indexer/migrations.py +816 -0
  68. seam/indexer/parser.py +204 -0
  69. seam/indexer/pipeline.py +197 -0
  70. seam/indexer/signatures.py +634 -0
  71. seam/indexer/signatures_ext.py +780 -0
  72. seam/indexer/sync.py +287 -0
  73. seam/indexer/synthesis_index.py +291 -0
  74. seam/indexer/tokenize.py +79 -0
  75. seam/installer/__init__.py +67 -0
  76. seam/installer/claude.py +97 -0
  77. seam/installer/codex.py +94 -0
  78. seam/installer/core.py +127 -0
  79. seam/installer/cursor.py +61 -0
  80. seam/installer/guide.py +110 -0
  81. seam/installer/jsonfile.py +85 -0
  82. seam/installer/markdownfile.py +146 -0
  83. seam/installer/tomlfile.py +72 -0
  84. seam/query/__init__.py +0 -0
  85. seam/query/clusters.py +206 -0
  86. seam/query/comments.py +217 -0
  87. seam/query/context.py +293 -0
  88. seam/query/engine.py +940 -0
  89. seam/query/fts.py +328 -0
  90. seam/query/names.py +470 -0
  91. seam/query/pack.py +433 -0
  92. seam/query/semantic.py +339 -0
  93. seam/query/structure.py +727 -0
  94. seam/server/__init__.py +0 -0
  95. seam/server/graph_api.py +437 -0
  96. seam/server/handler_common.py +323 -0
  97. seam/server/impact_handler.py +615 -0
  98. seam/server/mcp.py +556 -0
  99. seam/server/tools.py +697 -0
  100. seam/server/trace_handler.py +184 -0
  101. seam/server/web.py +922 -0
  102. seam/watcher/__init__.py +0 -0
  103. seam/watcher/__main__.py +56 -0
  104. seam/watcher/daemon.py +237 -0
  105. seam_code-0.3.0.dist-info/METADATA +318 -0
  106. seam_code-0.3.0.dist-info/RECORD +109 -0
  107. seam_code-0.3.0.dist-info/WHEEL +4 -0
  108. seam_code-0.3.0.dist-info/entry_points.txt +2 -0
  109. seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,217 @@
1
+ """Byte-ceiling trimmer for seam_impact output (E1-FULL).
2
+
3
+ LEAF MODULE — pure functions over plain dicts. Imports only stdlib (json, typing).
4
+ No database access, no config import, no IO, never raises.
5
+ Mirrors the leaf discipline of seam/analysis/relevance.py.
6
+
7
+ WHY this module exists (the usability gap it closes):
8
+ seam_impact output size is bounded by SEAM_IMPACT_MAX_RESULTS (an entry-count cap),
9
+ but entry count is a poor proxy for byte size: a tier of 25 entries with long
10
+ signatures and qualified names can be many kilobytes, while 25 short entries is tiny.
11
+ AI agents budget their context window in tokens (≈ characters), not entry counts, so
12
+ the count cap cannot guarantee the output fits a context budget.
13
+
14
+ fit_to_byte_budget() receives an already-assembled seam_impact response dict (after
15
+ the per-tier count cap and E2/E3 relevance ordering have already run) and trims entries
16
+ from the LEAST-VALUABLE end of a global priority order until the serialized response
17
+ fits the budget. Because entries are already ordered by priority (externals before
18
+ self-refs, production before test, nearest tier first), the survivors are guaranteed
19
+ the highest-signal dependents that fit.
20
+
21
+ Priority order (highest to lowest — front entries survive, tail entries are dropped):
22
+ 1. direction: upstream before downstream (what calls me? is more valuable than what I call)
23
+ 2. tier risk: WILL_BREAK → LIKELY_AFFECTED → MAY_NEED_TESTING
24
+ 3. intra-tier: existing E2/E3 entry order preserved (front entries survive)
25
+
26
+ Conservatism contract:
27
+ - NEVER exceed the budget for the trimmed response body — `running` is a proven UPPER
28
+ bound on the real serialized size (see the algorithm note), so `running <= budget`
29
+ guarantees `serialized_size(trimmed) <= budget`. The ONLY case the body can exceed
30
+ budget is when the budget is smaller than the irreducible envelope (no entries can
31
+ fit) — then every entry is dropped and the bare envelope is returned.
32
+ - NEVER raise on any input — the public entry point wraps the whole body and returns
33
+ (response, {}, 0) on failure (no trimming is safer than a crash; the handler degrades).
34
+ - NEVER mutate the input response dict.
35
+ - Non-direction keys (found, target, risk_summary, truncated, etc.) are ALWAYS copied
36
+ through unchanged — trimming touches only direction-tier entry lists.
37
+ - Budget <= 0 means unlimited — return input unchanged immediately.
38
+
39
+ Unit = characters of json.dumps(obj, ensure_ascii=False) — the SAME serialization the
40
+ CLI uses to emit results (seam/cli/output.py emit_json). Measuring with the emit
41
+ serializer means the budget bounds the actual rendered bytes, not a more-compact proxy
42
+ that would let the real output overrun the ceiling. This is a deterministic,
43
+ dependency-free token proxy (~4 chars/token); no tokenizer is used (a real tokenizer
44
+ would be an external model-specific dep violating zero-external-services).
45
+
46
+ Algorithm note (O(n) running-total prefix):
47
+ Entries are walked in keep-priority order. Each entry is charged its own serialized
48
+ size PLUS one separator char (the comma that joins it to its siblings). The running
49
+ total starts from the envelope size (the response with all tier lists emptied) and
50
+ accumulates per kept entry. The +1-per-entry charge is a deliberate OVER-estimate:
51
+ the first entry in a tier has no preceding comma, so `running` is always >= the true
52
+ serialized size — which is exactly what makes `running <= budget` a hard guarantee.
53
+ The first entry that would overflow stops all further placement (stop-at-first-overflow
54
+ prefix): every entry before position k is kept, every entry at or after k is dropped.
55
+ This is O(n) (no full re-serialization per placement), so it stays cheap even on the
56
+ documented limit=0 / unbounded blast-radius path.
57
+ """
58
+
59
+ import json
60
+ from typing import Any
61
+
62
+
63
+ def serialized_size(obj: Any) -> int:
64
+ """Return the character count of obj under the CLI emit serialization.
65
+
66
+ Matches seam/cli/output.py emit_json (json.dumps with default separators and
67
+ ensure_ascii=False) so the measured budget reflects the bytes actually rendered.
68
+ Exported as the single source of truth — the handler and tests measure with this
69
+ same function so budget arithmetic cannot drift between layers.
70
+ """
71
+ return len(json.dumps(obj, ensure_ascii=False))
72
+
73
+
74
+ def fit_to_byte_budget(
75
+ response: dict[str, Any],
76
+ *,
77
+ budget: int,
78
+ direction_order: tuple[str, ...] = ("upstream", "downstream"),
79
+ tier_order: tuple[str, ...] = ("WILL_BREAK", "LIKELY_AFFECTED", "MAY_NEED_TESTING"),
80
+ ) -> tuple[dict[str, Any], dict[str, dict[str, int]], int]:
81
+ """Trim seam_impact entries to fit within a byte budget.
82
+
83
+ Args:
84
+ response: The fully-assembled seam_impact response dict. NOT mutated.
85
+ budget: Maximum serialized size in characters. 0 or negative = unlimited
86
+ (return the input unchanged, no trimming).
87
+ direction_order: Keep-priority for directions (first = highest priority).
88
+ Default: upstream before downstream.
89
+ tier_order: Keep-priority for tiers within each direction (first = highest).
90
+ Default: WILL_BREAK → LIKELY_AFFECTED → MAY_NEED_TESTING.
91
+
92
+ Returns:
93
+ A 3-tuple:
94
+ trimmed_response — a NEW dict (input never mutated). Non-direction keys
95
+ (found, target, risk_summary, truncated, etc.) are copied
96
+ through unchanged. Direction-tier lists may be shorter.
97
+ byte_dropped — {direction: {tier: count}} of entries this function removed.
98
+ Only non-zero counts and non-empty direction dicts are included.
99
+ total_omitted — sum of all counts in byte_dropped (0 = ceiling did not fire /
100
+ everything fit).
101
+
102
+ Never raises. On any exception returns (response, {}, 0).
103
+ """
104
+ # Budget 0 or negative = unlimited. Return unchanged immediately (byte-identical path).
105
+ if budget <= 0:
106
+ return response, {}, 0
107
+
108
+ try:
109
+ return _fit_to_byte_budget_impl(response, budget, direction_order, tier_order)
110
+ except Exception:
111
+ # Safety net: return input unchanged on any failure (no trimming > crash).
112
+ # The handler detects this degradation (it knew the response did NOT fit yet
113
+ # nothing was dropped) and logs it, so the silent path is observable upstream.
114
+ return response, {}, 0
115
+
116
+
117
+ def _collect_entry_walk(
118
+ response: dict[str, Any],
119
+ direction_order: tuple[str, ...],
120
+ tier_order: tuple[str, ...],
121
+ ) -> list[tuple[str, str, Any]]:
122
+ """Build the priority-ordered flat walk of (direction, tier, entry) triples.
123
+
124
+ Only includes entries from directions/tiers that are BOTH present in the response
125
+ AND have a list value (malformed non-list tier values are skipped here; they are
126
+ copied through in the envelope, never trimmed).
127
+
128
+ The walk order is the keep-priority order: for direction in direction_order, for
129
+ tier in tier_order, for entry in that tier's list — earlier triples are more
130
+ valuable and survive trimming.
131
+ """
132
+ walk: list[tuple[str, str, Any]] = []
133
+ for direction in direction_order:
134
+ dir_group = response.get(direction)
135
+ if not isinstance(dir_group, dict):
136
+ continue
137
+ for tier in tier_order:
138
+ tier_val = dir_group.get(tier)
139
+ if not isinstance(tier_val, list):
140
+ continue
141
+ for entry in tier_val:
142
+ walk.append((direction, tier, entry))
143
+ return walk
144
+
145
+
146
+ def _build_envelope(
147
+ response: dict[str, Any],
148
+ direction_order: tuple[str, ...],
149
+ ) -> dict[str, Any]:
150
+ """Build a response copy with all direction-tier lists EMPTIED.
151
+
152
+ Non-direction keys and direction groups present in the original are preserved in
153
+ structure; only the entry lists are set to []. This gives the 'overhead' size that
154
+ any kept entry must fit on top of. Malformed tier values (non-list) are copied
155
+ through unchanged (they add to overhead but cannot be trimmed — conservatism).
156
+ """
157
+ envelope: dict[str, Any] = {}
158
+ for key, value in response.items():
159
+ if key in direction_order and isinstance(value, dict):
160
+ dir_copy: dict[str, Any] = {}
161
+ for tier_key, tier_val in value.items():
162
+ dir_copy[tier_key] = [] if isinstance(tier_val, list) else tier_val
163
+ envelope[key] = dir_copy
164
+ else:
165
+ envelope[key] = value
166
+ return envelope
167
+
168
+
169
+ def _fit_to_byte_budget_impl(
170
+ response: dict[str, Any],
171
+ budget: int,
172
+ direction_order: tuple[str, ...],
173
+ tier_order: tuple[str, ...],
174
+ ) -> tuple[dict[str, Any], dict[str, dict[str, int]], int]:
175
+ """Inner implementation — only called when budget > 0. Caller wraps for never-raises."""
176
+ # Fast path: the full response already fits.
177
+ if serialized_size(response) <= budget:
178
+ return response, {}, 0
179
+
180
+ walk = _collect_entry_walk(response, direction_order, tier_order)
181
+ # Running total starts at the envelope size (overhead with all tier lists empty).
182
+ running = serialized_size(_build_envelope(response, direction_order))
183
+
184
+ kept: dict[str, dict[str, list[Any]]] = {}
185
+ dropped: dict[str, dict[str, int]] = {}
186
+ overflow = False # once True, all remaining (lower-priority) entries are dropped
187
+
188
+ for direction, tier, entry in walk:
189
+ if not overflow:
190
+ # +1 charges the comma that joins this entry to its siblings. The first
191
+ # entry in a tier has no preceding comma, so this OVER-estimates — which is
192
+ # what makes `running <= budget` a hard upper bound on the real size.
193
+ cost = serialized_size(entry) + 1
194
+ if running + cost <= budget:
195
+ kept.setdefault(direction, {}).setdefault(tier, []).append(entry)
196
+ running += cost
197
+ continue
198
+ overflow = True
199
+ dir_dropped = dropped.setdefault(direction, {})
200
+ dir_dropped[tier] = dir_dropped.get(tier, 0) + 1
201
+
202
+ # Build the trimmed response: a fresh empty-list envelope with the kept lists filled.
203
+ # kept holds the SAME entry-dict references as the input (never copied, never mutated).
204
+ trimmed = _build_envelope(response, direction_order)
205
+ for direction, tier_map in kept.items():
206
+ for tier, entries in tier_map.items():
207
+ trimmed[direction][tier] = entries
208
+
209
+ # byte_dropped: drop zero counts and empty direction dicts.
210
+ byte_dropped: dict[str, dict[str, int]] = {}
211
+ for direction, count_map in dropped.items():
212
+ filtered = {tier: cnt for tier, cnt in count_map.items() if cnt > 0}
213
+ if filtered:
214
+ byte_dropped[direction] = filtered
215
+ total_omitted = sum(cnt for count_map in byte_dropped.values() for cnt in count_map.values())
216
+
217
+ return trimmed, byte_dropped, total_omitted