From 109cef1b1afee4567e1bb562c5e96b927ba45815 Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:16:12 +0100 Subject: [PATCH 01/11] docs: configure fork planning and GitHub wayfinding Record skill conventions and link the migrated MATLAB decision map. Mark fork-specific tracker guidance for exclusion from the upstream feature PR. Co-Authored-By: OpenAI Codex --- AGENTS.md | 16 +++++++++++ docs/agents/domain.md | 12 +++++++++ docs/agents/issue-tracker.md | 51 ++++++++++++++++++++++++++++++++++++ docs/agents/triage-labels.md | 12 +++++++++ 4 files changed, 91 insertions(+) create mode 100644 docs/agents/domain.md create mode 100644 docs/agents/issue-tracker.md create mode 100644 docs/agents/triage-labels.md diff --git a/AGENTS.md b/AGENTS.md index 38857bdc2d..3cc7f99414 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,3 +6,19 @@ Rules: - When working on Graphify itself, use the repository's existing Graphify guidance. For codebase questions, prefer scoped graph queries where available; use the report for broad orientation. Do not use the graph as evidence when the task concerns the graph's correctness itself. - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) + +## Agent skills + +### Issue tracker + +GitHub Issues on `JulioLeonPhD/graphify`. See `docs/agents/issue-tracker.md`. +Fork-local: exclude this subsection and `docs/agents/issue-tracker.md` from the upstream PR. + +### Triage labels + +Use the five default triage labels. See `docs/agents/triage-labels.md`. + +### Domain docs + +Single-context: root `GLOSSARY.md` and `docs/adr/`, created lazily. +See `docs/agents/domain.md`. diff --git a/docs/agents/domain.md b/docs/agents/domain.md new file mode 100644 index 0000000000..ddb420922d --- /dev/null +++ b/docs/agents/domain.md @@ -0,0 +1,12 @@ +# Domain Docs + +This repository uses a single domain context. + +Before exploring, read root `GLOSSARY.md` and relevant ADRs in `docs/adr/`. +If absent, proceed silently; do not create placeholder files. +Domain-modeling creates them lazily as terms and decisions are resolved. + +Use glossary vocabulary in issues, proposals, tests and explanations. +Reconsider undefined terminology or raise it during domain-modeling. + +Surface conflicts with existing ADRs explicitly; never silently override them. diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md new file mode 100644 index 0000000000..28b03ed424 --- /dev/null +++ b/docs/agents/issue-tracker.md @@ -0,0 +1,51 @@ +# Issue tracker: GitHub + +Issues and specs live in `JulioLeonPhD/graphify`. +Use the gh CLI with explicit `--repo JulioLeonPhD/graphify`; +do not infer the target from this clone's multiple remotes. + +MATLAB wayfinding: [Find the smallest maintainer-ready MATLAB extractor PR](https://github.com/JulioLeonPhd/graphify/issues/1). +The map links the migrated research evidence; its native sub-issues are the decision tickets. + +## Upstream PR boundary + +This tracker configuration is fork-local. Before opening the MATLAB extractor PR +against Graphify-Labs/graphify, remove this file and the fork-specific Issue tracker +subsection in AGENTS.md from the upstream PR diff. Do not replace the destination +with upstream and publish fork planning issues there. Keep local planning artifacts +out of the upstream PR. + +## Operations + +- Create: `gh issue create --repo JulioLeonPhD/graphify --title "..." --body "..."` +- Read: `gh issue view --repo JulioLeonPhD/graphify --json number,title,body,labels,comments,assignees` +- List: `gh issue list --repo JulioLeonPhD/graphify --state open --json number,title,body,labels,assignees` +- Comment: `gh issue comment --repo JulioLeonPhD/graphify --body "..."` +- Label: `gh issue edit --repo JulioLeonPhD/graphify --add-label "..."` or `--remove-label "..."` +- Close: `gh issue close --repo JulioLeonPhD/graphify` + +When a skill says publish or fetch a ticket, create or read a GitHub issue. + +## Pull requests as a triage surface + +PRs as a request surface: no. + +## Wayfinding operations + +- Map: one issue labelled `wayfinder:map`. +- Tickets: native sub-issues of the map, labelled `wayfinder:`. + Map and tickets carry only wayfinder labels, not triage labels. +- Parent link: POST `repos/JulioLeonPhD/graphify/issues//sub_issues` + with `sub_issue_id=`, using `gh api`. +- Blocking: native GitHub issue dependencies. POST + `repos/JulioLeonPhD/graphify/issues//dependencies/blocked_by` + with `issue_id=`. Use database ids, not issue numbers. +- Frontier: query the map's sub-issues; select open, unassigned tickets + with no open blockers. Preserve sub-issue order. +- Claim before work: assign the ticket to `@me`. +- Resolve: post a resolution comment, close the ticket, then append + a named link and one-line gist to the map's Decisions so far. +- Refer to maps and tickets by linked title, never a bare number. + +Only if native relationships are unavailable: use a task list on the map, +a parent link on each child, and a Blocked by line linking blockers. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md new file mode 100644 index 0000000000..a406f9828c --- /dev/null +++ b/docs/agents/triage-labels.md @@ -0,0 +1,12 @@ +# Triage Labels + +| Role | Tracker label | Meaning | +| --- | --- | --- | +| needs-triage | needs-triage | Maintainer evaluation needed | +| needs-info | needs-info | Waiting on reporter information | +| ready-for-agent | ready-for-agent | Fully specified, ready for an AFK agent | +| ready-for-human | ready-for-human | Requires human implementation | +| wontfix | wontfix | Will not be actioned | + +Use these labels when skills refer to the corresponding triage roles. +Wayfinder maps and decision tickets use only wayfinder labels. From 8d68a287b5242c03c991af6d9edb552cdd1b671b Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:50:05 +0100 Subject: [PATCH 02/11] test(matlab): add independently reviewed JSONLab corpus Co-Authored-By: OpenAI Codex --- tests/fixtures/matlab/jsonlab/LICENSE_BSD.txt | 33 + tests/fixtures/matlab/jsonlab/expected.json | 562 ++++++++++++++++++ tests/fixtures/matlab/jsonlab/loadjson.m | 552 +++++++++++++++++ 3 files changed, 1147 insertions(+) create mode 100644 tests/fixtures/matlab/jsonlab/LICENSE_BSD.txt create mode 100644 tests/fixtures/matlab/jsonlab/expected.json create mode 100644 tests/fixtures/matlab/jsonlab/loadjson.m diff --git a/tests/fixtures/matlab/jsonlab/LICENSE_BSD.txt b/tests/fixtures/matlab/jsonlab/LICENSE_BSD.txt new file mode 100644 index 0000000000..5db82c4549 --- /dev/null +++ b/tests/fixtures/matlab/jsonlab/LICENSE_BSD.txt @@ -0,0 +1,33 @@ +Copyright (c) 2011-2020 Qianqian Fang +Copyright (c) 2014,2016 Bastian Bechtold +Copyright (c) 2012, Kota Yamaguchi +Copyright (c) 2009, Nedialko Krouchev +Copyright (c) 2009, François Glineur +Copyright (c) 2008, Joel Feenstra + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/fixtures/matlab/jsonlab/expected.json b/tests/fixtures/matlab/jsonlab/expected.json new file mode 100644 index 0000000000..43dc928730 --- /dev/null +++ b/tests/fixtures/matlab/jsonlab/expected.json @@ -0,0 +1,562 @@ +{ + "schema_version": 1, + "provenance": { + "project": "JSONLab", + "release": "v2.0", + "repository": "https://github.com/NeuroJSON/jsonlab", + "commit": "5a58fafdca239e61be4d7b2270307b2445bb571a", + "license": "BSD-3-Clause", + "attribution": "Qianqian Fang and contributors; original author header and full LICENSE_BSD.txt retained verbatim.", + "files": [ + { + "path": "loadjson.m", + "source_url": "https://github.com/NeuroJSON/jsonlab/blob/5a58fafdca239e61be4d7b2270307b2445bb571a/loadjson.m", + "sha256": "ecb4bca8b9762a1849923536694f418b6cea9e6240959244e9ec629925d366f5", + "size_bytes": 21604 + }, + { + "path": "LICENSE_BSD.txt", + "source_url": "https://github.com/NeuroJSON/jsonlab/blob/5a58fafdca239e61be4d7b2270307b2445bb571a/LICENSE_BSD.txt", + "sha256": "2e5589a9aa67172658bd5f5d79313160d1327ecdb740ca6cf2fb05f6f4efa74e", + "size_bytes": 1707 + } + ] + }, + "contract": { + "source_file": "loadjson.m", + "coordinates": "One-based rows and UTF-8 byte columns; declaration start is function keyword.", + "keys": "Manifest-local logical keys, not Graphify IDs. Resolve actual endpoints using source file, metadata.kind, exact label, metadata.scope_chain and declaration source_location; all definitions here begin at byte column 1.", + "definitions": "Exhaustive trustworthy declarations; file node listed separately.", + "contains": "Exhaustive immediate containment; source_location is the child declaration row. No method or inherits edges exist in this slice.", + "calls": "Exhaustive eligible resolved same-file raw call sites, before persisted pair coalescing. Compare multisets by relationship, endpoint logical keys and source_location. column/source_text independently locate the written identifier; graph source_location uses L. Every call must have confidence EXTRACTED and weight 1.0.", + "metadata": "Every file/definition node has nested metadata.language=matlab. Definition kind is matlab_function, scope_chain is []. File kind is matlab_file with matlab_file_kind=function and matlab_primary_name=loadjson.", + "denominator": "Cross-file/builtin calls, indexing, handles, anonymous bodies, dotted dispatch and whole-scope-barred calls are excluded, not missing expected calls.", + "independence": "Manually reviewed pinned source and accepted issue 4 rules; no MATLAB frontend output, parser output or JSONLab execution used to choose records.", + "specification": "https://github.com/JulioLeonPhD/graphify/issues/4#issuecomment-6420416127" + }, + "file": { + "key": "file", + "path": "loadjson.m", + "kind": "matlab_file", + "matlab_file_kind": "function", + "matlab_primary_name": "loadjson" + }, + "definitions": [ + { + "key": "loadjson@L1:C1", + "name": "loadjson", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L1", + "column": 1, + "end_line": 167, + "source_text": "function data = loadjson(fname,varargin)" + }, + { + "key": "parse_array@L173:C1", + "name": "parse_array", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L173", + "column": 1, + "end_line": 312, + "source_text": "function [object, pos,index_esc] = parse_array(inputstr, pos, esc, index_esc, varargin) % JSON array is written in row-major order" + }, + { + "key": "parse_char@L315:C1", + "name": "parse_char", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L315", + "column": 1, + "end_line": 323, + "source_text": "function pos=parse_char(inputstr, pos, c)" + }, + { + "key": "next_char@L326:C1", + "name": "next_char", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L326", + "column": 1, + "end_line": 333, + "source_text": "function [c, pos] = next_char(inputstr, pos)" + }, + { + "key": "parseStr@L336:C1", + "name": "parseStr", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L336", + "column": 1, + "end_line": 396, + "source_text": "function [str, pos,index_esc] = parseStr(inputstr, pos, esc, index_esc, varargin)" + }, + { + "key": "parse_number@L399:C1", + "name": "parse_number", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L399", + "column": 1, + "end_line": 406, + "source_text": "function [num, pos] = parse_number(inputstr, pos, varargin)" + }, + { + "key": "parse_value@L409:C1", + "name": "parse_value", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L409", + "column": 1, + "end_line": 448, + "source_text": "function [val, pos,index_esc] = parse_value(inputstr, pos, esc, index_esc, varargin)" + }, + { + "key": "parse_object@L451:C1", + "name": "parse_object", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L451", + "column": 1, + "end_line": 481, + "source_text": "function [object, pos, index_esc] = parse_object(inputstr, pos, esc, index_esc, varargin)" + }, + { + "key": "error_pos@L485:C1", + "name": "error_pos", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L485", + "column": 1, + "end_line": 493, + "source_text": "function pos=error_pos(msg, inputstr, pos)" + }, + { + "key": "skip_whitespace@L497:C1", + "name": "skip_whitespace", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L497", + "column": 1, + "end_line": 502, + "source_text": "function newpos=skip_whitespace(pos, inputstr)" + }, + { + "key": "unescapejsonstring@L505:C1", + "name": "unescapejsonstring", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L505", + "column": 1, + "end_line": 521, + "source_text": "function newstr=unescapejsonstring(str)" + }, + { + "key": "sscanf_prep@L524:C1", + "name": "sscanf_prep", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L524", + "column": 1, + "end_line": 532, + "source_text": "function arraystr=sscanf_prep(str)" + }, + { + "key": "parse2darray@L535:C1", + "name": "parse2darray", + "kind": "matlab_function", + "scope_chain": [], + "source_location": "L535", + "column": 1, + "end_line": 552, + "source_text": "function [obj, nextidx,nextdim]=parse2darray(inputstr,startpos,arraystr)" + } + ], + "contains": [ + { + "source": "file", + "target": "loadjson@L1:C1", + "source_location": "L1" + }, + { + "source": "file", + "target": "parse_array@L173:C1", + "source_location": "L173" + }, + { + "source": "file", + "target": "parse_char@L315:C1", + "source_location": "L315" + }, + { + "source": "file", + "target": "next_char@L326:C1", + "source_location": "L326" + }, + { + "source": "file", + "target": "parseStr@L336:C1", + "source_location": "L336" + }, + { + "source": "file", + "target": "parse_number@L399:C1", + "source_location": "L399" + }, + { + "source": "file", + "target": "parse_value@L409:C1", + "source_location": "L409" + }, + { + "source": "file", + "target": "parse_object@L451:C1", + "source_location": "L451" + }, + { + "source": "file", + "target": "error_pos@L485:C1", + "source_location": "L485" + }, + { + "source": "file", + "target": "skip_whitespace@L497:C1", + "source_location": "L497" + }, + { + "source": "file", + "target": "unescapejsonstring@L505:C1", + "source_location": "L505" + }, + { + "source": "file", + "target": "sscanf_prep@L524:C1", + "source_location": "L524" + }, + { + "source": "file", + "target": "parse2darray@L535:C1", + "source_location": "L535" + } + ], + "calls": [ + { + "source": "loadjson@L1:C1", + "target": "error_pos@L485:C1", + "source_location": "L103", + "column": 8, + "source_text": " error_pos('input file does not exist');" + }, + { + "source": "loadjson@L1:C1", + "target": "next_char@L326:C1", + "source_location": "L136", + "column": 18, + "source_text": " [cc,pos]=next_char(inputstr, pos);" + }, + { + "source": "loadjson@L1:C1", + "target": "parse_object@L451:C1", + "source_location": "L139", + "column": 51, + "source_text": " [data{jsoncount},pos,index_esc] = parse_object(inputstr, pos, esc, index_esc,opt);" + }, + { + "source": "loadjson@L1:C1", + "target": "parse_array@L173:C1", + "source_location": "L141", + "column": 51, + "source_text": " [data{jsoncount},pos,index_esc] = parse_array(inputstr, pos, esc, index_esc,opt);" + }, + { + "source": "loadjson@L1:C1", + "target": "error_pos@L485:C1", + "source_location": "L143", + "column": 21, + "source_text": " pos=error_pos('Outer level structure must be an object or an array',inputstr,pos);" + }, + { + "source": "parse_char@L315:C1", + "target": "skip_whitespace@L497:C1", + "source_location": "L316", + "column": 9, + "source_text": " pos=skip_whitespace(pos, inputstr);" + }, + { + "source": "parse_char@L315:C1", + "target": "error_pos@L485:C1", + "source_location": "L318", + "column": 13, + "source_text": " pos=error_pos(sprintf('Expected %c at position %%d', c),inputstr,pos);" + }, + { + "source": "parse_char@L315:C1", + "target": "skip_whitespace@L497:C1", + "source_location": "L321", + "column": 13, + "source_text": " pos=skip_whitespace(pos, inputstr);" + }, + { + "source": "next_char@L326:C1", + "target": "skip_whitespace@L497:C1", + "source_location": "L327", + "column": 9, + "source_text": " pos=skip_whitespace(pos, inputstr);" + }, + { + "source": "parse_number@L399:C1", + "target": "error_pos@L485:C1", + "source_location": "L403", + "column": 13, + "source_text": " pos=error_pos('Error reading number at position %d',inputstr,pos);" + }, + { + "source": "parse_value@L409:C1", + "target": "parseStr@L336:C1", + "source_location": "L417", + "column": 36, + "source_text": " [val, pos,index_esc] = parseStr(inputstr, pos, esc, index_esc,varargin{:});" + }, + { + "source": "parse_value@L409:C1", + "target": "parse_array@L173:C1", + "source_location": "L420", + "column": 36, + "source_text": " [val, pos,index_esc] = parse_array(inputstr, pos, esc, index_esc, varargin{:});" + }, + { + "source": "parse_value@L409:C1", + "target": "parse_object@L451:C1", + "source_location": "L423", + "column": 36, + "source_text": " [val, pos,index_esc] = parse_object(inputstr, pos, esc, index_esc, varargin{:});" + }, + { + "source": "parse_value@L409:C1", + "target": "parse_number@L399:C1", + "source_location": "L426", + "column": 26, + "source_text": " [val, pos] = parse_number(inputstr, pos, varargin{:});" + }, + { + "source": "parse_value@L409:C1", + "target": "error_pos@L485:C1", + "source_location": "L447", + "column": 9, + "source_text": " pos=error_pos('Value expected at position %d',inputstr,pos);" + }, + { + "source": "parse_object@L451:C1", + "target": "parse_char@L315:C1", + "source_location": "L452", + "column": 9, + "source_text": " pos=parse_char(inputstr, pos, '{');" + }, + { + "source": "parse_object@L451:C1", + "target": "next_char@L326:C1", + "source_location": "L459", + "column": 14, + "source_text": " [cc,pos]=next_char(inputstr,pos);" + }, + { + "source": "parse_object@L451:C1", + "target": "parseStr@L336:C1", + "source_location": "L462", + "column": 37, + "source_text": " [str, pos, index_esc] = parseStr(inputstr, pos, esc, index_esc, varargin{:});" + }, + { + "source": "parse_object@L451:C1", + "target": "error_pos@L485:C1", + "source_location": "L464", + "column": 21, + "source_text": " pos=error_pos('Name of value at position %d cannot be empty',inputstr,pos);" + }, + { + "source": "parse_object@L451:C1", + "target": "parse_char@L315:C1", + "source_location": "L466", + "column": 17, + "source_text": " pos=parse_char(inputstr, pos, ':');" + }, + { + "source": "parse_object@L451:C1", + "target": "parse_value@L409:C1", + "source_location": "L467", + "column": 36, + "source_text": " [val, pos,index_esc] = parse_value(inputstr, pos, esc, index_esc, varargin{:});" + }, + { + "source": "parse_object@L451:C1", + "target": "next_char@L326:C1", + "source_location": "L473", + "column": 22, + "source_text": " [cc,pos]=next_char(inputstr,pos);" + }, + { + "source": "parse_object@L451:C1", + "target": "parse_char@L315:C1", + "source_location": "L477", + "column": 17, + "source_text": " pos=parse_char(inputstr, pos, ',');" + }, + { + "source": "parse_object@L451:C1", + "target": "parse_char@L315:C1", + "source_location": "L480", + "column": 9, + "source_text": " pos=parse_char(inputstr, pos, '}');" + }, + { + "source": "parse2darray@L535:C1", + "target": "sscanf_prep@L524:C1", + "source_location": "L537", + "column": 12, + "source_text": " rowstr=sscanf_prep(inputstr(startpos-1:rowend));" + } + ], + "excluded_same_file_sites": [ + { + "source": "parse_array@L173:C1", + "target": "parse_char@L315:C1", + "source_location": "L174", + "column": 9, + "source_text": " pos=parse_char(inputstr, pos, '[');", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "next_char@L326:C1", + "source_location": "L182", + "column": 14, + "source_text": " [cc,pos]=next_char(inputstr,pos);", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "sscanf_prep@L524:C1", + "source_location": "L191", + "column": 30, + "source_text": " arraystr=sscanf_prep(arraystr);", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "parse_char@L315:C1", + "source_location": "L201", + "column": 41, + "source_text": " pos=parse_char(inputstr, pos, ']');", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "parse2darray@L535:C1", + "source_location": "L210", + "column": 48, + "source_text": " [obj, nextidx]=parse2darray(inputstr,pos+rowstart,arraystr);", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "parse_char@L315:C1", + "source_location": "L217", + "column": 41, + "source_text": " pos=parse_char(inputstr, pos, ']');", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "parse_char@L315:C1", + "source_location": "L244", + "column": 45, + "source_text": " pos=parse_char(inputstr, pos, ']');", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "parse_value@L409:C1", + "source_location": "L272", + "column": 40, + "source_text": " [val, pos,index_esc] = parse_value(inputstr, pos, esc, index_esc,varargin{:});", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "next_char@L326:C1", + "source_location": "L274", + "column": 26, + "source_text": " [cc,pos]=next_char(inputstr,pos);", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "parse_char@L315:C1", + "source_location": "L278", + "column": 21, + "source_text": " pos=parse_char(inputstr, pos, ',');", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parse_array@L173:C1", + "target": "parse_char@L315:C1", + "source_location": "L307", + "column": 9, + "source_text": " pos=parse_char(inputstr, pos, ']');", + "reason": "whole-scope eval barrier at L261" + }, + { + "source": "parseStr@L336:C1", + "target": "error_pos@L485:C1", + "source_location": "L338", + "column": 13, + "source_text": " pos=error_pos('String starting with \" expected at position %d',inputstr,pos);", + "reason": "whole-scope bare command/script invocation barrier: keyboard; at L390" + }, + { + "source": "parseStr@L336:C1", + "target": "error_pos@L485:C1", + "source_location": "L371", + "column": 25, + "source_text": " pos=error_pos('End of file reached right after escape character',inputstr,pos);", + "reason": "whole-scope bare command/script invocation barrier: keyboard; at L390" + }, + { + "source": "parseStr@L336:C1", + "target": "error_pos@L485:C1", + "source_location": "L383", + "column": 33, + "source_text": " pos=error_pos('End of file reached in escaped unicode character',inputstr,pos);", + "reason": "whole-scope bare command/script invocation barrier: keyboard; at L390" + }, + { + "source": "parseStr@L336:C1", + "target": "unescapejsonstring@L505:C1", + "source_location": "L394", + "column": 9, + "source_text": " str=unescapejsonstring(str);", + "reason": "whole-scope bare command/script invocation barrier: keyboard; at L390" + }, + { + "source": "parseStr@L336:C1", + "target": "error_pos@L485:C1", + "source_location": "L395", + "column": 9, + "source_text": " pos=error_pos('End of file while expecting end of inputstr',inputstr,pos);", + "reason": "whole-scope bare command/script invocation barrier: keyboard; at L390" + } + ], + "review_notes": [ + "All 13 functions are immediate file children, each explicitly terminated by end. No nested definitions, classes, methods, duplicate names or inheritance occur. Primary function loadjson agrees with loadjson.m.", + "All accepted targets are unique exact-case file-local functions. Inputs, outputs and assignment roots in each caller do not bind any of these target names; no imports or nested lexical ancestors occur.", + "parse_array: eval(arraystr) at L261 vetoes the entire callable scope, including sites before eval and unreachable paths; catch does not restore binding confidence. The handle @unescapejsonstring at L263 is separately excluded.", + "parseStr: bare keyboard; at L390 is an unknown-workspace command invocation, even in a supposedly unreachable otherwise branch. It vetoes all five same-file call sites in that callable. The declaration and incoming calls remain valid.", + "No eligible same-file sites occur in error_pos, skip_whitespace, unescapejsonstring or sscanf_prep. parse2darray calls external match_bracket at L536 (excluded) and local sscanf_prep at L537 (included).", + "Parenthesis expressions on bound inputstr, data, objid, esc, str, object, poShow, arraystr, astr, dims and other assigned roots are indexing, never calls. Field/indexed assignments do not create callable targets.", + "External helpers include varargin2struct, jsonopt, jdatadecode, fast_match_bracket, nestbracket2dim, encodevarname and match_bracket. None has a declaration in this file, so none is in the call denominator.", + "containers.Map at L455 is dotted constructor syntax; anonymous-function bodies at L285 and L509 and replacement-expression string at L520 are excluded. Bare values such as pwd, inf and nargin in expressions are not direct parenthesized call sites or standalone command statements.", + "L103 error_pos has fewer arguments than its signature; static eligible call identity does not assert runtime validity. Corpus source is not repaired or executed.", + "This corpus is intentionally limited: synthetic fixtures, not this manifest, cover classes, namespaces, nested scopes and duplicate names." + ] +} diff --git a/tests/fixtures/matlab/jsonlab/loadjson.m b/tests/fixtures/matlab/jsonlab/loadjson.m new file mode 100644 index 0000000000..69811ed122 --- /dev/null +++ b/tests/fixtures/matlab/jsonlab/loadjson.m @@ -0,0 +1,552 @@ +function data = loadjson(fname,varargin) +% +% data=loadjson(fname,opt) +% or +% data=loadjson(fname,'param1',value1,'param2',value2,...) +% +% parse a JSON (JavaScript Object Notation) file or string +% +% authors:Qianqian Fang (q.fang neu.edu) +% created on 2011/09/09, including previous works from +% +% Nedialko Krouchev: http://www.mathworks.com/matlabcentral/fileexchange/25713 +% created on 2009/11/02 +% François Glineur: http://www.mathworks.com/matlabcentral/fileexchange/23393 +% created on 2009/03/22 +% Joel Feenstra: +% http://www.mathworks.com/matlabcentral/fileexchange/20565 +% created on 2008/07/03 +% +% input: +% fname: input file name; if fname contains "{}" or "[]", fname +% will be interpreted as a JSON string +% opt: (optional) a struct to store parsing options, opt can be replaced by +% a list of ('param',value) pairs - the param string is equivallent +% to a field in opt. opt can have the following +% fields (first in [.|.] is the default) +% +% SimplifyCell [1|0]: if set to 1, loadjson will call cell2mat +% for each element of the JSON data, and group +% arrays based on the cell2mat rules. +% FastArrayParser [1|0 or integer]: if set to 1, use a +% speed-optimized array parser when loading an +% array object. The fast array parser may +% collapse block arrays into a single large +% array similar to rules defined in cell2mat; 0 to +% use a legacy parser; if set to a larger-than-1 +% value, this option will specify the minimum +% dimension to enable the fast array parser. For +% example, if the input is a 3D array, setting +% FastArrayParser to 1 will return a 3D array; +% setting to 2 will return a cell array of 2D +% arrays; setting to 3 will return to a 2D cell +% array of 1D vectors; setting to 4 will return a +% 3D cell array. +% UseMap [0|1]: if set to 1, loadjson uses a containers.Map to +% store map objects; otherwise use a struct object +% ShowProgress [0|1]: if set to 1, loadjson displays a progress bar. +% ParseStringArray [0|1]: if set to 0, loadjson converts "string arrays" +% (introduced in MATLAB R2016b) to char arrays; if set to 1, +% loadjson skips this conversion. +% FormatVersion [2|float]: set the JSONLab format version; since +% v2.0, JSONLab uses JData specification Draft 1 +% for output format, it is incompatible with all +% previous releases; if old output is desired, +% please set FormatVersion to 1.9 or earlier. +% Encoding ['']: json file encoding. Support all encodings of +% fopen() function +% ObjectID [0|interger or list]: if set to a positive number, +% it returns the specified JSON object by index +% in a multi-JSON document; if set to a vector, +% it returns a list of specified objects. +% JDataDecode [1|0]: if set to 1, call jdatadecode to decode +% JData structures defined in the JData +% Specification. +% +% output: +% dat: a cell array, where {...} blocks are converted into cell arrays, +% and [...] are converted to arrays +% +% examples: +% dat=loadjson('{"obj":{"string":"value","array":[1,2,3]}}') +% dat=loadjson(['examples' filesep 'example1.json']) +% dat=loadjson(['examples' filesep 'example1.json'],'SimplifyCell',0) +% +% license: +% BSD or GPL version 3, see LICENSE_{BSD,GPLv3}.txt files for details +% +% -- this function is part of JSONLab toolbox (http://iso2mesh.sf.net/cgi-bin/index.cgi?jsonlab) +% + + opt=varargin2struct(varargin{:}); + + if(regexp(fname,'^\s*(?:\[.*\])|(?:\{.*\})\s*$','once')) + string=fname; + elseif(exist(fname,'file')) + try + encoding = jsonopt('Encoding','',opt); + if(isempty(encoding)) + string = fileread(fname); + else + fid = fopen(fname,'r','n',encoding); + string = fread(fid,'*char')'; + fclose(fid); + end + catch + try + string = urlread(['file://',fname]); + catch + string = urlread(['file://',fullfile(pwd,fname)]); + end + end + else + error_pos('input file does not exist'); + end + + pos = 1; inputlen = length(string); inputstr = string; + arraytokenidx=find(inputstr=='[' | inputstr==']'); + arraytoken=inputstr(arraytokenidx); + + % String delimiters and escape chars identified to improve speed: + esc = find(inputstr=='"' | inputstr=='\' ); % comparable to: regexp(inputstr, '["\\]'); + index_esc = 1; + + opt.arraytoken_=arraytoken; + opt.arraytokenidx_=arraytokenidx; + opt.simplifycell=jsonopt('SimplifyCell',1,opt); + opt.simplifycellarray=jsonopt('SimplifyCellArray',0,opt); + opt.formatversion=jsonopt('FormatVersion',2,opt); + opt.fastarrayparser=jsonopt('FastArrayParser',1,opt); + opt.parsestringarray=jsonopt('ParseStringArray',0,opt); + opt.usemap=jsonopt('UseMap',0,opt); + opt.arraydepth_=1; + + if(jsonopt('ShowProgress',0,opt)==1) + opt.progressbar_=waitbar(0,'loading ...'); + end + + objid=jsonopt('ObjectID',0,opt); + maxobjid=max(objid); + if(maxobjid==0) + maxobjid=inf; + end + + jsoncount=1; + while pos <= inputlen + [cc,pos]=next_char(inputstr, pos); + switch(cc) + case '{' + [data{jsoncount},pos,index_esc] = parse_object(inputstr, pos, esc, index_esc,opt); + case '[' + [data{jsoncount},pos,index_esc] = parse_array(inputstr, pos, esc, index_esc,opt); + otherwise + pos=error_pos('Outer level structure must be an object or an array',inputstr,pos); + end + if(jsoncount>=maxobjid) + break; + end + jsoncount=jsoncount+1; + end % while + + if(length(objid)>1 || min(objid)>1) + data=data(objid(objid<=length(data))); + end + + jsoncount=length(data); + if(jsoncount==1 && iscell(data)) + data=data{1}; + end + + if(jsonopt('JDataDecode',1,varargin{:})==1) + data=jdatadecode(data,'Base64',1,'Recursive',1,varargin{:}); + end + + if(isfield(opt,'progressbar_')) + close(opt.progressbar_); + end +end + +%%------------------------------------------------------------------------- +%% helper functions +%%------------------------------------------------------------------------- + +function [object, pos,index_esc] = parse_array(inputstr, pos, esc, index_esc, varargin) % JSON array is written in row-major order + pos=parse_char(inputstr, pos, '['); + object = cell(0, 1); + arraydepth=varargin{1}.arraydepth_; + pbar=-1; + if(isfield(varargin{1},'progressbar_')) + pbar=varargin{1}.progressbar_; + end + format=varargin{1}.formatversion; + [cc,pos]=next_char(inputstr,pos); + endpos=[]; + + if cc ~= ']' + try + if((varargin{1}.fastarrayparser)>=1 && arraydepth>=varargin{1}.fastarrayparser) + [endpos, maxlevel]=fast_match_bracket(varargin{1}.arraytoken_,varargin{1}.arraytokenidx_,pos); + if(~isempty(endpos)) + arraystr=['[' inputstr(pos:endpos)]; + arraystr=sscanf_prep(arraystr); + if(isempty(find(arraystr=='"', 1))) + % handle 1D array first + if(maxlevel==1) + astr=arraystr(2:end-1); + astr(astr==' ')=''; + [obj, count, errmsg, nextidx]=sscanf(astr,'%f,',[1,inf]); + if(nextidx>=length(astr)-1) + object=obj; + pos=endpos; + pos=parse_char(inputstr, pos, ']'); + return; + end + end + + % next handle 2D array, these are most common ones + if(maxlevel==2 && ~isempty(regexp(arraystr(2:end),'^\s*\[','once'))) + rowstart=find(arraystr(2:end)=='[',1)+1; + if(rowstart) + [obj, nextidx]=parse2darray(inputstr,pos+rowstart,arraystr); + if(nextidx>=length(arraystr)-1) + object=obj; + if(format>1.9) + object=object.'; + end + pos=endpos; + pos=parse_char(inputstr, pos, ']'); + if(pbar>0) + waitbar(pos/length(inStr),pbar,'loading ...'); + end + return; + end + end + end + + % for N-D packed array in a nested array construct, + % in the future can replace 1d and 2d cases + if(maxlevel>2 && ~isempty(regexp(arraystr(2:end),'^\s*\[\s*\[','once'))) + astr=arraystr; + dims=nestbracket2dim(astr); + if(any(dims==0) || all(mod(dims(:),1) == 0)) % all dimensions are integers - this can be problematic + astr=arraystr; + astr(astr=='[')=''; + astr(astr==']')=''; + astr=regexprep(astr,'\s*,',','); + astr=regexprep(astr,'\s*$',''); + [obj, count, errmsg, nextidx]=sscanf(astr,'%f,',inf); + if(nextidx>=length(astr)-1) + object=reshape(obj,dims); + if(format>1.9) + object=permute(object,ndims(object):-1:1); + end + pos=endpos; + pos=parse_char(inputstr, pos, ']'); + if(pbar>0) + waitbar(pos/length(inStr),pbar,'loading ...'); + end + return; + end + end + end + end + end + end + if(isempty(regexp(arraystr,':','once'))) + arraystr=regexprep(arraystr,'\[','{'); + arraystr=regexprep(arraystr,'\]','}'); + if(varargin{1}.parsestringarray==0) + arraystr=regexprep(arraystr,'\"',''''); + end + object=eval(arraystr); + if(iscell(object)) + object=cellfun(@unescapejsonstring,object,'UniformOutput',false); + end + pos=endpos; + end + catch + end + if(isempty(endpos) || pos~=endpos) + while 1 + varargin{1}.arraydepth_=arraydepth+1; + [val, pos,index_esc] = parse_value(inputstr, pos, esc, index_esc,varargin{:}); + object{end+1} = val; + [cc,pos]=next_char(inputstr,pos); + if cc == ']' + break; + end + pos=parse_char(inputstr, pos, ','); + end + end + end + + if(varargin{1}.simplifycell) + if(iscell(object) && ~isempty(object) && isnumeric(object{1})) + if(all(cellfun(@(e) isequal(size(object{1}), size(e)) , object(2:end)))) + try + oldobj=object; + if(iscell(object) && length(object)>1 && ndims(object{1})>=2) + catdim=size(object{1}); + catdim=ndims(object{1})-(catdim(end)==1)+1; + object=cat(catdim,object{:}); + object=permute(object,ndims(object):-1:1); + else + object=cell2mat(object')'; + end + if(iscell(oldobj) && isstruct(object) && numel(object)>1 && varargin{1}.simplifycellarray==0) + object=oldobj; + end + catch + end + end + end + if(~iscell(object) && size(object,1)>1 && ndims(object)==2) + object=object'; + end + end + pos=parse_char(inputstr, pos, ']'); + + if(pbar>0) + waitbar(pos/length(inputstr),pbar,'loading ...'); + end +end +%%------------------------------------------------------------------------- + +function pos=parse_char(inputstr, pos, c) + pos=skip_whitespace(pos, inputstr); + if pos > length(inputstr) || inputstr(pos) ~= c + pos=error_pos(sprintf('Expected %c at position %%d', c),inputstr,pos); + else + pos = pos + 1; + pos=skip_whitespace(pos, inputstr); + end +end +%%------------------------------------------------------------------------- + +function [c, pos] = next_char(inputstr, pos) + pos=skip_whitespace(pos, inputstr); + if pos > length(inputstr) + c = []; + else + c = inputstr(pos); + end +end + +%%------------------------------------------------------------------------- +function [str, pos,index_esc] = parseStr(inputstr, pos, esc, index_esc, varargin) + if inputstr(pos) ~= '"' + pos=error_pos('String starting with " expected at position %d',inputstr,pos); + else + pos = pos + 1; + end + str = ''; + while pos <= length(inputstr) + while index_esc <= length(esc) && esc(index_esc) < pos + index_esc = index_esc + 1; + end + if index_esc > length(esc) + str = [str inputstr(pos:end)]; + pos = length(inputstr) + 1; + break; + else + str = [str inputstr(pos:esc(index_esc)-1)]; + pos = esc(index_esc); + end + nstr = length(str); + switch inputstr(pos) + case '"' + pos = pos + 1; + if(~isempty(str)) + if(strcmp(str,'_Inf_')) + str=Inf; + elseif(strcmp(str,'-_Inf_')) + str=-Inf; + elseif(strcmp(str,'_NaN_')) + str=NaN; + end + end + return; + case '\' + if pos+1 > length(inputstr) + pos=error_pos('End of file reached right after escape character',inputstr,pos); + end + pos = pos + 1; + switch inputstr(pos) + case {'"' '\' '/'} + str(nstr+1) = inputstr(pos); + pos = pos + 1; + case {'b' 'f' 'n' 'r' 't'} + str(nstr+1) = sprintf(['\' inputstr(pos)]); + pos = pos + 1; + case 'u' + if pos+4 > length(inputstr) + pos=error_pos('End of file reached in escaped unicode character',inputstr,pos); + end + str(nstr+(1:6)) = inputstr(pos-1:pos+4); + pos = pos + 5; + end + otherwise % should never happen + str(nstr+1) = inputstr(pos); + keyboard; + pos = pos + 1; + end + end + str=unescapejsonstring(str); + pos=error_pos('End of file while expecting end of inputstr',inputstr,pos); +end +%%------------------------------------------------------------------------- + +function [num, pos] = parse_number(inputstr, pos, varargin) + currstr=inputstr(pos:min(pos+30,end)); + [num, one, err, delta] = sscanf(currstr, '%f', 1); + if ~isempty(err) + pos=error_pos('Error reading number at position %d',inputstr,pos); + end + pos = pos + delta-1; +end +%%------------------------------------------------------------------------- + +function [val, pos,index_esc] = parse_value(inputstr, pos, esc, index_esc, varargin) + len=length(inputstr); + if(isfield(varargin{1},'progressbar_')) + waitbar(pos/len,varargin{1}.progressbar_,'loading ...'); + end + + switch(inputstr(pos)) + case '"' + [val, pos,index_esc] = parseStr(inputstr, pos, esc, index_esc,varargin{:}); + return; + case '[' + [val, pos,index_esc] = parse_array(inputstr, pos, esc, index_esc, varargin{:}); + return; + case '{' + [val, pos,index_esc] = parse_object(inputstr, pos, esc, index_esc, varargin{:}); + return; + case {'-','0','1','2','3','4','5','6','7','8','9'} + [val, pos] = parse_number(inputstr, pos, varargin{:}); + return; + case 't' + if pos+3 <= len && strcmpi(inputstr(pos:pos+3), 'true') + val = true; + pos = pos + 4; + return; + end + case 'f' + if pos+4 <= len && strcmpi(inputstr(pos:pos+4), 'false') + val = false; + pos = pos + 5; + return; + end + case 'n' + if pos+3 <= len && strcmpi(inputstr(pos:pos+3), 'null') + val = []; + pos = pos + 4; + return; + end + end + pos=error_pos('Value expected at position %d',inputstr,pos); +end + +%%------------------------------------------------------------------------- +function [object, pos, index_esc] = parse_object(inputstr, pos, esc, index_esc, varargin) + pos=parse_char(inputstr, pos, '{'); + usemap=varargin{1}.usemap; + if(usemap) + object = containers.Map(); + else + object = []; + end + [cc,pos]=next_char(inputstr,pos); + if cc ~= '}' + while 1 + [str, pos, index_esc] = parseStr(inputstr, pos, esc, index_esc, varargin{:}); + if isempty(str) + pos=error_pos('Name of value at position %d cannot be empty',inputstr,pos); + end + pos=parse_char(inputstr, pos, ':'); + [val, pos,index_esc] = parse_value(inputstr, pos, esc, index_esc, varargin{:}); + if(usemap) + object(str)=val; + else + object.(encodevarname(str,varargin{:}))=val; + end + [cc,pos]=next_char(inputstr,pos); + if cc == '}' + break; + end + pos=parse_char(inputstr, pos, ','); + end + end + pos=parse_char(inputstr, pos, '}'); +end + +%%------------------------------------------------------------------------- + +function pos=error_pos(msg, inputstr, pos) + poShow = max(min([pos-15 pos-1 pos pos+20],length(inputstr)),1); + if poShow(3) == poShow(2) + poShow(3:4) = poShow(2)+[0 -1]; % display nothing after + end + msg = [sprintf(msg, pos) ': ' ... + inputstr(poShow(1):poShow(2)) '' inputstr(poShow(3):poShow(4)) ]; + error( ['JSONLAB:JSON:InvalidFormat: ' msg] ); +end + +%%------------------------------------------------------------------------- + +function newpos=skip_whitespace(pos, inputstr) + newpos=pos; + while newpos <= length(inputstr) && isspace(inputstr(newpos)) + newpos = newpos + 1; + end +end + +%%------------------------------------------------------------------------- +function newstr=unescapejsonstring(str) + newstr=str; + if(iscell(str)) + try + newstr=cell2mat(cellfun(@(x) cell2mat(x),str(:),'un',0)); + catch + end + end + if(~ischar(str)) + return; + end + escapechars={'\\','\"','\/','\a','\b','\f','\n','\r','\t','\v'}; + for i=1:length(escapechars); + newstr=regexprep(newstr,regexprep(escapechars{i},'\\','\\\\'), escapechars{i}); + end + newstr=regexprep(newstr,'\\u([0-9A-Fa-f]{4})', '${char(base2dec($1,16))}'); +end + +%%------------------------------------------------------------------------- +function arraystr=sscanf_prep(str) + arraystr=str; + if(regexp(str,'"','once')) + arraystr=regexprep(arraystr,'"_NaN_"','NaN'); + arraystr=regexprep(arraystr,'"([-+]*)_Inf_"','$1Inf'); + end + arraystr(arraystr==sprintf('\n'))=[]; + arraystr(arraystr==sprintf('\r'))=[]; +end + +%%------------------------------------------------------------------------- +function [obj, nextidx,nextdim]=parse2darray(inputstr,startpos,arraystr) + rowend=match_bracket(inputstr,startpos); + rowstr=sscanf_prep(inputstr(startpos-1:rowend)); + [vec1, nextdim, errmsg, nextidx]=sscanf(rowstr,'%f,',[1 inf]); + if(nargin==2) + obj=nextdim; + return; + end + astr=arraystr; + astr(astr=='[')=''; + astr(astr==']')=''; + astr=regexprep(deblank(astr),'\s+,',','); + [obj, count, errmsg, nextidx]=sscanf(astr,'%f,',inf); + if(nextidx>=length(astr)-1) + obj=reshape(obj,nextdim,numel(obj)/nextdim); + nextidx=length(arraystr)+1; + end +end From f7a02647894578e064e9efa705a12ec056a6d7f6 Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:50:13 +0100 Subject: [PATCH 03/11] fix(matlab): isolate persisted build and cross-repo relationships Co-Authored-By: OpenAI Codex --- graphify/build.py | 5 + graphify/cross_repo_calls.py | 3 + graphify/cross_repo_types.py | 3 + tests/test_matlab_merge_isolation.py | 149 +++++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 tests/test_matlab_merge_isolation.py diff --git a/graphify/build.py b/graphify/build.py index bba67c1c1a..11371d4c69 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -1764,6 +1764,11 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat # 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; extraction provenance wins over suffixes. + 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: + 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) diff --git a/graphify/cross_repo_calls.py b/graphify/cross_repo_calls.py index 7e17aaddbb..e386f5f8c6 100644 --- a/graphify/cross_repo_calls.py +++ b/graphify/cross_repo_calls.py @@ -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")) diff --git a/graphify/cross_repo_types.py b/graphify/cross_repo_types.py index 3cd9ea0124..cf2434242a 100644 --- a/graphify/cross_repo_types.py +++ b/graphify/cross_repo_types.py @@ -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"): diff --git a/tests/test_matlab_merge_isolation.py b/tests/test_matlab_merge_isolation.py new file mode 100644 index 0000000000..b0202f8d51 --- /dev/null +++ b/tests/test_matlab_merge_isolation.py @@ -0,0 +1,149 @@ +"""MATLAB provenance isolates persisted relationships without hiding native matches.""" + +import networkx as nx +import pytest + +from graphify.affected import load_graph +from graphify.build import build, build_merge, prefix_graph_for_global +from graphify.cross_repo_calls import link_cross_repo_member_calls +from graphify.cross_repo_types import link_shared_type_declarations +from graphify.export import to_json + + +def _node(node_id, label, source, *, matlab=False, kind="function", **metadata): + if matlab: + metadata.update(language="matlab", kind=f"matlab_{kind}") + return { + "id": node_id, "label": label, "source_file": source, + "source_location": "L1", "file_type": "code", "type": "code", + "_callable": kind != "file", "_callable_class": kind == "class", + "metadata": metadata, + } + + +def _edge(source, target, relation, source_file, confidence="EXTRACTED"): + return { + "source": source, "target": target, "relation": relation, + "source_file": source_file, "source_location": "L3", + "confidence": confidence, "weight": 1.0, + } + + +def _persist(tmp_path, name, graph): + path = tmp_path / f"{name}.json" + to_json(graph, {}, str(path), force=True) + return path, load_graph(path).to_undirected() + + +@pytest.mark.parametrize("native_file", ["Foo.h", "Foo.m", "Foo.mm", "Foo.c"]) +@pytest.mark.parametrize("relation", ["calls", "imports", "imports_from", "references"]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_persisted_matlab_native_homonyms_are_not_neighbors( + tmp_path, native_file, relation, reverse, +): + nodes = [ + _node("matlab", "Foo", "Foo.m", matlab=True), + _node("native", "Foo", native_file), + _node("native_target", "bar", "bar.m"), + ] + source, target = ("matlab", "native") if reverse else ("native", "matlab") + graph = build([{"nodes": nodes, "edges": [ + _edge(source, target, relation, nodes[1 if not reverse else 0]["source_file"], "INFERRED"), + _edge("native", "native_target", relation, native_file, "INFERRED"), + ]}], dedup=False) + _, reloaded = _persist(tmp_path, "mixed", graph) + assert set(reloaded.neighbors("matlab")) == set() + assert set(reloaded.neighbors("native")) == {"native_target"} + assert reloaded.edges["native", "native_target"]["relation"] == relation + + +def _type_graph(*, matlab=False, source="Greeter.m", method="-greet"): + return build([{"nodes": [ + _node("greeter", "Greeter", source, matlab=matlab, kind="class", namespace="Shared"), + _node("greet", method, source, matlab=matlab, kind="method"), + ], "edges": [_edge("greeter", "greet", "method", source)]}], dedup=False) + + +@pytest.mark.parametrize("native_sources", [[], ["Greeter.m"], ["Greeter.mm"], ["Greeter.m", "Greeter.mm"]]) +def test_persisted_matlab_collision_does_not_answer_or_obscure_native_calls(tmp_path, native_sources): + caller = build([{"nodes": [_node("caller", "run", "app.m", unresolved_calls=[{ + "lang": "objc", "receiver_type": "Greeter", "callee": "greet", "line": "L3", + }])], "edges": []}], dedup=False) + _, caller = _persist(tmp_path, "caller", caller) + _, matlab = _persist(tmp_path, "matlab", _type_graph(matlab=True)) + graphs = [prefix_graph_for_global(caller, "app"), prefix_graph_for_global(matlab, "matlab")] + for index, source in enumerate(native_sources): + _, native = _persist(tmp_path, f"native{index}", _type_graph(source=source)) + graphs.append(prefix_graph_for_global(native, f"native{index}")) + merged = nx.compose_all(graphs) + link_cross_repo_member_calls(merged) + _, merged = _persist(tmp_path, "merged", merged) + expected = {"native0::greet"} if len(native_sources) == 1 else set() + assert set(merged.neighbors("app::caller")) == expected + assert set(merged.neighbors("matlab::greet")) == {"matlab::greeter"} + + +def test_namespace_matlab_classes_do_not_join_native_or_matlab_types(tmp_path): + graphs = [] + for repo, matlab, source in [ + ("matlab1", True, "+Shared/Greeter.m"), + ("matlab2", True, "+Shared/Greeter.m"), + ("native1", False, "Greeter.cs"), + ("native2", False, "Greeter.cs"), + ]: + _, graph = _persist(tmp_path, repo, _type_graph(matlab=matlab, source=source)) + graphs.append(prefix_graph_for_global(graph, repo)) + merged = nx.compose_all(graphs) + link_shared_type_declarations(merged) + _, merged = _persist(tmp_path, "merged", merged) + assert set(merged.neighbors("matlab1::greeter")) == {"matlab1::greet"} + assert set(merged.neighbors("matlab2::greeter")) == {"matlab2::greet"} + assert set(merged.neighbors("native1::greeter")) == {"native1::greet", "native2::greeter"} + assert merged.edges["native1::greeter", "native2::greeter"]["relation"] == "same_type_as" + + +def test_incremental_carry_forward_preserves_local_calls_and_source_inheritance(tmp_path): + nodes = [ + _node("child_file", "Child.m", "+Shared/Child.m", matlab=True, kind="file", + matlab_file_kind="class", matlab_primary_name="Child"), + _node("child", "Child", "+Shared/Child.m", matlab=True, kind="class", + namespace="Shared", scope_chain=[], + matlab_superclasses=[{"name": "Shared.Base", "source_location": "L1"}]), + _node("base_file", "Base.m", "+Shared/Base.m", matlab=True, kind="file", + matlab_file_kind="class", matlab_primary_name="Base"), + _node("base", "Base", "+Shared/Base.m", matlab=True, kind="class", namespace="Shared"), + _node("run", "run", "+Shared/Child.m", matlab=True, kind="method"), + _node("helper", "helper", "+Shared/Child.m", matlab=True), + ] + edges = [ + _edge("child_file", "child", "contains", "+Shared/Child.m"), + _edge("base_file", "base", "contains", "+Shared/Base.m"), + _edge("child", "base", "inherits", "+Shared/Child.m"), + _edge("child", "run", "method", "+Shared/Child.m"), + _edge("run", "helper", "calls", "+Shared/Child.m"), + ] + path, _ = _persist(tmp_path, "base", build([{"nodes": nodes, "edges": edges}], dedup=False)) + updated = build_merge([{ + "nodes": [_node("native", "Child", "Child.h")], + "edges": [_edge("native", "child", "references", "Child.h", "INFERRED")], + }], path, dedup=False) + _, updated = _persist(tmp_path, "updated", updated) + native = build([{"nodes": [ + _node("child", "Child", "Child.cs", kind="class", namespace="Shared"), + ], "edges": []}], dedup=False) + merged = nx.compose(prefix_graph_for_global(updated, "matlab"), + prefix_graph_for_global(native, "native")) + link_cross_repo_member_calls(merged) + link_shared_type_declarations(merged) + _, merged = _persist(tmp_path, "merged", merged) + assert set(merged.neighbors("matlab::native")) == set() + assert set(merged.neighbors("matlab::run")) == {"matlab::child", "matlab::helper"} + assert set(merged.neighbors("matlab::child")) == { + "matlab::child_file", "matlab::run", "matlab::base", + } + assert merged.edges["matlab::run", "matlab::helper"]["confidence"] == "EXTRACTED" + assert merged.edges["matlab::child", "matlab::base"]["relation"] == "inherits" + assert merged.nodes["matlab::child"]["metadata"]["matlab_superclasses"] == [ + {"name": "Shared.Base", "source_location": "L1"}, + ] + assert merged.nodes["matlab::child_file"]["metadata"]["matlab_primary_name"] == "Child" From 30ddcbc0396b19a75fb1cc2a5a58b14bb415ebd9 Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:28:10 +0100 Subject: [PATCH 04/11] fix(matlab): isolate shared extraction resolution Co-Authored-By: OpenAI Codex --- graphify/extract.py | 90 +++++++++--- tests/test_matlab_native_isolation.py | 196 ++++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 16 deletions(-) create mode 100644 tests/test_matlab_native_isolation.py diff --git a/graphify/extract.py b/graphify/extract.py index e22c2fd370..0381980eb7 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -3907,6 +3907,11 @@ def _node_label_key(node: dict, fold: bool = False) -> str: return key.lower() if fold else key +def _is_matlab_node(node: dict) -> bool: + """Use explicit provenance; `.m` also denotes native Objective-C sources.""" + return (node.get("metadata") or {}).get("language") == "matlab" + + def _is_top_level_function_definition(node: dict) -> bool: """A free/top-level function def (label ``name()``), not a method or type. @@ -3929,8 +3934,11 @@ def _rewire_unique_stub_nodes(nodes: list[dict], edges: list[dict]) -> None: real_by_label_ci: dict[str, list[dict]] = {} # case-INSENSITIVE-language reals only func_by_label: dict[str, list[dict]] = {} # top-level function defs (#1781) stubs: list[dict] = [] + matlab_ids = {node.get("id") for node in nodes if _is_matlab_node(node)} for node in nodes: + if node.get("id") in matlab_ids: + continue key = _node_label_key(node) if not key: continue @@ -3959,6 +3967,8 @@ def _rewire_unique_stub_nodes(nodes: list[dict], edges: list[dict]) -> None: supertype_stub_ids: set[str] = set() # stubs used as a base type — never a function _SUPERTYPE_RELATIONS = {"inherits", "implements", "extends"} for edge in edges: + if edge.get("source") in matlab_ids: + continue rel = edge.get("relation") for endpoint in ("source", "target"): nid = edge.get(endpoint) @@ -4082,6 +4092,8 @@ def _cross_language_candidate(edge: dict, remapped_id: str) -> bool: ) for edge in edges: + if edge.get("source") in matlab_ids: + continue is_csharp_scoped_edge = ( _is_dotnet_source_file(str(edge.get("source_file", ""))) and edge.get("relation") in csharp_scoped_relations @@ -4174,10 +4186,18 @@ def _merge_swift_extensions( # bare-reference shadow nodes (`let x: Foo`) carry a source_file but are NOT # contained, so excluding them keeps a stub from making a real type look # ambiguous — same predicate the Swift member-call resolver uses (#2538). - contained = {e.get("target") for e in all_edges if e.get("relation") == "contains"} + matlab_ids = {n.get("id") for n in all_nodes if _is_matlab_node(n)} + contained = { + e.get("target") for e in all_edges + if e.get("relation") == "contains" + and e.get("source") not in matlab_ids + and e.get("target") not in matlab_ids + } label_to_canonical: dict[str, list[str]] = {} for n in all_nodes: + if _is_matlab_node(n): + continue if n.get("id") in extension_nids: continue label = n.get("label") @@ -4595,13 +4615,21 @@ def _key(label: str) -> str: # Bare type references create a same-label shadow node (via ensure_named_node) # that carries a source_file but is NOT contained; excluding non-contained # nodes keeps that shadow from making a real type name look ambiguous. - contained = {e.get("target") for e in all_edges if e.get("relation") == "contains"} + matlab_ids = {n.get("id") for n in all_nodes if _is_matlab_node(n)} + contained = { + e.get("target") for e in all_edges + if e.get("relation") == "contains" + and e.get("source") not in matlab_ids + and e.get("target") not in matlab_ids + } # Type name -> definition node ids (real, source-backed, type-like defs only). # len != 1 is the god-node guard: an ambiguous type name bails. type_def_nids: dict[str, list[str]] = {} node_by_id: dict[str, dict] = {} for n in all_nodes: + if _is_matlab_node(n): + continue node_by_id[n.get("id")] = n if n.get("source_file") and n.get("id") in contained and _is_type_like_definition(n): type_def_nids.setdefault(_key(n.get("label", "")), []).append(n["id"]) @@ -4609,7 +4637,7 @@ def _key(label: str) -> str: # (type_node_id, method_key) -> method_node_id, from `method` edges. method_index: dict[tuple[str, str], str] = {} for e in all_edges: - if e.get("relation") != "method": + if e.get("relation") != "method" or e.get("source") in matlab_ids: continue src, tgt = e.get("source"), e.get("target") tnode = node_by_id.get(tgt) @@ -4632,9 +4660,13 @@ def _key(label: str) -> str: # method nid -> marked plain-return target nids (must be exactly one). return_targets_by_method: dict[str, set[str]] = {} for e in all_edges: - if (e.get("relation") == "references" - and e.get("context") == "return_type" - and (e.get("metadata") or {}).get("swift_plain_return")): + if ( + e.get("relation") == "references" + and e.get("context") == "return_type" + and (e.get("metadata") or {}).get("swift_plain_return") + and e.get("source") not in matlab_ids + and e.get("target") not in matlab_ids + ): return_targets_by_method.setdefault( e.get("source"), set()).add(e.get("target")) for path, pending in factory_by_file.items(): @@ -5405,13 +5437,19 @@ def _key(label: str) -> str: return re.sub(r"[^a-zA-Z0-9]+", "", str(label)).lower() # A genuine C++ type is the target of a `contains` edge from its file node; - # bare-reference shadow nodes (ensure_named_node stubs) are not contained, so - # excluding non-contained nodes keeps them from making a real type ambiguous. - contained = {e.get("target") for e in all_edges if e.get("relation") == "contains"} + matlab_ids = {n.get("id") for n in all_nodes if _is_matlab_node(n)} + contained = { + e.get("target") for e in all_edges + if e.get("relation") == "contains" + and e.get("source") not in matlab_ids + and e.get("target") not in matlab_ids + } type_def_nids: dict[str, list[str]] = {} node_by_id: dict[str, dict] = {} for n in all_nodes: + if _is_matlab_node(n): + continue node_by_id[n.get("id")] = n if n.get("source_file") and n.get("id") in contained and _is_type_like_definition(n): type_def_nids.setdefault(_key(n.get("label", "")), []).append(n["id"]) @@ -5425,7 +5463,7 @@ def _key(label: str) -> str: enclosing_type: dict[str, str] = {} for rel in ("defines", "method"): for e in all_edges: - if e.get("relation") != rel: + if e.get("relation") != rel or e.get("source") in matlab_ids: continue src, tgt = e.get("source"), e.get("target") tnode = node_by_id.get(tgt) @@ -5447,6 +5485,8 @@ def _key(label: str) -> str: # exactly-one-candidate. qualified_method_nids: dict[str, list[str]] = {} for n in all_nodes: + if _is_matlab_node(n): + continue label = str(n.get("label", "")) if n.get("source_file") and label.endswith("()") and "::" in label: qualified_method_nids.setdefault(label, []).append(n["id"]) @@ -6433,7 +6473,13 @@ def _resolve_objc_member_calls( def _key(label: str) -> str: return re.sub(r"[^a-zA-Z0-9]+", "", str(label)).lower() - contained = {e.get("target") for e in all_edges if e.get("relation") == "contains"} + matlab_ids = {n.get("id") for n in all_nodes if _is_matlab_node(n)} + contained = { + e.get("target") for e in all_edges + if e.get("relation") == "contains" + and e.get("source") not in matlab_ids + and e.get("target") not in matlab_ids + } def _is_protocol_declaration(n: dict) -> bool: """A ``@protocol`` declaration, which the ObjC extractor labels ````. @@ -6448,6 +6494,8 @@ def _is_protocol_declaration(n: dict) -> bool: type_def_nids: dict[str, list[str]] = {} node_by_id: dict[str, dict] = {} for n in all_nodes: + if _is_matlab_node(n): + continue node_by_id[n.get("id")] = n if (n.get("source_file") and n.get("id") in contained and _is_type_like_definition(n) and not _is_protocol_declaration(n)): @@ -6456,12 +6504,12 @@ def _is_protocol_declaration(n: dict) -> bool: method_index: dict[tuple[str, str], str] = {} enclosing_type: dict[str, str] = {} for e in all_edges: - if e.get("relation") != "method": + if e.get("relation") != "method" or e.get("source") in matlab_ids: continue src, tgt = e.get("source"), e.get("target") - enclosing_type.setdefault(tgt, src) tnode = node_by_id.get(tgt) if tnode is not None: + enclosing_type.setdefault(tgt, src) # ObjC method labels carry a +/- sigil (`-doThing`); strip it so the # selector `doThing` keys to the method. method_index[(src, _key(tnode.get("label", "")))] = tgt @@ -6475,7 +6523,9 @@ def _is_protocol_declaration(n: dict) -> bool: # subclass too (#3151): walk the inherits chain, nearest table first. _objc_bases: dict[str, list[str]] = {} for e in all_edges: - if e.get("relation") == "inherits": + if (e.get("relation") == "inherits" + and e.get("source") not in matlab_ids + and e.get("target") not in matlab_ids): _objc_bases.setdefault(e["source"], []).append(e["target"]) def _field_type_up_chain(cls, receiver): @@ -10420,6 +10470,8 @@ def _looks_like_bash(result: object) -> bool: if n.get("id") and n["id"] not in _fresh_ids ] for n in resolution_nodes: + if _is_matlab_node(n): + continue if n.get("file_type") == "rationale" or n.get("type") == "namespace": continue raw = n.get("label", "") @@ -10440,11 +10492,17 @@ def _looks_like_bash(result: object) -> bool: # `resolution_nodes`, not `all_nodes` (#2438): an unchanged callee's context node # carries the marker persisted in graph.json, so an incremental rebuild keeps # resolving callbacks into unchanged files while data symbols stay excluded. - callable_nids = {n["id"] for n in resolution_nodes if n.get("_callable")} + callable_nids = { + n["id"] for n in resolution_nodes + if n.get("_callable") and not _is_matlab_node(n) + } # Class defs are callable only via their constructor; they are frequently passed # as descriptive values (`select(Model)`, exception tuples), not invoked. Exclude # them from the indirect_call guard below to avoid false edges (#2137). - class_nids = {n["id"] for n in resolution_nodes if n.get("_callable_class")} + class_nids = { + n["id"] for n in resolution_nodes + if n.get("_callable_class") and not _is_matlab_node(n) + } # Kotlin import targets (#2526): rewrite each `imports` edge from the bare # last-segment id to the node its written FQN names, via the per-file diff --git a/tests/test_matlab_native_isolation.py b/tests/test_matlab_native_isolation.py new file mode 100644 index 0000000000..66d7bab6db --- /dev/null +++ b/tests/test_matlab_native_isolation.py @@ -0,0 +1,196 @@ +"""MATLAB provenance never enters shared/native name resolution candidates.""" +from copy import deepcopy + +import pytest + +from graphify.extract import ( + _merge_swift_extensions, + _resolve_cpp_member_calls, + _resolve_objc_member_calls, + _resolve_swift_member_calls, + _rewire_unique_stub_nodes, + extract, +) + + +def _node(nid, label, source_file, *, matlab=False, **flags): + node = { + "id": nid, "label": label, "source_file": source_file, + "file_type": "code", "source_location": "L1", **flags, + } + if matlab: + node["metadata"] = {"language": "matlab"} + return node + + +@pytest.mark.parametrize("label", ["Widget", "helper()"]) +@pytest.mark.parametrize("native_present", [False, True]) +def test_stub_rewiring_excludes_matlab_before_uniqueness(label, native_present): + nodes = [ + _node("caller", "caller()", "caller.cpp"), + _node("stub", label.strip("()"), ""), + _node("matlab", label, "symbol.m", matlab=True), + ] + if native_present: + nodes.append(_node("native", label, "symbol.cpp")) + edges = [{"source": "caller", "target": "stub", "relation": "references", + "source_file": "caller.cpp"}] + + _rewire_unique_stub_nodes(nodes, edges) + + assert edges[0]["target"] == ("native" if native_present else "stub") + assert any(n["id"] == "matlab" for n in nodes) + + +def test_stub_rewiring_preserves_matlab_source_edges(): + nodes = [ + _node("matlab", "caller()", "caller.m", matlab=True), + _node("native", "Widget", "widget.cpp"), + _node("stub", "Widget", ""), + _node("caller", "caller()", "caller.cpp"), + ] + edges = [ + {"source": "matlab", "target": "stub", "relation": "references", + "source_file": "caller.m"}, + {"source": "caller", "target": "stub", "relation": "references", + "source_file": "caller.cpp"}, + ] + matlab_edge = deepcopy(edges[0]) + + _rewire_unique_stub_nodes(nodes, edges) + + assert edges[0] == matlab_edge + assert edges[1]["target"] == "native" + + +@pytest.mark.parametrize("language,resolver,suffix", [ + ("cpp", _resolve_cpp_member_calls, ".cpp"), + ("objc", _resolve_objc_member_calls, ".m"), + ("swift", _resolve_swift_member_calls, ".swift"), +]) +@pytest.mark.parametrize("native_present", [False, True]) +def test_native_member_candidates_exclude_matlab(language, resolver, suffix, native_present): + caller_file = "caller" + suffix + nodes = [ + _node("caller", "caller()", caller_file), + _node("matlab_class", "Widget", "Widget.m", matlab=True, _callable_class=True), + _node("matlab_method", ".work()", "Widget.m", matlab=True, _callable=True), + ] + edges = [ + {"source": "matlab_file", "target": "matlab_class", "relation": "contains"}, + {"source": "matlab_class", "target": "matlab_method", "relation": "method"}, + ] + if native_present: + nodes.extend([ + _node("native_class", "Widget", "native" + suffix, _callable_class=True), + _node("native_method", ".work()", "native" + suffix, _callable=True), + ]) + edges.extend([ + {"source": "native_file", "target": "native_class", "relation": "contains"}, + {"source": "native_class", "target": "native_method", "relation": "method"}, + ]) + original_edges = deepcopy(edges) + per_file = [{ + "raw_calls": [{"caller_nid": "caller", "callee": "work", "receiver": "Widget", + "is_member_call": True, "lang": language, "source_file": caller_file}], + language + "_type_table": {"path": caller_file, "table": {}}, + }] + + resolver(per_file, nodes, edges) + + assert edges[:len(original_edges)] == original_edges + calls = [e for e in edges if e.get("source") == "caller"] + assert [(e["relation"], e["target"]) for e in calls] == ( + [("calls", "native_method")] if native_present else [] + ) + + +@pytest.mark.parametrize("native_present", [False, True]) +def test_swift_extensions_do_not_merge_into_matlab(native_present): + nodes = [ + _node("extension", "Widget", "extension.swift"), + _node("matlab", "Widget", "Widget.m", matlab=True), + ] + edges = [{"source": "matlab_file", "target": "matlab", "relation": "contains"}, + {"source": "extension", "target": "method", "relation": "method"}] + if native_present: + nodes.append(_node("native", "Widget", "Widget.swift")) + edges.append({"source": "native_file", "target": "native", "relation": "contains"}) + per_file = [{"swift_extensions": [{"nid": "extension", "label": "Widget"}]}] + + _merge_swift_extensions(per_file, nodes, edges) + + assert next(e for e in edges if e["relation"] == "method")["source"] == ( + "native" if native_present else "extension" + ) + assert any(n["id"] == "matlab" for n in nodes) + + +@pytest.mark.parametrize("native_location", ["absent", "fresh", "unchanged"]) +@pytest.mark.parametrize("suffix,caller_body,native_body,matlab_label", [ + (".c", "void caller() { helper(); }", "void helper() {}", "helper()"), + (".php", " Date: Tue, 27 Oct 2026 08:28:10 +0100 Subject: [PATCH 05/11] feat(matlab): resolve exact repository source inheritance Co-Authored-By: OpenAI Codex --- graphify/extractors/resolution.py | 108 ++++++++++++++ tests/test_matlab_inheritance.py | 227 ++++++++++++++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 tests/test_matlab_inheritance.py diff --git a/graphify/extractors/resolution.py b/graphify/extractors/resolution.py index 1a599d4fe9..0d08742f35 100644 --- a/graphify/extractors/resolution.py +++ b/graphify/extractors/resolution.py @@ -1198,6 +1198,114 @@ def _disambiguate_colliding_node_ids( elif raw_call.get("caller_nid") in unambiguous_remaps: raw_call["caller_nid"] = unambiguous_remaps[str(raw_call["caller_nid"])] +def _matlab_inheritance_edges(nodes: list[dict]) -> list[dict]: + """Match written bases to exact repository source, never MATLAB's runtime path. + + File context must establish a unique primary class. Keep uncertain source + candidates in the index so malformed duplicate package roots cannot make a + different root appear uniquely resolvable. Inputs and reference metadata are + read-only; callers own final-ID remapping and inheritance refreshes. + """ + identifier = re.compile(r"[A-Za-z][A-Za-z0-9_]*\Z") + files: dict[tuple[str | None, str], list[dict]] = {} + classes: dict[tuple[str | None, str], list[dict]] = {} + for node in nodes: + metadata = node.get("metadata") or {} + if not isinstance(metadata, dict) or metadata.get("language") != "matlab": + continue + source = node.get("source_file") + repo = node.get("repo") + if not isinstance(source, str) or not source or not (repo is None or isinstance(repo, str)): + continue + key = (repo, source) + if metadata.get("kind") == "matlab_file": + files.setdefault(key, []).append(node) + elif metadata.get("kind") == "matlab_class": + classes.setdefault(key, []).append(node) + + qualified: dict[tuple[str | None, str], list[tuple[str | None, str]]] = {} + eligible: dict[tuple[str | None, str], dict] = {} + namespaces: dict[tuple[str | None, str], str] = {} + for key in files.keys() | classes.keys(): + repo, source = key + path = Path(source) + if path.suffix != ".m" or not identifier.fullmatch(path.stem): + continue + if any(part == "private" or part.startswith("@") for part in path.parts[:-1]): + continue + parts = [] + for part in reversed(path.parts[:-1]): + if not part.startswith("+"): + break + parts.append(part[1:]) + if any(not identifier.fullmatch(part) for part in parts): + continue + namespace = ".".join(reversed(parts)) + namespaces[key] = namespace + if namespace: + qualified.setdefault((repo, f"{namespace}.{path.stem}"), []).append(key) + file_nodes = files.get(key, []) + class_nodes = classes.get(key, []) + if len(file_nodes) != 1 or len(class_nodes) != 1: + continue + file_node, class_node = file_nodes[0], class_nodes[0] + file_metadata, class_metadata = file_node["metadata"], class_node["metadata"] + if ( + file_node.get("type") != "file" + or class_node.get("type") != "class" + or file_node.get("file_type") != "code" + or class_node.get("file_type") != "code" + or file_node.get("external") + or class_node.get("external") + or not isinstance(class_node.get("id"), str) + or not class_node["id"] + or file_metadata.get("matlab_file_kind") != "class" + or file_metadata.get("matlab_primary_name") != path.stem + or class_node.get("label") != path.stem + or class_metadata.get("scope_chain") != [] + or file_metadata.get("namespace", "") != namespace + or class_metadata.get("namespace", "") != namespace + ): + continue + eligible[key] = class_node + + edges = [] + for key, child in eligible.items(): + repo, source = key + bases = child["metadata"].get("matlab_superclasses", []) + if not isinstance(bases, list): + continue + for base in bases: + if not isinstance(base, dict): + continue + name, location = base.get("name"), base.get("source_location") + if not isinstance(name, str) or not all(identifier.fullmatch(part) for part in name.split(".")): + continue + if not isinstance(location, str) or not re.fullmatch(r"L[1-9][0-9]*", location): + continue + if "." in name: + matches = qualified.get((repo, name), []) + elif not namespaces[key]: + matches = [(repo, str(Path(source).with_name(f"{name}.m")))] + else: + continue + if len(matches) != 1: + continue + target = eligible.get(matches[0]) + if target is None or target["id"] == child["id"]: + continue + edges.append({ + "source": child["id"], + "target": target["id"], + "relation": "inherits", + "confidence": "EXTRACTED", + "source_file": source, + "source_location": location, + "weight": 1.0, + }) + return sorted(edges, key=lambda edge: (edge["source"], edge["source_location"], edge["target"])) + + def _is_type_like_definition(node: dict) -> bool: if node.get("type") == "namespace": return False diff --git a/tests/test_matlab_inheritance.py b/tests/test_matlab_inheritance.py new file mode 100644 index 0000000000..270b3ca23c --- /dev/null +++ b/tests/test_matlab_inheritance.py @@ -0,0 +1,227 @@ +"""The pure repository-source MATLAB inheritance contract (issue #4).""" +from copy import deepcopy + +import pytest + +from graphify.extractors.resolution import _matlab_inheritance_edges + + +def _class(source, name, *, namespace="", bases=(), repo=None): + common = {"file_type": "code", "source_file": source} + if repo is not None: + common["repo"] = repo + prefix = f"{repo}::" if repo is not None else "" + return [ + { + **common, + "id": f"{prefix}{source}:file", + "type": "file", + "label": source, + "metadata": { + "language": "matlab", + "kind": "matlab_file", + "namespace": namespace, + "matlab_file_kind": "class", + "matlab_primary_name": name, + }, + }, + { + **common, + "id": f"{prefix}{source}:class:{name}", + "type": "class", + "label": name, + "source_location": "L1", + "metadata": { + "language": "matlab", + "kind": "matlab_class", + "namespace": namespace, + "scope_chain": [], + "matlab_superclasses": [ + {"name": base, "source_location": f"L{row}"} + for row, base in enumerate(bases, start=2) + ], + }, + }, + ] + + +def test_exact_source_matches_preserve_written_bases_and_input_records(): + nodes = ( + _class("src/Child.m", "Child", bases=("tools.io.Base", "Sibling", "handle")) + + _class("lib/+tools/+io/Base.m", "Base", namespace="tools.io") + + _class("src/Sibling.m", "Sibling") + ) + original = deepcopy(nodes) + + assert _matlab_inheritance_edges(nodes) == [ + { + "source": "src/Child.m:class:Child", + "target": "lib/+tools/+io/Base.m:class:Base", + "relation": "inherits", + "confidence": "EXTRACTED", + "source_file": "src/Child.m", + "source_location": "L2", + "weight": 1.0, + }, + { + "source": "src/Child.m:class:Child", + "target": "src/Sibling.m:class:Sibling", + "relation": "inherits", + "confidence": "EXTRACTED", + "source_file": "src/Child.m", + "source_location": "L3", + "weight": 1.0, + }, + ] + assert nodes == original + + +@pytest.mark.parametrize( + ("child_path", "child_namespace", "base_path", "base_namespace", "written"), + [ + ("Child.m", "", "elsewhere/Base.m", "", "Base"), + ("Child.m", "", "+pkg/Base.m", "pkg", "Base"), + ("+pkg/Child.m", "pkg", "+pkg/Base.m", "pkg", "Base"), + ("Child.m", "", "+pkg/Base.m", "pkg", "pkg.base"), + ("Child.m", "", "+pkg/Base.m", "pkg", "Pkg.Base"), + ("Child.m", "", "base.m", "", "Base"), + ("Child.m", "", "private/+pkg/Base.m", "pkg", "pkg.Base"), + ("Child.m", "", "+pkg/@Base/Base.m", "", "pkg.Base"), + ("@Child/Child.m", "", "+pkg/Base.m", "pkg", "pkg.Base"), + ("private/Child.m", "", "+pkg/Base.m", "pkg", "pkg.Base"), + ("Child.m", "", "+outer/ordinary/+pkg/Base.m", "pkg", "outer.pkg.Base"), + ("Child.m", "", "+bad-name/Base.m", "bad-name", "bad-name.Base"), + ("Child.m", "", "Base.M", "", "Base"), + ], +) +def test_only_exact_namespace_or_unpacked_sibling_matches( + child_path, child_namespace, base_path, base_namespace, written +): + nodes = _class(child_path, "Child", namespace=child_namespace, bases=(written,)) + nodes += _class(base_path, "Base", namespace=base_namespace) + assert _matlab_inheritance_edges(nodes) == [] + + +def test_namespace_chain_stops_at_ordinary_directory(): + nodes = _class("Child.m", "Child", bases=("pkg.Base",)) + nodes += _class("+outer/ordinary/+pkg/Base.m", "Base", namespace="pkg") + edges = _matlab_inheritance_edges(nodes) + assert [(edge["source"], edge["target"]) for edge in edges] == [ + ("Child.m:class:Child", "+outer/ordinary/+pkg/Base.m:class:Base") + ] + + +def test_duplicate_package_roots_and_class_declarations_are_ambiguous(): + nodes = _class("Child.m", "Child", bases=("pkg.Base", "Sibling")) + nodes += _class("one/+pkg/Base.m", "Base", namespace="pkg") + nodes += _class("two/+pkg/Base.m", "Base", namespace="pkg") + nodes += _class("Sibling.m", "Sibling") + duplicate = deepcopy(nodes[-1]) + duplicate["id"] += ":L10" + duplicate["source_location"] = "L10" + nodes.append(duplicate) + assert _matlab_inheritance_edges(nodes) == [] + + +@pytest.mark.parametrize( + ("record", "field", "value"), + [ + (0, "matlab_file_kind", "script"), + (0, "matlab_primary_name", "Other"), + (0, "namespace", "other"), + (1, "namespace", "other"), + (1, "scope_chain", ["Owner"]), + (1, "scope_chain", None), + (1, "language", "objc"), + ], +) +def test_uncertain_primary_class_context_is_not_a_target(record, field, value): + child = _class("Child.m", "Child", bases=("pkg.Base",)) + base = _class("+pkg/Base.m", "Base", namespace="pkg") + base[record]["metadata"][field] = value + assert _matlab_inheritance_edges(child + base) == [] + + +def test_missing_file_context_and_filename_declaration_mismatch_fail_closed(): + child = _class("Child.m", "Child", bases=("Base",)) + base = _class("Base.m", "Base") + assert _matlab_inheritance_edges(child + base[1:]) == [] + assert _matlab_inheritance_edges(child + _class("Base.m", "Other")) == [] + assert _matlab_inheritance_edges(child + _class("Other.m", "Base")) == [] + + +def test_malformed_duplicate_package_root_cannot_make_another_root_unique(): + nodes = _class("Child.m", "Child", bases=("pkg.Base",)) + nodes += _class("one/+pkg/Base.m", "Base", namespace="pkg") + nodes += _class("two/+pkg/Base.m", "Wrong", namespace="pkg") + assert _matlab_inheritance_edges(nodes) == [] + + +def test_self_inheritance_and_unsupported_or_external_references_stay_metadata(): + nodes = _class( + "+pkg/Child.m", "Child", namespace="pkg", + bases=("pkg.Child", "handle", "matlab.mixin.Copyable", "pkg.Base(x)", "pkg..Base", " pkg.Base"), + ) + nodes[-1]["metadata"]["matlab_superclasses"].extend([ + {"raw": "pkg.Base{T}", "source_location": "L9"}, + {"name": "pkg.Base", "source_location": None}, + {"name": "pkg.Base", "source_location": "L0"}, + {"name": None, "source_location": "L12"}, + None, + ]) + nodes += _class("+pkg/Base.m", "Base", namespace="pkg") + original = deepcopy(nodes) + assert _matlab_inheritance_edges(nodes) == [] + assert nodes == original + + +def test_external_spelling_can_match_a_real_eligible_repository_class(): + nodes = _class("Child.m", "Child", bases=("handle", "matlab.mixin.Copyable")) + nodes += _class("handle.m", "handle") + nodes += _class("+matlab/+mixin/Copyable.m", "Copyable", namespace="matlab.mixin") + assert [edge["target"] for edge in _matlab_inheritance_edges(nodes)] == [ + "handle.m:class:handle", "+matlab/+mixin/Copyable.m:class:Copyable", + ] + + +def test_repository_tags_partition_identical_source_paths_and_namespaces(): + nodes = [] + for repo in (None, "one", "two"): + nodes += _class("Child.m", "Child", bases=("pkg.Base", "Base"), repo=repo) + nodes += _class("+pkg/Base.m", "Base", namespace="pkg", repo=repo) + nodes += _class("Base.m", "Base", repo=repo) + edges = _matlab_inheritance_edges(nodes) + assert {(edge["source"], edge["target"]) for edge in edges} == { + ("Child.m:class:Child", "+pkg/Base.m:class:Base"), + ("Child.m:class:Child", "Base.m:class:Base"), + ("one::Child.m:class:Child", "one::+pkg/Base.m:class:Base"), + ("one::Child.m:class:Child", "one::Base.m:class:Base"), + ("two::Child.m:class:Child", "two::+pkg/Base.m:class:Base"), + ("two::Child.m:class:Child", "two::Base.m:class:Base"), + } + assert _matlab_inheritance_edges(list(reversed(nodes))) == edges + assert _matlab_inheritance_edges(nodes[3:] + nodes[:3]) == edges + + +def test_foreign_repository_and_untagged_candidates_cannot_fill_missing_bases(): + nodes = _class("Child.m", "Child", bases=("pkg.Base", "Base"), repo="child") + nodes += _class("+pkg/Base.m", "Base", namespace="pkg", repo="foreign") + nodes += _class("Base.m", "Base") + assert _matlab_inheritance_edges(nodes) == [] + + +def test_nonsource_nodes_cannot_supply_class_targets(): + child = _class("Child.m", "Child", bases=("Base",)) + for field, value in ( + ("external", True), ("file_type", "document"), ("id", None), ("type", "function") + ): + base = _class("Base.m", "Base") + base[-1][field] = value + assert _matlab_inheritance_edges(child + base) == [] + + +def test_non_file_nodes_cannot_establish_source_context(): + child = _class("Child.m", "Child", bases=("Base",)) + base = _class("Base.m", "Base") + base[0]["type"] = "module" + assert _matlab_inheritance_edges(child + base) == [] From c50b71aaebaf755eb322dab39de5b56202cf8408 Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:43:30 +0100 Subject: [PATCH 06/11] feat(matlab): extract definitions and conservative lexical calls Co-Authored-By: OpenAI Codex --- graphify/extractors/matlab.py | 439 +++++++++++++++++++++ pyproject.toml | 1 + tests/fixtures/matlab/frontend/scopes.m | 16 + tests/fixtures/matlab/frontend/structure.m | 16 + tests/test_matlab.py | 361 +++++++++++++++++ uv.lock | 294 +++++++------- 6 files changed, 994 insertions(+), 133 deletions(-) create mode 100644 graphify/extractors/matlab.py create mode 100644 tests/fixtures/matlab/frontend/scopes.m create mode 100644 tests/fixtures/matlab/frontend/structure.m create mode 100644 tests/test_matlab.py diff --git a/graphify/extractors/matlab.py b/graphify/extractors/matlab.py new file mode 100644 index 0000000000..087f6b6472 --- /dev/null +++ b/graphify/extractors/matlab.py @@ -0,0 +1,439 @@ +"""Source-backed MATLAB definitions and conservative same-file lexical calls.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +import re +from typing import Any + +from graphify.extractors.base import ( + _file_stem, + _make_id, + _read_source_bytes, + _read_text, + active_scan_root, +) + + +_DECLARATIONS = {"function_definition", "function_signature", "class_definition"} +_WORKSPACE_CHANGES = {"eval", "evalin", "assignin", "load", "clear"} +_MATLAB_IDENTIFIER = re.compile(r"[^\W\d_]\w*", re.UNICODE) +_MATLAB_NAME = re.compile(r"[^\W\d_]\w*(?:\.[^\W\d_]\w*)*", re.UNICODE) + + +@dataclass +class _Scope: + syntax: Any + name: str + kind: str + owner: _Scope | None + record: dict | None = None + children: list[_Scope] = field(default_factory=list) + bindings: set[str] = field(default_factory=set) + unsafe: bool = False + primary: bool = False + + +def extract_matlab(path: Path) -> dict: + """Extract declarations and only statically justified same-file calls.""" + try: + import tree_sitter_matlab + from tree_sitter import Language, Parser + except ImportError: + return {"nodes": [], "edges": [], "error": "tree-sitter-matlab not installed"} + + try: + source = _read_source_bytes(path) + root = Parser(Language(tree_sitter_matlab.language())).parse(source).root_node + except Exception as exc: + return {"nodes": [], "edges": [], "error": str(exc)} + + scan_root = active_scan_root() + relative = path + if scan_root is not None: + try: + relative = path.resolve().relative_to(scan_root.resolve()) + except ValueError: + pass + stem = _file_stem(relative) + namespace_parts: list[str] = [] + for part in reversed(relative.parent.parts): + if not part.startswith("+") or not _MATLAB_IDENTIFIER.fullmatch(part[1:]): + break + namespace_parts.append(part[1:]) + namespace = ".".join(reversed(namespace_parts)) + nodes: list[dict] = [] + edges: list[dict] = [] + + def text(node) -> str: + return _read_text(node, source) if node is not None else "" + + def add_node(nid: str, name: str, syntax, kind: str, chain: list[str]) -> dict: + metadata = { + "language": "matlab", + "kind": f"matlab_{kind}", + "scope_chain": chain, + "namespace": namespace, + } + record = { + "id": nid, + "label": name, + "file_type": "code", + "source_file": str(path), + "source_location": f"L{syntax.start_point.row + 1}", + "metadata": metadata, + } + nodes.append(record) + return record + + def add_edge(source_id: str, target_id: str, relation: str, syntax) -> None: + edges.append({ + "source": source_id, + "target": target_id, + "relation": relation, + "confidence": "EXTRACTED", + "weight": 1.0, + "source_file": str(path), + "source_location": f"L{syntax.start_point.row + 1}", + }) + + file_scope = _Scope(root, "", "file", None) + file_scope.record = add_node( + _make_id(relative.as_posix()), path.name, root, "file", [] + ) + scopes = [file_scope] + + def exact_superclass_name(value: str) -> bool: + return bool(_MATLAB_NAME.fullmatch(value)) + + def superclass_facts(class_node) -> list[dict[str, str]]: + facts: list[dict[str, str]] = [] + groups = [child for child in class_node.named_children if child.type == "superclasses"] + for group in groups: + bases = [ + base for base in group.named_children + if base.type not in {"comment", "line_continuation"} + ] + if bases: + for base in bases: + spelling = text(base).strip() + parsed = ( + not group.has_error + and not base.has_error + and not base.is_missing + and base.type == "property_name" + and exact_superclass_name(spelling) + ) + facts.append({ + "name" if parsed else "raw": spelling, + "source_location": f"L{base.start_point.row + 1}", + }) + elif group.has_error: + raw = text(group).strip().lstrip("<").strip() + if raw: + facts.append({ + "raw": raw, + "source_location": f"L{group.start_point.row + 1}", + }) + if not groups: + for child in class_node.named_children: + if child.type != "ERROR" or "<" not in text(child): + continue + raw = text(child).split("<", 1)[1].strip() + if raw: + facts.append({ + "raw": raw, + "source_location": f"L{child.start_point.row + 1}", + }) + return facts + + def collect(syntax, owner: _Scope, chain: list[str], in_methods: bool = False) -> None: + for child in syntax.named_children: + if child.type in _DECLARATIONS: + name_node = child.child_by_field_name("name") + name = text(name_node).strip() + kind = "class" if child.type == "class_definition" else ( + "method" if in_methods and owner.kind == "class" else "function" + ) + scope = _Scope(child, name, kind, owner) + owner.children.append(scope) + scopes.append(scope) + + header = [ + part for part in child.named_children + if part.type not in {"block", "methods", "properties", "events", "enumeration", "superclasses"} + ] + trustworthy_name = ( + name_node is not None + and name_node.type == "identifier" + and not name_node.has_error + and not name_node.is_missing + and bool(name) + ) + trustworthy_header = kind == "class" or not any( + part.has_error or part.is_missing for part in header + ) + if not trustworthy_name or not trustworthy_header: + scope.unsafe = True + if owner.kind in {"function", "method"} and not ( + trustworthy_name and trustworthy_header + ): + owner.unsafe = True + next_chain = [*chain, name] if name else chain + if trustworthy_name and trustworthy_header: + nid = _make_id( + stem, + "matlab", + kind, + *chain, + name, + f"L{child.start_point.row + 1}", + f"C{child.start_point.column + 1}", + ) + scope.record = add_node(nid, name, child, kind, chain) + if child.type == "function_signature": + scope.record["metadata"]["matlab_declaration_only"] = True + if kind == "class": + scope.record["metadata"]["matlab_superclasses"] = superclass_facts(child) + if owner.record is not None: + add_edge( + owner.record["id"], + nid, + "method" if kind == "method" else "contains", + child, + ) + + collect(child, scope, next_chain, False) + elif syntax.type in {"source_file", "function_definition"} and child.type == "block": + collect(child, owner, chain) + elif syntax.type == "class_definition" and child.type == "methods": + collect(child, owner, chain, True) + + collect(root, file_scope, []) + excluded_callable_names = { + scope.name for scope in scopes if scope.kind in {"class", "method"} and scope.name + } + + meaningful = [ + child for child in root.named_children + if child.type not in {"comment", "line_continuation"} + ] + if meaningful: + first = meaningful[0] + while first.type == "block": + nested = next( + (child for child in first.named_children + if child.type not in {"comment", "line_continuation"}), + None, + ) + if nested is None: + break + first = nested + primary = next((scope for scope in file_scope.children if scope.syntax == first), None) + if primary is not None and primary.record is not None: + primary.primary = True + file_scope.record["metadata"].update( + matlab_file_kind=primary.kind, + matlab_primary_name=primary.name, + ) + elif first.type not in {"ERROR", *_DECLARATIONS}: + file_scope.record["metadata"]["matlab_file_kind"] = "script" + + def node_key(node) -> tuple[str, int, int]: + return node.type, node.start_byte, node.end_byte + + known_scope_keys = {node_key(scope.syntax) for scope in scopes[1:]} + + def lvalue_roots(node) -> set[str]: + if node is None: + return set() + if node.type == "identifier": + return {text(node)} + if node.type == "multioutput_variable": + return set().union(*(lvalue_roots(child) for child in node.named_children)) + if node.type == "function_call": + return lvalue_roots(node.child_by_field_name("name")) + if node.type == "field_expression": + return lvalue_roots(node.child_by_field_name("object")) + return set() + + def parameter_names(node) -> set[str]: + names = set() + stack = list(node.named_children) + while stack: + child = stack.pop() + if child.type == "identifier": + names.add(text(child)) + else: + stack.extend(child.named_children) + return names + + def is_call_site(node, scope_node) -> bool: + """A direct unqualified call expression, not an index or dynamic callee.""" + if not any(child.type == "(" for child in node.children): + return False + if any(child.type in {"{", "superclass"} for child in node.children): + return False + parent = node.parent + while parent is not None and node_key(parent) != node_key(scope_node): + if parent.type in {"lambda", "handle_operator", "arguments_statement", "field_expression"}: + return False + if parent.type == "assignment": + left = parent.child_by_field_name("left") + cursor = node + while cursor is not None and node_key(cursor) != node_key(parent): + if left is not None and node_key(cursor) == node_key(left): + return False + cursor = cursor.parent + if parent.type == "function_call": + outer_name = parent.child_by_field_name("name") + if outer_name is not None and node_key(outer_name) == node_key(node): + return False + parent = parent.parent + return True + + def inspect_scope(scope: _Scope) -> list[tuple[str, Any]]: + """Collect whole-scope bindings and uncertainty barriers.""" + calls: list[tuple[str, Any]] = [] + root_key = node_key(scope.syntax) + for child in scope.syntax.named_children: + if child.type == "function_output": + for output in child.named_children: + scope.bindings.update(lvalue_roots(output)) + elif child.type == "function_arguments": + scope.bindings.update(parameter_names(child)) + + def mark_uncertain_subtree(node) -> None: + stack = [node] + while stack: + child = stack.pop() + if child.type == "ERROR" or child.is_missing: + scope.unsafe = True + if child.type in {"command", "import_statement", "import_declaration"}: + scope.unsafe = True + if child.type == "function_call": + callee = child.child_by_field_name("name") + if ( + callee is not None + and callee.type == "identifier" + and text(callee) in _WORKSPACE_CHANGES + ): + scope.unsafe = True + if child.type not in _DECLARATIONS: + stack.extend(child.children) + + def visit(node) -> None: + if node_key(node) != root_key and node.type in _DECLARATIONS: + if node_key(node) not in known_scope_keys: + scope.unsafe = True + return + if node.type == "ERROR" or node.is_missing: + scope.unsafe = True + if node.type in {"command", "import_statement", "import_declaration"}: + scope.unsafe = True + if node.type == "arguments_statement" and node.has_error: + scope.unsafe = True + if node.type == "arguments_statement": + for prop in node.named_children: + if prop.type != "property": + continue + name_node = prop.child_by_field_name("name") + if name_node is not None and name_node.type == "identifier": + scope.bindings.add(text(name_node)) + mark_uncertain_subtree(node) + return + if node.type == "assignment": + scope.bindings.update(lvalue_roots(node.child_by_field_name("left"))) + elif node.type == "iterator": + first = next((child for child in node.named_children), None) + if first is not None and first.type == "identifier": + scope.bindings.add(text(first)) + elif node.type == "catch_clause": + identifier = next( + (child for child in node.named_children if child.type == "identifier"), + None, + ) + if identifier is not None: + scope.bindings.add(text(identifier)) + elif node.type in {"global_operator", "persistent_operator"}: + scope.bindings.update( + text(child) for child in node.named_children if child.type == "identifier" + ) + + if node.type == "lambda": + mark_uncertain_subtree(node) + return + if node.type == "handle_operator": + return + if node.type == "function_call": + name_node = node.child_by_field_name("name") + if name_node is not None and name_node.type == "identifier": + name = text(name_node) + if name in _WORKSPACE_CHANGES: + scope.unsafe = True + if name != "feval" and is_call_site(node, scope.syntax): + calls.append((name, node)) + for child in node.children: + visit(child) + + visit(scope.syntax) + return calls + + calls_by_scope: list[tuple[_Scope, list[tuple[str, Any]]]] = [] + for scope in scopes[1:]: + if scope.kind in {"function", "method"}: + calls_by_scope.append((scope, inspect_scope(scope))) + + def callable_ancestors(scope: _Scope): + current: _Scope | None = scope + while current is not None and current.kind not in {"file", "class"}: + yield current + current = current.owner + + def caller_is_unsafe(scope: _Scope) -> bool: + return any(ancestor.unsafe for ancestor in callable_ancestors(scope)) + + def lexical_tiers(scope: _Scope): + current: _Scope | None = scope + while current is not None: + yield [child for child in current.children if child.kind == "function"] + parent = current.owner + if parent is None or parent.kind in {"file", "class"}: + return + current = parent + + def file_target(name: str) -> _Scope | None: + declarations = [ + child for child in file_scope.children + if child.kind == "function" and child.name == name + ] + if len(declarations) != 1: + return None + target = declarations[0] + if target.record is None or (target.primary and target.name != path.stem): + return None + return target + + for caller, sites in calls_by_scope: + if caller.record is None or caller_is_unsafe(caller): + continue + for name, syntax in sites: + if name in excluded_callable_names: + continue + if any(name in ancestor.bindings for ancestor in callable_ancestors(caller)): + continue + target = None + found_nearest = False + for tier in lexical_tiers(caller): + declarations = [candidate for candidate in tier if candidate.name == name] + if declarations: + found_nearest = True + if len(declarations) == 1 and declarations[0].record is not None: + target = declarations[0] + break + if not found_nearest: + target = file_target(name) + if target is not None and target.record is not None: + add_edge(caller.record["id"], target.record["id"], "calls", syntax) + + return {"nodes": nodes, "edges": edges} diff --git a/pyproject.toml b/pyproject.toml index 7d2dd62ea0..0d88097e06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "tree-sitter-elixir>=0.3,<0.5", "tree-sitter-objc>=3.0,<4.0", "tree-sitter-julia>=0.23,<0.25", + "tree-sitter-matlab==1.3.1", "tree-sitter-verilog>=1.0,<2.0", "tree-sitter-fortran>=0.6,<0.8", "tree-sitter-bash>=0.23,<0.27", diff --git a/tests/fixtures/matlab/frontend/scopes.m b/tests/fixtures/matlab/frontend/scopes.m new file mode 100644 index 0000000000..d0a807ba52 --- /dev/null +++ b/tests/fixtures/matlab/frontend/scopes.m @@ -0,0 +1,16 @@ +function entry() + local(); + function local() + sibling(); + end + function sibling() + local(); + end +end + +function public() + local(); +end + +function local() +end diff --git a/tests/fixtures/matlab/frontend/structure.m b/tests/fixtures/matlab/frontend/structure.m new file mode 100644 index 0000000000..c9d1df71bc --- /dev/null +++ b/tests/fixtures/matlab/frontend/structure.m @@ -0,0 +1,16 @@ +classdef C < pkg.Base & handle + methods (Abstract) + y = declared(obj, x) + end + methods + function y = run(obj, x) + y = nested(x); + function z = nested(v) + z = v; + end + end + end +end + +function helper() +end diff --git a/tests/test_matlab.py b/tests/test_matlab.py new file mode 100644 index 0000000000..1612960e40 --- /dev/null +++ b/tests/test_matlab.py @@ -0,0 +1,361 @@ +"""Standalone MATLAB extraction contract; normal dispatch is tested separately.""" + +from pathlib import Path + +import pytest + +from graphify.extractors.base import set_active_scan_root +from graphify.extractors.matlab import extract_matlab + + +def extract_source(tmp_path, source, name="main.m"): + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source) + previous = set_active_scan_root(tmp_path) + try: + return extract_matlab(path) + finally: + set_active_scan_root(previous) + + +def extract_fixture(tmp_path, filename, name): + fixture = Path(__file__).parent / "fixtures" / "matlab" / "frontend" / filename + return extract_source(tmp_path, fixture.read_text(), name) + + +def definitions(result): + return [node for node in result["nodes"] if node["metadata"]["kind"] != "matlab_file"] + + +def call_sites(result): + labels = {node["id"]: node["label"] for node in result["nodes"]} + return [(labels[edge["source"]], labels[edge["target"]], edge["source_location"]) + for edge in result["edges"] if edge["relation"] == "calls"] + + +def test_structure_owns_methods_and_nested_functions(tmp_path): + result = extract_fixture(tmp_path, "structure.m", "+pkg/C.m") + found = {node["label"]: node for node in definitions(result)} + assert set(found) == {"C", "declared", "run", "nested", "helper"} + assert found["C"]["metadata"]["matlab_superclasses"] == [ + {"name": "pkg.Base", "source_location": "L1"}, + {"name": "handle", "source_location": "L1"}, + ] + assert found["declared"]["metadata"]["matlab_declaration_only"] is True + assert found["nested"]["metadata"]["scope_chain"] == ["C", "run"] + assert found["helper"]["metadata"]["scope_chain"] == [] + assert all(node["metadata"]["language"] == "matlab" for node in result["nodes"]) + assert result["nodes"][0]["metadata"]["matlab_file_kind"] == "class" + assert result["nodes"][0]["metadata"]["matlab_primary_name"] == "C" + assert found["C"]["metadata"]["namespace"] == "pkg" + ownership = {(edge["source"], edge["target"], edge["relation"]) + for edge in result["edges"] if edge["relation"] in {"contains", "method"}} + assert (found["C"]["id"], found["run"]["id"], "method") in ownership + assert (found["C"]["id"], found["run"]["id"], "contains") not in ownership + assert (found["run"]["id"], found["nested"]["id"], "contains") in ownership + assert not result.get("raw_calls") + + +def test_identities_include_owner_and_declaration_location(tmp_path): + source = """function main() + function Foo() + end + function foo() + end + function left() + function same() + end + end + function right() + function same() + end + end +end +""" + first = extract_source(tmp_path / "one", source) + second = extract_source(tmp_path / "two", source) + assert len({node["id"] for node in definitions(first)}) == 7 + assert [node["id"] for node in first["nodes"]] == [node["id"] for node in second["nodes"]] + + +@pytest.mark.parametrize(("name", "namespace"), [ + ("src/+tools/+io/main.m", "tools.io"), + ("+outer/plain/+inner/main.m", "inner"), + ("+outer/plain/main.m", ""), +]) +def test_namespace_is_contiguous_directory_context(tmp_path, name, namespace): + result = extract_source(tmp_path, "function main()\nend\n", name) + assert definitions(result)[0]["metadata"].get("namespace", "") == namespace + + +def test_nearest_lexical_tiers_and_recursion(tmp_path): + result = extract_source(tmp_path, """function main() + helper(); + main(); + function helper() + helper(); + sibling(); + end + function sibling() + function deeper() + helper(); + end + end +end +function helper() + helper(); +end +function independent() + sibling(); + helper(); +end +""") + assert call_sites(result) == [ + ("main", "helper", "L2"), ("main", "main", "L3"), + ("helper", "helper", "L5"), ("helper", "sibling", "L6"), + ("deeper", "helper", "L10"), ("helper", "helper", "L15"), + ("independent", "helper", "L19"), + ] + calls = [edge for edge in result["edges"] if edge["relation"] == "calls"] + assert calls[0]["target"] != calls[-1]["target"] + assert all(edge["confidence"] == "EXTRACTED" and edge["weight"] == 1.0 for edge in calls) + + +def test_duplicate_tier_vetoes_instead_of_falling_back(tmp_path): + result = extract_source(tmp_path, """function main() + helper(); + function helper() + end + function helper() + end +end +function helper() +end +""") + assert len(definitions(result)) == 4 + assert call_sites(result) == [] + + +def test_case_sensitive_lookup_and_primary_identity(tmp_path): + result = extract_source(tmp_path, """function wrong() + wrong(); + helper(); + Helper(); + HELPER(); +end +function helper() +end +function Helper() +end +""") + assert call_sites(result) == [("wrong", "helper", "L3"), ("wrong", "Helper", "L4")] + assert result["nodes"][0]["metadata"]["matlab_file_kind"] == "function" + assert result["nodes"][0]["metadata"]["matlab_primary_name"] == "wrong" + + +def test_methods_call_functions_but_not_methods_or_constructors(tmp_path): + result = extract_source(tmp_path, """classdef C + methods + function run(obj) + other(); C(); helper(); + end + function other(obj) + end + end +end +function helper() +end +function other() +end +""", "C.m") + assert call_sites(result) == [("run", "helper", "L4")] + + +def test_repeated_call_sites_remain_distinct(tmp_path): + result = extract_source(tmp_path, "function main()\n helper();\n helper();\nend\nfunction helper()\nend\n") + assert call_sites(result) == [("main", "helper", "L2"), ("main", "helper", "L3")] + + +def test_fixture_lexical_visibility_and_local_function_scope(tmp_path): + result = extract_fixture(tmp_path, "scopes.m", "scopes.m") + assert call_sites(result) == [ + ("entry", "local", "L2"), + ("local", "sibling", "L4"), + ("sibling", "local", "L7"), + ("public", "local", "L12"), + ] + assert not result.get("raw_calls") + + +@pytest.mark.parametrize(("header", "prefix", "body"), [ + ("function caller(helper)", "", ""), + ("function [helper, output] = caller()", "", ""), + ("function caller()", "arguments\n helper (1, 1) double\nend\n", ""), + ("function caller()", "", "helper = 1;"), + ("function caller()", "", "[helper, other] = deal(1, 2);"), + ("function caller()", "", "helper(1) = 1;"), + ("function caller()", "", "helper{1} = 1;"), + ("function caller()", "", "helper.field = 1;"), + ("function caller()", "", "for helper = 1:2\n end"), + ("function caller()", "", "parfor helper = 1:2\n end"), + ("function caller()", "", "try\n catch helper\n end"), + ("function caller()", "", "global helper;"), + ("function caller()", "", "persistent helper;"), + ("function caller()", "", "if true\n helper = 1;\n end"), +]) +def test_whole_scope_bindings_veto_calls(tmp_path, header, prefix, body): + source = ( + f"{header}\n{prefix} helper();\n{body}\nend\n" + "function helper()\nend\n" + ) + assert call_sites(extract_source(tmp_path, source)) == [] + + +def test_field_name_is_not_a_variable_binding(tmp_path): + result = extract_source(tmp_path, """function caller() + object.helper = 1; + helper(); +end +function helper() +end +""") + assert call_sites(result) == [("caller", "helper", "L3")] + + +def test_ancestor_bindings_apply_but_sibling_bindings_do_not_leak(tmp_path): + result = extract_source(tmp_path, """function main() + helper = 1; + function nested() + helper(); + end + function sibling() + helper(); + end +end +function assigned() + helper(); + helper = 1; +end +function independent() + helper(); +end +function clean() + helper(); +end +function helper() +end +""") + assert call_sites(result) == [ + ("independent", "helper", "L15"), + ("clean", "helper", "L18"), + ] + + +def test_script_workspace_is_not_a_calling_scope(tmp_path): + result = extract_source(tmp_path, """helper(); +helper = 1; +function local() + helper(); +end +function helper() +end +""") + assert result["nodes"][0]["metadata"]["matlab_file_kind"] == "script" + assert call_sites(result) == [("local", "helper", "L4")] + + +@pytest.mark.parametrize("barrier", [ + "import pkg.helper", + "run_script arg", + "eval('x');", + "evalin('base', 'x');", + "assignin('base', 'x', 1);", + "load('data.mat');", + "clear helper", +]) +def test_workspace_barriers_suppress_the_entire_callable_scope(tmp_path, barrier): + result = extract_source(tmp_path, f"""function caller() + helper(); + {barrier} + helper(); +end +function helper() +end +""") + assert call_sites(result) == [] + + +def test_workspace_barrier_flows_to_nested_functions_only(tmp_path): + result = extract_source(tmp_path, """function main() + eval('x'); + function child() + helper(); + end +end +function independent() + helper(); +end +function helper() +end +""") + assert call_sites(result) == [("independent", "helper", "L8")] + + +def test_indexing_qualified_dynamic_and_handle_forms_are_not_calls(tmp_path): + result = extract_source(tmp_path, """function caller() + A = [1, 2]; + A(1); + A{1}; + indexed(1) = 2; + pkg.helper(); + obj.helper(); + feval('helper'); + callback = @helper; + anonymous = @(x) helper(x); + helper(); +end +function A() +end +function indexed() +end +function helper() +end +""") + assert call_sites(result) == [("caller", "helper", "L11")] + + +def test_parser_error_is_contained_to_its_callable_scope(tmp_path): + result = extract_source(tmp_path, """function broken() + helper(); + value = ; +end +function clean() + helper(); +end +function helper() +end +""") + assert {node["label"] for node in definitions(result)} == {"broken", "clean", "helper"} + assert call_sites(result) == [("clean", "helper", "L6")] + + +def test_file_paths_keep_same_named_definitions_distinct(tmp_path): + source = "function main()\n helper();\nend\nfunction helper()\nend\n" + left = extract_source(tmp_path, source, "left/main.m") + right = extract_source(tmp_path, source, "right/main.m") + left_ids = {node["id"] for node in definitions(left)} + right_ids = {node["id"] for node in definitions(right)} + assert left_ids.isdisjoint(right_ids) + assert call_sites(left) == [("main", "helper", "L2")] + assert call_sites(right) == [("main", "helper", "L2")] + + +def test_cell_indexing_is_never_a_call_even_without_a_local_binding(tmp_path): + result = extract_source(tmp_path, """function caller() + A{1}; +end +function A() +end +""") + assert call_sites(result) == [] diff --git a/uv.lock b/uv.lock index 68481d202f..2b86356adc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,18 +1,18 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -100,7 +100,7 @@ name = "autograd" version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/1c/3c24ec03c8ba4decc742b1df5a10c52f98c84ca8797757f313e7bdcdf276/autograd-1.8.0.tar.gz", hash = "sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683", size = 2562146, upload-time = "2025-05-05T12:49:02.502Z" } wheels = [ @@ -480,7 +480,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -551,17 +551,17 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } @@ -819,10 +819,10 @@ name = "ctranslate2" version = "4.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "pyyaml", marker = "python_full_version >= '3.11'" }, - { name = "setuptools", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml" }, + { name = "setuptools" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/cb/e0/b69c40c3d739b213a78d327071240590792071b4f890e34088b03b95bb1e/ctranslate2-4.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9017a355dd7c6d29dc3bca6e9fc74827306c61b702c66bb1f6b939655e7de3fa", size = 1255773, upload-time = "2026-02-04T06:11:04.769Z" }, @@ -932,7 +932,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -957,12 +957,12 @@ name = "faster-whisper" version = "1.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "av", marker = "python_full_version >= '3.11'" }, - { name = "ctranslate2", marker = "python_full_version >= '3.11'" }, - { name = "huggingface-hub", marker = "python_full_version >= '3.11'" }, - { name = "onnxruntime", marker = "python_full_version >= '3.11'" }, - { name = "tokenizers", marker = "python_full_version >= '3.11'" }, - { name = "tqdm", marker = "python_full_version >= '3.11'" }, + { name = "av" }, + { name = "ctranslate2" }, + { name = "huggingface-hub" }, + { name = "onnxruntime" }, + { name = "tokenizers" }, + { name = "tqdm" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" }, @@ -1065,10 +1065,10 @@ name = "gensim" version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "smart-open", marker = "python_full_version < '3.13'" }, + { name = "smart-open" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/80/fe9d2e1ace968041814dbcfce4e8499a643a36c41267fa4b6c4f54cce420/gensim-4.4.0.tar.gz", hash = "sha256:a3f5b626da5518e79a479140361c663089fe7998df8ba52d56e1ded71ac5bdf5", size = 23260095, upload-time = "2025-10-18T02:06:45.962Z" } wheels = [ @@ -1096,13 +1096,14 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.9.84" +version = "0.9.95" source = { editable = "." } dependencies = [ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pyyaml" }, { name = "rapidfuzz" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tree-sitter" }, @@ -1120,6 +1121,7 @@ dependencies = [ { name = "tree-sitter-julia" }, { name = "tree-sitter-kotlin" }, { name = "tree-sitter-lua" }, + { name = "tree-sitter-matlab" }, { name = "tree-sitter-objc" }, { name = "tree-sitter-php" }, { name = "tree-sitter-powershell" }, @@ -1336,6 +1338,7 @@ requires-dist = [ { name = "pypdf", marker = "extra == 'pdf'", specifier = ">=6.16.1" }, { name = "python-docx", marker = "extra == 'all'" }, { name = "python-docx", marker = "extra == 'office'" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "rapidfuzz", specifier = ">=3.0" }, { name = "robotframework", marker = "extra == 'all'", specifier = ">=4.0" }, { name = "robotframework", marker = "extra == 'robot'", specifier = ">=4.0" }, @@ -1370,6 +1373,7 @@ requires-dist = [ { name = "tree-sitter-language-pack", marker = "extra == 'erlang'", specifier = "==0.11.0" }, { name = "tree-sitter-language-pack", marker = "extra == 'r'", specifier = "==0.11.0" }, { name = "tree-sitter-lua", specifier = ">=0.2,<0.6" }, + { name = "tree-sitter-matlab", specifier = "==1.3.1" }, { name = "tree-sitter-objc", specifier = ">=3.0,<4.0" }, { name = "tree-sitter-ocaml", marker = "extra == 'all'" }, { name = "tree-sitter-ocaml", marker = "extra == 'ocaml'" }, @@ -1425,26 +1429,26 @@ name = "graspologic" version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anytree", marker = "python_full_version < '3.13'" }, - { name = "beartype", marker = "python_full_version < '3.13'" }, - { name = "future", marker = "python_full_version < '3.13'" }, - { name = "gensim", marker = "python_full_version < '3.13'" }, - { name = "graspologic-native", version = "1.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "hyppo", marker = "python_full_version < '3.13'" }, - { name = "joblib", marker = "python_full_version < '3.13'" }, - { name = "matplotlib", marker = "python_full_version < '3.13'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "anytree" }, + { name = "beartype" }, + { name = "future" }, + { name = "gensim" }, + { name = "graspologic-native", version = "1.2.5", source = { registry = "https://pypi.org/simple" } }, + { name = "hyppo" }, + { name = "joblib" }, + { name = "matplotlib" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "pot", marker = "python_full_version < '3.13'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "pot" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "seaborn", marker = "python_full_version < '3.13'" }, - { name = "statsmodels", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, - { name = "umap-learn", marker = "python_full_version < '3.13'" }, + { name = "seaborn" }, + { name = "statsmodels" }, + { name = "typing-extensions" }, + { name = "umap-learn" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/bb/0fe2ef85ea775e7b8416b2cf90097aa4b5e0c9c2271d7fe6789bab27d0ca/graspologic-3.4.4.tar.gz", hash = "sha256:79878caf367da3e89046a4ec94291c5b1a5da569f19fdd879d8b45c3563d7110", size = 5134258, upload-time = "2025-09-08T21:44:01.969Z" } wheels = [ @@ -1457,10 +1461,10 @@ version = "1.2.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -1485,8 +1489,8 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/25/607bcadd51954aa98892e44d1536560abb06a81ff48288982bd8303983ee/graspologic_native-1.3.1.tar.gz", hash = "sha256:fa6be9c292508b8a876d45ac98ee550534d9e33fcf7129a6df8a95573101be4c", size = 2655991, upload-time = "2026-06-14T01:01:51.23Z" } wheels = [ @@ -1582,15 +1586,15 @@ name = "huggingface-hub" version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock", marker = "python_full_version >= '3.11'" }, - { name = "fsspec", marker = "python_full_version >= '3.11'" }, - { name = "hf-xet", marker = "(python_full_version >= '3.11' and platform_machine == 'AMD64') or (python_full_version >= '3.11' and platform_machine == 'aarch64') or (python_full_version >= '3.11' and platform_machine == 'amd64') or (python_full_version >= '3.11' and platform_machine == 'arm64') or (python_full_version >= '3.11' and platform_machine == 'x86_64')" }, - { name = "httpx", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pyyaml", marker = "python_full_version >= '3.11'" }, - { name = "tqdm", marker = "python_full_version >= '3.11'" }, - { name = "typer", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } wheels = [ @@ -1615,18 +1619,18 @@ name = "hyppo" version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "autograd", marker = "python_full_version < '3.13'" }, - { name = "future", marker = "python_full_version < '3.13'" }, - { name = "numba", marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "autograd" }, + { name = "future" }, + { name = "numba" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "patsy", marker = "python_full_version < '3.13'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "patsy" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "statsmodels", marker = "python_full_version < '3.13'" }, + { name = "statsmodels" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/a6/0d84fe8486a1447da8bdb8ebb249d525fd8c1d0fe038bceb003c6e0513f9/hyppo-0.5.2.tar.gz", hash = "sha256:4634d15516248a43d25c241ed18beeb79bb3210360f7253693b3f154fe8c9879", size = 125115, upload-time = "2025-05-24T18:33:27.418Z" } wheels = [ @@ -1656,7 +1660,7 @@ name = "importlib-metadata" version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.11'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ @@ -2361,13 +2365,13 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } @@ -2395,8 +2399,8 @@ name = "numba" version = "0.65.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "llvmlite", marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "llvmlite" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } wheels = [ @@ -2432,10 +2436,10 @@ version = "1.26.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -2559,11 +2563,11 @@ name = "onnxruntime" version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "flatbuffers", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "flatbuffers" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "protobuf", marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "protobuf" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/d4/81/29a9eb470994a75eb7b3ccf32be314d7c66675a00ac7b50294816cc2db27/onnxruntime-1.26.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ee1109ef4ef27cad90e823399e61e03b3c6c7bfe0fb820b4baf3678c15be8b3c", size = 18005108, upload-time = "2026-05-08T19:08:11.728Z" }, @@ -2649,10 +2653,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -2711,16 +2715,16 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } wheels = [ @@ -2794,7 +2798,7 @@ name = "patsy" version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } wheels = [ @@ -2973,8 +2977,8 @@ name = "pot" version = "0.9.6.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/8b/5f939eaf1fbeb7ff914fe540d659486951a056e5537b8f454362045b6c72/pot-0.9.6.post1.tar.gz", hash = "sha256:9b6cc14a8daecfe1268268168cf46548f9130976b22b24a9e8ec62a734be6c43", size = 604243, upload-time = "2025-09-22T12:51:14.894Z" } @@ -3317,12 +3321,12 @@ name = "pynndescent" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "joblib", marker = "python_full_version < '3.13'" }, - { name = "llvmlite", marker = "python_full_version < '3.13'" }, - { name = "numba", marker = "python_full_version < '3.13'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "joblib" }, + { name = "llvmlite" }, + { name = "numba" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } @@ -3997,10 +4001,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -4042,17 +4046,17 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "joblib" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } wheels = [ @@ -4102,7 +4106,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -4162,17 +4166,17 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } @@ -4244,9 +4248,9 @@ name = "seaborn" version = "0.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib", marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } @@ -4286,7 +4290,7 @@ name = "smart-open" version = "7.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wrapt", marker = "python_full_version < '3.13'" }, + { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/65/3ada667d32675399001bf022ad3d9f3989b57101351ebc71d6fbe2384634/smart_open-7.6.1.tar.gz", hash = "sha256:4347996e7ba21db7cd1e059632e0b30395407e4f6c660d2ddffc8f2a9ae5f990", size = 54754, upload-time = "2026-05-09T06:23:37.06Z" } wheels = [ @@ -4351,12 +4355,12 @@ name = "statsmodels" version = "0.14.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "packaging", marker = "python_full_version < '3.13'" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "patsy", marker = "python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "patsy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } @@ -4477,7 +4481,7 @@ name = "tokenizers" version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "huggingface-hub", marker = "python_full_version >= '3.11'" }, + { name = "huggingface-hub" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } wheels = [ @@ -4908,6 +4912,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/97/3104ecfa3c34320411bcad9b4f2823956487b6e222edcc83689819badc9d/tree_sitter_lua-0.5.0-cp310-abi3-win_arm64.whl", hash = "sha256:8488f3bea40779896f5771bcfcdc26900eb21e94f6658eb68a848fc37dd39221", size = 23506, upload-time = "2026-02-26T17:07:32.775Z" }, ] +[[package]] +name = "tree-sitter-matlab" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/04/be954b2edb5b57648d91d4412e225e361e9c37ac22ce6c45d78713a9b744/tree_sitter_matlab-1.3.1.tar.gz", hash = "sha256:91a2ab61a6179b8aec0216f18f1c1eb4e1409b768641b9c661bab434648bb54d", size = 172140, upload-time = "2026-08-26T06:47:41.047Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ec/d7f27aa3dd0aed3a9bcdfd3f765d28ee4fba1b2473ca8d3e4c3f39eff78e/tree_sitter_matlab-1.3.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:dd080613d88e184c389a8026ff176c3b5a30cd164000ae0f121d5086794afa89", size = 86528, upload-time = "2026-08-26T06:47:19.22Z" }, + { url = "https://files.pythonhosted.org/packages/05/ce/5e8f5fb357426ec96762a3f063e053a8fefcfde7981f1f28016068f7f2e4/tree_sitter_matlab-1.3.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78f79557f6be5e98d3ba4aa76f655efaab1d5c21dea4127ccd7fe25e20415cf4", size = 116326, upload-time = "2026-08-26T06:47:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f6/0b44ab0452eec5a3d21c33009e58a3447e93a16545d1fa6f2ab491f6c163/tree_sitter_matlab-1.3.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52ada1a896da00cc6e3c918e5548e0b22dd5dcc3cf2ecf061135af45de13d74a", size = 123180, upload-time = "2026-08-26T06:47:22.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/8a/35d394daf205c758a9a0cb375638e5945c290b4200f7b80479d27479acc6/tree_sitter_matlab-1.3.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:14e4386c054507d55d0f5d7d88e53b990967a0ef29146fcc77af0ed5995b49e4", size = 120807, upload-time = "2026-08-26T06:47:23.46Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ea/bccd684fc87b7d4ae6b5375405c97a71518c3d1ab401cc7f29ca0b8d8cd6/tree_sitter_matlab-1.3.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6a789cf1ed450965f28c476a305c407ff2f0692a336d72fd8ea86a9221b6701f", size = 115012, upload-time = "2026-08-26T06:47:24.79Z" }, + { url = "https://files.pythonhosted.org/packages/77/6b/e0fc1b2ecd8e33c11ef82dacaa8e4f48c768bff29b987dce16206166b726/tree_sitter_matlab-1.3.1-cp310-abi3-win32.whl", hash = "sha256:bf2f24efcceb670828d4a8d82c5a551bd77e4752b0f2913d72145fc2b5380de9", size = 81100, upload-time = "2026-08-26T06:47:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/52/5b/f3151a2c407c0776a8c6d22d06bc727c813d755e3dc7cda3819f7f8ae2e9/tree_sitter_matlab-1.3.1-cp310-abi3-win_amd64.whl", hash = "sha256:505d5efdcbd1d76b7134eb7e79727265135c92f2cadbe119930604b5c65fdb46", size = 81914, upload-time = "2026-08-26T06:47:27.409Z" }, + { url = "https://files.pythonhosted.org/packages/b3/94/55f785a562baf807fddffe4d0208138fb73351eee2a20fd0dc3f65a795bc/tree_sitter_matlab-1.3.1-cp310-abi3-win_arm64.whl", hash = "sha256:325b4af0332ed2e13158d866245974f9f61ad817be159d5245692abaff3bb34e", size = 80980, upload-time = "2026-08-26T06:47:28.728Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/330d3500e0323d56367375fb07b41182ab86fc1f45be0f086250fca23b59/tree_sitter_matlab-1.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a4c06388d5334ef98d2f7cb40f93d578f3eb66cd8316a5305a59757c256d9060", size = 86587, upload-time = "2026-08-26T06:47:30.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b4/23fccc18ca1c8640476795223e49fe95cfe557f07bde10fd4b82e3dc043a/tree_sitter_matlab-1.3.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5bd17ef2ad965867d70af8e6be245e6886e5e8d115f66a2aa155380884a65252", size = 119143, upload-time = "2026-08-26T06:47:31.565Z" }, + { url = "https://files.pythonhosted.org/packages/1b/43/2d5eb367db854e41417c2dfc1ca549e00f0d6de14dbbcd2221b572831136/tree_sitter_matlab-1.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bde730be2a387afc3308ae2aa720ba8d11f91c5c8183acadf149bc427c63bcf8", size = 125836, upload-time = "2026-08-26T06:47:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6a/e7def13d2f86ba6ae735dd3ea76d62ebc726d8d7dd626e3db1691fb322fb/tree_sitter_matlab-1.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:955acf4ad3e7b9a4ec84e14dd8d6c42596a45d36d5f6d1a8fd9fdc68b2970dc3", size = 123558, upload-time = "2026-08-26T06:47:34.073Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/69bde9098b0fb3b605e975b4cc9144202fe45a8617e704bf9bff45b17e87/tree_sitter_matlab-1.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3173e14a34e82f13304cd4ae066aad7250bc73b0c855e2be96e889673b8e1f5c", size = 117824, upload-time = "2026-08-26T06:47:35.384Z" }, + { url = "https://files.pythonhosted.org/packages/91/df/24297ff094dbf66e4aebd53a877302b793412748c291481c409c616bdd44/tree_sitter_matlab-1.3.1-cp314-cp314t-win32.whl", hash = "sha256:5afe8ee1ecf3314bb7e111b34ae70bd53289b417059866c145c6e11193a594c3", size = 83702, upload-time = "2026-08-26T06:47:36.8Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/c02832e71e806bff22d872d168f454d95e38da97d9516f284f662c7f6e27/tree_sitter_matlab-1.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:b678a165ea792dc3a731580d7edeb838d29c5c95b475fe8bc840752afdc2e9b8", size = 84173, upload-time = "2026-08-26T06:47:38.156Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/71d6470bbe5433c1320b0060b66a30e4b100eb64f4c034bed97790966283/tree_sitter_matlab-1.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d13490be96978603f03d9decbb3e2216108b97b3ab434a7709c00278a5afacfd", size = 83503, upload-time = "2026-08-26T06:47:39.579Z" }, +] + [[package]] name = "tree-sitter-objc" version = "3.0.2" @@ -5175,10 +5203,10 @@ name = "typer" version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc", marker = "python_full_version >= '3.11'" }, - { name = "click", marker = "python_full_version >= '3.11'" }, - { name = "rich", marker = "python_full_version >= '3.11'" }, - { name = "shellingham", marker = "python_full_version >= '3.11'" }, + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } wheels = [ @@ -5220,14 +5248,14 @@ name = "umap-learn" version = "0.5.12" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numba", marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "pynndescent", marker = "python_full_version < '3.13'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numba" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "pynndescent" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "tqdm", marker = "python_full_version < '3.13'" }, + { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/ee/af4171241117f85c74b5ca6448ea1033cc28d599c13651d67289bacd4083/umap_learn-0.5.12.tar.gz", hash = "sha256:6aff02ecac5f2aad9f3c65ee518d7ae93e1a985ae38721fdcffceee4232c33c7", size = 96672, upload-time = "2026-04-08T20:03:54.012Z" } wheels = [ @@ -5248,8 +5276,8 @@ name = "uvicorn" version = "0.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, - { name = "h11", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "click" }, + { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } From 7cf4b89f807821be9a4e7cf6047554e283f77c11 Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:35:11 +0100 Subject: [PATCH 07/11] feat(matlab): extract inherited MATLAB symbols Use normal extractor dispatch and preserve exact-source inheritance lifecycle on schema 26. Add extraction, language, inheritance, merge isolation, incremental, integration, and watch coverage. Refs JulioLeonPhD/graphify#10 Co-Authored-By: OpenAI Codex --- graphify/build.py | 59 ++++- graphify/cache.py | 3 +- graphify/cli.py | 4 + graphify/extract.py | 45 +++- graphify/extractors/__init__.py | 2 + graphify/extractors/resolution.py | 4 +- graphify/watch.py | 5 + tests/test_extract.py | 25 +- tests/test_languages.py | 39 +++ tests/test_matlab_incremental.py | 362 +++++++++++++++++++++++++++ tests/test_matlab_inheritance.py | 14 +- tests/test_matlab_integration.py | 122 +++++++++ tests/test_matlab_merge_isolation.py | 51 +++- tests/test_matlab_watch.py | 128 ++++++++++ 14 files changed, 831 insertions(+), 32 deletions(-) create mode 100644 tests/test_matlab_incremental.py create mode 100644 tests/test_matlab_integration.py create mode 100644 tests/test_matlab_watch.py diff --git a/graphify/build.py b/graphify/build.py index 11371d4c69..d45008dd03 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -990,6 +990,45 @@ def dedupe_edges(edges: list[dict]) -> list[dict]: return out +def _refresh_matlab_inheritance(nodes: list[dict], edges: list[dict]) -> list[dict]: + """Replace only source-owned MATLAB inheritance facts for the current nodes.""" + from graphify.extractors.resolution import _matlab_inheritance_edges + + matlab_sources: dict[str, str] = {} + for node in 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 + try: + source_file = matlab_sources.get(edge.get("source")) + except TypeError: + return False + return ( + source_file is not None + and edge.get("source_file") == source_file + and _is_ast_tier(edge) + ) + + kept = [edge for edge in edges if not _is_matlab_inheritance(edge)] + refreshed = _matlab_inheritance_edges(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``. @@ -2350,6 +2389,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", []) + ) return new @@ -2639,7 +2681,22 @@ 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 = dedupe_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", []) + ] + all_chunks[0]["edges"] = _refresh_matlab_inheritance( + resolver_nodes, merged_edges + ) + for chunk in all_chunks[1:]: + chunk["edges"] = [] G = build( all_chunks, directed=directed, diff --git a/graphify/cache.py b/graphify/cache.py index 78634830cd..fdb678eb51 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -38,7 +38,8 @@ _EXTRACTOR_VERSION = "unknown" # Bump when AST cache-key semantics change independently of the package version. -_AST_CACHE_SCHEMA = 25 # Python/Go/C++/Rust explicit type aliases (and Rust `union` items) are extracted as type nodes so references resolve instead of minting sourceless stubs (#4578/#4579/#4580/#4581); each C# method overload gets its own node bound by arity (#4584); a package.json dependency's manifest entry links to its ref node (#4569), so cached extractions re-run; prior (24): Objective-C `NS_CLOSED_ENUM` declarations are extracted as enums like `NS_ENUM` (#4705), and a `CREATE TABLE` whose column default the SQL grammar rejects (MySQL `DEFAULT current_timestamp()`) is recovered from its ERROR node (#4613), so cached ObjC and SQL extractions re-run; prior (23): `.yaml`/`.yml` files are structurally extracted (keys as nodes, config-vs-data gated) and reclassified from document to code (#2214), so cached YAML re-extracts; prior (22): SQL labels drop T-SQL brackets and routine bodies emit `reads_from`/`writes_to` (#4635), and a `GO` batch separator ends a statement (#4651), so cached `.sql` extractions re-run; prior (21): a CommonJS `const X = require('./m')` binding no longer mints a module-scope node that shadows the class the required file exports, so `new X()` resolves to the required declaration (#4621); prior (20): Obsidian `[[wikilink]]` targets that start with the scan root's own folder names now resolve to the real page (#4559), so cached markdown extractions re-run; prior (19): Dart `typedef` declarations are extracted as aliases (not variables of type `typedef`); C++ Metal/CUDA GPU qualifiers no longer mint sourceless god nodes (#4550); a `.h` header containing `extern "C"` routes to the C++ extractor; prior (18): Kotlin `typealias` (#4553), C# `delegate` (#4552), and Swift `macro` declarations (#4551) are now extracted so references stop minting sourceless stubs; Rust `use` rooted at a `mod` declared in the same scope carries `_rust_use_name` (#4564); prior (17): Python pytest-fixture markers (`_py_fixture`/`_python_fixture_param`, #4332) and module-level class-id collision salting (#2810); TS `this.field` typed from any method incl. static (#4473); Solidity callables carry `metadata.arity` + inherited-call/`super`/modifier raw calls (#4379); Go type-decl and Rust file-item case-distinct node ids (#4444); prior (16): Rust `use` edges carry the imported name as written (`_rust_use_name`, #4439); (15): Go generic receiver types normalized (`Box[T]` -> `Box`) so methods attach to the type node; prior (14): C# receiver resolution for nameless/`new X()`/`foreach` receivers (defer + receiver-chain markers) and `_`/case-distinct node ids (underscore-salted); prior (13): Kotlin/Scala/C++/Swift generic & template type params no longer mint sourceless stubs; Objective-C NS_ENUM/NS_OPTIONS extraction and `_callable`/`_callable_class` markers; Go and PHP receiver-typed member calls park cross-repo (`go_type_table`/`php_type_table`, receiver fields); prior (12): Java inherited-supertype (`receiver_unbound`) and chained/new/cast receiver typing (`java_receiver_chain`, `_java_ret`); C/C++ quoted cross-directory include provenance (`include_kind`); JS external imports anchored to the package, not the path leaf; prior (11): Python property-accessor decorators / `Self`, Swift `Self`, Go generic type params, Python forward type-hint file node, PowerShell dot-source, C# verbatim-interpolated string recovery; (10): C# partial types carry `type_path`; (9): Python property-read raw calls and `_py_property` nodes; (8): Rust `use`-group per-binding edges; (7): Python opaque-base/super(args) markers + nested-class enclosing ids. +# MATLAB .m dispatch and persisted provenance invalidate pre-MATLAB AST results. +_AST_CACHE_SCHEMA = 26 # Python/Go/C++/Rust explicit type aliases (and Rust `union` items) are extracted as type nodes so references resolve instead of minting sourceless stubs (#4578/#4579/#4580/#4581); each C# method overload gets its own node bound by arity (#4584); a package.json dependency's manifest entry links to its ref node (#4569), so cached extractions re-run; prior (24): Objective-C `NS_CLOSED_ENUM` declarations are extracted as enums like `NS_ENUM` (#4705), and a `CREATE TABLE` whose column default the SQL grammar rejects (MySQL `DEFAULT current_timestamp()`) is recovered from its ERROR node (#4613), so cached ObjC and SQL extractions re-run; prior (23): `.yaml`/`.yml` files are structurally extracted (keys as nodes, config-vs-data gated) and reclassified from document to code (#2214), so cached YAML re-extracts; prior (22): SQL labels drop T-SQL brackets and routine bodies emit `reads_from`/`writes_to` (#4635), and a `GO` batch separator ends a statement (#4651), so cached `.sql` extractions re-run; prior (21): a CommonJS `const X = require('./m')` binding no longer mints a module-scope node that shadows the class the required file exports, so `new X()` resolves to the required declaration (#4621); prior (20): Obsidian `[[wikilink]]` targets that start with the scan root's own folder names now resolve to the real page (#4559), so cached markdown extractions re-run; prior (19): Dart `typedef` declarations are extracted as aliases (not variables of type `typedef`); C++ Metal/CUDA GPU qualifiers no longer mint sourceless god nodes (#4550); a `.h` header containing `extern "C"` routes to the C++ extractor; prior (18): Kotlin `typealias` (#4553), C# `delegate` (#4552), and Swift `macro` declarations (#4551) are now extracted so references stop minting sourceless stubs; Rust `use` rooted at a `mod` declared in the same scope carries `_rust_use_name` (#4564); prior (17): Python pytest-fixture markers (`_py_fixture`/`_python_fixture_param`, #4332) and module-level class-id collision salting (#2810); TS `this.field` typed from any method incl. static (#4473); Solidity callables carry `metadata.arity` + inherited-call/`super`/modifier raw calls (#4379); Go type-decl and Rust file-item case-distinct node ids (#4444); prior (16): Rust `use` edges carry the imported name as written (`_rust_use_name`, #4439); (15): Go generic receiver types normalized (`Box[T]` -> `Box`) so methods attach to the type node; prior (14): C# receiver resolution for nameless/`new X()`/`foreach` receivers (defer + receiver-chain markers) and `_`/case-distinct node ids (underscore-salted); prior (13): Kotlin/Scala/C++/Swift generic & template type params no longer mint sourceless stubs; Objective-C NS_ENUM/NS_OPTIONS extraction and `_callable`/`_callable_class` markers; Go and PHP receiver-typed member calls park cross-repo (`go_type_table`/`php_type_table`, receiver fields); prior (12): Java inherited-supertype (`receiver_unbound`) and chained/new/cast receiver typing (`java_receiver_chain`, `_java_ret`); C/C++ quoted cross-directory include provenance (`include_kind`); JS external imports anchored to the package, not the path leaf; prior (11): Python property-accessor decorators / `Self`, Swift `Self`, Go generic type params, Python forward type-hint file node, PowerShell dot-source, C# verbatim-interpolated string recovery; (10): C# partial types carry `type_path`; (9): Python property-read raw calls and `_py_property` nodes; (8): Rust `use`-group per-binding edges; (7): Python opaque-base/super(args) markers + nested-class enclosing ids. # Per-process memo: distributions() scans site-packages metadata, which is diff --git a/graphify/cli.py b/graphify/cli.py index 6d65b16038..b5d5843a3e 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -856,6 +856,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 diff --git a/graphify/extract.py b/graphify/extract.py index 0381980eb7..f90f34b65f 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -69,6 +69,7 @@ from graphify.extractors.yaml_config import extract_yaml # noqa: F401 from graphify.extractors.commonlisp import extract_commonlisp # noqa: F401 from graphify.extractors.markdown import extract_markdown, _MD_LINK_INDEX_CACHE # noqa: F401 +from graphify.extractors.matlab import extract_matlab # noqa: F401 from graphify.extractors.ocaml import extract_ocaml # noqa: F401 from graphify.extractors.pascal_forms import extract_delphi_form, extract_lazarus_form # noqa: F401 from graphify.extractors.powershell import extract_powershell, extract_powershell_manifest # noqa: F401 @@ -133,6 +134,7 @@ _js_top_level_function_bodies, _load_tsconfig_aliases, _load_tsconfig_base_url, + _matlab_inheritance_edges, _load_workspace_packages, _match_tsconfig_alias, _merge_decl_def_classes, @@ -9008,13 +9010,11 @@ def _get_extractor(path: Path) -> Any | None: # grammar has no class_specifier). Reroute to extract_cpp (#1547). if _is_cpp_header(path): return extract_cpp - # `.m` is Objective-C OR MATLAB. extract_objc unconditionally would force-parse - # MATLAB through the ObjC grammar into garbage (#1702). Route to extract_objc - # only when the file actually looks like Objective-C; otherwise leave it without - # an extractor (surfaced by the no-AST-extractor warning, #1689) rather than - # mis-parsed. `.mm` is unambiguously Objective-C++ and stays on extract_objc. + # `.m` is Objective-C OR MATLAB. Explicit remaps above retain priority, and + # Objective-C `.m` files keep their current extractor; all other `.m` files + # use the MATLAB frontend. `.mm` remains unambiguously Objective-C++. if suffix == ".m" and not _is_objc_source(path): - return None + return extract_matlab # Extensionless files: resolve by shebang, mirroring detect.classify_file. # Without this, detect labels e.g. `#!/usr/bin/env bash` CLIs as code but # extraction returns no extractor and the file silently contributes nothing. @@ -11223,6 +11223,39 @@ def _canon(nid: str) -> str: if _df and "\\" in str(_df): _item["definition_file"] = PurePath(_df).as_posix() + # Resolve MATLAB inheritance only after IDs and source paths are canonical. + # Unchanged context contributes class/file candidates, never output edges. + _matlab_kinds = {"matlab_file", "matlab_class"} + _fresh_matlab_nodes = [ + n for n in all_nodes + if isinstance(n.get("metadata"), dict) + and n["metadata"].get("language") == "matlab" + and n["metadata"].get("kind") in _matlab_kinds + ] + _fresh_matlab_class_ids = { + n["id"] for n in _fresh_matlab_nodes + if n["metadata"].get("kind") == "matlab_class" + and isinstance(n.get("id"), str) + } + if _fresh_matlab_class_ids: + _fresh_matlab_ids = {n.get("id") for n in _fresh_matlab_nodes} + _matlab_context_nodes = [ + n for n in resolution_context_nodes or [] + if isinstance(n.get("metadata"), dict) + and n["metadata"].get("language") == "matlab" + and n["metadata"].get("kind") in _matlab_kinds + and n.get("id") not in _fresh_matlab_ids + ] + _matlab_edges = [ + edge for edge in _matlab_inheritance_edges( + _fresh_matlab_nodes + _matlab_context_nodes + ) + if edge["source"] in _fresh_matlab_class_ids + ] + for edge in _matlab_edges: + edge["_origin"] = "ast" + all_edges.extend(_matlab_edges) + return { "nodes": all_nodes, "edges": all_edges, diff --git a/graphify/extractors/__init__.py b/graphify/extractors/__init__.py index e2c8afc627..1ffd76cd6f 100644 --- a/graphify/extractors/__init__.py +++ b/graphify/extractors/__init__.py @@ -23,6 +23,7 @@ from graphify.extractors.go import extract_go from graphify.extractors.json_config import extract_json from graphify.extractors.julia import extract_julia +from graphify.extractors.matlab import extract_matlab from graphify.extractors.markdown import extract_markdown from graphify.extractors.objc import extract_objc from graphify.extractors.pascal import extract_pascal @@ -57,6 +58,7 @@ "go": extract_go, "json": extract_json, "julia": extract_julia, + "matlab": extract_matlab, "lazarus_form": extract_lazarus_form, "markdown": extract_markdown, "objc": extract_objc, diff --git a/graphify/extractors/resolution.py b/graphify/extractors/resolution.py index 0d08742f35..6bf3d8c690 100644 --- a/graphify/extractors/resolution.py +++ b/graphify/extractors/resolution.py @@ -1251,8 +1251,8 @@ def _matlab_inheritance_edges(nodes: list[dict]) -> list[dict]: file_node, class_node = file_nodes[0], class_nodes[0] file_metadata, class_metadata = file_node["metadata"], class_node["metadata"] if ( - file_node.get("type") != "file" - or class_node.get("type") != "class" + file_metadata.get("kind") != "matlab_file" + or class_metadata.get("kind") != "matlab_class" or file_node.get("file_type") != "code" or class_node.get("file_type") != "code" or file_node.get("external") diff --git a/graphify/watch.py b/graphify/watch.py index 4ee6fc1db2..4f39add0cf 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -2202,6 +2202,11 @@ def _failed(f: str) -> bool: # graph". print(f"error: {exc}", file=sys.stderr) return False + # Refresh after reconciliation so unchanged MATLAB classes are in scope. + from graphify.build import _refresh_matlab_inheritance + result["edges"] = _refresh_matlab_inheritance( + result.get("nodes", []), result.get("edges", []) + ) # The equality skips below compare parsed data, not layout; an existing # multi-line graph.json must still be rewritten compact (#3413). diff --git a/tests/test_extract.py b/tests/test_extract.py index fdc783e24c..9e7cbc4ace 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -5662,10 +5662,8 @@ def test_extract_progress_final_line_uses_consistent_denominator(tmp_path, capsy def test_get_extractor_routes_matlab_m_away_from_objc(tmp_path): - # #1702: .m is shared by Objective-C and MATLAB. A real ObjC .m still routes to - # extract_objc, but a MATLAB .m must NOT be force-parsed by the ObjC grammar - # (which produces garbage) — it gets no extractor instead. - from graphify.extract import _get_extractor, extract_objc + # `.m` stays Objective-C when the detector matches, with MATLAB as its fallback. + from graphify.extract import _get_extractor, extract_matlab, extract_objc objc = tmp_path / "Foo.m" objc.write_text('#import "Foo.h"\n@implementation Foo\n- (void)bar {}\n@end\n') @@ -5676,10 +5674,10 @@ def test_get_extractor_routes_matlab_m_away_from_objc(tmp_path): mm = tmp_path / "x.mm" mm.write_text("#import \n@implementation X\n@end\n") - assert _get_extractor(objc) is extract_objc # real ObjC .m -> objc - assert _get_extractor(matlab_fn) is None # MATLAB function -> no garbage - assert _get_extractor(matlab_cls) is None # MATLAB classdef -> no garbage - assert _get_extractor(mm) is extract_objc # .mm is unambiguously ObjC++ + assert _get_extractor(objc) is extract_objc + assert _get_extractor(matlab_fn) is extract_matlab + assert _get_extractor(matlab_cls) is extract_matlab + assert _get_extractor(mm) is extract_objc def test_markdown_dispatch_matches_resolution_suffixes(): @@ -5693,14 +5691,15 @@ def test_markdown_dispatch_matches_resolution_suffixes(): assert dispatched == MARKDOWN_MENTION_SUFFIXES -def test_matlab_m_not_extracted_as_garbage(tmp_path, capsys): - # End to end: a MATLAB .m produces no (garbage) nodes and is surfaced by the - # no-AST-extractor warning (#1702 + #1689), rather than mis-parsed as ObjC. +def test_matlab_m_is_extracted_as_matlab(tmp_path): m = tmp_path / "controller.m" m.write_text("function u = controller(x)\n u = -x;\nend\n") result = extract([m], cache_root=tmp_path) - assert result["nodes"] == [] # no garbage ObjC nodes - assert "no AST extractor" in capsys.readouterr().err # surfaced, not silent + assert any( + n.get("label") == "controller" + and n.get("metadata", {}).get("language") == "matlab" + for n in result["nodes"] + ) def test_rewire_binds_cross_module_function_reference_to_definition(): diff --git a/tests/test_languages.py b/tests/test_languages.py index f1d981995e..da7b5cd857 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -1971,6 +1971,45 @@ def test_objc_splits_inherits_and_implements(): assert ("Dog", "Animal") in _edge_labels(r, "inherits") assert ("Animal", "SampleDelegate") in _edge_labels(r, "implements") +def test_matlab_and_native_same_name_types_remain_distinct(tmp_path): + from graphify.extract import extract + + (tmp_path / "Foo.h").write_text("@interface Foo : NSObject\n@end\n") + (tmp_path / "Foo.m").write_text("classdef Foo < Base\nend\n") + (tmp_path / "Base.m").write_text("classdef Base\nend\n") + (tmp_path / "Native.m").write_text( + "@interface NativeBase : NSObject\n@end\n" + "@interface NativeChild : NativeBase\n@end\n" + ) + (tmp_path / "NativeCpp.mm").write_text( + "@interface NativeCpp : NSObject\n@end\n" + "@implementation NativeCpp\n@end\n" + ) + + result = extract(sorted(tmp_path.glob("*")), root=tmp_path, cache_root=tmp_path, parallel=False) + by_id = {node["id"]: node for node in result["nodes"]} + foo_nodes = [node for node in result["nodes"] if node.get("label") == "Foo"] + assert {node.get("source_file") for node in foo_nodes} == {"Foo.h", "Foo.m"} + assert len({node["id"] for node in foo_nodes}) == 2 + matlab_foo = next(node for node in foo_nodes if node.get("source_file") == "Foo.m") + objc_foo = next(node for node in foo_nodes if node.get("source_file") == "Foo.h") + assert matlab_foo["metadata"]["language"] == "matlab" + + inherits = {(edge["source"], edge["target"]) for edge in result["edges"] + if edge.get("relation") == "inherits"} + base = next(node for node in result["nodes"] if node.get("label") == "Base") + native_base = next(node for node in result["nodes"] if node.get("label") == "NativeBase") + native_child = next(node for node in result["nodes"] if node.get("label") == "NativeChild") + native_cpp = next(node for node in result["nodes"] if node.get("label") == "NativeCpp") + assert (matlab_foo["id"], base["id"]) in inherits + assert base["metadata"]["language"] == "matlab" + assert (objc_foo["id"], base["id"]) not in inherits + assert (native_child["id"], native_base["id"]) in inherits + assert not any(source == matlab_foo["id"] and target == objc_foo["id"] + for source, target in inherits) + assert native_cpp.get("source_file") == "NativeCpp.mm" + assert by_id[native_cpp["id"]]["label"] == "NativeCpp" + def test_objc_protocol_adopts_protocol(): """`@protocol Derived ` must emit an implements edge Derived->Base. diff --git a/tests/test_matlab_incremental.py b/tests/test_matlab_incremental.py new file mode 100644 index 0000000000..536dca4108 --- /dev/null +++ b/tests/test_matlab_incremental.py @@ -0,0 +1,362 @@ +"""MATLAB inheritance refresh across raw and clustered incremental merges.""" +from __future__ import annotations + +import copy +import json +from pathlib import Path + +import pytest + +from graphify.build import build, build_merge, merge_raw_extraction +from graphify.export import to_json +from graphify.extract import extract + + +MERGE_PATHS = ("raw", "clustered") + + +def _class_source(name: str, superclass: str | None = None, revision: int | None = None) -> str: + parent = f" < {superclass}" if superclass else "" + source = f"classdef {name}{parent}\n" + if revision is not None: + source += ( + " methods\n" + " function value = version(obj)\n" + f" value = {revision};\n" + " end\n" + " end\n" + ) + return source + "end\n" + + +def _write(root: Path, relative: str, source: str) -> Path: + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source, encoding="utf-8") + return path + + +def _extract(root: Path, paths: list[Path], **kwargs) -> dict: + return extract(paths, root=root, cache_root=root.parent / "cache", parallel=False, **kwargs) + + +def _read_graph(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _edges(graph: dict) -> list[dict]: + return graph.get("links", graph.get("edges", [])) + + +def _write_raw(path: Path, graph: dict) -> None: + path.write_text( + json.dumps({key: graph[key] for key in ("nodes", "edges", "hyperedges") if key in graph}), + encoding="utf-8", + ) + + +def _start_graph(mode: str, root: Path, path: Path, files: list[Path]) -> None: + extracted = _extract(root, files) + if mode == "raw": + _write_raw(path, extracted) + else: + graph = build([extracted], dedup=False, root=root) + assert to_json(graph, {}, str(path), force=True) + + +def _merge( + mode: str, + root: Path, + path: Path, + changed: list[Path] | None = None, + deleted: list[Path] | None = None, +) -> dict: + changed_paths = changed or [] + fresh = _extract(root, changed_paths) if changed_paths else {"nodes": [], "edges": []} + prune = [str(item) for item in (deleted or [])] + if mode == "raw": + merged = merge_raw_extraction( + fresh, path, prune_sources=prune, root=root, ast_sources=changed_paths, + ) + _write_raw(path, merged) + else: + graph = build_merge( + [fresh] if changed_paths else [], path, prune_sources=prune, + dedup=False, root=root, ast_sources=changed_paths, + ) + assert to_json(graph, {}, str(path), force=True) + return _read_graph(path) + + +def _matlab_class(graph: dict, name: str, source_file: str) -> dict: + matches = [ + node for node in graph.get("nodes", []) + if node.get("label") == name + and node.get("source_file") == source_file + and node.get("metadata", {}).get("kind") == "matlab_class" + ] + assert len(matches) == 1, (name, source_file, matches) + return matches[0] + + +def _targets(graph: dict, child: dict) -> list[dict]: + by_id = {node["id"]: node for node in graph.get("nodes", [])} + return [ + by_id[edge["target"]] + for edge in _edges(graph) + if edge.get("relation") == "inherits" and edge.get("source") == child["id"] + ] + + +def _signature(graph: dict) -> tuple: + nodes = tuple(sorted(json.dumps(node, sort_keys=True) for node in graph.get("nodes", []))) + edges = tuple(sorted(json.dumps(edge, sort_keys=True) for edge in _edges(graph))) + return nodes, edges + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_changed_child_replaces_only_itself_and_keeps_context_read_only(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "Base.m", _class_source("Base", revision=1)) + child_path = _write(root, "Child.m", _class_source("Child", "Base", revision=1)) + _start_graph(mode, root, graph_path, [base_path, child_path]) + before = _read_graph(graph_path) + base_before = _matlab_class(before, "Base", "Base.m") + child_before = _matlab_class(before, "Child", "Child.m") + assert [node["label"] for node in _targets(before, child_before)] == ["Base"] + + context_nodes = copy.deepcopy([ + node for node in before["nodes"] if node.get("source_file") == "Base.m" + ]) + context_edges = copy.deepcopy([ + edge for edge in _edges(before) if edge.get("source_file") == "Base.m" + ]) + nodes_snapshot, edges_snapshot = copy.deepcopy(context_nodes), copy.deepcopy(context_edges) + child_path.write_text(_class_source("Child", "Base", revision=2), encoding="utf-8") + fresh = _extract( + root, [child_path], resolution_context_nodes=context_nodes, + resolution_context_edges=context_edges, + ) + assert context_nodes == nodes_snapshot + assert context_edges == edges_snapshot + assert {node.get("source_file") for node in fresh["nodes"]} == {"Child.m"} + + if mode == "raw": + merged = merge_raw_extraction( + fresh, graph_path, root=root, ast_sources=[child_path], + ) + _write_raw(graph_path, merged) + else: + graph = build_merge( + [fresh], graph_path, dedup=False, root=root, ast_sources=[child_path], + ) + assert to_json(graph, {}, str(graph_path), force=True) + after = _read_graph(graph_path) + + base_after = _matlab_class(after, "Base", "Base.m") + child_after = _matlab_class(after, "Child", "Child.m") + assert base_after["id"] == base_before["id"] + assert child_after["id"] == child_before["id"] + assert [node["label"] for node in _targets(after, child_after)] == ["Base"] + assert sum(node.get("source_file") == "Base.m" for node in after["nodes"]) == sum( + node.get("source_file") == "Base.m" for node in before["nodes"] + ) + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_unchanged_child_keeps_inheritance_after_base_content_change(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "Base.m", _class_source("Base", revision=1)) + child_path = _write(root, "Child.m", _class_source("Child", "Base")) + _start_graph(mode, root, graph_path, [base_path, child_path]) + before = _read_graph(graph_path) + child_before = _matlab_class(before, "Child", "Child.m") + + base_path.write_text(_class_source("Base", revision=2), encoding="utf-8") + after = _merge(mode, root, graph_path, [base_path]) + + child_after = _matlab_class(after, "Child", "Child.m") + assert child_after["id"] == child_before["id"] + assert [node["label"] for node in _targets(after, child_after)] == ["Base"] + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_unchanged_child_drops_inheritance_when_base_is_renamed(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "Base.m", _class_source("Base")) + child_path = _write(root, "Child.m", _class_source("Child", "Base")) + _start_graph(mode, root, graph_path, [base_path, child_path]) + child_before = _matlab_class(_read_graph(graph_path), "Child", "Child.m") + + renamed_path = root / "Renamed.m" + renamed_path.write_text(_class_source("Renamed"), encoding="utf-8") + base_path.unlink() + after = _merge(mode, root, graph_path, [renamed_path], [base_path]) + + child_after = _matlab_class(after, "Child", "Child.m") + assert child_after["id"] == child_before["id"] + assert _targets(after, child_after) == [] + assert not any(node.get("source_file") == "Base.m" for node in after["nodes"]) + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_unchanged_child_resolves_when_base_is_added(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + child_path = _write(root, "Child.m", _class_source("Child", "Base")) + _start_graph(mode, root, graph_path, [child_path]) + child_before = _matlab_class(_read_graph(graph_path), "Child", "Child.m") + assert _targets(_read_graph(graph_path), child_before) == [] + + base_path = _write(root, "Base.m", _class_source("Base")) + after = _merge(mode, root, graph_path, [base_path]) + + child_after = _matlab_class(after, "Child", "Child.m") + assert child_after["id"] == child_before["id"] + assert [node["label"] for node in _targets(after, child_after)] == ["Base"] + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_unchanged_child_loses_inheritance_when_base_is_deleted(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "Base.m", _class_source("Base")) + child_path = _write(root, "Child.m", _class_source("Child", "Base")) + _start_graph(mode, root, graph_path, [base_path, child_path]) + base_path.unlink() + + after = _merge(mode, root, graph_path, deleted=[base_path]) + + child = _matlab_class(after, "Child", "Child.m") + assert _targets(after, child) == [] + assert not any(node.get("source_file") == "Base.m" for node in after["nodes"]) + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_newly_ambiguous_base_removes_inheritance(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "+pkg/Base.m", _class_source("Base")) + child_path = _write(root, "+pkg/Child.m", _class_source("Child", "pkg.Base")) + _start_graph(mode, root, graph_path, [base_path, child_path]) + child_before = _matlab_class(_read_graph(graph_path), "Child", "+pkg/Child.m") + assert [node["label"] for node in _targets(_read_graph(graph_path), child_before)] == ["Base"] + + duplicate = _write(root, "other/+pkg/Base.m", _class_source("Base")) + after = _merge(mode, root, graph_path, [duplicate]) + + child_after = _matlab_class(after, "Child", "+pkg/Child.m") + assert child_after["id"] == child_before["id"] + assert _targets(after, child_after) == [] + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_disambiguating_deleted_base_restores_inheritance(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + first = _write(root, "one/+pkg/Base.m", _class_source("Base")) + second = _write(root, "two/+pkg/Base.m", _class_source("Base")) + child_path = _write(root, "+pkg/Child.m", _class_source("Child", "pkg.Base")) + _start_graph(mode, root, graph_path, [first, second, child_path]) + before = _read_graph(graph_path) + child_before = _matlab_class(before, "Child", "+pkg/Child.m") + assert _targets(before, child_before) == [] + + first.unlink() + after = _merge(mode, root, graph_path, deleted=[first]) + + child_after = _matlab_class(after, "Child", "+pkg/Child.m") + targets = _targets(after, child_after) + assert len(targets) == 1 + assert targets[0].get("source_file") == "two/+pkg/Base.m" + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_deleted_child_removes_its_nodes_and_derived_inheritance(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "Base.m", _class_source("Base")) + child_path = _write(root, "Child.m", _class_source("Child", "Base")) + _start_graph(mode, root, graph_path, [base_path, child_path]) + child_path.unlink() + + after = _merge(mode, root, graph_path, deleted=[child_path]) + + assert not any(node.get("source_file") == "Child.m" for node in after["nodes"]) + assert not any(edge.get("source_file") == "Child.m" and edge.get("relation") == "inherits" + for edge in _edges(after)) + assert _matlab_class(after, "Base", "Base.m") + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_no_op_update_preserves_matlab_graph(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "Base.m", _class_source("Base")) + child_path = _write(root, "Child.m", _class_source("Child", "Base")) + _start_graph(mode, root, graph_path, [base_path, child_path]) + before = _read_graph(graph_path) + + after = _merge(mode, root, graph_path) + + assert _signature(after) == _signature(before) + +def test_raw_graph_prune_refreshes_matlab_inheritance(tmp_path): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "Base.m", _class_source("Base")) + child_path = _write(root, "Child.m", _class_source("Child", "Base")) + _start_graph("raw", root, graph_path, [base_path, child_path]) + before = _read_graph(graph_path) + child = _matlab_class(before, "Child", "Child.m") + assert [node["label"] for node in _targets(before, child)] == ["Base"] + + from graphify.cli import _prune_graph_json_sources + + assert _prune_graph_json_sources(graph_path, ["Base.m"]) > 0 + after = _read_graph(graph_path) + child = _matlab_class(after, "Child", "Child.m") + assert _targets(after, child) == [] + assert not any(node.get("source_file") == "Base.m" for node in after["nodes"]) + + +@pytest.mark.parametrize("no_cluster", [True, False], ids=["raw-cli", "clustered-cli"]) +def test_update_cli_refreshes_unchanged_matlab_child_after_base_change(tmp_path, no_cluster): + import os + import subprocess + import sys + + root = tmp_path / "project" + root.mkdir() + base = _write(root, "Base.m", _class_source("Base", revision=1)) + _write(root, "Child.m", _class_source("Child", "Base")) + options = ["--code-only"] + (["--no-cluster"] if no_cluster else []) + first = subprocess.run( + [sys.executable, "-m", "graphify", "extract", str(root), *options], + cwd=tmp_path, capture_output=True, text=True, env=os.environ.copy(), + ) + assert first.returncode == 0, first.stderr + base.write_text(_class_source("Base", revision=2), encoding="utf-8") + update_options = ["--no-cluster"] if no_cluster else [] + second = subprocess.run( + [sys.executable, "-m", "graphify", "update", str(root), *update_options], + cwd=tmp_path, capture_output=True, text=True, env=os.environ.copy(), + ) + assert second.returncode == 0, second.stderr + + graph = _read_graph(root / "graphify-out" / "graph.json") + child_node = _matlab_class(graph, "Child", "Child.m") + assert [node["label"] for node in _targets(graph, child_node)] == ["Base"] diff --git a/tests/test_matlab_inheritance.py b/tests/test_matlab_inheritance.py index 270b3ca23c..19fa61a6b2 100644 --- a/tests/test_matlab_inheritance.py +++ b/tests/test_matlab_inheritance.py @@ -15,7 +15,6 @@ def _class(source, name, *, namespace="", bases=(), repo=None): { **common, "id": f"{prefix}{source}:file", - "type": "file", "label": source, "metadata": { "language": "matlab", @@ -28,7 +27,6 @@ def _class(source, name, *, namespace="", bases=(), repo=None): { **common, "id": f"{prefix}{source}:class:{name}", - "type": "class", "label": name, "source_location": "L1", "metadata": { @@ -213,15 +211,21 @@ def test_foreign_repository_and_untagged_candidates_cannot_fill_missing_bases(): def test_nonsource_nodes_cannot_supply_class_targets(): child = _class("Child.m", "Child", bases=("Base",)) for field, value in ( - ("external", True), ("file_type", "document"), ("id", None), ("type", "function") + ("external", True), ("file_type", "document"), ("id", None), + ("metadata.kind", "matlab_function"), ): base = _class("Base.m", "Base") - base[-1][field] = value + if field == "metadata.kind": + base[-1]["metadata"]["kind"] = value + else: + base[-1][field] = value assert _matlab_inheritance_edges(child + base) == [] def test_non_file_nodes_cannot_establish_source_context(): child = _class("Child.m", "Child", bases=("Base",)) base = _class("Base.m", "Base") - base[0]["type"] = "module" + base[0]["metadata"]["kind"] = "matlab_class" assert _matlab_inheritance_edges(child + base) == [] + + diff --git a/tests/test_matlab_integration.py b/tests/test_matlab_integration.py new file mode 100644 index 0000000000..4cb0dafffa --- /dev/null +++ b/tests/test_matlab_integration.py @@ -0,0 +1,122 @@ +"""Normal MATLAB dispatch, remap, and persisted-build integration coverage.""" +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from graphify.build import build +from graphify.export import to_json +from graphify.extract import extract + + +def _extract(root: Path, paths: list[Path]) -> dict: + return extract(paths, root=root, cache_root=root, parallel=False) + + +@pytest.mark.parametrize(("config", "name", "source", "expected_label"), [ + ( + "language.m=objc\n", + "Mapped.m", + "@interface Mapped : NSObject\n@end\n", + "Mapped", + ), + ( + "language.m=python\n", + "mapped.m", + "def chosen():\n return 1\n", + "chosen()", + ), + ( + "language.inc=.m\n", + "Mapped.inc", + "@interface Mapped : NSObject\n@end\n", + "Mapped", + ), +]) +def test_language_remaps_keep_matlab_and_objc_dispatch_contract( + tmp_path: Path, config: str, name: str, source: str, expected_label: str, +) -> None: + (tmp_path / ".graphifyrc").write_text(config, encoding="utf-8") + path = tmp_path / name + path.write_text(source, encoding="utf-8") + + result = _extract(tmp_path, [path]) + + assert any(node.get("label") == expected_label for node in result["nodes"]) + assert result["nodes"], "the selected remap must reach a real language extractor" + + +def test_matlab_calls_and_exact_source_inheritance_survive_build(tmp_path: Path) -> None: + base = tmp_path / "Base.m" + child = tmp_path / "Derived.m" + calls = tmp_path / "Calls.m" + base.write_text("classdef Base\nend\n", encoding="utf-8") + child.write_text("classdef Derived < Base\nend\n", encoding="utf-8") + calls.write_text( + "function y = caller(x)\n" + " y = helper(x);\n" + "end\n" + "function y = helper(x)\n" + " y = x + 1;\n" + "end\n", + encoding="utf-8", + ) + + result = _extract(tmp_path, [base, child, calls]) + by_id = {node["id"]: node for node in result["nodes"]} + raw_call = next( + edge for edge in result["edges"] + if edge.get("relation") == "calls" + and by_id[edge["source"]].get("label") == "caller" + and by_id[edge["target"]].get("label") == "helper" + ) + raw_inherits = next( + edge for edge in result["edges"] + if edge.get("relation") == "inherits" + and by_id[edge["source"]].get("label") == "Derived" + and by_id[edge["target"]].get("label") == "Base" + ) + assert raw_call.get("source_file") == "Calls.m" + assert raw_call.get("source_location") == "L2" + assert raw_inherits.get("source_file") == "Derived.m" + + graph = build([result], dedup=False, root=tmp_path) + path = tmp_path / "built.json" + assert to_json(graph, {}, str(path), force=True) + stored = json.loads(path.read_text(encoding="utf-8")) + stored_nodes = {node["id"]: node for node in stored["nodes"]} + stored_edges = stored.get("links", stored.get("edges", [])) + relations = { + (stored_nodes[edge["source"]].get("label"), + stored_nodes[edge["target"]].get("label"), + edge.get("relation"), edge.get("source_file"), edge.get("source_location")) + for edge in stored_edges + } + assert ("caller", "helper", "calls", "Calls.m", "L2") in relations + assert ("Derived", "Base", "inherits", "Derived.m", "L1") in relations + +def test_pre_upgrade_matlab_cache_does_not_mask_current_extraction(tmp_path, monkeypatch): + from graphify import cache + + source = tmp_path / "controller.m" + source.write_text("function value = controller(input)\n value = input;\nend\n", encoding="utf-8") + cache_root = tmp_path / "cache" + current_schema = cache._AST_CACHE_SCHEMA + # The pre-MATLAB AST cache namespace is the persisted schema-25 namespace. + monkeypatch.setattr(cache, "_AST_CACHE_SCHEMA", 25) + cache.save_cached( + source, + {"nodes": [{"id": "stale_cache_node", "label": "stale MATLAB parse", + "source_file": "controller.m"}], "edges": []}, + root=tmp_path, + cache_root=cache_root, + ) + monkeypatch.setattr(cache, "_AST_CACHE_SCHEMA", current_schema) + + result = extract([source], root=tmp_path, cache_root=cache_root, parallel=False) + + labels = {node.get("label") for node in result["nodes"]} + assert "controller" in labels + assert "stale MATLAB parse" not in labels diff --git a/tests/test_matlab_merge_isolation.py b/tests/test_matlab_merge_isolation.py index b0202f8d51..8611083da9 100644 --- a/tests/test_matlab_merge_isolation.py +++ b/tests/test_matlab_merge_isolation.py @@ -4,7 +4,7 @@ import pytest from graphify.affected import load_graph -from graphify.build import build, build_merge, prefix_graph_for_global +from graphify.build import _refresh_matlab_inheritance, build, build_merge, prefix_graph_for_global from graphify.cross_repo_calls import link_cross_repo_member_calls from graphify.cross_repo_types import link_shared_type_declarations from graphify.export import to_json @@ -102,16 +102,59 @@ def test_namespace_matlab_classes_do_not_join_native_or_matlab_types(tmp_path): assert merged.edges["native1::greeter", "native2::greeter"]["relation"] == "same_type_as" + +def test_matlab_inheritance_refresh_preserves_semantic_edges(): + child_source, base_source = "+Shared/Child.m", "+Shared/Base.m" + nodes = [ + _node( + "child_file", "Child.m", child_source, matlab=True, kind="file", + namespace="Shared", matlab_file_kind="class", matlab_primary_name="Child", + ), + _node( + "child", "Child", child_source, matlab=True, kind="class", + namespace="Shared", scope_chain=[], + matlab_superclasses=[{"name": "Shared.Base", "source_location": "L1"}], + ), + _node( + "base_file", "Base.m", base_source, matlab=True, kind="file", + namespace="Shared", matlab_file_kind="class", matlab_primary_name="Base", + ), + _node( + "base", "Base", base_source, matlab=True, kind="class", + namespace="Shared", scope_chain=[], + ), + ] + stale_ast = { + **_edge("child", "stale_base", "inherits", child_source), + "_origin": "ast", + } + semantic = { + **_edge("child", "semantic_base", "inherits", child_source), + "_origin": "semantic", + } + + refreshed = _refresh_matlab_inheritance(nodes, [stale_ast, semantic]) + + assert semantic in refreshed + assert stale_ast not in refreshed + assert [ + (edge["source"], edge["target"], edge["_origin"]) + for edge in refreshed + if edge.get("relation") == "inherits" and edge.get("_origin") == "ast" + ] == [("child", "base", "ast")] + + def test_incremental_carry_forward_preserves_local_calls_and_source_inheritance(tmp_path): nodes = [ _node("child_file", "Child.m", "+Shared/Child.m", matlab=True, kind="file", - matlab_file_kind="class", matlab_primary_name="Child"), + namespace="Shared", matlab_file_kind="class", matlab_primary_name="Child"), _node("child", "Child", "+Shared/Child.m", matlab=True, kind="class", namespace="Shared", scope_chain=[], matlab_superclasses=[{"name": "Shared.Base", "source_location": "L1"}]), _node("base_file", "Base.m", "+Shared/Base.m", matlab=True, kind="file", - matlab_file_kind="class", matlab_primary_name="Base"), - _node("base", "Base", "+Shared/Base.m", matlab=True, kind="class", namespace="Shared"), + namespace="Shared", matlab_file_kind="class", matlab_primary_name="Base"), + _node("base", "Base", "+Shared/Base.m", matlab=True, kind="class", + namespace="Shared", scope_chain=[]), _node("run", "run", "+Shared/Child.m", matlab=True, kind="method"), _node("helper", "helper", "+Shared/Child.m", matlab=True), ] diff --git a/tests/test_matlab_watch.py b/tests/test_matlab_watch.py new file mode 100644 index 0000000000..19b16e8af2 --- /dev/null +++ b/tests/test_matlab_watch.py @@ -0,0 +1,128 @@ +"""MATLAB inheritance refresh through the actual incremental watch rebuild.""" +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from graphify.watch import _rebuild_code + + +def _class_source(name: str, superclass: str | None = None, revision: int | None = None) -> str: + parent = f" < {superclass}" if superclass else "" + lines = [f"classdef {name}{parent}"] + if revision is not None: + lines.extend([ + " methods", + " function value = version(obj)", + f" value = {revision};", + " end", + " end", + ]) + return "\n".join([*lines, "end", ""]) + + +def _write(root: Path, relative: str, source: str) -> Path: + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source, encoding="utf-8") + return path + + +def _graph(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _class(graph: dict, name: str, source_file: str) -> dict: + matches = [ + node for node in graph.get("nodes", []) + if node.get("label") == name + and node.get("source_file") == source_file + and node.get("metadata", {}).get("kind") == "matlab_class" + ] + assert len(matches) == 1, (name, source_file, matches) + return matches[0] + + +def _inheritance_targets(graph: dict, child: dict) -> list[dict]: + by_id = {node["id"]: node for node in graph.get("nodes", [])} + return [ + by_id[edge["target"]] + for edge in graph.get("links", graph.get("edges", [])) + if edge.get("relation") == "inherits" and edge.get("source") == child["id"] + ] + + +@pytest.mark.parametrize("no_cluster", [True, False], ids=["raw", "clustered"]) +def test_incremental_watch_refreshes_unchanged_child_for_base_lifecycle(tmp_path, no_cluster): + root = tmp_path / "project" + root.mkdir() + graph_path = root / "graphify-out" / "graph.json" + _write(root, "+pkg/Child.m", _class_source("Child", "pkg.Base")) + + assert _rebuild_code(root, no_cluster=no_cluster, acquire_lock=False) is True + initial = _graph(graph_path) + child = _class(initial, "Child", "+pkg/Child.m") + child_id = child["id"] + assert _inheritance_targets(initial, child) == [] + + base_path = _write(root, "+pkg/Base.m", _class_source("Base")) + assert _rebuild_code( + root, changed_paths=[base_path], no_cluster=no_cluster, acquire_lock=False + ) is True + added = _graph(graph_path) + child = _class(added, "Child", "+pkg/Child.m") + targets = _inheritance_targets(added, child) + assert child["id"] == child_id + assert [(node["label"], node.get("source_file")) for node in targets] == [ + ("Base", "+pkg/Base.m") + ] + + before_noop = graph_path.read_bytes() + assert _rebuild_code( + root, changed_paths=[], no_cluster=no_cluster, acquire_lock=False + ) is True + assert graph_path.read_bytes() == before_noop + + base_path.write_text(_class_source("Base", revision=1), encoding="utf-8") + assert _rebuild_code( + root, changed_paths=[base_path], no_cluster=no_cluster, acquire_lock=False + ) is True + changed = _graph(graph_path) + child = _class(changed, "Child", "+pkg/Child.m") + assert child["id"] == child_id + assert [node["label"] for node in _inheritance_targets(changed, child)] == ["Base"] + + duplicate_path = _write( + root, "other/+pkg/Base.m", _class_source("Base") + ) + assert _rebuild_code( + root, changed_paths=[duplicate_path], no_cluster=no_cluster, acquire_lock=False + ) is True + ambiguous = _graph(graph_path) + child = _class(ambiguous, "Child", "+pkg/Child.m") + assert child["id"] == child_id + assert _inheritance_targets(ambiguous, child) == [] + + duplicate_path.unlink() + assert _rebuild_code( + root, changed_paths=[duplicate_path], no_cluster=no_cluster, acquire_lock=False + ) is True + disambiguated = _graph(graph_path) + child = _class(disambiguated, "Child", "+pkg/Child.m") + targets = _inheritance_targets(disambiguated, child) + assert child["id"] == child_id + assert [(node["label"], node.get("source_file")) for node in targets] == [ + ("Base", "+pkg/Base.m") + ] + + base_path.unlink() + assert _rebuild_code( + root, changed_paths=[base_path], no_cluster=no_cluster, acquire_lock=False + ) is True + removed = _graph(graph_path) + child = _class(removed, "Child", "+pkg/Child.m") + assert child["id"] == child_id + assert _inheritance_targets(removed, child) == [] + assert not any(node.get("source_file") == "+pkg/Base.m" for node in removed["nodes"]) From 8489154a4422fe69f5125e955a26ea6d8bad25bf Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:37:14 +0100 Subject: [PATCH 08/11] test(matlab): add reviewed corpus and determinism coverage Check normal extraction against the independently reviewed JSONLab manifest and provenance, plus repeat, order, and relocation determinism. Document the conservative MATLAB fallback, calls, inheritance, and persistence contract. Refs: JulioLeonPhD/graphify#12 Co-authored-by: OpenAI Codex --- ARCHITECTURE.md | 10 ++ README.md | 4 +- tests/test_matlab_corpus.py | 199 ++++++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 tests/test_matlab_corpus.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cb64e975bb..b5be951824 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -92,6 +92,16 @@ A plain `json_graph.node_link_graph()` load loses this: an undirected graph re-o 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. 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. + ## Security All external input passes through `graphify/security.py` before use: diff --git a/README.md b/README.md index a2240fea8f..e258703995 100644 --- a/README.md +++ b/README.md @@ -390,7 +390,9 @@ graphify honors `.gitignore` and supports `--exclude` patterns and a project `.g 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 diff --git a/tests/test_matlab_corpus.py b/tests/test_matlab_corpus.py new file mode 100644 index 0000000000..54902df689 --- /dev/null +++ b/tests/test_matlab_corpus.py @@ -0,0 +1,199 @@ +"""Source-reviewed JSONLab evidence for normal MATLAB extraction.""" +from collections import Counter +import hashlib +import json +from pathlib import Path +import shutil + +from graphify.extract import extract +from graphify.ids import make_id + + +CORPUS = Path(__file__).parent / "fixtures" / "matlab" / "jsonlab" +MANIFEST = json.loads((CORPUS / "expected.json").read_text(encoding="utf-8")) + + +def _stage_jsonlab(root: Path) -> Path: + root.mkdir(parents=True, exist_ok=True) + source = root / MANIFEST["contract"]["source_file"] + shutil.copyfile(CORPUS / source.name, source) + return source + + +def _extract(paths: list[Path], root: Path, cache_root: Path) -> dict: + return extract(paths, root=root, cache_root=cache_root, parallel=False) + + +def _assert_manifest_matches(result: dict, manifest: dict, source: bytes) -> None: + definitions = manifest["definitions"] + path = manifest["file"]["path"] + expected_by_signature = { + ( + definition["kind"], + definition["name"], + tuple(definition["scope_chain"]), + definition["source_location"], + ): definition["key"] + for definition in definitions + } + assert len(expected_by_signature) == len(definitions) + + nodes = result["nodes"] + assert len({node["id"] for node in nodes}) == len(nodes) + assert all((node.get("metadata") or {}).get("language") == "matlab" for node in nodes) + assert all(node.get("source_file") == path for node in nodes) + + file_nodes = [ + node for node in nodes + if (node.get("metadata") or {}).get("kind") == manifest["file"]["kind"] + ] + assert len(file_nodes) == 1 + file_node = file_nodes[0] + file_metadata = file_node["metadata"] + assert file_node["label"] == Path(path).name + assert file_metadata["matlab_file_kind"] == manifest["file"]["matlab_file_kind"] + assert file_metadata["matlab_primary_name"] == manifest["file"]["matlab_primary_name"] + + actual_by_key = {"file": file_node} + for node in nodes: + if node is file_node: + continue + metadata = node["metadata"] + signature = ( + metadata.get("kind"), + node.get("label"), + tuple(metadata.get("scope_chain", ())), + node.get("source_location"), + ) + assert signature in expected_by_signature, f"unexpected definition: {node}" + key = expected_by_signature[signature] + assert key not in actual_by_key, f"duplicate definition for manifest key {key}" + actual_by_key[key] = node + assert set(actual_by_key) == {"file", *(definition["key"] for definition in definitions)} + + source_lines = source.splitlines() + for definition in definitions: + line = source_lines[int(definition["source_location"][1:]) - 1] + assert line.decode("utf-8") == definition["source_text"] + assert line[definition["column"] - 1:].startswith(b"function ") + + expected_edges: Counter[tuple] = Counter() + for edge in manifest["contains"]: + expected_edges[("contains", edge["source"], edge["target"], edge["source_location"])] += 1 + + for call in manifest["calls"]: + line = source_lines[int(call["source_location"][1:]) - 1] + assert line.decode("utf-8") == call["source_text"] + target_name = next( + definition["name"] for definition in definitions + if definition["key"] == call["target"] + ) + assert line[call["column"] - 1:].startswith(target_name.encode("utf-8")) + expected_edges[( + "calls", call["source"], call["target"], call["source_location"], + "EXTRACTED", 1.0, + )] += 1 + + id_to_key = {node["id"]: key for key, node in actual_by_key.items()} + actual_edges: Counter[tuple] = Counter() + for edge in result["edges"]: + relation = edge.get("relation") + if relation not in {"contains", "method", "inherits", "calls"}: + continue + assert edge.get("source_file") == path, f"edge lost source provenance: {edge}" + assert edge.get("source") in id_to_key, f"unknown source endpoint: {edge}" + assert edge.get("target") in id_to_key, f"unknown target endpoint: {edge}" + record = ( + relation, + id_to_key[edge["source"]], + id_to_key[edge["target"]], + edge.get("source_location"), + ) + if relation == "calls": + record += (edge.get("confidence"), edge.get("weight")) + actual_edges[record] += 1 + + missing, unexpected = expected_edges - actual_edges, actual_edges - expected_edges + assert not missing and not unexpected, ( + f"manifest relationship mismatch; missing={dict(missing)}, unexpected={dict(unexpected)}" + ) + + +def test_jsonlab_corpus_matches_reviewed_manifest_through_normal_extraction(tmp_path): + source = _stage_jsonlab(tmp_path / "jsonlab") + source_bytes = source.read_bytes() + source_record = next( + record for record in MANIFEST["provenance"]["files"] + if record["path"] == MANIFEST["contract"]["source_file"] + ) + assert len(source_bytes) == source_record["size_bytes"] + assert hashlib.sha256(source_bytes).hexdigest() == source_record["sha256"] + + result = _extract([source], source.parent, tmp_path / "cache") + _assert_manifest_matches(result, MANIFEST, source_bytes) + + repeated = _extract([source], source.parent, tmp_path / "cache") + relocated_source = _stage_jsonlab(tmp_path / "relocated" / "jsonlab") + relocated = _extract( + [relocated_source], relocated_source.parent, tmp_path / "relocated-cache" + ) + assert _canonical_records(repeated) == _canonical_records(result) + assert _canonical_records(relocated) == _canonical_records(result) + + +def _stage_normalization_collisions(root: Path) -> list[Path]: + source = "function entry()\n Foo();\n foo();\nend\nfunction Foo()\nend\nfunction foo()\nend\n" + paths = [root / "pkg-one" / "main-v1.m", root / "pkg_one" / "main_v1.m"] + for path in paths: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source, encoding="utf-8") + return paths + + +def _canonical_records(result: dict) -> tuple[tuple[str, ...], tuple[str, ...]]: + nodes = tuple(sorted(json.dumps({ + "id": node["id"], + "label": node.get("label"), + "source_file": node.get("source_file"), + "source_location": node.get("source_location"), + "metadata": node.get("metadata"), + }, sort_keys=True) for node in result["nodes"])) + edges = tuple(sorted(json.dumps({ + key: edge.get(key) + for key in ( + "source", "target", "relation", "source_file", "source_location", + "confidence", "weight", + ) + }, sort_keys=True) for edge in result["edges"])) + return nodes, edges + + +def test_normal_extraction_is_deterministic_across_repeats_relocation_and_order(tmp_path): + assert make_id("pkg-one/main-v1") == make_id("pkg_one/main_v1") + assert make_id("Foo") == make_id("foo") + + root = tmp_path / "first-root" / "repo" + paths = _stage_normalization_collisions(root) + forward = _extract(paths, root, tmp_path / "cache-forward") + repeated = _extract(paths, root, tmp_path / "cache-forward") + reverse = _extract(list(reversed(paths)), root, tmp_path / "cache-reverse") + + relocated_root = tmp_path / "elsewhere" / "repo" + relocated_paths = _stage_normalization_collisions(relocated_root) + relocated = _extract(relocated_paths, relocated_root, tmp_path / "cache-relocated") + + expected = _canonical_records(forward) + assert _canonical_records(repeated) == expected + assert _canonical_records(reverse) == expected + assert _canonical_records(relocated) == expected + + assert len(forward["nodes"]) == 8 + assert len({node["id"] for node in forward["nodes"]}) == 8 + calls = [edge for edge in forward["edges"] if edge.get("relation") == "calls"] + assert len(calls) == 4 + assert {node["source_file"] for node in forward["nodes"]} == { + "pkg-one/main-v1.m", "pkg_one/main_v1.m", + } + for path in ("pkg-one/main-v1.m", "pkg_one/main_v1.m"): + assert sum(node.get("source_file") == path for node in forward["nodes"]) == 4 + assert sum(edge.get("source_file") == path for edge in calls) == 2 From 8db1f953f138628624785d17000d4efea72a8ff9 Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:07:18 +0100 Subject: [PATCH 09/11] fix(matlab): preserve eligible calls and source-backed relationships Address final specification review: retain lexical calls inside member arguments and documentation/config citations, and normalize inheritance refresh context without mutating inputs. Add consumer-visible regressions. Centrally verified 203 focused tests, Ruff, generated artifacts and distribution build; type diagnostics match the 656-error baseline. Full suite: 8893 passed with one unchanged failure reproduced at 109cef1b. Refs: JulioLeonPhD/graphify#12 Co-Authored-By: OpenAI Codex --- ARCHITECTURE.md | 2 +- graphify/build.py | 47 ++++++++++---- graphify/extractors/matlab.py | 6 +- tests/test_matlab_incremental.py | 105 ++++++++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 14 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b5be951824..ebd544252e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -98,7 +98,7 @@ For `.m` files, explicit `.graphifyrc` remaps take precedence, then the existing 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. 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. +- **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. diff --git a/graphify/build.py b/graphify/build.py index d45008dd03..c27f5776ff 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -990,12 +990,27 @@ def dedupe_edges(edges: list[dict]) -> list[dict]: return out -def _refresh_matlab_inheritance(nodes: list[dict], edges: list[dict]) -> list[dict]: +def _refresh_matlab_inheritance( + 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 nodes: + for node in resolver_nodes: if not isinstance(node, dict): continue metadata = node.get("metadata") @@ -1014,18 +1029,19 @@ def _refresh_matlab_inheritance(nodes: list[dict], edges: list[dict]) -> list[di def _is_matlab_inheritance(edge: dict) -> bool: if not isinstance(edge, dict) or edge.get("relation") != "inherits": return False - try: - source_file = matlab_sources.get(edge.get("source")) - except TypeError: + 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 in edges if not _is_matlab_inheritance(edge)] - refreshed = _matlab_inheritance_edges(nodes) + 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] @@ -1803,11 +1819,20 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat # 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; extraction provenance wins over suffixes. + # .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: - continue + 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) @@ -2390,7 +2415,7 @@ def _dropped(item: dict) -> bool: if unverified_semantic_shrink: new["_unverified_semantic_shrink"] = unverified_semantic_shrink new["edges"] = _refresh_matlab_inheritance( - dedupe_nodes(new.get("nodes", [])), new.get("edges", []) + dedupe_nodes(new.get("nodes", [])), new.get("edges", []), root=_eff_root, ) return new @@ -2693,7 +2718,7 @@ def _prune_match(sf: "str | None") -> bool: edge for chunk in all_chunks for edge in chunk.get("edges", []) ] all_chunks[0]["edges"] = _refresh_matlab_inheritance( - resolver_nodes, merged_edges + resolver_nodes, merged_edges, root=_eff_root, ) for chunk in all_chunks[1:]: chunk["edges"] = [] diff --git a/graphify/extractors/matlab.py b/graphify/extractors/matlab.py index 087f6b6472..090007275a 100644 --- a/graphify/extractors/matlab.py +++ b/graphify/extractors/matlab.py @@ -276,7 +276,11 @@ def is_call_site(node, scope_node) -> bool: return False parent = node.parent while parent is not None and node_key(parent) != node_key(scope_node): - if parent.type in {"lambda", "handle_operator", "arguments_statement", "field_expression"}: + if parent.type == "field_expression": + field = parent.child_by_field_name("field") + if field is not None and node_key(field) == node_key(node): + return False + if parent.type in {"lambda", "handle_operator", "arguments_statement"}: return False if parent.type == "assignment": left = parent.child_by_field_name("left") diff --git a/tests/test_matlab_incremental.py b/tests/test_matlab_incremental.py index 536dca4108..b21630926c 100644 --- a/tests/test_matlab_incremental.py +++ b/tests/test_matlab_incremental.py @@ -7,7 +7,12 @@ import pytest -from graphify.build import build, build_merge, merge_raw_extraction +from graphify.build import ( + _refresh_matlab_inheritance, + build, + build_merge, + merge_raw_extraction, +) from graphify.export import to_json from graphify.extract import extract @@ -165,6 +170,104 @@ def test_changed_child_replaces_only_itself_and_keeps_context_read_only(tmp_path ) +def test_matlab_inheritance_refresh_normalizes_mixed_paths_without_mutating_inputs(tmp_path): + root = tmp_path / "src" + root.mkdir() + child_source = str(root / "Child.m") + nodes = [ + { + "id": "child_file", + "label": "Child.m", + "source_file": child_source, + "file_type": "code", + "metadata": { + "language": "matlab", "kind": "matlab_file", "namespace": "", + "matlab_file_kind": "class", "matlab_primary_name": "Child", + }, + }, + { + "id": "child", + "label": "Child", + "source_file": child_source, + "file_type": "code", + "metadata": { + "language": "matlab", "kind": "matlab_class", "namespace": "", + "scope_chain": [], + "matlab_superclasses": [{"name": "Base", "source_location": "L2"}], + }, + }, + { + "id": "base_file", + "label": "Base.m", + "source_file": "Base.m", + "file_type": "code", + "metadata": { + "language": "matlab", "kind": "matlab_file", "namespace": "", + "matlab_file_kind": "class", "matlab_primary_name": "Base", + }, + }, + { + "id": "base", + "label": "Base", + "source_file": "Base.m", + "file_type": "code", + "metadata": { + "language": "matlab", "kind": "matlab_class", "namespace": "", + "scope_chain": [], + }, + }, + ] + edges = [ + {"source": "base_file", "target": "base", "relation": "contains", "source_file": "Base.m"}, + { + "source": "child", "target": "stale", "relation": "inherits", + "source_file": child_source, "_origin": "ast", + }, + ] + before = copy.deepcopy((nodes, edges)) + + refreshed = _refresh_matlab_inheritance(nodes, edges, root=root) + + assert (nodes, edges) == before + assert [ + (edge["source"], edge["target"], edge["source_file"]) + for edge in refreshed if edge.get("relation") == "inherits" + ] == [("child", "base", "Child.m")] + + +@pytest.mark.parametrize("mode", MERGE_PATHS) +def test_absolute_matlab_reextract_resolves_relative_unchanged_base(tmp_path, mode): + root = tmp_path / "src" + root.mkdir() + graph_path = tmp_path / "graph.json" + base_path = _write(root, "Base.m", _class_source("Base")) + child_path = _write(root, "Child.m", _class_source("Child", "Base")) + _start_graph(mode, root, graph_path, [base_path, child_path]) + fresh = _extract(root, [child_path]) + for item in fresh["nodes"] + fresh["edges"]: + if item.get("source_file"): + item["source_file"] = str(root / item["source_file"]) + + if mode == "raw": + merged = merge_raw_extraction( + fresh, graph_path, root=root, ast_sources=[child_path], + ) + _write_raw(graph_path, merged) + else: + graph = build_merge( + [fresh], graph_path, dedup=False, root=root, ast_sources=[child_path], + ) + assert to_json(graph, {}, str(graph_path), force=True) + + after = _read_graph(graph_path) + child = next( + node for node in after["nodes"] + if node.get("label") == "Child" + and node.get("metadata", {}).get("kind") == "matlab_class" + ) + assert [node["label"] for node in _targets(after, child)] == ["Base"] + + @pytest.mark.parametrize("mode", MERGE_PATHS) def test_unchanged_child_keeps_inheritance_after_base_content_change(tmp_path, mode): root = tmp_path / "src" From bd9890130dbd6f93721db338ce54459dbbe49f16 Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:09:32 +0100 Subject: [PATCH 10/11] test(matlab): retain final review regression coverage Keep the centrally verified member-argument call and source-backed documentation/config reference regressions with the review corrections. Final focused gate: 203 passed. Refs: JulioLeonPhD/graphify#12 Co-Authored-By: OpenAI Codex --- tests/test_matlab.py | 18 ++++++++++++++++++ tests/test_matlab_merge_isolation.py | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/tests/test_matlab.py b/tests/test_matlab.py index 1612960e40..afc9dcdf1b 100644 --- a/tests/test_matlab.py +++ b/tests/test_matlab.py @@ -4,6 +4,8 @@ import pytest +from graphify.build import build +from graphify.extract import extract from graphify.extractors.base import set_active_scan_root from graphify.extractors.matlab import extract_matlab @@ -302,6 +304,22 @@ def test_workspace_barrier_flows_to_nested_functions_only(tmp_path): assert call_sites(result) == [("independent", "helper", "L8")] +def test_unqualified_call_argument_inside_member_call_survives_build(tmp_path): + path = tmp_path / "main.m" + path.write_text( + "function main(obj)\nobj.run(helper());\nend\nfunction helper()\nend\n" + ) + extraction = extract( + [path], root=tmp_path, cache_root=tmp_path / "cache", parallel=False, + ) + assert call_sites(extraction) == [("main", "helper", "L2")] + + graph = build([extraction], dedup=False, root=tmp_path) + main = next(node["id"] for node in extraction["nodes"] if node["label"] == "main") + helper = next(node["id"] for node in extraction["nodes"] if node["label"] == "helper") + assert graph.edges[main, helper]["relation"] == "calls" + + def test_indexing_qualified_dynamic_and_handle_forms_are_not_calls(tmp_path): result = extract_source(tmp_path, """function caller() A = [1, 2]; diff --git a/tests/test_matlab_merge_isolation.py b/tests/test_matlab_merge_isolation.py index 8611083da9..00d3d33469 100644 --- a/tests/test_matlab_merge_isolation.py +++ b/tests/test_matlab_merge_isolation.py @@ -57,6 +57,25 @@ def test_persisted_matlab_native_homonyms_are_not_neighbors( assert reloaded.edges["native", "native_target"]["relation"] == relation +@pytest.mark.parametrize( + ("source_file", "file_type"), + [("guide.md", "document"), ("settings.json", "code")], +) +def test_source_backed_docs_and_configs_reference_matlab(source_file, file_type): + graph = build([{ + "nodes": [ + { + "id": "source", "label": source_file, "source_file": source_file, + "file_type": file_type, "type": file_type, + }, + _node("main", "main", "main.m", matlab=True), + ], + "edges": [_edge("source", "main", "references", source_file)], + }], dedup=False) + + assert graph.edges["source", "main"]["relation"] == "references" + + def _type_graph(*, matlab=False, source="Greeter.m", method="-greet"): return build([{"nodes": [ _node("greeter", "Greeter", source, matlab=matlab, kind="class", namespace="Shared"), From a24491c211fbb854a4d1e1241656af01d43fe849 Mon Sep 17 00:00:00 2001 From: Julio Leon <17530758+JulioLeonPhd@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:07:20 +0100 Subject: [PATCH 11/11] chore: narrow MATLAB contribution to upstream scope Remove unrelated agent-workflow guidance from the upstream delta; preserve local copies outside the tracked tree. Regenerate the lock with upstream-compatible uv 0.11.19, retaining existing dependency versions and removing resolver-format churn. Trim extra test EOF whitespace while preserving byte-pinned JSONLab sources and license. Verified 1093 focused regressions with one expected xfail, frozen sync, lock check, Ruff, 134 generated artifacts, real corpus extraction/query, and AST-only graph update. Full-suite and Pyright upstream debt remains disclosed in local maintenance evidence. Co-Authored-By: OpenAI Codex --- AGENTS.md | 16 -- docs/agents/domain.md | 12 - docs/agents/issue-tracker.md | 51 ---- docs/agents/triage-labels.md | 12 - tests/test_matlab_inheritance.py | 2 - uv.lock | 402 +++++++++++++++---------------- 6 files changed, 201 insertions(+), 294 deletions(-) delete mode 100644 docs/agents/domain.md delete mode 100644 docs/agents/issue-tracker.md delete mode 100644 docs/agents/triage-labels.md diff --git a/AGENTS.md b/AGENTS.md index 3cc7f99414..38857bdc2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,19 +6,3 @@ Rules: - When working on Graphify itself, use the repository's existing Graphify guidance. For codebase questions, prefer scoped graph queries where available; use the report for broad orientation. Do not use the graph as evidence when the task concerns the graph's correctness itself. - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) - -## Agent skills - -### Issue tracker - -GitHub Issues on `JulioLeonPhD/graphify`. See `docs/agents/issue-tracker.md`. -Fork-local: exclude this subsection and `docs/agents/issue-tracker.md` from the upstream PR. - -### Triage labels - -Use the five default triage labels. See `docs/agents/triage-labels.md`. - -### Domain docs - -Single-context: root `GLOSSARY.md` and `docs/adr/`, created lazily. -See `docs/agents/domain.md`. diff --git a/docs/agents/domain.md b/docs/agents/domain.md deleted file mode 100644 index ddb420922d..0000000000 --- a/docs/agents/domain.md +++ /dev/null @@ -1,12 +0,0 @@ -# Domain Docs - -This repository uses a single domain context. - -Before exploring, read root `GLOSSARY.md` and relevant ADRs in `docs/adr/`. -If absent, proceed silently; do not create placeholder files. -Domain-modeling creates them lazily as terms and decisions are resolved. - -Use glossary vocabulary in issues, proposals, tests and explanations. -Reconsider undefined terminology or raise it during domain-modeling. - -Surface conflicts with existing ADRs explicitly; never silently override them. diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md deleted file mode 100644 index 28b03ed424..0000000000 --- a/docs/agents/issue-tracker.md +++ /dev/null @@ -1,51 +0,0 @@ -# Issue tracker: GitHub - -Issues and specs live in `JulioLeonPhD/graphify`. -Use the gh CLI with explicit `--repo JulioLeonPhD/graphify`; -do not infer the target from this clone's multiple remotes. - -MATLAB wayfinding: [Find the smallest maintainer-ready MATLAB extractor PR](https://github.com/JulioLeonPhd/graphify/issues/1). -The map links the migrated research evidence; its native sub-issues are the decision tickets. - -## Upstream PR boundary - -This tracker configuration is fork-local. Before opening the MATLAB extractor PR -against Graphify-Labs/graphify, remove this file and the fork-specific Issue tracker -subsection in AGENTS.md from the upstream PR diff. Do not replace the destination -with upstream and publish fork planning issues there. Keep local planning artifacts -out of the upstream PR. - -## Operations - -- Create: `gh issue create --repo JulioLeonPhD/graphify --title "..." --body "..."` -- Read: `gh issue view --repo JulioLeonPhD/graphify --json number,title,body,labels,comments,assignees` -- List: `gh issue list --repo JulioLeonPhD/graphify --state open --json number,title,body,labels,assignees` -- Comment: `gh issue comment --repo JulioLeonPhD/graphify --body "..."` -- Label: `gh issue edit --repo JulioLeonPhD/graphify --add-label "..."` or `--remove-label "..."` -- Close: `gh issue close --repo JulioLeonPhD/graphify` - -When a skill says publish or fetch a ticket, create or read a GitHub issue. - -## Pull requests as a triage surface - -PRs as a request surface: no. - -## Wayfinding operations - -- Map: one issue labelled `wayfinder:map`. -- Tickets: native sub-issues of the map, labelled `wayfinder:`. - Map and tickets carry only wayfinder labels, not triage labels. -- Parent link: POST `repos/JulioLeonPhD/graphify/issues//sub_issues` - with `sub_issue_id=`, using `gh api`. -- Blocking: native GitHub issue dependencies. POST - `repos/JulioLeonPhD/graphify/issues//dependencies/blocked_by` - with `issue_id=`. Use database ids, not issue numbers. -- Frontier: query the map's sub-issues; select open, unassigned tickets - with no open blockers. Preserve sub-issue order. -- Claim before work: assign the ticket to `@me`. -- Resolve: post a resolution comment, close the ticket, then append - a named link and one-line gist to the map's Decisions so far. -- Refer to maps and tickets by linked title, never a bare number. - -Only if native relationships are unavailable: use a task list on the map, -a parent link on each child, and a Blocked by line linking blockers. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md deleted file mode 100644 index a406f9828c..0000000000 --- a/docs/agents/triage-labels.md +++ /dev/null @@ -1,12 +0,0 @@ -# Triage Labels - -| Role | Tracker label | Meaning | -| --- | --- | --- | -| needs-triage | needs-triage | Maintainer evaluation needed | -| needs-info | needs-info | Waiting on reporter information | -| ready-for-agent | ready-for-agent | Fully specified, ready for an AFK agent | -| ready-for-human | ready-for-human | Requires human implementation | -| wontfix | wontfix | Will not be actioned | - -Use these labels when skills refer to the corresponding triage roles. -Wayfinder maps and decision tickets use only wayfinder labels. diff --git a/tests/test_matlab_inheritance.py b/tests/test_matlab_inheritance.py index 19fa61a6b2..749a842d6c 100644 --- a/tests/test_matlab_inheritance.py +++ b/tests/test_matlab_inheritance.py @@ -227,5 +227,3 @@ def test_non_file_nodes_cannot_establish_source_context(): base = _class("Base.m", "Base") base[0]["metadata"]["kind"] = "matlab_class" assert _matlab_inheritance_edges(child + base) == [] - - diff --git a/uv.lock b/uv.lock index c86b31d2b1..508ba8952b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -8,10 +8,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -91,7 +91,7 @@ name = "autograd" version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/ac/4a8b58364ba865ab545251edbe475e5a35326814a5d274a2211d33ed8a5d/autograd-1.9.1.tar.gz", hash = "sha256:7818c5c69ddf9efb7da74097bd741a4c7920133f41966f68537223a15ef798bc", size = 2566975, upload-time = "2026-07-02T07:43:58.255Z" } wheels = [ @@ -104,10 +104,10 @@ version = "18.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/8d/f4/f22114d30d3435e38c6af2b4870f37b864403dca6ae7af747a289ce0a18e/av-18.1.0.tar.gz", hash = "sha256:47bfc286e1bc9de7ab4681fc2b575cd2460a66919d31ffe1bd5aa54fae531a28", size = 4451061, upload-time = "2026-08-12T22:28:18.761Z" } @@ -603,7 +603,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -674,14 +674,14 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -767,7 +767,7 @@ resolution-markers = [ "python_full_version == '3.13.*'", ] dependencies = [ - { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/5a/a55177dd22553a277388e8a1b3220e92de91bacb28356cdc73caa240121d/contourpy-1.4.0.tar.gz", hash = "sha256:20156f5a1ac4f8ce02656e39a61e82164a3d359796dc8026f75b062783d500e1", size = 13323726, upload-time = "2026-09-11T19:05:05.808Z" } wheels = [ @@ -975,7 +975,7 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] [[package]] @@ -989,10 +989,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/d2/79/e78bd8f75a9b8e32233ce92d4607f0a21ce88a9156cace2884e7175a82f5/coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712", size = 991021, upload-time = "2026-10-22T16:06:50.009Z" } @@ -1192,9 +1192,9 @@ name = "ctranslate2" version = "4.8.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "pyyaml" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/af/76/07c2d83393f9071e66f7395ec333d0adea3c8b8e35be2ab4deaa87e4473d/ctranslate2-4.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b174efd7f9554b87b5a5125129c76a82736c2154d0e734ea2e55b3c58e75ba16", size = 1283878, upload-time = "2026-10-13T05:56:52.823Z" }, @@ -1295,7 +1295,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1320,13 +1320,13 @@ name = "faster-whisper" version = "1.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "av", version = "18.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "av", version = "18.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "av", version = "19.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "ctranslate2" }, - { name = "huggingface-hub" }, - { name = "onnxruntime" }, - { name = "tokenizers" }, - { name = "tqdm" }, + { name = "ctranslate2", marker = "python_full_version >= '3.11'" }, + { name = "huggingface-hub", marker = "python_full_version >= '3.11'" }, + { name = "onnxruntime", marker = "python_full_version >= '3.11'" }, + { name = "tokenizers", marker = "python_full_version >= '3.11'" }, + { name = "tqdm", marker = "python_full_version >= '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" }, @@ -1355,10 +1355,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" } @@ -1461,10 +1461,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/94/36/102e180f8f5dbaee88b26595b01ca8aa80bf4e62128d9aa94265b3996c96/fonttools-4.67.0.tar.gz", hash = "sha256:3cb57e6600ca77c0b1729cf8adc23bc0652633a37f18cfa934d9c7bc3de25519", size = 3750028, upload-time = "2026-10-14T13:20:28.294Z" } @@ -1551,10 +1551,10 @@ name = "gensim" version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "smart-open" }, + { name = "smart-open", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/80/fe9d2e1ace968041814dbcfce4e8499a643a36c41267fa4b6c4f54cce420/gensim-4.4.0.tar.gz", hash = "sha256:a3f5b626da5518e79a479140361c663089fe7998df8ba52d56e1ded71ac5bdf5", size = 23260095, upload-time = "2025-10-18T02:06:45.962Z" } wheels = [ @@ -1931,28 +1931,28 @@ name = "graspologic" version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anytree" }, - { name = "beartype" }, - { name = "future" }, - { name = "gensim" }, - { name = "graspologic-native" }, - { name = "hyppo" }, - { name = "joblib" }, - { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "anytree", marker = "python_full_version < '3.13'" }, + { name = "beartype", marker = "python_full_version < '3.13'" }, + { name = "future", marker = "python_full_version < '3.13'" }, + { name = "gensim", marker = "python_full_version < '3.13'" }, + { name = "graspologic-native", marker = "python_full_version < '3.13'" }, + { name = "hyppo", marker = "python_full_version < '3.13'" }, + { name = "joblib", marker = "python_full_version < '3.13'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "matplotlib", version = "3.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "networkx", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "pot" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pot", marker = "python_full_version < '3.13'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "seaborn" }, - { name = "statsmodels" }, - { name = "typing-extensions" }, - { name = "umap-learn" }, + { name = "seaborn", marker = "python_full_version < '3.13'" }, + { name = "statsmodels", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "umap-learn", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/bb/0fe2ef85ea775e7b8416b2cf90097aa4b5e0c9c2271d7fe6789bab27d0ca/graspologic-3.4.4.tar.gz", hash = "sha256:79878caf367da3e89046a4ec94291c5b1a5da569f19fdd879d8b45c3563d7110", size = 5134258, upload-time = "2025-09-08T21:44:01.969Z" } wheels = [ @@ -2022,8 +2022,8 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "h11" }, - { name = "truststore" }, + { name = "h11", marker = "python_full_version < '3.11'" }, + { name = "truststore", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/ff/b53af1b49bb2cb5fa97c92215614b173ca5c1a99ded80e2d52f15a94fa6d/httpcore2-2.14.0.tar.gz", hash = "sha256:4f57a1a8249fe6af589d935cfd456f1d13bbdcb0f0fc72af9142368fc94811d7", size = 69096, upload-time = "2026-10-18T16:29:21.898Z" } wheels = [ @@ -2040,13 +2040,13 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "h11" }, - { name = "truststore" }, + { name = "h11", marker = "(python_full_version >= '3.11' and sys_platform != 'emscripten') or (python_full_version >= '3.13' and sys_platform == 'emscripten')" }, + { name = "truststore", marker = "(python_full_version >= '3.11' and sys_platform != 'emscripten') or (python_full_version >= '3.13' and sys_platform == 'emscripten')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/61/afa9d6b7452f9eca73f73d45324eb0bb52f098fd5e6049bf6b8263be9b4e/httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643", size = 69886, upload-time = "2026-10-21T07:09:45.31Z" } wheels = [ @@ -2061,11 +2061,11 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "anyio", marker = "sys_platform != 'emscripten'" }, - { name = "httpcore2", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'emscripten'" }, - { name = "idna" }, - { name = "truststore", marker = "sys_platform != 'emscripten'" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "python_full_version < '3.11' and sys_platform != 'emscripten'" }, + { name = "httpcore2", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'emscripten'" }, + { name = "idna", marker = "python_full_version < '3.11'" }, + { name = "truststore", marker = "python_full_version < '3.11' and sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e8/67/58353f1848b4ba568e1ec22ea1e0d8e039f0bca5fde7e2d1f07739fd83e7/httpx2-2.14.0.tar.gz", hash = "sha256:47ad61a3c40282cd8d0dc96311531f00f5bfc71a066c6fe3b36d8f63fb9e4f6e", size = 102036, upload-time = "2026-10-18T16:29:22.627Z" } wheels = [ @@ -2083,19 +2083,19 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "anyio", marker = "sys_platform != 'emscripten'" }, - { name = "httpcore2", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'emscripten'" }, + { name = "anyio", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" }, + { name = "httpcore2", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" }, { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, - { name = "idna" }, - { name = "truststore", marker = "sys_platform != 'emscripten'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.11'" }, + { name = "truststore", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/73/5ca2cedf5e4e50423d1c258877865a6b62793633b561c58499f8aeb33b7e/httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f", size = 103116, upload-time = "2026-10-21T07:09:46.348Z" } wheels = [ @@ -2116,15 +2116,15 @@ name = "huggingface-hub" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "filelock", version = "4.2.4", source = { registry = "https://pypi.org/simple" } }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'ARM64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "httpx2", version = "2.16.0", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "tqdm" }, - { name = "typing-extensions" }, + { name = "click", marker = "python_full_version >= '3.11'" }, + { name = "filelock", version = "4.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "fsspec", marker = "python_full_version >= '3.11'" }, + { name = "hf-xet", marker = "(python_full_version >= '3.11' and platform_machine == 'AMD64') or (python_full_version >= '3.11' and platform_machine == 'ARM64') or (python_full_version >= '3.11' and platform_machine == 'aarch64') or (python_full_version >= '3.11' and platform_machine == 'amd64') or (python_full_version >= '3.11' and platform_machine == 'arm64') or (python_full_version >= '3.11' and platform_machine == 'x86_64')" }, + { name = "httpx2", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "tqdm", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/dd/585639d95f6a987c0bdfb9045f050f4ad67240775644ba7a4fdc5aca3a3b/huggingface_hub-2.3.0.tar.gz", hash = "sha256:0b783b37f200f1d25bef2344a997c7d410f4437fbe9d8fd965f958e75cec7aef", size = 1063410, upload-time = "2026-10-23T08:52:06.594Z" } wheels = [ @@ -2139,8 +2139,8 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "exceptiongroup" }, - { name = "sortedcontainers" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/a8/bd70d7c2966e561228b9fdc075ee77c0ba577dcbbfbf921edf614db14f6a/hypothesis-6.168.5.tar.gz", hash = "sha256:76b9226962fe11d40858253a967eda95bb65811365286317e0118f4ec8f808c7", size = 511115, upload-time = "2026-10-05T23:26:35.416Z" } wheels = [ @@ -2241,14 +2241,14 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "sortedcontainers" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d7/1f/d08bc6d4548cf5f6a022895d2f32c23e981fb417517358aebe612544a2ad/hypothesis-6.170.8.tar.gz", hash = "sha256:dfc56ceac35d825fb6a85fa3ac36a598fee3101d264a39895a62f78c1cc95521", size = 518977, upload-time = "2026-10-22T23:01:19.151Z" } wheels = [ @@ -2341,18 +2341,18 @@ name = "hyppo" version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "autograd" }, - { name = "future" }, - { name = "numba" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "autograd", marker = "python_full_version < '3.13'" }, + { name = "future", marker = "python_full_version < '3.13'" }, + { name = "numba", marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "patsy" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "patsy", marker = "python_full_version < '3.13'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "statsmodels" }, + { name = "statsmodels", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/a6/0d84fe8486a1447da8bdb8ebb249d525fd8c1d0fe038bceb003c6e0513f9/hyppo-0.5.2.tar.gz", hash = "sha256:4634d15516248a43d25c241ed18beeb79bb3210360f7253693b3f154fe8c9879", size = 125115, upload-time = "2025-05-24T18:33:27.418Z" } wheels = [ @@ -2382,7 +2382,7 @@ name = "importlib-metadata" version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } wheels = [ @@ -2553,7 +2553,7 @@ name = "joblib" version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cloudpickle" }, + { name = "cloudpickle", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/1d/537ab090f302b838943a1b56497dd53059b9a9b46a074936470173a2e207/joblib-1.6.0.tar.gz", hash = "sha256:2ccc96785b12046c08fd6d55839c12857831b54a3c1673ffadd2f04bfc4eda03", size = 327903, upload-time = "2026-08-31T09:39:04.122Z" } wheels = [ @@ -2978,15 +2978,15 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, - { name = "cycler" }, - { name = "fonttools", version = "4.65.0", source = { registry = "https://pypi.org/simple" } }, - { name = "kiwisolver" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pillow", version = "12.3.0", source = { registry = "https://pypi.org/simple" } }, - { name = "pyparsing" }, - { name = "python-dateutil" }, + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "cycler", marker = "python_full_version < '3.11'" }, + { name = "fonttools", version = "4.65.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "kiwisolver", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", version = "12.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pyparsing", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } wheels = [ @@ -3057,24 +3057,24 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "contourpy", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "cycler" }, - { name = "fonttools", version = "4.67.0", source = { registry = "https://pypi.org/simple" } }, - { name = "kiwisolver" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cycler", marker = "python_full_version >= '3.11'" }, + { name = "fonttools", version = "4.67.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "kiwisolver", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "packaging" }, - { name = "pillow", version = "13.0.0", source = { registry = "https://pypi.org/simple" } }, - { name = "pyparsing" }, - { name = "python-dateutil" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pillow", version = "13.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyparsing", marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/c8/9aa712a0afb882649424dd8de8ad9aa6235e796e84c6052e8f6dc1598d0d/matplotlib-3.11.2.tar.gz", hash = "sha256:cec596316640f2b394b8f0daa0ea61a8eae82d017b620b9f202befb972a59ea4", size = 32660610, upload-time = "2026-09-11T19:05:31.214Z" } wheels = [ @@ -3318,10 +3318,10 @@ version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } @@ -3365,8 +3365,8 @@ name = "numba" version = "0.68.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "llvmlite" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "llvmlite", marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4e/cd/e8280f9ffa30fea9fabc5341223701231fcc5d53a31f51419d42d4bec3a6/numba-0.68.0.tar.gz", hash = "sha256:8a781de54b980b98f43bff7f1093701b5f07c80d031c7cfa8a87493d8bf73f2d", size = 2855363, upload-time = "2026-09-30T15:05:44.721Z" } wheels = [ @@ -3412,10 +3412,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -3529,11 +3529,11 @@ name = "onnxruntime" version = "1.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "flatbuffers" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "flatbuffers", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "packaging" }, - { name = "protobuf" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "protobuf", marker = "python_full_version >= '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/e7/61b2768393646bd12e31eeb71958193f4e02c98c4980cf9289d19bbb4a8f/onnxruntime-1.31.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cbf1a7f6470ddfe9dbc781966af8ce4a10e1858d75a93f93cc6b9367c9587870", size = 20871717, upload-time = "2026-10-09T04:18:03.504Z" }, @@ -3630,10 +3630,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -3695,16 +3695,16 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e2/17/d7b106e05bfa642e8694451e7d3d759c6a241c5386a5d962e4f66c047e06/pandas-3.0.6.tar.gz", hash = "sha256:66b07ef7315a31bfe1089cd3d71a7de781c9dca986762d0b4fe7c0ef17465d10", size = 4667686, upload-time = "2026-09-17T23:23:18.345Z" } wheels = [ @@ -3788,8 +3788,8 @@ name = "patsy" version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/e3/950e513affdfe1c1864397b4f40d6ed06ec1b54f608e462c27f151dd72df/patsy-1.0.3.tar.gz", hash = "sha256:79ebf4c93ff4d296e58a9d5be2b2ee31bd49d737cf11d70ffbd8a44b2de42e65", size = 399992, upload-time = "2026-08-29T21:35:32.258Z" } wheels = [ @@ -3904,10 +3904,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/ce/a4/9cc30690a394a8305e8be4c9036aa16d0dfddadd09079860b582f562496d/pillow-13.0.0.tar.gz", hash = "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", size = 47058631, upload-time = "2026-10-16T22:18:09.718Z" } @@ -4075,10 +4075,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } @@ -4100,8 +4100,8 @@ name = "pot" version = "0.9.7.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/e3/61271810cdea79e869531bbfcbc2b5a1d94e88ec5ebdd32ed81aa2ff40cc/pot-0.9.7.post1.tar.gz", hash = "sha256:2edd70845047ac2d378487b980a2bffb5ff58e9b92b7133896ff5753f6300cec", size = 1842013, upload-time = "2026-07-29T09:01:39.843Z" } @@ -4152,11 +4152,11 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, + { name = "cfgv", marker = "python_full_version < '3.11'" }, + { name = "identify", marker = "python_full_version < '3.11'" }, + { name = "nodeenv", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, + { name = "virtualenv", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } wheels = [ @@ -4174,18 +4174,18 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, + { name = "cfgv", marker = "python_full_version >= '3.11'" }, + { name = "identify", marker = "python_full_version >= '3.11'" }, + { name = "nodeenv", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "virtualenv", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" } wheels = [ @@ -4333,10 +4333,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" } @@ -4537,12 +4537,12 @@ name = "pynndescent" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "joblib" }, - { name = "llvmlite" }, - { name = "numba" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "joblib", marker = "python_full_version < '3.13'" }, + { name = "llvmlite", marker = "python_full_version < '3.13'" }, + { name = "numba", marker = "python_full_version < '3.13'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } @@ -4875,10 +4875,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/18/97/226c43b7b5d957bc3840ed52ea99eed261f99834c4619be7a4742cbaeafa/rapidfuzz-3.14.6.tar.gz", hash = "sha256:e13a8160d017b499ec7a2fa9d0ce1ae2e7377080815785819f966fb235d4eb60", size = 57955060, upload-time = "2026-08-30T21:45:51.097Z" } @@ -5324,10 +5324,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" } @@ -5506,10 +5506,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -5554,18 +5554,18 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "joblib" }, - { name = "narwhals" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "narwhals", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/eb/eaf5e07fcc0da7149b0e084f24e54edd7441b9a89ce7e034032ae97fe3a0/scikit_learn-1.9.1.tar.gz", hash = "sha256:629cada3e33e2b9bf376cdc7614a47a4140b8aedc1d836579e359736fbd82977", size = 7786908, upload-time = "2026-09-10T18:34:04.679Z" } wheels = [ @@ -5621,7 +5621,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -5681,14 +5681,14 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -5763,7 +5763,7 @@ resolution-markers = [ "python_full_version == '3.13.*'", ] dependencies = [ - { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" } wheels = [ @@ -5834,10 +5834,10 @@ name = "seaborn" version = "0.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "matplotlib", version = "3.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } @@ -5868,7 +5868,7 @@ name = "smart-open" version = "8.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wrapt" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/e3/1fb5c310b1f55d99ba51cb81b8f1a0e4b5f89be44de69405aeeb12814778/smart_open-8.0.4.tar.gz", hash = "sha256:8cff8321f0b0ae4bd9cb0cd0c507c135ad6aa980171248fca520b645fd6ff717", size = 62316, upload-time = "2026-10-17T19:34:45.106Z" } wheels = [ @@ -5949,8 +5949,8 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "anyio" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" } wheels = [ @@ -5968,15 +5968,15 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "anyio", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/eb/4e197bf26b023abafc5e9043ec8710d7de0fcb032d0969045236d72aa0b1/starlette-1.9.0.tar.gz", hash = "sha256:3d931de246ed870e35ce605974440816ef895e592f255d1396cfdf191d4c4318", size = 2732466, upload-time = "2026-10-18T15:50:33.189Z" } wheels = [ @@ -5988,12 +5988,12 @@ name = "statsmodels" version = "0.14.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "patsy" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "patsy", marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } @@ -6053,10 +6053,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/db/a1/3b8ed9c1fc3aa6eebb57732d924ddaa0500ecc3b638d0454816320994383/stevedore-5.9.1.tar.gz", hash = "sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf", size = 518111, upload-time = "2026-08-20T15:25:14.754Z" } @@ -6146,7 +6146,7 @@ name = "tokenizers" version = "0.23.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "huggingface-hub" }, + { name = "huggingface-hub", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/7c/2cabb2174e772636683008f2c5621949b645da7d303c596589e84516a184/tokenizers-0.23.3.tar.gz", hash = "sha256:cded33237c77caeef62944d32aa9a7ef42bdce2b3497e18d137e072a8c4be438", size = 385286, upload-time = "2026-10-09T10:16:55.759Z" } wheels = [ @@ -6940,14 +6940,14 @@ name = "umap-learn" version = "0.5.12" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numba" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "pynndescent" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "numba", marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pynndescent", marker = "python_full_version < '3.13'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "tqdm" }, + { name = "tqdm", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/ee/af4171241117f85c74b5ca6448ea1033cc28d599c13651d67289bacd4083/umap_learn-0.5.12.tar.gz", hash = "sha256:6aff02ecac5f2aad9f3c65ee518d7ae93e1a985ae38721fdcffceee4232c33c7", size = 96672, upload-time = "2026-04-08T20:03:54.012Z" } wheels = [ @@ -6968,8 +6968,8 @@ name = "uvicorn" version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "h11" }, + { name = "click", marker = "python_full_version < '3.11' or python_full_version >= '3.13' or sys_platform != 'emscripten'" }, + { name = "h11", marker = "python_full_version < '3.11' or python_full_version >= '3.13' or sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }