@polycode-projects/seonix 0.10.2 → 0.10.4
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.
- package/bin/cli.mjs +175 -9
- package/package.json +2 -2
- package/src/codegraph.mjs +27 -21
- package/src/viz.mjs +16 -3
package/bin/cli.mjs
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// codebase-memory-mcp so it drops into the same benchmark harness:
|
|
4
4
|
//
|
|
5
5
|
// seonix → stdio MCP server
|
|
6
|
+
// seonix ask "<question>" [--strict] → one-shot plain-English question, answer
|
|
7
|
+
// to stdout, then exit (no MCP, no model call — same engine as seonix_ask). The single-command
|
|
8
|
+
// equivalent of `claude --prompt`. `seonix --help` / `seonix cli --help` print full usage.
|
|
6
9
|
// seonix cli index_repository '{"repo_path":"<abs>"}' → deterministic index
|
|
7
10
|
// (honours <repo>/.seonixignore — gitignore-subset patterns; pass
|
|
8
11
|
// `"ignores":false` in the JSON arg to index everything regardless)
|
|
@@ -35,6 +38,10 @@
|
|
|
35
38
|
// seonix cli <toolName> '{…args}' → invoke ANY MCP tool via Bash (no MCP)
|
|
36
39
|
// (e.g. seonix cli seonix_ask '{"query":"<free text>"}' — mechanical, no-LLM NL question
|
|
37
40
|
// over the graph, PLAN_MECHANICAL_CHAT.md; no bespoke wiring needed, this fallback covers it)
|
|
41
|
+
// seonix cli seonix_ask "<free text>" → same, but the `seonix_` prefix on a bare
|
|
42
|
+
// tool name (ask/search/callers/…) auto-resolves, and a non-JSON payload auto-wraps into that
|
|
43
|
+
// tool's one primary argument — so `cli ask "what calls X"` and `cli seonix_search "text"` both
|
|
44
|
+
// work with no JSON quoting. `seonix cli --help` lists every tool + its example invocation.
|
|
38
45
|
// seonix hook-augment → PreToolUse Grep/Glob augmenter (stdin→stdout)
|
|
39
46
|
// seonix viz [--focus <sym>] [--depth N] [--out f.html] [--data-out f.json] [--repo-url <gitlab url> --ref main] [--site-nav] → render a focused sub-graph to HTML (one shared viewer; data embedded, or split out with --data-out). By default also writes code-browser.html + timeline.html next to --out with a working header nav; --graph-only suppresses the siblings
|
|
40
47
|
// seonix viz --browser-out f.html [--browser-data-out f.json] [--timeline-out f.html] [--scope product|<prefixes>] → override the sibling artifact paths (Chronograph code browser: temporal scrub + two-cursor diff + narration + ghost-branch merges + keyboard nav; archive/PLAN_CODE_BROWSER.md)
|
|
@@ -50,12 +57,12 @@ import { existsSync } from "node:fs";
|
|
|
50
57
|
import { readFile, writeFile } from "node:fs/promises";
|
|
51
58
|
import { fileURLToPath } from "node:url";
|
|
52
59
|
import { startServer } from "../src/server.mjs";
|
|
53
|
-
import { dispatchTool, buildContextBundle } from "../src/server.mjs";
|
|
60
|
+
import { dispatchTool, buildContextBundle, TOOLS } from "../src/server.mjs";
|
|
54
61
|
import { indexRepository, indexRepositories, discoverRepos, syncRepositories } from "../src/extract.mjs";
|
|
55
62
|
import { renderSummaryMd } from "../src/summary.mjs";
|
|
56
63
|
import { loadConfig, DEFAULT_GRAPH_REL } from "../src/config.mjs";
|
|
57
64
|
import * as source from "../src/source.mjs";
|
|
58
|
-
import { parseEntities, renderSearch, rankModulesByProximity, searchModulesRanked, selectRankedModules, DEFAULT_SCORE_GAP } from "../src/codegraph.mjs";
|
|
65
|
+
import { parseEntities, renderSearch, rankModulesByProximity, searchModulesRanked, selectRankedModules, DEFAULT_SCORE_GAP, COLD_TOOLS } from "../src/codegraph.mjs";
|
|
59
66
|
import { loadTomlConfig, normalizeConfig, mergeEffective, CONFIG_FILE } from "../src/toml-config.mjs";
|
|
60
67
|
import { compileGlobs } from "../src/walk.mjs";
|
|
61
68
|
import { createTelemetry, truncateStr, looksLikeMiss } from "../src/telemetry.mjs";
|
|
@@ -106,6 +113,102 @@ function defaultRepoPath(args, { mustExist = true } = {}) {
|
|
|
106
113
|
return args;
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
// Hot tools (server.mjs's TOOLS) carry an inputSchema but no example value, so their
|
|
117
|
+
// example-args + primary-arg-key aren't derivable the way COLD_TOOLS' are — hand-maintained
|
|
118
|
+
// here, right next to COLD_TOOLS' import, so a new hot tool is a one-line addition in both places.
|
|
119
|
+
const HOT_TOOL_EXAMPLES = {
|
|
120
|
+
seonix_context: { symbol: "django/utils/text.py" },
|
|
121
|
+
seonix_snippet: { symbol: "Truncator.chars" },
|
|
122
|
+
seonix_ask: { query: "which functions call Truncator.chars" },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Every seonix_* tool reachable via `cli <toolName>`: [name, description, exampleArgs]. */
|
|
126
|
+
function allToolEntries() {
|
|
127
|
+
const hot = TOOLS.map((t) => [t.name, t.description, HOT_TOOL_EXAMPLES[t.name] || {}]);
|
|
128
|
+
return [...hot, ...COLD_TOOLS];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The one argument key that carries a tool's free-text/name input, if it has exactly one
|
|
132
|
+
* obviously-primary key (the first key of its example-args object) — used to auto-wrap a
|
|
133
|
+
* bare non-JSON string into `{[key]: text}` so `cli seonix_ask 'how does X work'` works
|
|
134
|
+
* without JSON-quoting. Tools with no example args (e.g. seonix_untested) return null. */
|
|
135
|
+
function primaryArgKeyFor(name) {
|
|
136
|
+
const entry = allToolEntries().find(([n]) => n === name);
|
|
137
|
+
const keys = entry ? Object.keys(entry[2]) : [];
|
|
138
|
+
return keys[0] || null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** `ask` → `seonix_ask` when the bare name isn't itself a known tool but the seonix_-prefixed
|
|
142
|
+
* form is — the shape of mistake anyone typing this CLI for the first time makes, since every
|
|
143
|
+
* tool name in this project carries the prefix but nothing else about the CLI hints at it. */
|
|
144
|
+
function resolveToolName(sub) {
|
|
145
|
+
if (allToolEntries().some(([n]) => n === sub)) return sub;
|
|
146
|
+
const prefixed = `seonix_${sub}`;
|
|
147
|
+
if (allToolEntries().some(([n]) => n === prefixed)) return prefixed;
|
|
148
|
+
return sub;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Best-effort "did you mean" suggestion for a tool name that doesn't exist even after
|
|
152
|
+
* resolveToolName's prefix guess — substring/prefix match against the real catalog, so a
|
|
153
|
+
* typo or a half-remembered name gets pointed at the right place instead of a bare 404. */
|
|
154
|
+
function suggestToolName(sub) {
|
|
155
|
+
const names = allToolEntries().map(([n]) => n);
|
|
156
|
+
const needle = sub.replace(/^seonix_/, "").toLowerCase();
|
|
157
|
+
const hit = names.find((n) => n.replace(/^seonix_/, "").toLowerCase().includes(needle))
|
|
158
|
+
|| names.find((n) => needle.includes(n.replace(/^seonix_/, "").toLowerCase()));
|
|
159
|
+
return hit || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const MANAGEMENT_COMMANDS = [
|
|
163
|
+
["index_repository", "Build or rebuild the graph for a repo (or a multi-repo estate).", { repo_path: "/abs/path" }],
|
|
164
|
+
["digest", "Architecture map + per-module edit bundles to stdout — the no-MCP arm.", { repo_path: "/abs/path", modules: ["app/lib/a.mjs"] }],
|
|
165
|
+
["sync", "Incrementally re-index a multi_root estate (only changed repos).", { multi_root: "/abs/estate-dir" }],
|
|
166
|
+
["seonix_locate", "Ranked <path>\\t<score> lines for a free-text query — no digest, just the ranking.", { query: "template filters" }],
|
|
167
|
+
["browser_link", "NL-ish text → the code-browser query grammar → a self-tested URL.", { query: "classes that change with render" }],
|
|
168
|
+
["store_rebuild", "Force-(re)build the opt-in SQLite store from the current graph.json.", { repo_path: "/abs/path" }],
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
function printTopLevelUsage() {
|
|
172
|
+
process.stdout.write(
|
|
173
|
+
"seonix — deterministic, offline, zero-model-call code graph\n\n" +
|
|
174
|
+
"usage:\n" +
|
|
175
|
+
" seonix start the stdio MCP server\n" +
|
|
176
|
+
" seonix ask \"<question>\" one-shot plain-English question, answer to stdout, then exit\n" +
|
|
177
|
+
" (no MCP, no model call — same engine as seonix_ask; pass --strict\n" +
|
|
178
|
+
" to fail loudly on an ambiguous query instead of listing candidates)\n" +
|
|
179
|
+
" seonix chat [--repo <abs>] interactive chat session over the graph (/exit to leave)\n" +
|
|
180
|
+
" seonix cli <command> ['{json}'] run one command non-interactively — see `seonix cli --help`\n" +
|
|
181
|
+
" seonix viz [--focus <sym>] … render an HTML graph view (+ code browser + timeline)\n" +
|
|
182
|
+
" seonix init [--dotnet] [--force] seed a seonix.toml in the current directory\n" +
|
|
183
|
+
" seonix config --effective print the merged, resolved config for the current repo\n" +
|
|
184
|
+
" seonix hook-augment PreToolUse Grep/Glob augmenter (stdin → stdout)\n\n" +
|
|
185
|
+
"`--help` / `-h` works after any of the above (e.g. `seonix cli --help` lists every tool).\n",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function printToolCatalog() {
|
|
190
|
+
const lines = [
|
|
191
|
+
"seonix cli — every command, one JSON (or plain-text, for the tools below marked *) argument:\n",
|
|
192
|
+
" seonix cli index_repository ['{json}'] build/rebuild the graph (bare form indexes cwd)",
|
|
193
|
+
"",
|
|
194
|
+
"management commands:",
|
|
195
|
+
];
|
|
196
|
+
for (const [name, desc, example] of MANAGEMENT_COMMANDS) {
|
|
197
|
+
lines.push(` ${name} — ${desc}`);
|
|
198
|
+
lines.push(` seonix cli ${name} '${JSON.stringify(example)}'`);
|
|
199
|
+
}
|
|
200
|
+
lines.push("", "query tools (all also accept plain text for their * primary argument, e.g.");
|
|
201
|
+
lines.push("`seonix cli seonix_ask \"what calls X\"` needs no JSON quoting):");
|
|
202
|
+
for (const [name, desc, example] of allToolEntries()) {
|
|
203
|
+
const primary = primaryArgKeyFor(name);
|
|
204
|
+
const star = primary ? "*" : " ";
|
|
205
|
+
lines.push(` ${star} ${name} — ${desc}`);
|
|
206
|
+
lines.push(` seonix cli ${name} '${JSON.stringify(example)}'`);
|
|
207
|
+
}
|
|
208
|
+
lines.push("", "An omitted repo_path defaults to the nearest ancestor of cwd with a .seonix/ index.");
|
|
209
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
109
212
|
/** Load a repo's seonix.toml as a normalized config, or null. Returns null IMMEDIATELY when
|
|
110
213
|
* `args.config === false` (the BENCH-IMMUNITY flag — an arm's index/locate/digest must NEVER read a
|
|
111
214
|
* subject repo's seonix.toml) OR when no seonix.toml exists. So on the bench path, and with no
|
|
@@ -347,6 +450,33 @@ async function runSync(multiRoot, { ignores, historyDepth }) {
|
|
|
347
450
|
async function main() {
|
|
348
451
|
const [mode, sub, payload] = process.argv.slice(2);
|
|
349
452
|
|
|
453
|
+
// bare `seonix` with no args is still the MCP server (unchanged contract, handled below);
|
|
454
|
+
// an explicit --help/-h/help asks for usage instead.
|
|
455
|
+
if (mode === "--help" || mode === "-h" || mode === "help") { printTopLevelUsage(); return; }
|
|
456
|
+
if (mode === "cli" && (sub === "--help" || sub === "-h")) { printToolCatalog(); return; }
|
|
457
|
+
|
|
458
|
+
// `seonix ask "<question>" [--strict]`: the one-shot NL entry point — same engine as
|
|
459
|
+
// seonix_ask (mechanical, no model call), but no `cli`, no JSON, no MCP round-trip. The
|
|
460
|
+
// ergonomic gap this closes: typing a plain question at a terminal shouldn't need a JSON
|
|
461
|
+
// payload and the `seonix_` tool-name prefix, the two things that trip up a first run.
|
|
462
|
+
if (mode === "ask") {
|
|
463
|
+
const strict = process.argv.includes("--strict");
|
|
464
|
+
const query = process.argv.slice(3).filter((a) => a !== "--strict").join(" ").trim();
|
|
465
|
+
if (!query) {
|
|
466
|
+
process.stderr.write('seonix: usage: seonix ask "<question>" [--strict]\n');
|
|
467
|
+
process.exit(2);
|
|
468
|
+
}
|
|
469
|
+
const args = defaultRepoPath({ query, ...(strict ? { strict: true } : {}) });
|
|
470
|
+
const config = configFor(args.repo_path);
|
|
471
|
+
try {
|
|
472
|
+
process.stdout.write((await dispatchTool("seonix_ask", args, { config })) + "\n");
|
|
473
|
+
} catch (e) {
|
|
474
|
+
process.stderr.write(`seonix: ${e?.message || e}\n`);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
350
480
|
if (mode === "cli") {
|
|
351
481
|
// index mode: `cli index_repository '{"repo_path":"<abs>"}'` (single repo, unchanged),
|
|
352
482
|
// '{"repo_paths":[…],"out_root":"<abs>"}' (explicit n-repo merge) or
|
|
@@ -681,9 +811,32 @@ async function main() {
|
|
|
681
811
|
// tool-query fallback: any other `cli <toolName> '{…}'` routes to the MCP dispatcher,
|
|
682
812
|
// so "cold" tools are invokable from Bash without an MCP connection.
|
|
683
813
|
if (sub) {
|
|
684
|
-
|
|
814
|
+
// `ask` → `seonix_ask`, `search` → `seonix_search`, etc. — the prefix is the single most
|
|
815
|
+
// common typo this CLI produces, since nothing else about it signals every tool is
|
|
816
|
+
// seonix_-prefixed.
|
|
817
|
+
const resolved = resolveToolName(sub);
|
|
818
|
+
let args = parsePayload(payload);
|
|
819
|
+
if (args === null) {
|
|
820
|
+
// Not valid JSON — auto-wrap as plain text for the tool's one obviously-primary
|
|
821
|
+
// argument (query/symbol/module/…), UNLESS the payload starts with `{`: that's an
|
|
822
|
+
// attempted JSON object with a typo in it, and silently treating a broken `{"symbol":`
|
|
823
|
+
// as a literal search string would mask the real mistake instead of surfacing it.
|
|
824
|
+
const looksLikeAttemptedJson = typeof payload === "string" && payload.trim().startsWith("{");
|
|
825
|
+
const primaryKey = primaryArgKeyFor(resolved);
|
|
826
|
+
if (payload !== undefined && primaryKey && !looksLikeAttemptedJson) args = { [primaryKey]: payload };
|
|
827
|
+
}
|
|
685
828
|
if (args === null) {
|
|
686
|
-
|
|
829
|
+
const entry = allToolEntries().find(([n]) => n === resolved);
|
|
830
|
+
if (entry) {
|
|
831
|
+
process.stderr.write(`seonix: ${resolved} expects a JSON arg, e.g. '${JSON.stringify(entry[2])}'\n`);
|
|
832
|
+
} else {
|
|
833
|
+
const suggestion = suggestToolName(sub);
|
|
834
|
+
process.stderr.write(
|
|
835
|
+
`seonix: unknown tool "${sub}"` +
|
|
836
|
+
(suggestion ? ` — did you mean "${suggestion}"?\n` : ".\n") +
|
|
837
|
+
"Run `seonix cli --help` to list every tool.\n",
|
|
838
|
+
);
|
|
839
|
+
}
|
|
687
840
|
process.exit(2);
|
|
688
841
|
}
|
|
689
842
|
defaultRepoPath(args);
|
|
@@ -694,26 +847,38 @@ async function main() {
|
|
|
694
847
|
const tel = createTelemetry({ env: process.env, config, toml, surface: "cli" });
|
|
695
848
|
const t0 = Date.now();
|
|
696
849
|
try {
|
|
697
|
-
const text = await dispatchTool(
|
|
850
|
+
const text = await dispatchTool(resolved, args, { config });
|
|
698
851
|
process.stdout.write(text + "\n");
|
|
699
852
|
// Telemetry for the generic tool surface: which tool + how many chars it returned, plus
|
|
700
853
|
// (wh dogfood backlog #6) the args text/duration/hit-miss the log previously omitted.
|
|
701
854
|
tel?.record({
|
|
702
|
-
tool:
|
|
855
|
+
tool: resolved,
|
|
703
856
|
query: { raw: truncateStr(JSON.stringify(args)) },
|
|
704
857
|
perf: { ms_total: Date.now() - t0 },
|
|
705
858
|
quality: { miss: looksLikeMiss(text) },
|
|
706
859
|
cost: { returned_chars: text.length, returned_tokens_est: Math.ceil(text.length / 4) },
|
|
707
860
|
});
|
|
708
861
|
} catch (e) {
|
|
709
|
-
|
|
862
|
+
const msg = e?.message || String(e);
|
|
863
|
+
const unknown = /^unknown tool: (.+)$/.exec(msg);
|
|
864
|
+
if (unknown) {
|
|
865
|
+
const suggestion = suggestToolName(unknown[1]);
|
|
866
|
+
process.stderr.write(
|
|
867
|
+
`seonix: ${msg}` +
|
|
868
|
+
(suggestion ? ` — did you mean "${suggestion}"?\n` : ".\n") +
|
|
869
|
+
"Run `seonix cli --help` to list every tool.\n",
|
|
870
|
+
);
|
|
871
|
+
} else {
|
|
872
|
+
process.stderr.write(`seonix: ${msg}\n`);
|
|
873
|
+
}
|
|
710
874
|
process.exit(1);
|
|
711
875
|
}
|
|
712
876
|
return;
|
|
713
877
|
}
|
|
714
878
|
|
|
715
879
|
process.stderr.write("seonix: `cli` needs a sub-command (index_repository | digest | <toolName>). " +
|
|
716
|
-
"The JSON arg is optional — an omitted repo_path defaults to the nearest ancestor of cwd with a .seonix/ index
|
|
880
|
+
"The JSON arg is optional — an omitted repo_path defaults to the nearest ancestor of cwd with a .seonix/ index. " +
|
|
881
|
+
"Run `seonix cli --help` to list every command.\n");
|
|
717
882
|
process.exit(2);
|
|
718
883
|
}
|
|
719
884
|
|
|
@@ -777,7 +942,8 @@ async function main() {
|
|
|
777
942
|
}
|
|
778
943
|
|
|
779
944
|
process.stderr.write(`seonix: unknown invocation "${process.argv.slice(2).join(" ")}". ` +
|
|
780
|
-
"Use bare (MCP server), `cli index_repository …`, `cli <tool> …`, `chat`,
|
|
945
|
+
"Use bare (MCP server), `ask \"<question>\"`, `cli index_repository …`, `cli <tool> …`, `chat`, " +
|
|
946
|
+
"`hook-augment`, `viz`, `init`, or `config --effective`. Run `seonix --help` for the full list.\n");
|
|
781
947
|
process.exit(2);
|
|
782
948
|
}
|
|
783
949
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/seonix",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "SEONIX — a deterministic, offline, $0 code-graph tool that makes a cheap coding agent edit like an expensive one. Indexes a repo with Python ast + git (zero model calls) and renders a bounded edit-digest.",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
89
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
90
|
-
"@polycode-projects/the-mechanical-code-talker": "^0.9.
|
|
90
|
+
"@polycode-projects/the-mechanical-code-talker": "^0.9.3",
|
|
91
91
|
"cytoscape": "^3.30.0",
|
|
92
92
|
"smol-toml": "^1.7.0",
|
|
93
93
|
"tree-sitter": "^0.21.1",
|
package/src/codegraph.mjs
CHANGED
|
@@ -2010,31 +2010,37 @@ export function renderContextMore(plan) {
|
|
|
2010
2010
|
|
|
2011
2011
|
// ---- #7 cold-tool catalog (written to <repo>/.seonix/TOOLS.md by the index step) --
|
|
2012
2012
|
|
|
2013
|
+
/** The COLD tools (everything except the two hot MCP tools + seonix_ask): each entry is
|
|
2014
|
+
* [name, one-line purpose, example-args-object]. Single source of truth for the TOOLS.md
|
|
2015
|
+
* catalog, `seonix cli --help`, and the CLI's per-tool JSON-arg-error examples — so all
|
|
2016
|
+
* three stay in sync with one edit. */
|
|
2017
|
+
export const COLD_TOOLS = [
|
|
2018
|
+
["seonix_describe", "Locate one symbol and list its typed edges (both directions) with provenance.", { symbol: "django/utils/text.py" }],
|
|
2019
|
+
["seonix_signature", "One symbol's API surface (params, returns, raises/catches, flags, decorators, doc) without the body.", { symbol: "Truncator.chars" }],
|
|
2020
|
+
["seonix_impact", "Transitive reverse closure over imports/calls — what breaks if a module changes, by depth, with tests.", { module: "django/utils/text.py" }],
|
|
2021
|
+
["seonix_search", "Free-text/ranked lookup over the code-map to find the right module or symbol.", { query: "template filters", kind: "function" }],
|
|
2022
|
+
["seonix_members", "A class's methods + attributes (file:line, decorators) in one slice.", { class: "Truncator" }],
|
|
2023
|
+
["seonix_subclasses", "A class's base classes plus the transitive set of classes that extend it.", { class: "Field" }],
|
|
2024
|
+
["seonix_architecture", "Package/module map + the most-imported hub modules (optionally scoped to a package).", { package: "django/template" }],
|
|
2025
|
+
["seonix_exports", "A module's public __all__ surface, each name resolved to the module that defines it.", { module: "django/db/models/__init__.py" }],
|
|
2026
|
+
["seonix_tests_for", "The test modules covering a symbol or module, from the typed test edges.", { symbol: "django/utils/text.py" }],
|
|
2027
|
+
["seonix_untested", "Source modules with no covering test module — a coverage-gap view (no arguments).", {}],
|
|
2028
|
+
["seonix_history", "Recent commits that touched a symbol's module (newest first).", { symbol: "django/utils/text.py" }],
|
|
2029
|
+
["seonix_file_history", "Commits that touched a symbol's module, each with author / date / subject.", { symbol: "django/utils/text.py" }],
|
|
2030
|
+
["seonix_method_history", "Commits that touched a specific method symbol (fine-grained), with author / date / subject.", { symbol: "Truncator.chars" }],
|
|
2031
|
+
["seonix_class_history", "Commits that touched a specific class symbol (fine-grained), with author / date / subject.", { symbol: "Truncator" }],
|
|
2032
|
+
["seonix_callers", "Modules that call into a symbol's module (one hop).", { symbol: "django/utils/text.py" }],
|
|
2033
|
+
["seonix_callees", "Modules a symbol's module calls into (one hop).", { symbol: "django/utils/text.py" }],
|
|
2034
|
+
["seonix_calls", "The in-repo symbols a function calls (fn→fn), each with file:line.", { symbol: "slugify" }],
|
|
2035
|
+
["seonix_cochanges", "Modules that historically change in the same commit as a symbol's module (git co-change).", { symbol: "django/utils/text.py" }],
|
|
2036
|
+
["seonix_context_more", "The bundle sections a lean seonix_context omitted (siblings / tests / cochange / class members / re-exports).", { symbol: "django/utils/text.py" }],
|
|
2037
|
+
];
|
|
2038
|
+
|
|
2013
2039
|
/** Markdown catalog of the COLD tools (everything except the two hot MCP tools): each
|
|
2014
2040
|
* with a one-line purpose and the exact Bash invocation via the CLI `cli <tool>` route.
|
|
2015
2041
|
* Pure — `cliPath` is the absolute path to bin/cli.mjs the caller wants embedded. */
|
|
2016
2042
|
export function renderToolsCatalog(cliPath) {
|
|
2017
|
-
const cold =
|
|
2018
|
-
["seonix_describe", "Locate one symbol and list its typed edges (both directions) with provenance.", { symbol: "django/utils/text.py" }],
|
|
2019
|
-
["seonix_signature", "One symbol's API surface (params, returns, raises/catches, flags, decorators, doc) without the body.", { symbol: "Truncator.chars" }],
|
|
2020
|
-
["seonix_impact", "Transitive reverse closure over imports/calls — what breaks if a module changes, by depth, with tests.", { module: "django/utils/text.py" }],
|
|
2021
|
-
["seonix_search", "Free-text/ranked lookup over the code-map to find the right module or symbol.", { query: "template filters", kind: "function" }],
|
|
2022
|
-
["seonix_members", "A class's methods + attributes (file:line, decorators) in one slice.", { class: "Truncator" }],
|
|
2023
|
-
["seonix_subclasses", "A class's base classes plus the transitive set of classes that extend it.", { class: "Field" }],
|
|
2024
|
-
["seonix_architecture", "Package/module map + the most-imported hub modules (optionally scoped to a package).", { package: "django/template" }],
|
|
2025
|
-
["seonix_exports", "A module's public __all__ surface, each name resolved to the module that defines it.", { module: "django/db/models/__init__.py" }],
|
|
2026
|
-
["seonix_tests_for", "The test modules covering a symbol or module, from the typed test edges.", { symbol: "django/utils/text.py" }],
|
|
2027
|
-
["seonix_untested", "Source modules with no covering test module — a coverage-gap view (no arguments).", {}],
|
|
2028
|
-
["seonix_history", "Recent commits that touched a symbol's module (newest first).", { symbol: "django/utils/text.py" }],
|
|
2029
|
-
["seonix_file_history", "Commits that touched a symbol's module, each with author / date / subject.", { symbol: "django/utils/text.py" }],
|
|
2030
|
-
["seonix_method_history", "Commits that touched a specific method symbol (fine-grained), with author / date / subject.", { symbol: "Truncator.chars" }],
|
|
2031
|
-
["seonix_class_history", "Commits that touched a specific class symbol (fine-grained), with author / date / subject.", { symbol: "Truncator" }],
|
|
2032
|
-
["seonix_callers", "Modules that call into a symbol's module (one hop).", { symbol: "django/utils/text.py" }],
|
|
2033
|
-
["seonix_callees", "Modules a symbol's module calls into (one hop).", { symbol: "django/utils/text.py" }],
|
|
2034
|
-
["seonix_calls", "The in-repo symbols a function calls (fn→fn), each with file:line.", { symbol: "slugify" }],
|
|
2035
|
-
["seonix_cochanges", "Modules that historically change in the same commit as a symbol's module (git co-change).", { symbol: "django/utils/text.py" }],
|
|
2036
|
-
["seonix_context_more", "The bundle sections a lean seonix_context omitted (siblings / tests / cochange / class members / re-exports).", { symbol: "django/utils/text.py" }],
|
|
2037
|
-
];
|
|
2043
|
+
const cold = COLD_TOOLS;
|
|
2038
2044
|
const lines = [
|
|
2039
2045
|
"# seonix cold-tool catalog",
|
|
2040
2046
|
"",
|
package/src/viz.mjs
CHANGED
|
@@ -938,11 +938,24 @@ export async function runVizCli(argv) {
|
|
|
938
938
|
siteNav: values["site-nav"],
|
|
939
939
|
nav: navFor(browserOut),
|
|
940
940
|
});
|
|
941
|
-
|
|
942
|
-
|
|
941
|
+
const browserJson = JSON.stringify(browserData);
|
|
942
|
+
// Same large-graph gate as the main graph viewer (writeSplitViewer above): an
|
|
943
|
+
// estate-scale temporal graph embedded inline produces a multi-hundred-MB HTML
|
|
944
|
+
// file that hangs the browser tab parsing it, not just a file:// fetch failure —
|
|
945
|
+
// so this checks the ACTUAL embedded payload size, not graphBytes. Auto-triggers
|
|
946
|
+
// even without --browser-data-out; that flag still works to force sidecars early.
|
|
947
|
+
if (values["browser-data-out"] || (Buffer.byteLength(browserJson) > inlineAskDataMaxBytes() && !values["force-inline"])) {
|
|
948
|
+
const bDataOut = resolve(values["browser-data-out"] || join(dirname(browserOut), "code-browser-data.json"));
|
|
943
949
|
const rel = relative(dirname(browserOut), bDataOut) || "code-browser-data.json";
|
|
944
|
-
await writeFile(bDataOut,
|
|
950
|
+
await writeFile(bDataOut, browserJson);
|
|
945
951
|
await writeFile(browserOut, await renderBrowserHtml({ cytoscape, dataPath: rel === "code-browser-data.json" ? null : rel }));
|
|
952
|
+
if (!values["browser-data-out"]) {
|
|
953
|
+
const mb = Math.round(Buffer.byteLength(browserJson) / (1024 * 1024));
|
|
954
|
+
process.stderr.write(
|
|
955
|
+
`seonix viz: large chronograph payload (${mb} MB > 32 MB) — data written as a sidecar next to ${browserOut}; ` +
|
|
956
|
+
"file:// pages cannot fetch siblings, open via `seonix viz --serve` (or pass --force-inline to embed anyway)\n",
|
|
957
|
+
);
|
|
958
|
+
}
|
|
946
959
|
process.stderr.write(
|
|
947
960
|
`seonix viz: chronograph ${tg.nodes.length} nodes, ${tg.edges.length} edges, ${tg.commits.length} commits ` +
|
|
948
961
|
`(scope ${values.scope}) → ${browserOut} + ${bDataOut}\n`,
|