08451dd8bbb6

take license headers only in their real shapes, find the README intro as the Atlas finds a first paragraph, and pin real-source headers in tests (v3.0.12)

time
2026-09-26 18:17 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
claude-opus-5-5
session
kepr 3 implementation
parent
ffa764f54431
7 files package.json+1 −1src/describe/describe.ts+5 −4src/pages/markdown.ts+15 −13src/pages/repo.ts+4 −2tests/describe.test.ts+71 −0tests/fixture.ts+5 −1tests/pages.test.ts+4 −1
package.json+1 −1
@@ -1,6 +1,6 @@11 {22 	"name": "kepr",3-	"version": "3.0.11",3+	"version": "3.0.12",44 	"private": true,55 	"type": "module",66 	"scripts": {

view file

src/describe/describe.ts+5 −4
@@ -23,12 +23,13 @@2323 const README = /^readme\.(md|markdown)$/i;2424 2525 /**26- * A license header is the first comment block when it carries an SPDX tag, a copyright line or27- * "licensed under"/"all rights reserved". Only that one block is skipped, so a description that28- * merely mentions a license elsewhere is kept.26+ * A license header is the first comment block when it has a license header's shape: an SPDX tag,27+ * "Copyright (c)", "Copyright ©", "Copyright <year>", "© <year>", "licensed under" or "all rights28+ * reserved". Only that one block is skipped, and the word "copyright" alone is not enough, so29+ * `// Renders copyright notices.` keeps its description.2930  */3031 const LICENSE =31-	/SPDX-License-Identifier|\bcopyright\b|©|\blicensed under\b|\ball rights reserved\b/i;32+	/SPDX-License-Identifier|\bcopyright\s*(\(c\)|©|\d{4})|©\s*\d{4}|\blicensed under\b|\ball rights reserved\b/i;3233 /**3334  * Kotlin, Java and Swift put a file's words on its main declaration, after `package` and3435  * `import`; those lines only name the file's place, so they are passed over, not counted as code.

view file

src/pages/markdown.ts+15 −13
@@ -39,20 +39,22 @@3939 	return trusted(md.render(source, base));4040 }4141 42-/** The README's first paragraph (§21.4), as markup and as plain text for link previews. */43-export function firstParagraph(44-	source: string,45-	base: MarkdownBase,46-): { html: Html; text: string } | null {47-	const tokens = md.parse(source, base);48-	const index = tokens.findIndex((token) => token.type === "paragraph_open");49-	const inline = tokens[index + 1];50-	if (index < 0 || !inline?.children) return null;51-	const text = inline.children52-		.filter((token) => token.type === "text" || token.type === "code_inline")53-		.map((token) => token.content)42+/**43+ * A paragraph of Markdown source (the README's first paragraph, found as the Atlas finds it) as44+ * inline markup, and as plain text for link previews.45+ */46+export function renderParagraph(source: string, base: MarkdownBase): { html: Html; text: string } {47+	const children = md.parseInline(source, base)[0]?.children ?? [];48+	const text = children49+		.map((token) =>50+			token.type === "text" || token.type === "code_inline"51+				? token.content52+				: token.type === "softbreak"53+					? " "54+					: "",55+		)5456 		.join("");55-	return { html: trusted(md.renderer.renderInline(inline.children, md.options, base)), text };57+	return { html: trusted(md.renderer.renderInline(children, md.options, base)), text };5658 }5759 5860 /** One line of Markdown with links and images flattened away, for a row that is itself a link. */

view file

src/pages/repo.ts+4 −2
@@ -1,4 +1,5 @@11 import type { Hono } from "hono";2+import { describeFile } from "../describe/describe.ts";23 import {34 	fileAt,45 	filesAt,@@ -22,7 +23,7 @@2223 	plural,2324 	relativeTime,2425 } from "./layout.ts";25-import { firstParagraph, renderMarkdown } from "./markdown.ts";26+import { renderMarkdown, renderParagraph } from "./markdown.ts";2627 import { timelineSvg } from "./timeline.ts";2728 2829 const PAGE_SIZE = 50;@@ -39,8 +40,9 @@3940 		const pageNumber = Math.max(1, Number.parseInt(c.req.query("page") ?? "1", 10) || 1);4041 		const readme = readmeAtHead(repo);4142 		const name = repo.settings.name;43+		const paragraph = readme === null ? null : describeFile("README.md", readme);4244 		const intro =43-			readme === null ? null : firstParagraph(readme, { repo: name, dir: "", at: null });45+			paragraph === null ? null : renderParagraph(paragraph, { repo: name, dir: "", at: null });4446 		cacheFor(c, false);4547 		return c.html(4648 			page(ctx, {

view file

tests/describe.test.ts+71 −0
@@ -107,6 +107,13 @@107107 		);108108 	});109109 110+	test("the word copyright alone is not a license header", () => {111+		expect(describeFile("x.ts", "// Renders copyright notices for every package.\ncode();\n")).toBe(112+			"Renders copyright notices for every package.",113+		);114+		expect(describeFile("x.ts", "// Copyright © Asha.\n\n// Real.\ncode();\n")).toBe("Real.");115+	});116+110117 	test("only the first block, so a description mentioning a licence survives", () => {111118 		expect(describeFile("x.ts", "// Checks each repo's license file.\ncode();\n")).toBe(112119 			"Checks each repo's license file.",@@ -116,8 +123,72 @@116123 	});117124 });118125 126+describe("real sources, verbatim headers", () => {127+	const lines = (...text: string[]) => `${text.join("\n")}\n`;128+129+	test("Koh 3, V with //! lines", () => {130+		const v = lines(131+			"//! AWS Signature Version 4, the part S3 needs.",132+			"//!",133+			"//! Signing is a fixed recipe: canonicalise the request, hash it, derive a key",134+			"//! from the secret for that day and region, and sign the string-to-sign. It is",135+			"",136+			"module remote",137+		);138+		expect(firstSentence(describeFile("src/remote/sigv4.v", v) ?? "")).toBe(139+			"AWS Signature Version 4, the part S3 needs.",140+		);141+	});142+143+	test("Peach, Kotlin with package and imports before the KDoc", () => {144+		const kt = lines(145+			"package com.peachpasswords.vault",146+			"",147+			"import androidx.annotation.VisibleForTesting",148+			"import kotlinx.coroutines.CoroutineScope",149+			"import javax.inject.Singleton",150+			"",151+			"/**",152+			" * Idle auto-lock timer. The vault locks [VaultSettings.autoLockMinutes] after the",153+			' * last user interaction; a null (or non-positive) setting means "never". [poke]',154+			" */",155+			"@Singleton",156+		);157+		expect(firstSentence(describeFile("vault/VaultLockTimer.kt", kt) ?? "")).toBe(158+			"Idle auto-lock timer.",159+		);160+	});161+162+	test("nina, Nix and shell", () => {163+		const nix = lines(164+			"# Standalone NixOS module for nina — no flake inputs needed.",165+			"# Generated by the installer into ~/nina.nix. Also importable directly.",166+			"#",167+			"# Usage (in configuration.nix):",168+			"{ config, lib, pkgs, ... }:",169+		);170+		expect(firstSentence(describeFile("nixos-module.nix", nix) ?? "")).toBe(171+			"Standalone NixOS module for nina — no flake inputs needed.",172+		);173+		const sh = lines(174+			"#!/usr/bin/env sh",175+			"# G-01/G-02/G-03 generations validation, run inside the VM. Creates distinct",176+			"# system generations by changing the config between applies (a tiny /etc file via",177+			"set -u",178+		);179+		expect(firstSentence(describeFile("test/vm/tests/gen.sh", sh) ?? "")).toBe(180+			"G-01/G-02/G-03 generations validation, run inside the VM.",181+		);182+	});183+});184+119185 describe("folders", () => {120186 	const child = (name: string, text: string) => ({ name, description: describeFile(name, text) });187+188+	test("a README's leading HTML comment or raw <p align> block is not its first paragraph", () => {189+		const readme = '<!-- badges -->\n\n<p align="center">LOGO</p>\n\n# Title\n\nThe intro.\n';190+		expect(describeFile("README.md", readme)).toBe("The intro.");191+	});121192 122193 	test("a README's first paragraph wins over the module file", () => {123194 		const children = [

view file

tests/fixture.ts+5 −1
@@ -20,7 +20,11 @@2020 	(c) => c.charCodeAt(0),2121 );2222 23-const README = `# Demo23+const README = `<!-- a leading comment and a centred title are not the intro -->24+25+<p align="center">DEMO</p>26+27+# Demo2428 2529 A small repo for **Kepr's** tests.2630 

view file

tests/pages.test.ts+4 −1
@@ -73,7 +73,10 @@7373 		expect(response.headers.get("cache-control")).toBe("public, max-age=60");7474 		const page = await response.text();7575 		expect(page).toContain('<h1 class="repo-name">demo</h1>');76-		expect(page).toContain("A small repo for <strong>Kepr's</strong> tests.");76+		// The README opens with an HTML comment and a raw <p align>; neither is the intro.77+		expect(/<div class="repo-desc"><p>(.*?)<\/p>/.exec(page)?.[1]).toBe(78+			"A small repo for <strong>Kepr's</strong> tests.",79+		);7780 		expect(page).toContain(7881 			'<meta property="og:description" content="A small repo for Kepr&#39;s tests.">',7982 		);

view file