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.
- {codd_dev-0.5.0 → codd_dev-0.6.0}/PKG-INFO +1 -1
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/parsing.py +101 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/scanner.py +63 -55
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/synth.py +6 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/pyproject.toml +1 -1
- {codd_dev-0.5.0 → codd_dev-0.6.0}/.gitignore +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/LICENSE +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/README.md +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/__init__.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/cli.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/clustering.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/config.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/contracts.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/defaults.yaml +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/extractor.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/generator.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/graph.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/hooks.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/implementer.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/planner.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/propagate.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/risk.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/schema_refs.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/codd.yaml.tmpl +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/conventions.yaml.tmpl +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/doc_links.yaml.tmpl +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/extracted/system-context.md.j2 +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/gitignore.tmpl +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/templates/overrides.yaml.tmpl +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/traceability.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/validator.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/verifier.py +0 -0
- {codd_dev-0.5.0 → codd_dev-0.6.0}/codd/wiring.py +0 -0
- {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.
|
|
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)
|
|
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
|