graphifyy 0.9.2__tar.gz → 0.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {graphifyy-0.9.2/graphifyy.egg-info → graphifyy-0.9.4}/PKG-INFO +4 -2
- {graphifyy-0.9.2 → graphifyy-0.9.4}/README.md +3 -1
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/__main__.py +78 -9
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/affected.py +8 -1
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/build.py +170 -21
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/cluster.py +27 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/dedup.py +20 -2
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/diagnostics.py +7 -1
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/export.py +50 -3
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extract.py +1992 -246
- graphifyy-0.9.4/graphify/extractors/csharp.py +393 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/hooks.py +17 -0
- graphifyy-0.9.4/graphify/paths.py +234 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/reflect.py +307 -2
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/report.py +64 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/semantic_cleanup.py +10 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/serve.py +24 -3
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/symbol_resolution.py +22 -2
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/watch.py +8 -4
- {graphifyy-0.9.2 → graphifyy-0.9.4/graphifyy.egg-info}/PKG-INFO +4 -2
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/SOURCES.txt +9 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/pyproject.toml +1 -1
- graphifyy-0.9.4/tests/test_build_merge_hyperedges_and_prune.py +151 -0
- graphifyy-0.9.4/tests/test_community_hub_labels.py +66 -0
- graphifyy-0.9.4/tests/test_corrupt_graph_json.py +53 -0
- graphifyy-0.9.4/tests/test_cpp_objc_cross_file_calls.py +263 -0
- graphifyy-0.9.4/tests/test_csharp_type_resolution.py +569 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_dedup.py +40 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_explain_cli.py +43 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_export.py +69 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_extract.py +85 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_hooks.py +12 -2
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_hypergraph.py +93 -0
- graphifyy-0.9.4/tests/test_indirect_dispatch.py +479 -0
- graphifyy-0.9.4/tests/test_indirect_dispatch_assign_return.py +133 -0
- graphifyy-0.9.4/tests/test_indirect_dispatch_getattr.py +153 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_js_import_resolution.py +251 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_languages.py +488 -0
- graphifyy-0.9.4/tests/test_paths.py +99 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_reflect.py +246 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_report.py +44 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_semantic_cleanup.py +33 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_serve.py +48 -0
- graphifyy-0.9.4/tests/test_skill_version_warning.py +60 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_symbol_resolution.py +90 -4
- graphifyy-0.9.2/graphify/extractors/csharp.py +0 -158
- graphifyy-0.9.2/graphify/paths.py +0 -46
- graphifyy-0.9.2/tests/test_csharp_type_resolution.py +0 -172
- {graphifyy-0.9.2 → graphifyy-0.9.4}/LICENSE +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/__init__.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/_minhash.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/agents-md.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/antigravity-rules.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/claude-md.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/gemini-md.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/kiro-steering.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/vscode-instructions.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/analyze.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/benchmark.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/cache.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/callflow_html.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/cargo_introspect.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/command-kilo.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/detect.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/__init__.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/base.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/blade.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/elixir.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/razor.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/zig.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/file_slice.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/global_graph.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/google_workspace.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/ids.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/ingest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/llm.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/manifest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/manifest_ingest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/mcp_ingest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/multigraph_compat.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/pg_introspect.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/prs.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/querylog.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/resolver_registry.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/ruby_resolution.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/scip_ingest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/security.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-agents.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-aider.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-amp.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-claw.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-codex.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-copilot.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-devin.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-droid.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-kilo.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-kiro.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-opencode.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-pi.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-trae.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-vscode.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-windows.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/add-watch.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/exports.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/extraction-spec.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/github-and-merge.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/hooks.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/query.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/transcribe.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/update.md +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/transcribe.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/tree_html.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/validate.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/wiki.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/dependency_links.txt +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/entry_points.txt +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/requires.txt +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/top_level.txt +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/setup.cfg +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_affected_cli.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_agents_platform.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_analyze.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_anthropic_custom_endpoint.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_antigravity_install.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_astro_extraction.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_backend_extras.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_benchmark.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_build.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cache.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_callflow_html.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cargo_introspect.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_charmap_encoding.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_chunking.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_claude_cli_backend.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_claude_md.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cli_export.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cluster.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_codebuddy.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_confidence.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cpp_preprocess.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_dart.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_detect.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_devin.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_dotnet.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_extract_cli.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_extraction_spec_ids.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_falkordb_integration.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_file_node_id_spec.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_file_slice.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_global_graph.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_google_workspace.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_id_normalization_contract.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_image_vision.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_import_extension_resolution.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_incremental.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_ingest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install_references.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install_roundtrip.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install_strings.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install_upgrade.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_java_type_resolution.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_label_retry.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_labeling.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_language_resolvers.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_llm_backends.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_llm_parser.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_manifest_ingest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_mcp_ingest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_minhash.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_multigraph_compat.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_multigraph_diagnostics.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_multilang.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_obsidian_dangling_member.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_obsidian_filename_cap.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_office_limits.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_ollama.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_openai_custom_endpoint.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_pascal.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_path_cli.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_pg_introspect.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_pipeline.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_provider_registry.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_prs.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_python_import_resolution.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_query_cli.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_querylog.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_rationale.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_read_hook.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_ruby_resolution.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_scip_ingest.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_security.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_semantic_similarity.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_serve_http.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_skillgen.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_swift_cross_file_calls.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_swift_import_resolution.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_terraform.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_transcribe.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_ts_inheritance.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_validate.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_vue_extraction.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_watch.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_wheel_packaging.py +0 -0
- {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_wiki.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphifyy
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -687,7 +687,9 @@ graphify save-result --question "Q" --answer "A" --nodes Foo Bar --outcome usefu
|
|
|
687
687
|
graphify reflect # aggregate graphify-out/memory/ outcomes into reflections/LESSONS.md
|
|
688
688
|
graphify reflect --if-stale # no-op when LESSONS.md is already newer than every input (cheap to run each session)
|
|
689
689
|
graphify reflect --out docs/LESSONS.md # write the lessons doc somewhere else
|
|
690
|
-
graphify reflect --graph graphify-out/graph.json #
|
|
690
|
+
graphify reflect --graph graphify-out/graph.json # group lessons by community + write the work-memory overlay (.graphify_learning.json)
|
|
691
|
+
# the overlay tags nodes preferred/tentative/contested (recency-weighted, with provenance);
|
|
692
|
+
# graphify explain / query then show a "Lesson:" hint, flagged "code changed — re-verify" when the source moved on
|
|
691
693
|
|
|
692
694
|
graphify uninstall # remove from all platforms in one shot
|
|
693
695
|
graphify uninstall --purge # also delete graphify-out/
|
|
@@ -550,7 +550,9 @@ graphify save-result --question "Q" --answer "A" --nodes Foo Bar --outcome usefu
|
|
|
550
550
|
graphify reflect # aggregate graphify-out/memory/ outcomes into reflections/LESSONS.md
|
|
551
551
|
graphify reflect --if-stale # no-op when LESSONS.md is already newer than every input (cheap to run each session)
|
|
552
552
|
graphify reflect --out docs/LESSONS.md # write the lessons doc somewhere else
|
|
553
|
-
graphify reflect --graph graphify-out/graph.json #
|
|
553
|
+
graphify reflect --graph graphify-out/graph.json # group lessons by community + write the work-memory overlay (.graphify_learning.json)
|
|
554
|
+
# the overlay tags nodes preferred/tentative/contested (recency-weighted, with provenance);
|
|
555
|
+
# graphify explain / query then show a "Lesson:" hint, flagged "code changed — re-verify" when the source moved on
|
|
554
556
|
|
|
555
557
|
graphify uninstall # remove from all platforms in one shot
|
|
556
558
|
graphify uninstall --purge # also delete graphify-out/
|
|
@@ -148,7 +148,41 @@ def _check_skill_version(skill_dst: Path) -> None:
|
|
|
148
148
|
except OSError:
|
|
149
149
|
return
|
|
150
150
|
if installed != __version__:
|
|
151
|
-
|
|
151
|
+
if _version_tuple(installed) > _version_tuple(__version__):
|
|
152
|
+
# The skill on disk is NEWER than the running package. `graphify install`
|
|
153
|
+
# writes the package's OWN (older) bundled skill and re-stamps the version,
|
|
154
|
+
# so following the old "run install" advice would silently DOWNGRADE the
|
|
155
|
+
# skill. The real fix is to upgrade the package (#1568). Common for a stale
|
|
156
|
+
# `uv tool` CLI, or a contributor whose dev checkout stamped a newer skill.
|
|
157
|
+
print(
|
|
158
|
+
f" warning: skill is from graphify {installed}, but the package is "
|
|
159
|
+
f"{__version__} (older). Upgrade the package "
|
|
160
|
+
f"(e.g. 'uv tool upgrade graphifyy' or 'pip install -U graphifyy'); "
|
|
161
|
+
f"running 'graphify install' would downgrade the skill.",
|
|
162
|
+
file=sys.stderr,
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
print(f" warning: skill is from graphify {installed}, package is {__version__}. Run 'graphify install' to update.", file=sys.stderr)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _version_tuple(version: str) -> tuple[int, ...]:
|
|
169
|
+
"""Parse a version string into a comparable integer tuple (``0.9.2`` -> ``(0, 9, 2)``).
|
|
170
|
+
|
|
171
|
+
Reads the leading digits of each dot-segment, so pre/post-release suffixes
|
|
172
|
+
(``1.0.0rc1``) compare by their numeric core. A non-numeric or empty segment
|
|
173
|
+
becomes 0, so a malformed stamp degrades to a conservative comparison rather
|
|
174
|
+
than raising.
|
|
175
|
+
"""
|
|
176
|
+
parts: list[int] = []
|
|
177
|
+
for segment in str(version).split("."):
|
|
178
|
+
digits = ""
|
|
179
|
+
for ch in segment:
|
|
180
|
+
if ch.isdigit():
|
|
181
|
+
digits += ch
|
|
182
|
+
else:
|
|
183
|
+
break
|
|
184
|
+
parts.append(int(digits) if digits else 0)
|
|
185
|
+
return tuple(parts)
|
|
152
186
|
|
|
153
187
|
|
|
154
188
|
def _refresh_all_version_stamps() -> None:
|
|
@@ -3186,6 +3220,30 @@ def main() -> None:
|
|
|
3186
3220
|
)
|
|
3187
3221
|
print(f" Type: {d.get('file_type', '')}")
|
|
3188
3222
|
print(f" Community: {d.get('community_name') or d.get('community', '')}")
|
|
3223
|
+
# Work-memory overlay: a derived experiential hint from `graphify reflect`,
|
|
3224
|
+
# merged in display-only from the .graphify_learning.json sidecar next to
|
|
3225
|
+
# graph.json. No line when the node has no overlay entry.
|
|
3226
|
+
try:
|
|
3227
|
+
from graphify.reflect import load_learning_overlay as _llo
|
|
3228
|
+
from graphify.security import sanitize_label as _sl
|
|
3229
|
+
_overlay = _llo(gp)
|
|
3230
|
+
_entry = _overlay.get(str(nid))
|
|
3231
|
+
if _entry:
|
|
3232
|
+
_status = _sl(str(_entry.get("status", "")))
|
|
3233
|
+
if _status == "contested":
|
|
3234
|
+
_line = (f" Lesson: contested (useful {_entry.get('uses', 0)} / "
|
|
3235
|
+
f"dead-end {_entry.get('neg', 0)})")
|
|
3236
|
+
elif _status == "preferred":
|
|
3237
|
+
_line = (f" Lesson: preferred source (start here) — "
|
|
3238
|
+
f"{_entry.get('uses', 0)} useful, score={_entry.get('score', 0)}")
|
|
3239
|
+
else:
|
|
3240
|
+
_line = (f" Lesson: {_status or 'tentative'} — "
|
|
3241
|
+
f"{_entry.get('uses', 0)} useful, score={_entry.get('score', 0)}")
|
|
3242
|
+
if _entry.get("stale"):
|
|
3243
|
+
_line += " [code changed since — re-verify]"
|
|
3244
|
+
print(_line)
|
|
3245
|
+
except Exception:
|
|
3246
|
+
pass
|
|
3189
3247
|
print(f" Degree: {G.degree(nid)}")
|
|
3190
3248
|
from graphify.build import edge_data
|
|
3191
3249
|
connections: list[tuple[str, str, dict]] = [] # (direction, neighbor_id, edge_data)
|
|
@@ -3501,17 +3559,20 @@ def main() -> None:
|
|
|
3501
3559
|
else:
|
|
3502
3560
|
# No labels file yet (or `graphify label` forced a refresh). When run
|
|
3503
3561
|
# standalone there is no orchestrating agent to do skill.md Step 5, so
|
|
3504
|
-
# auto-name communities
|
|
3505
|
-
|
|
3562
|
+
# auto-name communities rather than leave "Community N" (#1097).
|
|
3563
|
+
from graphify.cluster import label_communities_by_hub
|
|
3506
3564
|
from graphify.llm import generate_community_labels
|
|
3507
3565
|
print("Labeling communities...")
|
|
3508
|
-
#
|
|
3509
|
-
#
|
|
3566
|
+
# Deterministic, LLM-free base labels: name each community after its
|
|
3567
|
+
# highest-degree hub, so the report is readable even with no backend
|
|
3568
|
+
# (previously bare "Community N"). A configured LLM backend overrides these
|
|
3569
|
+
# with richer names below; its no-backend placeholder fallback does NOT.
|
|
3570
|
+
hub_labels = label_communities_by_hub(G, communities)
|
|
3510
3571
|
label_communities_input = communities
|
|
3511
|
-
labels =
|
|
3572
|
+
labels = dict(hub_labels)
|
|
3512
3573
|
if missing_only:
|
|
3513
3574
|
labels = {
|
|
3514
|
-
cid: existing_labels.get(cid,
|
|
3575
|
+
cid: existing_labels.get(cid, hub_labels[cid])
|
|
3515
3576
|
for cid in communities
|
|
3516
3577
|
}
|
|
3517
3578
|
label_communities_input = {
|
|
@@ -3523,16 +3584,24 @@ def main() -> None:
|
|
|
3523
3584
|
G, label_communities_input, backend=label_backend, model=label_model, gods=gods,
|
|
3524
3585
|
max_concurrency=label_max_concurrency, batch_size=label_batch_size,
|
|
3525
3586
|
)
|
|
3526
|
-
|
|
3587
|
+
# Only let the LLM OVERRIDE where it produced a real name — its no-backend
|
|
3588
|
+
# fallback returns "Community {cid}" placeholders, which must not clobber
|
|
3589
|
+
# the deterministic hub labels.
|
|
3590
|
+
labels.update({
|
|
3591
|
+
cid: v for cid, v in generated_labels.items()
|
|
3592
|
+
if v and v != f"Community {cid}"
|
|
3593
|
+
})
|
|
3527
3594
|
stages.mark("label")
|
|
3528
3595
|
questions = suggest_questions(G, communities, labels)
|
|
3529
3596
|
tokens = {"input": 0, "output": 0}
|
|
3530
3597
|
from graphify.export import _git_head as _gh
|
|
3531
3598
|
_commit = _gh()
|
|
3599
|
+
from graphify.report import load_learning_for_report as _llfr
|
|
3532
3600
|
report = generate(G, communities, cohesion, labels, gods, surprises,
|
|
3533
3601
|
{"warning": "cluster-only mode — file stats not available"},
|
|
3534
3602
|
tokens, str(watch_path), suggested_questions=questions,
|
|
3535
|
-
min_community_size=min_community_size, built_at_commit=_commit
|
|
3603
|
+
min_community_size=min_community_size, built_at_commit=_commit,
|
|
3604
|
+
learning=_llfr(out / "graph.json"))
|
|
3536
3605
|
(out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8")
|
|
3537
3606
|
stages.mark("report")
|
|
3538
3607
|
from graphify.export import backup_if_protected as _backup
|
|
@@ -11,6 +11,7 @@ import networkx as nx
|
|
|
11
11
|
|
|
12
12
|
DEFAULT_AFFECTED_RELATIONS = (
|
|
13
13
|
"calls",
|
|
14
|
+
"indirect_call",
|
|
14
15
|
"references",
|
|
15
16
|
"imports",
|
|
16
17
|
"imports_from",
|
|
@@ -209,7 +210,13 @@ def load_graph(path: Path) -> nx.Graph:
|
|
|
209
210
|
import json
|
|
210
211
|
from networkx.readwrite import json_graph
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
try:
|
|
214
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
215
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
216
|
+
raise RuntimeError(
|
|
217
|
+
f"Cannot read graph file {path}: {exc}. "
|
|
218
|
+
"Re-run 'graphify extract' to regenerate it."
|
|
219
|
+
) from exc
|
|
213
220
|
# Force directed so stored caller→callee direction survives the round-trip;
|
|
214
221
|
# mirrors serve.py and __main__.py (#1174).
|
|
215
222
|
raw = {**raw, "directed": True}
|
|
@@ -53,6 +53,56 @@ _FILE_TYPE_SYNONYMS = {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
# Hyperedge member lists are canonically keyed `nodes` (see graphify/llm.py
|
|
57
|
+
# extraction spec), but LLM/subagent drift and externally-supplied graph.json
|
|
58
|
+
# sometimes emit `members` or `node_ids`. _normalize_hyperedge_members folds
|
|
59
|
+
# those aliases into `nodes` at ingest so every downstream consumer reads one
|
|
60
|
+
# canonical key — mirroring the `from`/`to` edge-endpoint tolerance below.
|
|
61
|
+
_HE_MEMBER_ALIASES = ("members", "node_ids")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _normalize_hyperedge_members(he: object) -> None:
|
|
65
|
+
"""Canonicalize a hyperedge's member list onto the `nodes` key, in place.
|
|
66
|
+
|
|
67
|
+
If `nodes` is already a list it wins (canonical), and only stray alias keys
|
|
68
|
+
are dropped. Otherwise the first alias (`members`, then `node_ids`) that is a
|
|
69
|
+
list is moved to `nodes`, deduped preserving order, with a single stderr
|
|
70
|
+
WARNING naming the hyperedge id and alias used. Leftover alias keys are
|
|
71
|
+
always removed so downstream code never re-reads them.
|
|
72
|
+
"""
|
|
73
|
+
if not isinstance(he, dict):
|
|
74
|
+
return
|
|
75
|
+
if not isinstance(he.get("nodes"), list):
|
|
76
|
+
for alias in _HE_MEMBER_ALIASES:
|
|
77
|
+
val = he.get(alias)
|
|
78
|
+
if isinstance(val, list):
|
|
79
|
+
seen: set = set()
|
|
80
|
+
deduped: list = []
|
|
81
|
+
for ref in val:
|
|
82
|
+
try:
|
|
83
|
+
is_dupe = ref in seen
|
|
84
|
+
except TypeError:
|
|
85
|
+
is_dupe = False # unhashable ref: keep it, validator flags it
|
|
86
|
+
if is_dupe:
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
seen.add(ref)
|
|
90
|
+
except TypeError:
|
|
91
|
+
pass
|
|
92
|
+
deduped.append(ref)
|
|
93
|
+
he["nodes"] = deduped
|
|
94
|
+
print(
|
|
95
|
+
f"[graphify] WARNING: hyperedge "
|
|
96
|
+
f"'{he.get('id', '?')}' uses field '{alias}' instead of "
|
|
97
|
+
f"'nodes'; normalizing.",
|
|
98
|
+
file=sys.stderr,
|
|
99
|
+
)
|
|
100
|
+
break
|
|
101
|
+
# Drop any leftover alias keys regardless of which branch ran above.
|
|
102
|
+
for alias in _HE_MEMBER_ALIASES:
|
|
103
|
+
he.pop(alias, None)
|
|
104
|
+
|
|
105
|
+
|
|
56
106
|
def _norm_source_file(p: str | None, root: str | None = None) -> str | None:
|
|
57
107
|
"""Normalize path separators and relativize absolute paths.
|
|
58
108
|
|
|
@@ -67,10 +117,43 @@ def _norm_source_file(p: str | None, root: str | None = None) -> str | None:
|
|
|
67
117
|
try:
|
|
68
118
|
p = Path(p).relative_to(root).as_posix()
|
|
69
119
|
except ValueError:
|
|
70
|
-
|
|
120
|
+
# Lexical relative_to failed. Retry with both sides fully resolved:
|
|
121
|
+
# a symlinked scan root (macOS /var -> /private/var, or a symlinked
|
|
122
|
+
# home/worktree) makes the raw prefixes differ even though they point
|
|
123
|
+
# at the same dir, which otherwise silently defeats prune/replace
|
|
124
|
+
# matching. Only the slow path resolves, so the common lexical match
|
|
125
|
+
# stays filesystem-free.
|
|
126
|
+
try:
|
|
127
|
+
p = Path(p).resolve().relative_to(Path(root).resolve()).as_posix()
|
|
128
|
+
except (ValueError, OSError):
|
|
129
|
+
pass
|
|
71
130
|
return p
|
|
72
131
|
|
|
73
132
|
|
|
133
|
+
def _infer_merge_root(graph_path: Path) -> str | None:
|
|
134
|
+
"""Best-effort scan root for relativizing paths in build_merge when the caller
|
|
135
|
+
passes no ``root`` (#1571).
|
|
136
|
+
|
|
137
|
+
Prefers the committed ``graphify-out/.graphify_root`` marker — the authoritative
|
|
138
|
+
scan root graphify records at build/watch time (#686/#1423) — then falls back to
|
|
139
|
+
the directory that contains the output dir (``graph.json``'s grandparent, i.e.
|
|
140
|
+
``<root>/graphify-out/graph.json`` -> ``<root>``). Returns None if neither
|
|
141
|
+
resolves, in which case normalization is a no-op (prior behavior).
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
marker = graph_path.parent / ".graphify_root"
|
|
145
|
+
if marker.exists():
|
|
146
|
+
recorded = marker.read_text(encoding="utf-8").strip()
|
|
147
|
+
if recorded:
|
|
148
|
+
return str(Path(recorded).resolve())
|
|
149
|
+
except OSError:
|
|
150
|
+
pass
|
|
151
|
+
try:
|
|
152
|
+
return str(graph_path.parent.parent.resolve())
|
|
153
|
+
except Exception:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
74
157
|
def edge_data(G: nx.Graph, u: str, v: str) -> dict:
|
|
75
158
|
"""Return one edge attribute dict for (u, v), tolerating MultiGraph.
|
|
76
159
|
|
|
@@ -279,6 +362,14 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
|
|
|
279
362
|
if ft and ft not in {"code", "document", "paper", "image", "rationale", "concept"}:
|
|
280
363
|
node["file_type"] = _FILE_TYPE_SYNONYMS.get(ft, "concept")
|
|
281
364
|
|
|
365
|
+
# Canonicalize hyperedge member lists (#1561): producers sometimes key the
|
|
366
|
+
# member list `members`/`node_ids` instead of `nodes`. Fold aliases onto
|
|
367
|
+
# `nodes` here — BEFORE validation and the semantic-rekey loop below — so
|
|
368
|
+
# every downstream consumer (rekey, source_file relativize, to_json) reads
|
|
369
|
+
# one canonical key, the same way edge endpoints alias from/to at build.
|
|
370
|
+
for he in extraction.get("hyperedges", []) or []:
|
|
371
|
+
_normalize_hyperedge_members(he)
|
|
372
|
+
|
|
282
373
|
errors = validate_extraction(extraction)
|
|
283
374
|
# Dangling edges (stdlib/external imports) are expected - only warn about real schema errors.
|
|
284
375
|
real_errors = [e for e in errors if "does not match any node id" not in e]
|
|
@@ -489,8 +580,15 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
|
|
|
489
580
|
".ts": "js", ".tsx": "js",
|
|
490
581
|
".go": "go", ".rs": "rs",
|
|
491
582
|
".java": "jvm", ".kt": "jvm", ".scala": "jvm", ".groovy": "jvm",
|
|
492
|
-
|
|
493
|
-
|
|
583
|
+
# C, C++, and ObjC interoperate within one compilation unit: a method
|
|
584
|
+
# declared in a shared `.h` is defined/called from a `.c`/`.cpp`/`.m`
|
|
585
|
+
# sibling, so a cross-file INFERRED call from impl to its header decl
|
|
586
|
+
# is legitimate, not a phantom name-collision across languages. Treat
|
|
587
|
+
# the whole C family as one so the receiver-typed C++/ObjC member-call
|
|
588
|
+
# resolvers' header-targeting edges survive build (#1547/#1556).
|
|
589
|
+
".c": "c", ".h": "c", ".cc": "c", ".cpp": "c", ".hpp": "c",
|
|
590
|
+
".cxx": "c", ".hh": "c", ".hxx": "c",
|
|
591
|
+
".cu": "c", ".cuh": "c", ".metal": "c", ".m": "c", ".mm": "c",
|
|
494
592
|
".rb": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua",
|
|
495
593
|
}
|
|
496
594
|
src_ext = Path(G.nodes[src].get("source_file") or "").suffix.lower()
|
|
@@ -577,7 +675,15 @@ def deduplicate_by_label(nodes: list[dict], edges: list[dict]) -> tuple[list[dic
|
|
|
577
675
|
"""Merge nodes that share a normalised label, rewriting edge references.
|
|
578
676
|
|
|
579
677
|
Prefers IDs without chunk suffixes (_c\\d+) and shorter IDs when tied.
|
|
580
|
-
Drops self-loops created by the merge.
|
|
678
|
+
Drops self-loops created by the merge.
|
|
679
|
+
|
|
680
|
+
Dormant: this is NOT wired into ``build()`` — the active dedup path is
|
|
681
|
+
``deduplicate_entities`` (imported and called in ``build``), which supersedes
|
|
682
|
+
it. The previous "Called in build() automatically" note was never true. It
|
|
683
|
+
also merges by label alone with no ``file_type`` guard, so it must not be
|
|
684
|
+
enabled for code nodes: same-label symbols from different files/packages
|
|
685
|
+
(e.g. two ``Account`` types) would collapse into one — the cross-file
|
|
686
|
+
conflation ``deduplicate_entities`` deliberately avoids for code (#1205).
|
|
581
687
|
"""
|
|
582
688
|
_CHUNK_SUFFIX = re.compile(r"_c\d+$")
|
|
583
689
|
canonical: dict[str, dict] = {} # norm_label -> surviving node
|
|
@@ -649,16 +755,36 @@ def build_merge(
|
|
|
649
755
|
# NetworkX round-trip loses direction permanently (#760).
|
|
650
756
|
from graphify.security import check_graph_file_size_cap
|
|
651
757
|
check_graph_file_size_cap(graph_path)
|
|
652
|
-
|
|
758
|
+
try:
|
|
759
|
+
data = json.loads(graph_path.read_text(encoding="utf-8"))
|
|
760
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
761
|
+
raise RuntimeError(
|
|
762
|
+
f"Cannot read {graph_path} for incremental merge: {exc}. "
|
|
763
|
+
"Delete the file and run a full rebuild."
|
|
764
|
+
) from exc
|
|
653
765
|
links_key = "links" if "links" in data else "edges"
|
|
654
766
|
existing_nodes = list(data.get("nodes", []))
|
|
655
767
|
existing_edges = list(data.get(links_key, []))
|
|
768
|
+
existing_hyperedges = list(data.get("hyperedges", []))
|
|
656
769
|
had_graph = True
|
|
657
770
|
else:
|
|
658
771
|
existing_nodes = []
|
|
659
772
|
existing_edges = []
|
|
773
|
+
existing_hyperedges = []
|
|
660
774
|
had_graph = False
|
|
661
775
|
|
|
776
|
+
# Effective root for relativizing absolute source_file / prune paths back to the
|
|
777
|
+
# stored relative source_file keys. When the caller passes root we use it;
|
|
778
|
+
# otherwise fall back to the graph's recorded scan root, so absolute
|
|
779
|
+
# prune_sources and new-chunk paths still match even when a caller omits root
|
|
780
|
+
# (#1571 — the skill's --update runbook calls build_merge without root, so
|
|
781
|
+
# absolute deleted-file paths never matched the relative node keys and their
|
|
782
|
+
# nodes survived as ghosts).
|
|
783
|
+
_eff_root = (
|
|
784
|
+
str(Path(root).resolve()) if root is not None
|
|
785
|
+
else _infer_merge_root(graph_path)
|
|
786
|
+
)
|
|
787
|
+
|
|
662
788
|
# Re-extracted files REPLACE their prior contribution. Every source_file
|
|
663
789
|
# present in new_chunks is dropped from the loaded base before merging, so a
|
|
664
790
|
# CHANGED file's stale nodes/edges don't accumulate across incremental
|
|
@@ -668,7 +794,7 @@ def build_merge(
|
|
|
668
794
|
# for them; genuinely deleted files are still handled via prune_sources.
|
|
669
795
|
# Matched in both raw and _norm_source_file form because new_chunks may carry
|
|
670
796
|
# absolute win32 paths while the stored graph keeps relative posix (#1007).
|
|
671
|
-
_replace_root =
|
|
797
|
+
_replace_root = _eff_root
|
|
672
798
|
new_sources: set[str] = set()
|
|
673
799
|
for ch in new_chunks:
|
|
674
800
|
for n in ch.get("nodes", []):
|
|
@@ -691,23 +817,46 @@ def build_merge(
|
|
|
691
817
|
all_chunks = base + list(new_chunks)
|
|
692
818
|
G = build(all_chunks, directed=directed, dedup=dedup, dedup_llm_backend=dedup_llm_backend, root=root)
|
|
693
819
|
|
|
820
|
+
# Prune set for deleted source files — both the raw form (matches nodes that
|
|
821
|
+
# kept absolute source_file) and the normalised relative form (matches nodes
|
|
822
|
+
# relativised by _norm_source_file at build time). .resolve() (via _eff_root)
|
|
823
|
+
# handles symlinked roots and ".." / "./" segments so Path.relative_to()
|
|
824
|
+
# succeeds even when the scan root is a symlink. (#1007, #1571)
|
|
825
|
+
prune_set: set[str] = set()
|
|
826
|
+
for p in (prune_sources or []):
|
|
827
|
+
if not p:
|
|
828
|
+
continue
|
|
829
|
+
prune_set.add(p)
|
|
830
|
+
norm = _norm_source_file(p, _eff_root)
|
|
831
|
+
if norm:
|
|
832
|
+
prune_set.add(norm)
|
|
833
|
+
|
|
834
|
+
# Carry forward hyperedges from files that were neither re-extracted nor
|
|
835
|
+
# deleted (#1574). build() only sees the new chunks' hyperedges, so without
|
|
836
|
+
# this every --update collapses the graph's hyperedge set down to just the
|
|
837
|
+
# changed files'. Re-extracted files' prior hyperedges are dropped (their new
|
|
838
|
+
# version is already in G — replace-per-source, like nodes/edges); deleted
|
|
839
|
+
# files' are dropped via prune_set. id-dedup (attach_hyperedges) so a carried
|
|
840
|
+
# hyperedge never duplicates one the new chunks re-emitted. Mirrors watch.py,
|
|
841
|
+
# which already preserves existing hyperedges across a rebuild.
|
|
842
|
+
if existing_hyperedges:
|
|
843
|
+
carried = []
|
|
844
|
+
for he in existing_hyperedges:
|
|
845
|
+
if not isinstance(he, dict):
|
|
846
|
+
continue
|
|
847
|
+
sf = he.get("source_file")
|
|
848
|
+
norm = _norm_source_file(sf, _eff_root)
|
|
849
|
+
if sf in new_sources or norm in new_sources:
|
|
850
|
+
continue # re-extracted — replaced by the new chunk's version
|
|
851
|
+
if sf in prune_set or norm in prune_set:
|
|
852
|
+
continue # deleted — pruned
|
|
853
|
+
carried.append(he)
|
|
854
|
+
if carried:
|
|
855
|
+
from graphify.export import attach_hyperedges
|
|
856
|
+
attach_hyperedges(G, carried)
|
|
857
|
+
|
|
694
858
|
# Prune nodes and edges from deleted source files
|
|
695
859
|
if prune_sources:
|
|
696
|
-
# Build a set containing both the raw form (matches nodes that kept
|
|
697
|
-
# absolute source_file) and the normalised relative form (matches nodes
|
|
698
|
-
# that were relativised by _norm_source_file at build time).
|
|
699
|
-
# .resolve() handles symlinked roots and redundant ".." / "./" segments
|
|
700
|
-
# so Path.relative_to() succeeds even when the scan root is a symlink.
|
|
701
|
-
# (#1007: manifest absolute paths vs graph relative source_file mismatch)
|
|
702
|
-
_root_str = str(Path(root).resolve()) if root is not None else None
|
|
703
|
-
prune_set: set[str] = set()
|
|
704
|
-
for p in prune_sources:
|
|
705
|
-
if not p:
|
|
706
|
-
continue
|
|
707
|
-
prune_set.add(p)
|
|
708
|
-
norm = _norm_source_file(p, _root_str)
|
|
709
|
-
if norm:
|
|
710
|
-
prune_set.add(norm)
|
|
711
860
|
to_remove = [
|
|
712
861
|
n for n, d in G.nodes(data=True)
|
|
713
862
|
if d.get("source_file") in prune_set
|
|
@@ -83,6 +83,33 @@ _COHESION_SPLIT_THRESHOLD = 0.05 # re-split communities with cohesion below this
|
|
|
83
83
|
_COHESION_SPLIT_MIN_SIZE = 50 # only cohesion-split if community has at least this many nodes
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def label_communities_by_hub(
|
|
87
|
+
G: nx.Graph, communities: dict[int, list[str]]
|
|
88
|
+
) -> dict[int, str]:
|
|
89
|
+
"""Deterministic, LLM-free community labels: name each community after its
|
|
90
|
+
highest-degree member — the structural hub — so a report reads ``auth`` /
|
|
91
|
+
``log_action`` instead of ``Community 70``. Degree is measured on the full graph
|
|
92
|
+
``G``; ties break by node id for run-to-run stability. A community whose members
|
|
93
|
+
are all absent from ``G`` falls back to ``Community {cid}``.
|
|
94
|
+
|
|
95
|
+
Used as the default (no-backend) labeler; an LLM naming pass, when configured,
|
|
96
|
+
overrides these with richer names.
|
|
97
|
+
"""
|
|
98
|
+
labels: dict[int, str] = {}
|
|
99
|
+
for cid, members in communities.items():
|
|
100
|
+
present = [n for n in members if n in G]
|
|
101
|
+
if not present:
|
|
102
|
+
labels[cid] = f"Community {cid}"
|
|
103
|
+
continue
|
|
104
|
+
# highest degree wins; ties broken by node id (ascending) for determinism
|
|
105
|
+
hub = min(present, key=lambda n: (-G.degree(n), str(n)))
|
|
106
|
+
name = str(G.nodes[hub].get("label") or hub).strip()
|
|
107
|
+
if name.endswith("()"):
|
|
108
|
+
name = name[:-2]
|
|
109
|
+
labels[cid] = name or f"Community {cid}"
|
|
110
|
+
return labels
|
|
111
|
+
|
|
112
|
+
|
|
86
113
|
def cluster(
|
|
87
114
|
G: nx.Graph,
|
|
88
115
|
resolution: float = 1.0,
|
|
@@ -6,6 +6,7 @@ Jaro-Winkler verification → same-community boost → union-find merge.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
import math
|
|
8
8
|
import re
|
|
9
|
+
import sys
|
|
9
10
|
import unicodedata
|
|
10
11
|
from collections import defaultdict
|
|
11
12
|
|
|
@@ -219,12 +220,29 @@ def deduplicate_entities(
|
|
|
219
220
|
if len(nodes) <= 1:
|
|
220
221
|
return nodes, edges
|
|
221
222
|
|
|
222
|
-
# Pre-deduplicate: keep first occurrence of each id
|
|
223
|
+
# Pre-deduplicate: keep first occurrence of each id.
|
|
224
|
+
# Warn when two nodes share an ID but originate from different source files —
|
|
225
|
+
# this indicates a cross-chunk ID collision (#1504) where silent data loss occurs.
|
|
223
226
|
seen_ids: dict[str, dict] = {}
|
|
224
227
|
for node in nodes:
|
|
225
228
|
nid = node.get("id", "")
|
|
226
|
-
if
|
|
229
|
+
if not nid:
|
|
230
|
+
continue
|
|
231
|
+
if nid not in seen_ids:
|
|
227
232
|
seen_ids[nid] = node
|
|
233
|
+
else:
|
|
234
|
+
existing_sf = seen_ids[nid].get("source_file") or ""
|
|
235
|
+
new_sf = node.get("source_file") or ""
|
|
236
|
+
if existing_sf != new_sf:
|
|
237
|
+
print(
|
|
238
|
+
f"[graphify] WARNING: node '{nid}' from '{new_sf}' collides with "
|
|
239
|
+
f"node from '{existing_sf}' — the second node will be dropped. "
|
|
240
|
+
f"This is a cross-chunk ID collision caused by two files with the "
|
|
241
|
+
f"same name in different directories. To avoid data loss, run "
|
|
242
|
+
f"'graphify extract' per subfolder and merge with "
|
|
243
|
+
f"'graphify merge-graphs'.",
|
|
244
|
+
file=sys.stderr,
|
|
245
|
+
)
|
|
228
246
|
unique_nodes = list(seen_ids.values())
|
|
229
247
|
|
|
230
248
|
if len(unique_nodes) <= 1:
|
|
@@ -274,7 +274,13 @@ def _read_json_file(path: str | Path) -> dict[str, Any]:
|
|
|
274
274
|
|
|
275
275
|
json_path = Path(path)
|
|
276
276
|
check_graph_file_size_cap(json_path)
|
|
277
|
-
|
|
277
|
+
try:
|
|
278
|
+
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
279
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
280
|
+
raise RuntimeError(
|
|
281
|
+
f"Cannot parse {json_path}: {exc}. "
|
|
282
|
+
"The file may be corrupted — re-run 'graphify extract'."
|
|
283
|
+
) from exc
|
|
278
284
|
if not isinstance(data, dict):
|
|
279
285
|
raise ValueError("diagnostic input must be a JSON object")
|
|
280
286
|
return data
|
|
@@ -636,6 +636,7 @@ def to_html(
|
|
|
636
636
|
community_labels: dict[int, str] | None = None,
|
|
637
637
|
member_counts: dict[int, int] | None = None,
|
|
638
638
|
node_limit: int | None = None,
|
|
639
|
+
learning_overlay: dict | None = None,
|
|
639
640
|
) -> None:
|
|
640
641
|
"""Generate an interactive vis.js HTML visualization of the graph.
|
|
641
642
|
|
|
@@ -678,7 +679,7 @@ def to_html(
|
|
|
678
679
|
if raw_hyperedges:
|
|
679
680
|
remapped = []
|
|
680
681
|
for he in raw_hyperedges:
|
|
681
|
-
he_members = he.get("nodes"
|
|
682
|
+
he_members = he.get("nodes", [])
|
|
682
683
|
comm_ids, seen = [], set()
|
|
683
684
|
for nid in he_members:
|
|
684
685
|
c = node_to_community.get(nid)
|
|
@@ -713,6 +714,21 @@ def to_html(
|
|
|
713
714
|
max_deg = max(degree.values(), default=1) or 1
|
|
714
715
|
max_mc = (max(member_counts.values(), default=1) or 1) if member_counts else 1
|
|
715
716
|
|
|
717
|
+
# Work-memory overlay (derived sidecar). When not passed explicitly, load it
|
|
718
|
+
# best-effort from the sibling .graphify_learning.json next to the output
|
|
719
|
+
# graph.html (which lives beside graph.json). Empty/missing => no learning
|
|
720
|
+
# fields, so the un-annotated render is byte-identical to pre-feature.
|
|
721
|
+
if learning_overlay is None:
|
|
722
|
+
learning_overlay = {}
|
|
723
|
+
try:
|
|
724
|
+
from graphify.reflect import load_learning_overlay as _llo
|
|
725
|
+
learning_overlay = _llo(Path(output_path))
|
|
726
|
+
except Exception:
|
|
727
|
+
learning_overlay = {}
|
|
728
|
+
# Status -> ring color. preferred=green, contested=amber. Tentative gets no
|
|
729
|
+
# ring (it's not yet trustworthy enough to highlight in the map).
|
|
730
|
+
_RING = {"preferred": "#22c55e", "contested": "#f59e0b"}
|
|
731
|
+
|
|
716
732
|
# Build nodes list for vis.js
|
|
717
733
|
vis_nodes = []
|
|
718
734
|
for node_id, data in G.nodes(data=True):
|
|
@@ -728,7 +744,7 @@ def to_html(
|
|
|
728
744
|
size = 10 + 30 * (deg / max_deg)
|
|
729
745
|
# Only show label for high-degree nodes by default; others show on hover
|
|
730
746
|
font_size = 12 if deg >= max_deg * 0.15 else 0
|
|
731
|
-
|
|
747
|
+
node = {
|
|
732
748
|
"id": node_id,
|
|
733
749
|
"label": label,
|
|
734
750
|
"color": {"background": color, "border": color, "highlight": {"background": "#ffffff", "border": color}},
|
|
@@ -740,7 +756,38 @@ def to_html(
|
|
|
740
756
|
"source_file": sanitize_label(str(data.get("source_file") or "")),
|
|
741
757
|
"file_type": data.get("file_type", ""),
|
|
742
758
|
"degree": deg,
|
|
743
|
-
}
|
|
759
|
+
}
|
|
760
|
+
# Conditional learning fields — only present for annotated nodes, so
|
|
761
|
+
# un-annotated output keeps the exact pre-feature node dict shape.
|
|
762
|
+
entry = learning_overlay.get(str(node_id)) if learning_overlay else None
|
|
763
|
+
if entry:
|
|
764
|
+
status = sanitize_label(str(entry.get("status", "")))
|
|
765
|
+
stale = bool(entry.get("stale"))
|
|
766
|
+
node["learning_status"] = status
|
|
767
|
+
node["learning_stale"] = stale
|
|
768
|
+
ring = _RING.get(status)
|
|
769
|
+
if ring:
|
|
770
|
+
# Status-colored ring via the border; stale => desaturated +
|
|
771
|
+
# dashed (vis.js supports per-node `shapeProperties.borderDashes`).
|
|
772
|
+
if stale:
|
|
773
|
+
ring = "#9ca3af"
|
|
774
|
+
node["shapeProperties"] = {"borderDashes": [4, 4]}
|
|
775
|
+
node["borderWidth"] = 3
|
|
776
|
+
node["color"] = {
|
|
777
|
+
"background": color, "border": ring,
|
|
778
|
+
"highlight": {"background": "#ffffff", "border": ring},
|
|
779
|
+
}
|
|
780
|
+
# Lesson line appended to the hover title.
|
|
781
|
+
if status == "contested":
|
|
782
|
+
lesson = f"Lesson: contested (useful {entry.get('uses', 0)} / dead-end {entry.get('neg', 0)})"
|
|
783
|
+
elif status == "preferred":
|
|
784
|
+
lesson = f"Lesson: preferred source ({entry.get('uses', 0)} useful, score={entry.get('score', 0)})"
|
|
785
|
+
else:
|
|
786
|
+
lesson = f"Lesson: {status} ({entry.get('uses', 0)} useful)"
|
|
787
|
+
if stale:
|
|
788
|
+
lesson += " [code changed — re-verify]"
|
|
789
|
+
node["title"] = _html.escape(label) + "\n" + _html.escape(sanitize_label(lesson))
|
|
790
|
+
vis_nodes.append(node)
|
|
744
791
|
|
|
745
792
|
# Build edges list. Restore original edge direction from _src/_tgt
|
|
746
793
|
# (stashed by build.py for exactly this reason): undirected NetworkX
|