009e5c4064e1

place timeline dots and card ticks between the earliest and latest save times, and end a /** */ description at its */ when code follows on the line (v3.0.46)

time
2026-09-26 21:24 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
claude-opus-5-5
session
kepr 3 implementation
parent
8722a5c608af
6 files package.json+1 −1src/describe/describe.ts+3 −0src/pages/library.ts+4 −2src/pages/timeline.ts+4 −3tests/describe.test.ts+4 −0tests/pages.test.ts+36 −1
package.json+1 −1
@@ -1,6 +1,6 @@11 {22 	"name": "kepr",3-	"version": "3.0.45",3+	"version": "3.0.46",44 	"private": true,55 	"type": "module",66 	"scripts": {

view file

src/describe/describe.ts+3 −0
@@ -140,6 +140,9 @@140140 			while (i < lines.length && !(lines[i] as string).includes("*/")) i++;141141 			if (i === lines.length) break;142142 			const body = lines.slice(start, ++i).map((l) => l.trim());143+			const last = body.length - 1;144+			// Code may follow the `*/` on its closing line.145+			body[last] = (body[last] as string).slice(0, (body[last] as string).indexOf("*/") + 2);143146 			blocks.push({ lines: body, starred: true });144147 			if (!(lines[i - 1] as string).trim().endsWith("*/")) break;145148 		} else if (isComment(line)) {

view file

src/pages/library.ts+4 −2
@@ -50,8 +50,10 @@5050 5151 // §21.2's mini-timeline: one tick per save, placed by time like the repo page's dots.5252 function ticks(saves: Save[]): Html {53-	const first = saves[0]?.created_at ?? 0;54-	const span = (saves[saves.length - 1]?.created_at ?? 0) - first;53+	const times = saves.map((save) => save.created_at);54+	// Replayed saves keep their own times, so the line is not in time order.55+	const first = times.reduce((a, b) => Math.min(a, b), times[0] ?? 0);56+	const span = times.reduce((a, b) => Math.max(a, b), first) - first;5557 	const lines = saves.map((save) => {5658 		const x = span === 0 ? 50 : 1 + (98 * (save.created_at - first)) / span;5759 		return html`<line x1="${x.toFixed(2)}" x2="${x.toFixed(2)}" y1="9" y2="27"/>`;

view file

src/pages/timeline.ts+4 −3
@@ -16,10 +16,11 @@1616  * where neighbours crowd within a fortieth of the span.1717  */1818 export function timelineSvg(repo: string, saves: Save[]): Html {19-	const first = saves[0]?.created_at ?? 0;20-	const span = (saves[saves.length - 1]?.created_at ?? 0) - first;21-	const reach = Math.max(span / 40, 3600);2219 	const times = saves.map((save) => save.created_at);20+	// Replayed saves keep their own times, so the line is not in time order.21+	const first = times.reduce((a, b) => Math.min(a, b), times[0] ?? 0);22+	const span = times.reduce((a, b) => Math.max(a, b), first) - first;23+	const reach = Math.max(span / 40, 3600);2324 	const dots = saves.map((save) => {2425 		const x = span === 0 ? 50 : 1 + (98 * (save.created_at - first)) / span;2526 		// ponytail: O(n²) neighbour count; a sliding window over sorted times if a repo reaches ~5,000 saves.

view file

tests/describe.test.ts+4 −0
@@ -26,6 +26,10 @@2626 		const ts = "/**\n * Markup that is `safe`.\n *\n * More.\n */\nexport class Html {}\n";2727 		expect(describeFile("html.ts", ts)).toBe("Markup that is `safe`.\n\nMore.");2828 		expect(describeFile("x.java", "/** One line. */\nclass X {}\n")).toBe("One line.");29+		expect(describeFile("x.ts", "/** Describes x. */ export const x = 1;\n")).toBe("Describes x.");30+		expect(describeFile("x.ts", "/**\n * Two lines.\n */ export const x = 1;\n")).toBe(31+			"Two lines.",32+		);2933 	});3034 3135 	test("a comment after the first code does not count", () => {

view file

tests/pages.test.ts+36 −1
@@ -1,3 +1,4 @@1+import { Database } from "bun:sqlite";12 import { beforeAll, describe, expect, test } from "bun:test";23 import { mkdtemp, writeFile } from "node:fs/promises";34 import { tmpdir } from "node:os";@@ -5,8 +6,10 @@56 import type { Hono } from "hono";67 import { openBucket } from "../src/bucket/reader.ts";78 import { syncAll } from "../src/bucket/sync.ts";8-import { fileAt, objectBytes } from "../src/mirror/queries.ts";9+import { fileAt, objectBytes, type Save } from "../src/mirror/queries.ts";910 import { openMirror } from "../src/mirror/store.ts";11+import { repoCards } from "../src/pages/library.ts";12+import { timelineSvg } from "../src/pages/timeline.ts";1013 import { createApp } from "../src/server/app.ts";1114 import { openKeprDb } from "../src/server/db.ts";1215 import { type Fixture, makeKohBucket } from "./fixture.ts";@@ -127,6 +130,38 @@127130 		expect(page).not.toContain('href="javascript:');128131 		expect(page).toContain("&lt;script&gt;alert");129132 		expect(page).not.toContain("<script>alert");133+	});134+});135+136+describe("the timeline and card ticks", () => {137+	// A replayed save keeps its original time, so newest-first order is not time order.138+	const saves = [300, 100, 500, 200].map(139+		(created_at, i): Save => ({140+			id: `00000000000${i}`,141+			parent: "",142+			message: "m",143+			created_at: created_at * 86400,144+			machine: "m",145+			agent: "",146+			session: "",147+			is_checkpoint: false,148+		}),149+	);150+151+	test("span from the earliest to the latest save, whatever the line's order", () => {152+		const dots = [...timelineSvg("demo", saves).value.matchAll(/cx="([\d.]+)%"/g)].map((m) => m[1]);153+		expect(dots).toEqual(["50.000", "1.000", "99.000", "25.500"]);154+		const card = repoCards([155+			{156+				settings: { name: "demo", category: "", homepage: "", installLines: [] },157+				mirror: new Database(":memory:"),158+				stats: { saves: 4, lines: 0, language: "", updated: null },159+				description: "",160+				saves,161+			},162+		]).value;163+		const ticks = [...card.matchAll(/<line x1="([\d.]+)"/g)].map((m) => m[1]);164+		expect(ticks).toEqual(["50.00", "1.00", "99.00", "25.50"]);130165 	});131166 });132167 

view file