@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/timeline.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// timeline.mjs — render the repo's commit timeline to a single, portable HTML
|
|
2
|
+
// page from a seonix graph artifact. Third sibling of the viz viewer and the
|
|
3
|
+
// Chronograph code browser: generated by `seonix viz` (--timeline-out, on by
|
|
4
|
+
// default next to --out) and served live at /timeline.html by `viz --serve`.
|
|
5
|
+
//
|
|
6
|
+
// Data comes straight from the graph artifact: Commit individuals carry the full
|
|
7
|
+
// sha (id), author, date, and message; each Module's `derived_from` lists the
|
|
8
|
+
// commits that touched it (`git:<short sha>`), which gives per-commit churn.
|
|
9
|
+
// Session individuals (chat sessions recorded by sessions.mjs) join the same
|
|
10
|
+
// timeline as `type:"session"` entries at their started timestamp — a graph with
|
|
11
|
+
// zero sessions renders exactly as before.
|
|
12
|
+
|
|
13
|
+
import { repoOfId, assignRepo, isMultiRepo } from "./temporal.mjs";
|
|
14
|
+
|
|
15
|
+
const esc = (s) =>
|
|
16
|
+
String(s == null ? "" : s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
17
|
+
|
|
18
|
+
export function extractTimeline(graph) {
|
|
19
|
+
const commits = [];
|
|
20
|
+
const sessions = [];
|
|
21
|
+
const churnByShort = new Map(); // short sha -> modules touched
|
|
22
|
+
const prefixByShort = new Map(); // short sha -> Map<repoPrefix, modules touched> (multi-repo)
|
|
23
|
+
for (const ind of graph.individuals || []) {
|
|
24
|
+
const attr = (key) => ind.attributes?.find((a) => a.key === key)?.value || "";
|
|
25
|
+
if (ind.class === "Module") {
|
|
26
|
+
const prefix = repoOfId(ind.id);
|
|
27
|
+
for (const ref of ind.derived_from || []) {
|
|
28
|
+
const short = ref.replace(/^git:/, "");
|
|
29
|
+
churnByShort.set(short, (churnByShort.get(short) || 0) + 1);
|
|
30
|
+
if (prefix) {
|
|
31
|
+
let m = prefixByShort.get(short);
|
|
32
|
+
if (!m) { m = new Map(); prefixByShort.set(short, m); }
|
|
33
|
+
m.set(prefix, (m.get(prefix) || 0) + 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else if (ind.class === "Commit") {
|
|
37
|
+
commits.push({
|
|
38
|
+
sha: ind.id.replace(/^commit:/, ""),
|
|
39
|
+
short: ind.label,
|
|
40
|
+
date: attr("date"),
|
|
41
|
+
author: attr("author"),
|
|
42
|
+
message: attr("message"),
|
|
43
|
+
});
|
|
44
|
+
} else if (ind.class === "Session") {
|
|
45
|
+
// chat sessions (sessions.mjs) enter the timeline at their started timestamp
|
|
46
|
+
sessions.push({
|
|
47
|
+
type: "session",
|
|
48
|
+
id: ind.id.replace(/^session:/, ""),
|
|
49
|
+
short: ind.label,
|
|
50
|
+
date: attr("started"),
|
|
51
|
+
turns: Number(attr("turns")) || 0,
|
|
52
|
+
touched: 0,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const c of commits) c.touched = churnByShort.get(c.short) || 0;
|
|
57
|
+
// P4 cross-repo: on a MERGED multi-repo graph, tag every commit with the repo it
|
|
58
|
+
// belongs to (inferred from its touched modules' id prefixes) so the render can group
|
|
59
|
+
// by repo on a single global time axis. Single-repo graphs never trip isMultiRepo, so
|
|
60
|
+
// no `repo` key is added and the output stays byte-identical to before.
|
|
61
|
+
if (isMultiRepo(commits.map((c) => prefixByShort.get(c.short) || new Map()))) {
|
|
62
|
+
for (const c of commits) {
|
|
63
|
+
const r = assignRepo(prefixByShort.get(c.short) || new Map());
|
|
64
|
+
if (r) c.repo = r;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const entries = [...commits, ...sessions];
|
|
68
|
+
entries.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
|
|
69
|
+
return entries;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Per-repo badge palette (multi-repo timelines only) — hues distinct from the default
|
|
73
|
+
// commit-bar blue (#3d59a1) and the session amber (#e0af68); assigned by sorted repo
|
|
74
|
+
// order so a repo's colour is stable across renders.
|
|
75
|
+
const REPO_PALETTE = ["#7aa2f7", "#9ece6a", "#bb9af7", "#f7768e", "#2ac3de", "#ff9e64", "#73daca", "#e0af68"];
|
|
76
|
+
const repoColorMap = (repos) => new Map(repos.map((r, i) => [r, REPO_PALETTE[i % REPO_PALETTE.length]]));
|
|
77
|
+
|
|
78
|
+
/** `nav` is the shared {name: href} nav object the viz CLI computes from the
|
|
79
|
+
* actual sibling output paths — header links render ONLY from its entries, so
|
|
80
|
+
* a page generated without siblings never carries dead links. */
|
|
81
|
+
export function renderTimelineHtml({ commits, repoUrl = "", repoRef = "main", generatedAt = "", nav = null } = {}) {
|
|
82
|
+
const base = repoUrl.replace(/\/+$/, "");
|
|
83
|
+
const sessionCount = commits.filter((c) => c.type === "session").length;
|
|
84
|
+
const commitCount = commits.length - sessionCount;
|
|
85
|
+
const maxTouched = Math.max(1, ...commits.map((c) => c.touched));
|
|
86
|
+
// P4 cross-repo: extractTimeline tags each commit of a MERGED graph with its `repo`.
|
|
87
|
+
// When present, entries are labelled and colour-badged by repo on one global time
|
|
88
|
+
// axis; when absent (single-repo or zero commits) every branch below is byte-for-byte
|
|
89
|
+
// the pre-P4 render.
|
|
90
|
+
const repos = [...new Set(commits.filter((c) => c.repo).map((c) => c.repo))].sort();
|
|
91
|
+
const multi = repos.length > 0;
|
|
92
|
+
const rc = multi ? repoColorMap(repos) : null;
|
|
93
|
+
const badgeStyle = (color) => `display:inline-block;padding:0 6px;border-radius:8px;font-size:11px;font-weight:600;color:#16161e;background:${color}`;
|
|
94
|
+
// Bars run oldest→newest left to right; the list reads newest first. Chat
|
|
95
|
+
// sessions are the visually distinct entries (amber, fixed-height bars). On a
|
|
96
|
+
// multi-repo timeline each commit bar is tinted with its repo colour.
|
|
97
|
+
const bars = commits
|
|
98
|
+
.map((c, i) => {
|
|
99
|
+
if (c.type === "session") {
|
|
100
|
+
return `<a class="bar sess" href="#c${i}" title="chat session ${esc(c.short)} · ${esc(c.date.slice(0, 10))} — ${c.turns} turn(s)"><i style="height:10px"></i></a>`;
|
|
101
|
+
}
|
|
102
|
+
const barRepoTitle = multi && c.repo ? `[${esc(c.repo)}] ` : "";
|
|
103
|
+
const barRepoBg = multi && c.repo ? `;background:${rc.get(c.repo)}` : "";
|
|
104
|
+
return `<a class="bar" href="#c${i}" title="${barRepoTitle}${esc(c.short)} · ${esc(c.date.slice(0, 10))} — ${esc(c.message)}"><i style="height:${Math.max(6, Math.round((c.touched / maxTouched) * 64))}px${barRepoBg}"></i></a>`;
|
|
105
|
+
})
|
|
106
|
+
.join("");
|
|
107
|
+
const rows = commits
|
|
108
|
+
.map((c, i) => {
|
|
109
|
+
if (c.type === "session") {
|
|
110
|
+
return `<li id="c${i}" class="sess"><span class="d">${esc(c.date.slice(0, 10))}</span> <code>${esc(c.short)}</code> <span class="m">chat session — ${c.turns} turn(s)</span></li>`;
|
|
111
|
+
}
|
|
112
|
+
const sha = base
|
|
113
|
+
? `<a href="${base}/-/commit/${esc(c.sha)}" target="_blank" rel="noopener"><code>${esc(c.short)}</code></a>`
|
|
114
|
+
: `<code>${esc(c.short)}</code>`;
|
|
115
|
+
const rowBadge = multi && c.repo ? `<span style="${badgeStyle(rc.get(c.repo))}">${esc(c.repo)}</span> ` : "";
|
|
116
|
+
return `<li id="c${i}">${rowBadge}<span class="d">${esc(c.date.slice(0, 10))}</span> ${sha} <span class="a">${esc(c.author)}</span> <span class="m">${esc(c.message)}</span> <span class="t" title="modules touched">${c.touched}⛁</span></li>`;
|
|
117
|
+
})
|
|
118
|
+
.reverse()
|
|
119
|
+
.join("\n");
|
|
120
|
+
const repoLegend = multi
|
|
121
|
+
? repos
|
|
122
|
+
.map((r) => `<span class="rl"><span style="display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:3px;vertical-align:middle;background:${rc.get(r)}"></span>${esc(r)}</span>`)
|
|
123
|
+
.join(" ")
|
|
124
|
+
: "";
|
|
125
|
+
const navLinks = nav
|
|
126
|
+
? Object.entries(nav)
|
|
127
|
+
.map(([name, href]) => `<a href="${esc(href)}">${name === "home" ? "← home" : esc(name)}</a>`)
|
|
128
|
+
.join("\n ")
|
|
129
|
+
: "";
|
|
130
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
131
|
+
<title>seonix — commit timeline</title>
|
|
132
|
+
<style>
|
|
133
|
+
html,body{margin:0;font:14px/1.5 system-ui,sans-serif;background:#1a1b26;color:#c0caf5}
|
|
134
|
+
header{padding:14px 20px;background:#16161e;border-bottom:1px solid #2a2e42;display:flex;gap:16px;align-items:baseline;flex-wrap:wrap}
|
|
135
|
+
header b{color:#7aa2f7} header a{color:#7aa2f7;text-decoration:none} header a:hover{text-decoration:underline}
|
|
136
|
+
header .meta{color:#565f89;font-size:12px;margin-left:auto}
|
|
137
|
+
#bars{display:flex;align-items:flex-end;gap:3px;padding:18px 20px 6px;overflow-x:auto}
|
|
138
|
+
.bar i{display:block;width:9px;background:#3d59a1;border-radius:2px 2px 0 0}
|
|
139
|
+
.bar:hover i{background:#7aa2f7}
|
|
140
|
+
ol{list-style:none;margin:6px 0 40px;padding:0 20px}
|
|
141
|
+
li{padding:6px 8px;border-bottom:1px solid #20222f;display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
|
|
142
|
+
li:target{background:#20253a;border-radius:4px}
|
|
143
|
+
.d{color:#565f89;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
144
|
+
code{color:#9ece6a} li a{text-decoration:none} li a:hover code{text-decoration:underline}
|
|
145
|
+
.a{color:#a9b1d6;white-space:nowrap} .m{flex:1 1 24ch} .t{color:#bb9af7;white-space:nowrap}
|
|
146
|
+
.bar.sess i{background:#8a6a2f;border-radius:5px} .bar.sess:hover i{background:#e0af68}
|
|
147
|
+
li.sess{border-left:3px solid #e0af68;padding-left:5px} li.sess code,li.sess .m{color:#e0af68}
|
|
148
|
+
</style></head><body>
|
|
149
|
+
<header>
|
|
150
|
+
<span><b>seonix</b> commit timeline — ${commitCount} commits${sessionCount ? ` · ${sessionCount} chat session(s)` : ""}${multi ? ` · ${repos.length} repos` : ""}</span>${multi ? `\n <span class="repolegend">${repoLegend}</span>` : ""}
|
|
151
|
+
${navLinks}
|
|
152
|
+
${base ? `<a href="${base}" target="_blank" rel="noopener">repository ↗</a>` : ""}
|
|
153
|
+
<span class="meta">bars = modules touched per commit · generated from the seonix graph artifact${generatedAt ? ` (${esc(generatedAt.slice(0, 10))})` : ""}</span>
|
|
154
|
+
</header>
|
|
155
|
+
<div id="bars">${bars}</div>
|
|
156
|
+
<ol>
|
|
157
|
+
${rows}
|
|
158
|
+
</ol>
|
|
159
|
+
</body></html>`;
|
|
160
|
+
}
|