UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

505 lines • 20.8 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= export const SKIPPED = '...'; // The cell a `--boundary` commit is drawn with, in place of `*`. export const BOUNDARY = 'o'; // The roots a run climbs through before it starts again in its own column. const CLIMB = 4; function line(sha, at, goal) { return { sha, at, goal, heading: 0 }; } /** * The `--graph` column to the left of a log, one commit at a time. Every * walked commit moves the graph on; a commit the log does not print draws * nothing, and the next printed one is preceded by a `...` row. Lines move * one cell per row, skipping a column another line stands in, and slide under * a run of standing columns with `_`; a printed commit owes the rows its * lines need to settle. A `--boundary` commit is drawn with `o`; a line leads * on from it only to a parent the graph draws too. A path-limited walk hands * over the parents it drew its history with. * * A root, a commit none of whose parents is in the walk and that no merge drew * as a later parent, stands a column right of its own when the commit walked * next has no line leading to it, unless that one is a root too with more * walked after it. Each root after a root, but the last commit walked, climbs * a column further right, back to its own column every fourth; one a line * leads to joins a run already climbing. A line leading to a root a column * right bends on a row of its own, which is the separator a format puts before * the commit, or none after a `...` row. Pinned against git 2.56.0. `order` * is every commit the graph is walked through, in order. Mirrors CommitGraph * in graph.py. */ export class CommitGraph { interesting; firstParent; order; boundary; parents; lines = []; pending = null; walked = 0; skipped = false; resumed = false; afterRoot = false; stairs = 0; fanned = new Set(); owed = []; ahead = []; width = 0; mergedAt = null; constructor(interesting, firstParent, order, boundary = new Set(), parents = null) { this.interesting = interesting; this.firstParent = firstParent; this.order = order; this.boundary = boundary; this.parents = parents; } /** Move on to the next walked commit, printed or not. */ walk(commit) { if (this.pending !== null) { const rows = this.place(this.pending); while (rows.next().done !== true) { // each row is drawn and dropped } this.owed = []; this.skipped = true; } this.pending = commit; this.walked += 1; } /** * The rows before the walked commit's own, each with its newline, then the * cells of its own row, to be followed by its first line; once the commit is * drawn, the cells in front of a further header. */ leadIn() { const commit = this.pending; if (commit === null) return this.rest(this.width); this.pending = null; const out = this.skipped ? this.skipRow(commit) + '\n' : ''; const rows = this.ahead.length > 0 ? this.ahead : [...this.place(commit)]; this.ahead = []; const star = rows.findIndex((row) => row.includes('*') || row.includes(BOUNDARY)); this.owed = rows.slice(star + 1); return (out + rows .slice(0, star) .map((row) => row + '\n') .join('') + (rows[star] ?? '')); } /** The cells in front of the commit's next line of text. */ nextRow() { return this.owed.shift() ?? this.rest(this.width); } /** * The cells in front of a line once the commit's rows are out. Before a * walked commit yet to be drawn it is the `...` row when commits were * skipped, the commit's first row when it spreads the lines to its right or * bends the line leading to it, and otherwise a row as wide as the commit's * where the lines stop. Before a commit leaving with a lead and two or more * parents besides, the lines right of it stand a cell over, and the row * widens by the octopus's dashes and a cell. */ filler() { const commit = this.pending; if (commit === null) return this.rest(this.width); if (this.skipped) return this.skipRow(commit); const shape = this.shape(commit); if (shape.spreads || shape.bends) { this.ahead = [...this.place(commit)]; return this.ahead.shift() ?? ''; } const { parents } = shape; if (parents.lead === null || parents.drawn.length < 2) return this.rest(shape.width, true); const width = Math.max(shape.width, 2 * (shape.columns + shape.extra) + 1); return this.rest(width, true, 2 * shape.col); } /** * The commit's text with the graph in front of each further line, then the * rows the commit still owes; when the text ends with a newline the owed * rows end with one too. */ around(text) { const lines = text.split('\n'); let out = lines[0] ?? ''; for (const piece of lines.slice(1, -1)) out += '\n' + this.nextRow() + piece; if (lines.length > 1) { const tail = lines[lines.length - 1] ?? ''; out += tail === '' ? '\n' : '\n' + this.nextRow(); out += tail; } const owed = this.owed; this.owed = []; if (text.endsWith('\n')) return out + owed.map((row) => row + '\n').join(''); return out + owed.map((row) => '\n' + row).join(''); } skipRow(commit) { const width = this.shape(commit).width; this.rest(width, true); this.skipped = false; this.resumed = true; return SKIPPED.padEnd(Math.max(SKIPPED.length, width)); } columns() { const order = [...this.lines].sort((a, b) => a.goal - b.goal); return [...new Set(order.map((each) => each.sha))]; } ownParents(commit) { return this.parents?.get(commit.oid) ?? commit.parents; } isRoot(commit) { return !this.fanned.has(commit.oid) && !this.ownParents(commit).some((p) => this.interesting(p)); } followed(commit) { const own = this.ownParents(commit); const followed = this.firstParent ? own.slice(0, 1) : own; return [...new Set(followed.filter((p) => this.interesting(p)))]; } parentsOf(commit, columns, col) { const parents = this.followed(commit); const where = new Map(columns.map((sha, i) => [sha, i])); const opened = []; let lead = null; let reach = false; let beside = false; const last = parents.length - 1; parents.forEach((parent, i) => { const j = where.get(parent); if (j !== undefined && i === 0 && j < col) lead = parent; else if (j !== undefined && i === 1 && lead !== null && j === col - 1 && parents.length >= 3) beside = true; else if (j !== undefined && i === last && opened.length > 0 && j === col + 1 && where.has(commit.oid)) reach = true; else opened.push(parent); }); const drawn = parents.filter((p) => p !== lead); return { opened, lead, drawn, reach, beside }; } shape(commit) { const columns = this.columns(); const known = columns.includes(commit.oid); const col = known ? columns.indexOf(commit.oid) : columns.length; const parents = this.parentsOf(commit, columns, col); const after = [...columns.slice(0, col), ...parents.opened, ...columns.slice(col + 1)]; const extra = Math.max(parents.drawn.length - 2, 0); const spreads = extra > 0 && this.lines.some((each) => each.sha !== commit.oid && each.goal > 2 * col); // Spreading moves the rightmost line two cells per extra parent, past the // columns its parents take when some reach or leave with the lead. const width = 2 * Math.max(columns.length + (known ? 0 : 1), after.length, spreads ? columns.length + extra : 0); const root = this.isRoot(commit); const upcoming = this.order.slice(this.walked, this.walked + 2); const stairs = root && this.afterRoot && upcoming.length > 0 && (!known || this.stairs > 0); const climb = stairs ? (this.stairs + 1) % CLIMB : 0; const follows = upcoming[0]; const lifted = root && follows !== undefined && !columns.includes(follows.oid) && !(upcoming.length > 1 && this.isRoot(follows)); const shift = 2 * Math.max(climb, lifted ? 1 : 0); return { col, columns: columns.length, parents, after, extra, spreads, width, root, stairs, shift, bends: known && climb === 0 && lifted, }; } /** * Move the lines through a commit's rows, each row yielded as it is drawn, * so that a commit the log skips holds none of them. */ *place(commit) { const shape = this.shape(commit); const { parents, width, extra } = shape; const here = 2 * shape.col; const resumed = this.resumed; this.resumed = false; if (shape.bends && !resumed) { const row = this.rest(width + 1, true, here - 1); yield row.slice(0, here + 1) + '\\' + row.slice(here + 2); } const final = [...new Set(shape.after)]; const goalOf = new Map(final.map((sha, i) => [sha, 2 * i])); const goal = (sha) => goalOf.get(sha) ?? 0; this.width = width + shape.shift; this.stairs = shape.stairs ? this.stairs + 1 : 0; this.afterRoot = shape.root; for (const parent of this.followed(commit).slice(1)) this.fanned.add(parent); const opened = parents.opened.length; const spread = 2 * Math.max(opened - 1, 0); const others = this.lines.filter((each) => each.sha !== commit.oid); const right = new Set(others.filter((each) => each.goal > here)); const base = new Map(); for (const each of right) base.set(each, each.at + (each.at % 2 ? each.heading : 0)); const lift = shape.spreads ? 2 * extra : 0; if (lift) yield* this.spreadRows(others, right, base, here, width, lift); const drawn = this.commitRow(others, right, base, here, width, lift, spread, goal, extra); const row = this.boundary.has(commit.oid) ? drawn.slice(0, here) + BOUNDARY + drawn.slice(here + 1) : drawn; yield row.slice(0, here) + ' '.repeat(shape.shift) + row.slice(here); const born = parents.opened.map((sha) => line(sha, here, goal(sha))); const led = parents.lead === null ? [] : [line(parents.lead, here, goal(parents.lead))]; for (const each of others) each.goal = goal(each.sha); const merging = parents.drawn.length + (parents.lead === null ? 0 : 1) >= 2; if (merging) yield this.mergeRow(others, right, base, born, led, here, width, spread, parents); // A reach's edge takes the fan's next cell after the opened parents. That // is the spread column's own cell unless a beside parent left the fan a // cell short, and then the edge stays a line of its own. const reached = []; const reach = parents.drawn[parents.drawn.length - 1]; if (parents.reach && parents.beside && lift && reach !== undefined) reached.push(line(reach, here + 2 * opened, goal(reach))); this.lines = [...others, ...born, ...led, ...reached]; this.join(); this.mergedAt = merging && this.settled() ? here : null; while (!this.settled()) yield this.collapseRow(this.width); } *spreadRows(others, right, base, here, width, lift) { for (let step = 0; step < lift; step++) { const cells = new Array(width).fill(' '); cells[here] = '|'; for (const each of others) { if (right.has(each)) { const moving = step > 0 || each.heading > 0 || (this.mergedAt !== null && each.at > this.mergedAt); each.at = (base.get(each) ?? each.at) + step; cells[each.at] = moving ? '\\' : '|'; } else { if (each.at % 2) each.at += each.heading; each.heading = 0; cells[each.at] = '|'; } } yield cells.join(''); } } commitRow(others, right, base, here, width, lift, spread, goal, extra) { const cells = new Array(width).fill(' '); const merging = spread > 0; for (const each of others) { const target = goal(each.sha); const onRight = right.has(each); let glyph; if (onRight && lift) { each.at = (base.get(each) ?? each.at) + lift; const moving = lift < spread; glyph = moving ? '\\' : '|'; each.heading = moving ? 1 : 0; } else if (each.at % 2 || each.heading) { if (each.at % 2) each.at += each.heading; if (each.heading < 0 && target < each.at && !(onRight && merging)) glyph = '/'; else if (each.heading > 0 && onRight && merging) glyph = '\\'; else { glyph = '|'; each.heading = 0; } } else { glyph = '|'; each.heading = 0; } cells[each.at] = glyph; } cells[here] = '*'; if (extra) { for (let i = 0; i < 2 * extra - 1; i++) cells[here + 1 + i] = '-'; cells[here + 2 * extra] = '.'; } return cells.join(''); } mergeRow(others, right, base, born, led, here, width, spread, parents) { const cells = new Array(width).fill(' '); for (const each of others) { if (right.has(each) && each.at < (base.get(each) ?? each.at) + spread) { each.at += 1; each.heading = 1; cells[each.at] = '\\'; } else cells[each.at] = '|'; } if (born.length) cells[here] = '|'; for (let k = 1; k < parents.drawn.length; k++) cells[here + 2 * k - 1] = '\\'; const settle = parents.reach && parents.drawn.length > 2; born.slice(1).forEach((each, index) => { const i = index + 1; if (settle) { each.at = here + 2 * i; return; } each.at = here + 2 * i - 1; each.heading = 1; }); if (parents.beside) cells[here - 1] = '/'; const standing = new Set([...others, ...born] .filter((each) => each.at === each.goal && each.heading === 0) .map((each) => each.at)); for (const each of led) { for (const [cell, glyph] of this.step(each, standing, true)) cells[cell] = glyph; if (each.at === each.goal + 1) { each.at = each.goal; each.heading = 0; } } return cells.join(''); } settled() { return this.lines.every((each) => each.at === each.goal || (each.at === each.goal + 1 && each.heading < 0) || (each.at === each.goal - 1 && each.heading > 0)); } /** * Move a line one row toward its column: the cells it draws and their * glyphs. `standing` holds the cells of columns whose lines stand still this * row, which a moving line passes under; `glide` whether it may slide under * a run of them; `taken` the cells lines start a collapsing row in. */ step(each, standing, glide, taken = new Set()) { if (each.at === each.goal) { each.heading = 0; return [[each.at, '|']]; } if (each.heading > 0 && each.goal > each.at) { each.at += 1; if (each.at === each.goal) { each.heading = 0; return [[each.at, '|']]; } return [[each.at, '\\']]; } if (each.heading > 0) each.at += 1; each.heading = -1; let marks; if (each.at % 2) { each.at -= 1; if (each.at !== each.goal && standing.has(each.at)) each.at -= 1; if (each.at === each.goal) { each.heading = 0; return [[each.at, '|']]; } marks = [[each.at, '/']]; if (each.at % 2 === 0) { // Landing beside a column whose line already stands there, with the // cell past it free, the line slides under it on the same row. const under = each.at - 2; if (!(glide && taken.has(under) && !taken.has(under - 1) && standing.has(under) && under > each.goal + 2)) return marks; each.at = under - 1; marks.push([each.at, '_']); } } else { each.at -= 1; marks = [[each.at, '/']]; } while (glide && standing.has(each.at - 1) && each.at - 1 > each.goal + 2) { each.at -= 2; marks.push([each.at, '_']); } return marks; } collapseRow(width) { const cells = new Array(width).fill(' '); const standing = new Set(this.lines .filter((each) => each.at === each.goal || (each.at % 2 !== 0 && each.at + each.heading === each.goal)) .map((each) => each.goal)); const busy = this.lines .filter((each) => each.at % 2 !== 0 && each.heading < 0 && each.at - 1 !== each.goal) .map((each) => each.at); const taken = new Set(this.lines.map((each) => each.at)); for (const each of [...this.lines].sort((a, b) => a.at - b.at)) { const glide = !busy.some((at) => at < each.at); for (const [cell, glyph] of this.step(each, standing, glide, taken)) if (cells[cell] === ' ' || glyph !== '|') cells[cell] = glyph; } this.join(); return cells.join(''); } join() { const kept = []; const seen = new Set(); const ordered = [...this.lines].sort((a, b) => a.at - b.at || Number(a.heading !== 0) - Number(b.heading !== 0)); for (const each of ordered) { const key = `${each.sha} ${String(each.at)}`; if (each.at === each.goal && seen.has(key)) continue; if (each.at === each.goal) seen.add(key); kept.push(each); } this.lines = kept; } /** * A row of every line in its column; with `settle` the lines stop too, * otherwise a line still on its way keeps going on the next commit's row; * with `past` the lines right of that cell are drawn a cell further right. */ rest(width, settle = false, past = null) { this.mergedAt = null; const cells = new Array(width).fill(' '); for (const each of this.lines) { if (each.at % 2) each.at += each.heading; if (settle) each.heading = 0; cells[each.at + (past !== null && each.at > past ? 1 : 0)] = '|'; } return cells.join(''); } } //# sourceMappingURL=graph.js.map