From 020fb729c27db1edbca57e88e617f43a29fcb4f3 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Sat, 14 Nov 2026 02:19:55 +0000 Subject: [PATCH 1/2] Spec 002 T027: a book's and a web page's references in today's form The user decided on 14 Nov 2026 that the analysis report prints these two kinds of reference as today's report does. Today's form is read from analysis-report's ApaStyle: a web page's title and "Retrieved from" its address, without authors or year; a book's authors and year, then its chapter's title and the book's parted by a comma, in italics, then its pages; no publisher or ISBN. So what T019 read as a book's publisher after a comma is the book's title (research R9). - Spec 001's reference() is shared through sectionHtml, so the report gets its own form, reportReference, passed in; spec 001's book is unchanged. Whether it changes too is left as a follow-up. - The address and publisher come from spec 001's literature query as it is; no field added. - compare-report's reader leaves out pdf-export's list number on a reference with no year, which a web page in today's form has none of. - Tests, each seen failing without its change: the fixture graph's book and web page in the report's form, spec 001's unchanged; a book and a web page in today's form and the report's agreeing in compare-report, its today's-form helper taught both from ApaStyle. Co-Authored-By: Claude Opus 5.5 --- specs/002-analysis-report/research.md | 28 +++++++++++--- specs/002-analysis-report/spec.md | 6 ++- specs/002-analysis-report/tasks.md | 23 ++++++++++- src/compare/reduce.ts | 4 +- src/layout/section.ts | 30 ++++++++++----- src/report/layout.ts | 29 +++++++++++++- test/compare/report.test.ts | 38 ++++++++++++++++++ test/compare/today-form.ts | 12 ++++++ test/report/body.test.ts | 55 ++++++++++++++++++++++++++- 9 files changed, 203 insertions(+), 22 deletions(-) diff --git a/specs/002-analysis-report/research.md b/specs/002-analysis-report/research.md index eb7a17e..7b95cdf 100644 --- a/specs/002-analysis-report/research.md +++ b/specs/002-analysis-report/research.md @@ -495,11 +495,29 @@ not known. **Differences found** (the same at 25 and at 50): -| Part | Sections (25 / 50) | Reason | -| -------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Edit history, order within a day | 4 / 8 (and in some of the next row's) | **Bug, fixed (#213).** Today's groups a day's edits into a hash map with `computeIfAbsent`, which puts a new key at the head of its bucket; Created, Authored and Reviewed share one, so a day reads Modified, Revised, Edited, Reviewed, Authored, Created. R2 inferred the order of `put`. | -| Edit history, Modified rows | 11 / 20 | **For a decision.** 20 of the 50 pathways have two `modified` edits in the release 98 graph (R2 said one). Today's prints none for them, and one where there is one; pdf-export prints a row for each. | -| References | 1 / 2 | A Greek letter today's typeface lacks, written as U+0000 (an improvement). **For a decision:** a book's and a web page's reference printed in spec 001's form, where today's prints a web page's address and a book's publisher after a comma. | +| Part | Sections (25 / 50) | Reason | +| -------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Edit history, order within a day | 4 / 8 (and in some of the next row's) | **Bug, fixed (#213).** Today's groups a day's edits into a hash map with `computeIfAbsent`, which puts a new key at the head of its bucket; Created, Authored and Reviewed share one, so a day reads Modified, Revised, Edited, Reviewed, Authored, Created. R2 inferred the order of `put`. | +| Edit history, Modified rows | 11 / 20 | **For a decision.** 20 of the 50 pathways have two `modified` edits in the release 98 graph (R2 said one). Today's prints none for them, and one where there is one; pdf-export prints a row for each. | +| References | 1 / 2 | A Greek letter today's typeface lacks, written as U+0000 (an improvement). **Decided 14 Nov 2026, today's form:** a book's and a web page's reference were printed in spec 001's form, where today's prints a web page's address and, after a comma, what this read as a book's publisher (corrected below). The report now prints today's form (T027). | + +**Today's form of a reference, read from its source (T027, 14 Nov 2026).** The analysis +report library's `ApaStyle` (the `1.4.0-SNAPSHOT` build of `reactome/analysis-report` in the +local Maven repository, read from its compiled classes; not shown to be the deployed build's +own copy) writes: + +- **an article:** `Authors (year). Title. Journal, volume, pages.`, as spec 001's but for the + authors' form, which the comparison does not read; +- **a web page:** `Title. Retrieved from address`, with no authors and no year; +- **a book:** `Authors (year).` then, in italics, `Chapter, Book` (or `. Book` with no + chapter), then `, pages` and a full stop; no publisher and no ISBN. + +So the words after the comma that this table first called a book's publisher are the book's +title, after its chapter's: on R-HSA-1280215 in release 98, `(2002). Cytokines and the immune +system, Cytokine Reference.`, the book's publisher being in the graph but not printed. Spec +001's form, which the report printed until T027, does print the publisher (`… In: Cytokine +Reference. Elsevier Science.`), and links a web page's title to its address without printing +it. Every other part agreed at both sizes. The edit history's grouping by day (T007) holds: on the two pathways with two edits of one kind on one day at different times, both diff --git a/specs/002-analysis-report/spec.md b/specs/002-analysis-report/spec.md index 7e8b1b3..83028a6 100644 --- a/specs/002-analysis-report/spec.md +++ b/specs/002-analysis-report/spec.md @@ -395,7 +395,11 @@ disk, the printer's memory-backed folder included, for the submitted identifiers here. - **One implementation:** the report is made by pdf-export, beside spec 001's single-event PDF, reusing its theme, fonts, layout, printing and figures; a reported - pathway's detail is close to spec 001's section, and shares its parts. The + pathway's detail is close to spec 001's section, and shares its parts. A book's and a web + page's references are printed in today's report's form, not spec 001's (decided 14 Nov + 2026, T027, research R9): a web page's title and address, without authors or year; a + book's authors and year, its chapter's title and the book's parted by a comma, and its + pages, without publisher or ISBN. Spec 001's book keeps its own form. The constitution's Principle I is amended with this spec (version 1.1.0) to cover the website's PDFs generally, the analysis report among them. - **Spec 001's FR-017** (characters no font draws) is merged before this spec. diff --git a/specs/002-analysis-report/tasks.md b/specs/002-analysis-report/tasks.md index 6b9628d..03ffbd7 100644 --- a/specs/002-analysis-report/tasks.md +++ b/specs/002-analysis-report/tasks.md @@ -483,7 +483,7 @@ compare-report [--expected ]`, reading both with s two `modified` edits, and three references (R9). The grouping by day holds: one row in both on the two pathways with two edits of one kind on one day at different times. - **Left open:** the Modified rows and the book and web-page references, for a decision - (R9); the expression, interactors and species-comparison tokens, and each figure's + (R9; the references decided 14 Nov 2026, T027); the expression, interactors and species-comparison tokens, and each figure's overlay check (quickstart §3), not run here. - [x] T020 [US2] The replay and the time (SC-202, SC-203, SC-204, SC-205, SC-207), after @@ -516,6 +516,27 @@ compare-report [--expected ]`, reading both with s themselves on a desktop (pdf.js, Poppler and PDFium opened both reports here), and the failure while the PDF is made (R5). +- [x] T027 [US1] The references in today's form, as the user decided on 14 Nov 2026 (T019, + research R9): a book's and a web page's, in the analysis report. + - Today's form read from `reactome/analysis-report`'s `ApaStyle` (R9): a web page's title + and address; a book's chapter's title and the book's parted by a comma, and its pages. + T019's "a book's publisher after a comma" was the book's title: today's prints no + publisher. + - Spec 001's `reference` is shared with the report (its `sectionHtml`), so the report has + its own form, `reportReference` (`src/report/layout.ts`), passed to `sectionHtml`; spec + 001's book is unchanged. An article is spec 001's in both. + - Read through spec 001's `literature` query, which already reads the address and the + publisher; no field added. + - Tests, each seen failing without the change: the fixture graph's book and web page in + the report's form, spec 001's form of them unchanged (`test/report/body.test.ts`); and + a book and a web page printed in today's form and the report's, agreeing in + `compare-report` (`test/compare/report.test.ts`, its today's-form helper taught both + from `ApaStyle`). The comparison's reader now leaves out pdf-export's list number on a + reference with no year (a web page in today's form), seen failing without it. + - **Follow-up, not done:** whether spec 001's book takes this form too is the user's to + decide later. + - **Done (#PR).** + - [ ] T021 [WA] The website routed to pdf-export (research R6; plan, Order of work, step 8), in WebsiteAngular under spec 010 (the analysis gateway, its Story 6), after T019 and T020. Last; deployed by the user. diff --git a/src/compare/reduce.ts b/src/compare/reduce.ts index 7471584..f71c435 100644 --- a/src/compare/reduce.ts +++ b/src/compare/reduce.ts @@ -600,7 +600,9 @@ function reportItems(part: PartName, texts: readonly string[], own: readonly Lin return found .map((text) => { const year = /\(\d{4}[a-z]?\)/.exec(text); - return unhyphened(year ? text.slice(year.index) : text); + // one with no year (a web page, in today's form) read whole, but pdf-export's + // list number (`3. `), which today's does not print + return unhyphened(year ? text.slice(year.index) : text.replace(/^\d+\.\s+/, '')); }) .filter((item) => item !== '') .map(hash); diff --git a/src/layout/section.ts b/src/layout/section.ts index b9dbd8e..00491e0 100644 --- a/src/layout/section.ts +++ b/src/layout/section.ts @@ -26,9 +26,10 @@ export interface SectionInput { report: Report; /** * The analysis report's own (spec 002 T009): markup in place of the edit history and what - * an analysis found, and the stable identifier linked to the event's page. + * an analysis found, the stable identifier linked to the event's page, and its own form of + * a reference (spec 002 T027). */ - analysisReport?: { tail: string; linkStId: true }; + analysisReport?: { tail: string; linkStId: true; reference: (r: Reference) => string }; } /** An event's heading level: the PDF's own event or a chapter's is h1, h6 the deepest. */ @@ -119,7 +120,7 @@ export function sectionHtml(section: Section, input: SectionInput): string { if (section.references.length > 0) { parts.push( `

Literature references

    ${section.references - .map((r) => `
  1. ${reference(r)}
  2. `) + .map((r) => `
  3. ${(input.analysisReport?.reference ?? reference)(r)}
  4. `) .join('')}
` ); } @@ -159,21 +160,30 @@ function definitions(rows: readonly [string, string][]): string { const capital = (word: string): string => word.charAt(0).toUpperCase() + word.slice(1); -/** A literature reference: its authors, year, title and where it was published. */ -export function reference(r: Reference): string { +/** A reference's authors and year, as markup: `Placeholder, A, Example, B (2003). `, or empty. */ +export function referenceLead(r: Reference): string { const authors = r.authors.length > 0 ? `${text(r.authors.join(', '))}${r.moreAuthors ? ' et al.' : ''}` : ''; const year = r.year === null ? '' : ` (${String(r.year)})`; const named = `${authors}${year}`; // a list of authors may end in an initial's full stop: it is not doubled - const lead = named === '' ? '' : `${named}${named.endsWith('.') ? '' : '.'} `; + return named === '' ? '' : `${named}${named.endsWith('.') ? '' : '.'} `; +} + +/** A reference's title as markup, linked to PubMed where it has a PubMed identifier. */ +export function referenceTitle(r: Reference): string { + return r.pubMedId === null + ? text(r.title) + : Links.a(`https://pubmed.ncbi.nlm.nih.gov/${String(r.pubMedId)}/`, r.title); +} + +/** A literature reference: its authors, year, title and where it was published. */ +export function reference(r: Reference): string { + const lead = referenceLead(r); if (r.kind === 'web page') { return `${lead}${r.url ? Links.a(r.url, r.title) : text(r.title)}.`; } - const title = - r.pubMedId === null - ? text(r.title) - : Links.a(`https://pubmed.ncbi.nlm.nih.gov/${String(r.pubMedId)}/`, r.title); + const title = referenceTitle(r); if (r.kind === 'book') { const chapter = r.chapterTitle ? `${text(r.chapterTitle)}. In: ` : ''; const rest = [r.publisher, r.isbn ? `ISBN ${r.isbn}` : null].filter((x) => x !== null); diff --git a/src/report/layout.ts b/src/report/layout.ts index a15f9d0..843c167 100644 --- a/src/report/layout.ts +++ b/src/report/layout.ts @@ -19,8 +19,9 @@ import type { ReportedPathway, ResourceTable, } from './model.ts'; +import type { Reference } from '../graph/queries.ts'; import type { Report } from '../layout/section.ts'; -import { sectionHtml } from '../layout/section.ts'; +import { reference, referenceLead, referenceTitle, sectionHtml } from '../layout/section.ts'; import { KEPT, OVERVIEW_CAPTION, @@ -362,6 +363,30 @@ export interface DetailsInput { report: Report; } +/** + * A literature reference as the analysis report prints it (spec 002 T027, decided 14 Nov + * 2026: today's form for a book and a web page, read from `reactome/analysis-report`'s + * `ApaStyle`; research R9). An article is spec 001's, which today's matches. + * + * - **A web page:** its title, then "Retrieved from" and its address, linked; today's names + * no authors and no year for one. + * - **A book:** its authors and year, then its chapter's title and the book's, parted by a + * comma, in italics, then its pages after a comma; today's prints no publisher or ISBN. + */ +export function reportReference(r: Reference): string { + if (r.kind === 'web page') { + return r.url === null + ? `${text(r.title)}.` + : `${text(r.title)}. Retrieved from ${Links.a(r.url, r.url)}`; + } + if (r.kind === 'book') { + const chapter = r.chapterTitle === null ? '' : `${text(r.chapterTitle)}, `; + const pages = r.pages === null ? '' : `, ${text(r.pages)}`; + return `${referenceLead(r)}${chapter}${referenceTitle(r)}${pages}.`; + } + return reference(r); +} + /** * One pathway's detail: spec 001's section for it, with the report's edit history in place * of spec 001's and its stable identifier linked to its page, then its entities found per @@ -392,7 +417,7 @@ export function detailHtml( figures: input.figures, found: null, report: input.report, - analysisReport: { tail, linkStId: true }, + analysisReport: { tail, linkStId: true, reference: reportReference }, } ); } diff --git a/test/compare/report.test.ts b/test/compare/report.test.ts index 48aefdb..1353993 100644 --- a/test/compare/report.test.ts +++ b/test/compare/report.test.ts @@ -54,6 +54,33 @@ const REFERENCE: Reference = { moreAuthors: false, }; +const BOOK: Reference = { + ...REFERENCE, + dbId: 2, + kind: 'book', + title: 'A made-up book', + chapterTitle: 'A made-up chapter', + journal: null, + volume: null, + pages: '1-20', + year: 2010, + isbn: '0000000000', + publisher: 'Placeholder Press', + authors: ['Sample, C'], +}; +const WEB_PAGE: Reference = { + ...REFERENCE, + dbId: 3, + kind: 'web page', + title: 'A made-up web page', + journal: null, + volume: null, + pages: null, + year: null, + url: 'https://example.org/placeholder', + authors: ['Dummy, D'], +}; + /** A made-up detail for a pathway: spec 001's section and an edit history. */ function detail( stId: string, @@ -214,6 +241,17 @@ describe("the analysis report's comparison (FR-208)", { timeout: 600_000 }, () = assert.ok((t.found?.[0]?.rows.length ?? 0) > 0, 'the identifiers found'); assert.equal(t.notFound?.length, baseline.notFound?.length); assert.deepEqual(differences(t, baseline), []); + // a book and a web page: today's form, which the report's matches (T027) + const cited = new Map( + [...details(model)].map(([id, d]) => [ + id, + { ...d, section: { ...d.section, references: [REFERENCE, BOOK, WEB_PAGE] } }, + ]) + ); + assert.deepEqual( + differences(await today(model, cited), (await printed(model, cited)).reduced), + [] + ); // no references: today's "References" over nothing is as pdf-export's none const bare = new Map( [...details(model)].map(([id, d]) => [ diff --git a/test/compare/today-form.ts b/test/compare/today-form.ts index fe2bf4e..4a8e12d 100644 --- a/test/compare/today-form.ts +++ b/test/compare/today-form.ts @@ -54,6 +54,12 @@ const FILLER = 6 ); +/** + * A reference as today's `ApaStyle` writes it (reactome/analysis-report): an article's + * authors, year, title and where; a web page's title and address alone; a book's authors and + * year, then (with no space, as today's) its chapter's title and the book's parted by a + * comma, and its pages; no publisher or ISBN. + */ function reference(r: Reference): string { const authors = r.authors.map(todayName); const names = @@ -61,6 +67,12 @@ function reference(r: Reference): string { ? `${authors.slice(0, -1).join(', ')} & ${authors.at(-1) ?? ''}` : (authors[0] ?? ''); const year = r.year === null ? '' : ` (${String(r.year)})`; + if (r.kind === 'web page') return `${escape(r.title)}. Retrieved from ${escape(r.url ?? '')}`; + if (r.kind === 'book') { + const title = r.chapterTitle === null ? `. ${r.title}` : `.${r.chapterTitle}, ${r.title}`; + const pages = r.pages === null ? '' : `, ${r.pages}`; + return `${escape(`${names}${year}`)}${escape(title)}${escape(pages)}.`; + } const where = [r.journal, r.volume === null ? null : String(r.volume), r.pages].filter( (x) => x !== null ); diff --git a/test/report/body.test.ts b/test/report/body.test.ts index 2e8b4e1..a70f838 100644 --- a/test/report/body.test.ts +++ b/test/report/body.test.ts @@ -4,10 +4,10 @@ import { after, before, describe, test } from 'node:test'; import { checkToken, readReportAnalysis, type ReportFilter } from '../../src/analysis/report.ts'; import { graphSettings, openGraph } from '../../src/graph/connection.ts'; -import type { SpeciesRef } from '../../src/graph/queries.ts'; +import { literature, type SpeciesRef } from '../../src/graph/queries.ts'; import { CharacterLog, REPLACEMENT } from '../../src/layout/characters.ts'; import { Links } from '../../src/layout/html.ts'; -import type { Report } from '../../src/layout/section.ts'; +import { reference, type Report } from '../../src/layout/section.ts'; import { readDetails, type PathwayDetail } from '../../src/report/details.ts'; import { onRequestReport, @@ -170,5 +170,56 @@ describe("the report's details and identifier lists", () => { await graph.close(); } }); + + test("a book's and a web page's references in today's form; spec 001's unchanged (T027)", async () => { + const graph = openGraph(graphSettings(process.env)); + try { + const m = await model(REPORT_TOKENS.overrepresentation); + const details = await readDetails(graph, m.pathways, new Set(['R-HSA-90000300'])); + // the fixture's book (with its publisher) and web page, read by spec 001's query + const SHARED_REACTION = 90000220; + const cited = (await literature(graph, [SHARED_REACTION])).get(SHARED_REACTION) ?? []; + assert.deepEqual( + cited.map((r) => r.kind), + ['book', 'web page'] + ); + const [book, page] = cited; + assert.ok(book && page); + assert.equal(book.publisher, 'Placeholder Press'); + assert.equal(page.url, 'https://example.org/placeholder'); + const alphaId = 90000200; + const alphaDetail = details.get(alphaId); + assert.ok(alphaDetail); + const out = body( + m, + new Map([ + ...details, + [alphaId, { ...alphaDetail, section: { ...alphaDetail.section, references: cited } }], + ]) + ); + const items = [ + ...(/
    ([\s\S]*?)<\/ol>/ + .exec(/

    /.exec(out)?.[0] ?? '')?.[1] + ?.matchAll(/
  1. ([\s\S]*?)<\/li>/g) ?? []), + ].map((item) => item[1] ?? ''); + const plain = (markup: string): string => markup.replace(/<[^>]*>/g, ''); + // today's: a book's chapter and title parted by a comma, in italics, its pages after + // a comma, no publisher or ISBN; a web page's title and address, no authors or year + assert.deepEqual(items.map(plain), [ + 'Placeholder01, P. (2010). Placeholder chapter, Placeholder book, 1-20.', + 'Placeholder web page. Retrieved from https://example.org/placeholder', + ]); + assert.match(items[0] ?? '', /Placeholder chapter, Placeholder book<\/i>/); + assert.match(items[1] ?? '', //); + // spec 001's form, which its book keeps + assert.equal( + plain(reference(book)), + 'Placeholder01, P. (2010). Placeholder chapter. In: Placeholder book. Placeholder Press, ISBN 0000000000.' + ); + assert.equal(plain(reference(page)), 'Placeholder02, P. Placeholder web page.'); + } finally { + await graph.close(); + } + }); }); }); From 922e940bfdd00eecb938677c660455fc4ca7c60d Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Sat, 14 Nov 2026 02:20:21 +0000 Subject: [PATCH 2/2] Spec 002 T027: its PR number Co-Authored-By: Claude Opus 5.5 --- specs/002-analysis-report/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/002-analysis-report/tasks.md b/specs/002-analysis-report/tasks.md index 03ffbd7..637ca1b 100644 --- a/specs/002-analysis-report/tasks.md +++ b/specs/002-analysis-report/tasks.md @@ -535,7 +535,7 @@ compare-report [--expected ]`, reading both with s reference with no year (a web page in today's form), seen failing without it. - **Follow-up, not done:** whether spec 001's book takes this form too is the user's to decide later. - - **Done (#PR).** + - **Done (#218).** - [ ] T021 [WA] The website routed to pdf-export (research R6; plan, Order of work, step 8), in WebsiteAngular under spec 010 (the analysis gateway, its Story 6), after