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
↓ kepr-08451dd8.tar.gzatlas at this save
files changed7 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 −1package.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": {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.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. */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, {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 = [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 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's tests.">',7982 );