A summary that stops inside its description's first sentence prints that sentence whole

GET /v1/ai/memory/recall's comment has no full stop, so zipdoc cut its
summary at the end of the first line: the CLI row and the API index read
"Recall recent/relevant memories for context injection; with q it". The
loader now prints the description's first sentence when the summary is a
strict prefix of it, and early() names a line that ends on no stop while its
paragraph goes on; the whole-document test reads every printed summary
against its description.

A lead name whose first word would be one letter (iCloud, eBay, gRPC,
OAuth2Client) stays whole instead of reading "I cloud", so the rules name it.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-26 07:33:20 -07:00
co-authored by Hanzo Dev
parent 781e5e64e2
commit da72bf0527
5 changed files with 94 additions and 15 deletions
+13
View File
@@ -201,6 +201,19 @@ test('a line runs past a quotation its sentence goes on after', async ({ page })
);
});
// A summary cut where its comment's first line ended carries no "…": the memory
// recall row read "… for context injection; with q it" on the CLI page and the
// API index. Both print the description's first sentence whole.
test('a row runs to the end of the sentence its summary stopped inside', async ({ page }) => {
for (const path of ['/docs/cli/ai/', '/docs/openapi/ai/']) {
await open(page, path, 'dark');
const rows = (await page.locator('.prose td:last-child').allInnerTexts()).map((c) => c.trim());
expect(rows.filter((c) => c.startsWith('Recall recent/relevant memories')), path).toEqual([
expect.stringMatching(/; with q it ranks semantically, without q it returns the most recent\.?$/),
]);
}
});
test('prose is spaced: a paragraph has a margin', async ({ page }) => {
await open(page, '/docs/quickstart/', 'dark');
const margin = await page
+42 -1
View File
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { parse as parseYaml } from 'yaml';
import { parse as parseYaml, stringify as yaml } from 'yaml';
import { beforeAll, describe, expect, it } from 'vitest';
import { BRANDS, DOCUMENT, loadDocument, METHODS, unimplemented, unname } from './openapi-doc';
import { genCliPages, heading, nounOf } from './gen-cli-pages';
@@ -154,6 +154,28 @@ describe('a Go name is not prose', () => {
);
});
// A comment with no full stop is summarised as its first LINE, so the row read
// "… for context injection; with q it" — no "…", and no rule on the row alone
// can tell it stopped. The loader prints the description's first sentence.
it("prints a summary that stops inside its description's first sentence whole", () => {
const line = 'Recall recent/relevant memories for context injection; with q it';
const text = `${line}\nranks semantically, without q it returns the most recent`;
const raw = parseYaml(fs.readFileSync(DOCUMENT, 'utf8'));
Object.assign(raw.paths['/v1/ai/memory/recall'].get, { summary: line, description: text });
Object.assign(raw.paths['/v1/ai/memory/list'].get, {
summary: 'Lists memories.',
description: 'Lists memories. Newest first.',
});
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'summary-')), 'openapi.yaml');
fs.writeFileSync(file, yaml(raw));
const doc = loadDocument(file);
const whole = `${line} ranks semantically, without q it returns the most recent`;
expect(doc.byId.get('get_ai_memory_recall')?.summary).toBe(whole);
expect(doc.byId.get('get_ai_memory_list')?.summary).toBe('Lists memories.');
expect(early(line, text)).not.toBe('');
expect(early(whole, text)).toBe('');
}, 60_000);
it('strips one comment at every address that carries it', () => {
const jwks = loadDocument(DOCUMENT).byId.get('get_licensing_jwks');
expect(jwks?.summary.startsWith('Publishes the Ed25519 PUBLIC verification key')).toBe(true);
@@ -185,6 +207,20 @@ describe('a Go name is not prose', () => {
'JavaScript code the page runs.',
);
});
// Split, these read "I cloud linked." and "G RPC served." — English to every
// rule. Whole, the rules name the identifier and the brand goes into WRITTEN.
it('leaves a name whose first word would be one letter whole, for the rules to name', () => {
for (const [said, left] of [
['Is iCloud linked.', 'iCloud linked.'],
['Is eBay linked.', 'eBay linked.'],
['Is gRPC served.', 'gRPC served.'],
['Is OAuth2Client the client.', 'OAuth2Client the client.'],
]) {
expect(unname(said, 'get_x')).toBe(left);
expect(named(left, [])).not.toBe('');
}
});
});
describe('the rules a line is read against', () => {
@@ -256,6 +292,11 @@ describe('the rules a line is read against', () => {
expect(early(line, text)).not.toBe('');
expect(early(firstSentence(text), text)).toBe('');
}
expect(early('Recall memories; with q it', 'Recall memories; with q it\nranks them.')).not.toBe(
'',
);
expect(early('Recall memories', 'Recall memories')).toBe('');
expect(early('Recall memories.', 'Recall memories. Newest first.')).toBe('');
expect(early('It said "stop."', 'It said "stop." Then it stopped.')).toBe('');
expect(early('It said "stop."', 'It said "stop."\n\nthen more.')).toBe('');
expect(early('Is it on?', 'Is it on? ask.')).toBe('');
+13 -6
View File
@@ -37,16 +37,23 @@ export function cut(line: string): string {
/**
* Why `line`, printed as the first sentence of `text`, stops before that
* sentence does, or '' when it does not. `cut` reads the line alone and cannot
* see this: a quotation's own "?" or full stop reads as a sentence's end —
* `Answers "what am I approving?"` — while the paragraph it came from goes on
* in lower case ("for a pending device code.") or after a dash. Only the
* source says the sentence went on, so this rule reads the line against it.
* see this, so this rule reads the line against its source:
*
* mid-sentence the line ends on no stop while its paragraph goes on — a
* summary cut at the end of a comment's first line, "Recall
* … for context injection; with q it" of "… it ranks
* semantically, …"
* at a quotation a quotation's own "?" or full stop reads as a sentence's end —
* `Answers "what am I approving?"` — while the paragraph goes
* on in lower case ("for a pending device code.") or after a
* dash
*/
export function early(line: string, text: string): string {
const t = line.trim();
const para = (text.trim().split(/\n\s*\n/)[0] ?? '').replace(/\s+/g, ' ').trim();
if (!/[.?!]["'”’]+$/.test(t) || !para.startsWith(t)) return '';
return /^\s*(?:[a-z]|[—–-])/.test(para.slice(t.length))
if (!t || para.length <= t.length || !para.startsWith(t)) return '';
if (!/[.?!]["'”’]*$/.test(t)) return 'stops mid-sentence where its paragraph goes on';
return /[.?!]["'”’]+$/.test(t) && /^\s*(?:[a-z]|[—–-])/.test(para.slice(t.length))
? 'stops at a quotation its sentence goes on after'
: '';
}
+16 -6
View File
@@ -564,6 +564,9 @@ const capabilityOf = (op: any): string =>
* `ClickHouse`, `YouTube` and an exported `DetachMethod` keep their casing — a
* brand split into "Click house" reads wrong and no test can see it, where a Go
* name left whole is caught by the reference tests and fixed at its comment.
* So does a name whose first word would be one letter (`iCloud`, `eBay`, `gRPC`,
* `OAuth2Client`): split, it read "I cloud", "G RPC", "O Auth2 client", which
* no rule can tell from English; whole, it fails the reference tests loudly.
*/
const OPENS = new Set(
(
@@ -584,9 +587,10 @@ export function unname(prose: string, id: string): string {
rest = prose.slice(m[1].length + 1 + (m[2] === 'is' || m[2] === 'are' ? m[2].length + 1 : 0));
if (rest === prose || !rest) return prose;
const lead = /^[A-Za-z]*[a-z0-9][A-Z][A-Za-z0-9]*(?= )/.exec(rest)?.[0] ?? '';
if (BRANDS.has(lead)) return rest;
if (/^[a-z]|\d/.test(lead)) rest = words(lead) + rest.slice(lead.length);
return rest[0].toUpperCase() + rest.slice(1);
if (!lead) return rest[0].toUpperCase() + rest.slice(1);
const split = words(lead);
if (BRANDS.has(lead) || !/^[a-z]|\d/.test(lead) || /^\S /.test(split)) return rest;
return split[0].toUpperCase() + split.slice(1) + rest.slice(lead.length);
}
/** A capitalised first word, followed by `verb`, that names a function. */
@@ -620,14 +624,20 @@ const words = (name: string): string =>
* flattened into one line, printed as its title, its CLI row and its index row.
* A sentence does not cross a paragraph, so a summary that runs past the first
* one is that paragraph's sentence instead.
*
* A comment with no full stop at all is cut where its first LINE ends: GET
* /v1/ai/memory/recall was summarised "Recall recent/relevant memories for
* context injection; with q it" while its description goes on "ranks
* semantically, …". A summary that stops inside its description's first
* sentence is that sentence, whole.
*/
const sentenceOf = (summary: string, description: string): string => {
const flat = summary.replace(/\s+/g, ' ').trim();
const whole = firstSentence(description);
if (flat && whole.length > flat.length && whole.startsWith(flat)) return whole;
const [lead, ...block] = description.split(/\n\s*\n/);
const p = (lead ?? '').replace(/\s+/g, ' ').trim().replace(/:$/, '');
return block.length && p && flat.length > p.length + 1 && flat.startsWith(p)
? firstSentence(description)
: flat;
return block.length && p && flat.length > p.length + 1 && flat.startsWith(p) ? whole : flat;
};
/**
+10 -2
View File
@@ -171,8 +171,11 @@ describe('readable — a page describes its operation in English', () => {
// own — so the rule is held against ALL of the document's prose, not the
// pages: a sentence that stops at a quotation's own "?" while the paragraph
// goes on in lower case, or with a dash, stopped too soon —
// `Answers "what am I approving?"` of "… for a pending device code." And a
// summary does not run on past the paragraph its description opens with.
// `Answers "what am I approving?"` of "… for a pending device code." A
// summary does not run on past the paragraph its description opens with, and
// the line printed from it — CLI row, index row, MCP legend — does not stop
// inside that description's first sentence: "… with q it" of "… with q it
// ranks semantically, …".
it('ends each first sentence where its paragraph does not go on', () => {
const stopped: string[] = [];
let read = 0;
@@ -194,13 +197,18 @@ describe('readable — a page describes its operation in English', () => {
expect(stopped).toEqual([]);
expect(read).toBeGreaterThan(10_000);
const ran: string[] = [];
const short: string[] = [];
for (const op of doc.operations) {
const [first = '', ...block] = op.description.split(/\n\s*\n/);
const lead = flat(first).replace(/:$/, '');
const past = op.summary.length > lead.length + 1 && op.summary.startsWith(lead);
if (block.length && lead && past) ran.push(op.id);
const line = firstSentence(op.summary || op.description);
const why = early(line, op.description);
if (why) short.push(`${op.id}: ${why}: ${line.slice(-50)}`);
}
expect(ran).toEqual([]);
expect(short).toEqual([]);
});
it('writes no page for an operation that answers 501 to every call', () => {