@polycode-projects/seonix 0.2.1 → 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 +204 -25
- package/bin/cli.mjs +78 -18
- package/package.json +5 -3
- package/roslyn/Program.cs +79 -11
- package/src/ask-nlp.mjs +73 -0
- package/src/ask-vocab.mjs +370 -5
- package/src/ask.mjs +1523 -83
- package/src/browser.mjs +99 -19
- package/src/chat.mjs +785 -0
- package/src/codegraph.mjs +213 -5
- package/src/cs_treesitter.mjs +57 -34
- package/src/embed.mjs +191 -0
- package/src/extract.mjs +220 -39
- package/src/java_treesitter.mjs +82 -55
- package/src/jsts_tsc.mjs +51 -4
- package/src/nlp-bundle.mjs +120 -0
- package/src/prose-nlp.mjs +52 -0
- package/src/prose.mjs +42 -0
- package/src/schema-docs.mjs +29 -0
- package/src/server.mjs +27 -4
- package/src/sessions.mjs +206 -0
- package/src/temporal.mjs +70 -0
- package/src/timeline.mjs +160 -0
- package/src/viz.mjs +273 -83
package/src/browser.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { execFile } from "node:child_process";
|
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
|
-
import { deriveNodeInterval, deriveEdgeInterval } from "./temporal.mjs";
|
|
17
|
+
import { deriveNodeInterval, deriveEdgeInterval, repoOfId, assignRepo, isMultiRepo } from "./temporal.mjs";
|
|
18
18
|
|
|
19
19
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const execFileP = promisify(execFile);
|
|
@@ -79,8 +79,22 @@ const attr = (ind, key) => {
|
|
|
79
79
|
return a ? String(a.value) : "";
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Ordered (oldest → newest) shas for the graph's Commit individuals via git log.
|
|
84
|
+
*
|
|
85
|
+
* A MERGED multi-repo graph (extract.mjs indexRepositories) has no single git repo to
|
|
86
|
+
* shell — `repo` is the common-ancestor out_root, which is usually not itself a git
|
|
87
|
+
* checkout — so `git log` there returns nothing that matches. In that case, if the
|
|
88
|
+
* caller supplies `datesBySha` (commit sha → ISO commit date), fall back to a global
|
|
89
|
+
* chronological order across all repos. Single-repo callers pass no dates and keep the
|
|
90
|
+
* existing git path unchanged. (buildTemporalGraph independently re-derives the
|
|
91
|
+
* date-order when it detects a merge, so a viz caller that passes only cwd is still
|
|
92
|
+
* correct; this fallback makes gitCommitOrder itself repo-set-agnostic and testable.)
|
|
93
|
+
* @param {string} repo
|
|
94
|
+
* @param {Iterable<string>} graphCommitIds
|
|
95
|
+
* @param {{datesBySha?: Map<string,string>|null}} [opts]
|
|
96
|
+
*/
|
|
97
|
+
export async function gitCommitOrder(repo, graphCommitIds, { datesBySha = null } = {}) {
|
|
84
98
|
const shas = new Set([...graphCommitIds].map((id) => String(id).replace(/^commit:/, "")));
|
|
85
99
|
let ordered = [];
|
|
86
100
|
try {
|
|
@@ -89,7 +103,18 @@ export async function gitCommitOrder(repo, graphCommitIds) {
|
|
|
89
103
|
});
|
|
90
104
|
ordered = stdout.trim().split("\n").reverse().filter((s) => shas.has(s));
|
|
91
105
|
} catch {
|
|
92
|
-
/* fall through to graph order */
|
|
106
|
+
/* fall through to graph/date order */
|
|
107
|
+
}
|
|
108
|
+
if (!ordered.length && datesBySha) {
|
|
109
|
+
// Merged-graph fallback: one commit date axis across every repo (tie-break by sha
|
|
110
|
+
// so equal timestamps stay deterministic).
|
|
111
|
+
ordered = [...shas].sort((a, b) => {
|
|
112
|
+
const da = datesBySha.get(a) || "";
|
|
113
|
+
const db = datesBySha.get(b) || "";
|
|
114
|
+
if (da < db) return -1;
|
|
115
|
+
if (da > db) return 1;
|
|
116
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
117
|
+
});
|
|
93
118
|
}
|
|
94
119
|
if (!ordered.length) ordered = [...shas];
|
|
95
120
|
return ordered;
|
|
@@ -137,9 +162,40 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
137
162
|
|
|
138
163
|
const commitInds = individuals.filter((i) => i.class === "Commit");
|
|
139
164
|
const commitById = new Map(commitInds.map((c) => [c.id, c]));
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
165
|
+
|
|
166
|
+
// P4 cross-repo: a commit's repo is inferred from the id-prefixes of the modules it
|
|
167
|
+
// touches (touchesSymbol edges) — commits themselves are unprefixed (one per sha).
|
|
168
|
+
// This is order-independent, so we can detect a merge BEFORE choosing the time axis.
|
|
169
|
+
const prefixByCommit = new Map(); // commit id -> Map<repoPrefix, touch count>
|
|
170
|
+
for (const g of groups) {
|
|
171
|
+
if (kindOf(g) !== "touchesSymbol") continue;
|
|
172
|
+
for (const e of g.examples || []) {
|
|
173
|
+
const prefix = repoOfId(e.object);
|
|
174
|
+
if (!prefix || !commitById.has(e.subject)) continue;
|
|
175
|
+
let m = prefixByCommit.get(e.subject);
|
|
176
|
+
if (!m) { m = new Map(); prefixByCommit.set(e.subject, m); }
|
|
177
|
+
m.set(prefix, (m.get(prefix) || 0) + 1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const multi = isMultiRepo(commitInds.map((c) => prefixByCommit.get(c.id) || new Map()));
|
|
181
|
+
const repoOfCommitId = (id) => (multi ? assignRepo(prefixByCommit.get(id) || new Map()) : null);
|
|
182
|
+
|
|
183
|
+
// Single-repo: git-log order (or the given order) exactly as before. Merged graph:
|
|
184
|
+
// there is no single repo to shell, so order by the stored commit date across ALL
|
|
185
|
+
// repos — one global time axis, tie-broken by sha for determinism.
|
|
186
|
+
const order = multi
|
|
187
|
+
? [...commitInds]
|
|
188
|
+
.sort((a, b) => {
|
|
189
|
+
const da = attr(a, "date");
|
|
190
|
+
const db = attr(b, "date");
|
|
191
|
+
if (da < db) return -1;
|
|
192
|
+
if (da > db) return 1;
|
|
193
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
194
|
+
})
|
|
195
|
+
.map((c) => String(c.id).replace(/^commit:/, ""))
|
|
196
|
+
: (orderShas && orderShas.length)
|
|
197
|
+
? orderShas
|
|
198
|
+
: commitInds.map((c) => String(c.id).replace(/^commit:/, ""));
|
|
143
199
|
const ordinalOf = new Map(order.map((sha, i) => [`commit:${sha}`, i]));
|
|
144
200
|
const headIdx = Math.max(0, order.length - 1);
|
|
145
201
|
const commits = order.map((sha, i) => {
|
|
@@ -155,7 +211,7 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
155
211
|
if (pi != null) parentIdx.push(pi);
|
|
156
212
|
else ghostParents += 1;
|
|
157
213
|
}
|
|
158
|
-
|
|
214
|
+
const commit = {
|
|
159
215
|
idx: i,
|
|
160
216
|
sha,
|
|
161
217
|
shortSha: sha.slice(0, 12),
|
|
@@ -166,6 +222,12 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
166
222
|
ghostParents,
|
|
167
223
|
merge: parentIdx.length + ghostParents > 1,
|
|
168
224
|
};
|
|
225
|
+
// P4: tag with the owning repo ONLY on a merged graph (single-repo stays identical).
|
|
226
|
+
if (multi) {
|
|
227
|
+
const r = repoOfCommitId(`commit:${sha}`);
|
|
228
|
+
if (r) commit.repo = r;
|
|
229
|
+
}
|
|
230
|
+
return commit;
|
|
169
231
|
});
|
|
170
232
|
|
|
171
233
|
const symInds = individuals.filter((i) => i.class !== "Commit" && inScope(i.id));
|
|
@@ -232,7 +294,7 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
232
294
|
const touches = [...(touchesByNode.get(ind.id) || [])].sort((a, b) => a - b);
|
|
233
295
|
const [born, died] = deriveNodeInterval(touches, headIdx, fallbackBorn(ind.id));
|
|
234
296
|
bornOf.set(ind.id, born);
|
|
235
|
-
|
|
297
|
+
const node = {
|
|
236
298
|
id: ind.id,
|
|
237
299
|
type: ind.class,
|
|
238
300
|
label: ind.label || ind.id,
|
|
@@ -241,13 +303,19 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
241
303
|
died,
|
|
242
304
|
touches,
|
|
243
305
|
churn: touches.length,
|
|
244
|
-
}
|
|
306
|
+
};
|
|
307
|
+
// P4: a symbol's repo is right in its id prefix on a merged graph — carry it so the
|
|
308
|
+
// browser can badge by repo without re-parsing. Guarded → single-repo unchanged.
|
|
309
|
+
if (multi) { const r = repoOfId(ind.id); if (r) node.repo = r; }
|
|
310
|
+
nodes.push(node);
|
|
245
311
|
}
|
|
246
312
|
for (const c of commits) {
|
|
247
|
-
|
|
313
|
+
const node = {
|
|
248
314
|
id: `commit:${c.sha}`, type: "Commit", label: c.shortSha, site: "",
|
|
249
315
|
born: c.idx, died: headIdx, touches: [], churn: 0, commitIdx: c.idx,
|
|
250
|
-
}
|
|
316
|
+
};
|
|
317
|
+
if (multi && c.repo) node.repo = c.repo;
|
|
318
|
+
nodes.push(node);
|
|
251
319
|
bornOf.set(`commit:${c.sha}`, c.idx);
|
|
252
320
|
}
|
|
253
321
|
|
|
@@ -289,9 +357,13 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
289
357
|
return {
|
|
290
358
|
meta: {
|
|
291
359
|
scope,
|
|
292
|
-
commit_order:
|
|
360
|
+
commit_order: multi
|
|
361
|
+
? "oldest-first by commit date across all repos (merged multi-repo graph)"
|
|
362
|
+
: "oldest-first (ordinal 0 = oldest, N-1 = HEAD)",
|
|
293
363
|
note: "died = HEAD for every node: a HEAD-only index cannot observe deletions.",
|
|
294
364
|
counts: { commits: commits.length, nodes: nodes.length, edges: edges.length, touchesSymbol_edges: tsEdges.length },
|
|
365
|
+
// P4: present ONLY on a merged graph, so single-repo meta is byte-identical.
|
|
366
|
+
...(multi ? { multi_repo: true, repos: [...new Set(commits.map((c) => c.repo).filter(Boolean))].sort() } : {}),
|
|
295
367
|
},
|
|
296
368
|
commits,
|
|
297
369
|
nodes,
|
|
@@ -305,8 +377,10 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
305
377
|
* the page's whole view state already round-trips through the URL (P1) — a plain
|
|
306
378
|
* reload on HEAD change is enough to pick up a re-index without losing the cursor,
|
|
307
379
|
* selection, or query. */
|
|
308
|
-
export function buildBrowserData(tg, { repoUrl = "", repoRef = "main", siteNav = false, live = false, gitHead = "" } = {}) {
|
|
309
|
-
|
|
380
|
+
export function buildBrowserData(tg, { repoUrl = "", repoRef = "main", siteNav = false, live = false, gitHead = "", nav = null } = {}) {
|
|
381
|
+
// `nav` ({name: href}) rebuilds the page's #sitenav; `siteNav` is the legacy
|
|
382
|
+
// flag — stale deployed data keeps its absolute links.
|
|
383
|
+
return { ...tg, repoUrl: String(repoUrl).replace(/\/+$/, ""), repoRef, siteNav: !!siteNav, ...(nav ? { nav } : {}), live: !!live, gitHead };
|
|
310
384
|
}
|
|
311
385
|
|
|
312
386
|
/** The tested temporal core, ready to inline into the page (exports stripped). */
|
|
@@ -430,7 +504,8 @@ function init(tg){
|
|
|
430
504
|
for(const n of tg.nodes)S.byId.set(n.id,n);
|
|
431
505
|
document.title='seon chronograph — '+(tg.meta&&tg.meta.scope||'');
|
|
432
506
|
$('scopelabel').textContent=(tg.meta&&tg.meta.scope?'· '+tg.meta.scope:'')+' · '+tg.commits.length+' commits · '+tg.nodes.length+' nodes'+(tg.live?' · live':'');
|
|
433
|
-
if(tg.
|
|
507
|
+
if(tg.nav){$('sitenav').innerHTML=Object.entries(tg.nav).map(([k,h])=>'<a href="'+esc(h)+'">'+esc(k)+'</a>').join('');$('sitenav').hidden=false;}
|
|
508
|
+
else if(tg.siteNav)$('sitenav').hidden=false;
|
|
434
509
|
// ---- URL state in (query-as-URL: the whole view is the link) ----
|
|
435
510
|
const st=decodeViewState(location.search);
|
|
436
511
|
if(st.q){S.q=st.q;$('q').value=st.q;S.qSet=matchQuery(tg,st.q);}
|
|
@@ -477,9 +552,12 @@ function syncUrl(){
|
|
|
477
552
|
const full=(keep?'data='+encodeURIComponent(keep)+(qs?'&':''):'')+qs;
|
|
478
553
|
history.replaceState(null,'',full?('?'+full):location.pathname);
|
|
479
554
|
}
|
|
555
|
+
// Legend chips per type GROUP — Function/Method share one chip (legend-only
|
|
556
|
+
// merge; node fills stay distinct), counts summed across the group.
|
|
557
|
+
const GROUPS=[['Module'],['Class'],['Function','Method'],['Attribute'],['GlobalVariable'],['Commit']];
|
|
480
558
|
function buildChips(){
|
|
481
|
-
$('legend').innerHTML=
|
|
482
|
-
document.querySelectorAll('.typechk').forEach(c=>c.addEventListener('change',()=>{c.checked?S.enabled.add(
|
|
559
|
+
$('legend').innerHTML=GROUPS.map(g=>'<label class="lg" title="show/hide '+g.join('/')+' nodes"><input type="checkbox" class="typechk" data-cls="'+g.join(',')+'"'+(g.every(t=>S.enabled.has(t))?' checked':'')+'>'+g.map(t=>'<i style="background:'+COLORS[t]+'"></i>').join('')+g.join('/')+'<span class="cnt" data-cnt="'+g.join(',')+'"></span></label>').join(' ');
|
|
560
|
+
document.querySelectorAll('.typechk').forEach(c=>c.addEventListener('change',()=>{c.dataset.cls.split(',').forEach(t=>c.checked?S.enabled.add(t):S.enabled.delete(t));render();}));
|
|
483
561
|
}
|
|
484
562
|
function buildCy(tg){
|
|
485
563
|
const els=[];
|
|
@@ -562,7 +640,7 @@ function render(){
|
|
|
562
640
|
const vis={},tot={};
|
|
563
641
|
S.tg.nodes.forEach(n=>{tot[n.type]=(tot[n.type]||0)+1;});
|
|
564
642
|
cy.nodes().forEach(cn=>{if(cn.style('display')==='element'){const t=cn.data('type');vis[t]=(vis[t]||0)+1;}});
|
|
565
|
-
document.querySelectorAll('.cnt').forEach(sp=>{const
|
|
643
|
+
document.querySelectorAll('.cnt').forEach(sp=>{const ks=sp.dataset.cnt.split(',');sp.textContent=ks.reduce((a,k)=>a+(vis[k]||0),0)+'/'+ks.reduce((a,k)=>a+(tot[k]||0),0);});
|
|
566
644
|
// narration: single cursor = the commit's story; two cursors = the range's story
|
|
567
645
|
$('narr').textContent=cmp?narrateRange(S.tg,lo,hi):narrateCommit(S.tg,idx);
|
|
568
646
|
$('qnote').style.display=(S.qSet&&S.qSet.size===0)?'block':'none';
|
|
@@ -695,6 +773,7 @@ function drawTimeline(){
|
|
|
695
773
|
const mergeNote=c&&c.merge?' <span style="color:#565f89">· ⑂ merge ('+(c.parentIdx.length+c.ghostParents)+' parent'+((c.parentIdx.length+c.ghostParents)===1?'':'s')+')</span>':'';
|
|
696
774
|
$('tlmeta').innerHTML=(c?('<b>'+esc(c.shortSha)+'</b> · '+(c.date||'').slice(0,10)+' · '+esc(c.author)+' — '+esc(c.subject)):'')
|
|
697
775
|
+mergeNote
|
|
776
|
+
+(c&&c.repo?' <span style="color:#7aa2f7">· ⛃ '+esc(c.repo)+'</span>':'')
|
|
698
777
|
+(S.cursorB!=null?' <span style="color:#e0af68">· B: '+esc(shortAt(S.cursorB))+'</span>':'')
|
|
699
778
|
+' <span style="color:#565f89">('+(S.cursor+1)+' / '+S.tg.commits.length+')</span>';
|
|
700
779
|
}
|
|
@@ -729,6 +808,7 @@ function showBiography(id){
|
|
|
729
808
|
:'';
|
|
730
809
|
$('detail').innerHTML='<h3>'+esc(c.shortSha)+'</h3>'
|
|
731
810
|
+'<span class="badge"><i style="background:'+COLORS.Commit+'"></i>Commit '+(c.idx+1)+' of '+S.tg.commits.length+'</span>'
|
|
811
|
+
+(c.repo?' <span class="badge">repo '+esc(c.repo)+'</span>':'')
|
|
732
812
|
+'<div class="row">'+esc(c.author)+' · '+(c.date||'').slice(0,10)+'</div><div class="row"><em>'+esc(c.subject)+'</em></div>'
|
|
733
813
|
+'<div class="row">'+(url?'<a class="btn" href="'+url+'" target="_blank" rel="noopener">open in GitLab ↗</a>':'')
|
|
734
814
|
+'<button class="btn" id="gocommit">scrub here</button></div>'
|