Three independent Windows compatibility fixes shipped together because they
all surface during the same first /graphify run on Windows.
graphify/benchmark.py
print_benchmark() unconditionally printed U+2500 (box-drawing) and U+2192
(rightwards arrow), which UnicodeEncodeError'd on stdouts that can't encode
them — most notably the legacy Windows console at cp1252. New _safe()
helper falls back to ASCII when the active stdout encoding can't carry the
glyph; _hr() uses it. Two regression tests cover both paths and prove
print_benchmark survives a cp1252-strict stream.
graphify/extract.py
ProcessPoolExecutor on Windows uses spawn, so worker subprocesses
re-import the calling __main__. When the caller is `python -c "..."` or a
script without an `if __name__ == "__main__":` guard, the workers
recursively spawn themselves and the pool dies. The user-visible failure
was a 290-line traceback ending in BrokenProcessPool, hiding the actual
cause. _extract_parallel now catches BrokenProcessPool, prints a one-line
warning that names the __main__-guard idiom, and returns False so the
public extract() routes to the existing _extract_sequential fallback. Two
tests cover the parallel-returns-False contract and the sequential
fallback wiring.
graphify/skill-windows.md
Every `python -c "..."` block (30 in total) is replaced with a
Write+run+delete pattern using PowerShell's literal here-string @'...'@.
The old form was a quote-escaping minefield: any double-quote inside the
Python source had to be backslash-escaped for the shell, and PowerShell's
parser ate them inconsistently — failing on f-strings like
`f'AST: {len(result["nodes"])} nodes'`. The new form passes Python source
to disk literally, so what the model writes is what Python sees. The AST
step's script template now includes an explicit `if __name__ == "__main__":`
guard so multi-core extraction works even before the runtime fallback above
kicks in. All 31 resulting heredoc blocks parse cleanly under
`ast.parse`.
Co-authored-by: Nauman Hameed <Nauman.Hameed@enghouse.com>
Adaptive retry only recovered from `finish_reason="length"` (output
truncation). It did not handle the other shape of overflow: the API
rejecting the prompt outright with a 400 because the input plus
`max_completion_tokens` doesn't fit in the model's context window.
This shows up immediately on local OpenAI-compatible servers (LM
Studio, llama.cpp, vLLM) where the default context is small (4K-32K)
and a 60K-token chunk packed for cloud Kimi/Claude blows past it.
Without retry the whole chunk fails with no output, even though the
two halves would each fit cleanly.
Catch a heuristic set of context-overflow exception messages,
classify them as the same kind of recoverable failure as
`finish_reason="length"`, and split-recurse on the same path. Single-
file overflow returns an empty fragment so the rest of the corpus
keeps running. Unrelated errors (rate limit, auth, etc.) still
propagate.
Tested with qwen3.5-9b on LM Studio (32K ctx) against a 215-file
corpus where chunks 4-12 of 12 previously failed; with this change
the overflowing chunks self-heal by splitting in half.
The SQL parser (`extract_sql`) previously only extracted foreign key
relationships defined inline within CREATE TABLE column definitions.
FK constraints added via ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY
... REFERENCES were silently ignored.
Additionally, `_obj_name()` only read the first identifier child of
object_reference nodes, so schema-qualified names like `Sales.Customer`
were truncated to just `Sales`.
Changes:
- Add `alter_table` handler to `walk()` that extracts FK edges from
ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY ... REFERENCES
- Fix `_obj_name()` to read the full object_reference text, preserving
schema-qualified names (e.g. `Sales.Customer`)
- Fix inline FK resolution in create_table and _walk_from_refs to use
full object_reference text instead of first identifier only
Rewrote _CLAUDE_MD_SECTION, _AGENTS_MD_SECTION, and _GEMINI_MD_SECTION to use forceful ALWAYS/NEVER directives instead of soft suggestions.
Agents must now consult the knowledge graph before file operations.
Context:
- Updated AGENTS.md template injected via _agents_install()
- Updated CLAUDE.md template injected via claude install
- Updated GEMINI.md template injected via gemini_install()
10 skill-*.md files had descriptions that only described what graphify does (input->pipeline->output), not when agents should use it. This meant skills never loaded proactively on codebase questions.
Changed to hybrid descriptions that retain the pipeline summary but add trigger conditions: 'Use when user asks any question about a codebase, project content, architecture, or file relationships'.
tree-sitter-typescript ships two grammars:
- language_typescript: pure TypeScript, no JSX support
- language_tsx: JSX-aware variant for .tsx files
Currently both .ts and .tsx are parsed with language_typescript, which
treats JSX syntax as parse errors. Every function declaration, arrow
function, and call_expression nested inside a JSX tree is silently
dropped from the extracted graph.
Repro on a representative React+TypeScript codebase (a 13-file Tauri app):
parsing each .tsx with language_typescript produces ~276 ERROR nodes per
file. Only declarations that happen to live before the first JSX block
survive.
Fix: add _TSX_CONFIG that mirrors _TS_CONFIG but selects language_tsx,
and route .tsx files to it in extract_js().
Effect on the same repo (graphify update --force):
Nodes: 303 → 618 (+104%)
Edges: 482 → 779 (+62%)
Communities: 28 → 45 (+61%)
Parse errors 276 → 0 per .tsx file
Tests added:
- tsx fixture with helpers + JSX-returning component
- helpers and component are captured
- JSX expression calls ({fmtDate(now)}) resolve to call edges
- wiring check: .tsx uses language_tsx, .ts uses language_typescript
Note: this fixes the parsing layer. Calls inside deeply nested arrow
function callbacks (e.g. items.map(x => <T>{f(x)}</T>)) are still
missed by the call extraction logic — separate enhancement.
Co-authored-by: Serkan Gezici <serkan@quadroaipilot.com>
Adds support for Quarto markdown (.qmd) files by:
- Adding '.qmd' to document file extensions in detection
- Updating export logic to handle .qmd in filename sanitization
- Adding .qmd extractor dispatch using the existing markdown extractor
- Updating watch comments to include .qmd files
The cross-file call resolver in `extract()` unconditionally marked every
resolved call edge as INFERRED with confidence_score 0.8 — even when the
caller's file had an explicit `imports` (symbol) or `imports_from`
(module) edge to the callee. The new CJS require handler made this gap
visible: imports were correctly EXTRACTED but the call edges that those
imports backed remained INFERRED, so downstream consumers couldn't tell
high-evidence calls apart from name-match guesses.
This pass runs after the file-id remap (line 4736), so we relativize
node `source_file` paths before computing file_nids — otherwise the
caller's computed file_nid (absolute-path-derived) wouldn't match the
imports_from edge source (already remapped to relative form).
Promotion rule:
- Symbol-level `imports` edge from caller's file -> callee node id
=> EXTRACTED, confidence_score 1.0
- Module-level `imports_from` edge from caller's file -> callee's file
=> EXTRACTED, confidence_score 1.0
- Otherwise => INFERRED, confidence_score 0.8 (existing behavior)
Validated on a 92-file CJS orchestrator: 5 previously-INFERRED edges
from runExecute() now resolve to EXTRACTED, and 88% of cross-file calls
in the corpus (104 of 118) promote, leaving INFERRED only for genuine
heuristic guesses with no import backing.
Adds two tests:
- test_cross_file_call_promoted_to_extracted_with_import_evidence
- test_cross_file_call_remains_inferred_without_import_evidence
The JS/TS extractor only handled ES `import` statements; CommonJS
`require()` calls produced no import edges. Downstream, the call-graph
pass could not resolve which symbols belonged to which file, so every
cross-file call in CJS Node.js codebases was downgraded to INFERRED
even when the binding was a top-of-file destructured require.
Adds three patterns to `_js_extra_walk` via a new `_require_imports_js`
helper:
const { foo, bar: alias } = require('./mod') -> imports_from + per-symbol imports
const mod = require('./mod') -> imports_from
const x = require('./mod').y -> imports_from + symbol edge for y
Refactors path-resolution out of `_import_js` into
`_resolve_js_import_target` so ES imports and CJS requires share the
relative / tsconfig-alias / bare-module logic.
Tested in a 92-file CJS Node.js orchestrator codebase: confirmed all
five previously INFERRED `runExecute -> {loadFoundation,
validateDispatchConfig, fetchSymphonyIssues, listSymphonyWorktrees,
workspacePathForIssue}` edges resolve to real top-of-file destructured
requires, so downstream calls would now be EXTRACTED instead of
INFERRED.