codd-dev 0.5.0__tar.gz → 0.6.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.
Files changed (39) hide show
  1. {codd_dev-0.5.0 → codd_dev-0.6.0}/PKG-INFO +1 -1
  2. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/parsing.py +101 -0
  3. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/scanner.py +63 -55
  4. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/synth.py +6 -0
  5. {codd_dev-0.5.0 → codd_dev-0.6.0}/pyproject.toml +1 -1
  6. {codd_dev-0.5.0 → codd_dev-0.6.0}/.gitignore +0 -0
  7. {codd_dev-0.5.0 → codd_dev-0.6.0}/LICENSE +0 -0
  8. {codd_dev-0.5.0 → codd_dev-0.6.0}/README.md +0 -0
  9. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/__init__.py +0 -0
  10. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/cli.py +0 -0
  11. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/clustering.py +0 -0
  12. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/config.py +0 -0
  13. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/contracts.py +0 -0
  14. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/defaults.yaml +0 -0
  15. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/extractor.py +0 -0
  16. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/generator.py +0 -0
  17. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/graph.py +0 -0
  18. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/hooks.py +0 -0
  19. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/implementer.py +0 -0
  20. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/planner.py +0 -0
  21. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/propagate.py +0 -0
  22. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/risk.py +0 -0
  23. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/schema_refs.py +0 -0
  24. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/codd.yaml.tmpl +0 -0
  25. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/conventions.yaml.tmpl +0 -0
  26. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
  27. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/doc_links.yaml.tmpl +0 -0
  28. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
  29. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
  30. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
  31. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
  32. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/system-context.md.j2 +0 -0
  33. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/gitignore.tmpl +0 -0
  34. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/overrides.yaml.tmpl +0 -0
  35. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/traceability.py +0 -0
  36. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/validator.py +0 -0
  37. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/verifier.py +0 -0
  38. {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/wiring.py +0 -0
  39. {codd_dev-0.5.0 → codd_dev-0.6.0}/hooks/pre-commit +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codd-dev
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: CoDD: Coherence-Driven Development — cross-artifact change impact analysis
5
5
  Project-URL: Homepage, https://github.com/yohey-w/codd-dev
6
6
  Project-URL: Repository, https://github.com/yohey-w/codd-dev
@@ -301,6 +301,8 @@ class TreeSitterExtractor:
301
301
  root = self._parse(content)
302
302
  if self.language == "python":
303
303
  return _extract_python_call_graph(root, content, file_path, symbols)
304
+ if self.language in {"typescript", "javascript"}:
305
+ return _extract_ts_call_graph(root, content, file_path, symbols)
304
306
  except Exception:
305
307
  return []
306
308
  return []
@@ -1009,6 +1011,105 @@ def _extract_python_call_graph(root: Any, content: str, file_path: str, symbols:
1009
1011
  return edges
1010
1012
 
1011
1013
 
1014
+ _TS_BUILTIN_NAMES = {
1015
+ "console", "Math", "JSON", "Object", "Array", "Promise",
1016
+ "setTimeout", "setInterval", "clearTimeout", "clearInterval",
1017
+ "require", "parseInt", "parseFloat", "isNaN",
1018
+ "encodeURIComponent", "decodeURIComponent",
1019
+ }
1020
+
1021
+
1022
+ def _extract_ts_call_graph(root: Any, content: str, file_path: str, symbols: list[Symbol]) -> list[CallEdge]:
1023
+ """Extract function call edges from TypeScript/JavaScript AST using tree-sitter."""
1024
+ from codd.extractor import CallEdge
1025
+
1026
+ content_bytes = content.encode("utf-8", errors="ignore")
1027
+ edges: list[CallEdge] = []
1028
+ symbol_names = {s.name for s in symbols}
1029
+
1030
+ def _current_scope(node: Any) -> str:
1031
+ """Walk parents to find enclosing function/method/class scope."""
1032
+ parts: list[str] = []
1033
+ current = node.parent
1034
+ while current is not None:
1035
+ if current.type in (
1036
+ "function_declaration",
1037
+ "function",
1038
+ "method_definition",
1039
+ "arrow_function",
1040
+ "class_declaration",
1041
+ "class",
1042
+ ):
1043
+ name = _field_text(content_bytes, current, "name")
1044
+ if name:
1045
+ parts.append(name)
1046
+ current = current.parent
1047
+ parts.reverse()
1048
+ return ".".join(parts) if parts else "<module>"
1049
+
1050
+ def _callee_name(func_node: Any) -> str | None:
1051
+ """Extract callee name from the function child of a call/new expression."""
1052
+ if func_node is None:
1053
+ return None
1054
+ node_type = func_node.type
1055
+ if node_type == "identifier":
1056
+ return _node_text(content_bytes, func_node).strip()
1057
+ if node_type in ("member_expression", "optional_chain"):
1058
+ obj = func_node.child_by_field_name("object")
1059
+ prop = func_node.child_by_field_name("property")
1060
+ if obj is not None and prop is not None:
1061
+ obj_text = _node_text(content_bytes, obj).strip()
1062
+ prop_text = _node_text(content_bytes, prop).strip()
1063
+ return f"{obj_text}.{prop_text}"
1064
+ # Fallback: return full text
1065
+ return _node_text(content_bytes, func_node).strip()
1066
+ # Other node types (parenthesized_expression, etc.) — use full text
1067
+ return _node_text(content_bytes, func_node).strip()
1068
+
1069
+ for node in _iter_named_nodes(root):
1070
+ if node.type == "call_expression":
1071
+ func_node = node.child_by_field_name("function")
1072
+ is_async = node.parent is not None and node.parent.type == "await_expression"
1073
+ elif node.type == "new_expression":
1074
+ func_node = node.child_by_field_name("constructor")
1075
+ is_async = False
1076
+ else:
1077
+ continue
1078
+
1079
+ callee_text = _callee_name(func_node)
1080
+ if not callee_text:
1081
+ continue
1082
+
1083
+ bare_name = callee_text.split(".")[-1] if "." in callee_text else callee_text
1084
+
1085
+ # Skip known JS/TS builtins
1086
+ root_name = callee_text.split(".")[0]
1087
+ if root_name in _TS_BUILTIN_NAMES or bare_name in _TS_BUILTIN_NAMES:
1088
+ continue
1089
+
1090
+ # Only include calls to known project symbols (intra-project filter)
1091
+ if bare_name not in symbol_names and callee_text not in symbol_names:
1092
+ # Allow method calls on this/self (this.method)
1093
+ if callee_text.startswith("this."):
1094
+ method_name = callee_text[5:]
1095
+ if method_name not in symbol_names:
1096
+ continue
1097
+ else:
1098
+ continue
1099
+
1100
+ caller = _current_scope(node)
1101
+ line_no = node.start_point.row + 1
1102
+
1103
+ edges.append(CallEdge(
1104
+ caller=caller,
1105
+ callee=callee_text,
1106
+ call_site=f"{file_path}:{line_no}",
1107
+ is_async=is_async,
1108
+ ))
1109
+
1110
+ return edges
1111
+
1112
+
1012
1113
  def _sql_first_object_name(content_bytes: bytes, node: Any) -> str:
1013
1114
  for child in getattr(node, "named_children", []):
1014
1115
  if child.type == "object_reference":
@@ -11,10 +11,10 @@ import re
11
11
  from pathlib import Path
12
12
  from typing import Any
13
13
 
14
- import yaml
15
-
16
- from codd.graph import CEG
17
- from codd.parsing import get_extractor
14
+ import yaml
15
+
16
+ from codd.graph import CEG
17
+ from codd.parsing import get_extractor
18
18
 
19
19
 
20
20
  def run_scan(project_root: Path, codd_dir: Path):
@@ -228,6 +228,14 @@ def _load_frontmatter(ceg: CEG, doc_path: str, codd: dict):
228
228
  ceg.add_evidence(edge_id, "frontmatter", "frontmatter", 0.75,
229
229
  detail=data_dep.get("condition", ""))
230
230
 
231
+ # R6.2: source_files bridge edges (extracted design → source file)
232
+ for source_file in codd.get("source_files", []):
233
+ file_node_id = f"file:{source_file}"
234
+ ceg.upsert_node(file_node_id, "file", path=source_file, name=file_node_id)
235
+ edge_id = ceg.add_edge(node_id, file_node_id, "extracted_from", "technical")
236
+ ceg.add_evidence(edge_id, "frontmatter", "source_files", 0.85,
237
+ detail=f"design doc maps to source file {source_file}")
238
+
231
239
 
232
240
  # ═══════════════════════════════════════════════════════════
233
241
  # Legacy: annotations/ YAML support (backward compatibility)
@@ -323,8 +331,8 @@ def _load_legacy_data_dependency(ceg: CEG, dep: dict):
323
331
  # Phase 2: Source code scanning
324
332
  # ═══════════════════════════════════════════════════════════
325
333
 
326
- def _scan_source_directory(ceg: CEG, project_root: Path, src_dir: Path,
327
- language: str, exclude_patterns: list):
334
+ def _scan_source_directory(ceg: CEG, project_root: Path, src_dir: Path,
335
+ language: str, exclude_patterns: list):
328
336
  """Scan source files for import/call dependencies."""
329
337
  extensions = {
330
338
  "python": [".py"],
@@ -343,60 +351,60 @@ def _scan_source_directory(ceg: CEG, project_root: Path, src_dir: Path,
343
351
  full = Path(root) / fname
344
352
  rel = full.relative_to(project_root).as_posix()
345
353
 
346
- if any(_match_glob(rel, pat) for pat in exclude_patterns):
347
- continue
348
-
349
- ceg.upsert_node(f"file:{rel}", "file", path=rel, name=fname)
350
- file_count += 1
351
- _extract_imports_basic(ceg, project_root, src_dir, full, rel, language)
354
+ if any(_match_glob(rel, pat) for pat in exclude_patterns):
355
+ continue
356
+
357
+ ceg.upsert_node(f"file:{rel}", "file", path=rel, name=fname)
358
+ file_count += 1
359
+ _extract_imports_basic(ceg, project_root, src_dir, full, rel, language)
352
360
 
353
361
  if file_count > 0:
354
362
  print(f" Source: {file_count} {language} files in {src_dir.relative_to(project_root)}")
355
363
 
356
364
 
357
- def _extract_imports_basic(ceg: CEG, project_root: Path, src_dir: Path, file_path: Path,
358
- rel_path: str, language: str):
359
- """Basic import extraction using the shared parsing backend."""
360
- try:
361
- content = file_path.read_text(errors="ignore")
362
- except Exception:
363
- return
364
-
365
- source_id = f"file:{rel_path}"
366
- extractor = get_extractor(language, "source")
367
- internal, _ = extractor.extract_imports(content, file_path, project_root, src_dir)
368
-
369
- if language in ("typescript", "javascript"):
370
- for import_lines in internal.values():
371
- for line in import_lines:
372
- match = re.search(r'''(?:import|from)\s+['"]([^'"]+)['"]''', line)
373
- if not match:
374
- continue
375
- target_module = match.group(1)
376
- if not target_module.startswith("."):
377
- continue
378
- resolved = (file_path.parent / target_module).resolve()
379
- extensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"]
380
- for ext in [""] + extensions:
381
- candidate = Path(f"{resolved}{ext}")
382
- if not candidate.exists():
383
- continue
384
- try:
385
- target_rel = candidate.relative_to(project_root).as_posix()
386
- except ValueError:
387
- continue
388
- target_id = f"file:{target_rel}"
389
- ceg.upsert_node(target_id, "file", path=target_rel)
390
- edge_id = ceg.add_edge(source_id, target_id, "imports", "structural")
391
- ceg.add_evidence(edge_id, "static", "ast_import", 0.95)
392
- break
393
-
394
- elif language == "python":
395
- for target_module in internal:
396
- target_id = f"module:{target_module}"
397
- ceg.upsert_node(target_id, "module", name=target_module)
398
- edge_id = ceg.add_edge(source_id, target_id, "imports", "structural")
399
- ceg.add_evidence(edge_id, "static", "ast_import", 0.90)
365
+ def _extract_imports_basic(ceg: CEG, project_root: Path, src_dir: Path, file_path: Path,
366
+ rel_path: str, language: str):
367
+ """Basic import extraction using the shared parsing backend."""
368
+ try:
369
+ content = file_path.read_text(errors="ignore")
370
+ except Exception:
371
+ return
372
+
373
+ source_id = f"file:{rel_path}"
374
+ extractor = get_extractor(language, "source")
375
+ internal, _ = extractor.extract_imports(content, file_path, project_root, src_dir)
376
+
377
+ if language in ("typescript", "javascript"):
378
+ for import_lines in internal.values():
379
+ for line in import_lines:
380
+ match = re.search(r'''(?:import|from)\s+['"]([^'"]+)['"]''', line)
381
+ if not match:
382
+ continue
383
+ target_module = match.group(1)
384
+ if not target_module.startswith("."):
385
+ continue
386
+ resolved = (file_path.parent / target_module).resolve()
387
+ extensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"]
388
+ for ext in [""] + extensions:
389
+ candidate = Path(f"{resolved}{ext}")
390
+ if not candidate.exists():
391
+ continue
392
+ try:
393
+ target_rel = candidate.relative_to(project_root).as_posix()
394
+ except ValueError:
395
+ continue
396
+ target_id = f"file:{target_rel}"
397
+ ceg.upsert_node(target_id, "file", path=target_rel)
398
+ edge_id = ceg.add_edge(source_id, target_id, "imports", "structural")
399
+ ceg.add_evidence(edge_id, "static", "ast_import", 0.95)
400
+ break
401
+
402
+ elif language == "python":
403
+ for target_module in internal:
404
+ target_id = f"module:{target_module}"
405
+ ceg.upsert_node(target_id, "module", name=target_module)
406
+ edge_id = ceg.add_edge(source_id, target_id, "imports", "structural")
407
+ ceg.add_evidence(edge_id, "static", "ast_import", 0.90)
400
408
 
401
409
 
402
410
  # ═══════════════════════════════════════════════════════════
@@ -242,6 +242,7 @@ def _render_module_detail(env: Environment, facts: ProjectFacts, module: ModuleI
242
242
  confidence=_module_confidence(module),
243
243
  today=today,
244
244
  depends_on=_module_depends_on(facts, module),
245
+ source_files=sorted(module.files),
245
246
  ),
246
247
  mod=module,
247
248
  layer_name=layer_name,
@@ -280,6 +281,7 @@ def _render_schema_design(env: Environment, relative_path: str, schema: Any, tod
280
281
  node_id=_schema_node_id(relative_path),
281
282
  confidence=_schema_confidence(schema),
282
283
  today=today,
284
+ source_files=[relative_path],
283
285
  ),
284
286
  relative_path=relative_path,
285
287
  slug=_slugify(Path(relative_path).with_suffix("").as_posix()),
@@ -302,6 +304,7 @@ def _render_api_contract(env: Environment, relative_path: str, spec: Any, today:
302
304
  node_id=_api_node_id(relative_path),
303
305
  confidence=_api_confidence(spec),
304
306
  today=today,
307
+ source_files=[relative_path],
305
308
  ),
306
309
  relative_path=relative_path,
307
310
  spec=spec,
@@ -318,6 +321,7 @@ def _build_frontmatter(
318
321
  confidence: float,
319
322
  today: str,
320
323
  depends_on: list[dict[str, Any]] | None = None,
324
+ source_files: list[str] | None = None,
321
325
  ) -> str:
322
326
  codd: dict[str, Any] = {
323
327
  "node_id": node_id,
@@ -326,6 +330,8 @@ def _build_frontmatter(
326
330
  "confidence": round(confidence, 2),
327
331
  "last_extracted": today,
328
332
  }
333
+ if source_files:
334
+ codd["source_files"] = source_files
329
335
  if depends_on:
330
336
  codd["depends_on"] = depends_on
331
337
  payload = yaml.safe_dump({"codd": codd}, sort_keys=False, allow_unicode=True)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codd-dev"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "CoDD: Coherence-Driven Development — cross-artifact change impact analysis"
9
9
  readme = "README.md"
10
10
  license = "MIT"
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