diff --git a/specs/002-analysis-report/research.md b/specs/002-analysis-report/research.md index 78dc935..badd98b 100644 --- a/specs/002-analysis-report/research.md +++ b/specs/002-analysis-report/research.md @@ -498,11 +498,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 | **Listed (decided 14 Nov 2026).** 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, kept by the user's decision. `compare-report` lists it itself (T026). | -| 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 | **Listed (decided 14 Nov 2026).** 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, kept by the user's decision. `compare-report` lists it itself (T026). | +| 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 f0e860a..281ba39 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 31ef04e..9b34aed 100644 --- a/specs/002-analysis-report/tasks.md +++ b/specs/002-analysis-report/tasks.md @@ -483,9 +483,9 @@ 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 Modified rows decided 14 Nov 2026, T026); the expression, interactors and - species-comparison tokens, and each figure's overlay check (quickstart §3), not run - here. + (R9; the Modified rows decided 14 Nov 2026, T026, and the references, 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 T014, T015 and T017. @@ -528,6 +528,27 @@ compare-report [--expected ]`, reading both with s of pdf-export's, is still caught, seen failing with the rule widened. - **Done (#217).** +- [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 (#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 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 10cc9c4..8579044 100644 --- a/test/compare/report.test.ts +++ b/test/compare/report.test.ts @@ -55,6 +55,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, @@ -215,6 +242,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(); + } + }); }); });