Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
109cef1
docs: configure fork planning and GitHub wayfinding
JulioLeonPhd Oct 26, 2026
8d68a28
test(matlab): add independently reviewed JSONLab corpus
JulioLeonPhd Oct 26, 2026
f7a0264
fix(matlab): isolate persisted build and cross-repo relationships
JulioLeonPhd Oct 26, 2026
dfff15d
merge: integrate MATLAB persistence isolation
JulioLeonPhd Oct 27, 2026
be1aac4
merge: integrate MATLAB corpus evidence
JulioLeonPhd Oct 27, 2026
30ddcbc
fix(matlab): isolate shared extraction resolution
JulioLeonPhd Oct 27, 2026
a82c56b
feat(matlab): resolve exact repository source inheritance
JulioLeonPhd Oct 27, 2026
4c869e9
merge: integrate MATLAB option 1 baseline
JulioLeonPhd Oct 27, 2026
0f43e89
merge: integrate MATLAB option 1 baseline
JulioLeonPhd Oct 27, 2026
ece476e
merge: integrate MATLAB option 1 WP2
JulioLeonPhd Oct 27, 2026
179d610
merge: integrate MATLAB option 1 WP4
JulioLeonPhd Oct 27, 2026
c50b71a
feat(matlab): extract definitions and conservative lexical calls
JulioLeonPhd Oct 27, 2026
10e1a7c
merge: synchronize MATLAB frontend with integration
JulioLeonPhd Oct 27, 2026
f151c1c
merge: integrate MATLAB option 1 frontend
JulioLeonPhd Oct 27, 2026
7cf4b89
feat(matlab): extract inherited MATLAB symbols
JulioLeonPhd Oct 27, 2026
8489154
test(matlab): add reviewed corpus and determinism coverage
JulioLeonPhd Oct 27, 2026
8db1f95
fix(matlab): preserve eligible calls and source-backed relationships
JulioLeonPhd Oct 27, 2026
bd98901
test(matlab): retain final review regression coverage
JulioLeonPhd Oct 27, 2026
e8f1e98
Merge remote-tracking branch 'origin/v8' into matlab-ast
JulioLeonPhd Oct 27, 2026
a24491c
chore: narrow MATLAB contribution to upstream scope
JulioLeonPhd Oct 27, 2026
1e214e6
fix(matlab): integrate current v8 and preserve AST provenance
JulioLeonPhd Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ AST cache schema 43 invalidates older template and conditional recovery records.
4. Add the tree-sitter package to `pyproject.toml` dependencies.
5. Add a fixture file to `tests/fixtures/` and tests to `tests/test_languages.py`.

### MATLAB fallback contract

For `.m` files, explicit `.graphifyrc` remaps take precedence, then the existing Objective-C content detector; a non-Objective-C `.m` falls back to MATLAB. `.mm` remains Objective-C, and `language.inc=.m` keeps its existing Objective-C interpretation. This adds no MATLAB remap alias or force-language option.

The file node remains the script/file navigation anchor and records the file kind when established; there is no separate synthetic script node. Located classes and functions are definition nodes; use immediate `contains` for file/owner-to-definition relationships and `method` for class-to-method relationships, not both for one pair. Trustworthy definitions remain even when relationship binding is unsafe; declaration-only methods are declarations, not fabricated executable bodies. This is not full MATLAB runtime semantics, and separately stored `@Class` methods are not associated.

- **Calls:** emit only uniquely resolved, unqualified parenthesized calls in executable function or method bodies to lexically visible same-file functions. A call nested in a member/qualified call's argument is still eligible; the member/qualified callee itself is not. Parentheses/braces used for indexing, parameter/output/assignment bindings, and uncertain scopes do not produce calls. Script top-level, cross-file, qualified/member, command, handle, constructor/dynamic, and class-method target guesses are omitted; there is no generic unresolved-call pass.
- **Bases:** preserve written superclass names and source locations in class metadata, including external and unresolved names. Emit `inherits` only for a unique exact source match: a qualified namespace path, or an eligible unpackaged sibling file with a matching class declaration. This does not claim to model MATLAB runtime path selection and does not invent a target.
- **Provenance and call sites:** each MATLAB file/definition node keeps nested `metadata.language = "matlab"` through persistence. Extraction retains separate located call sites, while graph assembly coalesces repeated same-pair edges; the persisted graph is not a lossless call-site multigraph.
## Search hook scope

The search hook omits project hints when a structured Grep path or every literal
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Perf: **extract** the AST worker pool is sized by the amount of uncached work (one worker per ~500 KB of uncached source, capped at CPU count and file count) when no worker count is set, so small incremental builds stop spinning up a full pool; output is invariant to worker count (thanks @rohit-jsfreaky).
- Change: **cloud** the OSS surfaces (README, CLI help/install text, the translated docs in all 31 locales, and the agent skill) now point at the free plan with no card at `app.graphify.com/signup`, instead of a "14-day trial" at `/login` (the trial is paid-plans-only and needs a card, so the old copy conflated the two). Links carry `utm_campaign=oss` UTMs tagged per surface, so a signup can be attributed to where it came from.
- Change: **cloud** the post-build CLI nudge is contextual. It is TTY-only (never in CI or the agent pipeline), respects `GRAPHIFY_NO_TIPS`/`GRAPHIFY_NO_CTA`, and shows on builds 1, 5 and 20 then stays quiet for good. When a local graph reaches 25k nodes, the free plan's per-repo cut-off, the nudge switches to a scale-specific line (tagged `utm_content=scale`) that sells Graphify Pro and links to pricing with the 14-day trial, instead of the free signup: a repo that size no longer fits the free plan, so pointing it at the free plan would land straight on the limit.
- Feat: **matlab** `.m` files fall back to MATLAB only after explicit remaps and Objective-C content detection; extraction records located definitions, conservative same-file lexical calls, and exact-source inheritance while preserving `.mm` and `.inc=.m` Objective-C routing.

The AST cache schema is bumped to 44; warm caches re-run the affected C++, PHP, Java and module-level-id extractions.
The AST cache schema is bumped to 45; warm caches re-run the affected C++, PHP, Java, module-level-id and MATLAB extractions.

## 0.9.106 (2026-11-10)

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,9 @@ remain effective.

Create a `.graphifyrc` in the scan root for settings that belong to the project rather than to one run. One `key=value` per line; lines beginning with `#` are comments.

**Ambiguous extensions.** Some extensions mean different languages in different projects: `.inc` is PHP on pfSense, Pascal in a Delphi tree, SQL or assembly elsewhere; `.h` is C or C++; `.m` is Objective-C or MATLAB. graphify has to pick one global default, and when it picks wrong the file does not fail: it parses as the wrong language and yields a handful of incidental nodes, so the graph looks populated while the real symbols are missing. Declare what the extension means in your repo:
**Ambiguous extensions.** Some extensions mean different languages in different projects: `.inc` is PHP on pfSense, Pascal in a Delphi tree, SQL or assembly elsewhere; `.h` is C or C++; `.m` is Objective-C or MATLAB. Explicit `.graphifyrc` remaps let projects declare their intent; otherwise, an ambiguous extension can be parsed as the wrong language and yield incidental nodes while missing its real symbols. Declare what the extension means in your repo:

For `.m` files, explicit `.graphifyrc` remaps take precedence, then the existing Objective-C content detector; a non-Objective-C `.m` falls back to MATLAB. `.mm` remains Objective-C, and `language.inc=.m` keeps its existing Objective-C interpretation. This adds no MATLAB remap alias or force-language option.

```
# .graphifyrc
Expand Down
96 changes: 95 additions & 1 deletion graphify/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,61 @@ def dedupe_edges(edges: list[dict]) -> list[dict]:
return out


def _refresh_matlab_inheritance(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — _refresh_matlab_inheritance()

10 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

nodes: list[dict], edges: list[dict], root: str | Path | None = None,
) -> list[dict]:
"""Replace only source-owned MATLAB inheritance facts for the current nodes."""
from graphify.extractors.resolution import _matlab_inheritance_edges
_root = str(Path(root).resolve()) if root else None

def rooted(item: dict) -> dict:
if not isinstance(item, dict):
return item
source = item.get("source_file")
normalized = _norm_source_file(source, _root) if isinstance(source, str) else source
return item if normalized == source else {**item, "source_file": normalized}

# Resolver inputs may combine absolute incoming paths with relative stored
# paths. Canonicalize private views only; unchanged extraction context is read-only.
resolver_nodes = [rooted(node) for node in nodes]
resolver_edges = [rooted(edge) for edge in edges]

matlab_sources: dict[str, str] = {}
for node in resolver_nodes:
if not isinstance(node, dict):
continue
metadata = node.get("metadata")
node_id = node.get("id")
source_file = node.get("source_file")
if not isinstance(metadata, dict) or metadata.get("language") != "matlab":
continue
if (
not isinstance(node_id, str)
or not isinstance(source_file, str)
or not source_file
):
continue
matlab_sources[node_id] = source_file

def _is_matlab_inheritance(edge: dict) -> bool:
if not isinstance(edge, dict) or edge.get("relation") != "inherits":
return False
source = edge.get("source")
if not isinstance(source, str):
return False
source_file = matlab_sources.get(source)
return (
source_file is not None
and edge.get("source_file") == source_file
and _is_ast_tier(edge)
)

kept = [edge for edge, resolver_edge in zip(edges, resolver_edges)
if not _is_matlab_inheritance(resolver_edge)]
refreshed = _matlab_inheritance_edges(resolver_nodes)
return kept + [{**edge, "_origin": "ast"} for edge in refreshed]


def _old_file_stems(rel: Path) -> list[str]:
"""Pre-migration stem forms a semantic fragment may have used for ``rel``.

Expand Down Expand Up @@ -2111,6 +2166,20 @@ def _resolve_endpoint(raw_id: str, edge_source_file: object) -> str:
# Python `import time` must not bind to a `time.ts`, #1749).
_edge_rel = attrs.get("relation")
if _edge_rel in ("calls", "imports", "imports_from", "references"):
# .m is shared with Objective-C. Keep that provenance boundary for
# native code while letting source-backed docs/configs reach the
# established family gate below.
src_matlab = (G.nodes[src].get("metadata") or {}).get("language") == "matlab"
tgt_matlab = (G.nodes[tgt].get("metadata") or {}).get("language") == "matlab"
if src_matlab != tgt_matlab:
non_matlab = G.nodes[tgt] if src_matlab else G.nodes[src]
non_matlab_ext = Path(non_matlab.get("source_file") or "").suffix.lower()
source_backed_doc_or_config = bool(non_matlab.get("source_file")) and (
non_matlab.get("file_type") in {"document", "paper", "image"}
or non_matlab_ext not in _EDGE_LANG_FAMILY
)
if _edge_rel == "calls" or not source_backed_doc_or_config:
continue
src_ext = Path(G.nodes[src].get("source_file") or "").suffix.lower()
tgt_ext = Path(G.nodes[tgt].get("source_file") or "").suffix.lower()
src_fam = _EDGE_LANG_FAMILY.get(src_ext)
Expand Down Expand Up @@ -2760,6 +2829,9 @@ def _dropped(item: dict) -> bool:
new["hyperedges"] = carried_hyper + list(new.get("hyperedges", []))
if unverified_semantic_shrink:
new["_unverified_semantic_shrink"] = unverified_semantic_shrink
new["edges"] = _refresh_matlab_inheritance(
dedupe_nodes(new.get("nodes", [])), new.get("edges", []), root=_eff_root,
)
return new


Expand Down Expand Up @@ -3049,7 +3121,29 @@ def _prune_match(sf: "str | None") -> bool:
if isinstance(n, dict) and n.get("id")
} if had_graph else None

all_chunks = base + list(new_chunks)
all_chunks = base + [dict(chunk) for chunk in new_chunks]
if all_chunks:
resolver_nodes = [
node
for chunk in all_chunks
for node in chunk.get("nodes", [])
if isinstance(node, dict) and not _prune_match(node.get("source_file"))
]
merged_edges = [
edge for chunk in all_chunks for edge in chunk.get("edges", [])
]
if dedup:
from graphify.dedup import _namespace_reference_stub_collisions
resolver_nodes, _ = _namespace_reference_stub_collisions(
resolver_nodes, [], None,
Path(_eff_root).resolve() if _eff_root else None,
)
resolver_nodes = dedupe_nodes(resolver_nodes)
all_chunks[0]["edges"] = _refresh_matlab_inheritance(
resolver_nodes, merged_edges, root=_eff_root,
)
for chunk in all_chunks[1:]:
chunk["edges"] = []
G = build(
all_chunks,
directed=directed,
Expand Down
3 changes: 2 additions & 1 deletion graphify/cache.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,10 @@ def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int
data[links_key] = kept_edges
if "hyperedges" in data:
data["hyperedges"] = kept_hyper
from graphify.build import _refresh_matlab_inheritance, dedupe_nodes
data[links_key] = _refresh_matlab_inheritance(
dedupe_nodes(kept_nodes), kept_edges
)
from graphify.export import backup_if_protected as _backup
_backup(graph_path.parent)
from graphify.paths import write_json_atomic
Expand Down
3 changes: 3 additions & 0 deletions graphify/cross_repo_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ def _index_declarations(merged: "nx.Graph") -> tuple[dict[str, list[str]], set[s
for node, data in merged.nodes(data=True):
if not data.get("_callable_class") or not data.get("source_file"):
continue
# MATLAB shares .m with Objective-C, but cannot answer native parked calls.
if (data.get("metadata") or {}).get("language") == "matlab":
continue
if not data.get("repo"):
continue
name = _key(data.get("label"))
Expand Down
3 changes: 3 additions & 0 deletions graphify/cross_repo_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def link_shared_type_declarations(merged: "nx.Graph") -> int:
for node, data in merged.nodes(data=True):
if not data.get("_callable_class") or not data.get("source_file"):
continue
# MATLAB package names do not establish shared types across repositories.
if (data.get("metadata") or {}).get("language") == "matlab":
continue
namespace = str((data.get("metadata") or {}).get("namespace") or "")
label = str(data.get("label") or "")
if not namespace or not label or not data.get("repo"):
Expand Down
Loading
Loading