above it, then scrolls its own pane. syncIdentificationChoices(), line 11858. Pushes the current
Born Twice: Specimen and Record at the Moment of Collection
Born Twice: Specimen and Record at the Moment of Collection Premise. Record it in the field, where the knowledge is. Like a tattoo, the record should go everywhere you do and outlast whatever held it first. Methods and Results. The Independent File (IF) is a single HTML document that starts the voucher record at collection. A UUID and QR code tie specimen, photographs, collector, locality, and date together before the plant goes in the press. Fields, validation, Darwin Core mappings, and export rules live in one place: semantics by construction. IF runs in a browser with no installation, account, server, or network. The demonstration produced a documented voucher in under five minutes. IF prints labels; exports HTML, JSON, CSV, and XLSX; writes Symbiota, Specify, and DiSSCo packages; and pushes to GitHub. A take-home bundle reads on its own and imports back into IF. Downstream systems can receive the record without becoming conditions for making or keeping it. Nothing leaves the device unless the collector sends it. Conclusions. A voucher record does not need a permanent digital home to keep its identity. Open formats, inspectable source, and no single host point toward decades of use, though no service life has been measured. The aim is a field book that follows a collecting career and still makes sense to the next person who opens it. The specimen goes to a collection; the record can stay with the collector. 1.Introduction The broken chain Herbarium specimens underpin systematics, floristics, conservation biology, and climate research only as far as the attached record can be trusted (Liu et al., 2022; Chen et al., 2022). Trust starts with provenance: who collected it, where, when, and under what name. Strip those and a mounted specimen proves that a plant once existed somewhere. That is not nothing, but it is close. Yet voucher information is still assembled piecemeal as the specimen moves through pressing, drying, freezing, imaging, identification, and data entry. That chain takes months or years to close (Bebber et al., 2010; pers. obs.). Each handoff is a chance to omit, mistype, or diverge. We spent decades improving the databases at the end of the chain and almost nothing on the collecting event, the one moment when everything is known. The system starts the record at that moment and keeps identifiers, fields, Darwin Core mappings (Wieczorek et al., 2012), images, validation, and export rules in one file. The design principle is semantics by construction: the record's structure and behavior carry its meaning, so nobody has to rebuild it later across several systems. A phone can already photograph a specimen, record where it stood, store structured data, and move the record along. The herbarium then receives a record instead of rebuilding one. The remaining job is accessioning, not data rescue. The record begins too late Digitization has never lacked ambition. Large programs have spent decades databasing, imaging, georeferencing, and mobilizing collections (Bebber et al., 2010; Barkworth and Murrell, 2012; Brilhante et al., 2026). The Muséum national d'Histoire naturelle has imaged all 5.4 million of its vascular-plant sheets, yet holds field-collection information for only 16% (Le Bras et al., 2017). About 2.5% of the 5.3 million records it serves through GBIF carry coordinates (MNHN and Chagnoux, 2025). Smaller herbaria run short of money, labor, training, and hardware (Harris and Marsico, 2017; Powell et al., 2021). Effort is not the problem. Timing is: where, when, and by whom a specimen first becomes a computational record. Recent tools attack the backlog with real ingenuity. VARP recovers barcodes from specimen images (Powell and Shaw, 2021). pyzbar decodes barcodes and QR in Python (Hudson, 2022). mvh builds virtual herbaria from open images in R (Vasconcelos and Boyko, 2025). Hespi reads sheet labels with OCR, HTR, and multimodal models (Turnbull et al., 2025). iNaturalist tools print labels from observations (Rockefeller, 2026). Unvouchered observations now outnumber specimens and misrepresent global patterns (Daru and Rodriguez, 2023). Each solves a real problem. Nearly all begin after collection, a handoff or two too late. The gap follows specimens into the molecular repositories. Of 1.36 million flowering-plant records in GenBank, 52.9% carried a specimen-voucher identifier (Nakazato and Jinbo, 2022). In Dendrobium, 31.6% of records gave a country of origin and 4.8% named a collector or assessor (Wu et al., 2021). An author audit of COI records across six animal genera found the same holes: voucher identity, collection date, collector, coordinates, and identifier (Table 4; procedure in Appendix D; see also Carné et al., 2025). A sequence is digital from birth. Its provenance is not. None of this argues for rebuilding historical collections. Existing barcodes can stay. Sheets need not be relabeled, and institutional databases need not be replaced (Heil et al., 2026). That horse has left the barn. The useful question is what happens to the next specimen collected. The answer here is to assign Darwin Core terms when the record is made. Exports then present that record in whatever form a destination needs, photographs embedded or linked. The QR code can point to the fuller record or carry a minimal Darwin Core record itself. Either way, identity, taxon, place, datum, and date outlast any server or link. In that narrow sense the record resembles a digital paratype (Jones, 2025), kin to the sequence-based types proposed by Thiele et al. (2023), minus the four-letter code and its IUPAC embellishments. Here it is plain JSON. Symbiota (Gilbert et al., 2026; Gries et al., 2014), Specify (Specify Collections Consortium, 2026), GBIF, and many other systems remain the destinations. The aim is to feed them from the field and plug the holes in the data dike before information leaks out. 2.Materials & Methods 2.1The Zero-Dependency Primitive The result is an Independent File (IF): a document that computes. One HTML file of about 750 KB holds the interface, code, styles, and record rules. No external libraries, framework, packages, build step, or required service. TiddlyWiki has shipped as a single HTML file since 2004 (Ruston, 2026). The median webpage in 2025 weighed 2.9 MB (Barret et al., 2026). The reference deployment ships inside the Carex key (Section 9), though the voucher needs no taxon data. Appendix A lists every job the file does. 2-1aTwo Workflows: Preprint Vs Cowboy Methods Pre-printing prevents poor performance. Bring labels when you can. Print as many QR labels as you expect to need with IF's print function. Keep them in the press, a folder, wherever is handy. Unused labels stay usable. Discard a damaged, soaked, or torn one; you lose only paper and ink. Hit Scan and frame that label's QR code with your phone or tablet. IF adopts the identifier. Fill out your fields. Put the specimen and its preprinted label together in the press. No labels? Go Cowboy. Possible, but not recommended. Use the QR code IF generates. After imaging, a pop-up gives the occurrence identifier's last four characters. Write them down with the specimen. Back at the shop, QR tags · Reconcile Cowboy prints labels for those records. Each label shows the last four characters and the binomial. Use them to rebuild the day and reunite the digital orphans with their specimens. This study treats the QR record as a field-to-voucher workflow. Testing turned up other uses (specimen reminders, prepublication sharing, provisional identification, trip planning, record recovery), none evaluated here. The system is left open for workflows this paper did not think of. IF nags once, and about the record, not the user: after three days of work living only in the browser, or ten unbundled vouchers, it asks to be copied out. One refusal and it shuts up. 2.2Quick Start How to use the field voucher Open the IF file. To work offline, save a local copy first. It runs in the browser and stores records locally. Your device may ask permission for the camera and GPS. Annoying, and necessary for those features. The file generates a unique QR code and voucher identifier when it opens. Photograph the specimen in situ, with habit and habitat when useful. Existing images and photographed field notes can be added from other folders. Select Use Current Location (GPS). Coordinates can be corrected later. Add the useful photographs; the collector decides what earns its keep. Email attachments usually cap around 25 MB, so one canonical image may do for a day trip; plan storage for a long one. A no-photo option exists. Select Save Voucher. The record is stored in this browser. Email or export it when you are ready to move it. Cost $40 monthly in subscriptions over 90 days, no API billing or coupons used. IF: build at a glance Build 1008 · figures checked 29 September 2026 (freeze) Appendix G: collection-system field crosswalk IF is built to be taken apart. Every named function carries a comment stating what it does, and each is listed in the function manifest. A reader can trace any behavior to its source without running the file. Comments make up ~30% of the JavaScript by character count. The interactive wiring diagram (Figure 1) follows build 1008 from printed identifiers and field entry through validation, local storage, and export. It is an explanatory map, not a live execution trace. For the trace, and for something to fly, go to Appendix F. The Happy Path atlas plays recorded calls through the application. The small-world flight deck lets you fly the architecture. IF: interactive wiring diagram Select a node to inspect its functions and connections, or choose Light the fuse to follow a voucher. Search for a function, drag to pan, and use Fit to see the whole map. Expand opens a larger view; Save SVG exports the figure. Figure 1. Architecture map for build 1008. Self-test coverage and disclosed limits are available inside the diagram. 2.3Development and Testing Environment Development proceeded by iterative triage across several tools, operating systems, browsers, and devices (Table 2). The workflow was tool-assisted but tied to no single environment. 2.4Repository Audit Procedure Each audit froze its sampling frame, then audited and scored the frozen records from preserved local evidence. Procedures are in Appendices B.4 and D; checks that they match the reported results are still outstanding. The author ran the NCBI audits interactively with ChatGPT, inspecting outputs step by step, and that work set the procedure for SERNEC. For SERNEC, the author set the design and constraints, ChatGPT turned them into a frozen manifest and agent instructions, and Claude Cowork ran the retrieval (Appendix B.4). 2.5Options 2.6Export and Archiving Export does two jobs: it hands a record on, and it gives the collector a copy to bring home. Formats differ in what they carry and how they handle photographs (Section 5.4). The take-home HTML record opens on its own and loads back into IF. Browser storage is no place to leave anything; keep exports and photographs elsewhere, plus a second copy on another device or service. A receiving institution may apply its own mapping and accession checks. The collector keeps the source record either way. 2.7Failure Modes Addressed The failure addressed is a specimen separated from its documentation anywhere between collection and databasing. It happens the ordinary ways: notes never transferred; notes lost, wet, or illegible; the label written weeks later when the locality has gone soft; sheet and field report parted; the collector dead. Happens. The shared identifier reconnects a separated sheet and record through the label, standalone HTML voucher, or bulk export table. If that sounds like a storage burden, all of GBIF's specimen records, imagery excluded, fit on a jump drive in this format. Failing everything, copy the UUID by hand with a pencil. 3.Results The results address two questions: can the file produce a field voucher in minutes, and how often do existing repository records keep the provenance fields a voucher is meant to preserve? 3.1Field Demonstration The demonstration shows a working capture-and-save workflow, not downstream adoption. Completion time was not measured under controlled conditions. To watch one save move through the code, play the Save chapter of the Happy Path atlas (Appendix F). The author has run the file end to end more than 200 times, about half on real plants and half on synthetic records stuffed with placeholder text and autocomplete to push it toward failure. Those are stress tests, not a benchmark. With practice a record takes the author about 90 seconds, an anecdote, not a result. Outside collectors invited to test the precursor build, tmjv8 (Jones, 2026a), sent no data back. Completion time across collectors, devices, and conditions remains untested. Florilegium Cariciscarex_na29 Single-file key to 466 North American Carex taxa. The reference deployment: IF runs as its voucher tab and carries the key's filter path (Jones, 2026b; DOI: 10.5281/zenodo.19138828).tjid3.org/CAREXAI/carex_na29 Mulemule-kicks6 Carex Atelier Mule, a guided field workbench. Built in reverse: interface first, matrix added afterward (Jones, in prep.).tjid3.org/mule/mule-kicks6 tmjv8field voucher v8 Standalone precursor build of the voucher, the version sent to outside testers (Jones, 2026a).tjid3.org/voucher/tmjv8 Repository provenance audits The audits ask how often repository records carry the provenance fields a voucher should guarantee. Table 3A: the SERNEC random survey, 21,110 herbarium records (SERNEC Data Portal, 2026). Table 3B: the plant barcode stress test, identified by the author as NCBI matK and rbcL records. Table 4: 600 GenBank COI records across six animal genera. All count field presence, not accuracy. 3.2Herbarium and plant barcode records The total row gives pooled percentages across the displayed records. Appendix E: matK and rbcL audit BioPython. 3.3Animal COI records The tables show uneven retention of collection date, collector, coordinates, and identification. A record can keep one part of its provenance and lose another. The audits establish the documentation problem in the examined records; they do not test whether populated fields are true, or measure any improvement from IF. They identify what is worth securing at collection, before later workflows have to recover it. The Protein Data Bank has a related gap. Its PDBx/mmCIF dictionary separates natural-tissue from genetically manipulated sources, and gene-source organism from expression host (wwPDB, n.d.-a, n.d.-b). It also offers a free-text field for natural-source details (wwPDB, n.d.-c), so there is room for a fuller account. But a taxon name, tissue, or supplier does not establish which organism supplied the material, where and when it was collected, or whether a specimen survives to be re-examined. We did not audit how often PDB records make those links. The point is narrower: precision about a structure is not provenance for its source. 4.The Inertia Straitjacket The backlog is not rhetorical. As of 31 December 2025, Index Herbariorum listed 4,035 active herbaria holding a reported 406,426,591 specimens (Thiers, 2026). Every one needs a name, a place, a date, a collector, and a durable route into a data system. A collection that size will not be rescued by meetings or the next platform. It needs small tools that cut friction at the moment the record is born. Pressed plants are the obvious beachhead: flat, large, label-driven, tied to locality, already a sheet plus a text record. Not the only target, just the cleanest first one. Fish, birds, and arthropods share the core problem: a physical object must stay bound to a record through time. Human-readable ID. The survival layer. Read, copied, photographed, typed, or recovered after the machines are gone. QR code. The field and phone layer. Binds plant, photograph, bag, label, and record before the institutional workflow begins. One-dimensional barcode. The legacy accession layer, scanned at the workstation once the specimen enters a collection. RFID. The cabinet-scale audit layer for types and high-value specimens: thousands read without line-of-sight handling. The layered view prevents identifier warfare. The identifier is the specimen; the machine-readable forms are doors into the same record. None needs to kill the others. Table 5 compares the carriers. The paper label is the only one with a demonstrated multi-century record, and nothing here replaces it. Code 39 adds machine reading at the cost of a dedicated scanner. QR adds it on hardware the collector already carries (Statista, 2026), and resolves without a network when the data travel in the code (up to about 4,000 characters). NFC is convenient but mortal. Visual search is powerful but infrastructural. GS1 standardizes meaning without guaranteeing permanence. Notes No symbology has a lifespan rating; longevity belongs to substrate, ink, and storage. Fused toner on archival stock persists; dye inkjet fades. Deep-freezing at accession, routine for pests, stresses adhesives and encapsulation; chip-retention figures do not cover it. Capacity maxima are quoted at error-correction level L, the weakest. Raising error correction improves damage recovery and lowers capacity; the two maxima cannot be had at once. Program costs are excluded from the per-object comparison: for NFC, encoding and tag verification; for visual search, cameras, image storage, annotation, model development, inference hardware, validation, and maintenance; for GS1, licensing, identifier governance, resolver hosting, database integration, and scanner upgrades. GS1 Digital Link is an identifier syntax carried by a QR Code or DataMatrix, not a physical carrier. It is included because institutions may evaluate it, not because it is a peer of the symbologies above. This is why the system is deliberately primitive. The advanced solution is the institutional stack: server, database, grant, account, scanner, support queue, policy meeting, migration plan. Here the collector finishes the documentation while specimen and context are still at hand, and the institutional workflow can arrive later without making the field event wait. The competition is paper, and paper lasts hundreds of years. 5.System Description 5.1Technical Architecture The file contains HTML, CSS, and vanilla JavaScript, roughly 12K lines in the version documented in Appendix B. All fx's are labeled for searchability. QR generation uses Kazuhiko Arase's QR Code Generator for JavaScript, MIT licensed and embedded (Arase, 2009). A pure-JavaScript parser reads GPS coordinates straight from image bytes (Section 5.5). Records, addenda, and interface state live in LocalStorage; photographs in IndexedDB (Appendix B.1). Both survive restarts within the browser's storage limits, unless the browser clears them (Section 8). To see what a refused storage write does downstream, run Failure in the small-world flight deck (Appendix F). The file follows the 20-Year File / Digital Paratype architecture (Jones, 2025): installing it means copying it. Twenty years is a design goal, not a measured lifespan. Exports follow Lots of Copies Keep Stuff Safe (Reich and Rosenthal, 2001): more copies in more places. Copying alone does not check or repair copies the way a managed preservation system does. 5.2The Voucher Record Each saved voucher record in IF is built from the form fields in Table 6. CSV and XLSX exports use the same saved keys through FIELD_DEFS; JSON carries the same record plus a photo manifest when photographs are present. 5.3QR Code Implementation Each record gets a unique identifier when the form opens, before save, shown as a QR code and carried through every export. HTML voucher exports embed the QR with a generous white margin; the bulk HTML table puts one in the first column of each row; printed labels place it flush right with a 0.18-inch border. That border is the quiet zone scanners need; without it labels scan poorly in low light and on low-resolution phone cameras. 5.4Export Formats DwC JSON A complete Simple Darwin Core occurrence export, fields mapped per Table 6, importable to most herbarium databases. Take Home A portable, independently readable record that can be imported back into IF. HTML Table All vouchers in one landscape HTML file, a QR image per row. Opens in any browser; drags into most spreadsheets. CSV / XLSX One row per voucher, all Table 6 fields, as plain CSV or a formatted XLSX workbook, for institutional import or bulk editing before re-import. Label Print Labels open in a browser print window; a blocked window is reported. No separate application needed. Two across at 4 × 3 inches on legal paper, for adhesive stock cut to size or card stock cut by hand. Matches no pre-perforated Avery template; test-print on plain paper first. Optional connections IF can work alone, sit inside an identification key, or feed a receiving workflow through an adapter. The same record serves all three. Git and GitHub give copies a place to land and keep version history (GitHub, Inc., 2026). Specify and Symbiota are the institutional collection workflows (Specify Collections Consortium, 2026; Gries et al., 2014). All of these connections are optional. DiSSCo is one more destination. It ingests registered institutional source systems, fed by a Darwin Core Archive or a BioCASe endpoint, and turns their data into openDS Digital Specimens (DiSSCo, n.d.; DiSSCo, 2026a). IF writes the first, and writes openDS directly. I checked that export against the openDS 0.4.0 JSON Schema: everything IF writes passed. Seven required keys were missing, all assigned by DiSSCo at ingest, such as the specimen DOI and source-system ID. A Meise Botanic Garden record from the DiSSCo Sandbox passed the same check cleanly (DiSSCo, 2026b). The shapes match. Acceptance is a separate step; no IF record has been through DiSSCo ingestion yet. 5.5EXIF Extraction When a photograph is attached, IF parses its EXIF in the browser for GPS coordinates and reported error. Empty coordinate fields are filled, with EXIF as source; existing coordinates are kept, and the distance between the two is reported. A separate writer embeds the voucher ID in the resized JPEG IF stores, beside its visible ID stamp (Appendix A); the source photograph is untouched. Capture date is not extracted; eventDate is typed or defaults to the day of save. A photo with no GPS tag (older camera, scan, screenshot) falls back to device geolocation or manual entry, and the coordinate source field records which path was used (limits in Section 8). One caveat: hand-entered GPS is GPS, and phone-assigned GPS is GPS plus, usually, Wi-Fi positioning, maybe A-GPS, cell-tower triangulation, and other stuff as well. For the sake of sanity it is not examined here. 6.Unresolved Records as Positive Data The architecture buys resolution, not information. The same system yields a second data type that usually becomes a ghost: the observation made, documented, and given up on. Databases treat a missing determination as silence, and silence reads as absence. That error compounds across decades of range modeling and conservation assessment. A collector who meets twenty Carex of section Ovales on a Rocky Mountain summit at 3000 m, measures perigynia, notes scale color and beak length, and writes "unresolved; complex difficult here; characters noted" has produced a difficulty signal: place, date, character states, and a flag that the taxonomy hit morphological compression right here. A useful fail. Aggregated across collectors and years, such records show where a complex's hard boundaries run, which records range modelers should handle with kid gloves, and where the next specialist should look. A database that demands a name gets one, and a forced determination that goes quietly wrong corrupts everything downstream until someone catches it. Expert bypass is the limit case: when Tony Reznicek says Carex frankii in Ohio, the assertion is the chain. The system takes that as readily as the first-year student at Cedar Bog who loses the determination on a perigynium and says so. Neither is nothing. My two cents. Messy biological reality → conceptual model → RDF ontology → mappings from legacy systems → mappings between ontology versions → application interpretation. That is not a service to anyone. It formalizes an unstable ontology without stabilizing the knowledge under it; it mummifies today's reading of that knowledge. And the mummy always comes back in the movies. 7.The Fifty Cabinets The backlog includes collectors now infirm or dead, their localities incomplete or gone. The fifty-cabinet figure is informal: herbaria of every size, nearly three decades of field, curatorial, and computational work in Cyperaceae. These specimens are not recoverable. The collectors are gone. The mental index, the memory of which road, which bog, which August, died with them. They survive in drawers as pressed plant material with no coordinates in space or time. No tool recovers a broken chain, and this paper does not claim to. What the system does is prevent the next fifty cabinets. The chain breaks at a predictable moment, the end of a hard field day, when documentation gets deferred one more time. Five minutes on a phone the collector already carries removes the friction behind the deferral. The author's own cabinets, several hundred specimens, are the same failure, recoverable only because the author is still alive and the mental index persists, barely. This system exists in part because those cabinets do. 8.Advantages and Limitations Keeping the field record on the device assigns custody and backup to the collector. The costs are listed here rather than left in the source. Custody. The collector holds the only copy until export. Clearing browser data, or losing the device, loses unexported vouchers. No server-side redundancy, by design; backup is the user's job. EXIF coverage. HEIC originals from some iOS capture flows are not parsed for GPS, and capture date is not extracted; eventDate is entered by hand or defaults to the day of save. Storage persistence is requested through navigator.storage.persist() where available; this is a request, not a guarantee. The separate Safari advisory is described in Appendix B.3 and remains unverified on Apple hardware. Georeferencing. Precision depends on the device, the canopy, and the conditions at collection. The recorded uncertainty is whatever the device reports. Label stock. No pre-perforated template matches the 4 × 3 inch layout (Section 5.4). Test-print first. Browser dependency. The file uses ES2025 and IndexedDB. Browsers predating those features will not run it. Designing for decades I cannot give IF an expiry date, and I don't try. The argument is about recovery. The Library of Congress weighs disclosure, adoption, transparency, self-documentation, and external dependencies in judging whether a format lasts (Library of Congress, n.d.). IF meets each plainly: open source, common formats, documented fields, no remote service. That is my reading, not a preservation certification. The W3C Technical Architecture Group puts compatibility with existing content first when web features change (W3C TAG, 2026), which favors IF. Three things have to survive: the bytes, the meaning, and the software. Backups and integrity checks cover the bytes. Identifiers, field definitions, and exports carry the meaning. Browser compatibility decides the software. If the interface breaks someday, readable exports and the source still leave a way back. Keeping the tool running is a harder bar than keeping the evidence readable. The defensible claim is a tool designed for decades of use, with no service life proven yet. 9.Origins and Scope The voucher began with the matrix of the Carex Interactive Identification Key of North America (CIIK), built in DELTA, moved to Lucid, and developed over twenty years. That matrix went into Florilegium Caricis (Jones, 2026b; DOI: 10.5281/zenodo.19138828), a single-file key to 466 North American Carex taxa built on Flora of North America Volume 23. There the voucher has its own tab and talks to the key: a record can carry the current filter path, so the character data behind an identification travel with the locality. Florilegium Caricis tested function. A second work, Mule (Jones, in prep.), went in reverse: UI first, matrix parachuted in afterward. The voucher is described separately because it works alone, and its scope runs past Carex and past keys. I built IF as one field botanist with a lot of help from AI systems, through round after round of iterative triage (Section 2.3). What it gets wrong is mine to answer for. A whole career is the hope, kept together well enough to hand on. A generational scrapbook with identifiers: places revisited, names corrected, observations somebody else can still use. If it lets someone leave a fuller account than I can piece together from my own cabinets, it has done its bit. 10.Conclusion Semantics by construction starts the record in the field, while plant, collector, place, and circumstances are still in one spot. Required fields, identifiers, corrections, and export mappings are built in, so whatever handles the record next has something to work from. The record can leave the field, enter another system, and come back to the collector. No single host has to last forever. IF is a file with no home, so it lives everywhere. A copy on a phone, laptop, thumb drive, GitHub repository, or email attachment is the whole system, not a pointer to one. Nothing has to stay running for it to open. A page from the web's first year still opens in any browser (Noyes, 2013; W3C, 1992), about as good a longevity guarantee as software gets. The whole program is under a megabyte, smaller than one photograph from the phone that carries it. No computational liturgy required. A herbarium keeps the specimens. A portable field record can keep the years spent finding them. The fifty cabinets are a sunk cost of past practice. The next fifty are preventable. Data and Code Availability The complete HTML file is available at tjid3.org/if/if.html. The voucher component is also preserved within the archived Florilegium Caricis release at Zenodo (DOI 10.5281/zenodo.19138828). Summary voucher-chain audit results are in Table 3A, Table 3B, and Table 4; the SERNEC retrieval doctrine and sampling script/.md are in Appendix B.4; the reconstructed COI audit script is in Appendix D; the field demonstration is in Section 3.1. Author Contributions T.M.J. conceived the system and directed its development throughout. He built and reviewed the software with AI assistance, ran the demonstration, interpreted the results, and wrote and revised the manuscript. AI systems helped with code generation, debugging, review, and prose. The author checked and accepted every design decision, every line that shipped, and every claim in this paper, and takes full responsibility for them (Section 2.3). Funding No external funding. The work was carried out independently under TJID3 Research. Conflict of Interest The author wrote the software described here and maintains its deployment. No commercial interest, license, subscription, or revenue is attached to the tool, which is released CC BY 4.0. References Anthropic. (2026). Claude Sonnet 5 [Large language model]. https://www.anthropic.com/claude Anthropic. (2026). Claude Cowork [Agentic software environment]. https://claude.com/product/cowork Anthropic. (2026). Claude Code, version 2.1.283 [Agentic coding tool]. https://www.anthropic.com/claude-code (accessed 26 September 2026). Anthropic. (2026). Claude Opus 5 [Large language model]. https://www.anthropic.com/claude/opus Anthropic. (2026). Claude Fable 5.1 [Large language model]. https://www.anthropic.com/claude/fable OpenAI. (2026). ChatGPT, GPT-5.6 Sol [Large language model]. https://chatgpt.com/ OpenAI. (2026). ChatGPT, GPT-6 Astra [Large language model]. https://chatgpt.com/ DeepSeek. (2026). DeepSeek-V4-Flash / Dan [Large language model]. https://www.deepseek.com/ Arase, K. (2009). QR Code Generator for JavaScript [Computer software]. MIT License. GitHub. https://github.com/kazuhikoarase/qrcode-generator Biopython Contributors. (2026). Biopython, version 1.88 [Computer software]. https://biopython.org/ Blender Online Community. (2026). Blender: A 3D modelling and rendering package [Computer software]. Blender Foundation, Amsterdam. https://www.blender.org/ GitHub, Inc. (2026). GitHub [Software development and version control platform]. https://github.com/ (accessed 26 September 2026). Graphviz Authors. (2026). Graphviz: Open source graph visualization software [Computer software]. https://graphviz.org/ Microsoft Corporation. Microsoft Excel [Computer software]. https://www.microsoft.com/microsoft-365/excel Microsoft Corporation. Microsoft Word [Computer software]. https://www.microsoft.com/microsoft-365/word Microsoft Corporation. PowerShell [Computer software]. https://learn.microsoft.com/powershell/ Microsoft Corporation. Visual Studio Code [Computer software]. https://code.visualstudio.com/ Python Software Foundation. (2026). Python, version 3.14.3 [Computer software]. https://www.python.org/downloads/release/python-3143/ Specify Collections Consortium. (2026). Specify 7, including WorkBench [Computer software]. https://www.specifysoftware.org/ (accessed 26 September 2026). Sublime HQ Pty Ltd. Sublime Text 4 [Computer software]. https://www.sublimetext.com/ Barkworth, M. E., and Murrell, Z. E. (2012). The US Virtual Herbarium: Working with individual herbaria to build a national resource. ZooKeys, 209, 55–73. https://doi.org/10.3897/zookeys.209.3205 Barret, R., Indigo, J., Smart, D., and Cano, M. (2026). Page Weight. In The 2025 Web Almanac, Chapter 14. HTTP Archive. https://doi.org/10.5281/zenodo.18246723 Bebber, D. P., Carine, M. A., Wood, J. R. I., Wortley, A. H., Harris, D. J., Prance, G. T., Davidse, G., Paige, J., Pennington, T. D., Robson, N. K. B., and Scotland, R. W. (2010). Herbaria are a major frontier for species discovery. Proceedings of the National Academy of Sciences, 107(51), 22169–22171. https://doi.org/10.1073/pnas.1011841108 Brilhante, M., Darbyshire, I., Duarte, M. C., Moldão, M., Bandeira, S., and Romeiras, M. M. (2026). Mapping Biodiversity Through Time and Space: Patterns and Drivers of Fabaceae Collection in Mozambique. Ecology and Evolution, 16(2), e72854. https://doi.org/10.1002/ece3.72854 Carné, A., Vieites, D. R., and van den Burg, M. P. (2025). In Vouchers We (Hope to) Trust: Unveiling Hidden Errors in GenBank's Tetrapod Taxonomic Foundations. Molecular Ecology, 34(13), e17812. https://doi.org/10.1111/mec.17812 Chen, T., Liu, H., Han, G., Bao, B., Li, M., Xu, Z., and Qin, H. (2022). Curating and administrating the Plant Specimen Sub-Platform and CVH: activities, experience and lessons learned. Guihaia, 42(S1), 18–28. https://doi.org/10.11931/guihaia.gxzw202209068 Daru, B. H., and Rodriguez, J. (2023). Mass production of unvouchered records fails to represent global biodiversity patterns. Nature Ecology & Evolution, 7(6), 816–831. https://doi.org/10.1038/s41559-023-02047-3 DiSSCo. (n.d.). Services: DiSSCo Orchestration Service and Open DS Specification. https://www.dissco.eu/services/ (accessed 26 September 2026). DiSSCo. (2026a). open Digital Specimen (openDS) specification, version 0.4.0 [Data standard, JSON Schema]. Apache-2.0. https://github.com/DiSSCo/openDS (accessed 26 September 2026). DiSSCo. (2026b). DiSSCo Sandbox environment [Web service]. https://sandbox.dissco.tech/ (accessed 26 September 2026). GBIF Germany. (2025). Digitisation of German Herbaria. https://herbarium.gbif.de/digitisation/ Gilbert, E., et al. (2026). Symbiota [Computer software]. Symbiota Support Hub. https://github.com/Symbiota/Symbiota Gries, C., Gilbert, E. E., and Franz, N. M. (2014). Symbiota: A virtual platform for creating voucher-based biodiversity information communities. Biodiversity Data Journal, 2, e1114. https://doi.org/10.3897/BDJ.2.e1114 Harris, K. M., and Marsico, T. D. (2017). Digitizing specimens in a small herbarium: A viable workflow for collections working with limited resources. Applications in Plant Sciences, 5(4), 1600125. https://doi.org/10.3732/apps.1600125 Heil, K. F., Alioto, T. S., Böhne, A., Brown, T., Chadwick, E., Chua, P., de Guttry, C., De Panis, D., Goble, C., Gut, I., Gut, M., Juty, N., McTaggart, S., Najera-Cortazar, L., Paupério, J., Rewicz, T., Shaw, F., Soiland-Reyes, S., Waterhouse, R. M., Woollard, P., and Vos, R. A. (2026). Integrating the biodiversity genomics continuum: harmonising data from barcodes to reference genomes. Research Ideas and Outcomes, 12, e187033. https://doi.org/10.3897/rio.12.e187033 Hudson, L. (2022). pyzbar: Read one-dimensional barcodes and QR codes from Python 2 and 3 (Version 0.1.9) [Computer software]. PyPI. https://pypi.org/project/pyzbar/ iDigBio. (2026). Integrated Digitized Biocollections (iDigBio) Portal [Web portal]. University of Florida. https://portal.idigbio.org/ (accessed 1 October 2026). Jones, T. M. (2025). The 20-Year File: A zero-dependency architecture for digital paratypes. TJID3 Research. Zenodo. https://doi.org/10.5281/zenodo.18054789. Current edition: https://tjid3.org/tech Jones, T. M. (2026a). Field Voucher Record, tmjv8 [Computer software]. TJID3 Research. https://tjid3.org/voucher/tmjv8. Archived within the Florilegium Caricis release, https://doi.org/10.5281/zenodo.19138828 Jones, T. M. (2026b). Florilegium Caricis, North America [Computer software]. TJID3 Research. Zenodo. https://doi.org/10.5281/zenodo.19138828. Live build: https://tjid3.org/CAREXAI/carex_na29 Jones, T. M. (in prep.). Carex Atelier Mule: Guided field workbench [Computer software]. TJID3 Research. https://tjid3.org/mule/mule-kicks6 Kartesz, J. T., and The Biota of North America Program (BONAP). (2015). North American Plant Atlas. Chapel Hill, North Carolina. http://bonap.net/napa. Maps generated from Kartesz, J. T. (2015), Floristic Synthesis of North America, Version 1.0, Biota of North America Program (BONAP). Le Bras, G., Pignal, M., Jeanson, M. L., et al. (2017). The French Muséum national d’histoire naturelle vascular plant herbarium collection dataset. Scientific Data, 4, 170016. https://doi.org/10.1038/sdata.2017.16 Library of Congress. (n.d.). Sustainability of Digital Formats: Sustainability Factors. https://www.loc.gov/preservation/digital/formats/sustain/sustain.shtml (accessed 26 September 2026). Liu, H. Y., Chen, T. X., Han, G. X., Li, Y., Li, R. G., Li, M., Xu, Z. P., and Qin, H. N. (2022). On the history and function of the Plant Specimen Sub-Platform and Chinese Virtual Herbarium (CVH). Guihaia, 42(S1), 3–17. https://doi.org/10.11931/guihaia.gxzw202209069 MNHN, and Chagnoux, S. (2025). The vascular plants collection (P) at the Herbarium of the Muséum national d'Histoire Naturelle (MNHN - Paris), version 69.422 [Occurrence dataset]. Muséum national d'Histoire naturelle. https://doi.org/10.15468/nc6rxy (accessed via GBIF.org 27 September 2026). Nakazato, T., and Jinbo, U. (2022). Cross-sectional use of Barcode of Life Data System and GenBank as DNA barcoding databases for the advancement of museomics. Frontiers in Ecology and Evolution, 10, 966605. https://doi.org/10.3389/fevo.2022.966605 Noyes, D. (2013, April 3). Restoring the first website. W3C Web History Community Group. https://www.w3.org/community/webhistory/2013/04/03/restoring-the-first-website/ Powell, C., Krakowiak, A., Fuller, R., Rylander, E., Gillespie, E. B., Krosnick, S., Ruhfel, B., Morris, A. B., and Shaw, J. (2021). Estimating herbarium specimen digitization rates: Accounting for human experience. Applications in Plant Sciences, 9(4), e11415. https://doi.org/10.1002/aps3.11415 Powell, C., and Shaw, J. (2021). Performant barcode decoding for herbarium specimen images using vector-assisted region proposals (VARP). Applications in Plant Sciences, 9(5), e11436. https://doi.org/10.1002/aps3.11436 Reich, V., and Rosenthal, D. S. H. (2001). LOCKSS: A Permanent Web Publishing and Access System. D-Lib Magazine, 7(6). https://doi.org/10.1045/june2001-reich Rockefeller, A. (2026). iNaturalist Herbarium Label Generator [Computer software]. GitHub. https://github.com/AlanRockefeller/inat.label.py Ruston, J. (2026). TiddlyWiki: A non-linear personal web notebook [Computer software, 2004 to present]. TiddlyWiki community. https://tiddlywiki.com/ SERNEC Data Portal. (2026). Southeast Regional Network of Expertise and Collections (SERNEC). Accessed September 24, 2026. https://sernecportal.org/ Statista. (2026). Smartphone sales worldwide. Worldwide smartphone sales to end users, including 2025 annual unit sales. Accessed August 9, 2026. https://www.statista.com/statistics/263437/global-smartphone-sales-to-end-users-since-2007/ Thiele, K. R., Groom, Q., Renner, S. S., Applequist, W. L., and Lehtonen, S. (2023). (329–338) Proposals to permit DNA sequences to serve as types of names in prescribed circumstances. Taxon, 72(5), 1143–1145. https://doi.org/10.1002/tax.13031 Thiers, B. M. (2026). Index Herbariorum: A Global Directory of Public Herbaria and Associated Staff. New York Botanical Garden's Virtual Herbarium. Updated continuously; accessed June 24, 2026. https://sweetgum.nybg.org/science/ih/ Turnbull, R., Fitzgerald, E., Thompson, K. M., and Birch, J. L. (2025). Hespi: A pipeline for automatically detecting information from herbarium specimen sheets. BioScience, 75(8), 637–648. https://doi.org/10.1093/biosci/biaf042 Vasconcelos, T., and Boyko, J. D. (2025). mvh: An R tool to assemble and organize virtual herbaria from openly available specimen images. Applications in Plant Sciences, 13(2), e11631. https://doi.org/10.1002/aps3.11631 W3C. (1992). World Wide Web. Preserved November 1992 website snapshot. https://www.w3.org/History/19921103-hypertext/hypertext/WWW/TheProject.html (accessed 26 September 2026). W3C Technical Architecture Group (W3C TAG). (2026, September 14). Web Platform Design Principles. Section 1.8, Prioritize compatibility when changing or removing features. W3C Group Note. https://www.w3.org/TR/2026/NOTE-design-principles-20260914/ Wieczorek, J., Bloom, D., Guralnick, R., Blum, S., Döring, M., Giovanni, R., Robertson, T., and Vieglais, D. (2012). Darwin Core: An evolving community-developed biodiversity data standard. PLOS ONE, 7(1), e29715. https://doi.org/10.1371/journal.pone.0029715 Worldwide Protein Data Bank (wwPDB). (n.d.-a). Data category entity_src_nat. PDBx/mmCIF Dictionary, Version 5. Accessed September 19, 2026. https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Categories/entity_src_nat.html Worldwide Protein Data Bank (wwPDB). (n.d.-b). Data category entity_src_gen. PDBx/mmCIF Dictionary, Version 5. Accessed September 19, 2026. https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Categories/entity_src_gen.html Worldwide Protein Data Bank (wwPDB). (n.d.-c). Data item _entity_src_nat.details. PDBx/mmCIF Dictionary, Version 5. Accessed September 19, 2026. https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_entity_src_nat.details.html Wu, H.-Y., Chan, K.-T., But, G. W.-C., and Shaw, P.-C. (2021). Assessing the reliability of medicinal Dendrobium sequences in GenBank for botanical species identification. Scientific Reports, 11, 3439. https://doi.org/10.1038/s41598-021-82385-z Appendices Every job the file performs, in plain language, for a reader who uses a phone and a spreadsheet and has never looked at code. Items are numbered so they can be cited. The file itself 1. One file, and the file is the whole program. Open it in any browser and it runs: no install, account, server, or internet once it is open. Even the QR codes are made on your device. 2. Everything stays on your device. Records live in browser storage; photographs in a larger browser store built for files. Nothing leaves unless you export or share it. Save the file and it is yours. 3. It remembers how you left it: light or dark, open sections, and field values you reuse. 4. It carries its own version number. Every record and export is stamped with the format version, so a future reader can tell which generation of the file made it. Identity: how a specimen gets a name that never changes 5. Every specimen gets a permanent identifier when a draft begins: the occurrence UUID, a long random string never reused or changed. Photos, corrections, and exports all point back to it. 6. Every specimen also gets a voucher ID, shown as a QR code. This is the number for the label and sticker. Each record gets a fresh one. 7. Preprinted QR stickers work too. Scan one with the camera (Chrome or Edge) or type its ID, and the record adopts it. Attached photos move over. 8. It refuses bad IDs. An ID that looks like a web address is blocked, because a QR that points to a website dies with the website. An ID already used in this file is blocked too. 9. It cleans up its own past mistakes. Earlier versions allowed web-address IDs. On first run, after backing up the whole record set, any such record gets a proper ID and keeps the old one as legacy voucher ID. 10. It knows which device made each record. Each browser install gets a session UUID, stamped on everything it saves and shown in the corner badge (tap to copy). Merged exports from several people or machines still say where each record came from. 11. You can download the QR code as an image for a notebook page or another document. Filling in a record 12. The fields follow Darwin Core, the shared vocabulary of GBIF and herbarium databases. Every field exports under its Darwin Core name, so the file goes to a collection database without re-entry. 13. You can add Darwin Core fields the form does not show, from a searchable picker grouped the way the official reference groups them. They save, export, and re-import like any other field. 14. Required fields are marked; the record will not save without them. A box lists what is missing and highlights it. The gate checks presence, not truth: it knows a field is blank, not that it is wrong. 15. It checks formats. Dates must be real year-month-day dates and are corrected as you type. Latitude and longitude must be in range. 16. One button fills coordinates from the device GPS, with reported accuracy, and notes GPS as the source. 17. It records where every coordinate came from (GPS button, photo metadata, or typed) under the Darwin Core georeference protocol term. 18. It handles several collectors. Add, reorder, remove. The first is primary; the rest export as additional collectors. 19. The collector is assumed to be the identifier until you say otherwise. The primary collector's name is copied into Identified by until you type a different one. 20. It suggests recent values, so a field day with one collector and one institution does not mean typing them fifty times. 21. Some fields carry over on purpose. ORCID, institution, country, and kingdom belong to the sitting, not the specimen, and stay filled after a save. Kingdom survives a reload, so a morning of insects is not filed under Plantae. 22. Duplicate for this site. Several specimens from one stop is the normal case. This starts a new record with a new ID, keeping locality, coordinates, habitat, and collectors. 23. A one-line trip summary sits above the form: collectors, institution, country. 24. It times each record, silently, from the first keystroke, selection, GPS fix, or photo to save. Export, print, and delete clicks do not count. 25. It saves the draft as you type. If the browser closes mid-record, a banner offers to restore it, IDs included, so it rejoins its photos. A restored draft is the same specimen, not a new one wearing the old words. 26. Deleting is reversible for a moment. Deleting a record shows an undo prompt. Clearing everything asks first. Photographs 27. Photos attach from the camera or camera roll, are stored beside the record, and show in a small gallery on the saved card. 28. Photos are shrunk to 1920 pixels on the longest edge and re-saved as JPEG. Plenty for identification, and fifty vouchers do not fill the device. 29. The voucher ID is written onto the photo twice. Once visibly in a corner, once invisibly in the JPEG's EXIF block in three places. A photo separated from its record still carries its ID. 30. It reads location from a photo. Empty coordinate fields are filled, with the photo as source. If coordinates exist, it reports the distance between the two, so a photo taken at the truck and a specimen up the slope do not silently disagree. 31. "No photo" and "photo attached" cannot both be true. Ticking the no-photo box on a record with photos is refused. Adding a photo unticks the box. The save gate checks again. 32. Photos follow the record. If the ID changes, photos are re-keyed and re-stamped to match. Deleting a record deletes its photos rather than leaving orphans. Corrections after the fact 33. The original record is never overwritten. A redetermination or coordinate correction is saved as a separate addendum pointing to the occurrence UUID. The original stays as first written. 34. Two kinds of addenda exist: redetermination and georeference correction, each with its own short form. 35. Exports show original and current state: the original fields, matching "current" columns from the latest addendum, and a count of addenda. 36. Correction history survives a spreadsheet round trip. JSON nests the chain under each record; CSV, XLSX, and HTML carry it in one extra column, and re-import rebuilds it instead of quietly reverting a redetermination. 37. It watches for broken chains. An addendum whose parent record is missing is reported, on the main page and in the compiler. The saved list 38. Search, sort, and select. Every export and print action works on the selection, or on everything if nothing is selected. 39. Each card shows its addenda as badges and a panel. Getting data out 40. CSV. Darwin Core column names. Opens in any spreadsheet. 41. JSON. Keeps numbers as numbers and true/false as true/false, nests the addenda, and lists each record's photos by name and size without embedding them. 42. XLSX. A real Excel workbook written by code inside this file. Opens in Excel, LibreOffice, Numbers, and Google Sheets. 43. HTML table. A self-contained page of the records, readable in any browser, printable in landscape. 44. Printed labels. 4 × 3 inches, two across on legal paper: QR, name, family, locality, coordinates, habitat, associates, collector, number, date. A redetermined specimen prints the current name with "Orig. det." underneath, redeterminer and date included, which is what an annotation slip would say anyway. 45. Photos as a ZIP, built by code inside this file, each photo still carrying its stamped ID. 46. Email or share. On a phone, the share sheet with the JSON attached. On a desktop, a download plus a blank email with instructions to attach it. 47. Exports are spreadsheet-safe. A cell a spreadsheet would read as a formula is neutralized, so a pasted locality note cannot run as a command. Real numbers, negative longitudes included, are left alone. Getting data in 48. Import CSV or JSON produced by this tool, or by another copy of it on another device. 49. It refuses to overwrite by accident. An incoming record whose voucher ID matches a saved one is blocked and the clash reported. 50. It flags what it cannot trust. Malformed or incomplete records are imported anyway and marked in an import review column, so nothing is lost and nothing passes as clean. 51. Typed fields are restored on import. Spreadsheets turn everything into text; numbers and true/false values come back per the schema, and anything unrecognized stays text for review, not guessed at. The compiler drawer 52. It merges many exports into one set: JSON, CSV, HTML, XLSX, and photo ZIPs from any number of devices or seasons. A specimen in several files is matched by occurrence UUID, kept once, and its addenda merged. 53. It counts what it did: records, source files, collectors, photo references, photo files, duplicates skipped, addenda merged, problem children. 54. It exports the merged set in every format, every row stamped with a compile batch UUID naming this particular merge. 55. It is deliberately temporary. The compiler empties on reload: a workbench, not a second database. "Add this report" pulls in this device's own records alongside the loaded files. Self-checks and honesty about failure 56. It tells you when the browser refused to save. Browsers refuse writes when storage is full or the window is private. A banner says the record is on screen but not on disk, export now, and stays until a save succeeds. A fading message is how people come to believe a problem went away. 57. It asks the browser to keep the data, where supported, so storage is spared from automatic cleanup. The result shows as a status line. 58. It warns Safari users specifically as the problem child. Safari deletes a site's data after seven days without a visit; the file advises adding the page to the Home Screen, Apple's documented exemption. Tested on Apple hardware only at cellphone kiosks; appears functional; shims are built in. 59. It migrates its own old storage layouts, once, on first run. 60. Dates are local. The date on a record is the calendar date where you are standing, not the date in Greenwich, so an evening collection in Louisiana is not filed under tomorrow. Line numbers refer to ironfist911.html as distributed (11,943 lines). Function names are as they appear in the source. A leading underscore marks a helper not meant to be called from markup. The section numbers (1.1, 5.11, and so on) are the file's own; its module map at line 1182 lists them, and searching the source for a number jumps to that section. B.1 Architecture File layout. One HTML document, no external requests. Lines 1 to 23 are document metadata (Dublin Core, citation, canonical DOI). Lines 24 to 26 are a JSON-LD block typing the file as SoftwareSourceCode, WebApplication, and ScholarlyArticle. Lines 27 to 634 are CSS, light and dark themes as CSS custom properties. Lines 636 to 1132 are markup: the three panes (field form, saved-record list, office tools) and the compiler drawer. Lines 1133 to 1173 hold the QR generator as its own plain script. Lines 1174 to 10706 are the main program, Parts 1 to 8. Lines 10707 to 10936 are the dialogs. Lines 10937 to 11689 are the compiler drawer (Part 9), a second script in its own closure. Lines 11691 to 11938 are the pane shell (Part 10), a third. Runtime layers. Four things carry the state; everything else reads through them. FIELD_DEFS (line 1312) is the schema. Each entry maps an internal key to a CSV/Darwin Core column name and a human label, with an optional type of boolean or number. Every exporter, importer, form reader, and the HTML table iterate this one array. _coerceFieldValue() (line 1461) is the only place typed values are converted on the way in from flat formats. Store (line 1599) is the persistence kernel: a single localStorage key (fieldVoucherRecords_v1, line 1573) holding an object of typed lists (voucher, addendum, uiState, list, bundle). Operations are all, get, put (upsert by id), remove, replaceAll, mutate, filter, lastWriteOk, and invalidateCache. Every write re-reads disk first, so two tabs on one workspace do not overwrite each other; mutate (line 1715) is the read-modify-write form. A refused write sets a flag and routes to the storage failure reporter. Upsert-by-id is the property the merge model rests on: two devices generating different UUIDs can union their exports without collision, and re-importing the same file is idempotent. Bus (line 1797) is a minimal publish/subscribe with on and emit. The list, gallery, problems note, and workspace bar redraw on vouchers:changed; addenda:changed and draft:restore work the same way. Listeners are wrapped so one failing listener cannot stop the rest. Draft (line 5074) is a plain object mirroring the form. Delegated input and change listeners update it as the user types; bulk writers (reset, restore, duplicate, GPS, EXIF) call _draftSyncFromDOM() (line 5076) immediately after setting values, because programmatic .value assignment fires no input event. voucherFromForm() (line 5384), the save gate, and the autosave all read Draft rather than the DOM. Workspaces. One URL parameter, ?ws=name (line 1296), turns the file into separate data sets. Every storage key gets the workspace name appended through _wsKey(); the default workspace keeps the bare keys, so data from earlier builds is where it always was. The list of known workspaces and the install's device tag are shared on purpose. Identifiers. Three UUIDs with three lifespans, all minted by uid() (line 1873, RFC 4122 v4 via crypto.randomUUID, falling back to getRandomValues, then to Math.random). occurrenceID is one per specimen, immutable, minted at draft start by voucherOccurrenceUUID() (line 2777), and is the foreign key for addenda and the dedupe key for merges. The Voucher ID (id) is the label and QR number; it is generated per draft but may be replaced by a preprinted sheet, and photos are re-keyed when it changes. sessionId() (line 2016) is one per browser install, stored under the legacy name deviceTag for backward compatibility, and stamped on every voucher and addendum at save. compileBatchId is one per compiler clear, stamped on compiled exports only. Storage keys. Records: fieldVoucherRecords_v1 (line 1573). Draft autosave: fieldVoucherDraftAutosave_v1 (line 5115). Pre-migration backup of URL-shaped IDs: fieldVoucherreport_redirect_backup_v1 (line 2896). Photos: IndexedDB database fieldVoucherPhotos_v1, store photos (lines 3299 to 3300), holding Blobs keyed by photo id with a voucherId index. All four take the workspace suffix. Shared across workspaces: fieldVoucherWorkspaces_v1 (line 1303) and fieldVoucherDeviceTag_v1 (line 2006). Legacy keys read once by migration and then ignored: fieldVoucherreport_v1 (line 1304), fieldVoucherCollectors_v1 (line 4159), fieldVoucherDwcExtraFields_v1 (line 4318). Record schema version constant: RECORD_SCHEMA_VERSION = "v2" (line 1579). Addenda model. Corrections are their own Store type (addendum), each with its own UUID, a parentOccurrenceID, a parentVoucherId, a type of georeference or redetermination, a payload, an author, a device tag, and a timestamp. The voucher row is never rewritten. computeCurrentStateFields() (line 2159) derives the current_* columns: the standing redetermination, and the standing georeference by supersession rather than array order (_geoHeads(), line 2259). Decoration happens at export and render time, never at write time. DERIVED_STATE_KEYS (line 2219) is computed from the same function so the two cannot drift. One door in. canonicalRecord() (line 2522) is the single entry for every saved and imported record: FIELD_DEFS keys plus four named extras, so an unrecognized column cannot ride into the store. addRecords() (line 2631) is the single import path for JSON, CSV, and take-home bundles. The compiler drawer uses the same canonicalRecord(). Export and import symmetry. JSON nests the addenda array. CSV, XLSX, and HTML serialize the same array into the addenda_json column. parseAddendaField() (line 2137) accepts either shape, so a flat round trip is chain-safe. Ways out. Files: CSV, JSON, XLSX, photo ZIP, HTML table, email, labels, QR sheets, and QR tags (Part 5). Take it home (section 5.5b, line 7131) writes one ZIP per part holding a viewer page, the JSON and CSV exports, the photos, a manifest with a SHA-256 for every file, and a README; a GitHub route cuts parts under GitHub's browser limits (line 7171). Import bundle (section 5.5c, line 7713) brings a bundle back and checks every photo against its manifest hash before reattaching it. Collection exports for Symbiota, Specify, and DiSSCo share one reader, _csPrepare() (line 8820). Init sequence (lines 10625 to 10705). Tracer install, kernel migration from legacy keys, theme, dates, kingdom, pristine snapshot, form listeners, Voucher ID and occurrenceID, Darwin Core fields, collectors, autofill guards, section state, autosave check, trip strip, timer arming listeners (click, input, change, capture phase), legacy URL-ID migration followed by vouchers:changed, store reconciliation and the snapshot offer, storage resilience check, the self test when asked for, session badge, top stamp, workspace bar, two-tab presence, and the associated-voucher readout. Third-party code. One item: Kazuhiko Arase's QR Code Generator for JavaScript, MIT, vendored at lines 1133 to 1173. It runs as its own script and sets window.QR; _ensureQR() (line 2747) only confirms it is there. Everything else, ZIP reader and writer, XLSX writer, EXIF reader and writer, JPEG re-encoder, SHA-256 through the browser's crypto.subtle, is in-file. B.2 Function reference All 488 functions in the file, in source order, grouped by the file's own sections. Each description is the one-line comment above the function in the source. 1.1 Workspaces (lines 1278 to 1306) _wsSanitize(v), line 1295. Folds any workspace name to lowercase letters, digits and hyphens, 24 characters max. _wsKey(k), line 1300. Storage key for this workspace; the default workspace keeps the bare key. _wsFileName(n), line 1302. Download filename for this workspace: pests_field_vouchers_2026-09-19.csv. 1.2 Field definitions (lines 1307 to 1526) _coerceFieldValue(def, value), line 1461. Turns a raw cell value into the type its FIELD_DEFS entry declares (boolean, number, text). _boolTrue(v), line 1481. True for true, "true", 1 or "1"; everything else is false. _mergeAddendumCopies(kept, incoming), line 1493. Two copies of the SAME addendum (same id) that disagree on isCurrentIdentification. 1.3 Kernel: Store and Bus (lines 1527 to 1756) _load(fresh), line 1621. Reads the whole record store from localStorage, using the cache when nothing has changed. _save(), line 1648. Writes the cache to localStorage and reports a refused write instead of swallowing it. all(type), line 1664. Every record of one type (a copy, safe to change). get(type, id), line 1666. One record by type and id, or null. put(type, id, data), line 1671. Adds or replaces one record, then saves. remove(type, id), line 1683. Removes one record by type and id, then saves. replaceAll(type, records), line 1704. Swaps out every record of one type in a single write. 801: this re-reads disk and then throws the re-read away. mutate(type, fn), line 1715. Read, modify, write against what is on disk NOW. fn receives the current list (a copy, safe to mutate) and returns the list to store; returning nothing keeps the copy it was handed. filter(pred, type), line 1726. Every record that passes a test, across one type or all of them. lastWriteOk(), line 1737. Did the last write to localStorage succeed? invalidateCache(), line 1743. Drops the parsed copy so the next read goes back to disk. 1.4 Storage failure reporting (lines 1757 to 1811) storageWriteFailed(), line 1768. True while localStorage is refusing writes. reportStorageWriteFailure(err, what), line 1770. Records a failed write and warns the user once. noteStorageWriteRecovered(), line 1789. Clears the storage warning once writes succeed again. on(evt, fn), line 1800. Subscribes a function to a named event. emit(evt), line 1802. Calls every function subscribed to an event; one failing listener never stops the rest. 1.5 Util (lines 1812 to 1889) esc(s), line 1816. Escapes text for safe use inside HTML. _forceLowercase(el), line 1822. Species epithets are never capitalized in botanical nomenclature, but autocapitalize="off" alone is not honoured by every mobile keyboard, so this forces the value itself lowercase as it's typed rather than trusting the attribute. todayISO(), line 1841. Today's date from local calendar parts, not UTC, so an evening record is not dated tomorrow. fullName(first, last), line 1849. First and last name joined into one display name. _isoDateInputFilter(el), line 1857. Keeps a typed date in YYYY-MM-DD form as the user types digits. uid(), line 1873. A random v4 UUID, with fallbacks for older browsers. 1.6 Legacy to Store migration (lines 1890 to 1948) _kernelMigrateLegacyIfNeeded(), line 1897. One-time move of old per-feature storage keys into the Store. 2.1 Addenda (lines 1949 to 2320) _sharedDeviceTag(), line 2008. The install-wide copy of the tag, or "" if none or unreadable. _rememberSharedDeviceTag(tag), line 2012. Writes the install-wide copy of the tag, only if it is still empty. sessionId(), line 2016. The per-install session tag: reads it, borrows the shared copy, or mints one on first call. createAddendum(parentOccurrenceID, parentVoucherId, addendumType, payload, authorFirst, authorLast), line 2038. Files a new addendum (redetermination, georeference, note) against a saved voucher. getAddendaFor(occurrenceID), line 2081. Every addendum for one occurrenceID, oldest first. deleteAddendaForOccurrences(occurrenceIDs), line 2094. Deletes the addenda of vouchers being deleted, so none are left pointing at a missing occurrenceID; returns the count. deleteAllAddenda(), line 2103. Removes every addendum in this workspace; returns how many went. countAddendaForOccurrences(occurrenceIDs), line 2110. Counts the addenda that point at any of these occurrenceIDs. deleteAddendum(id), line 2116. Deletes one addendum after a confirm; the voucher itself is untouched. _normalizeAddendum(a), line 2130. Pulls the addenda chain off an incoming record whatever format carried it in: JSON nests the real array under .addenda, the flat formats carry the same array serialized in the addenda_json column. parseAddendaField(src), line 2137. Reads the addenda chain from an imported record, whether nested JSON or an addenda_json cell. computeCurrentStateFields(rec, addenda), line 2159. Works out the current name, determiner and coordinates from a voucher and its addenda. decorateWithCurrentState(rec), line 2223. A saved voucher plus its live addenda and current-state fields. decorateAllWithCurrentState(recs), line 2228. decorateWithCurrentState() over a list. decorateWithNestedAddenda(rec), line 2234. Current-state fields from addenda already on the record (no Store lookup). decorateAllWithNestedAddenda(recs), line 2239. decorateWithNestedAddenda() over a list. _geoHeads(addenda), line 2259. The standing georeferences for one specimen, the ones nothing supersedes. One head is current; more than one is a conflict. _geoLineage(head, addenda), line 2278. The standing georeference followed by everything it was made on top of, newest first, using the same supersession rule as _geoHeads(). findAddendumProblems(vouchers, addenda), line 2295. Finds orphaned addenda and specimens with zero or several current determinations. findLocalAddendumProblems(), line 2317. findAddendumProblems() over this workspace's own saved data. 2.2 Saved list and import checks (lines 2321 to 2486) load(), line 2325. Every saved voucher in this workspace. persist(arr), line 2327. Replaces the saved voucher list. persistUpdate(fn), line 2332. The read-modify-write form of persist(). Use this for anything that edits the current set (add one, delete one, delete these); persist() stays for a genuine wholesale replacement. fn gets the list as it is on disk at this instant, not as it was when the screen last drew. _uniqueList(arr), line 2334. Distinct non-empty values, sorted. _shortIdList(arr, max), line 2336. A short comma list of IDs for messages, with "+N more" when long. _existingVoucherIdSet(records), line 2341. The set of Voucher IDs already in a record list. _voucherIdInUse(id, records), line 2347. Is this Voucher ID already taken? _freshUniqueId(used), line 2349. A new UUID not in the given set; adds it to the set. _findIncomingIdClashes(items, existingRecords), line 2356. Lists incoming Voucher IDs that clash with saved ones or repeat inside the import. _blockDuplicateImportIfNeeded(items, existingRecords), line 2369. Stops an import with a message if any Voucher ID would clash. ingestAddendaFor(item, rec), line 2400. The import half of the addenda fix, and the actual bug: export nested the chain correctly, import ran every record through FIELD_DEFS and threw the nested array on the floor. _malformedAddendaCell(src), line 2424. True when an addenda_json cell is present but not a readable list. _addendaIntegrityIssues(src), line 2436. The dialog runs _addendumFormatProblems() on every addendum typed here; an addendum arriving in a file never did, so a georeference with latitude 999 imported clean and became current. _recordIntegrityIssues(src), line 2448. Format problems in one record (bad coordinates, bad dates) for the import flag. _joinImportIssues(existing, added), line 2480. Merges two lists of import issues into one "; " string without repeats. 2.3 Canonical record (lines 2487 to 2742) canonicalRecord(raw, opts), line 2522. Builds one clean record from any input, field by field through FIELD_DEFS. _fingerprintSource(rec), line 2591. The exact string every fingerprint is taken over. _recordFingerprint(rec), line 2602. A content fingerprint of one record, used to spot exact duplicates on import. _recordFingerprintLegacy(rec), line 2615. The pre-801 low-byte hash, kept so old stamps still verify. _fingerprintMatches(rec, stamp), line 2625. True when a stored fingerprint still matches, old format or new. addRecords(items), line 2631. Adds imported records to the saved list, merging, de-duplicating and ingesting addenda. 2.4 Voucher identity and QR engine (lines 2743 to 3135) _ensureQR(cb), line 2747. The QR library runs as its own plain script at page load and sets window.QR; this just confirms it is there, then runs the callback. voucherId(), line 2756. The draft's current Voucher ID (the QR value). generateVoucherQR(optId), line 2758. Sets the draft's Voucher ID and draws its QR code. voucherOccurrenceUUID(), line 2777. The draft's occurrenceID, created on first use. _draftIdentityKey(), line 2784. Which draft is on screen. The occurrenceID is minted fresh for every new draft and restored with a restored one, and a relabel (new Voucher ID) leaves it alone, so it changes exactly when the specimen changes. newDraftOccurrenceUUID(), line 2786. Starts a fresh occurrenceID for a new draft and shows it. queuePhotoIdMigration(oldId, newId), line 2808. Moves the draft's photos to a new Voucher ID, one move at a time. refreshVoucherQR(), line 2824. Mints a fresh Voucher ID for the draft and redraws its QR. downloadVoucherQR(), line 2833. Saves the draft's QR code as a PNG. makeCardQR(id, px), line 2845. A QR code as a PNG data URL, for cards and printouts. _voucherQrMode(mode), line 2854. Switches the QR panel between a generated ID and a pre-printed sheet ID. _cleanVoucherId(id), line 2868. Strips control characters and spaces from a Voucher ID. _voucherIdIsUrl(id), line 2891. True when a Voucher ID is a web address or other resolvable link (not allowed). async migrateLegacyRedirectVoucherIds(), line 2898. One-time repair: replaces URL-style Voucher IDs from old builds with UUIDs, keeping a backup. _assignPreprintId(id), line 2966. Takes a scanned or typed pre-printed ID as the draft's Voucher ID. async startVoucherScan(), line 2997. Opens the camera and starts looking for a QR code. _doScanFrame(), line 3019. Checks one video frame for a QR code, then schedules the next. _stopScanStream(), line 3036. Stops the camera and frame loop. _qrModalSetTarget(t), line 3054. Sets the scan dialog's wording for its job: pre-printed sheet or associated voucher. openQRModal(mode, target), line 3063. Opens the scan / type ID dialog. closeQRModal(), line 3074. Closes the scan / type ID dialog and stops the camera. _qrModalKeydown(e), line 3090. Escape closes the scan dialog. _qrModalBackdropClick(e), line 3092. A click outside the box closes the scan dialog. qrModalTab(mode), line 3094. Switches the scan dialog between Scan and Type tabs. voucherManualIdPreview(), line 3114. Previews a typed ID as a QR code while typing. voucherAssignManualId(), line 3121. Accepts the typed ID, for the draft or for an associated voucher link. 3.1 Coordinates (lines 3136 to 3260) _setNoFixFromGPS(reason, displayReason), line 3140. Records a failed GPS attempt without wiping coordinates already entered. useCurrentLocation(), line 3180. GPS button: the location prompt appears here, on the press, and nothing else prompts. markManualCoordEntry(), line 3246. Marks coordinates as typed by hand and notes the source. _coordNoFixClean(), line 3256. True when GPS failed and the coordinate boxes are still empty. 3.2 Photo store (lines 3261 to 3924) _openPhotoDB(), line 3305. Opens (or creates) the photo database. _photoId(), line 3335. A unique id for one stored photo. async resizeImageFile(file, maxDim, quality, stampId), line 3349. Shrinks and re-encodes a photo to JPEG, stamping the Voucher ID into it. _stampPhotoId(ctx, w, h, id, coverId), line 3400. Burns the Voucher ID into the photo's pixels, sized so a long ID fits on a small photo. async _restampPhotoBlob(blob, oldId, newId), line 3431. Re-draws a photo's stamped ID after the voucher's ID changes. sanitizeFilename(s), line 3461. Makes text safe to use as a file name. _asciiBytes(s), line 3475. Text as null-terminated ASCII bytes, for EXIF fields. _buildVoucherExifSegment(id), line 3483. Builds an EXIF block that carries the Voucher ID. entry(tag, type, count, value), line 3507. Writes one EXIF directory entry. _isExifApp1(bytes, off), line 3533. True when these bytes start an EXIF APP1 segment. _stripExifApp1(bytes), line 3538. Removes any existing EXIF segments from a JPEG. async _jpegWithVoucherExif(blob, id), line 3558. Puts the Voucher ID EXIF block into a JPEG. async _jpegHasVoucherExif(blob, id), line 3569. Does this JPEG already carry this Voucher ID in its EXIF? _photoExt(p), line 3600. The file extension that matches what is actually stored. photoFilename(idVal, index, p), line 3611.
_NN.
for one photo in an export. stablePhotoNames(voucherIdVal, photos, rec), line 3625. Photo id to filename for a voucher's photos. A photo keeps the first name it went out under; a new photo takes the lowest unused number. async addPhotosToVoucher(voucherIdVal, files), line 3647. Saves photos to the database under one Voucher ID. async getPhotosForVoucher(voucherIdVal), line 3687. Every stored photo for one Voucher ID. async photoVoucherIdCounts(), line 3745. Every voucherId that has at least one photo row against it, with a count. async deletePhoto(photoId), line 3766. Deletes one stored photo. async deletePhotosForVoucher(voucherIdVal), line 3775. Deletes every photo for one Voucher ID. async clearAllPhotos(), line 3787. Deletes every stored photo in this workspace. async migratePhotosVoucherId(oldId, newId), line 3799. Moves photos from an old Voucher ID to a new one, re-stamping each. async renderPhotoGallery(), line 3829. Draws the thumbnail strip under the Photos field. async deletePhotoUI(photoId), line 3853. Removes one photo after a confirm. onPhotoSkipToggle(), line 3859. Shows or hides the "no photo" reason box. openPhotoInput(which), line 3888. Opens the camera or the picker from a real user press. async handlePhotoInput(inputEl), line 3897. Handles photos picked in the form: store, stamp, check GPS. 3.3 EXIF GPS (lines 3925 to 4078) async extractExifGPS(file), line 3944. Reads GPS coordinates from a JPEG's EXIF, or null. _parseExifGPS(view, tiffBase), line 3967. Walks the EXIF directories to the GPS tags and returns decimal lat/lon. _haversineMeters(lat1, lon1, lat2, lon2), line 4027. Distance in meters between two lat/lon points. async reconcileExifGPS(files, draftKey), line 4034. Compares photo GPS with the form's coordinates; fills or flags as needed. 3.4 Voucher timer (lines 4079 to 4146) beginVoucherTimer(), line 4113. Starts the draft's timer. disarmVoucherTimer(), line 4117. Clears the draft's timer. _isDraftSurface(el), line 4126. True when an element is part of the draft (form or QR block). _armVoucherTimerOnFirstAction(ev), line 4131. Starts the timer on the first real edit to the draft. 3.5 Collectors (lines 4147 to 4305) _collectorsLoad(), line 4165. The saved collector list. _collectorsPersist(), line 4170. Saves the collector list. _collectorsSyncFromDOM(), line 4175. Copies typed collector names from the form into memory. _collectorsSyncAndSave(), line 4183. Reads, saves and passes collector changes on to the trip strip and Identified-by. _syncDetFromCollector(), line 4196. Mirrors the primary collector's name into the Identified-by (det) fields: the collector is the default determiner at time of discovery. renderCollectorRows(), line 4209. Draws the collector rows. addCollectorRow(), line 4241. Adds an empty collector row. removeCollectorRow(id), line 4250. Removes a collector row, with Undo. moveCollectorRow(id, dir), line 4268. Moves a collector up or down the list. getCollectorNamesList(), line 4281. Every collector's full name, primary first. getPrimaryCollectorFirst(), line 4286. The primary collector's first name. getPrimaryCollectorLast(), line 4288. The primary collector's last name. getAdditionalCollectorsFromForm(), line 4290. The other collectors, joined with " | ". _restoreExtraCollectorValues(), line 4294. Redraws the collector rows from memory. initCollectors(), line 4298. Loads saved collectors and draws their rows at startup. 3.6 Additional Darwin Core fields (lines 4306 to 4572) _dwcLoadSchema(), line 4321. The saved list of added Darwin Core terms. _dwcSaveSchema(), line 4326. Saves the list of added Darwin Core terms. _dwcSafeId(term), line 4330. A Darwin Core term made safe for use in an element id. _dwcCategoryFor(term), line 4332. The picklist category a Darwin Core term belongs to. _dwcAllTermsFlat(), line 4337. Every Darwin Core term in the picklist, with its category. filterDwcTerms(), line 4344. Filters the Darwin Core picklist as the user types. hideDwcTermDropdown(), line 4365. Hides the Darwin Core picklist. selectDwcTerm(term), line 4367. Adds the chosen Darwin Core term to the form. _dwcAddRow(term, value), line 4379. Draws one added Darwin Core row, with an optional value. removeDwcExtraField(term), line 4395. Removes an added Darwin Core row, with Undo. getDwcExtraObjectFromForm(), line 4412. The added Darwin Core values as {term: value}. packDwc(obj), line 4422. {term: value} packed as "term: value | term: value". getDwcExtraFieldsFromForm(), line 4426. The added Darwin Core values, packed. _parsePackedDwc(str), line 4453. The packed extraDwcFields string as a term/value object. dwcShapeConflicts(rec), line 4462. Terms where a record's dwc object and its packed string disagree. parseExtraDwc(rec), line 4473. A record's added Darwin Core terms, from either shape or both. dwcColumnsFor(recs, opts), line 4499. The added Darwin Core columns for an export, in a stable order: this workspace's configured terms first, then any others the records carry. exportFieldDefs(recs, opts), line 4520. The export column list: FIELD_DEFS with added Darwin Core columns in place. flattenDwc(rec), line 4528. One record with its added Darwin Core values spread into their own columns. flattenAllDwc(recs), line 4535. flattenDwc() over a list. dwcTermForHeader(h), line 4541. Matches an import column header to a Darwin Core term, any case. dwcFromColumns(headers, cells, taken), line 4549. Collects Darwin Core values from import columns nothing else claimed. attachDwc(rec, src, fromCols), line 4561. Puts the Darwin Core values on a record, both as an object and packed. initDwcExtraFields(), line 4568. Restores the added Darwin Core rows at startup. 3.7 Associated voucher (lines 4573 to 4773) _dwcEnsureTerm(term), line 4600. Adds a Darwin Core row to the form if it isn't there; returns its input. _assocParts(v), line 4605. Splits a " | " list into parts. _assocSet(el, parts), line 4607. Writes a DwC row's value the way typing would, so Draft and the autosave see it. invalidateWorkspaceIndex(), line 4636. Drops the cross-workspace index so the next lookup rebuilds it. _workspaceIndex(), line 4638. The parsed index of every other workspace's vouchers, built once and reused. unreadableWorkspaces(), line 4667. The workspaces whose record store could not be read just now. findVoucherAnywhere(idOrOcc), line 4669. Finds a voucher by Voucher ID or occurrenceID in any workspace here. _assocRel(), line 4677. The relationship, tidied: no parentheses or bars, which would break the DwC string. linkAssociatedVoucher(raw), line 4683. The link itself: look the scanned ID up, write the DwC rows, show it. unlinkAssociatedVoucher(i), line 4709. Removes one link, and its associatedTaxa entry when there is one. renderAssocReadout(), line 4727. One line per link: relationship, name, and where it lives. toggleOtherTaxa(force), line 4756. The Other taxa bar under Additional Darwin Core field: opens and closes the associated-voucher block. initAssociatedVoucher(), line 4765. Restores the associated-voucher controls at startup. 3.8 Suggestions (lines 4774 to 4936) renderOptionDropdown(box, items, rowHtml), line 4784. Fills a suggestion box with rows, or hides it when empty. hideDropdown(boxId), line 4791. Hides a suggestion box. _recentValuesFor(key, max), line 4804. Recent distinct values of one field across saved vouchers. showRecentSuggestions(fieldId, boxId, key), line 4813. Shows recent values under a field. hideRecentSuggestions(boxId), line 4826. Hides recent values. _recentPick(fieldId, idx), line 4828. Puts a picked recent value into its field. _familyCandidates(), line 4862. Family names to suggest: this workspace's own first, then the seed list. showFamilySuggestions(), line 4875. Shows family suggestions as the user types. _familyPick(i), line 4889. Puts the picked family into the field. _familyKey(e), line 4899. Arrow keys walk the list, Enter or Tab takes the highlighted row (Tab takes the top row when none is highlighted), Escape closes it. updateRecentCollectorNames(), line 4919. Refreshes the collector name suggestions from saved vouchers. 3.9 Trip strip (lines 4937 to 4955) renderTripStrip(), line 4943. Draws the trip strip: collectors and institution for this sitting. 3.10 Duplicate for this site (lines 4956 to 5059) _legacyAdminKeys(o), line 4982. Maps the old State/Province and County/Parish draft keys onto their current names. _siteFromForm(), line 4992. The site fields (place, coordinates, habitat) from the form. _siteHasContent(site), line 5004. True when a site has a locality or coordinates. rememberSite(site), line 5009. Remembers the last site for Duplicate for this site. lastRememberedSite(), line 5013. The last remembered site, or null. _applySite(site), line 5018. Writes a site back into the form. _draftHasSpecimenContent(), line 5030. True when the draft has any specimen-level content. async duplicateForThisSite(), line 5034. Starts a new draft at the same site as the last one. 3.11 Draft state (lines 5060 to 5086) _draftSyncFromDOM(), line 5076. Copies every form field into Draft. draftVal(id), line 5085. One draft value: from Draft, else from the form. 3.12 Draft autosave (lines 5087 to 5294) _draftIdentitySnapshot(), line 5121. The draft's IDs, photo choice and added fields, for the autosave. _draftAutosaveNow(), line 5142. Writes the draft autosave now. scheduleDraftAutosave(), line 5161. Autosaves the draft shortly after typing stops. clearDraftAutosave(), line 5166. Deletes the draft autosave. capturePristineDraft(), line 5184. Notes what a blank form looks like, so the autosave can tell real edits. _draftHasContent(data), line 5189. True when a draft differs from a blank form. formHasUnsavedContent(), line 5215. True when the form holds unsaved work. checkForDraftAutosave(), line 5230. Offers to restore a saved draft at startup. _showDraftRestoreBanner(data, photoCount), line 5249. Shows the restore banner for a saved draft; photoCount > 0 says photos are the reason. restoreDraftAutosave(), line 5278. OWNERSHIP, stated once so the next person reading this doesn't have to infer it from call sites: Store owns record state. discardDraftAutosave(), line 5285. Throws away the saved draft and hides the banner. dismissDraftRestoreBanner(), line 5290. Hides the restore-draft banner. 3.13 Apply a restored draft (lines 5295 to 5381) applyDraftSnapshot(data), line 5301. Writes a restored draft back into the form. 3.14 Form (lines 5382 to 5441) voucherFromForm(), line 5384. Builds a voucher record from the form. 3.15 Save gate (lines 5442 to 5844) isNotRecorded(v), line 5487. True when a value is the "Not recorded" placeholder. recordedOrBlank(v), line 5489. A value, or blank if it is "Not recorded". fillNotRecorded(v), line 5491. Fills empty optional fields with "Not recorded"; returns which. clearFieldHighlights(), line 5500. Removes red highlights from form fields. _isRealISODate(s), line 5514. The gate tested non-empty and stopped there, so latitude=banana, longitude=x and 2026-99-99 all sailed through and landed in the record, the export, and GBIF's lap. _coordProblem(raw, limit, name), line 5524. A message if a coordinate is not a valid decimal number in range, else "". validateFieldFormats(), line 5533. Checks coordinate and date formats before a save. validateRequiredFields(), line 5559. Lists required fields that are empty. async validatePhotoRequirement(voucherIdVal), line 5599. The photo rule at save: a photo, or No photo ticked, never both. showIncompleteVoucherModal(missing), line 5615. Shows the incomplete-voucher dialog and opens the sections at fault. async saveVoucher(ev), line 5660. The form submit handler. Does one thing of its own, which is to refuse a second save while the first is still running, then hands off to _saveVoucherBody. async _saveVoucherBody(), line 5671. The save itself, run one at a time by saveVoucher above. rememberKingdom(v), line 5784. Kingdom belongs to the sitting, not to the specimen. lastKingdom(), line 5788. The last kingdom used, or Plantae. resetVoucherForm(), line 5793. Clears the form for the next voucher, keeping sitting-level values. async clearVoucherDraft(skipConfirm), line 5830. Discards the draft and its photos, after a confirm if needed. 3.16 Autofill spill guard (lines 5845 to 5982) _suppressBrowserAutofill(root), line 5860. Stops the browser's own address autofill from cross-wiring the form. spillGuardedIds(), line 5897. The field ids the autofill spill guard watches, as a live list. _spillSnapshot(), line 5912. The guarded fields' values before autofill can touch them. _installAutofillSpillGuard(), line 5918. Installs the guard that undoes browser autofill spilling into other fields. 4.1 Render list, search, sort, bulk select (lines 5983 to 6189) onVoucherSearchInput(), line 5990. Search box typed: filter the saved list. onVoucherSortChange(), line 5996. Sort menu changed: re-sort the saved list. toggleSelectAllVouchers(checked), line 6002. Ticks or clears every voucher currently shown. toggleVoucherSelected(id, checked), line 6009. Ticks or clears one voucher. updateBulkControls(), line 6014. Enables the bulk buttons and shows how many are ticked. bulkDeleteSelected(), line 6029. Deletes every ticked voucher, with their photos and addenda. renderVouchers(), line 6047. Schedules a list redraw 80 ms out, so a burst of changes draws once. _renderVouchersNow(), line 6052. Draws the saved-voucher list now: filter, sort, cards. async renderCardPhotos(voucherIdVal, slot), line 6159. Fills one card's photo strip. 4.2 Addendum UI (lines 6190 to 6438) _addendaBadgeHTML(a), line 6197. The badge that says which device filed an addendum. _addendumSummaryHTML(a), line 6206. One line describing an addendum's content. renderAddendaPanelHTML(occurrenceID), line 6225. The addenda panel shown on a saved voucher's card. _addendumPersonDefault(voucherId), line 6256. Who an addendum should be attributed to before anyone edits it, in the order of preference set out above. _rememberAddendumPerson(first, last, orcid), line 6268. Remembers who filed the last addendum, for next time. showAddAddendumModal(voucherId, occurrenceID, type), line 6274. Opens the add-addendum dialog for one voucher. closeAddendumModal(), line 6298. Closes the add-addendum dialog. _addendumFormatProblems(type, payload), line 6310. The save gate learned to check coordinates and dates in 108; this door never did, and it's the worse of the two. submitAddendumModal(), line 6325. Checks and saves the addendum from the dialog. deleteVoucher(id), line 6387. Deletes one voucher, its photos and addenda, with Undo. openClearAllDialog(), line 6400. Opens the Clear all dialog. closeClearAllDialog(), line 6402. Closes the Clear all dialog. async confirmClearAll(), line 6412. Clears every voucher, addendum and photo, and redraws only after the photo sweep has finished. 4.3 Addendum problems note (lines 6439 to 6471) renderAddendumProblems(), line 6450. findLocalAddendumProblems() has existed since the addenda went in and nothing ever called it outside the Compiler Drawer, so the local report could hold an orphan or two disagreeing redeterminations and show no sign of either. 5.1 Download helpers (lines 6472 to 6496) downloadBlob(name, content, mime), line 6474. Saves content to a file with the workspace-prefixed name. downloadCSV(name, csv), line 6484. Saves CSV text (with a BOM so Excel reads UTF-8). downloadJSON(name, obj), line 6486. Saves an object as pretty-printed JSON. downloadHTMLFile(name, html), line 6488. Saves an HTML string as a file. recordsFor(ids), line 6491. The saved vouchers with these IDs, or all of them. 5.2 CSV export and import (lines 6497 to 6598) _csvFormulaSafe(v), line 6515. A cell that starts with = + @ or a tab is a FORMULA to Excel, LibreOffice and Sheets, not text: they evaluate it on open, and =cmd|'/c ...'!A1 is a live command. csvUnescapeFormulaGuard(v), line 6522. Removes the formula guard quote added on export. csvEscape(v), line 6527. One CSV cell: formula-guarded and quoted when needed. toCSV(records), line 6533. Records as CSV text. exportCSV(ids), line 6540. Exports vouchers as CSV. parseCSV(text), line 6549. Parses CSV text into rows of cells. importCSV(ev), line 6569. Imports vouchers from a CSV file. 5.3 JSON export and import (lines 6599 to 6710) async recordsWithPhotoManifest(recs), line 6606. Photo files travel separately via Export All Photos (ZIP); this JSON carries a manifest only. async exportJSON(ids), line 6692. Exports vouchers as JSON, with a photo manifest. importJSON(ev), line 6699. Imports vouchers from a JSON file. 5.4 ZIP and XLSX export (lines 6711 to 6936) crc32(bytes), line 6717. CRC-32 checksum of bytes, for ZIP entries. _u16(n), line 6727. A 16-bit number as two little-endian bytes. _u32(n), line 6729. A 32-bit number as four little-endian bytes. strToBytes(s), line 6731. Text as UTF-8 bytes. base64ToBytes(b64), line 6733. Base64 text as bytes. colLetter(n), line 6740. A column number as a spreadsheet letter (1 = A, 27 = AA). xmlEsc(s), line 6773. Escapes text for XML, keeping line breaks. makeZip(files), line 6782. Packs files into an uncompressed ZIP (used for XLSX and photo exports). buildXLSX(records), line 6824. Builds an XLSX workbook (with QR images) from records. exportXLSX(ids), line 6923. Exports vouchers as XLSX. 5.5 Photo ZIP export (lines 6937 to 7009) async exportAllPhotosZip(ids), line 6953. Exports every photo for these vouchers as one ZIP, with a manifest. 5.5b Keep a copy of this file (lines 7010 to 7130) _downloadFolderHint(), line 7041. Names this device's usual download folder, for the notice. saveThisFileToDevice(), line 7059. Saves a copy of this report file itself to the device. async _finishSaveToDevice(bytes, picked, name), line 7085. Waits for the bytes and the folder choice, then writes the copy. 5.5b Take it home (lines 7131 to 7712) async _sha256Hex(bytes), line 7177. SHA-256 of some bytes as lowercase hex, or "" where the browser offers no crypto.subtle. makeZipBlob(entries), line 7189. A stored (uncompressed) ZIP as a Blob. Same layout and UTF-8 name flag as makeZip(), but each entry's data may be a Blob that is never copied: the photo's own Blob goes into the archive by reference, so a 200 MB part never needs 200 MB of bytes in memory at once. async _bundleTextEntry(name, text), line 7210. One text file as a ZIP entry, with its crc, size and SHA-256. _bundledPhotoMap(), line 7216. Photo id to the name of a saved or shared bundle that holds it. async _bundlePlan(recs, limits, opts), line 7223. Reads which photos each voucher has and cuts the vouchers into parts. _bundleCard(rec, photoPaths), line 7253. One voucher as the viewer page shows it. Names come through _csPrepare() and _odsNameHTML(), the same reader every other export uses, so the page and the files beside it cannot disagree on the current name. _bundleViewerHTML(meta, cards), line 7276. The open-me.html page. Everything it shows is embedded, so it opens from a folder with no server and no network. _bundleReadme(meta, fileCount), line 7339. README.txt for one part: how to open it and how to check it. async _bundleBuildPart(plan, k, bundle), line 7375. Builds one part: reads each photo once (for its crc and SHA-256), then hands the photo's own Blob to the ZIP. _takeHomeSettings(), line 7442. Where bundles are going, remembered per workspace: route ("device" or "github"), and for GitHub the owner/repo and branch. _githubRepoClean(v), line 7447. Owner/repo as GitHub allows it, or "" when it is not one. _githubUploadURL(repo, branch), line 7452. The repo's upload page, pointed at the take-home folder on that branch. async openTakeHomeModal(ids, opts), line 7457. Opens the Take it home dialog for these vouchers, or all of them. async _takeHomeSetNewOnly(on), line 7470. The "only photos not taken home yet" box. async _takeHomeReplan(), line 7477. Cuts the parts for the chosen route and redraws. async _takeHomeSetting(key, value), line 7485. Saves a change to the route, repo or branch and replans. _takeHomeGithubNote(), line 7495. The GitHub line: the upload link once the repo is valid, and what to do there. _takeHomeRender(), line 7507. Draws the dialog from _takeHome. closeTakeHomeModal(), line 7555. Closes the Take it home dialog and lets go of any built ZIPs. async _takeHomePrepare(k), line 7562. Builds one part, logs what went into it, then offers Share and Save. _takeHomeHandedOff(k, how), line 7589. Marks a part as having left the device, in the log the Remove step reads. async _takeHomeShare(k), line 7595. Sends a prepared part through the device's share sheet. _takeHomeSave(k), line 7610. Downloads a prepared part. async clearBundledPhotos(), line 7629. The Remove step. Walks every saved voucher; a voucher's photos go only if every one of them is in a bundle that was saved or shared and still hashes to what that bundle recorded. 5.5c Bring it back, and what is not safe yet (lines 7713 to 7960) async _putPhotoRecord(rec), line 7727. Writes one photo row straight into the photo store, keeping its original id. async importBundleFiles(ev), line 7736. The Import bundle button: every picked ZIP, in name order. async _importOneBundle(file, total), line 7758. One take-home ZIP: records in through addRecords(), then each photo checked and re-attached to the voucher as saved here. scheduleTakeHomeNote(), line 7849. Schedules a refresh of the not-yet-taken-home line. async renderTakeHomeNote(), line 7854. Counts vouchers with photos in no handed-off bundle, and draws the line. _anyDialogOpen(), line 7908. True when some other dialog is already up, so the offer waits its turn. async maybeOfferSnapshot(), line 7913. Decides whether to offer a snapshot now, and offers it. _snapshotSnooze(hours), line 7947. Closes the offer and holds off for this many hours. _snapshotYes(), line 7953. Yes: close the offer, open the snapshot. The short snooze keeps the offer from coming straight back if the snapshot is then abandoned. 5.6 Email (lines 7961 to 7997) async emailreport(ids), line 7969. Shares or emails the JSON export. 5.7 HTML table export (lines 7998 to 8030) plainDoc(title, inner, extraCSS), line 8000. A plain printable HTML page around some content. exportHTMLTable(ids), line 8018. Exports vouchers as a printable HTML table. 5.8 Print labels (lines 8031 to 8178) _taxonHTML(name, qualifier), line 8041. A determination as label HTML: name words italic, qualifier and rank connectors roman. labelInner(v), line 8058. The inside of one herbarium label. labelDoc(records), line 8107. A page of herbarium labels, four by three inches. printHTMLNow(html), line 8139. Opens HTML in a new window and prints it. printLabels(ids), line 8173. Opens herbarium labels for printing: the current determination on top, the original kept as Orig. det. 5.9 QR sheets (preprint) (lines 8179 to 8371) _voucherIdTail(id), line 8197. Last four of a Voucher ID, lowercase; "" if too short. _setVoucherIdLabel(el, id), line 8202. Writes a Voucher ID into the draft QR panel with its last four in bold. _qrSheetDefaultRows(), line 8209. The sheet's lines as [{label, value}], remembered in uiState. _qrSheetRows(), line 8217. The saved QR sheet lines, or the defaults. _qrSheetRowsFromDOM(), line 8225. Reads the row editor back out of the dialog; empty rows drop. _qrSheetSave(), line 8232. Saves as you type, so Cancel still remembers. _qrSheetRenderRows(rows), line 8234. Draws the row editor: label, value, remove. _qrSheetAddRow(), line 8244. Adds an empty line and puts the cursor in its label. _mintSheetIds(n), line 8255. Mints n lowercase v4 UUIDs whose last four are unique within the batch and don't repeat any last four already on a voucher in this browser (or the current draft). qrSheetDoc(ids, fields), line 8273. Builds the print window: one letter page per UUID. openQrSheetModal(), line 8310. Opens the QR sheets dialog, building it on first use. closeQrSheetModal(), line 8344. Closes the QR sheets dialog. _qrSheetKeydown(e), line 8351. Escape closes the dialog. _qrSheetSetCount(n), line 8353. Picks 10/20/50/100 and lights the matching button. _qrSheetResetFields(), line 8358. Back to the default six lines, names from the primary collector. printQrSheets(), line 8363. Saves the lines, mints the batch, opens the print window. 5.10 QR tags (reconciliation print) (lines 8372 to 8624) _qrTagPerPage(), line 8412. The saved tags-per-page choice, or 12. _qrTagSetPerPage(n), line 8418. Picks 20/12/6 per page, lights the button, remembers it. _qrTagNameHTML(v), line 8425. The name line for a tag: qualifier upright, binomial in italics. _qrTagTime(v), line 8434. 24-hour local time the draft was started (falls back to save time). _qrTagDates(), line 8440. Distinct collection dates with counts, newest first. _qrTagRecords(scope), line 8446. The records a scope names, oldest saved first (field order). _qrTagRenderScopes(), line 8453. Fills the scope picker: selected (if any), each day, all. _qrTagRenderList(), line 8464. Draws the checklist for the current scope; sheet-ID vouchers start unticked. _qrTagToggle(cb), line 8480. One checkbox in or out of the print. _qrTagSetAll(on), line 8485. All / None for the checklist. _qrTagCount(), line 8489. Keeps the Print button honest about how many tags it makes. _qrTagSetScope(v), line 8495. Scope picker changed. qrTagDoc(recs, perPage), line 8498. Builds the print window: a letter-page grid of cut-apart tags, grouped under a collector and date heading. openQrTagModal(), line 8560. Opens the QR tags dialog, building it on first use. closeQrTagModal(), line 8598. Closes the QR tags dialog. _qrTagKeydown(e), line 8605. Escape closes the dialog. printQrTags(), line 8608. Prints the ticked vouchers in save order. The print window is opened inside the click so a pop-up blocker doesn't eat it. 5.11 Collection systems: Symbiota, Specify and DiSSCo (lines 8625 to 9518) _csTermURI(name), line 8703. The term URI for a column name: Dublin Core, Symbiota, or Darwin Core. _csCell(v), line 8709. One CSV cell for a machine file: control characters dropped, RFC 4180 quoting, nothing else. _csCSV(headers, rows), line 8715. A header and rows of objects as CSV text, LF line ends, trailing newline. _csTidy(v), line 8721. Trims and collapses runs of whitespace. _csFull(first, last), line 8723. "First Last" from two parts, skipping blanks. _csParseName(raw), line 8729. Splits a scientific name into genus, epithet, rank, infraspecific epithet and authorship. _csMeters(raw), line 8764. Reads "±4365 m", "10m", "0.5 km", "30 ft", "0.5 mi" as whole metres. _csQualifier(raw), line 8778. Ironfist's qualifier as a collection system writes it. determined is no qualifier. _csUSState(raw), line 8788. A U.S. state or territory by postal code or full name. { value, matched }. _csOrcid(raw), line 8800. A bare ORCID iD as its https://orcid.org/ URI; anything else passes through. _csSplitCollectors(raw), line 8807. additionalCollectors ("A B | C D") as [{ first, last, guess }]. _csPrepare(recIn), line 8820. The one reader. Every derived value both exports use, worked out once from a decorated record (addenda and current_* fields on it), plus the review flags. _csReviewRows(P, system), line 8970. review.csv rows for one prepared record, only the flags that apply to this system. _csReview(rows), line 8976. review.csv text, sorted fix, then check, then info; with the counts. _csSymbiotaOcc(P), line 8988. The occurrence core row for one prepared record, plus any added-term conflicts as flags. _csSymbiotaDets(P), line 9021. Identification extension rows for one prepared record, oldest first. _csMetaXml(occCols, detCols), line 9030. meta.xml for the archive: core and extension, one field element per column after the key. buildSymbiotaArchive(recs), line 9045. The Symbiota ZIP entries ([{ name, text }]) for these decorated records, and the review counts. _csSpecifyTaxonCols(n, h, family), line 9096. One determination's WorkBench columns as [header, value] pairs. n is 1 for the current one (no prefix, and Family), 2 and up for earlier ones ("Det 2 ..."). _csSpecifyRow(P, nColl, nDet, addedTerms), line 9117. One WorkBench row as [header, value] pairs, sized to the most collectors and determinations any exported record has, so every row has the same columns. _csSpecifyTarget(header), line 9145. Where a WorkBench column goes in Specify's stock schema: { table, field, note }. buildSpecifyWorkbench(recs), line 9208. The Specify ZIP entries ([{ name, text }]) for these decorated records, and the review counts. _odsClean(v), line 9301. Drops empty strings, nulls, empty arrays and objects left holding nothing but their @type, so the file carries no hollow keys. _odsNum(s), line 9313. A decimal string as a number, or undefined when it isn't one. _odsAgent(first, last, orcid, role, position), line 9315. One person as an openDS agent with a single role. _odsNameHTML(n), line 9325. ods:scientificNameHTMLLabel: genus and epithets italic, the × outside the italics, rank marker and authorship roman. _odsSpecimen(P), line 9340. One prepared record as an ods:DigitalSpecimen. buildOpenDS(recs), line 9430. The openDS ZIP entries ([{ name, text }]) for these decorated records, and the review counts. _csDownload(built, base, label), line 9488. Zips a builder's entries and downloads them, then reports the review counts. async exportSymbiota(ids), line 9496. Export for Symbiota: a Darwin Core Archive ZIP of these vouchers, or all of them. async exportOpenDS(ids), line 9502. Export for DiSSCo: an openDS Digital Specimen ZIP of these vouchers, or all of them. async exportSpecify(ids), line 9508. Export for Specify: a WorkBench data set ZIP of these vouchers, or all of them. 6.1 Toasts (lines 9519 to 9563) showUndoToast(message, undoFn), line 9531. Shows a message with an Undo button for a few seconds. hideUndoToast(), line 9541. Hides the Undo message. runUndo(), line 9548. Runs the pending undo, then hides the message. showNotice(message, isError), line 9554. Shows a short message; errors stay up longer. 6.2 Section state (lines 9564 to 9577) initSectionPersistence(), line 9568. Remembers which form sections are open or closed. 6.3 Theme (lines 9578 to 9593) toggleTheme(), line 9580. Switches light and dark theme and remembers it. initTheme(), line 9587. Applies the saved theme, or the system's. 6.4 Build name (lines 9594 to 9608) buildLabel(), line 9600. The short form the header chip wears, I.F.
. applyBuildName(), line 9602. Puts the build name in the tab title and the header stamp. 6.5 Workspace bar (lines 9609 to 9728) _wsHue(name), line 9619. A stable hue from the workspace name. I just know this is gonna bite me in the arse. _wsColor(name), line 9625. The workspace's color, or none for the default. _wsListLoad(), line 9627. The shared list of workspaces this browser has opened. _wsListRemember(name), line 9632. Adds a workspace to the shared list. _wsHref(name), line 9641. URL for a workspace: this same file, ?ws=name (none for default). _wsFavicon(), line 9643. Tab icon: the workspace's first letter on its color. renderWorkspaceBar(), line 9657. Paints the bar, the stripe and the icon for this workspace. _wsRenderList(), line 9673. The switcher list: default plus every named workspace, as plain links. toggleWorkspacePanel(force), line 9683. Opens or closes the workspace switcher. confirmWorkspaceSwitch(name), line 9693. New workspace: tidy the name, remember it, go there in this tab. guardWorkspaceLink(e), line 9705. Intercepts a click on a workspace link so the draft is flushed before the navigation takes the page away. openNewWorkspace(), line 9715. Reads the name box, normalises it to the lowercase-hyphen shape workspace names are limited to, and opens that workspace. 6.6 Same workspace, two tabs (lines 9729 to 9775) _wsBeat(), line 9744. Tells other tabs this workspace is open here. _wsWarnTwice(), line 9746. Warns that this workspace is also open in another tab. initWorkspacePresence(), line 9753. Starts the two-tabs check. 6.7 Session badge and top stamp (lines 9776 to 9823) renderSessionBadge(), line 9785. Shows the schema version and short device tag. renderTopStamp(), line 9793. Shows when this page was opened and the short device tag. async copySessionIdToClipboard(elId), line 9801. Copies the full device tag to the clipboard. 7.1 Storage resilience (lines 9824 to 9991) _persistAsked(), line 9884. Has this browser already been asked for persistent storage? _markPersistAsked(), line 9890. Notes that this browser has now actually answered the ask. requestPersistNow(), line 9895. The storage strip's button: asks this browser to keep the data. _persistWithTimeout(), line 9910. Resolves to null if the browser prompt goes unanswered, so nothing here hangs on it. async checkStorageResilience(opts), line 9917. Checks how safe local storage is here and shows a note if it is at risk. 7.2 Store reconciliation (lines 9992 to 10185) async reconcileStores(), line 10024. Compares saved vouchers against stored photos and reports orphans either way. async purgeOrphanPhotos(), line 10127. Deletes every photo with no voucher behind it, after confirming. async exportOrphanPhotos(), line 10151. Saves the orphaned photos to a ZIP before anyone deletes them. 7.3 Runtime tracer (?trace) (lines 10186 to 10300) _traceOn(), line 10220. True when the URL asks for ?trace. _traceWrap(name, fn), line 10225. Wraps a function so the tracer counts its calls and callers. _traceModule(moduleName, fns), line 10244. Wraps every function in a module object for the tracer. _traceInstall(), line 10251. Turns the tracer on for every global function (only with ?trace). _traceReport(), line 10283. What the tracer has seen so far. _traceDump(), line 10293. Prints the tracer report to the console. 7.4 Self test (?selftest) (lines 10301 to 10612) async runSelfTest(), line 10318. The ?selftest round trip: one fixture voucher through store, exports and back. mapTableForSelfTest(headers, rows), line 10595. Maps a parsed table's columns to FIELD_DEFS, for the self test's CSV re-import. 9.2 Compiler drawer: merge rules (lines 10959 to 11082) withCompileBatchId(recs), line 10967. Stamps this compile's batch id on each record. mergeAddendaInto(existing, incoming), line 10979. Folds a second copy's addenda into the kept record, deduplicated by addendum id, so a duplicate occurrenceID no longer loses its corrections. mergePhotoLists(target, winnerPhotos, otherPhotos), line 11005. Union of two photo manifests for one specimen. _stampMs(v), line 11026. A timestamp as ms, or null. mergeFieldsInto(existing, incoming, incomingStamp), line 11039. The field half of a same-occurrenceID merge: the newer copy's values win, photo lists are unioned, and the copy set aside is recorded as a conflict. 9.3 Compiler drawer: compile and display (lines 11083 to 11241) el(id), line 11085. Shorthand for document.getElementById inside the drawer. recordKey(r), line 11095. The merge key for a record: its occurrenceID, or its Voucher ID when it has none. mergeRecords(items, sourceLabel), line 11101. Merges one source's records into the compiled set, counting what changed. cleanRecords(), line 11146. The compiled records without the drawer's own bookkeeping fields. collectorsCount(), line 11154. How many distinct collectors are in the compiled set. photoRefNames(), line 11163. Every photo filename the compiled records point at. fingerprintDrift(), line 11178. The provenance question a reviewer actually asks about a drawer that holds its own private copy of everything: can what it exports disagree with the vouchers it read? render(), line 11189. Redraws the drawer's counts, messages and conflict list. 9.4 Compiler drawer: readers (lines 11242 to 11466) mapTable(headers, rows), line 11244. Maps table columns to FIELD_DEFS and returns records. parseJsonText(text), line 11264. Voucher records from JSON text. parseCsvText(text), line 11271. Voucher records from CSV text. parseHtmlText(text), line 11277. Voucher records from an exported HTML table. async _inflateRaw(bytes, expected), line 11300. Inflates one deflated ZIP entry, refusing a runaway one. async readStoredZip(buffer), line 11321. Reads an uncompressed ZIP into {path: bytes}. colIndex(ref), line 11364. A spreadsheet cell reference's column as a 0-based index. _xmlDoc(bytes, what), line 11370. Parses XML bytes, with a clear error if broken. _firstSheetPath(entries), line 11383. The path of the first worksheet in workbook order. _excelSerialToISO(v), line 11404. An Excel date serial as YYYY-MM-DD; other values pass through. async parseXlsxBuffer(buffer), line 11411. Voucher records from an XLSX file. async parsePhotoZip(buffer), line 11447. Loads photos and their manifest from a photo ZIP. 9.5 Compiler drawer: file intake (lines 11467 to 11532) async processFile(file), line 11469. Reads one dropped file by type: JSON, CSV, HTML, XLSX or photo ZIP. async loadFiles(files), line 11493. Reads every dropped file in turn, reporting failures without stopping. async addThisreport(), line 11509. THE SEAM. This function is the only place in this closure that reads anything belonging to the rest of the file: load() for the saved vouchers, getPhotosForVoucher() for their images. 9.6 Compiler drawer: exports (lines 11533 to 11612) saveBlob(blob, filename), line 11535. Saves a Blob with the workspace-prefixed name. exportCompilerJSON(), line 11542. Exports the compiled set as JSON. exportCompilerCSV(), line 11553. Exports the compiled set as CSV. exportCompilerHTML(), line 11558. Exports the compiled set as an HTML table. exportCompilerXLSX(), line 11569. Exports the compiled set as XLSX. exportCompilerPhotos(), line 11578. Exports the compiled set's photos as one ZIP. clearCompiler(), line 11607. Empties the drawer and starts a new batch id. 10 Pane shell (lines 11692 to 11937) isNarrow(), line 11712. True while the layout shows one pane at a time. currentPane(), line 11715. The pane showing on a phone: "1", "2" or "3". showPane(n), line 11718. Shows one pane on a phone. On a wide screen it just makes sure that pane is open. toggleSide(side), line 11729. The wedges. Narrow: swap between that pane and pane 2. paint(), line 11742. Keeps the tab states and the wedge arrows honest. revealElement(el), line 11767. Brings an element into view wherever it is parked: switches to its pane on a phone, reopens a collapsed pane on a wide screen, opens every