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
seam/indexer/parser.py ADDED
@@ -0,0 +1,204 @@
1
+ """Tree-sitter parsing layer — one function per supported language.
2
+
3
+ Returns raw tree-sitter root Nodes for graph.py to interpret.
4
+ Never raises on parse errors; returns None instead.
5
+
6
+ Guards applied before parsing:
7
+ 1. File size > SEAM_MAX_FILE_BYTES → None
8
+ 2. Binary file (null byte in first 1KB) → None
9
+ 3. Any read / OS error → None
10
+ 4. broad Exception backstop → None
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+ import tree_sitter_c as tsc
16
+ import tree_sitter_c_sharp as tscsharp
17
+ import tree_sitter_cpp as tscpp
18
+ import tree_sitter_go as tsgo
19
+ import tree_sitter_java as tsjava
20
+ import tree_sitter_php as tsphp
21
+ import tree_sitter_python as tspython
22
+ import tree_sitter_ruby as tsruby
23
+ import tree_sitter_rust as tsrust
24
+ import tree_sitter_swift as tsswift
25
+ import tree_sitter_typescript as tstypescript
26
+ from tree_sitter import Language, Node, Parser
27
+
28
+ import seam.config as config
29
+
30
+ # Build Language objects once at module level (cheap singletons).
31
+ # Phase 9: six new grammars added alongside the original five.
32
+ _PY_LANG = Language(tspython.language())
33
+ _TS_LANG = Language(tstypescript.language_typescript())
34
+ # TSX grammar is a superset of JS+JSX — used for .js/.mjs/.cjs (no separate JS dep)
35
+ _TSX_LANG = Language(tstypescript.language_tsx())
36
+ _GO_LANG = Language(tsgo.language())
37
+ _RUST_LANG = Language(tsrust.language())
38
+ # Phase 9 grammars
39
+ _JAVA_LANG = Language(tsjava.language())
40
+ _CSHARP_LANG = Language(tscsharp.language())
41
+ _RUBY_LANG = Language(tsruby.language())
42
+ _C_LANG = Language(tsc.language())
43
+ _CPP_LANG = Language(tscpp.language())
44
+ # PHP: language_php() (not language()) to handle the <?php open tag correctly.
45
+ _PHP_LANG = Language(tsphp.language_php())
46
+ # Phase 10 — Swift
47
+ _SWIFT_LANG = Language(tsswift.language())
48
+
49
+
50
+ def _parse(path: Path, language: Language) -> Node | None:
51
+ """Internal helper: guard checks then parse with tree-sitter.
52
+
53
+ Returns the root_node of the parsed tree, or None if the file should
54
+ be skipped. Never raises — the outer except is the final backstop.
55
+ """
56
+ try:
57
+ # Guard 1: file size
58
+ try:
59
+ file_size = path.stat().st_size
60
+ except OSError:
61
+ return None # file does not exist or unreadable
62
+
63
+ if file_size > config.SEAM_MAX_FILE_BYTES:
64
+ return None
65
+
66
+ # Guard 2: binary check — read first 1 KB and look for null byte
67
+ try:
68
+ with path.open("rb") as fh:
69
+ header = fh.read(1024)
70
+ except OSError:
71
+ return None
72
+
73
+ if b"\x00" in header:
74
+ return None # binary file, skip gracefully
75
+
76
+ # Guard 3: read full content
77
+ try:
78
+ source_bytes = path.read_bytes()
79
+ except OSError:
80
+ return None
81
+
82
+ # Parse — tree-sitter never raises on syntax errors, returns ERROR nodes
83
+ parser = Parser(language)
84
+ tree = parser.parse(source_bytes)
85
+ return tree.root_node
86
+
87
+ except Exception: # noqa: BLE001 — broad backstop so parsers never raise
88
+ return None
89
+
90
+
91
+ def parse_python(path: Path) -> Node | None:
92
+ """Parse a Python source file.
93
+
94
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
95
+ Malformed Python still returns a (possibly partial) tree with ERROR nodes.
96
+ """
97
+ return _parse(path, _PY_LANG)
98
+
99
+
100
+ def parse_typescript(path: Path) -> Node | None:
101
+ """Parse a TypeScript (.ts / .tsx) source file.
102
+
103
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
104
+ """
105
+ return _parse(path, _TS_LANG)
106
+
107
+
108
+ def parse_javascript(path: Path) -> Node | None:
109
+ """Parse a JavaScript (.js / .mjs / .cjs) source file using the TSX grammar.
110
+
111
+ tree-sitter-typescript's TSX grammar is a superset that covers JS+JSX.
112
+ No separate JS grammar is needed; this is a deliberate Phase 0 decision.
113
+ See lessons.md: '2026-06-01 — JavaScript parsed via the TSX grammar'.
114
+ """
115
+ return _parse(path, _TSX_LANG)
116
+
117
+
118
+ def parse_go(path: Path) -> Node | None:
119
+ """Parse a Go source file (.go).
120
+
121
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
122
+ Malformed Go still returns a (possibly partial) tree with ERROR nodes.
123
+ """
124
+ return _parse(path, _GO_LANG)
125
+
126
+
127
+ def parse_rust(path: Path) -> Node | None:
128
+ """Parse a Rust source file (.rs).
129
+
130
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
131
+ Malformed Rust still returns a (possibly partial) tree with ERROR nodes.
132
+ """
133
+ return _parse(path, _RUST_LANG)
134
+
135
+
136
+ # ── Phase 9 parsers ────────────────────────────────────────────────────────────
137
+
138
+
139
+ def parse_java(path: Path) -> Node | None:
140
+ """Parse a Java source file (.java).
141
+
142
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
143
+ Malformed Java still returns a (possibly partial) tree with ERROR nodes.
144
+ """
145
+ return _parse(path, _JAVA_LANG)
146
+
147
+
148
+ def parse_csharp(path: Path) -> Node | None:
149
+ """Parse a C# source file (.cs).
150
+
151
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
152
+ Malformed C# still returns a (possibly partial) tree with ERROR nodes.
153
+ """
154
+ return _parse(path, _CSHARP_LANG)
155
+
156
+
157
+ def parse_ruby(path: Path) -> Node | None:
158
+ """Parse a Ruby source file (.rb).
159
+
160
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
161
+ Malformed Ruby still returns a (possibly partial) tree with ERROR nodes.
162
+ """
163
+ return _parse(path, _RUBY_LANG)
164
+
165
+
166
+ def parse_c(path: Path) -> Node | None:
167
+ """Parse a C source file (.c / .h).
168
+
169
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
170
+ Both .c and .h files use the same C grammar — .h→C is a deliberate MVP decision.
171
+ Malformed C still returns a (possibly partial) tree with ERROR nodes.
172
+ """
173
+ return _parse(path, _C_LANG)
174
+
175
+
176
+ def parse_cpp(path: Path) -> Node | None:
177
+ """Parse a C++ source file (.cpp / .cc / .cxx / .hpp / .hh / .hxx / .c++).
178
+
179
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
180
+ Malformed C++ still returns a (possibly partial) tree with ERROR nodes.
181
+ """
182
+ return _parse(path, _CPP_LANG)
183
+
184
+
185
+ def parse_php(path: Path) -> Node | None:
186
+ """Parse a PHP source file (.php).
187
+
188
+ Uses language_php() (not language()) which handles the <?php open tag.
189
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
190
+ Malformed PHP still returns a (possibly partial) tree with ERROR nodes.
191
+ """
192
+ return _parse(path, _PHP_LANG)
193
+
194
+
195
+ # ── Phase 10 parser ────────────────────────────────────────────────────────────
196
+
197
+
198
+ def parse_swift(path: Path) -> Node | None:
199
+ """Parse a Swift source file (.swift).
200
+
201
+ Returns tree-sitter root Node, or None for binary/oversized/unreadable files.
202
+ Malformed Swift still returns a (possibly partial) tree with ERROR nodes.
203
+ """
204
+ return _parse(path, _SWIFT_LANG)
@@ -0,0 +1,197 @@
1
+ """Indexing pipeline — the shared parse -> extract -> upsert path.
2
+
3
+ Lives in indexer/ (not cli/) because BOTH the CLI (`seam init`) and the watcher
4
+ daemon consume it. Putting it here keeps the import hierarchy intact:
5
+ cli -> indexer.pipeline and watcher -> indexer.pipeline
6
+ (watcher importing from cli would violate the layer rules in BACKEND_STRUCTURE.md).
7
+
8
+ Pure-ish glue: no Typer, no watchdog — just stdlib + the indexer modules.
9
+ """
10
+
11
+ import hashlib
12
+ import logging
13
+ import sqlite3
14
+ from pathlib import Path
15
+
16
+ import seam.config as config
17
+ from seam.analysis.imports import extract_import_mappings
18
+ from seam.indexer.db import upsert_file, upsert_import_mappings
19
+ from seam.indexer.graph import extract_comments, extract_edges, extract_symbols
20
+ from seam.indexer.parser import (
21
+ parse_c,
22
+ parse_cpp,
23
+ parse_csharp,
24
+ parse_go,
25
+ parse_java,
26
+ parse_javascript,
27
+ parse_php,
28
+ parse_python,
29
+ parse_ruby,
30
+ parse_rust,
31
+ parse_swift,
32
+ parse_typescript,
33
+ )
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # Directories to skip when walking the project tree.
38
+ # Dot-dirs are skipped by default; this list catches common non-dot dirs.
39
+ #
40
+ # WHY build/output dirs matter: indexing compiled artifacts (e.g. a minified JS
41
+ # bundle in seam/_web/assets/) injects thousands of garbage symbols and pollutes
42
+ # clustering with meaningless "areas" named after the bundle file. We skip the
43
+ # standard build/output/vendor dir names so `seam init` indexes SOURCE, not
44
+ # generated output. (`dist`/`build`/`node_modules` were always here; `_web`,
45
+ # `target`, `out`, `coverage`, `vendor` close the remaining common gaps.)
46
+ SKIP_DIRS: frozenset[str] = frozenset(
47
+ {
48
+ ".git",
49
+ "node_modules",
50
+ ".venv",
51
+ "__pycache__",
52
+ ".seam",
53
+ "dist",
54
+ "build",
55
+ "_web", # built SPA output (seam/_web/) — minified, not source
56
+ "out", # common JS/TS build output
57
+ "target", # Rust/Maven/Gradle compiled output
58
+ "vendor", # Go/PHP third-party deps (not first-party source)
59
+ "coverage", # coverage report output
60
+ "site", # mkdocs default build output (bundled/minified JS — pollutes clusters)
61
+ "_site", # Jekyll default build output
62
+ ".mypy_cache",
63
+ ".ruff_cache",
64
+ ".pytest_cache",
65
+ }
66
+ )
67
+
68
+
69
+ def sha1(content: bytes) -> str:
70
+ """Return the SHA-1 hex digest of file content bytes (change detection only)."""
71
+ return hashlib.sha1(content).hexdigest() # noqa: S324 — not used for security
72
+
73
+
74
+ def _dispatch_parser(path: Path, language: str): # type: ignore[return]
75
+ """Call the correct parser for a language string. Returns root Node or None."""
76
+ if language == "python":
77
+ return parse_python(path)
78
+ if language == "typescript":
79
+ return parse_typescript(path)
80
+ if language == "javascript":
81
+ return parse_javascript(path)
82
+ if language == "go":
83
+ return parse_go(path)
84
+ if language == "rust":
85
+ return parse_rust(path)
86
+ # Phase 9 — new languages
87
+ if language == "java":
88
+ return parse_java(path)
89
+ if language == "csharp":
90
+ return parse_csharp(path)
91
+ if language == "ruby":
92
+ return parse_ruby(path)
93
+ if language == "c":
94
+ return parse_c(path)
95
+ if language == "cpp":
96
+ return parse_cpp(path)
97
+ if language == "php":
98
+ return parse_php(path)
99
+ # Phase 10 — Swift
100
+ if language == "swift":
101
+ return parse_swift(path)
102
+ return None
103
+
104
+
105
+ def index_one_file(conn: sqlite3.Connection, path: Path) -> tuple[int, int] | None:
106
+ """Parse, extract, and upsert a single source file.
107
+
108
+ Returns:
109
+ (symbol_count, edge_count) when the file was INDEXED (upserted) — note
110
+ this is (0, 0) for a valid-but-empty file like an empty __init__.py,
111
+ which IS indexed.
112
+ None when the file was SKIPPED (unsupported ext, oversize, binary/
113
+ unreadable, or an error) — the reason is logged at DEBUG so a
114
+ systematic failure (e.g. a grammar mismatch breaking every .ts file)
115
+ is recoverable instead of silently invisible.
116
+
117
+ Distinguishing None (skipped) from (0, 0) (indexed-but-empty) lets callers
118
+ report an honest skipped-file count. Never raises.
119
+ """
120
+ try:
121
+ ext = path.suffix.lower()
122
+ language = config.SEAM_LANGUAGE_MAP.get(ext)
123
+ if language is None:
124
+ logger.debug("skip %s: unsupported extension", path)
125
+ return None
126
+
127
+ try:
128
+ if path.stat().st_size > config.SEAM_MAX_FILE_BYTES:
129
+ logger.debug("skip %s: over size limit", path)
130
+ return None
131
+ except OSError as exc:
132
+ logger.debug("skip %s: stat failed: %s", path, exc)
133
+ return None
134
+
135
+ root = _dispatch_parser(path, language)
136
+ if root is None:
137
+ logger.debug("skip %s: parser returned None (binary/unreadable)", path)
138
+ return None
139
+
140
+ try:
141
+ content = path.read_bytes()
142
+ except OSError as exc:
143
+ logger.debug("skip %s: read failed: %s", path, exc)
144
+ return None
145
+
146
+ file_hash = sha1(content)
147
+ symbols = extract_symbols(root, language, path)
148
+ # Pass symbols so extract_edges can resolve confidence (EXTRACTED/AMBIGUOUS/INFERRED)
149
+ # against the same-file symbol set. Cross-file ambiguity is handled at query time.
150
+ edges = extract_edges(root, language, path, symbols=symbols)
151
+ # Extract semantic comments (WHY/HACK/NOTE/TODO/FIXME); never raises.
152
+ comments = extract_comments(root, language, path)
153
+
154
+ upsert_file(conn, path, language, file_hash, symbols, edges, comments)
155
+
156
+ # Phase 5: extract and store import mappings for this file.
157
+ # Only runs when SEAM_IMPORT_RESOLUTION is 'on' (default).
158
+ # extract_import_mappings never raises; failures silently return [].
159
+ if config.SEAM_IMPORT_RESOLUTION == "on":
160
+ import_mappings = extract_import_mappings(root, path, language)
161
+ upsert_import_mappings(conn, path, import_mappings)
162
+
163
+ return len(symbols), len(edges)
164
+
165
+ except Exception as exc: # noqa: BLE001 — one bad file must not abort the run
166
+ logger.debug("skip %s: unexpected error: %s", path, exc)
167
+ return None
168
+
169
+
170
+ def walk_project(root: Path) -> list[Path]:
171
+ """Walk root recursively, skipping ignored dirs, returning indexable files.
172
+
173
+ Rules:
174
+ - Skip any directory whose name starts with '.' (hidden dirs).
175
+ - Skip any directory in SKIP_DIRS.
176
+ - Skip minified bundles (name contains '.min.', e.g. foo.min.js) — they are
177
+ generated, not source, and inject garbage symbols that pollute clustering.
178
+ - Collect files whose suffix is in config.SEAM_LANGUAGE_MAP.
179
+
180
+ The dot/skip check uses only the parts BELOW root (root may itself sit under
181
+ a dot-dir, e.g. ~/.config/proj — that must not exclude everything).
182
+ """
183
+ files: list[Path] = []
184
+ root_depth = len(root.parts)
185
+ for item in root.rglob("*"):
186
+ if any(
187
+ part.startswith(".") or part in SKIP_DIRS
188
+ for part in item.parts[root_depth:]
189
+ ):
190
+ continue
191
+ if (
192
+ item.is_file()
193
+ and item.suffix.lower() in config.SEAM_LANGUAGE_MAP
194
+ and ".min." not in item.name # skip minified bundles (foo.min.js, bundle.min.mjs)
195
+ ):
196
+ files.append(item)
197
+ return files