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
↓ kepr-009e5c40.tar.gzatlas at this save
files changed6 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 −1package.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": {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)) {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"/>`;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.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", () => {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("<script>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