A god node labelled index() (an ordinary web view) or a community named
after one slugs to "index", so to_wiki wrote its article to index.md and
then replaced it with the catalog; the catalog entry linked back to the
catalog itself. Reserve the index slug in the dedup set, and link the
navigation footer to index.md directly so the label resolver can map an
"index" label to its own deduplicated file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit aad5e0feab)
- The bare 'graphify' / 'graphify --help' screen now prints the logo
banner on top (TTY-gated, reusing _print_banner) and the
app.graphify.com hosted-platform line at the bottom, matching the
install banner.
- Update the hosted-platform offer copy to 'Try the graphify platform
free for 14 days' across the CLI help, the install banner, and the
README early-access banner (was 'open free before the public v1 launch').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An R6 method reaches a sibling only through self$method() or private$method(),
never as a bare name, but walk_calls only handled a bare identifier callee. So
every intra-class call inside an R6 class was dropped, leaving methods that
clearly call each other disconnected in the graph.
Handle an extract_operator callee whose receiver is self or private: resolve
the method against the enclosing class scope, which is already reachable from
the caller via scope_parent. super$ is deliberately excluded — it dispatches to
a parent class this single-file pass cannot see, so a local binding would be
wrong.
Added a regression test covering both self$ and private$ calls.
(cherry picked from commit 4e00eaf105)
R6::R6Class(...) is the idiomatic, library()-free way to define an R6 class,
but call_parts returns the qualified text 'R6::R6Class', which never matched
_CLASS_CONSTRUCTORS. The binding therefore fell through to a plain variable
node and the entire class body — every method — was dropped. The same applied
to methods::setClass / methods::setRefClass in the statement path.
Strip the namespace qualifier before the constructor check in both the binding
path and process_call_declaration, so a qualified constructor is treated
exactly like its bare form. Unqualified declarations are unaffected.
(cherry picked from commit 513bea43c2)
enum_declaration was missing from the Groovy config's class_types, so a Groovy
enum and every constant it declares were dropped from the graph — a consumer
could not see which value a branch selects. tree-sitter-groovy exposes the same
enum_constant shape (a name field, optional class_body) that Java uses, so the
enum type now joins class_types and the existing Java enum-constant handler is
reused to emit a node per member with a case_of edge.
Added a regression test asserting the enum type and its constants are present,
attached to the enum rather than the file.
(cherry picked from commit 2c386de21a)
Two defects left CREATE TRIGGER under-extracted:
- The create_trigger AST branch read the subject table off keyword_for, but the
table follows ON ('... AFTER INSERT ON users'); FOR EACH ROW carries no table.
So even a cleanly-parsed trigger got a node with no link to the table it fires
on. Read the table after keyword_on instead.
- A trigger with a procedural body ('FOR EACH ROW BEGIN ... END') has no grammar
production, so the statement lands in an ERROR node and only the whole-file
routine-recovery scan can save it — but that scan's pattern matched only
FUNCTION/PROCEDURE, so the trigger and its table were dropped entirely. Add a
dedicated trigger-recovery pattern that mints the trigger node (label without
(), since a trigger is not callable) and a triggers edge to the table named by
the first ON after the trigger name. A trigger already recovered via the AST
path is skipped to avoid a duplicate edge.
Added regression tests for both the clean EXECUTE FUNCTION form and the
procedural BEGIN...END form.
(cherry picked from commit a28de74a52)
_to_absolute_from_storage normalized a foreign-platform absolute key
with os.path.normpath, which applies the current platform's rules: on
Windows "/home/u/foo.py" became "\home\u\foo.py", a string POSIX cannot
read back. Collapse dot segments under the key's own syntax instead
(ntpath for a drive letter or leading backslash, posixpath otherwise).
test_save_manifest_relativize_step_collapses_seeded_duplicates never
reached save_manifest's own collapse: load_manifest already removes the
duplicate first, so the test passed with the save side reverted to
last-wins. Stub load_manifest to hand over the un-collapsed rows.
Also annotate _collapse_manifest_duplicates like its sibling helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9db2cfe2eb)
_fetch_tweet did url.replace("x.com", "twitter.com") over the whole URL,
so "x.com" in the path or query was rewritten too (a ?next= link, or a
handle like dropbox.com_fan becoming dropbotwitter.com_fan), and an
uppercase X.COM host was missed. Match and rewrite the host only, the
same way _detect_url_type classifies it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3ec4fdcfca)
- SUPPORT.md routes help requests to docs.graphify.com / Discussions /
Discord, keeps issues for bugs and the security policy for vulns.
- CODEOWNERS sets @safishamsi as default owner with explicit routing for
the roundtrip-sensitive skillgen paths, workflows, and security/release
docs (advisory review routing; not a merge gate on its own).
- Q&A discussion template nudges askers to check docs/search first and
include version + environment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add CI status, Apache-2.0 license, and Docs (docs.graphify.com) badges
to the top row (health signals were absent).
- Link docs.graphify.com from 'Learn more' and repoint the issue-template
Documentation contact to the docs site (was README, 0 prior mentions).
- Replace the inline Contributing <details> block (duplicated CONTRIBUTING.md
and prone to drift) with a short pointer to CONTRIBUTING.md/ARCHITECTURE.md;
the issue-template Contributing link already targets CONTRIBUTING.md.
README drops from ~64.8 KB to ~62 KB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#3881.
On Windows, _rebuild_lock falls back to yield True when fcntl cannot be
imported, leaving concurrent rebuilds completely unserialized and causing
file collisions on .graph.tmp.json.
Implement Windows file locking using Python's standard library msvcrt module.
Lock a byte range at offset 4096 so external readers can still read the owning
PID at byte 0 without hitting Windows mandatory lock PermissionErrors.
Un-skip the 4 rebuild lock tests and 3 pending queue tests on Windows, and
add test_rebuild_lock_blocks_concurrent_process.
(cherry picked from commit d8cd456650)
The Fortran capital-F cpp step relied on -nostdinc -I /dev/null to block
#include-based host-file reads, but cpp always searches the source file's
own directory for quoted includes and always honours absolute paths
regardless of those flags. A malicious .F/.F90 file with
#include "/etc/passwd" or #include "../../../secret" could inline
arbitrary host files into graph.json/GRAPH_REPORT.md and the LLM context
on the default offline path (CWE-22/73/200, high severity).
Strip every #include directive before preprocessing and feed the
sanitized source to cpp on stdin, so cpp never opens the file's directory
or any absolute/traversing include path. Macro expansion (#define/#ifdef,
the actual reason capital-F files need cpp) is preserved; cross-file
include fidelity is traded away to close the arbitrary-read vector.
Feeding stdin also removes the -I/etc/x.F90 argument-injection edge case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the scan root is a sub-directory of a monorepo package hierarchy
(e.g., root = Company/Apps/Jobs/Team/), absolute imports using the full
dotted namespace (from Company.Apps.Jobs.Team.lib import delivery) were
unresolved because the scan root probed root/Company/Apps/Jobs/Team/lib
which doesn't exist inside root.
_infer_scan_root_namespace walks above the scan root collecting ancestor
directory names that contain __init__.py, establishing the root's
namespace prefix. When an absolute import's module name starts with that
prefix, the prefix is stripped and the remainder is re-probed relative
to root.
The existing root/rel fast path, ancestor walk (#2072), ambiguous import
guard (#3729), and phantom-edge retraction (#3784) are all untouched.
Third-party imports cannot false-match because the prefix check requires
a strict dotted prefix plus a remaining local component.
Co-Authored-By: Google Antigravity <noreply@google.com>
(cherry picked from commit 640e39efc4)
With a lowercase receiver, <icons.Close /> and <props.Comp /> resolved by the
property name and bound to unrelated local Close() / Comp() functions. None
of the member tags tried produced a correct edge, so only bare identifiers
are handled now.
(cherry picked from commit aa4be46dac)
<Comp /> and <Comp>...</Comp> are parsed as jsx_self_closing_element /
jsx_opening_element with the tag in the `name` field, and neither type was
in call_types, so components that are only rendered got no incoming edge.
Add both types to the JS and TSX call_types and read the callee from
`name`. Tags starting with a lowercase letter are intrinsic elements and
are skipped; for member tags the last part decides, so <div> and
<motion.div> don't bind to a local div(). Namespaced tags and fragments
are ignored.
(cherry picked from commit 2828b55851)
Per Copilot review: the regression test must prove both halves of the fix —
element names are excluded, AND the real element type references are still
collected. Add a test using user-defined element types `(Mode mode, Label
label)`: asserts Mode/Label nodes and their references edges survive while
the names mode/label are not minted.
(cherry picked from commit 6506c32824)
A named tuple type `(int mode, string label)` is a `tuple_type` whose
`tuple_element` children carry both a `type` and a `name` field.
_csharp_collect_type_refs walked every named child, so the element NAMES
were collected as type references, minted as sourceless placeholder nodes
and emitted `references` edges — one junk type node per element, repeated
in every file declaring such a signature.
Only the `type` field of each tuple element should feed type-reference
collection; the `name` is an identifier, not a type reference. Fix the
collector to descend into the element's type field only.
Regression: tests/test_csharp_tuple_type_refs.py asserts no node/edge is
minted for element names (all existing C# tests pass).
(cherry picked from commit bfcdda97b0)
The ECS environment idiom { name = "DB_PASSWORD", value = "hunter2" } names the
secret in the name literal, not in a key, so key-name redaction let the value
reach graph.json and the MCP surface verbatim. When a map's name literal
matches the sensitive pattern, redact its value/valueFrom.
Fixes#3787
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e11cf947d0)
Shows a Discord button on the New Issue page so people can jump to
real-time discussion before filing. No mass backfill of existing threads.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the GitHub Discussions link style so both point via [Discord
server](...) / [GitHub Discussions](...).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-comment on every opened issue and pull request (skipping bots)
pointing contributors to the Discord server for real-time discussion and
to GitHub Discussions for longer-form threads. Uses actions/github-script;
pull_request_target is used so fork PRs can be commented on, and the job
only posts a comment (no checkout/run of PR code), so it is safe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up edits on #3845 to match the settled maintainer decisions:
- CODE_OF_CONDUCT.md: set the enforcement/reporting contact to
safi@graphify.com (was an unspecified 'email the maintainer if known').
- README team-graph section: add the machine-local never-commit list
(.graphify_root, .graphify_python, .graphify_analysis.json, the AST
cache, needs_update) and the re-extract-to-update note. manifest.json
stays committable since #3781 made its keys portable.
- Add RELEASING.md (maintainer-facing): version bump + changelog +
uv.lock, test on 3.10/3.13, push v8, gh release create -> PyPI via
trusted publishing (no twine/token), verify with uvx --refresh.
- CONTRIBUTING Further Reading: drop the paid Gumroad link, point at
RELEASING.md instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers the review finding directly: a Windows drive letter key, a
UNC key, and a POSIX absolute key must all be recognized as already
absolute regardless of which platform the test runs on, and must not
be joined onto the scan root. Confirmed against pre fix code via git
stash, the test fails there since the recognizing helper does not
exist yet.
(cherry picked from commit 33b3a7a00a)
Path.is_absolute() only recognizes the current platform's own path
syntax, so a manifest key using a different platform's absolute form,
a POSIX /abs/path loaded on Windows, or a C: or UNC path loaded on
POSIX, was wrongly judged relative and joined onto the scan root,
producing a nonsense path like root plus C: colon Users colon x colon
foo dot py instead of being left alone. A manifest genuinely can move
between platforms, which is this whole module's own stated scope for
tolerating format mismatches.
A new helper checks for a leading slash or backslash or a drive
letter pattern in addition to the native is_absolute check, so a
foreign absolute key is recognized and passed through unchanged on
any platform instead of only the one that produced it.
Fixes review finding on PR 3781.
(cherry picked from commit 7f47a5fef4)
Covers the review finding directly: a relative manifest key with a
redundant dot dot segment must canonicalize onto the same absolute
path as a plain key for the same file and collapse with it, keeping
the more recently seen entry, instead of surviving as a second,
uncollapsed row.
Confirmed against pre fix code via git stash: the test fails there
and passes only with the fix applied.
(cherry picked from commit 7505a29bb3)
_to_absolute_from_storage joined a relative key onto the resolved
root with a plain path join, which never collapses a parent segment
the way resolve does. A relative key such as sub/../foo.py, the kind
of format mismatch this function exists to tolerate across a mix of
call sites and versions per its own docstring, then canonicalized to
a different string than the plain absolute key for the same file, so
the duplicate collapse this function exists for silently missed it.
The joined result is now run through normpath, which purely
lexically collapses dot segments without touching the filesystem or
following symlinks, matching the sibling relativize function's own
choice not to resolve the key.
Fixes review finding on PR 3781.
(cherry picked from commit e558ed93c5)
One test covers load_manifest directly, checking both on disk
orderings of a stale absolute entry and a fresh relative entry for
the same file, confirming the fresher one wins regardless of which
key iterates last. A companion test covers the write side, seeding a
save through an untouched file's existing duplicate rows and
confirming the relativize step keeps the fresher entry there too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 4839ae9aba)
A manifest written across a mix of versions or call sites, some
passing root to relativize keys and some not, an outdated installed
skill runbook for one, can end up carrying both an absolute and a
relative key for the same file, each with different data. Both
load_manifest and the relativize step inside save_manifest already
canonicalize every key through a comprehension, so the two forms do
collapse into one entry, but a plain dict comprehension keeps
whichever raw key happens to iterate last, an artifact of on disk
JSON key order that has nothing to do with which entry is actually
current. A fresher hash could be silently discarded in favor of a
stale one purely because of how the keys happened to accumulate,
letting the incremental change detector call a genuinely changed file
unchanged.
Reproduced directly: a manifest with a stale absolute entry and a
fresh relative entry for the same file returns the stale one when it
happens to be written second, and the fresh one when the order is
reversed, confirming the collapse really is order dependent today.
Both call sites now go through one small helper that breaks a
collision by each entry's own seen timestamp when both sides have one
and they disagree, since that field exists specifically to record
when an entry was last confirmed current. A legacy or partial entry
without that field falls back to the existing last one wins behavior
unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit fc7dc25a0a)
_detect_url_type tested substrings of the whole URL, so any host ending in
x.com (dropbox.com, netflix.com, linux.com, box.com) was treated as a tweet
and saved as an empty oEmbed stub, and a name in the path or query was
enough for the arxiv, github and youtube types. Match the parsed host or
its subdomains instead.
(cherry picked from commit d48ba15fe4)
Under the spawn start method (the Windows default) each worker re-imports the
parent's __main__ by its __file__ path. A stdin (`… | python -`), `python -c`,
or REPL caller has no such file — the path is missing or `<stdin>` — so every
worker dies during bootstrap and the pool raises BrokenProcessPool before any
work is done. The shipped SKILL.md pipes a heredoc into the interpreter, so on
Windows this is the path most users take, not an edge case: every skill-driven
run prints one full worker traceback per core, then a fallback warning that
blames a missing `if __name__ == "__main__":` guard — a fix the caller cannot
apply, because there is no file to put a guard in.
Detect the unusable-__main__ case before creating the pool: when the start
method is spawn (or the platform is Windows) and __main__ has no importable
file, run sequentially and print one note that names the real cause (stdin/-c/
REPL) and the remedy (write the step to a .py file, or pass parallel=False). A
script that DOES have a __main__ file but omits the guard is a different failure
this cannot catch here; it still surfaces via the existing BrokenProcessPool
fallback, whose __main__-guard message is now accurate for that remaining case.
Tests cover the detector (stdin, REPL-without-__file__, a real script, and the
fork start method) and that extract() with >= _PARALLEL_THRESHOLD uncached files
and an unusable __main__ never attempts the pool yet still produces correct
output. All five fail without the guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit b475196ed1)
`_read_tsconfig_aliases` walks `extends` chains but never `references`. In the
standard Vite / `tsc -b` layout the root `tsconfig.json` is a solution file —
`"files": []` with `references: [{path: "./tsconfig.app.json"}, …]` — and every
`compilerOptions.paths` block lives in a referenced project config. The loader
finds the solution file, sees no `paths` and no `extends`, and stops, so every
path-alias import in the project silently produces no `imports_from` edge and
`graphify affected` returns a partial consumer set with no warning.
Follow `references` the same way `extends` is followed: merge each referenced
config's aliases like an additional parent (the referencing config's own `paths`
still override), guarded by the existing `seen` set that protects against
cycles. A reference `path` may name a directory (resolved to its
`tsconfig.json`, per `tsc -b`) or a config file directly.
Regression tests cover both the solution-file layout and a directory-valued
reference; both fail on the unpatched loader and pass with the fix. The existing
JS/TS resolution, jsconfig-baseUrl, extension-resolution and Astro suites stay
green (135 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 4e84a23822)
Two core Julia constructs were dropped by the extractor:
- macro name(...) ... end produced no node, even though macros are first-class,
heavily used definitions. The grammar gives a macro the same signature shape
as a function, so it now mints a definition (labelled @name) and its body is
walked for calls -- with the signature skipped so the macro does not call
itself.
- @enum Name a b c (and the @enum Name begin ... end block form) is a
macrocall_expression that was ignored entirely. The first identifier in the
argument list is the enum type; the rest, including members inside a begin/end
block, are contained by it. Other macro calls still fall through so
definitions nested in e.g. an @testset block are walked.
Added tests covering macro extraction with body-call attribution (no self-loop)
and both @enum forms with members contained by their type.
(cherry picked from commit 87edb5a278)
The OCaml walker had no branch for class_definition, so a class object --
its methods, its instance variables, and the calls made from method bodies --
was dropped entirely. A file made only of classes produced no nodes at all.
Add a class_definition branch that mints the class node (defines/contains),
walks the object body (object_expression in .ml, class_body_type in .mli) for
method and instance-variable members, and attributes calls in each method body
to that method. Method names are deliberately not registered as local defs:
they are reachable only through obj#m, so binding a bare call to a same-named
method would be a false edge.
(cherry picked from commit 63572f0cbe)
Polish on #3848: remove the unused mid-file tree_sitter imports (F401)
and add a negative test asserting a bare constructor param without
val/var is not emitted as a field property.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A derived type's contains section binds procedures as the type's methods
(procedure :: area => circle_area, or plain procedure :: scale), but the
derived_type_definition branch only minted the type node and ignored the
binding list entirely. The type ended up in the graph with no connection to
its own methods, and calls dispatched through the type were unrepresented.
Collect the bindings while walking the type and, after the whole tree is
walked (so the module's internal procedures already have nodes), add a method
edge from the type to each implementing procedure. A binding to a procedure
from another module resolves to a sourceless stub via ensure_named_node, the
same treatment parameter and return types already get, so the corpus rewire
can collapse it and no dangling edge is produced.
(cherry picked from commit 75f8fff533)