@polycode-projects/seonix 0.3.0 → 0.4.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.
- package/README.md +93 -23
- package/bin/cli.mjs +9 -3
- package/package.json +1 -1
- package/src/ask-vocab.mjs +198 -2
- package/src/ask.mjs +824 -8
- package/src/browser.mjs +87 -13
- package/src/chat.mjs +644 -47
- package/src/codegraph.mjs +55 -2
- package/src/nlp-bundle.mjs +120 -0
- package/src/prose.mjs +42 -0
- package/src/temporal.mjs +70 -0
- package/src/timeline.mjs +51 -8
- package/src/viz.mjs +88 -19
package/src/viz.mjs
CHANGED
|
@@ -26,6 +26,7 @@ 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
30
|
|
|
30
31
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
32
|
const execFileP = promisify(execFile);
|
|
@@ -203,12 +204,24 @@ const inlineJson = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
|
203
204
|
* silently produce incomplete answers, which this project's honesty contract
|
|
204
205
|
* doesn't allow (see the file-level comment's "ONE viewer, three data channels" —
|
|
205
206
|
* this is a fourth, for the same one-viewer-many-modes reason).
|
|
207
|
+
*
|
|
208
|
+
* The OPTIONAL wink-nlp lemma tier (ask-nlp.mjs's browser twin, from nlp-bundle.mjs)
|
|
209
|
+
* reaches the chat two ways: `nlpInline` inlines the ~4 MB bundle as its own <script>
|
|
210
|
+
* so it registers `window.__seonixNlp` at page load (portable `viz --nlp`); `nlpPath`
|
|
211
|
+
* points the page at a same-origin sibling asset the chat LAZY-loads on first ask
|
|
212
|
+
* (the SITE build). Neither is present in the default local single-file — that viewer
|
|
213
|
+
* stays lemma-off and fetches nothing, exactly as before, preserving its
|
|
214
|
+
* no-external-fetch guarantee. ask() picks up whatever `window.__seonixNlp` is set,
|
|
215
|
+
* or degrades to the adapter-less tiers when it's absent.
|
|
206
216
|
*/
|
|
207
|
-
export function renderViewerHtml({ cytoscape, askEngine = "", embedData = null, dataPath = null, askData = null, askDataPath = null } = {}) {
|
|
217
|
+
export function renderViewerHtml({ cytoscape, askEngine = "", embedData = null, dataPath = null, askData = null, askDataPath = null, nlpInline = null, nlpPath = null } = {}) {
|
|
208
218
|
const embedded = embedData ? `<script type="application/json" id="seonix-data">${inlineJson(embedData)}</script>\n` : "";
|
|
209
219
|
const askEmbedded = askData ? `<script type="application/json" id="seonix-ask-data">${inlineJson(askData)}</script>\n` : "";
|
|
210
|
-
const cfgObj = { ...(dataPath ? { data: dataPath } : {}), ...(askDataPath ? { ask: askDataPath } : {}) };
|
|
220
|
+
const cfgObj = { ...(dataPath ? { data: dataPath } : {}), ...(askDataPath ? { ask: askDataPath } : {}), ...(nlpPath ? { nlp: nlpPath } : {}) };
|
|
211
221
|
const cfg = Object.keys(cfgObj).length ? `<script type="application/json" id="seonix-cfg">${inlineJson(cfgObj)}</script>\n` : "";
|
|
222
|
+
// Inline bundle: escape any literal </script inside the wink model data so the
|
|
223
|
+
// block can't be closed early (harmless inside JS strings, impossible outside them).
|
|
224
|
+
const nlpEmbedded = nlpInline ? `<script>${String(nlpInline).replace(/<\/script/gi, "<\\/script")}</script>\n` : "";
|
|
212
225
|
return `<!doctype html><html><head><meta charset="utf-8"><title>seon code-map</title>
|
|
213
226
|
<!-- generated by: seonix viz (see packages/seonix/src/viz.mjs) — one viewer, data loaded at runtime -->
|
|
214
227
|
<style>
|
|
@@ -226,13 +239,20 @@ export function renderViewerHtml({ cytoscape, askEngine = "", embedData = null,
|
|
|
226
239
|
#depthval{min-width:1.2em;text-align:center;display:inline-block;cursor:help}
|
|
227
240
|
#hub,#beam{width:3.5em;background:#1a1b26;color:#c0caf5;border:1px solid #2a2e42;border-radius:4px;padding:1px 4px;font:inherit}
|
|
228
241
|
#legend{padding:4px 12px 6px;background:#16161e;border-bottom:1px solid #2a2e42;display:flex;flex-wrap:wrap;gap:2px 12px;font-size:12px;flex:0 0 auto}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
/* Graph on top (full width, generous height); the chat + node-detail ride a
|
|
243
|
+
full-width row BENEATH it (chat grows, detail a compact fixed column). On a
|
|
244
|
+
narrow screen #below stacks so the body never scrolls sideways. */
|
|
245
|
+
#main{flex:1 1 auto;display:flex;flex-direction:column;min-height:0}
|
|
246
|
+
#cy{flex:1 1 auto;position:relative;min-height:260px}
|
|
247
|
+
#below{flex:0 0 auto;display:flex;background:#16161e;border-top:1px solid #2a2e42;max-height:46vh;box-sizing:border-box}
|
|
248
|
+
#ask{flex:1 1 auto;min-width:0;padding:14px 16px;overflow:auto;box-sizing:border-box;display:flex;flex-direction:column}
|
|
249
|
+
#detail{flex:0 0 300px;padding:14px;overflow:auto;border-left:1px solid #2a2e42;box-sizing:border-box}
|
|
250
|
+
@media(max-width:720px){#below{flex-direction:column;max-height:58vh;overflow:auto}#detail{flex:0 0 auto;border-left:0;border-top:1px solid #2a2e42}}
|
|
232
251
|
.lg{white-space:nowrap;cursor:pointer;user-select:none;color:#a9b1d6}.lg i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:4px;vertical-align:middle}.lg input{vertical-align:middle;margin:0 3px 0 0}
|
|
233
252
|
.lg .cnt{color:#565f89;margin-left:3px;font-size:11px;font-variant-numeric:tabular-nums}
|
|
234
253
|
select{background:#1a1b26;color:#c0caf5;border:1px solid #2a2e42;border-radius:4px;font:inherit}
|
|
235
|
-
#
|
|
254
|
+
#detail h3{margin:2px 0 8px;color:#c0caf5;font-size:15px;word-break:break-all}
|
|
255
|
+
#ask h4{margin:0 0 8px;color:#a9b1d6;font-size:11px;text-transform:uppercase;letter-spacing:.04em}
|
|
236
256
|
.badge{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border:1px solid #2a2e42;border-radius:10px;font-size:11px;color:#a9b1d6}
|
|
237
257
|
.badge i{width:8px;height:8px;border-radius:50%;display:inline-block}
|
|
238
258
|
#detail dl{display:grid;grid-template-columns:auto 1fr;gap:2px 12px;margin:10px 0;font-size:12px;color:#c0caf5}
|
|
@@ -243,11 +263,10 @@ export function renderViewerHtml({ cytoscape, askEngine = "", embedData = null,
|
|
|
243
263
|
.hint{color:#565f89;font-size:11px;margin-top:10px}
|
|
244
264
|
.empty{color:#a9b1d6;font-size:12px;line-height:1.7}
|
|
245
265
|
code{color:#9aa5ce;word-break:break-all}
|
|
246
|
-
#ask{padding-bottom:12px;margin-bottom:12px;border-bottom:1px solid #2a2e42}
|
|
247
266
|
.askrow{display:flex;gap:6px}
|
|
248
|
-
#askq{flex:1;min-width:0;background:#1a1b26;color:#c0caf5;border:1px solid #2a2e42;border-radius:4px;padding:
|
|
267
|
+
#askq{flex:1;min-width:0;background:#1a1b26;color:#c0caf5;border:1px solid #2a2e42;border-radius:4px;padding:7px 10px;font:inherit;font-size:13px}
|
|
249
268
|
#askq:focus{outline:none;border-color:#7aa2f7}
|
|
250
|
-
.askresult{margin-top:
|
|
269
|
+
.askresult{margin-top:10px;flex:1 1 auto;font-size:12.5px;line-height:1.6;color:#c0caf5;overflow:auto}
|
|
251
270
|
.askresult.miss{color:#a9b1d6;font-style:italic}
|
|
252
271
|
.askresult .askq-echo{color:#565f89;margin-bottom:4px;font-style:normal}
|
|
253
272
|
</style></head><body>
|
|
@@ -269,8 +288,9 @@ export function renderViewerHtml({ cytoscape, askEngine = "", embedData = null,
|
|
|
269
288
|
<div id="legend"></div>
|
|
270
289
|
<div id="main">
|
|
271
290
|
<div id="cy"></div>
|
|
272
|
-
<div id="
|
|
291
|
+
<div id="below">
|
|
273
292
|
<div id="ask">
|
|
293
|
+
<h4>Ask the graph</h4>
|
|
274
294
|
<div class="askrow"><input id="askq" type="text" autocomplete="off" placeholder='ask e.g. "what calls this"'><button class="btn" id="asksubmit">ask</button></div>
|
|
275
295
|
<div id="askresult" class="askresult"></div>
|
|
276
296
|
</div>
|
|
@@ -279,7 +299,7 @@ export function renderViewerHtml({ cytoscape, askEngine = "", embedData = null,
|
|
|
279
299
|
</div>
|
|
280
300
|
${embedded}${askEmbedded}${cfg}<script>${cytoscape}</script>
|
|
281
301
|
<script>${askEngine}</script>
|
|
282
|
-
<script>
|
|
302
|
+
${nlpEmbedded}<script>
|
|
283
303
|
// Type palette (see the CVD note in viz.mjs) — viewer styling, identical for every repo.
|
|
284
304
|
const COLORS={Module:'#3987e5',Class:'#c98500',Function:'#008300',Method:'#d55181',Attribute:'#9085e9',GlobalVariable:'#d95926',Commit:'#199e70'};
|
|
285
305
|
const $=id=>document.getElementById(id);
|
|
@@ -322,6 +342,29 @@ function loadAskData(){
|
|
|
322
342
|
})();
|
|
323
343
|
return _askGraphPromise;
|
|
324
344
|
}
|
|
345
|
+
// The OPTIONAL wink-nlp lemma adapter (window.__seonixNlp — see nlp-bundle.mjs).
|
|
346
|
+
// An inlined bundle registers it at page load; the site build ships it as a
|
|
347
|
+
// same-origin sibling this injects LAZILY on the first ask (never at boot), so the
|
|
348
|
+
// ~4 MB model is only paid for when the chat is actually used. With neither present
|
|
349
|
+
// (the default local single-file) this resolves to undefined and the chat runs
|
|
350
|
+
// adapter-less — ask() then falls back to its curated + fuzzy tiers, exactly as
|
|
351
|
+
// before, and NOTHING is fetched, keeping the no-external-fetch guarantee intact.
|
|
352
|
+
let _nlpPromise=null;
|
|
353
|
+
function loadNlp(){
|
|
354
|
+
if(window.__seonixNlp) return Promise.resolve(window.__seonixNlp);
|
|
355
|
+
if(_nlpPromise) return _nlpPromise;
|
|
356
|
+
const cfgEl=document.getElementById('seonix-cfg');
|
|
357
|
+
const url=cfgEl?JSON.parse(cfgEl.textContent).nlp:null;
|
|
358
|
+
if(!url) return Promise.resolve(undefined);
|
|
359
|
+
_nlpPromise=new Promise(res=>{
|
|
360
|
+
const s=document.createElement('script');
|
|
361
|
+
s.src=url;
|
|
362
|
+
s.onload=()=>res(window.__seonixNlp);
|
|
363
|
+
s.onerror=()=>res(undefined); // a failed asset degrades to lemma-off, never a broken chat
|
|
364
|
+
document.head.appendChild(s);
|
|
365
|
+
});
|
|
366
|
+
return _nlpPromise;
|
|
367
|
+
}
|
|
325
368
|
function init(G){
|
|
326
369
|
document.title='seon code-map — '+G.focusLabel;
|
|
327
370
|
$('focuslabel').textContent=G.focusLabel;
|
|
@@ -564,8 +607,10 @@ function init(G){
|
|
|
564
607
|
return;
|
|
565
608
|
}
|
|
566
609
|
out.textContent='thinking…';
|
|
567
|
-
|
|
568
|
-
|
|
610
|
+
// Load the graph and (if configured) the wink lemma adapter together, then run
|
|
611
|
+
// the mechanical ask. An undefined nlp makes ask() degrade to its adapter-less tiers.
|
|
612
|
+
Promise.all([loadAskData(),loadNlp()]).then(([askGraph,nlp])=>{
|
|
613
|
+
const {content,seonix_ask}=ask(askGraph,query,{contextId:selId,nlp});
|
|
569
614
|
const total=(seonix_ask.matches||[]).length;
|
|
570
615
|
const shown=total?highlightAsk(seonix_ask.matches.map(m=>m.id)):0;
|
|
571
616
|
out.classList.toggle('miss',!!seonix_ask.miss);
|
|
@@ -647,10 +692,12 @@ function resolveFocus(graph, focusArg) {
|
|
|
647
692
|
* code browser rides along at `/code-browser.html` (alias `/browser`) with a live
|
|
648
693
|
* temporal payload — the local equivalent of the site's browser section.
|
|
649
694
|
*/
|
|
650
|
-
export async function startVizServer({ values, cytoscape, askEngine = "", log = () => {} }) {
|
|
695
|
+
export async function startVizServer({ values, cytoscape, askEngine = "", nlpBundle = null, log = () => {} }) {
|
|
651
696
|
const port = Math.max(0, parseInt(values.port, 10) || 0);
|
|
652
697
|
const repoUrl = values["repo-url"] || (await repoUrlFromGit(process.cwd()));
|
|
653
|
-
|
|
698
|
+
// With --nlp the local live view gets the same lemma tier as the site: the bundle
|
|
699
|
+
// is served in-process at /seonix-nlp.js and the viewer lazy-loads it on first ask.
|
|
700
|
+
const viewer = renderViewerHtml({ cytoscape, askEngine, nlpPath: nlpBundle ? "/seonix-nlp.js" : null });
|
|
654
701
|
const browserPage = await renderBrowserHtml({ cytoscape });
|
|
655
702
|
// nav = the routes this server itself serves; injected into every payload/page
|
|
656
703
|
const nav = { home: "/", graph: "/seonix-graph.html", browser: "/code-browser.html", timeline: "/timeline.html" };
|
|
@@ -748,6 +795,9 @@ export async function startVizServer({ values, cytoscape, askEngine = "", log =
|
|
|
748
795
|
res.writeHead(500, { "content-type": "application/json" });
|
|
749
796
|
res.end(JSON.stringify({ error: String(err.message || err) }));
|
|
750
797
|
}
|
|
798
|
+
} else if (path === "/seonix-nlp.js" && nlpBundle) {
|
|
799
|
+
res.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
|
|
800
|
+
res.end(nlpBundle);
|
|
751
801
|
} else {
|
|
752
802
|
res.writeHead(404, { "content-type": "text/plain" });
|
|
753
803
|
res.end("not found");
|
|
@@ -782,13 +832,21 @@ export async function runVizCli(argv) {
|
|
|
782
832
|
"timeline-out": { type: "string" },
|
|
783
833
|
"graph-only": { type: "boolean", default: false },
|
|
784
834
|
scope: { type: "string", default: "product" },
|
|
835
|
+
// Include the OPTIONAL wink-nlp lemma tier in the chat (see nlp-bundle.mjs).
|
|
836
|
+
// Site mode (--data-out): written as a same-origin sibling the page lazy-loads.
|
|
837
|
+
// Portable/serve mode: inlined (single-file) / served in-process. Off by default,
|
|
838
|
+
// so the local single-file stays lean and lemma-off exactly as before.
|
|
839
|
+
nlp: { type: "boolean", default: false },
|
|
785
840
|
},
|
|
786
841
|
allowPositionals: false,
|
|
787
842
|
});
|
|
788
843
|
const cytoscape = await cytoscapeSource();
|
|
789
844
|
const askEngine = await askSource();
|
|
845
|
+
// Build the wink lemma bundle once, only when --nlp asked for it (~4 MB; skip the
|
|
846
|
+
// work entirely otherwise). Wired below as a sibling asset (site) or inline (portable).
|
|
847
|
+
const nlpBundle = values.nlp ? await winkBrowserBundle() : null;
|
|
790
848
|
if (values.serve) {
|
|
791
|
-
await startVizServer({ values, cytoscape, askEngine, log: (s) => process.stderr.write(s) });
|
|
849
|
+
await startVizServer({ values, cytoscape, askEngine, nlpBundle, log: (s) => process.stderr.write(s) });
|
|
792
850
|
return; // keeps serving until Ctrl-C
|
|
793
851
|
}
|
|
794
852
|
const { config, graph, raw } = await loadGraph(values);
|
|
@@ -838,19 +896,30 @@ export async function runVizCli(argv) {
|
|
|
838
896
|
const rel = relative(dirname(out), dataOut) || "seonix-graph-data.json";
|
|
839
897
|
const askDataOut = resolve(dirname(out), "seonix-ask-data.json");
|
|
840
898
|
const askRel = relative(dirname(out), askDataOut) || "seonix-ask-data.json";
|
|
899
|
+
// Site build: the wink lemma bundle rides the same sibling-asset convention as
|
|
900
|
+
// the data files — a same-origin ./seonix-nlp.js the page lazy-loads on first ask.
|
|
901
|
+
let nlpPath = null;
|
|
902
|
+
if (nlpBundle) {
|
|
903
|
+
const nlpOut = resolve(dirname(out), "seonix-nlp.js");
|
|
904
|
+
await writeFile(nlpOut, nlpBundle);
|
|
905
|
+
nlpPath = "./seonix-nlp.js";
|
|
906
|
+
}
|
|
841
907
|
await Promise.all([writeFile(dataOut, JSON.stringify(data)), writeFile(askDataOut, JSON.stringify(raw))]);
|
|
842
908
|
await writeFile(out, renderViewerHtml({
|
|
843
909
|
cytoscape, askEngine,
|
|
844
910
|
dataPath: rel === "seonix-graph-data.json" ? null : rel,
|
|
845
911
|
askDataPath: askRel === "seonix-ask-data.json" ? null : askRel,
|
|
912
|
+
nlpPath,
|
|
846
913
|
}));
|
|
847
914
|
process.stderr.write(
|
|
848
915
|
`seonix viz: ${subgraph.nodes.length} nodes, ${subgraph.edges.length} edges around "${focus.label}" ` +
|
|
849
|
-
`(depth ${values.depth})${subgraph.truncated ? " [capped]" : ""} → ${out} + ${dataOut} + ${askDataOut}
|
|
916
|
+
`(depth ${values.depth})${subgraph.truncated ? " [capped]" : ""} → ${out} + ${dataOut} + ${askDataOut}` +
|
|
917
|
+
`${nlpPath ? ` + ${resolve(dirname(out), "seonix-nlp.js")} (wink lemma tier)` : ""}\n`,
|
|
850
918
|
);
|
|
851
919
|
} else {
|
|
852
|
-
// portable mode: one self-contained file, both channels embedded
|
|
853
|
-
|
|
920
|
+
// portable mode: one self-contained file, both channels embedded (+ inline wink
|
|
921
|
+
// bundle when --nlp; otherwise lemma-off, no external fetch, exactly as before)
|
|
922
|
+
await writeFile(out, renderViewerHtml({ cytoscape, askEngine, embedData: data, askData: raw, nlpInline: nlpBundle }));
|
|
854
923
|
process.stderr.write(
|
|
855
924
|
`seonix viz: ${subgraph.nodes.length} nodes, ${subgraph.edges.length} edges around "${focus.label}" ` +
|
|
856
925
|
`(depth ${values.depth})${subgraph.truncated ? " [capped]" : ""} → ${out}\n`,
|