Graph analysis plugins were slow in a specific, measurable way. Not slow because the algorithms were wrong — slow because every property lookup issued a separate request to the graph server. One node, one question, one round-trip. Multiply by every node in the graph, by every inference phase, by every plugin. On a large codebase: millions of requests per analysis run, every time.
Sprint v0.2 opened with one measurement: 6 minutes and 51 seconds. The target was under five minutes. The N+1 optimization campaign ran through three plugins in sequence. The brigade established a pattern on the first plugin and applied it to all three: bulk-preload the relevant subgraph with one Datalog query, resolve in JavaScript, write results in batched flushes. The final tally:
| Plugin | IPC calls (before) | Queries (after) | Commit |
|---|---|---|---|
shape-verifier |
600,000 – 700,000 | 2 | 5ac5a302 |
method-call-resolver |
~600,000 | 4 | d3ce94e1 |
type-inference |
~1,270,000 | 7 | 4d8edc98 |
| Total | ~2,500,000 | 13 | Branch: perf/plugin-early-exit-gates |
All three rewrites are gated behind GRAFEMA_DATALOG_PLUGINS. The legacy implementations are preserved in full. 681 tests pass across all variants. The benchmark — the actual 6m51s vs. target ≤5m measurement — awaits CI completion with compiled native binaries.
type-inference.mjs was last — and largest. The first two rewrites established the pattern; by the time the brigade reached type-inference, the approach was proven. Only the scale was different. This plugin is 525 lines, responsible for computing type edges across all untyped variables in the graph. The original approach issued one to several IPC calls per node per inference phase. Across approximately 75,000 nodes over five phases, this accumulated to roughly 1.27 million IPC round-trips — more than the other two plugins combined.
The rewrite replaces the per-node loop with seven Datalog queries. Each query preloads a complete resolution table; the result is joined in JavaScript. The seven queries, by phase:
needs_inference(N) replaces ~350,000 IPC callsinfer_literal(N, TypeName)infer_constructor(N, ClassName)new ClassName() → INSTANCE_OF → ClassName.infer_import_ctor(N, ClassName)new X() where X arrives via an IMPORT edge.infer_annotation(N, TypeName)call_needs_singleton(C, ClassName) · builtin_method(M, ClassName) replaces ~125,000 IPC callsconsole.log, Math.floor, etc.) to their canonical class. First-wins dedup by ClassName::MethodName eliminates duplicates from multi-file graphs.param_typed(P, TypeName) replaces ~180,000 IPC callsREFERENCE hop.Results from Q2 through Q4 are merged with first-wins priority in a single inferredVars Set. Write-backs are batched at 500 edges per flush — a pattern established in the first two rewrites. Inspection approved. The third rewrite was the hardest. It was also the most impactful.
Sprint v0.2 was not only about performance. Three bugs were in the queue — each independent, each with a known root cause, each a trap waiting for the next developer who ran into it. The brigade cleared all three alongside the N+1 campaign.
The GUI graph view had a systematic misattribution that only appeared in multi-function files. Every CALL node inside any function appeared to originate from the first function in the file — not the one that actually contained it. A file with five functions would show all calls as coming from function one. The graph was structurally wrong in any file with more than one function, and had been for some time.
The /api/graph-stream endpoint was not walking the CONTAINS parent chain — it was assigning every invisible node to the first visible node unconditionally.
The fix: approximately 33 lines of Rust in build_graph_stream_body(). A parent_of: HashMap<u128, u128> is built from CONTAINS edges (excluding synthetic layout-pack edges), then each invisible node walks the ancestry chain until it finds its correct visible container. If no ancestor is found, the original fallback behavior is preserved. The algorithm was already proven in edges_stream() — this was a port, not a discovery. Performance impact: approximately +2 seconds to graph-stream loading. The brigade assessed this as acceptable for correctness.
grafema analyze --clear was supposed to shut down the server and start fresh. It did not. Every subsequent run after the first ended in ECONNREFUSED. The pattern was consistent: the process exited, the PID file and socket file stayed behind, and the next connect() call found them, concluded the server was already running, and refused to start a new one. It was not running. The files were lying.
shutdownServer() waited for the process to exit but did not delete the PID file or socket file. Two unlinkSync calls after _waitForPidExit — with try/catch for ENOENT — closed the loop. 21/21 tests pass.
grafema doctor now detects stale socket files and removes them automatically, returning a warn status with the message: "Stale socket removed. Run grafema analyze to restart server." Previously, a stale socket caused doctor to report an error with no remediation path. Intelligence notes: this is the same pattern as REG-1129. The server that used to leave its keys in the lock has learned to take them out.
Every developer who had ever added a new Grafema plugin had paid the same tax: read the orchestrator source code to discover the configuration format. There was no documentation. There was no example file. There was source code, and you read it. The tax was not large. It was not necessary.
Adding a new plugin to Grafema now requires no source-reading. The setup workflow is two commands:
cp _ai/orchestrator.config.example.yaml _ai/orchestrator.config sed -i "s|<GRAFEMA_ROOT>|$(pwd)|g" _ai/orchestrator.config
The full registration procedure is in CONTRIBUTING.md. The annotated configuration template is at _ai/orchestrator.config.example.yaml. Every future plugin author avoids the tax that every previous one paid.
Sprint v0.2 opened with one measurement: 6 minutes and 51 seconds. It closes with four completed items, 681 passing tests, and a benchmark pending in CI. Intelligence does not predict benchmark results. It counts what is confirmed: 2.5 million IPC calls removed. Thirteen queries standing where they stood.
The graph is correct where it was wrong. The server shuts down where it did not. The plugin registration is documented where it was not. The measurement will follow from CI.
Разведка данных завершена. Итог: 2 500 000 IPC-вызовов ликвидировано. 13 запросов несут вахту. Ожидаем бенчмарка.
GRAFEMA_DATALOG_PLUGINS=shape-verifier,method-call-resolver,type-inference grafema analyze
perf/plugin-early-exit-gates · PR #259 · CI runningГлеб Всеволодович Сыскарёв filed this report at 23:45. He has already begun counting the next set of IPC calls.