@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/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
- /** Ordered (oldest → newest) shas for the graph's Commit individuals via git log. */
83
- export async function gitCommitOrder(repo, graphCommitIds) {
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
- const order = (orderShas && orderShas.length)
141
- ? orderShas
142
- : commitInds.map((c) => String(c.id).replace(/^commit:/, ""));
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
- return {
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
- nodes.push({
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
- nodes.push({
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: "oldest-first (ordinal 0 = oldest, N-1 = HEAD)",
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,
@@ -701,6 +773,7 @@ function drawTimeline(){
701
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>':'';
702
774
  $('tlmeta').innerHTML=(c?('<b>'+esc(c.shortSha)+'</b> · '+(c.date||'').slice(0,10)+' · '+esc(c.author)+' — '+esc(c.subject)):'')
703
775
  +mergeNote
776
+ +(c&&c.repo?' <span style="color:#7aa2f7">· ⛃ '+esc(c.repo)+'</span>':'')
704
777
  +(S.cursorB!=null?' <span style="color:#e0af68">· B: '+esc(shortAt(S.cursorB))+'</span>':'')
705
778
  +' <span style="color:#565f89">('+(S.cursor+1)+' / '+S.tg.commits.length+')</span>';
706
779
  }
@@ -735,6 +808,7 @@ function showBiography(id){
735
808
  :'';
736
809
  $('detail').innerHTML='<h3>'+esc(c.shortSha)+'</h3>'
737
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>':'')
738
812
  +'<div class="row">'+esc(c.author)+' · '+(c.date||'').slice(0,10)+'</div><div class="row"><em>'+esc(c.subject)+'</em></div>'
739
813
  +'<div class="row">'+(url?'<a class="btn" href="'+url+'" target="_blank" rel="noopener">open in GitLab ↗</a>':'')
740
814
  +'<button class="btn" id="gocommit">scrub here</button></div>'