Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Fix: when duplicate nodes merge, the richer (more complete) node is now kept as the survivor and the losers' non-empty fields are folded in, instead of a shorter-id passing mention winning and dropping content (#3372, thanks @abhay-codes07).
- Fix: a C# generic call site with explicit type arguments — `Get<int>(...)`, unqualified or through `this` — now resolves to the method definition instead of capturing `Get<int>` as the callee and failing to match (#3406, thanks @abhay-codes07).
- Fix: `this.X = function` / `this.X = () => …` members are now captured in every enclosing-function form (function expressions, arrows, IIFEs, callbacks), not just function declarations (#3408, thanks @abhay-codes07).
- Fix: Jinja templates (`.j2`, `.jinja`, `.jinja2`) are now classified as documents instead of being silently unclassified — a codegen repo's graph described only the generated files marked `DO NOT EDIT` and none of the templates they come from; oversized templates are sliced like every other text document (#3427, thanks @AmberProof).

## 0.9.56 (2026-09-07)

Expand Down
11 changes: 10 additions & 1 deletion graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,16 @@ class FileType(str, Enum):
_MTIME_SUBSECOND_S = 0.05

CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.ml', '.mli', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger', '.lisp', '.cl', '.lsp', '.asd', '.robot', '.resource'}
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.skill', '.txt', '.rst', '.html', '.yaml', '.yml'}
# Jinja templates are documents, not code: in a codegen repo the template is the
# only file a developer is allowed to edit (the generated module says DO NOT EDIT
# and is overwritten every build), so leaving `.j2` unclassified made the graph
# describe exactly the half that must not be touched (#3427). They are not routed
# to CODE because a template is not valid source in its target language —
# `{% for %}` in a .py.j2 is a parse error, not an AST — and the semantic pass
# reads the prose, generator header and structure that carry the real link.
# Note the outer suffix is what classifies: `module.py.j2` -> `.j2`.
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.skill', '.txt', '.rst', '.html', '.yaml', '.yml',
'.j2', '.jinja', '.jinja2'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
OFFICE_EXTENSIONS = {'.docx', '.xlsx'}
Expand Down
1 change: 1 addition & 0 deletions graphify/file_slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
_SPLITTABLE_TEXT_SUFFIXES = frozenset({
".md", ".mdx", ".markdown", ".txt", ".rst",
".qmd", ".skill", ".html", ".yaml", ".yml",
".j2", ".jinja", ".jinja2",
})

# Document types whose BYTES are not what the model is shown. `llm._file_to_text`
Expand Down
31 changes: 31 additions & 0 deletions tests/test_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ def test_classify_skill():
# #1901: .skill agent files (Markdown with YAML frontmatter) were dropped as unclassified.
assert classify_file(Path("10_Orchestrator.skill")) == FileType.DOCUMENT

def test_classify_jinja_template():
# #3427: .j2/.jinja/.jinja2 were unclassified, so a codegen repo's graph
# described only the generated files that must NOT be edited.
assert classify_file(Path("templates/module.py.j2")) == FileType.DOCUMENT
assert classify_file(Path("templates/base.jinja")) == FileType.DOCUMENT
assert classify_file(Path("templates/base.jinja2")) == FileType.DOCUMENT
# The outer suffix classifies, whatever the target language underneath.
for inner in ("py", "ts", "yaml", "conf", "sql", "tf"):
assert classify_file(Path(f"t/app.{inner}.j2")) == FileType.DOCUMENT

def test_classify_pdf():
assert classify_file(Path("paper.pdf")) == FileType.PAPER

Expand Down Expand Up @@ -97,6 +107,27 @@ def test_detect_skips_noise_dot_dirs():
assert noise not in f


def test_detect_finds_jinja_templates_beside_generated_code(tmp_path):
"""The reported shape: a codegen repo where the generated module is graphed
and the template it comes from is invisible (#3427)."""
(tmp_path / "templates").mkdir()
(tmp_path / "templates" / "module.py.j2").write_text(
"# generated from module.py.j2 — DO NOT EDIT MANUALLY\n"
"class {{ name }}:\n pass\n"
)
(tmp_path / "templates" / "config.yaml.jinja2").write_text("name: {{ name }}\n")
(tmp_path / "module.py").write_text("class Generated:\n pass\n")

result = detect(tmp_path)

assert str(tmp_path / "module.py") in result["files"]["code"]
assert set(result["files"]["document"]) == {
str(tmp_path / "templates" / "module.py.j2"),
str(tmp_path / "templates" / "config.yaml.jinja2"),
}
assert result["unclassified"] == []


def test_detect_skips_obsidian_vault_metadata_dirs(tmp_path):
"""Obsidian metadata and plugin caches are not part of the source corpus (#2493)."""
for directory in (".obsidian", ".smart-env"):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_oversized_document_slicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def test_slices_stay_within_the_cap(tmp_path):
# End to end through the prompt builder
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("ext", [".qmd", ".html", ".yaml", ".yml", ".skill"])
@pytest.mark.parametrize("ext", [".qmd", ".html", ".yaml", ".yml", ".skill", ".j2"])
def test_the_tail_of_a_big_document_reaches_the_prompt(tmp_path, ext):
"""The symptom a user would notice: content past 20k was invisible to the
semantic pass, so nothing in the tail could ever become a node."""
Expand Down