codegraph-engine 3.0.0__1-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +859 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
codegraph/monorepo.py ADDED
@@ -0,0 +1,925 @@
1
+ """Monorepo, Workspace, and Package Boundary Intelligence for CodeGraph MCP.
2
+
3
+ Core Principle:
4
+ The AI reasons.
5
+ CodeGraph interrogates the repository.
6
+
7
+ Invariants:
8
+ 1. Never infer a package boundary solely from directory names (apps/, packages/, services/).
9
+ 2. A package boundary must be supported by concrete evidence (manifests, workspace configs).
10
+ 3. Collision-safe, machine-independent package identities (workspace_id::root_path).
11
+ 4. No arbitrary command execution — static AST/JSON/TOML/YAML parsing only.
12
+ 5. Reuse Phase 8 artifact classification: vendor/build/generated directories are never workspace packages.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import fnmatch
17
+ import json
18
+ import posixpath
19
+ import re
20
+ import sqlite3
21
+ import tomllib
22
+ from dataclasses import dataclass, field
23
+ from enum import StrEnum
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from codegraph.errors import SecurityError
28
+ from codegraph.indexing.classifier import FileCategory, classify_file
29
+ from codegraph.security import is_sensitive, safe_path
30
+
31
+ try:
32
+ import yaml
33
+ _HAS_YAML = True
34
+ except ImportError:
35
+ _HAS_YAML = False
36
+
37
+
38
+ class PackageType(StrEnum):
39
+ APPLICATION = "APPLICATION"
40
+ LIBRARY = "LIBRARY"
41
+ SERVICE = "SERVICE"
42
+ UNKNOWN = "UNKNOWN"
43
+
44
+
45
+ _APP_FRAMEWORKS = {
46
+ "express",
47
+ "next",
48
+ "nextjs",
49
+ "react",
50
+ "vue",
51
+ "svelte",
52
+ "fastapi",
53
+ "flask",
54
+ "django",
55
+ "tornado",
56
+ "aiohttp",
57
+ "starlette",
58
+ "nestjs",
59
+ "koa",
60
+ "remix",
61
+ }
62
+
63
+ _BACKEND_SERVER_FRAMEWORKS = {
64
+ "express",
65
+ "fastapi",
66
+ "flask",
67
+ "django",
68
+ "tornado",
69
+ "aiohttp",
70
+ "starlette",
71
+ "nestjs",
72
+ "koa",
73
+ }
74
+
75
+ _SERVICE_INDICATORS = {
76
+ "microservice",
77
+ "daemon",
78
+ "worker",
79
+ "grpc",
80
+ "kafka",
81
+ "rabbitmq",
82
+ "celery",
83
+ "pika",
84
+ "bullmq",
85
+ "dramatiq",
86
+ "rq",
87
+ }
88
+
89
+ _PRUNED_WALK_DIRS = frozenset({
90
+ ".git",
91
+ ".hg",
92
+ ".svn",
93
+ ".venv",
94
+ "venv",
95
+ "env",
96
+ "node_modules",
97
+ "dist",
98
+ "build",
99
+ "out",
100
+ "target",
101
+ "vendor",
102
+ "third_party",
103
+ "__pycache__",
104
+ ".next",
105
+ ".nuxt",
106
+ ".tox",
107
+ ".mypy_cache",
108
+ ".pytest_cache",
109
+ ".ruff_cache",
110
+ "coverage",
111
+ })
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class PackageDependency:
116
+ source_package_id: str
117
+ target_package_id: str
118
+ source_name: str
119
+ target_name: str
120
+ specifier: str = ""
121
+ evidence: str = ""
122
+ is_workspace: bool = False
123
+
124
+ def as_dict(self) -> dict[str, object]:
125
+ return {
126
+ "source_package_id": self.source_package_id,
127
+ "target_package_id": self.target_package_id,
128
+ "source_name": self.source_name,
129
+ "target_name": self.target_name,
130
+ "relationship": "DEPENDS_ON_PACKAGE",
131
+ "specifier": self.specifier,
132
+ "evidence": self.evidence,
133
+ "is_workspace": self.is_workspace,
134
+ }
135
+
136
+
137
+ @dataclass
138
+ class PackageInfo:
139
+ package_id: str
140
+ name: str
141
+ root_path: str
142
+ manifest_path: str
143
+ language: str
144
+ package_manager: str
145
+ workspace_id: str
146
+ package_type: PackageType
147
+ entrypoints: list[str] = field(default_factory=list)
148
+ exports: dict[str, str] = field(default_factory=dict)
149
+ dependency_names: list[str] = field(default_factory=list)
150
+ dependency_package_ids: list[str] = field(default_factory=list)
151
+ source_directories: list[str] = field(default_factory=list)
152
+ test_directories: list[str] = field(default_factory=list)
153
+ metadata: dict[str, Any] = field(default_factory=dict)
154
+
155
+ def as_dict(self) -> dict[str, object]:
156
+ return {
157
+ "package_id": self.package_id,
158
+ "name": self.name,
159
+ "root_path": self.root_path,
160
+ "manifest_path": self.manifest_path,
161
+ "language": self.language,
162
+ "package_manager": self.package_manager,
163
+ "workspace_id": self.workspace_id,
164
+ "package_type": self.package_type.value,
165
+ "entrypoints": self.entrypoints,
166
+ "exports": self.exports,
167
+ "dependency_names": self.dependency_names,
168
+ "dependency_package_ids": self.dependency_package_ids,
169
+ "source_directories": self.source_directories,
170
+ "test_directories": self.test_directories,
171
+ }
172
+
173
+
174
+ @dataclass
175
+ class WorkspaceInfo:
176
+ workspace_id: str
177
+ root_path: str
178
+ workspace_type: str # "npm" | "pnpm" | "yarn" | "python" | "standalone"
179
+ packages: dict[str, PackageInfo] = field(default_factory=dict)
180
+ package_by_name: dict[str, list[PackageInfo]] = field(default_factory=dict)
181
+ package_by_path: dict[str, PackageInfo] = field(default_factory=dict)
182
+ dependencies: list[PackageDependency] = field(default_factory=list)
183
+ unresolved_dependencies: list[dict[str, str]] = field(default_factory=list)
184
+ ambiguous_packages: dict[str, list[str]] = field(default_factory=dict)
185
+
186
+ def get_package_for_file(self, file_path: str) -> PackageInfo | None:
187
+ """Find the package that owns file_path based on longest matching root_path."""
188
+ clean = file_path.replace("\\", "/").lstrip("./")
189
+ best_pkg: PackageInfo | None = None
190
+ best_len = -1
191
+
192
+ for pkg in self.packages.values():
193
+ pkg_root = pkg.root_path
194
+ if pkg_root == "" or pkg_root == ".":
195
+ if best_len < 0:
196
+ best_pkg = pkg
197
+ best_len = 0
198
+ elif clean == pkg_root or clean.startswith(f"{pkg_root}/"):
199
+ if len(pkg_root) > best_len:
200
+ best_pkg = pkg
201
+ best_len = len(pkg_root)
202
+ return best_pkg
203
+
204
+ def get_dependent_packages(self, package_id: str) -> list[PackageInfo]:
205
+ """Return all packages in the workspace that depend on package_id."""
206
+ dependents: list[PackageInfo] = []
207
+ for dep in self.dependencies:
208
+ if dep.target_package_id == package_id:
209
+ source_pkg = self.packages.get(dep.source_package_id)
210
+ if source_pkg and source_pkg not in dependents:
211
+ dependents.append(source_pkg)
212
+ return dependents
213
+
214
+ def resolve_package_import_detailed(
215
+ self,
216
+ import_specifier: str,
217
+ source_file: str | None = None,
218
+ ) -> dict[str, Any]:
219
+ """Resolve a package import specifier with explicit AMBIGUOUS reporting when duplicate
220
+ package names exist in the workspace. Never silently guesses `candidates[0]`.
221
+ """
222
+ del source_file
223
+ if not import_specifier:
224
+ return {"status": "UNRESOLVED", "package": None, "entrypoint": None, "candidates": []}
225
+
226
+ # 1. Exact match on package name
227
+ candidates = self.package_by_name.get(import_specifier)
228
+ if candidates:
229
+ if len(candidates) > 1:
230
+ return {
231
+ "status": "AMBIGUOUS",
232
+ "package": None,
233
+ "entrypoint": None,
234
+ "candidates": sorted(c.package_id for c in candidates),
235
+ "reason": "duplicate_package_name",
236
+ }
237
+ pkg = candidates[0]
238
+ target_file = pkg.exports.get(".") or (pkg.entrypoints[0] if pkg.entrypoints else None)
239
+ return {
240
+ "status": "RESOLVED",
241
+ "package": pkg,
242
+ "entrypoint": target_file,
243
+ "candidates": [pkg.package_id],
244
+ }
245
+
246
+ # 2. Subpath import (e.g. '@company/auth/service' or 'shared/utils')
247
+ for name, pkg_list in self.package_by_name.items():
248
+ if import_specifier.startswith(f"{name}/"):
249
+ if len(pkg_list) > 1:
250
+ return {
251
+ "status": "AMBIGUOUS",
252
+ "package": None,
253
+ "entrypoint": None,
254
+ "candidates": sorted(c.package_id for c in pkg_list),
255
+ "reason": "duplicate_package_name",
256
+ }
257
+ subpath = "." + import_specifier[len(name):]
258
+ pkg = pkg_list[0]
259
+ if subpath in pkg.exports:
260
+ return {
261
+ "status": "RESOLVED",
262
+ "package": pkg,
263
+ "entrypoint": pkg.exports[subpath],
264
+ "candidates": [pkg.package_id],
265
+ }
266
+ rel_candidate = posixpath.normpath(posixpath.join(pkg.root_path, subpath.lstrip("./")))
267
+ return {
268
+ "status": "RESOLVED",
269
+ "package": pkg,
270
+ "entrypoint": rel_candidate,
271
+ "candidates": [pkg.package_id],
272
+ }
273
+
274
+ return {"status": "UNRESOLVED", "package": None, "entrypoint": None, "candidates": []}
275
+
276
+ def resolve_package_import(
277
+ self,
278
+ import_specifier: str,
279
+ source_file: str | None = None,
280
+ ) -> tuple[PackageInfo | None, str | None]:
281
+ """Resolve a package import specifier to `(PackageInfo, entrypoint_file)`.
282
+
283
+ If multiple workspace packages share `import_specifier`, returns `(None, None)`
284
+ rather than guessing `candidates[0]`. Use `resolve_package_import_detailed` to
285
+ inspect `AMBIGUOUS` candidate lists.
286
+ """
287
+ detail = self.resolve_package_import_detailed(import_specifier, source_file=source_file)
288
+ if detail["status"] == "RESOLVED":
289
+ return detail["package"], detail["entrypoint"]
290
+ return None, None
291
+
292
+ def as_dict(self) -> dict[str, object]:
293
+ out: dict[str, object] = {
294
+ "workspace_id": self.workspace_id,
295
+ "root_path": self.root_path,
296
+ "workspace_type": self.workspace_type,
297
+ "package_count": len(self.packages),
298
+ "packages": [p.as_dict() for p in self.packages.values()],
299
+ "package_dependencies": [d.as_dict() for d in self.dependencies],
300
+ "unresolved_dependencies": self.unresolved_dependencies,
301
+ }
302
+ if self.ambiguous_packages:
303
+ out["ambiguous_packages"] = self.ambiguous_packages
304
+ return out
305
+
306
+
307
+ def _parse_yaml_packages(content: str) -> list[str]:
308
+ """Parse packages list from pnpm-workspace.yaml safely."""
309
+ if _HAS_YAML:
310
+ try:
311
+ data = yaml.safe_load(content)
312
+ if isinstance(data, dict):
313
+ pkgs = data.get("packages")
314
+ if isinstance(pkgs, list):
315
+ return [str(p) for p in pkgs if isinstance(p, (str, int))]
316
+ except Exception:
317
+ pass
318
+
319
+ # Simple line-based fallback parser
320
+ patterns: list[str] = []
321
+ in_packages = False
322
+ for line in content.splitlines():
323
+ stripped = line.strip()
324
+ if not stripped or stripped.startswith("#"):
325
+ continue
326
+ if stripped.startswith("packages:"):
327
+ in_packages = True
328
+ continue
329
+ if in_packages:
330
+ if stripped.startswith("-"):
331
+ pat = stripped.lstrip("-").strip().strip("'\"")
332
+ if pat:
333
+ patterns.append(pat)
334
+ elif not line.startswith(" ") and not line.startswith("\t"):
335
+ break
336
+ return patterns
337
+
338
+
339
+ def _scan_manifests(
340
+ repository: Path,
341
+ con: sqlite3.Connection | None = None,
342
+ max_depth: int = 5,
343
+ ) -> list[Path]:
344
+ """Discover manifest files safely with bounded recursion (`max_depth`),
345
+ skipping vendor/build/generated artifacts and sensitive paths.
346
+ """
347
+ root = repository.resolve(strict=True)
348
+ manifest_paths: list[Path] = []
349
+ target_names = {"package.json", "pyproject.toml", "setup.cfg"}
350
+
351
+ # Priority: if database connection is available, use indexed files table
352
+ if con is not None:
353
+ try:
354
+ rows = con.execute(
355
+ "SELECT path, category FROM files WHERE "
356
+ "path LIKE '%package.json' OR path LIKE '%pyproject.toml' "
357
+ "OR path LIKE '%setup.py' OR path LIKE '%setup.cfg'"
358
+ ).fetchall()
359
+ for r in rows:
360
+ p_str = str(r[0])
361
+ cat = str(r[1])
362
+ if cat in ("VENDOR", "BUILD_ARTIFACT", "BINARY"):
363
+ continue
364
+ if p_str.count("/") > max_depth:
365
+ continue
366
+ try:
367
+ p = safe_path(root, p_str)
368
+ if p.is_file():
369
+ manifest_paths.append(p)
370
+ except SecurityError:
371
+ continue
372
+ if manifest_paths:
373
+ return sorted(set(manifest_paths))
374
+ except Exception:
375
+ pass
376
+
377
+ # Bounded recursive filesystem walk (never descends beyond max_depth or into vendor/build dirs)
378
+ def _walk(current_dir: Path, depth: int) -> None:
379
+ if depth > max_depth:
380
+ return
381
+ try:
382
+ entries = sorted(current_dir.iterdir(), key=lambda e: e.name)
383
+ except OSError:
384
+ return
385
+
386
+ for entry in entries:
387
+ try:
388
+ if entry.is_symlink():
389
+ # Verify symlink stays inside repository root
390
+ resolved_entry = entry.resolve(strict=False)
391
+ resolved_entry.relative_to(root)
392
+ rel = entry.relative_to(root)
393
+ except (ValueError, OSError, SecurityError):
394
+ continue
395
+
396
+ if is_sensitive(rel):
397
+ continue
398
+
399
+ if entry.is_dir():
400
+ if entry.name in _PRUNED_WALK_DIRS or (entry.name.startswith(".") and entry.name != "."):
401
+ continue
402
+ cat_dir = classify_file(rel)
403
+ if cat_dir in (FileCategory.VENDOR, FileCategory.BUILD_ARTIFACT, FileCategory.BINARY):
404
+ continue
405
+ _walk(entry, depth + 1)
406
+ elif entry.is_file() and entry.name in target_names:
407
+ cat_file = classify_file(rel)
408
+ if cat_file in (FileCategory.VENDOR, FileCategory.BUILD_ARTIFACT, FileCategory.BINARY):
409
+ continue
410
+ manifest_paths.append(entry)
411
+
412
+ _walk(root, 0)
413
+ return sorted(set(manifest_paths))
414
+
415
+
416
+ def _resolve_entrypoints_js(
417
+ root_rel: str,
418
+ pkg_data: dict[str, Any],
419
+ manifest_dir: Path,
420
+ ) -> tuple[list[str], dict[str, str]]:
421
+ """Extract entrypoints and exports map from package.json."""
422
+ entrypoints: list[str] = []
423
+ exports_map: dict[str, str] = {}
424
+
425
+ def _add_entry(cand: str, key: str = ".") -> None:
426
+ cand_clean = cand.replace("\\", "/").lstrip("./")
427
+ full_rel = posixpath.normpath(posixpath.join(root_rel, cand_clean)) if root_rel else cand_clean
428
+ if full_rel not in entrypoints:
429
+ entrypoints.append(full_rel)
430
+ if key not in exports_map:
431
+ exports_map[key] = full_rel
432
+
433
+ # 1. package.json "main"
434
+ main_field = pkg_data.get("main")
435
+ if isinstance(main_field, str) and main_field.strip():
436
+ _add_entry(main_field, ".")
437
+
438
+ # 2. package.json "module"
439
+ module_field = pkg_data.get("module")
440
+ if isinstance(module_field, str) and module_field.strip():
441
+ _add_entry(module_field, ".")
442
+
443
+ # 3. package.json "bin"
444
+ bin_field = pkg_data.get("bin")
445
+ if isinstance(bin_field, str):
446
+ _add_entry(bin_field, "./bin")
447
+ elif isinstance(bin_field, dict):
448
+ for b_name, b_path in bin_field.items():
449
+ if isinstance(b_path, str):
450
+ _add_entry(b_path, f"./bin/{b_name}")
451
+
452
+ # 4. package.json "exports"
453
+ exports_field = pkg_data.get("exports")
454
+ if isinstance(exports_field, str):
455
+ _add_entry(exports_field, ".")
456
+ elif isinstance(exports_field, dict):
457
+ for exp_key, exp_val in exports_field.items():
458
+ if isinstance(exp_val, str):
459
+ _add_entry(exp_val, exp_key)
460
+ elif isinstance(exp_val, dict):
461
+ # Conditional exports (import, require, default)
462
+ for cond in ("import", "require", "default", "types"):
463
+ c_val = exp_val.get(cond)
464
+ if isinstance(c_val, str):
465
+ _add_entry(c_val, exp_key)
466
+ break
467
+
468
+ # 5. Common conventions if no explicit entrypoint
469
+ if not entrypoints:
470
+ for standard in (
471
+ "src/index.ts",
472
+ "src/index.js",
473
+ "src/index.tsx",
474
+ "src/main.ts",
475
+ "src/main.js",
476
+ "index.ts",
477
+ "index.js",
478
+ ):
479
+ if (manifest_dir / standard).is_file():
480
+ _add_entry(standard, ".")
481
+ break
482
+
483
+ return entrypoints, exports_map
484
+
485
+
486
+ def _resolve_entrypoints_py(
487
+ root_rel: str,
488
+ py_data: dict[str, Any],
489
+ manifest_dir: Path,
490
+ ) -> tuple[list[str], dict[str, str]]:
491
+ """Extract entrypoints and scripts from pyproject.toml."""
492
+ entrypoints: list[str] = []
493
+ exports_map: dict[str, str] = {}
494
+
495
+ def _add_entry(cand: str, key: str = ".") -> None:
496
+ cand_clean = cand.replace("\\", "/").lstrip("./")
497
+ full_rel = posixpath.normpath(posixpath.join(root_rel, cand_clean)) if root_rel else cand_clean
498
+ if full_rel not in entrypoints:
499
+ entrypoints.append(full_rel)
500
+ if key not in exports_map:
501
+ exports_map[key] = full_rel
502
+
503
+ # Check project.scripts
504
+ proj = py_data.get("project", {}) if isinstance(py_data, dict) else {}
505
+ scripts = proj.get("scripts", {}) if isinstance(proj, dict) else {}
506
+ if isinstance(scripts, dict):
507
+ for s_name, s_spec in scripts.items():
508
+ if isinstance(s_spec, str) and ":" in s_spec:
509
+ mod = s_spec.split(":")[0].replace(".", "/")
510
+ _add_entry(f"{mod}.py", f"./bin/{s_name}")
511
+
512
+ # Check poetry.scripts
513
+ tool = py_data.get("tool", {}) if isinstance(py_data, dict) else {}
514
+ poetry = tool.get("poetry", {}) if isinstance(tool, dict) else {}
515
+ p_scripts = poetry.get("scripts", {}) if isinstance(poetry, dict) else {}
516
+ if isinstance(p_scripts, dict):
517
+ for s_name, s_spec in p_scripts.items():
518
+ if isinstance(s_spec, str) and ":" in s_spec:
519
+ mod = s_spec.split(":")[0].replace(".", "/")
520
+ _add_entry(f"{mod}.py", f"./bin/{s_name}")
521
+
522
+ # Look for package module / __init__.py
523
+ pkg_name = proj.get("name") or poetry.get("name")
524
+ if isinstance(pkg_name, str) and pkg_name:
525
+ clean_pkg = pkg_name.replace("-", "_")
526
+ for cand in (
527
+ f"src/{clean_pkg}/__init__.py",
528
+ f"{clean_pkg}/__init__.py",
529
+ f"src/{clean_pkg}.py",
530
+ f"{clean_pkg}.py",
531
+ ):
532
+ if (manifest_dir / cand).is_file():
533
+ _add_entry(cand, ".")
534
+ break
535
+
536
+ if not entrypoints:
537
+ for cand in ("src/__init__.py", "app.py", "main.py"):
538
+ if (manifest_dir / cand).is_file():
539
+ _add_entry(cand, ".")
540
+ break
541
+
542
+ return entrypoints, exports_map
543
+
544
+
545
+ def _matches_ws_pattern(rel_path: str, pat: str) -> bool:
546
+ """Match a package relative path against a workspace glob pattern."""
547
+ clean_pat = pat.strip().rstrip("/")
548
+ if not clean_pat:
549
+ return False
550
+ return (
551
+ fnmatch.fnmatch(rel_path, clean_pat)
552
+ or fnmatch.fnmatch(rel_path, pat.rstrip("/*"))
553
+ or (clean_pat.endswith("/*") and rel_path.startswith(clean_pat[:-2] + "/"))
554
+ or (clean_pat.endswith("/**") and (rel_path == clean_pat[:-3] or rel_path.startswith(clean_pat[:-3] + "/")))
555
+ )
556
+
557
+
558
+ def _has_typescript_files_bounded(manifest_dir: Path, entrypoints: list[str]) -> bool:
559
+ """Check whether a JS/TS package uses TypeScript without unbounded globbing."""
560
+ if (manifest_dir / "tsconfig.json").is_file():
561
+ return True
562
+ if any(ep.endswith((".ts", ".tsx")) for ep in entrypoints):
563
+ return True
564
+ try:
565
+ for child in manifest_dir.iterdir():
566
+ if child.is_file() and child.suffix in (".ts", ".tsx"):
567
+ return True
568
+ if child.is_dir() and child.name not in _PRUNED_WALK_DIRS and not child.name.startswith("."):
569
+ for sub in child.iterdir():
570
+ if sub.is_file() and sub.suffix in (".ts", ".tsx"):
571
+ return True
572
+ except OSError:
573
+ pass
574
+ return False
575
+
576
+
577
+ def _classify_package_type(
578
+ name: str,
579
+ root_rel: str,
580
+ dependencies: list[str],
581
+ entrypoints: list[str],
582
+ has_bin: bool,
583
+ con: sqlite3.Connection | None,
584
+ ) -> PackageType:
585
+ """Statically determine PackageType based on verified evidence without guessing from directory names.
586
+
587
+ Precedence:
588
+ 1. Explicit service indicators (celery, kafka, grpc, worker, daemon, etc.) -> SERVICE
589
+ 2. Backend server frameworks (FastAPI, Flask, Django, Express, etc.) paired with service
590
+ evidence (ASGI/WSGI runners, service entrypoints, or service package identity) -> SERVICE
591
+ 3. Application frameworks (Next, React, Vue, standalone web apps) -> APPLICATION
592
+ 4. Indexed framework routes under package root -> APPLICATION
593
+ 5. CLI binary without application server -> APPLICATION
594
+ 6. Library entrypoints without binary -> LIBRARY
595
+ 7. Otherwise -> UNKNOWN
596
+ """
597
+ all_deps_lower = {d.lower() for d in dependencies}
598
+ name_lower = name.lower()
599
+
600
+ # Evidence 1: Service markers in dependencies (checked BEFORE generic app frameworks)
601
+ if any(ind in all_deps_lower for ind in _SERVICE_INDICATORS):
602
+ return PackageType.SERVICE
603
+
604
+ # Evidence 2: Backend server framework acting as a service (service runner / service entrypoint / service identity)
605
+ has_backend_server = any(fw in all_deps_lower for fw in _BACKEND_SERVER_FRAMEWORKS)
606
+ has_service_runner = any(srv in all_deps_lower for srv in ("uvicorn", "gunicorn", "hypercorn", "daphne"))
607
+ has_service_entrypoint = any(
608
+ ep.endswith(("service.py", "service.ts", "service.js", "worker.py", "worker.ts", "daemon.py"))
609
+ for ep in entrypoints
610
+ )
611
+ has_service_identity = (
612
+ name_lower.endswith(("-service", "_service", "-svc", "_svc", "-worker", "_worker", "-daemon"))
613
+ or root_rel.startswith("services/")
614
+ )
615
+ if has_backend_server and (has_service_runner or has_service_entrypoint or has_service_identity):
616
+ return PackageType.SERVICE
617
+
618
+ # Evidence 3: Application frameworks
619
+ if any(fw in all_deps_lower for fw in _APP_FRAMEWORKS):
620
+ return PackageType.APPLICATION
621
+
622
+ # Evidence 4: Database routes registered under this package root
623
+ if con is not None:
624
+ try:
625
+ count = con.execute(
626
+ "SELECT count(*) FROM framework_routes WHERE file_path LIKE ?",
627
+ (f"{root_rel}/%" if root_rel else "%",),
628
+ ).fetchone()[0]
629
+ if count > 0:
630
+ return PackageType.APPLICATION
631
+ except Exception:
632
+ pass
633
+
634
+ # Evidence 5: CLI binary without application server
635
+ if has_bin and not any(fw in all_deps_lower for fw in _APP_FRAMEWORKS):
636
+ return PackageType.APPLICATION
637
+
638
+ # Evidence 6: Library characteristics (entrypoints exist, no app frameworks)
639
+ if entrypoints and not has_bin:
640
+ return PackageType.LIBRARY
641
+
642
+ return PackageType.UNKNOWN
643
+
644
+
645
+ def detect_workspace(
646
+ repository: Path,
647
+ con: sqlite3.Connection | None = None,
648
+ ) -> WorkspaceInfo:
649
+ """Deterministically analyze repository workspace structure, packages, and dependencies."""
650
+ root = repository.resolve(strict=True)
651
+
652
+ # 1. Detect root workspace type and declared package patterns
653
+ workspace_type = "standalone"
654
+ workspace_patterns: list[str] = []
655
+ root_name = root.name
656
+
657
+ # Check pnpm-workspace.yaml
658
+ pnpm_ws = root / "pnpm-workspace.yaml"
659
+ if pnpm_ws.is_file():
660
+ workspace_type = "pnpm"
661
+ try:
662
+ text = pnpm_ws.read_text(encoding="utf-8", errors="replace")
663
+ workspace_patterns.extend(_parse_yaml_packages(text))
664
+ except OSError:
665
+ pass
666
+
667
+ # Check root package.json
668
+ root_pkg_json = root / "package.json"
669
+ if root_pkg_json.is_file():
670
+ try:
671
+ data = json.loads(root_pkg_json.read_text(encoding="utf-8", errors="replace"))
672
+ if isinstance(data, dict):
673
+ r_name = data.get("name")
674
+ if isinstance(r_name, str) and r_name:
675
+ root_name = r_name
676
+ ws = data.get("workspaces")
677
+ if ws:
678
+ if workspace_type == "standalone":
679
+ workspace_type = "npm"
680
+ if isinstance(ws, list):
681
+ workspace_patterns.extend([str(p) for p in ws if isinstance(p, str)])
682
+ elif isinstance(ws, dict):
683
+ pkgs = ws.get("packages")
684
+ if isinstance(pkgs, list):
685
+ workspace_patterns.extend([str(p) for p in pkgs if isinstance(p, str)])
686
+ except Exception:
687
+ pass
688
+
689
+ # Check root pyproject.toml
690
+ root_pyproj = root / "pyproject.toml"
691
+ if root_pyproj.is_file() and workspace_type == "standalone":
692
+ try:
693
+ p_data = tomllib.loads(root_pyproj.read_text(encoding="utf-8", errors="replace"))
694
+ if isinstance(p_data, dict):
695
+ proj = p_data.get("project", {})
696
+ if isinstance(proj, dict) and proj.get("name"):
697
+ root_name = str(proj["name"])
698
+ tool = p_data.get("tool", {})
699
+ if isinstance(tool, dict) and "poetry" in tool:
700
+ workspace_type = "python"
701
+ except Exception:
702
+ pass
703
+
704
+ workspace_id = root_name
705
+
706
+ pos_patterns = [p.strip() for p in workspace_patterns if p.strip() and not p.strip().startswith("!")]
707
+ neg_patterns = [p.strip()[1:].strip() for p in workspace_patterns if p.strip().startswith("!") and p.strip()[1:].strip()]
708
+
709
+ # 2. Discover candidate package manifests
710
+ manifest_files = _scan_manifests(root, con=con)
711
+
712
+ packages: dict[str, PackageInfo] = {}
713
+ package_by_name: dict[str, list[PackageInfo]] = {}
714
+ package_by_path: dict[str, PackageInfo] = {}
715
+
716
+ for manifest_path in manifest_files:
717
+ try:
718
+ rel_manifest = manifest_path.relative_to(root).as_posix()
719
+ except ValueError:
720
+ continue
721
+
722
+ manifest_dir = manifest_path.parent
723
+ try:
724
+ root_rel = manifest_dir.relative_to(root).as_posix()
725
+ if root_rel == ".":
726
+ root_rel = ""
727
+ except ValueError:
728
+ continue
729
+
730
+ # Respect negated workspace globs (e.g. '!packages/legacy')
731
+ if root_rel != "" and neg_patterns:
732
+ if any(_matches_ws_pattern(root_rel, np) for np in neg_patterns):
733
+ continue
734
+
735
+ # If workspace patterns are defined (e.g. apps/*, packages/*), verify membership for child packages
736
+ is_workspace_member = True
737
+ if pos_patterns and root_rel != "":
738
+ is_workspace_member = any(_matches_ws_pattern(root_rel, pat) for pat in pos_patterns)
739
+
740
+ # Package identity is strictly deterministic and collision-safe
741
+ package_id = f"{workspace_id}::{root_rel}" if root_rel else f"{workspace_id}::root"
742
+
743
+ # Parse manifest data
744
+ m_name = manifest_path.name
745
+ pkg_name = root_rel if root_rel else workspace_id
746
+ pkg_lang = "unknown"
747
+ pkg_mgr = "unknown"
748
+ dep_names: list[str] = []
749
+ entrypoints: list[str] = []
750
+ exports: dict[str, str] = {}
751
+ has_bin = False
752
+ raw_metadata: dict[str, Any] = {"is_workspace_member": is_workspace_member}
753
+
754
+ if m_name == "package.json":
755
+ pkg_mgr = workspace_type if workspace_type in ("npm", "pnpm", "yarn") else "npm"
756
+ try:
757
+ p_dict = json.loads(manifest_path.read_text(encoding="utf-8", errors="replace"))
758
+ if isinstance(p_dict, dict):
759
+ raw_metadata.update(p_dict)
760
+ if isinstance(p_dict.get("name"), str) and p_dict["name"].strip():
761
+ pkg_name = p_dict["name"].strip()
762
+ has_bin = bool(p_dict.get("bin"))
763
+ entrypoints, exports = _resolve_entrypoints_js(root_rel, p_dict, manifest_dir)
764
+
765
+ # Extract all dependencies
766
+ for dep_key in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
767
+ d_dict = p_dict.get(dep_key)
768
+ if isinstance(d_dict, dict):
769
+ for d_name, _d_spec in d_dict.items():
770
+ if d_name not in dep_names:
771
+ dep_names.append(d_name)
772
+ except Exception:
773
+ continue
774
+ pkg_lang = "typescript" if _has_typescript_files_bounded(manifest_dir, entrypoints) else "javascript"
775
+
776
+ elif m_name in ("pyproject.toml", "setup.cfg", "setup.py"):
777
+ pkg_lang = "python"
778
+ pkg_mgr = "poetry" if workspace_type == "python" else "pip"
779
+ if m_name == "pyproject.toml":
780
+ try:
781
+ py_dict = tomllib.loads(manifest_path.read_text(encoding="utf-8", errors="replace"))
782
+ if isinstance(py_dict, dict):
783
+ raw_metadata = py_dict
784
+ proj = py_dict.get("project", {})
785
+ if isinstance(proj, dict) and proj.get("name"):
786
+ pkg_name = str(proj["name"])
787
+ deps = proj.get("dependencies", [])
788
+ if isinstance(deps, list):
789
+ for d in deps:
790
+ if isinstance(d, str):
791
+ dep_names.append(re.split(r"[><=~;!]", d)[0].strip())
792
+ tool = py_dict.get("tool", {})
793
+ if isinstance(tool, dict) and "poetry" in tool:
794
+ p_sec = tool["poetry"]
795
+ if isinstance(p_sec, dict) and p_sec.get("name"):
796
+ pkg_name = str(p_sec["name"])
797
+ p_deps = p_sec.get("dependencies", {})
798
+ if isinstance(p_deps, dict):
799
+ for d in p_deps.keys():
800
+ if d != "python" and d not in dep_names:
801
+ dep_names.append(d)
802
+ entrypoints, exports = _resolve_entrypoints_py(root_rel, py_dict, manifest_dir)
803
+ except Exception:
804
+ continue
805
+
806
+ pkg_type = _classify_package_type(
807
+ name=pkg_name,
808
+ root_rel=root_rel,
809
+ dependencies=dep_names,
810
+ entrypoints=entrypoints,
811
+ has_bin=has_bin,
812
+ con=con,
813
+ )
814
+
815
+ # Separate source and test directories under this package
816
+ source_dirs: set[str] = set()
817
+ test_dirs: set[str] = set()
818
+ for sub in manifest_dir.iterdir():
819
+ if sub.is_dir() and not sub.name.startswith("."):
820
+ sub_rel = sub.relative_to(root).as_posix()
821
+ if sub.name in ("tests", "test", "__tests__", "spec", "specs"):
822
+ test_dirs.add(sub_rel)
823
+ elif sub.name not in ("node_modules", "dist", "build", ".venv", "vendor"):
824
+ source_dirs.add(sub_rel)
825
+
826
+ info = PackageInfo(
827
+ package_id=package_id,
828
+ name=pkg_name,
829
+ root_path=root_rel,
830
+ manifest_path=rel_manifest,
831
+ language=pkg_lang,
832
+ package_manager=pkg_mgr,
833
+ workspace_id=workspace_id,
834
+ package_type=pkg_type,
835
+ entrypoints=sorted(entrypoints),
836
+ exports=exports,
837
+ dependency_names=sorted(dep_names),
838
+ source_directories=sorted(source_dirs),
839
+ test_directories=sorted(test_dirs),
840
+ metadata=raw_metadata,
841
+ )
842
+
843
+ packages[package_id] = info
844
+ package_by_path[root_rel] = info
845
+ package_by_name.setdefault(pkg_name, []).append(info)
846
+
847
+ ambiguous_packages: dict[str, list[str]] = {
848
+ p_name: sorted(p.package_id for p in p_list)
849
+ for p_name, p_list in sorted(package_by_name.items())
850
+ if len(p_list) > 1
851
+ }
852
+
853
+ # 3. Resolve inter-package dependencies and detect unresolved or ambiguous dependencies
854
+ dependencies: list[PackageDependency] = []
855
+ unresolved_dependencies: list[dict[str, str]] = []
856
+
857
+ for _pkg_id, pkg in packages.items():
858
+ resolved_dep_ids: list[str] = []
859
+ raw_deps: dict[str, Any] = {}
860
+ if pkg.manifest_path.endswith("package.json") and isinstance(pkg.metadata, dict):
861
+ for k in ("dependencies", "devDependencies", "peerDependencies"):
862
+ d_sec = pkg.metadata.get(k)
863
+ if isinstance(d_sec, dict):
864
+ raw_deps.update(d_sec)
865
+
866
+ for dep_name in pkg.dependency_names:
867
+ spec = str(raw_deps.get(dep_name, ""))
868
+ is_ws_spec = "workspace:" in spec
869
+ target_pkgs = [p for p in (package_by_name.get(dep_name) or []) if p.package_id != pkg.package_id]
870
+
871
+ if len(target_pkgs) == 1:
872
+ target_pkg = target_pkgs[0]
873
+ dependencies.append(
874
+ PackageDependency(
875
+ source_package_id=pkg.package_id,
876
+ target_package_id=target_pkg.package_id,
877
+ source_name=pkg.name,
878
+ target_name=target_pkg.name,
879
+ specifier=spec,
880
+ evidence=f"{pkg.manifest_path}:dependencies['{dep_name}']",
881
+ is_workspace=is_ws_spec,
882
+ )
883
+ )
884
+ resolved_dep_ids.append(target_pkg.package_id)
885
+ elif len(target_pkgs) > 1:
886
+ # Duplicate package name in workspace: never guess target_pkgs[0]
887
+ unresolved_dependencies.append({
888
+ "source_package_id": pkg.package_id,
889
+ "dependency_name": dep_name,
890
+ "specifier": spec,
891
+ "status": "AMBIGUOUS",
892
+ "reason": "ambiguous_duplicate_package_name",
893
+ "candidates": ",".join(sorted(p.package_id for p in target_pkgs)),
894
+ "evidence": f"{pkg.manifest_path}:dependencies['{dep_name}']",
895
+ })
896
+ elif is_ws_spec:
897
+ # workspace:* dependency requested but package not found
898
+ unresolved_dependencies.append({
899
+ "source_package_id": pkg.package_id,
900
+ "dependency_name": dep_name,
901
+ "specifier": spec,
902
+ "status": "UNRESOLVED",
903
+ "reason": "unresolved_workspace_dependency",
904
+ "evidence": f"{pkg.manifest_path}:dependencies['{dep_name}']={spec}",
905
+ })
906
+
907
+ pkg.dependency_package_ids = sorted(set(resolved_dep_ids))
908
+
909
+ dependencies.sort(key=lambda d: (d.source_package_id, d.target_package_id))
910
+ unresolved_dependencies.sort(key=lambda u: (u["source_package_id"], u["dependency_name"]))
911
+
912
+ return WorkspaceInfo(
913
+ workspace_id=workspace_id,
914
+ root_path="",
915
+ workspace_type=workspace_type,
916
+ packages=packages,
917
+ package_by_name=package_by_name,
918
+ package_by_path=package_by_path,
919
+ dependencies=dependencies,
920
+ unresolved_dependencies=unresolved_dependencies,
921
+ ambiguous_packages=ambiguous_packages,
922
+ )
923
+
924
+
925
+ discover_workspace = detect_workspace