@polycode-projects/seonix 0.7.3 → 0.8.0

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.
@@ -0,0 +1,37 @@
1
+ // chat-shim.mjs — `seonix chat` is now tmct's chat over seonix's graph.
2
+ //
3
+ // PLAN_CHAT_EXTRACTION.md, the inversion: the conversational surface belongs to tmct
4
+ // (@polycode-projects/the-mechanical-code-talker). seonix stops running its own REPL and
5
+ // launches tmct's, handing it graph truth through tmct's provider seam. seonix's on-disk
6
+ // graph is interned v2, which tmct cannot read raw, so we expand it first (seonix's own
7
+ // parseEntities does the same). The old opt-in --with-claude/--with-copilot fallback does
8
+ // not port: tmct is no-LLM by contract.
9
+ import { runChat } from "@polycode-projects/the-mechanical-code-talker/chat";
10
+ import { registerProvider } from "@polycode-projects/the-mechanical-code-talker/fetchEntities";
11
+ import { join } from "node:path";
12
+ import { expandGraphPayload } from "./graph-format.mjs";
13
+ import { fetchEntities } from "./source.mjs";
14
+ import { loadConfig, DEFAULT_GRAPH_REL } from "./config.mjs";
15
+
16
+ /** The seonix graph config for a repo (mirrors cli.mjs's configFor): an explicit
17
+ * --repo dir points at its .seonix/graph.json; otherwise the loaded (cwd) config. */
18
+ function seonixConfigFor(repoPath) {
19
+ return repoPath ? { graphFile: join(repoPath, DEFAULT_GRAPH_REL) } : loadConfig();
20
+ }
21
+
22
+ /**
23
+ * Run tmct's chat over seonix's native graph. Registers the (expanded) seonix graph as
24
+ * tmct's in-process data provider, runs the REPL, then clears the provider so nothing
25
+ * leaks across calls (important for tests and repeated invocations).
26
+ * @param {{repoPath?: string, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream,
27
+ * env?: object, cwd?: string, gitRoot?: Function}} [opts]
28
+ */
29
+ export async function runSeonixChat({ repoPath, input, output, env, cwd, gitRoot } = {}) {
30
+ const payload = expandGraphPayload(await fetchEntities(seonixConfigFor(repoPath)));
31
+ registerProvider(() => payload);
32
+ try {
33
+ return await runChat({ repoPath, input, output, env, cwd, gitRoot });
34
+ } finally {
35
+ registerProvider(null);
36
+ }
37
+ }
package/src/server.mjs CHANGED
@@ -51,7 +51,7 @@ import {
51
51
  renderMethodHistory,
52
52
  renderClassHistory,
53
53
  } from "./codegraph.mjs";
54
- import { ask } from "./ask.mjs";
54
+ import { makeSeonixProvider } from "./tmct-provider.mjs";
55
55
 
56
56
  const SNIPPET_MAX_LINES = 200;
57
57
 
@@ -367,11 +367,11 @@ export async function dispatchTool(name, args, { config, source = defaultSource
367
367
  const query = String(args?.query || "").trim();
368
368
  if (!query) throw new ToolError("query is required");
369
369
  const graph = await loadGraph(config, source);
370
- const { content, seonix_ask } = ask(graph, query);
371
- // Every dispatchTool caller (this MCP handler, the CLI fallback) expects a plain string —
372
- // append the structured envelope as a delimited, machine-parseable block rather than
373
- // changing that shared contract for one tool. PLAN_MECHANICAL_CHAT.md §6.2.
374
- return `${content}\n\n---seonix_ask---\n${JSON.stringify(seonix_ask, null, 2)}`;
370
+ // The NL engine is tmct's now (PLAN_CHAT_EXTRACTION.md, the inversion): resolve through tmct's
371
+ // Repository Interface over seonix's graph. The `---seonix_ask---` delimited envelope is preserved
372
+ // so every dispatchTool caller (this MCP handler, the CLI fallback) is unchanged.
373
+ const { content, tmct_ask } = makeSeonixProvider(graph, { sourceAccess: false }).ask(query).value;
374
+ return `${content}\n\n---seonix_ask---\n${JSON.stringify(tmct_ask, null, 2)}`;
375
375
  }
376
376
  if (
377
377
  name === "seonix_tests_for" || name === "seonix_history" || name === "seonix_callers" ||
@@ -0,0 +1,37 @@
1
+ // tmct-provider.mjs — seonix as a tmct Repository-Interface provider.
2
+ //
3
+ // The inversion (PLAN_CHAT_EXTRACTION.md; tmct's PLAN_REPOSITORY_INTERFACE.md):
4
+ // tmct owns the natural-language interpreter, and seonix hands it graph truth
5
+ // through tmct's typed Repository Interface. seonix's parseEntities() result is
6
+ // byte-identical in shape to tmct's, so tmct's createGraphService consumes it
7
+ // directly. The only seonix-specific step is expanding the interned v2 on-disk
8
+ // graph — which parseEntities already does (via graph-format's expandGraphPayload),
9
+ // and tmct cannot read raw.
10
+ //
11
+ // Graph-only by default: sourceAccess=false, so snippet/context honestly answer
12
+ // miss(NO_SOURCE). A caller with a working tree passes sourceAccess (and, later,
13
+ // a source layer) to serve real bodies.
14
+ import { createGraphService } from "@polycode-projects/the-mechanical-code-talker/graph-service";
15
+ import { parseEntities } from "./codegraph.mjs";
16
+ import { fetchEntities } from "./source.mjs";
17
+
18
+ /**
19
+ * Wrap an already-parsed seonix graph (a parseEntities() result) as a tmct
20
+ * Repository-Interface service object.
21
+ * @param {object} graph a parseEntities() result ({ individuals, byId, relations, … })
22
+ * @param {{sourceAccess?: boolean}} [opts]
23
+ */
24
+ export function makeSeonixProvider(graph, { sourceAccess = false } = {}) {
25
+ return createGraphService(graph, { sourceAccess });
26
+ }
27
+
28
+ /**
29
+ * Load a seonix graph from a config ({ graphFile } or a registered provider) and
30
+ * wrap it as a tmct service. parseEntities expands the interned v2 payload.
31
+ * @param {object} config a seonix config ({ graphFile })
32
+ * @param {{sourceAccess?: boolean}} [opts]
33
+ */
34
+ export async function loadSeonixProvider(config, { sourceAccess = false } = {}) {
35
+ const graph = parseEntities(await fetchEntities(config));
36
+ return makeSeonixProvider(graph, { sourceAccess });
37
+ }
package/src/viz.mjs CHANGED
@@ -26,7 +26,8 @@ import { loadConfig } from "./config.mjs";
26
26
  import * as source from "./source.mjs";
27
27
  import { buildTemporalGraph, buildBrowserData, gitCommitOrder, gitCommitParents, renderBrowserHtml } from "./browser.mjs";
28
28
  import { extractTimeline, renderTimelineHtml } from "./timeline.mjs";
29
- import { winkBrowserBundle } from "./nlp-bundle.mjs";
29
+ // The wink lemma tier is gone (PLAN_CHAT_EXTRACTION.md Stage 6): the ask panel runs tmct's engine
30
+ // adapter-less, so there is no ~4 MB browser model to build. `--nlp` is now a no-op.
30
31
 
31
32
  // Portable single-file gate: above this serialized graph size, embedding the
32
33
  // full raw graph inline yields an unopenable multi-hundred-MB HTML, so the
@@ -189,17 +190,11 @@ export async function cytoscapeSource() {
189
190
  * render logic. `export`/`import` lines stripped so the three files run as one
190
191
  * plain <script> block; nothing here diverges from the node-tested source. */
191
192
  async function askSource() {
192
- const [codegraph, vocab, ask] = await Promise.all(
193
- ["codegraph.mjs", "ask-vocab.mjs", "ask.mjs"].map((f) => readFile(join(here, f), "utf8")),
194
- );
195
- // Non-greedy up to the closing `";` (not `$`-anchored): ask.mjs's own import spans
196
- // multiple lines (a multi-name destructure), which a single-line `^...$` pattern
197
- // silently leaves in place — and unlike a stray declaration, a surviving `import`
198
- // is a hard SyntaxError in a classic (non-module) inlined <script>, not a quiet bug.
199
- const strip = (src) => src
200
- .replace(/^import\s[\s\S]*?from\s+"[^"]+";\s*$/gm, "")
201
- .replace(/^export (?=(function|const|async function))/gm, "");
202
- return [strip(codegraph), strip(vocab), strip(ask)].join("\n");
193
+ // The panel's mechanical NL engine is tmct's, prebuilt by scripts/build-ask-bundle.mjs into an
194
+ // IIFE (src/ask-browser.bundle.js) that registers globalThis.ask / parseEntities. We inline it
195
+ // verbatim into the viewer page (PLAN_CHAT_EXTRACTION.md, the tmct inversion). It runs adapter-less
196
+ // in the browser, so the old ~4 MB wink lemma bundle is gone. Regenerate: `npm run build:ask-bundle`.
197
+ return readFile(join(here, "ask-browser.bundle.js"), "utf8");
203
198
  }
204
199
 
205
200
  /** JSON safe to sit inside a <script> block (no </script> or comment-open breakouts). */
@@ -875,9 +870,9 @@ export async function runVizCli(argv) {
875
870
  });
876
871
  const cytoscape = await cytoscapeSource();
877
872
  const askEngine = await askSource();
878
- // Build the wink lemma bundle once, only when --nlp asked for it (~4 MB; skip the
879
- // work entirely otherwise). Wired below as a sibling asset (site) or inline (portable).
880
- const nlpBundle = values.nlp ? await winkBrowserBundle() : null;
873
+ // The wink lemma tier was removed in Stage 6 (the panel runs tmct's engine adapter-less), so there
874
+ // is never a bundle to wire. `--nlp` is accepted but inert; the downstream null-guards no-op.
875
+ const nlpBundle = null;
881
876
  if (values.serve) {
882
877
  await startVizServer({ values, cytoscape, askEngine, nlpBundle, log: (s) => process.stderr.write(s) });
883
878
  return; // keeps serving until Ctrl-C
package/src/ask-nlp.mjs DELETED
@@ -1,73 +0,0 @@
1
- // ask-nlp.mjs — the OPTIONAL wink-nlp adapter behind ask.mjs's lemma/POS tier.
2
- //
3
- // BOUNDARY (hard, do not move): this file is Node-only and is NEVER inlined into
4
- // the viewer bundle. viz.mjs's askSource() inlines codegraph.mjs + ask-vocab.mjs +
5
- // ask.mjs ONLY and strips their import lines, so the portable single-file HTML has
6
- // no wink, no model, and no `nlpAdapter` binding at all — ask.mjs reaches this
7
- // module exclusively through a `typeof nlpAdapter === "function"` guard and
8
- // degrades to adapter-less parsing (lemma/POS tiers off; the curated tables and
9
- // the bounded edit-distance tier still work, browser and Node alike). Keeping the
10
- // ~1MB CJS model out of the page is the point of the split.
11
- //
12
- // wink-nlp and wink-eng-lite-web-model are CJS — loaded via createRequire, the
13
- // same Node-module-resolution approach viz.mjs uses to locate cytoscape (resolve
14
- // through the module system, never a guessed path). The require happens lazily on
15
- // first use and failure is cached as null: a checkout without the optional deps
16
- // installed answers exactly like the browser bundle, it never throws.
17
-
18
- import { createRequire } from "node:module";
19
-
20
- let cached; // undefined = not tried yet; null = unavailable (tried once, honestly off)
21
-
22
- /** Lazily build the {lemma, posTags} adapter, or null when wink isn't loadable.
23
- * Deterministic: wink-nlp's tagger/lemmatiser is a fixed model with no sampling,
24
- * so the same input always yields the same tags/lemmas across processes. */
25
- export function nlpAdapter() {
26
- if (cached !== undefined) return cached;
27
- try {
28
- const require = createRequire(import.meta.url);
29
- const winkNLP = require("wink-nlp");
30
- const model = require("wink-eng-lite-web-model");
31
- const nlp = winkNLP(model);
32
- const its = nlp.its;
33
- cached = {
34
- /** Lowercase lemma of a single token ("imported" -> "import"); the word
35
- * itself when wink has nothing better (unknown words come back as-is). */
36
- lemma(word) {
37
- const w = String(word || "");
38
- try {
39
- const out = nlp.readDoc(w).tokens().out(its.lemma);
40
- return String(out[0] || w).toLowerCase();
41
- } catch {
42
- return w.toLowerCase();
43
- }
44
- },
45
- /** UPOS tags aligned to the CALLER's word array. wink re-tokenizes (it
46
- * splits "walk.mjs" into three tokens), so each input word is greedily
47
- * matched to the run of wink tokens that spell it and takes its FIRST
48
- * sub-token's tag; null per word on any surprise, never a throw. */
49
- posTags(words) {
50
- try {
51
- const toks = nlp.readDoc(words.join(" ")).tokens();
52
- const texts = toks.out();
53
- const tags = toks.out(its.pos);
54
- const out = [];
55
- let k = 0;
56
- for (const w of words) {
57
- if (k >= texts.length) { out.push(null); continue; }
58
- out.push(tags[k]);
59
- let acc = texts[k];
60
- k += 1;
61
- while (acc.length < w.length && k < texts.length) { acc += texts[k]; k += 1; }
62
- }
63
- return out;
64
- } catch {
65
- return words.map(() => null);
66
- }
67
- },
68
- };
69
- } catch {
70
- cached = null;
71
- }
72
- return cached;
73
- }