From 94ec62e3c538f4eabb55870357928eacc033d0c1 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Sat, 14 Nov 2026 02:06:43 +0000 Subject: [PATCH 1/2] Spec 002 T026: every Modified row printed, a listed difference The user decided on 14 Nov 2026 to keep pdf-export's Modified rows: a pathway with two or more modified edits prints a row for each, where today's report prints none. T019's run found 20 of 50 pathways so in release 98. - spec.md's Assumptions list it; research R2's "at most one Modified row" corrected with a dated note; R9's row marked decided. - compare-report lists it itself, narrowly: a Modified row in ours and not in today's, for a pathway where ours has two or more, is marked recorded with its reason; anything else about Modified rows is still a difference. applyRecorded leaves such a difference as it is. - Tests: the listed case, seen failing without the rule; a Modified row missing or extra where ours has one, and today's own row missing beside two of ours, still caught, seen failing with the rule widened. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 +- specs/002-analysis-report/research.md | 9 +++-- specs/002-analysis-report/spec.md | 6 +++ specs/002-analysis-report/tasks.md | 16 +++++++- src/compare/compare.ts | 8 +++- src/compare/output.ts | 5 ++- src/compare/report.ts | 52 ++++++++++++++++++++++++- test/compare/report.test.ts | 55 +++++++++++++++++++++++++++ test/compare/units.test.ts | 10 +++++ 9 files changed, 155 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8e763be..d9e9392 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ and, for the analysis report, `reactome/analysis-report`. `--site`) makes one report, for checks by hand and the comparison. - **`pdf-export compare-report [--expected ]`** compares a report with today's by meaning (FR-208), printing counts and salted hashes, never a - name or a submitted identifier. + name or a submitted identifier. One listed difference it marks itself: a pathway's + Modified rows where it has two or more `modified` edits (the spec's Assumptions). See [`specs/002-analysis-report/`](specs/002-analysis-report/) for the address's answers (`contracts/service.md`) and what the report holds. diff --git a/specs/002-analysis-report/research.md b/specs/002-analysis-report/research.md index eb7a17e..78dc935 100644 --- a/specs/002-analysis-report/research.md +++ b/specs/002-analysis-report/research.md @@ -135,8 +135,11 @@ no diagram of its own. Three additions: four kinds where it reads them, before both (`src/model/build.ts`), with a test that fails without that filter, on the rows and on the cover's names; spec 001's PDFs are otherwise unchanged. The report reads the extended query's edits itself, for its own - rule, not through spec 001's `assemble`, whose sections hold spec 001's merged rows. (The graph keeps one `modified` per object, - the latest, so there is at most one Modified row, as today.) + rule, not through spec 001's `assemble`, whose sections hold spec 001's merged rows. (This + said the graph keeps one `modified` per object, so there is at most one Modified row. + **Corrected 14 Nov 2026 by T019's run (R9):** in release 98, 20 of the 50 pathways + compared have two `modified` edits. Today's report prints no Modified row for those, and + pdf-export prints one for each; the user kept pdf-export's, a listed difference.) - **The edit history** follows today's report's rule, not spec 001's: one row for each date and action (created, modified, authored, edited, reviewed, revised), sorted by date, each with that row's names (up to five, then "et al."), and an edit with no @@ -498,7 +501,7 @@ not known. | 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. | +| 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. | Every other part agreed at both sizes. The edit history's grouping by day (T007) holds: diff --git a/specs/002-analysis-report/spec.md b/specs/002-analysis-report/spec.md index 7e8b1b3..f0e860a 100644 --- a/specs/002-analysis-report/spec.md +++ b/specs/002-analysis-report/spec.md @@ -444,6 +444,12 @@ disk, the printer's memory-backed folder included, for the submitted identifiers always empty and left out (an obvious bug: it compares their display names with the mappings' resources); - an edit with no date left out of the edit history, where today's report fails; + - a Modified row for each `modified` edit of a pathway that has two or more, where + today's report prints no Modified row for it (20 of 50 pathways in release 98, T019, + research R9): every edit printed (decided 14 Nov 2026). `compare-report` lists it + itself, narrowly: a Modified row in pdf-export's and not in today's, for a pathway + where pdf-export's has two or more; any other Modified row missing or extra is still a + difference; - a character today's typeface lacks (a Greek letter in a reference's title) printed, where today's writes U+0000 in its place (T019, research R9); - an `importableOnly` of spaces alone read as the default (false), where today's framework diff --git a/specs/002-analysis-report/tasks.md b/specs/002-analysis-report/tasks.md index 6b9628d..0cc1b08 100644 --- a/specs/002-analysis-report/tasks.md +++ b/specs/002-analysis-report/tasks.md @@ -483,8 +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 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); 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. @@ -516,6 +517,17 @@ 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] T026 [US1] The Modified rows, as the user decided on 14 Nov 2026 (T019, research R9): + every `modified` edit printed, as pdf-export does, a listed difference. + - Spec Assumptions lists it; R2's "at most one Modified row" corrected with a dated note. + - `compare-report` lists it itself (`listedModifiedRows`, `src/compare/report.ts`), + narrowly: a Modified row in pdf-export's and not in today's, for a pathway where + pdf-export's has two or more Modified rows, marked recorded with its reason, so it is + not counted as unrecorded. Seen failing without it; and a test that a Modified row + missing or extra where pdf-export's has only one, or one of today's missing beside two + of pdf-export's, is still caught, seen failing with the rule widened. + - **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/compare.ts b/src/compare/compare.ts index 50b6545..30c468f 100644 --- a/src/compare/compare.ts +++ b/src/compare/compare.ts @@ -314,7 +314,8 @@ function compareContents( /** * The differences with those `--expected` records marked, with their reasons, and the - * records that matched nothing, by their place in the file. A record naming events covers + * records that matched nothing (a difference the comparison lists itself, already marked, is + * left as it is), by their place in the file. A record naming events covers * only those, and never a difference that names none. */ export function applyRecorded( @@ -324,6 +325,11 @@ export function applyRecorded( const used = new Set(); const out: Difference[] = []; for (const difference of differences) { + // a difference the comparison already lists keeps its own reason + if (difference.recorded !== undefined) { + out.push({ ...difference }); + continue; + } let rest: Difference | null = { ...difference }; for (const [index, record] of recorded.entries()) { if (!rest) break; diff --git a/src/compare/output.ts b/src/compare/output.ts index c576c12..f550d9d 100644 --- a/src/compare/output.ts +++ b/src/compare/output.ts @@ -231,7 +231,10 @@ export interface Difference { events?: string[]; reference?: unknown; new?: unknown; - /** Why it is expected, from `--expected`'s file (never read from a PDF). */ + /** + * Why it is expected: from `--expected`'s file, or a difference the comparison lists itself + * (spec 002's Modified rows); never read from a PDF. + */ recorded?: string; } diff --git a/src/compare/report.ts b/src/compare/report.ts index e6ab57f..dffcae9 100644 --- a/src/compare/report.ts +++ b/src/compare/report.ts @@ -507,8 +507,23 @@ export function compareReports(file: string, ref: ReportReduced, neu: ReportRedu contents: null, shortEntries: 0, }); + const modified = listedModifiedRows(ref.reduced, neu.reduced); + if (modified.events.length > 0) { + add({ + part: 'section', + field: 'edits', + kind: 'extra', + count: modified.events.length, + events: modified.events, + recorded: MODIFIED_ROWS_LISTED, + }); + } out.push( - ...compareReduced(file, shell(ref.reduced, ref.properties), shell(neu.reduced, neu.properties)) + ...compareReduced( + file, + shell(ref.reduced, ref.properties), + shell(modified.reduced, neu.properties) + ) ); for (const [side, properties] of [ ['reference', ref.properties], @@ -595,6 +610,41 @@ export function compareReports(file: string, ref: ReportReduced, neu: ReportRedu return out; } +/** Why a pathway's Modified rows are a listed difference (spec 002 Assumptions). */ +export const MODIFIED_ROWS_LISTED = + "Listed (spec 002 Assumptions, decided 14 Nov 2026): a pathway with two or more modified edits has a Modified row for each, where today's report prints none"; + +/** An edit-history row's action, as reportEditRows writes it: ` `. */ +const actionOf = (row: string): string | undefined => row.split(' ')[1]; + +/** + * The listed difference in the edit history's Modified rows: where pdf-export's detail of a + * pathway has two or more Modified rows, each that today's does not have is taken out of + * pdf-export's side, and the pathway named. Nothing else is: a Modified row missing from + * pdf-export's, or one extra where it has only one, is still a difference. + */ +export function listedModifiedRows( + ref: Reduced, + neu: Reduced +): { reduced: Reduced; events: string[] } { + const events: string[] = []; + const sections = neu.sections.map((section) => { + const theirs = ref.sections.find((s) => s.stId === section.stId); + const ours = section.parts.find((part) => part.part === 'edits'); + if (!theirs || !ours) return section; + if (ours.items.filter((row) => actionOf(row) === 'modified').length < 2) return section; + const today = theirs.parts.find((part) => part.part === 'edits')?.items ?? []; + const kept = ours.items.filter((row) => actionOf(row) !== 'modified' || today.includes(row)); + if (kept.length === ours.items.length) return section; + events.push(section.stId); + return { + ...section, + parts: section.parts.map((part) => (part === ours ? { ...part, items: kept } : part)), + }; + }); + return { reduced: { ...neu, sections }, events }; +} + /** A part on both sides compared, or said missing or extra. */ function comparePart( part: 'pathways' | 'found' | 'not-found', diff --git a/test/compare/report.test.ts b/test/compare/report.test.ts index 48aefdb..10cc9c4 100644 --- a/test/compare/report.test.ts +++ b/test/compare/report.test.ts @@ -11,6 +11,7 @@ import { after, before, describe, test } from 'node:test'; import { checkToken, readReportAnalysis } from '../../src/analysis/report.ts'; import { + MODIFIED_ROWS_LISTED, compareReportCommand, compareReports, reduceReportPdf, @@ -383,6 +384,60 @@ describe("the analysis report's comparison (FR-208)", { timeout: 600_000 }, () = ); }); + test("the edit history's Modified rows: listed where pdf-export's has two or more, caught where it has one", async () => { + const modified = (date: string): HistoryRow => ({ + date, + action: 'Modified', + names: ['Example, B'], + etAl: false, + }); + // two modified edits: pdf-export prints a row for each, today's none (listed, R9) + const two = await printed( + model, + details(model, [...HISTORY, modified('2026-02-01'), modified('2026-03-01')]) + ); + const found = compareReports('report.pdf', baseline, two.reduced); + assert.deepEqual( + found.filter((d) => d.recorded === undefined), + [] + ); + assert.deepEqual( + found.map((d) => [d.part, d.field, d.kind, d.events, d.recorded]), + [ + [ + 'section', + 'edits', + 'extra', + model.pathways.slice(0, 2).map((p) => p.stId), + MODIFIED_ROWS_LISTED, + ], + ] + ); + const out: string[] = []; + const ours = await printed(model, details(model)); + assert.equal( + await compareReportCommand([ours.path, two.path], { + out: (text) => out.push(text), + err: () => undefined, + output: new Output('salt'), + }), + 0 + ); + assert.match(out.join(''), /"unrecorded": 0/); + // one modified edit: today's prints its row too, so a row missing or extra is caught + const one = await printed(model, details(model, [...HISTORY, modified('2026-02-01')])); + assert.deepEqual(differences(baseline, one.reduced), ['section edits text']); + assert.deepEqual(differences(one.reduced, baseline), ['section edits text']); + // two in pdf-export's and another Modified row in today's: that row is still missing + const elsewhere = await printed(model, details(model, [...HISTORY, modified('2026-04-01')])); + assert.deepEqual( + compareReports('report.pdf', elsewhere.reduced, two.reduced) + .filter((d) => d.recorded === undefined) + .map((d) => `${d.part} ${d.field ?? ''} ${d.kind}`), + ['section edits text'] + ); + }); + test('a figure present on one side only', async () => { const figures = new Map(model.pathways.slice(0, 1).map((p) => [`diagram/${p.stId}`, SVG])); const found = differences(baseline, (await printed(model, details(model), figures)).reduced); diff --git a/test/compare/units.test.ts b/test/compare/units.test.ts index f2c034a..a9d98e1 100644 --- a/test/compare/units.test.ts +++ b/test/compare/units.test.ts @@ -333,6 +333,16 @@ describe('--expected', () => { ); assert.equal(other.differences[0]?.recorded, undefined); assert.deepEqual(other.unmatched, [0]); + // a difference the comparison lists itself keeps its reason, and uses up no record + const listed = applyRecorded( + [{ ...difference, recorded: 'listed' }], + [{ part: 'section', kind: 'text', reason: 'x' }] + ); + assert.deepEqual( + listed.differences.map((d) => d.recorded), + ['listed'] + ); + assert.deepEqual(listed.unmatched, [0]); }); }); From 0f36b746a02b494e04d585eff0b6229dbd54c7a7 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Sat, 14 Nov 2026 02:07:05 +0000 Subject: [PATCH 2/2] Spec 002 T026: 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 0cc1b08..31ef04e 100644 --- a/specs/002-analysis-report/tasks.md +++ b/specs/002-analysis-report/tasks.md @@ -526,7 +526,7 @@ compare-report [--expected ]`, reading both with s not counted as unrecorded. Seen failing without it; and a test that a Modified row missing or extra where pdf-export's has only one, or one of today's missing beside two of pdf-export's, is still caught, seen failing with the rule widened. - - **Done (#PR).** + - **Done (#217).** - [ ] 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