Skip to content
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
worked/**/*.html linguist-vendored=true
graphify-out/**/*.html linguist-vendored=true
*.html linguist-detectable=false
graphify-out/graph.json merge=graphify
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,44 @@ What you get out of the box:
| **Cross-file links** | `calls` / `imports` / `inherits` / `mixes_in` resolved across ~40 languages via tree-sitter AST |
| **Query, path, explain** | Ask a question, trace the path between two things, or explain one concept, all against `graph.json` |
| **Rationale + doc refs** | `# NOTE:` / `# WHY:` comments and ADR/RFC citations become first-class nodes linked to the code |
| **Validated knowledge** | `lat.md/` sections, summaries, wiki links, source links, and `@lat` implementation references join the code graph deterministically |
| **Beyond code** | Docs, PDFs, images, and video/audio all map into the same graph |
| **Local-first** | Code is parsed locally with tree-sitter (no LLM, nothing leaves your machine); only the semantic pass over docs/media calls a backend, and only if you configure one |

---

## Validated knowledge with lat.md

Graphify automatically recognizes Markdown files inside a project-level
`lat.md/` directory. Each heading becomes a stable `knowledge_section` node,
the first paragraph becomes its searchable summary, and wiki links connect the
curated knowledge to other sections or source files. Explicit implementation
comments connect knowledge back to code:

```python
# @lat: [[security#Security#Tenant isolation]]
def load_orders(tenant_id):
...
```

Run the normal update command to include the lattice in `graph.json`, then
validate referential integrity separately when needed:

```bash
graphify update .
graphify check-knowledge .
graphify check-knowledge . --json
graphify query "tenant isolation constraints"
```

`check-knowledge` reports broken or ambiguous wiki links, missing source files,
and leaf sections marked `require-code-mention: true` that have no matching
`@lat` reference. The supported format is compatible with the public lat.md
heading, wiki-link, source-reference, and code-mention conventions while
remaining fully local and deterministic.

---

## Benchmarks

| Benchmark | Metric | graphify | Field |
Expand Down Expand Up @@ -213,6 +246,7 @@ for example `graphify claude install --project` or `graphify codex install --pro
| Claude Code (Windows) | `graphify install` (auto-detected) or `graphify install --platform windows` |
| CodeBuddy | `graphify install --platform codebuddy` |
| Codex | `graphify install --platform codex` |
| Jcode | `graphify jcode install` |
| OpenCode | `graphify install --platform opencode` |
| Kilo Code | `graphify install --platform kilo` |
| GitHub Copilot CLI | `graphify install --platform copilot` |
Expand Down
79 changes: 79 additions & 0 deletions docs/testing/validated-knowledge-ingestion.tdd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Validated knowledge ingestion TDD evidence

## Source

The user approved implementation of the recommended Graphify and Jcode validated
knowledge plan. This document covers the first Graphify milestone: native
`lat.md` ingestion, integrity checking, implementation linkage, and retrieval.

## User journeys

1. As an agent, I can query curated design constraints together with source-code
symbols so that I do not miss project invariants before editing code.
2. As a maintainer, I can validate wiki links, source links, and required
implementation mentions so that project knowledge does not silently drift.
3. As a project adopting lat.md, I can use Graphify's normal update workflow
without running a second retrieval service.

## Requirement-to-check evidence

| # | Guarantee | Test or command | Type | Result |
|---|---|---|---|---|
| 1 | Headings become stable knowledge sections with first-paragraph summaries | `test_lattice_markdown_emits_stable_sections_summaries_and_wiki_edges` | Unit | PASS |
| 2 | Inline and fenced examples are ignored while source links become `documents` edges | `test_lattice_ignores_example_links_and_emits_source_documentation_edges` | Integration | PASS |
| 3 | `@lat` comments connect a knowledge section to its implementation file | `test_full_extract_links_at_lat_comment_to_knowledge_section` | Integration | PASS |
| 4 | Cross-file shorthand wiki links resolve to full stable section IDs | `test_full_extract_resolves_cross_file_short_wiki_reference` | Integration | PASS |
| 5 | Broken, ambiguous, and missing implementation references are diagnosed | `test_validate_lattice_reports_broken_ambiguous_and_unimplemented_required_sections` | Unit | PASS |
| 6 | Invalid lattices return structured JSON and exit code 1 | `test_check_knowledge_cli_returns_json_and_nonzero_for_invalid_lattice` | CLI acceptance | PASS |
| 7 | Query scoring searches summaries and returns the summary in bounded output | `test_query_retrieves_lattice_summary_after_normal_extraction` | Public retrieval | PASS |
| 8 | A real valid lattice is accepted through the public CLI | `uv run python -m graphify check-knowledge /home/sergey/.jcode/scratch/graphify-lattice-valid --json` | CLI acceptance | PASS, 2 sections, 0 errors |
| 9 | The implementation is compatible with the official lat.md repository | `validate_lattice(Path('/home/sergey/.jcode/scratch/lat.md-official'))` | Real integration | PASS, 24 files, 192 sections, 0 errors |
| 10 | Dotted lattice filenames remain knowledge references rather than source paths | `test_dotted_lattice_file_reference_is_not_misclassified_as_source` | Integration | PASS |
| 11 | Incremental lattice updates rediscover mentions in unchanged source files | `test_lattice_change_rescans_unchanged_source_mentions` | Incremental integration | PASS |
| 12 | Source links cannot escape the project root | `test_source_reference_cannot_escape_project_root` | Security | PASS |
| 13 | Code-mention validation honors Graphify ignore rules | `test_validation_respects_graphifyignore_when_scanning_code_mentions` | Integration | PASS |
| 14 | Adjacent extraction, CLI, query, and security behavior remains intact | focused regression command below | Regression | PASS, 460 tests |

## RED evidence

1. `uv run pytest -q tests/test_lattice_ingest.py` failed during collection with
`ModuleNotFoundError: graphify.lattice_ingest` before production code existed.
2. After the first minimal implementation, the strengthened tests failed because
cross-file shorthand links were pruned and summary-only queries returned
`No matching nodes found.`
3. The source-link compatibility test failed because no `documents` edge was
emitted before Markdown-aware parsing was implemented.
4. Independent review added four regressions which initially failed: incremental
`@lat` rediscovery, dotted lattice filenames, source-root containment, and
ignore-aware validation scanning.

Checkpoint commits:

- `79fe28b` specifies the initial missing behavior.
- `cee64f3` specifies cross-file resolution and public retrieval behavior.

## GREEN evidence

- `uv run pytest -q tests/test_lattice_ingest.py`: 11 passed.
- `uv run pytest -q tests/test_lattice_ingest.py tests/test_manifest_ingest.py tests/test_languages.py tests/test_cli_export.py tests/test_query_cli.py tests/test_security.py`: 460 passed.
- `env -u DEEPSEEK_API_KEY -u DEEPSEEK_BASE_URL uv run pytest -q --ignore=tests/test_falkordb_integration.py`: 4,157 passed, 1 skipped. The excluded integration requires a FalkorDB server with the `GRAPH.QUERY` module; the available localhost service was plain Redis.
- `uv run ruff check graphify/lattice_ingest.py graphify/extract.py graphify/serve.py graphify/cli.py graphify/__main__.py tests/test_lattice_ingest.py`: passed.
- GREEN implementation checkpoint: `5dcee5a`.

## Coverage

`uv run pytest -q tests/test_lattice_ingest.py --cov=graphify.lattice_ingest --cov-report=term-missing --cov-fail-under=80`

The initial seven-test milestone reached 89% statement coverage. The final
eleven-test suite adds independent-review coverage for incremental, security,
ignore, and dotted-filename edge cases.

## Known boundaries

- Graphify validates that referenced source files exist. Symbol-level source-link
validation is deferred to the next milestone, where links will resolve against
Graphify's language-aware symbol index.
- This milestone does not add a separate lat.md semantic index or service.
Curated summaries participate directly in Graphify's existing query scorer.
- Jcode changes are intentionally deferred until the Graphify public graph and
CLI contract is committed and stable.
6 changes: 5 additions & 1 deletion graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
_install_codex_hook,
_install_gemini_hook,
_install_kilo_plugin,
_install_jcode_hook,
_install_opencode_plugin,
_install_skill_references,
_kilo_config_path,
Expand Down Expand Up @@ -85,6 +86,7 @@
_uninstall_codex_hook,
_uninstall_gemini_hook,
_uninstall_kilo_plugin,
_uninstall_jcode_hook,
_uninstall_opencode_plugin,
claude_install,
claude_uninstall,
Expand Down Expand Up @@ -507,7 +509,7 @@ def _run_cli() -> None:
print("Usage: graphify <command>")
print()
print("Commands:")
print(" install [--platform P] copy skill to platform config dir (claude|windows|codebuddy|codex|opencode|aider|amp|agents|claw|droid|trae|trae-cn|gemini|cursor|antigravity|hermes|kiro|pi|devin)")
print(" install [--platform P] copy skill to platform config dir (claude|windows|codebuddy|codex|jcode|opencode|aider|amp|agents|claw|droid|trae|trae-cn|gemini|cursor|antigravity|hermes|kiro|pi|devin)")
print(" uninstall remove graphify from all detected platforms in one shot")
print(" --purge also delete graphify-out/ directory")
print(" path \"A\" \"B\" shortest path between two nodes in graph.json")
Expand Down Expand Up @@ -539,6 +541,8 @@ def _run_cli() -> None:
print(" --force overwrite graph.json even if the rebuild has fewer nodes")
print(" (also: GRAPHIFY_FORCE=1 env var; use after refactors that delete code)")
print(" --no-cluster skip clustering, write raw extraction only")
print(" check-knowledge <path> validate lat.md wiki links and @lat code references")
print(" --json emit machine-readable validation results")
print(" cluster-only <path> rerun clustering on an existing graph.json and regenerate report")
print(" --no-viz skip graph.html generation (useful for >5000 node graphs / CI)")
print(" --graph <path> path to graph.json (default <path>/graphify-out/graph.json)")
Expand Down
84 changes: 84 additions & 0 deletions graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,52 @@ def _run_hook_guard(kind: str, strict: bool = False) -> None:
pass


def _run_jcode_hook_guard() -> bool:
"""Redirect Jcode's first raw code lookup to Graphify.

Jcode sends the tool input JSON on stdin, exports metadata through
``JCODE_HOOK_*``, and treats exit 2 plus stderr as a blocked tool call.
Return True only for the first matching raw lookup in a session; malformed
input and unsupported tools fail open.
"""
from graphify.paths import out_path

try:
if not out_path("graph.json").is_file() or _query_stamp_fresh():
return False
payload = json.loads(sys.stdin.buffer.read().decode("utf-8", "replace"))
if not isinstance(payload, dict):
return False

tool_name = os.environ.get("JCODE_HOOK_TOOL_NAME", "").strip().lower()
command = str(payload.get("command") or "")
command_lower = command.lower()
if "graphify query" in command_lower:
return False

is_raw_lookup = tool_name in {"agentgrep", "grep", "read"}
if tool_name == "bash":
is_raw_lookup = any(
token in command_lower
for token in ("grep", "ripgrep", "rg ", "find ", "fd ", "ack ", "ag ")
)
if not is_raw_lookup:
return False

session_id = os.environ.get("JCODE_HOOK_SESSION_ID", "").strip()
if not session_id or not _mark_session_denied(f"jcode-{session_id}"):
return False

sys.stderr.write(
"Graphify knowledge graph is available for this project. "
"Run `graphify query \"<your codebase question>\"` before raw "
"search/read, then retry this tool if more detail is needed.\n"
)
return True
except Exception:
return False


def _target_is_indexed(file_path: str, root: "Path") -> bool:
"""Guard the strict deny: only block a read of a file the graph actually indexes.
Reads manifest.json (cheap, capped); on any doubt (missing/corrupt/oversized
Expand Down Expand Up @@ -1658,6 +1704,40 @@ def dispatch_command(cmd: str) -> None:
else:
print(format_diagnostic_report(summary))

elif cmd == "check-knowledge":
import json as _json
from graphify.lattice_ingest import validate_lattice

check_path = Path(".")
for arg in sys.argv[2:]:
if not arg.startswith("--"):
check_path = Path(arg)
break
if not check_path.exists() or not check_path.is_dir():
print(f"error: path not found or not a directory: {check_path}", file=sys.stderr)
sys.exit(1)
result = validate_lattice(check_path)
if "--json" in sys.argv:
print(_json.dumps(result, indent=2, ensure_ascii=False))
else:
if result["valid"]:
print(
f"Knowledge lattice valid: {result['sections']} sections "
f"across {result['files']} files."
)
else:
for error in result["errors"]:
print(
f"{error['file']}:{error['line']}: "
f"{error['code']}: {error['message']}"
)
print(
f"Knowledge lattice invalid: {len(result['errors'])} error(s).",
file=sys.stderr,
)
if not result["valid"]:
sys.exit(1)

elif cmd == "add":
if len(sys.argv) < 3:
print(
Expand Down Expand Up @@ -2147,6 +2227,10 @@ def dispatch_command(cmd: str) -> None:
strict="--strict" in sys.argv[3:],
)
sys.exit(0)
elif cmd == "jcode-hook":
# Jcode pre_tool gate: exit 2 blocks once and exposes stderr to the
# model; every unsupported/error path fails open with exit 0.
sys.exit(2 if _run_jcode_hook_guard() else 0)
elif cmd == "check-update":
if len(sys.argv) < 3:
print("Usage: graphify check-update <path>", file=sys.stderr)
Expand Down
53 changes: 53 additions & 0 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
from .cache import load_cached, save_cached
from .mcp_ingest import extract_mcp_config, is_mcp_config_path
from .manifest_ingest import extract_package_manifest, is_package_manifest_path
from .lattice_ingest import (
extract_lattice_code_ref_edges,
extract_lattice_markdown,
is_lattice_markdown_path,
project_source_paths,
resolve_lattice_reference_edges,
)
from .resolver_registry import (
LanguageResolver,
register as register_language_resolver,
Expand Down Expand Up @@ -4719,6 +4726,11 @@ def _is_cpp_header(path: Path) -> bool:

def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_extractor()

fans out to 7 callees (efferent coupling); 26 callers depend on it (afferent coupling).

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_get_extractor()

fans out to 7 callees (efferent coupling); 26 callers depend on it (afferent coupling).

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

"""Return the correct extractor function for a file, or None if unsupported."""
# A lat.md lattice is curated, validated knowledge rather than ordinary
# prose. Route it before generic Markdown so section ids, summaries and
# wiki-link edges remain compatible with lat.md's public format.
if is_lattice_markdown_path(path):
return extract_lattice_markdown
if path.name.lower().endswith(".blade.php"):
return extract_blade
# MCP config files (.mcp.json, claude_desktop_config.json, ...) are routed
Expand Down Expand Up @@ -5257,6 +5269,47 @@ def extract(
all_nodes.extend(result.get("nodes", []))
all_edges.extend(result.get("edges", []))
all_raw_calls.extend(result.get("raw_calls", []))
# Bind curated knowledge to implementation files through explicit
# `@lat: [[section]]` comments. The file endpoint uses the same pre-remap id
# recipe as every extractor, so the canonical path remap below updates it
# together with the file node.
resolve_lattice_reference_edges(all_edges, all_nodes)
lattice_changed = any(is_lattice_markdown_path(path) for path in paths)
if lattice_changed:
# Incremental updates often contain only the changed lat.md file. Rescan
# unchanged, ignore-filtered source files so newly-added section ids can
# bind to existing @lat comments in the merged graph.
code_ref_paths = project_source_paths(root)
else:
code_ref_paths = paths
lattice_code_edges = extract_lattice_code_ref_edges(code_ref_paths, all_nodes)
if lattice_changed:
# The incremental extraction result must retain these edges until it is
# merged with the previous graph. Add minimal file endpoints for unchanged
# mentioned sources, otherwise the normal dangling-edge cleanup would
# discard the relationship before the merge can reconnect it.
existing_ids = {str(node.get("id")) for node in all_nodes}
mentioned_paths = {
Path(str(edge["source_file"]))
for edge in lattice_code_edges
if edge.get("source_file")
}
for mentioned_path in sorted(mentioned_paths):
file_id = _file_node_id(mentioned_path)
if file_id in existing_ids:
continue
existing_ids.add(file_id)
all_nodes.append(
{
"id": file_id,
"label": mentioned_path.name,
"type": "file",
"file_type": "code",
"source_file": str(mentioned_path),
"source_location": "L1",
}
)
all_edges.extend(lattice_code_edges)
# Function / method / class def ids for the cross-file indirect_call callable
# guard. Built from the `_callable` node marker AFTER the id-remap / disambiguation
# passes below (which rewrite node ids), so it can never go stale — see the
Expand Down
Loading