sourcecode 0.34.0__tar.gz → 0.36.0__tar.gz
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-0.34.0 → sourcecode-0.36.0}/PKG-INFO +1 -1
- {sourcecode-0.34.0 → sourcecode-0.36.0}/pyproject.toml +1 -1
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/ast_extractor.py +44 -9
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/contract_pipeline.py +225 -20
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/relevance_scorer.py +18 -5
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/serializer.py +3 -1
- {sourcecode-0.34.0 → sourcecode-0.36.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/.gitignore +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/.ruff.toml +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/CONTRIBUTING.md +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/LICENSE +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/README.md +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/SECURITY.md +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/docs/privacy.md +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/docs/schema.md +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/raw +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/__init__.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/conftest.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_architecture_summary.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_ast_extractor.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_cli.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_contract_pipeline.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_dependency_analyzer_node_python.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_detector_dotnet.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_hybrid_inference.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_dependencies.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_phase1_improvements.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_pipeline_integrity.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_real_projects.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_schema.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_signal_hierarchy.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_summarizer.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_telemetry.py +0 -0
- {sourcecode-0.34.0 → sourcecode-0.36.0}/tests/test_workspace_analyzer.py +0 -0
|
@@ -296,11 +296,20 @@ def _ts_exports(root: Any, src: bytes) -> list[ExportRecord]:
|
|
|
296
296
|
handled = True
|
|
297
297
|
|
|
298
298
|
if not handled and is_default:
|
|
299
|
-
# export default <expression>
|
|
299
|
+
# export default <expression> — preserve local binding name when available
|
|
300
300
|
for child in node.children:
|
|
301
301
|
if child.type not in ("export", "default", ";") and not child.type.startswith("comment"):
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
if child.type in ("identifier", "type_identifier"):
|
|
303
|
+
# export default app → name="app"
|
|
304
|
+
name = _text(child, src)
|
|
305
|
+
elif child.type == "call_expression":
|
|
306
|
+
# export default defineConfig({}) → name="defineConfig"
|
|
307
|
+
fn_n = _find_child(child, "identifier")
|
|
308
|
+
name = _text(fn_n, src) if fn_n else "default"
|
|
309
|
+
else:
|
|
310
|
+
# object/array/other expression — look one level deep
|
|
311
|
+
name_n = _find_child(child, "identifier", "type_identifier")
|
|
312
|
+
name = _text(name_n, src) if name_n else "default"
|
|
304
313
|
records.append(ExportRecord(name=name, kind="default"))
|
|
305
314
|
break
|
|
306
315
|
|
|
@@ -373,7 +382,8 @@ def _ts_types(root: Any, src: bytes) -> list[TypeDefinition]:
|
|
|
373
382
|
continue
|
|
374
383
|
name = _text(name_n, src)
|
|
375
384
|
fields: list[TypeField] = []
|
|
376
|
-
|
|
385
|
+
# "interface_body" in tree-sitter-typescript >= 0.21; "object_type" in older builds
|
|
386
|
+
body_n = _find_child(node, "interface_body", "object_type")
|
|
377
387
|
if body_n:
|
|
378
388
|
for prop in _walk(body_n):
|
|
379
389
|
if prop.type in ("property_signature", "method_signature"):
|
|
@@ -385,7 +395,7 @@ def _ts_types(root: Any, src: bytes) -> list[TypeDefinition]:
|
|
|
385
395
|
required = not any(c.type == "?" for c in prop.children)
|
|
386
396
|
fields.append(TypeField(name=prop_name, type=type_text, required=required))
|
|
387
397
|
extends: list[str] = []
|
|
388
|
-
heritage_n = _find_child(node, "extends_type_clause", "class_heritage")
|
|
398
|
+
heritage_n = _find_child(node, "extends_type_clause", "extends_clause", "class_heritage")
|
|
389
399
|
if heritage_n:
|
|
390
400
|
for ext_n in _walk(heritage_n):
|
|
391
401
|
if ext_n.type == "type_identifier":
|
|
@@ -429,6 +439,25 @@ def _ts_hooks(root: Any, src: bytes) -> list[str]:
|
|
|
429
439
|
return sorted(used)
|
|
430
440
|
|
|
431
441
|
|
|
442
|
+
def _merge_imports(imports: list[ImportRecord]) -> list[ImportRecord]:
|
|
443
|
+
"""Merge multiple ImportRecords with the same source into one.
|
|
444
|
+
|
|
445
|
+
Tree-sitter correctly captures `import { A }` and `import type { B }` from
|
|
446
|
+
the same module as two separate statements. Merging them produces a compact,
|
|
447
|
+
predictable contract where each source appears exactly once.
|
|
448
|
+
"""
|
|
449
|
+
merged: dict[str, ImportRecord] = {}
|
|
450
|
+
for imp in imports:
|
|
451
|
+
if imp.source in merged:
|
|
452
|
+
existing = merged[imp.source]
|
|
453
|
+
combined_symbols = sorted(set(existing.symbols) | set(imp.symbols))
|
|
454
|
+
kind = existing.kind if existing.kind != "side_effect" else imp.kind
|
|
455
|
+
merged[imp.source] = ImportRecord(source=imp.source, symbols=combined_symbols, kind=kind)
|
|
456
|
+
else:
|
|
457
|
+
merged[imp.source] = imp
|
|
458
|
+
return list(merged.values())
|
|
459
|
+
|
|
460
|
+
|
|
432
461
|
def _extract_ts_js_tree_sitter(path: str, source: str, lang_obj: Any, language: str) -> FileContract:
|
|
433
462
|
try:
|
|
434
463
|
parser = _get_parser(lang_obj)
|
|
@@ -436,7 +465,7 @@ def _extract_ts_js_tree_sitter(path: str, source: str, lang_obj: Any, language:
|
|
|
436
465
|
tree = parser.parse(src_bytes)
|
|
437
466
|
root = tree.root_node
|
|
438
467
|
|
|
439
|
-
imports = _ts_imports(root, src_bytes)
|
|
468
|
+
imports = _merge_imports(_ts_imports(root, src_bytes))
|
|
440
469
|
exports = _ts_exports(root, src_bytes)
|
|
441
470
|
exported_names = {e.name for e in exports}
|
|
442
471
|
functions = _ts_functions(root, src_bytes, exported_names)
|
|
@@ -672,7 +701,7 @@ def _heuristic_ts_types(source: str) -> list[TypeDefinition]:
|
|
|
672
701
|
return types
|
|
673
702
|
|
|
674
703
|
|
|
675
|
-
def _extract_ts_js_heuristic(path: str, source: str, language: str) -> FileContract:
|
|
704
|
+
def _extract_ts_js_heuristic(path: str, source: str, language: str, *, ts_installed: bool = False) -> FileContract:
|
|
676
705
|
imports = _heuristic_ts_imports(source)
|
|
677
706
|
exports = _heuristic_ts_exports(source)
|
|
678
707
|
exported_names = {e.name for e in exports}
|
|
@@ -691,6 +720,12 @@ def _extract_ts_js_heuristic(path: str, source: str, language: str) -> FileContr
|
|
|
691
720
|
if not imp.source.startswith(".") and not imp.source.startswith("/")
|
|
692
721
|
})
|
|
693
722
|
|
|
723
|
+
# Distinguish "tree-sitter absent" from "language parser not loaded"
|
|
724
|
+
if ts_installed:
|
|
725
|
+
lim = f"ts_lang_missing: tree-sitter parser for {language!r} not loaded; install sourcecode[ast]"
|
|
726
|
+
else:
|
|
727
|
+
lim = "tree_sitter_unavailable: install sourcecode[ast] for full TS/JS extraction"
|
|
728
|
+
|
|
694
729
|
return FileContract(
|
|
695
730
|
path=path,
|
|
696
731
|
language=language,
|
|
@@ -701,7 +736,7 @@ def _extract_ts_js_heuristic(path: str, source: str, language: str) -> FileContr
|
|
|
701
736
|
hooks_used=hooks_used,
|
|
702
737
|
dependencies=deps,
|
|
703
738
|
extraction_method="heuristic",
|
|
704
|
-
limitations=[
|
|
739
|
+
limitations=[lim],
|
|
705
740
|
)
|
|
706
741
|
|
|
707
742
|
|
|
@@ -1019,7 +1054,7 @@ class AstExtractor:
|
|
|
1019
1054
|
if lang_obj is not None:
|
|
1020
1055
|
contract = _extract_ts_js_tree_sitter(rel_path, source, lang_obj, language)
|
|
1021
1056
|
else:
|
|
1022
|
-
contract = _extract_ts_js_heuristic(rel_path, source, language)
|
|
1057
|
+
contract = _extract_ts_js_heuristic(rel_path, source, language, ts_installed=True)
|
|
1023
1058
|
else:
|
|
1024
1059
|
contract = _extract_ts_js_heuristic(rel_path, source, language)
|
|
1025
1060
|
|
|
@@ -8,6 +8,8 @@ Produces a list of FileContracts ranked by semantic importance,
|
|
|
8
8
|
with fan-in/fan-out computed from the import graph.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
11
13
|
import subprocess
|
|
12
14
|
from collections import Counter
|
|
13
15
|
from pathlib import Path
|
|
@@ -25,6 +27,23 @@ from sourcecode.schema import EntryPoint, MonorepoPackageInfo
|
|
|
25
27
|
_MAX_FILES = 500 # hard cap on files extracted per run
|
|
26
28
|
_SRC_EXTENSIONS: frozenset[str] = frozenset(_LANGUAGE_MAP.keys())
|
|
27
29
|
|
|
30
|
+
# Role-based score adjustments applied after contract extraction.
|
|
31
|
+
# Runtime roles get a boost; config/util are neutral or penalized.
|
|
32
|
+
_ROLE_SCORE: dict[str, float] = {
|
|
33
|
+
"entrypoint": 0.15,
|
|
34
|
+
"service": 0.10,
|
|
35
|
+
"route": 0.10,
|
|
36
|
+
"api": 0.08,
|
|
37
|
+
"middleware": 0.06,
|
|
38
|
+
"store": 0.05,
|
|
39
|
+
"model": 0.05,
|
|
40
|
+
"hook": 0.05,
|
|
41
|
+
"component": 0.03,
|
|
42
|
+
"util": 0.00,
|
|
43
|
+
"config": -0.10,
|
|
44
|
+
"unknown": 0.00,
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
RankStrategy = Literal["relevance", "centrality", "git-churn"]
|
|
29
48
|
|
|
30
49
|
|
|
@@ -206,9 +225,9 @@ class ContractPipeline:
|
|
|
206
225
|
if changed_only:
|
|
207
226
|
src_paths = [p for p in src_paths if p in changed_files]
|
|
208
227
|
|
|
209
|
-
# Apply max_files cap
|
|
210
|
-
|
|
211
|
-
|
|
228
|
+
# Apply max_files cap — bypass when symbol search to ensure defining files are found.
|
|
229
|
+
# A symbol query over a large repo needs all files; result set is small after filtering.
|
|
230
|
+
if symbol is None and len(src_paths) > self.max_files:
|
|
212
231
|
src_paths = sorted(
|
|
213
232
|
src_paths,
|
|
214
233
|
key=lambda p: (p in entry_paths, scorer.score(p)),
|
|
@@ -255,23 +274,19 @@ class ContractPipeline:
|
|
|
255
274
|
# 7. Rank
|
|
256
275
|
contracts = self._rank(contracts, rank_by)
|
|
257
276
|
|
|
258
|
-
# 8. Symbol filter — keep files that
|
|
277
|
+
# 8. Symbol filter — keep files that define or import the symbol
|
|
259
278
|
if symbol:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
importer_contracts = [c for c in contracts if c.path in importer_paths]
|
|
273
|
-
symbol_contracts = list({c.path: c for c in symbol_contracts + importer_contracts}.values())
|
|
274
|
-
contracts = sorted(symbol_contracts, key=lambda c: -c.relevance_score)
|
|
279
|
+
contracts = _filter_by_symbol(contracts, symbol)
|
|
280
|
+
# When shallow scan missed the defining file (deep monorepo), fall back
|
|
281
|
+
# to a grep-based filesystem search over the full directory tree.
|
|
282
|
+
if not contracts:
|
|
283
|
+
contracts = self._symbol_deep_scan(
|
|
284
|
+
root, symbol,
|
|
285
|
+
known_paths=set(src_paths),
|
|
286
|
+
entry_paths=entry_paths,
|
|
287
|
+
changed_files=changed_files,
|
|
288
|
+
scorer=scorer,
|
|
289
|
+
)
|
|
275
290
|
|
|
276
291
|
# 9. Entrypoints-only filter
|
|
277
292
|
if entrypoints_only and not symbol:
|
|
@@ -323,6 +338,9 @@ class ContractPipeline:
|
|
|
323
338
|
churn_score = min(churn.get(c.path, 0) / 20.0, 0.1)
|
|
324
339
|
base += churn_score
|
|
325
340
|
|
|
341
|
+
# Role-based boost: runtime roles score higher than auxiliary
|
|
342
|
+
base += _ROLE_SCORE.get(c.role, 0.0)
|
|
343
|
+
|
|
326
344
|
return min(1.0, base)
|
|
327
345
|
|
|
328
346
|
def _rank(self, contracts: list[FileContract], rank_by: RankStrategy) -> list[FileContract]:
|
|
@@ -334,6 +352,38 @@ class ContractPipeline:
|
|
|
334
352
|
# Default: relevance
|
|
335
353
|
return sorted(contracts, key=lambda c: (-c.is_entrypoint, -c.relevance_score))
|
|
336
354
|
|
|
355
|
+
def _symbol_deep_scan(
|
|
356
|
+
self,
|
|
357
|
+
root: Path,
|
|
358
|
+
symbol: str,
|
|
359
|
+
known_paths: set[str],
|
|
360
|
+
entry_paths: set[str],
|
|
361
|
+
changed_files: set[str],
|
|
362
|
+
scorer: RelevanceScorer,
|
|
363
|
+
) -> list[FileContract]:
|
|
364
|
+
"""Grep-based fallback when the shallow scan missed the defining files.
|
|
365
|
+
|
|
366
|
+
Searches the full directory tree for source files containing *symbol*,
|
|
367
|
+
extracts contracts for candidates not already processed, then re-applies
|
|
368
|
+
the symbol filter. Fan-in/fan-out are not computed for these contracts.
|
|
369
|
+
"""
|
|
370
|
+
candidates = _find_symbol_files(root, symbol, known_paths, scorer)
|
|
371
|
+
if not candidates:
|
|
372
|
+
return []
|
|
373
|
+
|
|
374
|
+
extra: list[FileContract] = []
|
|
375
|
+
for rel_path in candidates[:300]: # cap to prevent excessive extraction
|
|
376
|
+
abs_path = root / rel_path
|
|
377
|
+
contract = self._extractor.extract(abs_path, root)
|
|
378
|
+
if contract is None:
|
|
379
|
+
continue
|
|
380
|
+
contract.is_entrypoint = rel_path in entry_paths
|
|
381
|
+
contract.is_changed = rel_path in changed_files
|
|
382
|
+
contract.relevance_score = scorer.score(rel_path)
|
|
383
|
+
extra.append(contract)
|
|
384
|
+
|
|
385
|
+
return _filter_by_symbol(extra, symbol)
|
|
386
|
+
|
|
337
387
|
|
|
338
388
|
# ---------------------------------------------------------------------------
|
|
339
389
|
# Helpers
|
|
@@ -348,7 +398,6 @@ def _compress_contract_types(c: FileContract) -> None:
|
|
|
348
398
|
(r"React\.ReactNode", "ReactNode"),
|
|
349
399
|
(r"React\.ReactElement", "ReactElement"),
|
|
350
400
|
]
|
|
351
|
-
import re
|
|
352
401
|
for fn in c.functions:
|
|
353
402
|
for pattern, repl in _replacements:
|
|
354
403
|
fn.signature = re.sub(pattern, repl, fn.signature)
|
|
@@ -385,6 +434,162 @@ def _limit_symbols(contracts: list[FileContract], max_symbols: int) -> list[File
|
|
|
385
434
|
return result
|
|
386
435
|
|
|
387
436
|
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
# Symbol-aware filter
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileContract]:
|
|
442
|
+
"""Return contracts that define, import, or structurally reference *symbol*.
|
|
443
|
+
|
|
444
|
+
Four tiers applied in order:
|
|
445
|
+
1. Exact name match — export/function/type names.
|
|
446
|
+
2. Case-insensitive name match when tier 1 yields nothing.
|
|
447
|
+
3. Import symbol match — name appears in import symbol list.
|
|
448
|
+
4. Type-reference match — symbol in extends clauses, field types, or
|
|
449
|
+
function signatures (word-boundary). Only used when tiers 1-3 fail.
|
|
450
|
+
|
|
451
|
+
Defining contracts are ranked first; importers and references follow.
|
|
452
|
+
"""
|
|
453
|
+
sym_l = symbol.lower()
|
|
454
|
+
word_re = re.compile(
|
|
455
|
+
r"(?<![A-Za-z0-9_])" + re.escape(symbol) + r"(?![A-Za-z0-9_])",
|
|
456
|
+
re.IGNORECASE,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def _defines(c: FileContract, case: bool) -> bool:
|
|
460
|
+
cmp = (lambda a, b: a.lower() == b.lower()) if case else (lambda a, b: a == b)
|
|
461
|
+
return (
|
|
462
|
+
any(cmp(e.name, symbol) for e in c.exports)
|
|
463
|
+
or any(cmp(f.name, symbol) for f in c.functions)
|
|
464
|
+
or any(cmp(t.name, symbol) for t in c.types)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def _imports_sym(c: FileContract, case: bool) -> bool:
|
|
468
|
+
if case:
|
|
469
|
+
return any(sym_l == s.lower() for imp in c.imports for s in imp.symbols)
|
|
470
|
+
return any(symbol in imp.symbols for imp in c.imports)
|
|
471
|
+
|
|
472
|
+
def _references_type(c: FileContract) -> bool:
|
|
473
|
+
"""Tier 4: symbol appears in extends clauses, field types, or signatures."""
|
|
474
|
+
for t in c.types:
|
|
475
|
+
if any(sym_l in ext.lower() for ext in t.extends):
|
|
476
|
+
return True
|
|
477
|
+
for field in t.fields:
|
|
478
|
+
if sym_l in field.type.lower():
|
|
479
|
+
return True
|
|
480
|
+
for f in c.functions:
|
|
481
|
+
if word_re.search(f.signature):
|
|
482
|
+
return True
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
# Tier 1: exact name match
|
|
486
|
+
defining = [c for c in contracts if _defines(c, case=False)]
|
|
487
|
+
# Tier 2: case-insensitive name match
|
|
488
|
+
if not defining:
|
|
489
|
+
defining = [c for c in contracts if _defines(c, case=True)]
|
|
490
|
+
|
|
491
|
+
defining_paths = {c.path for c in defining}
|
|
492
|
+
|
|
493
|
+
# Tier 3: import matching (case-insensitive when no definers found)
|
|
494
|
+
ci_imports = len(defining) == 0
|
|
495
|
+
importer_paths = {c.path for c in contracts if _imports_sym(c, case=ci_imports)}
|
|
496
|
+
importers = [c for c in contracts if c.path in importer_paths and c.path not in defining_paths]
|
|
497
|
+
|
|
498
|
+
# Tier 4: type-reference matching (only when tiers 1-3 yield nothing)
|
|
499
|
+
references: list[FileContract] = []
|
|
500
|
+
if not defining and not importers:
|
|
501
|
+
ref_paths = {c.path for c in contracts if _references_type(c)}
|
|
502
|
+
references = [c for c in contracts if c.path in ref_paths]
|
|
503
|
+
|
|
504
|
+
# Merge in priority order: defining > importers > type-references
|
|
505
|
+
seen: set[str] = set()
|
|
506
|
+
merged: list[FileContract] = []
|
|
507
|
+
for c in defining + importers + references:
|
|
508
|
+
if c.path not in seen:
|
|
509
|
+
seen.add(c.path)
|
|
510
|
+
merged.append(c)
|
|
511
|
+
|
|
512
|
+
return sorted(merged, key=lambda c: (
|
|
513
|
+
c.path not in defining_paths,
|
|
514
|
+
c.path not in importer_paths,
|
|
515
|
+
-c.relevance_score,
|
|
516
|
+
))
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# ---------------------------------------------------------------------------
|
|
520
|
+
# Deep symbol scan — grep-based fallback for shallow-scanned repos
|
|
521
|
+
# ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
_DEEP_SCAN_NOISE_DIRS: frozenset[str] = frozenset({
|
|
524
|
+
"node_modules", ".git", "dist", "build", "__pycache__",
|
|
525
|
+
".venv", "venv", "target", ".next", ".nuxt", ".turbo", "coverage",
|
|
526
|
+
".nyc_output", ".mypy_cache", ".pytest_cache",
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _find_symbol_files(
|
|
531
|
+
root: Path,
|
|
532
|
+
symbol: str,
|
|
533
|
+
known_paths: set[str],
|
|
534
|
+
scorer: RelevanceScorer,
|
|
535
|
+
) -> list[str]:
|
|
536
|
+
"""Find source files outside *known_paths* that contain *symbol* as text.
|
|
537
|
+
|
|
538
|
+
Uses subprocess grep when available (fast); falls back to os.walk + read.
|
|
539
|
+
Returns repo-relative paths, noise-filtered.
|
|
540
|
+
"""
|
|
541
|
+
found: list[str] = []
|
|
542
|
+
|
|
543
|
+
# Try grep (fast, available on Linux/Mac)
|
|
544
|
+
try:
|
|
545
|
+
result = subprocess.run(
|
|
546
|
+
[
|
|
547
|
+
"grep", "-rl",
|
|
548
|
+
"--include=*.ts", "--include=*.tsx",
|
|
549
|
+
"--include=*.js", "--include=*.jsx",
|
|
550
|
+
"--include=*.py",
|
|
551
|
+
symbol, ".",
|
|
552
|
+
],
|
|
553
|
+
cwd=str(root),
|
|
554
|
+
capture_output=True,
|
|
555
|
+
text=True,
|
|
556
|
+
timeout=20,
|
|
557
|
+
)
|
|
558
|
+
for line in result.stdout.splitlines():
|
|
559
|
+
line = line.strip()
|
|
560
|
+
if line.startswith("./"):
|
|
561
|
+
line = line[2:]
|
|
562
|
+
line = line.replace("\\", "/")
|
|
563
|
+
if line and line not in known_paths and not scorer.is_noise(line):
|
|
564
|
+
found.append(line)
|
|
565
|
+
return found
|
|
566
|
+
except Exception:
|
|
567
|
+
pass
|
|
568
|
+
|
|
569
|
+
# Python fallback — os.walk + text search
|
|
570
|
+
for dirpath, dirnames, filenames in os.walk(str(root)):
|
|
571
|
+
dirnames[:] = sorted(d for d in dirnames if d not in _DEEP_SCAN_NOISE_DIRS)
|
|
572
|
+
for fname in filenames:
|
|
573
|
+
if Path(fname).suffix.lower() not in _SRC_EXTENSIONS:
|
|
574
|
+
continue
|
|
575
|
+
full = os.path.join(dirpath, fname)
|
|
576
|
+
try:
|
|
577
|
+
rel = Path(full).relative_to(root)
|
|
578
|
+
rel_str = str(rel).replace("\\", "/")
|
|
579
|
+
except ValueError:
|
|
580
|
+
continue
|
|
581
|
+
if rel_str in known_paths or scorer.is_noise(rel_str):
|
|
582
|
+
continue
|
|
583
|
+
try:
|
|
584
|
+
content = Path(full).read_text(encoding="utf-8", errors="replace")
|
|
585
|
+
if symbol in content:
|
|
586
|
+
found.append(rel_str)
|
|
587
|
+
except OSError:
|
|
588
|
+
pass
|
|
589
|
+
|
|
590
|
+
return found
|
|
591
|
+
|
|
592
|
+
|
|
388
593
|
# ---------------------------------------------------------------------------
|
|
389
594
|
# Dependency graph emission
|
|
390
595
|
# ---------------------------------------------------------------------------
|
|
@@ -104,6 +104,18 @@ _LOW_RUNTIME_STEMS: frozenset[str] = frozenset({
|
|
|
104
104
|
"gruntfile", "gulpfile", "webpack.config", "vite.config",
|
|
105
105
|
"rollup.config", "babel.config", "jest.config", "vitest.config",
|
|
106
106
|
"tsconfig", "jsconfig", ".eslintrc", ".prettierrc", ".editorconfig",
|
|
107
|
+
# doc-site tooling
|
|
108
|
+
"rspress", "rspress.config", "docusaurus.config", "docusaurus",
|
|
109
|
+
"vuepress.config", "vuepress", "nextra.config",
|
|
110
|
+
"astro.config", "gatsby.config", "gatsby-config",
|
|
111
|
+
# build/workspace orchestration
|
|
112
|
+
"turbo", "turbo.config", "nx", "nx.config", "lerna",
|
|
113
|
+
"esbuild.config", "swc.config", "postcss.config",
|
|
114
|
+
"tailwind.config", "tailwind",
|
|
115
|
+
# storybook
|
|
116
|
+
"main.storybook", "preview.storybook",
|
|
117
|
+
# playwright / cypress / e2e
|
|
118
|
+
"playwright.config", "cypress.config",
|
|
107
119
|
})
|
|
108
120
|
|
|
109
121
|
_HIGH_VALUE_SUFFIXES: frozenset[str] = frozenset({
|
|
@@ -169,15 +181,16 @@ class RelevanceScorer:
|
|
|
169
181
|
if (any(m in f"/{norm}/" for m in _TEST_DIR_MARKERS)
|
|
170
182
|
or any(fname.startswith(p.strip(".")) or p in fname
|
|
171
183
|
for p in _TEST_FILE_PATTERNS)):
|
|
172
|
-
base -= 0.
|
|
184
|
+
base -= 0.30
|
|
173
185
|
|
|
174
|
-
# Config/tooling filename penalty
|
|
186
|
+
# Config/tooling filename penalty — stronger than before
|
|
175
187
|
if stem.lower() in _LOW_RUNTIME_STEMS:
|
|
176
|
-
base -= 0.
|
|
188
|
+
base -= 0.30
|
|
177
189
|
|
|
178
|
-
# Auxiliary dir penalty
|
|
190
|
+
# Auxiliary dir penalty (docs, examples, demos, fixtures, scripts…)
|
|
191
|
+
# Aggressive: these almost never belong in top-ranked agent context
|
|
179
192
|
if self._is_auxiliary(norm):
|
|
180
|
-
base -= 0.
|
|
193
|
+
base -= 0.40
|
|
181
194
|
|
|
182
195
|
return max(0.0, min(1.0, base))
|
|
183
196
|
|
|
@@ -964,7 +964,9 @@ def _contract_view_minimal(
|
|
|
964
964
|
# Compact summary
|
|
965
965
|
if sm.contract_summary is not None:
|
|
966
966
|
cs = sm.contract_summary
|
|
967
|
-
degraded
|
|
967
|
+
# degraded only when tree-sitter is actually unavailable — not when individual
|
|
968
|
+
# files fall back due to parse errors or size limits.
|
|
969
|
+
degraded = any("tree_sitter_unavailable" in lim for lim in cs.limitations)
|
|
968
970
|
summary: dict[str, Any] = {
|
|
969
971
|
"files": cs.extracted_files,
|
|
970
972
|
"total": cs.total_files,
|
{sourcecode-0.34.0 → sourcecode-0.36.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md
RENAMED
|
File without changes
|
{sourcecode-0.34.0 → sourcecode-0.36.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md
RENAMED
|
File without changes
|
{sourcecode-0.34.0 → sourcecode-0.36.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sourcecode-0.34.0 → sourcecode-0.36.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|