After `uv tool upgrade graphifyy` or `pip install -U graphifyy`, the
installed skills stay on the old version. `graphify install` only fixes
the platform it detects, so codex, opencode, gemini and the others keep
running outdated instructions until someone reinstalls each one by hand.
The CLI now checks the version stamp of every user-scope skill on its
first run after an upgrade, `graphify update` included, and re-copies
the ones that are older than the package. This is on by default and can
be turned off with GRAPHIFY_NO_AUTO_REFRESH=1.
It is deliberately conservative about what it touches:
- only skills graphify installed itself (stamp and SKILL.md both present)
- never a skill newer than the package, so nothing gets downgraded (#1568)
- never a directory that two installers write to, such as copilot and
`graphify vscode install`, or two platform dirs symlinked to the same
place, because the stamp can't say which variant lives there
- a locally edited SKILL.md is kept as SKILL.md.bak (#3144)
Install, uninstall and the refresh take the same lock per skill
directory, so two processes never write through each other's fixed
.tmp staging names, and an uninstall can't race a refresh that would
put the skill back. It is an OS file lock (flock on POSIX,
msvcrt.locking on Windows). The OS releases it however the holder
exits, so a crash never leaves a directory locked and there is no
staleness timeout to tune. The lock file lives under
~/.cache/graphify/skill-locks/ rather than in the skill directory, so a
project install leaves nothing behind in the repository. Install and
uninstall wait for the lock; the refresh skips a directory another
process holds and reads the stamp again once it has the lock. Where the
file system can't lock, install and uninstall run unlocked as they did
before, and the refresh leaves the skill to the version warning.
Output goes to stderr, one line per refreshed directory, so --json and
the MCP stdio server are unaffected. A failed refresh never breaks the
command the user actually ran: the stamp stays old and the existing
version warning still points to the manual fix.
While here, the version warning now also checks the gemini skill, which
it used to miss because gemini is not in _PLATFORM_CONFIG, and the vscode
skill path moved into _vscode_skill_destination() so install, uninstall
and the refresh share it. The README lists the new variable and mentions
the refresh in the version-mismatch troubleshooting entry.
(cherry picked from commit 50ebe642af)
The code fix landed via #3915 (byte-identical hoist). Keep @harshaygadekar's
independent regression test for #3884 (found simultaneously) for coverage
and contributor credit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An annotation on a class property with no explicit type never assigned
`line`, so the annotation loop raised UnboundLocalError and _safe_extract
dropped the whole file. Set `line` before the type-node check so the file
and its attribute edge both survive.
Fixes#3884
Co-Authored-By: Grok 4.7 <noreply@x.ai>
(cherry picked from commit 03ba8cc9a3)
In _resolve_python_member_calls, candidate imported modules are gathered from imported_by_filenode by checking in contains_children. Following #3410, functions enclosing a nested def also have contains edges to their nested functions. When a caller imports both a module and a function from that module (where the function contains a nested def), both the module and the function matched the candidate filter because the function's _module_stem_key evaluates to the stem of its source file. The resulting len(mods) == 2 triggered the ambiguity guard and dropped all module.func() calls from that file.
Exclude contained symbols by verifying not in file_of_node, which is already populated for all containment targets and excludes top-level file/module nodes.
(cherry picked from commit 542e4869ba)
The Blade extractor recognised @include, <livewire:> components, and
wire:click bindings, but not @extends. Template inheritance is the primary
structural relationship in a Blade view — most page templates render inside
a layout via @extends('layouts.app') — so the child was left with no edge
to its parent layout in the graph.
Emit an 'extends' edge from the view to the named layout, mirroring the
existing @include handling (dotted view name -> path). Adds the first test
coverage for the Blade extractor.
(cherry picked from commit efeeeefa53)
@code is the Blazor (.razor) spelling of an embedded C# member block;
@functions is the equivalent Razor Pages / MVC (.cshtml) spelling. Both
compile to class members, and this extractor serves both file types, but
only @code was matched. A .cshtml page that declared its methods inside an
@functions block lost every one of them.
Match @functions alongside @code in the block regex; the existing
brace-balanced body scan and method extraction are reused unchanged.
(cherry picked from commit e3693430b3)
With a bare `BEGIN;` transaction, a later `DO $$ ... END $$;` makes
tree-sitter-sql read everything from BEGIN up to the DO body's END as a
single block node. The top-level dispatch had no branch for `block`, so
every CREATE TABLE inside it was silently dropped. Walk block nodes the
same way transaction nodes are walked.
Fixes#3886
(cherry picked from commit 8f47001d5b)
Add a Contributors section rendering the contributor avatars via
contrib.rocks, linking to the contributors graph.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Atomic label sidecars (#3853), docx table/all-text extraction (#3833),
wiki index-collision (#3818) and escaped wikilink aliases (#3772),
community-listing real-node filter (#3836), warn-once on missing pypdf
extra (#3710), plus the --help logo banner + app.graphify.com line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Publish the community-label and signature sidecars via the existing
atomic write helper and write labels before the .sig, so an interruption
between the two leaves the .sig trailing (which the staleness guard reads
as a mismatch and safely re-labels) rather than silently keeping stale
labels for a clustering that no longer exists.
Cherry-picked from #3853 by @shashank-100; the unrelated CI-matrix change
(dropping Python 3.13/3.14) in that PR was omitted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers issue 3794 directly: a community made of real code nodes plus
a rationale docstring fragment node and a concept node must only
count and list the real code nodes in its Nodes (N) line, matching
the exact repro shape from the issue. Confirmed against pre fix code
via git stash, the test fails there and reproduces the issue's own
example output almost exactly.
(cherry picked from commit 02435f5dd3)
The per community Nodes count and listing loop only excluded file
nodes, so a rationale docstring fragment node or a concept node
leaked into Nodes (N) as if it were a real code declaration, and
inflated the reported count by including it. The Knowledge Gaps
section a few lines below already excluded file, concept and
rationale nodes together, the correct set an earlier issue
established for this same function, the community listing loop was
never given the same treatment.
Consolidates every real node check this function makes, the summary
thin and shown counts, the community listing, and the gaps section
isolated and thin community counts, onto one shared predicate instead
of four separate inline filters that had drifted apart. Fixing only
the display line without also fixing the count computations that
decide which communities are thin or shown would have reintroduced
the exact header versus render disagreement an earlier issue fixed,
since the header counts and the rendered content need to agree on
what counts as a real node.
Fixes issue 3794.
(cherry picked from commit 57fab42e81)
docx_to_markdown wrote every paragraph first and appended all tables at the
end, so a table lost the heading it belongs to. Paragraph.text also drops
tracked insertions, content controls, fields and text boxes. And a cell's
text went into its markdown row as is, so a line break split the row and a
pipe added a column.
Walk the body in document order, read each paragraph's runs inside those
containers (skipping deleted text and the fallback copy of a text box), and
put a cell on one line with its pipes escaped.
(cherry picked from commit ee72d5256f)
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>