Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <today's.pdf> <new.pdf> [--expected <file>]`** 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.
Expand Down
9 changes: 6 additions & 3 deletions specs/002-analysis-report/research.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions specs/002-analysis-report/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions specs/002-analysis-report/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,9 @@ compare-report <today's.pdf> <new.pdf> [--expected <file>]`, 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.
Expand Down Expand Up @@ -516,6 +517,17 @@ compare-report <today's.pdf> <new.pdf> [--expected <file>]`, 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 (#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
T019 and T020. Last; deployed by the user.
Expand Down
8 changes: 7 additions & 1 deletion src/compare/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -324,6 +325,11 @@ export function applyRecorded(
const used = new Set<number>();
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;
Expand Down
5 changes: 4 additions & 1 deletion src/compare/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
52 changes: 51 additions & 1 deletion src/compare/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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: `<date> <action> <names>`. */
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<T>(
part: 'pathways' | 'found' | 'not-found',
Expand Down
55 changes: 55 additions & 0 deletions test/compare/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions test/compare/units.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});

Expand Down
Loading