Search import programme: wizard, reference formats, attachments and diagnostics¶
What this is: the delivery plan for importing a systematic search, with or without its PDFs, from the
reference formats researchers actually export, and for telling them exactly what was imported, omitted,
matched, skipped and verified. It is temporary: delete it when the last slice ships, after moving the
lasting parts into docs/features/ris-import.md and the user guide.
Status: approved by the owner on 2026-10-27 (prototype decisions 1-11, decisions D1-D9, the PubMed plan). Interactive prototype with the decisions: https://claude.ai/artifact/6WD4q3wi9s62Rn1T2QqpQQ.
1. Scope¶
Four journeys:
- File only. Import a reference library file. Citations import whether or not PDFs are supplied.
- File, then optional PDFs. When the file contains attachment references, the wizard offers an optional PDF step, previews the match, explains missing files and allows citation-only completion.
- Library folder. Accept a dropped or selected exported library folder holding a reference file and PDF subfolders. Preserve relative paths, find candidate reference files, ask when there is more than one, and offer a file and folder picker where folder drag-and-drop is unavailable.
- Standalone bulk PDF upload for an existing search stays. It shares matching, validation, progress and availability logic with journeys 2 and 3.
Formats: RIS (common format and tested exporter variants, Zotero included), EndNote XML from EndNote, Zotero and other compatible exporters, Zotero RDF (Zotero's export vocabulary only, not arbitrary RDF), PubMed NBIB (citations only), and the existing CSV/TSV.
Out of scope: fetching remote URLs, reading local absolute paths, arbitrary RDF, storing several PDFs per study (D1 follow-up), EndNote tagged-text import (recognised and refused with guidance).
2. Decisions¶
| ID | Decision | Source |
|---|---|---|
| P-1 | PDFs are matched after the import (studies exist only then); the browser preview before import is an estimate at most | Owner, 2026-10-27 |
| P-2 | After matching, the wizard stops on the preview; uploading never starts without confirmation | Owner, 2026-10-27 |
| P-3 | The standalone upload's overwrite policy is hidden in the wizard (a new search has no PDFs) | Owner, 2026-10-27 |
| P-4 | Closing the dialog keeps the chosen PDFs and the run in a root-level store; only a reload or closed tab loses them; the search row and a snackbar lead back to the match | Owner, 2026-10-27 |
| P-5 | "Retry PDFs" reuses the chosen folder and starts a fresh upload of the matched files only; after a reload it falls back to choosing the folder again | Owner, 2026-10-27 |
| P-6 | New default-off Release flag searchImportWizard; flag off keeps the current dialog; the PDF step also needs bulkPdfUpload and the project's permission |
Owner, 2026-10-27 |
| P-7 | Sentence-case button labels; test ids follow the existing page object | Owner, 2026-10-27 |
| P-8 | The wizard waits for m3e-angular on main (#4598, which superseded the #4485 experiment) and is built with its components | Owner, 2026-10-27 |
| P-9 | "Create new stage…" opens the existing Create stage dialog (saved immediately, inactive) and the new stage is auto-selected | Owner, 2026-10-27 |
| P-10 | A file without attachment references offers only Skip at the PDF step and explains the manual route; never promises file-name links that don't exist | Owner, 2026-10-27 |
| P-11 | Attached and verified are separate: the run is not finished until the availability check (#4571) passes | Owner, 2026-10-27 |
| P-10 amended | RIS is not a format without attachment references: RIS L1 attachments are preserved for matching (requirements handoff). PubMed stays citation-only. Capability is detected from content, never from the format name |
Requirements handoff, 2026-10-27 |
| PubMed | LibraryFileType.PubmedNbib = 6; streaming reader, 64 MiB and 50,000 records; mapping in §4.4; GN - SyRF-ID:<id> in the returned file; backend PR first |
Owner, 2026-10-27 |
| D1 | A record with several attachments: the first PDF in record order becomes PdfRelativePath; every other attachment is reported per study in the import diagnostics and the match preview, never dropped silently. The same file listed twice in one record (normaliser-equal paths) is one attachment, not an extra one. Several PDFs per study is a follow-up model change |
Owner, 2026-10-27 |
| D2 | Parser and diagnostics changes ship unflagged (additive, parity-tested). New formats are offered to users only inside the wizard, behind searchImportWizard |
Owner, 2026-10-27 |
| D3 | The "PDFs matched" snackbar shows anywhere in the app | Owner, 2026-10-27 |
| D4 | #4571 (served-PDF verification) merges before the wizard PR | Owner, 2026-10-27 |
| D5 | EndNote non-journal publication name: book title (secondary-title) for sections and proceedings, not the record's own title |
Owner, 2026-10-27 |
| D6 | The owner supplied private Zotero exports (EndNote XML, RIS, Zotero RDF) as diagnostic evidence; they are never committed (§3) | Owner, 2026-10-27 |
| D7 | The wizard uses the m3e-angular sidebar stepper (MatExpStepper/MatExpStep from @camaradesuk/m3e-angular, ADR 0012 in m3e-angular) inside mat-exp-dialog-layout; SyRF pins 0.1.0-alpha.7, the first release that contains it (ADR-027) |
Owner, 2026-10-27 |
| D8 | Parsers and processors stream wherever possible so very large libraries work, follow the record-reader/processor pipeline, and share one well-structured error model (slice S10). The PubMed reader streams lines; no parser in S10's scope loads the whole file or holds every study | Owner, 2026-10-27 |
| D9 | With streaming (S10), RIS's 10 MiB limit is raised to match EndNote XML and Zotero RDF (256 MiB, 100,000 records) so very large RIS libraries import; PubMed keeps 64 MiB / 50,000 (PubMed itself exports at most 10,000 per file). Shipped in S10 (#4597) with both browser checks (create-search and search-import/core/reference-format.ts); the search-upload signing endpoint has no size condition, so the parser's pass-1 limit is the server bound |
Owner, 2026-10-27 |
| EndNote | EndNote XML import moves onto the record pipeline (NewEndnoteReader + StudyEndnoteRecordProcessor), the legacy EndnoteXmlParseImplementation is deleted, and any producer is accepted when the structure is EndNote XML |
Owner, 2026-10-27 |
3. Evidence¶
Owner-supplied Zotero exports (EndNote XML, RIS, Zotero RDF) are private diagnostic inputs and are never committed. Structural findings only:
- All three describe the same 723 records (714 journal articles, 7 books, 2 book sections), 77 with exactly one PDF attachment and 679 with a DOI.
- Attachment forms: EndNote XML
internal-pdf://<n>/<name>.pdf(export folderPDF/<n>/); RISL1 - files/<n>/<name>.pdf; RDFlink:linkto az:Attachmentwhosez:path rdf:resourceisfiles/<n>/<name>.pdf(linkMode 1,application/pdf). All paths contain spaces, three contain non-ASCII characters, none contains%: Zotero writes paths unencoded, even insiderdf:resource. - Today: the EndNote XML import fails outright ("Record is not sourced from EndNote"). The RIS import
completes with 723 studies, discards all 77
L1attachments, and shows 99 identical unnamed "One or more RIS fields … were omitted" warnings plus a truncation notice, because every record carriesLA,DAandCY. Reading the EndNote file with today's rules would also set 77 studies' URL to theinternal-pdf://link.
4. Shared rules¶
4.1 Attachment references and paths¶
Every parser turns attachment references into one AttachmentReference (C#, mirrored in TypeScript, both
tested against one shared vector file):
| Raw reference | Classification | Stored path |
|---|---|---|
internal-pdf://<rest> |
relative to the library's PDF root | <rest> |
files/<n>/x.pdf, PDF/x.pdf, x.pdf |
relative | as written, \ folded to / |
file://…, C:\…, \\server\…, leading / |
local absolute: refused | none; counted diagnostic |
http://, https://, other schemes |
remote: refused, never fetched | none; counted diagnostic |
a .. segment that escapes the root |
traversal: refused | none; counted diagnostic |
relative, extension not .pdf |
non-PDF attachment | reported, not attached |
- Paths are never URL-decoded: no exporter seen writes them encoded, and decoding would corrupt a file
name with a literal
%. Percent signs, spaces and Unicode are kept as written. - Matching uses the existing
PdfPathNormalizer(NFC, punctuation fold, Unicode full case fold, separator fold), so the browser preview, the agent and PM agree. - Two selected files whose normalised paths collide are refused, as today; they are never collapsed.
- One file referenced by several studies is attached to each of them, as today, and the preview says so.
- Several attachments on one record follow D1.
4.2 Library root¶
The reference paths are relative to the exported library's root, which differs by exporter (EndNote and
Zotero's EndNote XML: the PDF folder; Zotero RIS and RDF: the export folder that contains files/). The
browser therefore resolves the root by matching, not by folder name: for each selected PDF path and each
reference path, a selected path that ends with /<reference path> votes for its prefix as the root. Only
references with at least one folder segment vote; a bare x.pdf matches at whichever root wins and never
votes, because a common file name would otherwise vote for many prefixes. A reference whose path matches
under several candidate prefixes casts no vote. The prefix with the most votes wins; a tie is shown to the
user with the candidates. When no reference votes (every reference is a bare file name, as in a flat
PDF/ folder or a CSV of file names), each folder that directly holds selected PDFs is a candidate. A bare
name matches a candidate only when a PDF of that name sits directly in it (not deeper), so a parent folder
never outscores its subfolders by containing them. The candidate with the most matches is chosen
automatically; the folder the user selected wins a tie that includes it, and any other tie, or no match at
all, is shown to the user. Files outside the root are out of
selection and never uploaded.
4.3 Import diagnostics (and rollback)¶
Parsers emit structured warnings (code, field, cause, record number). The import job stores an
aggregate: one group per field and cause with the number of affected records, up to five example record
numbers, and the total warning count, kept separate from any capped sample. The search page shows the
groups, expandable to the examples. It distinguishes citations imported, optional metadata omitted,
attachment references missing or refused, unmatched files skipped, and PDFs failing availability. The
original upload is retained and labelled "your original file, kept as uploaded"; omitted fields in it are
not searchable as study fields. Transfers say "uploaded", never "copied".
Rollback: the parser and diagnostics slices ship unflagged (D2), so their rollback is reverting the slice PR. Studies already imported keep their data; a reverted EndNote pipeline makes EndNote imports go back to the legacy parser (and Zotero EndNote XML to failing). No stored data changes shape, so no migration is needed either way.
4.4 PubMed (NBIB) mapping¶
| PubMed tag | Study field | Rule |
|---|---|---|
| TI, then BTI, then TT | Title | Required; wrapped lines joined with one space; TT only with a warning; missing refuses the import with the record number and PMID |
| AB | Abstract | Wrapped lines joined with one space; structured labels kept; OAB not added |
| FAU, else AU; CN | Authors | Document order; CN is a corporate author; investigators (IR, FIR) not added |
| AD | Author address | Distinct values joined with "; " |
| DP, else DEP | Year | First four digits 1000-2999, else omitted with a warning |
| JT; TA | Publication name; alternate | Each falls back to the other |
LID or AID ending [doi] |
DOI | Suffix removed, RIS DOI normalisation; invalid omitted with a warning |
| PMID | URL | https://pubmed.ncbi.nlm.nih.gov/<PMID>/; missing PMID imports without a URL, with a warning |
| PT | Reference type | First value |
| OT, MH | Keywords | In order, MeSH * removed, duplicates dropped |
| Other official tags | none | Kept in the original and returned files; one diagnostic group per tag |
| Unknown tags | none | One diagnostic group per tag |
Refusals (whole import): no records; a first record that isn't a tag line; a line that is neither a tag line, an indented continuation nor blank (line and record number); a record without a title; UTF-32 or malformed encoding; any size bound. Strict UTF-8 (with or without BOM), UTF-16 with BOM and Windows-1252 are read as for RIS.
5. Slices¶
Each slice is one PR. Acceptance criteria (AC) are verified by the named tests in the slice PR.
| Slice | Content | Needs |
|---|---|---|
| S0 | This plan | none |
| S1 | Import diagnostics aggregate (4.3), RIS fields named in its warnings | S0 |
| S2 | Attachment references and path policy (4.1), shared vectors | S0 |
| S3 | EndNote XML on the record pipeline, any producer, field cleaning | S1, S2 |
| S4 | RIS attachment references (L1) |
S1, S2 |
| S5 | PubMed NBIB | S1 |
| S6 | Zotero RDF | S1, S2, S10 |
| S7 | Web shared core: detection, library-folder intake, root resolution, preview additions, diagnostics view; standalone upload uses it | S1, S2 |
| S8 | Wizard UI behind searchImportWizard |
S3-S7, S10, #4598 (m3e-angular 0.1.0-alpha.7 with the sidebar stepper, D7), #4571 |
| S9 | Isolated end-to-end journey | S8 |
| S10 | Streaming record-pipeline import for every format (D8) | S3, S4, S5 |
Waves: W1 = S1 ∥ S2; W2 = S3 ∥ S4 ∥ S5 ∥ S7; W3 = S10 → S6; W4 = S8 → S9.
S1 Import diagnostics¶
- AC1.1
ParseWarninggains optionalFieldandCause; old documents deserialise unchanged (BSON round trip). - AC1.2
SearchImportJob.ParseDiagnosticsholds the total, the groups and aSampleCappedflag; groups count affected records once per record, not once per occurrence. - AC1.3 RIS warnings name the omitted tags (for example
LA,DA,CY), aggregated into one group with 723 affected records for the evidence shape, not 99 unnamed messages. - AC1.4 API DTO and generated client carry the aggregate; the searches page shows groups with counts and expandable examples, and never presents a warning count as a failed-study count.
S2 Attachment references¶
- AC2.1 Classification table 4.1 holds for every row, in C# and TypeScript, from one vector file.
- AC2.2 No URL decoding anywhere;
%20stays%20. - AC2.3 Traversal, absolute and remote references are refused with a counted diagnostic.
- AC2.4 Multiplicity: first PDF chosen, others reported (D1); non-PDF attachments reported.
S3 EndNote XML¶
- AC3.1 Parity: EndNote desktop journal-article fixtures produce identical studies through the legacy parser and the new pipeline (run before the legacy parser is deleted).
- AC3.2 Any
source-app(or none) is accepted when the structure is EndNote XML; a Zotero-shaped synthetic export imports every record. - AC3.3 Field cleaning:
<style>runs and stray inline markup (i,b,u,em,strong,sup,sub,sc,span,a,br,p,div,font,jats:*,mml:*) reduce to plain text with entities decoded once; whitespace collapsed in single-line fields; the abstract keeps paragraph breaks. Fallbacks: titletitle→alt-title→short-title→translated-title; authorsauthors→secondary-authors(with a note); publication nameperiodical/full-title→alt-periodical/full-title→titles/secondary-title→periodical/abbr-1(D5); yeardates/year→dates/pub-dates/date; DOIelectronic-resource-num→ adoi.orgrelated or web URL; URLrelated-urls→web-urls→text-urls; keywords split on line breaks and de-duplicated. - AC3.4 URL never comes from
pdf-urls; attachment paths follow S2. - AC3.5 Every record is read: adjacent records with no whitespace between them, pretty-printed records, a DOCTYPE, and malformed XML (refused with the line number).
- AC3.6 EndNote tagged-text (
%0 …) and.enl/.enlxlibrary files are identified and refused with "export as XML" guidance.
S4 RIS attachments¶
- AC4.1
L1references follow S2; the Zotero-shaped fixture yields 77 attachment pathsfiles/<n>/…. - AC4.2 RIS exporter variants (Zotero, EndNote
internal-pdf://, Mendeley-style absolutefile://) are classified per 4.1.
S5 PubMed NBIB¶
- AC5.1 Mapping and refusals per §4.4; fixtures cover several records, blank-line variants, CRLF/LF, repeated
and value-less tags, FAU-less records, CN, wrapped TI/AB, LID/AID forms, unknown tags, Unicode in each
accepted encoding, missing PMID/DOI, invalid DP and a BTI book; refusals cover each case in §4.4 and
each limit; the returned file gains one
GN - SyRF-ID:per record and re-import replaces it. - AC5.2 The import reports that PubMed files carry no attachment references; no PDF step is offered and no matching from URLs or identifiers is promised.
S6 Zotero RDF¶
- AC6.1
LibraryFileType.ZoteroRdf = 7(appended), detected by the Zotero export namespace and RDF root. - AC6.2 Items (
bib:Article,bib:Book,bib:BookSection, otherz:itemTypes) map title, authors (bib:authors/rdf:Seqorder), journal viadcterms:isPartOf(by reference or nested), DOI from the journal node'sdc:identifier "DOI …", year fromdc:date, abstract, keywords (dc:subject), PMID as the PubMed URL. - AC6.3 Attachments follow
link:linktoz:Attachment;z:pathfollows S2; linked URLs and linked absolute files are refused with a counted diagnostic. - AC6.4 Unknown RDF vocabularies are refused as "not a Zotero RDF export".
S10 Streaming record pipeline (D8)¶
One orchestrator, RecordPipelineImport, used by every format (CSV/TSV, RIS, EndNote XML, PubMed, Zotero RDF):
- Pass 1, inspect and validate: streaming, keeps nothing; structural refusals, record count and
per-record required fields; any error saves nothing and opens no output.
- Pass 2, map, write and save: reopens the source, maps one record at a time, writes the returned file
record by record, and hands studies to the study processor in batches; one diagnostics builder.
- Study processor contract: called once per batch, in order; progress uses the pass-1 total;
cancellation stops within one batch and saved studies are deleted by the existing failure path. The
call site that changes is the save closure in ProjectManagementService (search import parse): today it
materialises its input, resets its saved count per call, uses the call's own count as the total and marks
the final update at the end of each call. It must keep a running saved count, take the total from the
parse, and mark the final update once. A pass-2 failure deletes the generated file as well as the saved
studies (today only cancellation deletes the file).
- Errors: one bounded collector (100 record errors, then "{k} more records have errors."), 1-based
record numbers, messages never echo field values; exceptions while mapping become record errors (pass 1)
or a clean import failure (pass 2).
- RIS moves onto a streaming record reader (strict UTF-8 validated in pass 1, Windows-1252 otherwise,
UTF-16 with BOM) carrying S4's L1 mapping, and the legacy RIS parser is removed after parity; its tests
move to the new reader and processor test classes. Zotero RDF streams once to index the nodes items
reference by rdf:about, keeping only what mapping needs (journal or book title, alternative, DOI; an
attachment's path, link mode and type), then streams the items. The index holds only referenced nodes:
every attachment an item links to (an item can have several, such as a PDF and a web snapshot) plus the
shared journal and book nodes. Its size therefore grows with the number of attachments and containers,
not with the rest of the file, and is checked against a total budget at the record limit.
- Out of scope: PubmedXml (legacy PubMed XML parser, not offered in the upload dialog) and
LivingSearchJson (living searches, a separate pipeline) keep their current parsers; retiring or
migrating them is a separate decision.
- Limits: EndNote XML and RDF gain explicit limits (256 MiB, 100,000 records); RIS follows D9 (256 MiB,
100,000 records); PubMed keeps 64 MiB / 50,000.
- AC10.1 No parser in scope (CSV/TSV, RIS, EndNote XML, PubMed, Zotero RDF) materialises all records or all studies; each runs through the orchestrator.
- AC10.2 A file with a record error saves nothing and opens no output, for every format.
- AC10.3 Parity suites (CSV, EndNote, RIS including
L1attachments) green against the pipeline; legacy RIS removed after. - AC10.4 Batched hand-off through the changed
ProjectManagementServicesave closure: progress and saved counts correct across batches, one final update; cancellation between batches stops within one batch; a pass-2 failure deletes the saved studies and the generated file. - AC10.5 No unhandled exception escapes an import; mapping exceptions are record errors or a clean failure.
- AC10.6 A bounded-memory test per format streams a generated file at that format's record limit (RIS: as many records as its byte limit allows) through a counting stream; for Zotero RDF it also asserts the index holds only referenced nodes (entry count equals the referenced attachments plus containers) and that its total memory stays within a budget measured on a generated file at the record limit with several attachments per item: S10 measures it once, records the figure in the test, and fails on any regression beyond that baseline plus 25%.
- AC10.7
docs/features/ris-import.mddescribes the passes, limits and error model.
S7 Web shared core¶
- AC7.1 Format detection from content: RIS, EndNote XML, Zotero RDF, PubMed NBIB, CSV/TSV; PubMed XML, EndNote tagged text and library binaries explained.
- AC7.2 Library-folder intake: candidate reference files found; one candidate auto-selected; several ask; none explains. Root resolved per 4.2, including bare file names that never vote, references matching under several prefixes, ties, and the bare-names-only fallback; out-of-root files excluded.
- AC7.3 Match preview shows shared files, additional and non-PDF attachments, and unmatched files as skipped before upload; only matched valid PDFs are uploaded (as #4569).
- AC7.4 The standalone bulk upload uses the same core: it already shares folder validation, match preview, path normalisation and attachment classification with the wizard. Library-folder intake in the standalone dialog is not part of this programme; the wizard (S8) owns it.
S8 Wizard¶
- AC8.1 The prototype's flows on m3e components, behind
searchImportWizard; the legacy dialog unchanged with the flag off. - AC8.2 Import recoverable when PDFs fail; retry PDFs only; uploaded, attached and ready-to-view counts distinct; "ready" only after #4571's availability check.
- AC8.3 Accessible progress, errors, recovery and optional steps; desktop, phone and keyboard checked.
S9 End-to-end¶
- AC9.1 Isolated stack (
e2e/run-local.sh): import a synthetic library folder (reference file plus PDF folder), confirm the match, upload, see attached and verified; then a file-only import and a later standalone PDF upload. Never against staging.
6. Requirement-to-test checklist¶
| Requirement (handoff) | Slice | Planned test |
|---|---|---|
| File-only import | S3-S6, S8 | per-format fixture without attachments; E2E file-only |
| Optional PDF step when references exist | S7, S8 | capability detection specs; wizard step spec |
| Missing files explained, citation-only completion | S7, S8 | preview spec; wizard skip spec |
| Library folder, relative paths, multiple candidates, picker fallback | S7, S8 | intake specs (one, several, none); root-vote specs (votes, multi-prefix references, ties, bare-names-only fallback with its tie and no-match cases); picker spec |
| Standalone upload shares logic | S7 | standalone dialog uses shared core (spec) |
| RIS variants incl. Zotero, attachments preserved | S4, S10 | S4's variant fixtures, now RisParserParityTestBase (run against the legacy parser and the pipeline, then the pipeline only), RisParityCorpus golden renderings, RisRecordReaderTests (S10) |
| EndNote XML any producer, structure detection | S3 | processor and orchestrator tests, Zotero-shaped fixture |
| EndNote tagged text identified | S3, S7 | refusal tests (server and browser) |
| Zotero RDF vocabulary and attachments | S6 | RDF fixture with by-reference and nested journals, attachments, linked URL |
| PubMed citation-only, limitation stated | S5 | approved fixture set; no-attachment capability test |
| Capability from content | S2-S7 | fixture with format but no references |
| Separators, Unicode, spaces, %, URL encoding, case | S2 | shared vectors |
| Traversal, out-of-selection, no remote fetch, no absolute paths | S2, S7 | vectors; root resolution specs |
| Missing, ambiguous, duplicate basenames, multiple attachments, non-PDF, shared files | S2, S7 | vectors; preview specs |
| Only matched valid PDFs uploaded | S7 | existing #4569 specs extended |
| Uploaded, attached, ready distinct; availability before ready; safe retry | S8 (with #4571) | wizard result specs; E2E |
| Field-level diagnostics, aggregated, total vs sample | S1 | aggregation tests; DTO mapping; search page spec |
| Distinguish import, omissions, missing, skipped, availability | S1, S8 | result panel specs |
| Original upload retained and explained | S1, S8 | copy spec |
| "uploaded" not "copied" | S8 | copy spec |
| Material 3 Expressive, accessibility | S8 | visual check desktop/phone/keyboard |
| Isolated end-to-end journey | S9 | E2E spec |
| Very large libraries: streaming, bounded memory (D8) | S10 | RecordPipelineBoundedMemoryTests: RIS 256 MiB / 100,000, EndNote XML 100,000, CSV 50,000 generated records; RecordPipelineImportTests streaming guard |
| Well-structured error reporting and handling (D8) | S10 | ImportErrorCollectorTests; RecordPipelineImportTests (mapping exceptions, refusals, pass-2 failures, cancellation); pass-1 refusal tests in each format's suite |
7. Unsupported variants and known gaps¶
- Several PDFs per study (D1): the first is attached, the rest reported.
- Local absolute and remote attachment references are refused, not fetched or read.
- EndNote tagged text, EndNote library binaries (
.enl,.enlx), PubMed XML and arbitrary RDF are refused with guidance. - EndNote XML exporters other than EndNote and Zotero import, but only those two are tested with real exports.
- Size limits: EndNote XML, RIS (D9) and Zotero RDF 256 MiB / 100,000 records (S10); PubMed 64 MiB / 50,000 records.