# Changelog

All notable changes to MRRC will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

### Changed

### Fixed

## [0.8.2] - 2026-06-09

### Added

- `mrrc.StaleFieldError` (subclass of `MrrcException`), raised when a field
  handle is used after fields were removed from its record.
- Coverage-guided fuzz targets for the JSON (`parse_json`, `parse_marcjson`),
  MARCXML (`parse_marcxml`), MODS (`parse_mods`), and MARC-8 (`decode_marc8`)
  read paths, run nightly alongside the existing binary-MARC targets.
- CI and `.cargo/check.sh` now run `stubtest` to verify `mrrc/_mrrc.pyi`
  matches the compiled extension, preventing type-stub drift.
- A documentation-build check runs on pull requests that touch `docs/`,
  `mkdocs.yml`, or the Python sources the API reference is generated from, so a
  broken docs build is caught before merge instead of only on the main deploy.
  The docs build now uses `mkdocs build --strict` everywhere (PR check, deploy,
  and `.cargo/check.sh`), turning broken links and unresolved mkdocstrings
  references into errors.
- A process-label lint (in `.cargo/check.sh` and CI) fails the build when
  source, tests, or non-CHANGELOG docs embed implementation-process labels
  (bead IDs, PR/issue numbers, or release-tagged claims like "since 0.8.1"),
  which belong in git history and the CHANGELOG instead.

### Changed

- The Python API reference is now generated from source with mkdocstrings
  instead of hand-maintained signature tables, so it cannot drift from the
  code. The page also gained reference sections for the query DSL
  (`FieldQuery`, `TagRangeQuery`, `SubfieldPatternQuery`,
  `SubfieldValueQuery`) and the parallel-processing classes.
- Documentation fixes: the BIBFRAME-to-MARC guide example now uses the real
  `RdfGraph.parse(...)` API (it previously referenced a nonexistent
  `BibframeGraph.from_turtle`), and docstring examples are fenced so they
  render as code blocks rather than leaking headings into the reference.
- Reader option arguments (`recovery_mode`, `validation_level`, `max_errors`)
  are now keyword-only on `MARCReader`, `AuthorityMARCReader`, and
  `HoldingsMARCReader`, matching the documented signatures; pass them by name.
  The record source remains the first positional argument.
- The `mrrc/_mrrc.pyi` type stubs now match the compiled extension exactly:
  missing members were added and entries absent at runtime were removed, so
  stub-driven type checking reflects the real API.
- CI PR workflows add cancel-in-progress concurrency groups and per-job
  timeouts, skip the heavy wheel and benchmark jobs on Dependabot pull requests
  (lint, tests, and semver still run), and share one `rust-cache` configuration
  across the Rust compile jobs.
- Documentation now states explicitly that CSV and Dublin Core are write-only
  (export) formats: both are lossy projections of a MARC record, so MRRC emits
  them but does not parse them back into MARC.
- Documentation no longer presents specific throughput figures or pymarc
  multipliers as current measurements; early benchmark results are summarized
  with the caveat that they need re-measurement, and the benchmark docs now
  describe the measurement infrastructure and the procedure for producing
  citable numbers.
- The Python CodSpeed CI job now runs in simulation mode (previously walltime),
  making PR performance-regression detection deterministic on hosted runners.
  Parallel-throughput benchmarks (Python and Rust) are excluded from CodSpeed,
  since Valgrind serializes threads; they remain runnable locally via pytest
  and criterion.
- CI Windows jobs are pinned to `windows-2025` (previously `windows-latest`)
  ahead of GitHub's 2026-06-15 redirect of that label to a new image.
- Pull requests now build and test a slim wheel matrix (8 jobs) instead of the
  full 30; the full `os × python` matrix still runs on push to main and on
  release tags. The standalone Rust `build.yml` workflow is removed; its
  examples-compile step moved into the test workflow, and cross-OS core
  compilation remains covered by the maturin wheel matrix. `.cargo/check.sh`
  gains a `--release` flag to build the Python extension in release mode for
  perf-sensitive debugging.
- Continuous integration trims redundant work: the Python pull-request
  benchmark matrix (`pytest-benchmark` across five Python versions) is dropped
  in favor of the CodSpeed regression gate, CHANGELOG linting now runs in CI,
  and commits that touch only `.beads/**` no longer trigger the test, benchmark,
  wheel, and lint workflows.
- `.cargo/check.sh` now compiles `examples/` and benchmarks (`cargo bench
  --no-run`) in its full pre-push run, catching breakage that previously
  surfaced only in CI.

### Removed

- The Python `Record.fields_by_tag(tag)` method. Use the pymarc-standard
  `get_fields(tag)`, which returns the same live-handle fields for a data tag
  and also accepts multiple tags and control (00X) tags. The Rust
  `Record::fields_by_tag` is unchanged.

### Fixed

- `Record.remove_field(field)` now removes exactly the given field instead of
  every field sharing its tag, matching pymarc; detached fields match by value
  and raise `ValueError` when absent. `remove_field`/`remove_fields` with a
  control tag now remove the control field (previously a silent no-op).
  `fields()` now enumerates repeated control tags (006/007) identically to
  `get_fields()`. (#248)
- Python in-place field edits now persist to the record, matching pymarc.
  Fields obtained from a record (`record[tag]`, `get_field`, `get_fields`,
  `fields`) are live handles: indicator and subfield edits, and control-field
  `.data` assignment, write through to the record. (#241)
- Corrected documentation to match the current API. The Rust API-reference
  "Key Methods" tables now use real method names, distinguish public fields
  from methods, and show accurate return types; the `AuthorityRecord` /
  `HoldingsRecord` examples use the real `::builder(leader)` API. Several Rust
  and Python tutorial/reference examples were fixed (query DSL types,
  `subfields_by_code`, error matching, batch parsing, `get_fields` for
  pymarc-style access, `record.leader()`). Removed documentation for MARC-8
  *output*, which is not supported — MRRC writes UTF-8. Reported by @acdha.

### Dependencies

- Bump ruff from 0.15.12 to 0.15.15
- Bump syrupy from 5.1.0 to 5.3.1
- Bump pyright from 1.1.409 to 1.1.410
- Bump astral-sh/setup-uv from 7 to 8.1.0
- Bump CodSpeedHQ/action from 4 to 4.17.0
- Bump actions/checkout from 6 to 6.0.3
- Bump codecov/codecov-action from 6 to 6.0.1

## [0.8.1] - 2026-05-29

### Added

- `get_field(tag)` and `get_field_or_err(tag)` accessors on all three record
  types (`Record`, `AuthorityRecord`, `HoldingsRecord`), in both Rust and
  Python. `get_field` returns the first matching field as `Option<&Field>` /
  `None`; `get_field_or_err` raises `mrrc.FieldNotFound` (E105) with
  `field_tag` and `record_control_number` populated. Existing `get_fields` is
  unchanged.
- `max_errors` kwarg on Python `MARCReader`. Caps the total recovered errors
  across a `lenient` / `permissive` stream; the read after the (N+1)-th raises
  `mrrc.FatalReaderError` (E099). `None` (default) and `0` both disable the
  cap. Inert in strict mode.
- Documented pymarc 5.3.1 exception-class-name parity in the error-handling
  reference and the migration guide: the mapping table, the names mrrc
  deliberately omits, the known hierarchy divergences, and porting recipes.
- `MARCReader.current_exception` and `MARCReader.current_chunk` —
  pymarc-compatible accessors. After each `__next__`, `current_chunk` holds
  the bytes just read and `current_exception` holds the exception swallowed
  under `permissive=True` (or `None` on a clean read). See the migration guide
  for the encoding-strictness divergence.
- New `validation_level` reader kwarg (`"structural"` default,
  `"strict_marc"`), orthogonal to `recovery_mode` and applied uniformly across
  all three readers. At `strict_marc`, indicator bytes (E201), subfield-code
  bytes (E202), and UTF-8 decoding (E301) enforce MARC 21 byte-level rules; at
  `structural` those bytes are accepted as-is and invalid UTF-8 falls back to
  `U+FFFD`.
- Per-record diagnostics. `record.errors` (Rust + Python) carries the typed
  exceptions for non-fatal defects recovered in `lenient` / `permissive` modes
  (always empty in `strict`). `MARCReader.iter_with_errors()` yields `(record,
  errors)` tuples, and under `permissive=True` yields `(None, [exception])` so
  unsalvageable records stay observable. `record.errors` is on all three
  readers; `iter_with_errors` is bibliographic-only.
- Strict-mode parsing now verifies that the byte at the leader's claimed
  end-of-record position is `RECORD_TERMINATOR` (0x1D); a different byte fires
  `EndOfRecordNotFound` (E006). Previously the byte was unchecked and a
  malformed record with the wrong terminator parsed silently. Lenient and
  permissive modes are unchanged — the recovery cap continues to absorb the
  disagreement via existing directory/field paths.
- `validation_level="strict_marc"` also runs MARC 21 semantic checks: per-tag
  indicator rules (e.g. 245 first indicator must be `0` or `1`) and
  leader-byte semantics, firing E201 and E002 respectively (both recoverable
  in `lenient` / `permissive`). `IsbnValidator` / `EncodingValidator` remain
  opt-in helpers; see the validators reference.
- `MarcError` now implements `Clone` (Rust), enabling inspection of recovered
  errors on `record.errors` after lenient parsing. Clone is lossy for the
  three variants wrapping foreign causes (`IoError`, `XmlError`, `JsonError`):
  it preserves the rendered message but drops the non-string inner cause.
- `SubfieldPatternQuery` now exposes its regex via a `pattern` getter (Rust
  `SubfieldPatternQuery::pattern()`), and its `repr()` includes the pattern —
  `<SubfieldPatternQuery tag=084 subfield=a pattern="^abc">` — so the most
  useful field for debugging a query is recoverable from the REPL or logs. The
  `tag` and `subfield_code` getters are now also declared in the type stubs.
  Thanks to @acdha (#226).

### Changed

- Leader errors from the MARCXML, JSON, and marcjson readers now carry
  `record_index` (previously stripped) — identifying the failing record in a
  multi-record collection, or `1` for single-record APIs. Affects E001–E004
  from these paths; the ISO 2709 path was already enriched.
- `MarcError::IoError` (E007) raised mid-record — when the underlying source
  fails while reading a record's data area — now carries `record_index`,
  `byte_offset`, and `source_name`, instead of the context-free
  `From<io::Error>` fallback that left them `None`. I/O failures at a record
  boundary (before a record is in progress) stay context-free by design;
  Python's `OSError` surface is unchanged.
- Retired the internal cumulative-budget perf-gate CI workflow added during
  the error-handling epic — it served its purpose and the cumulative
  v0.8.0→v0.8.1 hot-path cost stayed negligible. Codspeed continues general
  perf tracking, and the benchmarks still run locally.
- Python `MARCReader` / `AuthorityMARCReader` / `HoldingsMARCReader` now
  default to `recovery_mode="permissive"` (was `"strict"`), matching the
  pymarc / marc4j convention — a fresh reader iterates past per-record defects
  instead of aborting on the first. The Rust core's `mrrc::MarcReader` keeps
  `Strict`. Pass `recovery_mode="strict"` explicitly for the old behavior; the
  `permissive=True` pymarc-compat path is unchanged. See the error-handling
  guide for the trade-offs.
- Leader-validation errors now fire the field-specific variants their
  documentation describes: `RecordLengthInvalid` (E001) for non-digit bytes
  0-4 or `record_length < 24`, `BaseAddressInvalid` (E003) for non-digit bytes
  12-16 or `data_base_address < 24`, and `BaseAddressNotFound` (E004) for
  `data_base_address > record_length`. Previously all of these collapsed to
  `InvalidLeader` (E002) or, for E004, fell through to a misleading
  `InvalidField` from directory parsing.
- A non-digit byte in a directory entry's length or start-position field now
  fires `DirectoryInvalid` (E101) with `field_tag` and a precise
  `byte_offset`, instead of the misleading `InvalidField` (E106) it previously
  forwarded.
- The performance-tuning, migration-from-pymarc, and working-with-large-files
  guides now point to the Query DSL guide where field filtering is discussed,
  so readers discover the indicator/range/pattern/subfield matching path.
  Thanks to @acdha (#234).
- The Rust examples throughout the docs (quickstart, tutorials, reference) now
  match the real API and compile: `field.get_subfield('a')` (char), the public
  `tag` / `indicator1` / `indicator2` fields, the `FieldQuery::new()` builder
  with `record.fields_matching(&query)`, `Leader::from_bytes(...)`, the
  `record_to_json` / `record_to_marcxml` / `record_to_marcjson` conversion
  functions, and `use mrrc::RecordHelpers;` for `record.title()`. Many
  previously referenced methods that don't exist. Reported by @acdha (#233).

### Fixed

- Two out-of-bounds slice panics in the lenient/permissive recovery path:
  salvage attempts no longer crash when a directory entry's `start_position`
  lies past the buffer (now bailing out of the salvage branch), and the
  control-field decode path now guards against zero-length directory entries
  (where `end_position == start_position`) before invoking `saturating_sub(1)`
  on the slice end. Surfaced by the recovery-mode-consistency fuzz target.
- `MarcReader` rejects non-ASCII bytes in directory entry tags (firing
  `DirectoryInvalid` / E101) instead of lossily substituting `U+FFFD` and
  producing records whose tag re-encodes to more than 3 bytes. `MarcWriter`
  and the authority/holdings writers also refuse records whose tags aren't 3
  ASCII bytes, returning `WriterError` (E404). Surfaced by the
  error-classification fuzz target's round-trip assertion.
- `TruncatedRecord` (E005) now surfaces on `record.errors` in `lenient` and
  `permissive` modes instead of being silently swallowed (it had been
  cascading into a misleading E201). Strict mode is unchanged.
- Python `mrrc.MARCReader` now honors `recovery_mode` on short body reads: a
  truncated body no longer raises `TruncatedRecord` (E005) before the
  recovery-aware parser runs, so in `lenient` / `permissive` it lands on
  `record.errors` instead. Strict mode still raises.
- Release workflow now attaches wheel assets to the GitHub Release page
  automatically. Previously, `actions/checkout` ran after `download-artifact`
  and wiped `dist/` before the gh-release step, leaving the release page with
  notes but zero assets. Steps reordered so checkout runs first.
- `MarcWriter` and the authority/holdings writers now refuse records whose
  serialized length or base address exceeds the ISO 2709 5-digit limit (99999
  bytes), returning `WriterError` (E404) with positional context — instead of
  silently emitting an unparseable leader (or, for holdings, the wrong
  `InvalidField` variant).
- `mrrc.MARCWriter.write_record` now raises the typed `mrrc.WriterError`
  (E404) instead of a plain `OSError`; the binding had been collapsing every
  writer `MarcError` into `io::Error` across the FFI boundary.
- `InvalidLeader` (E002) errors now carry the full positional context
  (`record_index`, `byte_offset`, `record_byte_offset`, `source_name`) the
  v0.8.0 error work promised; the leader-validation path had been building the
  variant message-only and discarding every positional field.
- `TruncatedRecord` (E005) raised across the Python FFI now preserves the same
  positional context (`record_index`, `byte_offset`, `record_byte_offset`) the
  Rust core attaches; Python callers previously saw a wrong `actual_length`
  and missing stream-position metadata.
- `AuthorityMarcReader` and `HoldingsMarcReader` at
  `validation_level="strict_marc"` no longer trip `InvalidLeader` (E002) on
  leader bytes valid for their own record type. Each reader now applies its
  own MARC 21 format's leader rules (Authority / Holdings) rather than the
  Bibliographic allowed-value sets; bibliographic dispatch is unchanged.

### Dependencies

- Bump urllib3 from 2.6.3 to 2.7.0
- Bump mypy from 1.20.2 to 2.1.0

## [0.8.0] - 2026-04-29

### Breaking

- **`MarcError` variants restructured to struct form** carrying
  positional metadata (`record_index`, `byte_offset`,
  `record_byte_offset`, `record_control_number`, `field_tag`,
  `indicator_position`, `subfield_code`, `found`, `expected`,
  `source_name`). Wrapping variants (`IoError`, `XmlError`, `JsonError`)
  chain via `#[source]` so `Error::source()` walks correctly; `Display`
  output changed for every variant. Direct constructors and pattern
  matches need updating. **Migration:** match on `.code()` (`"E001"`–
  `"E099"`, `"E101"`+) instead of variant names — codes are stable
  across future enum changes.
- **`MarcError::InvalidRecord` removed.** Use the new specific variants
  (`DirectoryInvalid`, `RecordLengthInvalid`, …) or `InvalidField` as
  the fall-through.
- **`MarcError::ParseError` removed.** Same migration as
  `InvalidRecord`: the new specific variants and `InvalidField`
  cover the cases that `ParseError` used to wrap. Match on `.code()`
  for stable identity.
- **`recovery::RecoveryContext` removed from the public re-exports.**
  No external consumer; the type was an internal helper that
  accumulated messages and dropped them. Pattern-match on
  `MarcError` instead.
- **`recovery::try_recover_record` takes a `&ParseContext`** as a
  fifth parameter. The previous v0.7 signature constructed
  `RecoveryContext` internally; v0.8 callers pass the
  `ParseContext` that carries record-index / byte-offset state.

### Added

- **Structured positional context on every parse error** — record
  index, absolute and record-relative byte offsets, 001 control number,
  field tag (and indicator position or subfield code where applicable),
  offending bytes (capped at 32), and source filename. New
  `MarcError::detailed()` multi-line diagnostic includes a hex-dump of
  the byte window around the error offset; the default `Display` is a
  one-liner. Both produce byte-for-byte identical output across Rust
  and Python via matching `_format()` / `detailed()` methods on every
  Python exception class.
- **Typed Python exception subclasses** for every `MarcError` variant
  (e.g. `InvalidIndicator`, `BadSubfieldCode`, `TruncatedRecord`,
  `XmlError`, `JsonError`, `WriterError`, `FatalReaderError`)
  extending the closest pymarc-named parent so existing pymarc-style
  `except` clauses keep catching the same conditions. Each carries
  positional kwargs, supports pickle round-trip, and survives the
  PyO3 boundary with all attributes intact.
- **Stable error codes** (`E001`–`E007`, `E099`, `E101`, `E105`,
  `E106`, `E201`, `E202`, `E301`, `E401`, `E402`, `E404`) on every
  variant via `MarcError::code()` / `slug()`, with matching `code` /
  `slug` / `help_url()` on every Python class. Codes never get
  renumbered (policy in `CONTRIBUTING.md`); `MRRC_DOCS_BASE_URL`
  overrides the help-URL host. All codes documented in
  `docs/reference/error-codes.md`.
- **Structured error serialization** via `to_dict()` / `to_json()` on
  every Python exception (suitable for ELK / Datadog / Splunk) and
  `MarcError::to_json_value()` / `to_json()` on the Rust side; bytes
  fields hex-encoded, `_cause` flattens the exception chain,
  `schema_version: 1` for forward-compat. New `BytesNear` public
  struct exposes the captured byte window.
- **`with_source()` / `from_path()` builder methods** on `MarcReader`,
  `AuthorityMarcReader`, and `HoldingsMarcReader`. `from_path` populates
  the source name from the file path so emitted errors carry the
  filename.
- **Per-stream recovered-error cap**
  ([#110](https://github.com/dchud/mrrc/issues/110)) via
  `with_max_errors(n)` on all three readers. In `Lenient` /
  `Permissive` modes the reader counts each recovered failure and
  halts with `MarcError::FatalReaderError` (E099) once the cap
  (default 10000) trips. Pass `0` to disable.
- **Per-field lenient/permissive recovery parity across all three
  ISO 2709 readers** ([#121](https://github.com/dchud/mrrc/issues/121),
  [#122](https://github.com/dchud/mrrc/issues/122)).
  `AuthorityMarcReader` and `HoldingsMarcReader` now honor the same
  per-field error sites the bibliographic reader does (bad
  field-length / start-position digits, field-extends-past-data,
  data-field-too-short-for-indicators, `parse_data_field` failure).
  Strict-mode behavior unchanged at every site; lenient/permissive
  mode skips the offending entry, counts the recovery against the
  per-stream cap, and continues. Truncated-record dispatch in
  lenient/permissive mode no longer returns a hard error on a short
  read; instead it notes the recovery and falls through to
  best-effort directory parsing.
- **8 property tests** (`tests/properties.rs`) covering binary,
  MARCXML, and MARCJSON round-trips plus four ISO 2709 structural
  invariants (leader length, directory tiling, indicator byte set,
  subfield code shape). `ProptestConfig { cases: 64 }`; full suite
  ~3s locally. Primer at `docs/contributing/formal-methods.md`
  ([#111](https://github.com/dchud/mrrc/issues/111)).
- **Coverage-guided fuzzing**
  ([#90](https://github.com/dchud/mrrc/issues/90),
  [#115](https://github.com/dchud/mrrc/issues/115)). Standalone
  `fuzz/` Cargo workspace with cargo-fuzz; nightly CI matrix runs
  `parse_record` (full reader) and `roundtrip_binary` (reader →
  writer → reader coupling) for 5 minutes daily at 03:00 UTC.
  Findings are not a PR gate; reproducers get copied into
  `tests/data/fuzz-regressions/` to run on every PR. Triage playbook
  in `docs/contributing/fuzzing.md`.
- **`docs` extra in `pyproject.toml`** for mkdocs site dependencies;
  CI builds via `uv sync --extra docs --no-install-project` so the
  docs-only job doesn't need a Rust toolchain.

### Changed

- **Shared ISO 2709 parsing primitives + generic skeleton**
  ([#125](https://github.com/dchud/mrrc/issues/125)). `src/iso2709.rs`
  owns the leader read, truncation-aware record-data read,
  single-entry directory parsing, ASCII numeric helpers, and the
  control-field-tag predicate. New `iso2709_skeleton::Iso2709Builder`
  trait + `parse_iso2709_record<R, B>` skeleton drives one record's
  parse end-to-end; each reader implements `Iso2709Builder` via a
  small adapter (~60 lines each) so `read_record` collapses to a
  one-line dispatch. ~200 lines of near-duplicate `read_record` body
  across three readers replaced by one shared implementation. New
  `recovery::RecoveryCap` struct consolidates the per-stream cap
  state machine that had been duplicated. Behavior changes that
  fall out of the unification: `AuthorityMarcReader` now treats
  too-short data fields as a strict-Err / lenient-skip event with
  cap accounting (was silently skipped in all modes);
  `HoldingsMarcReader`'s field-extends-beyond-data lenient branch now
  salvages a clamped slice (matching bib); the holdings "field
  exceeds data" error message changed wording to match the other two
  readers. The skeleton is `<R: Read, B: Iso2709Builder>` with no
  `dyn` dispatch so trait calls monomorphize and inline at every
  call site, preserving hot-loop characteristics. Per-type quirks
  (authority's tag UTF-8 strictness + trailing `0x1F` trim, holdings'
  strict UTF-8 on control fields) preserved via trait-method
  overrides; the wider strict-vs-lossy unification is tracked
  separately as a follow-up.
- **`HoldingsMarcReader::with_max_errors`** is now active. Originally
  landed inert when the cap was introduced (no recovery sites in the
  holdings path); the recovery sites added in this cycle hook into it.
- **`MarcError` source-error chain walks correctly** for `IoError`,
  `XmlError`, `JsonError` (was previously empty). Code that was
  checking `Error::source() == None` may need updating.
- **Pinned Rust toolchain to 1.95.0**
  ([#96](https://github.com/dchud/mrrc/issues/96),
  [#97](https://github.com/dchud/mrrc/pull/97)) via
  `rust-toolchain.toml`. Library MSRV
  (`Cargo.toml` → `rust-version = "1.71"`) is unchanged.
- **CI: skip workflows on docs-only changes.** Added `paths-ignore`
  for `**.md`, `docs/**`, `mkdocs.yml`, `LICENSE`, and `.gitignore` to
  `lint`, `test`, `build`, `python-build`, `benchmark-python`, and
  `benchmark-rust` workflows. A docs-only push or PR previously fired
  ~47 jobs across the six workflows; it now fires zero. Mixed PRs
  (code + docs) still run normally — `paths-ignore` skips only when
  every changed path matches.
- **`.cargo/check.sh` builds the docs site** (`mkdocs build`) in full
  mode, surfacing broken cross-links pre-push. Skipped under `--quick`.
  Requires the `docs` extra (`uv sync --all-extras`).
- **mkdocs warnings cleanup.** Excluded `docs/history/` (archival per
  CLAUDE.md) from the published site, removed its nav entry, and
  fixed broken cross-links and a stale anchor in active docs. Build
  warnings down from 18 to 4.

### Fixed

- **Python typing fidelity improvements.** Stub gaps in `mrrc/_mrrc.pyi`
  closed (`parse_batch_parallel` / `parse_batch_parallel_limited`
  signatures, `Field.delete_subfield`, `Record.to_marc21`,
  module-level `__version__`) and several wrapper-side narrowing
  issues fixed. `mypy mrrc/` and `pyright mrrc/` now both report zero
  errors and run in `.cargo/check.sh` full mode. Type-only — no
  runtime behavior change.
- **MARCXML reader: missing XML 1.1 §2.11 end-of-line normalization
  in text and CDATA content**
  ([#112](https://github.com/dchud/mrrc/issues/112)). Switched both
  arms of `read_leaf_text` from `decode()` to `xml_content()` so
  CR / CRLF / NEL / LSEP normalize to LF per spec. Domain impact is
  small (MARC field content rarely carries line separators) but the
  divergence from spec was real.
- **Read-path performance regression from structured-error refactor**
  ([#117](https://github.com/dchud/mrrc/issues/117)). The
  `ParseContext` refactor stopped the compiler from inlining
  `parse_data_field`, costing ~15-17% on read hot paths vs v0.7.6
  (investigated in
  [#116](https://github.com/dchud/mrrc/issues/116)). Restored to
  within +9-11% of baseline via `#[inline(always)]` on
  `parse_data_field` paired with shrinking
  `ParseContext::current_field_tag` to `Option<[u8; 3]>`. The
  compact context is what lets forced inlining avoid ballooning L1-i
  cache usage on parallel workloads.
- **CI: Clippy `collapsible_match` errors** in
  `src/bibframe/converter.rs` after Rust stable advanced to 1.95.0
  ([#96](https://github.com/dchud/mrrc/issues/96),
  [#97](https://github.com/dchud/mrrc/pull/97)).
- **CI: ASAN job** failed with `-Zbuild-std` on stable; sets
  `RUSTUP_TOOLCHAIN=nightly` on the ASAN step
  ([#105](https://github.com/dchud/mrrc/pull/105)).
- **CI: Miri job** could not run `insta` snapshot tests; setting
  `INSTA_WORKSPACE_ROOT` skips the `cargo metadata` spawn
  ([#106](https://github.com/dchud/mrrc/pull/106)).
- **CI: docs deploy** broke when [#130](https://github.com/dchud/mrrc/pull/130)
  switched the install line to build mrrc itself; now uses
  `uv sync --extra docs --no-install-project`
  ([#131](https://github.com/dchud/mrrc/pull/131)).

### Dependencies

- Bump ruff from 0.15.11 to 0.15.12
- Bump mypy from 1.20.1 to 1.20.2
- Bump pyright from 1.1.408 to 1.1.409

## [0.7.6] - 2026-04-14

### Added

- **`MARCReader` error-handling flags** ([#78](https://github.com/dchud/mrrc/issues/78), [#80](https://github.com/dchud/mrrc/pull/80)): pymarc-compatible kwargs for `MARCReader`:
  - `to_unicode`: accepted for compatibility, warns if `False` (mrrc always converts to Unicode).
  - `permissive`: yields `None` for bad records instead of raising, matching pymarc behavior.
  - `recovery_mode`: exposes mrrc's `RecoveryMode` for salvaging partial data from malformed records.
  - Thanks to [@acdha](https://github.com/acdha) for filing [#78](https://github.com/dchud/mrrc/issues/78).

### Fixed

- **Repeated control fields silently dropped** ([#77](https://github.com/dchud/mrrc/issues/77), [#79](https://github.com/dchud/mrrc/pull/79)): Control fields (e.g., multiple 007s) were stored in `IndexMap<String, String>`, causing later values to overwrite earlier ones. Changed to `IndexMap<String, Vec<String>>` across all three record types (`Record`, `AuthorityRecord`, `HoldingsRecord`). Updated all 7 serialization formats (ISO 2709, JSON, MARCJSON, MARCXML, CSV, Dublin Core/MODS, BIBFRAME) to emit all values. The pymarc-compatible `get_fields()` API now correctly returns all repeated control fields, matching pymarc behavior. Thanks to [@acdha](https://github.com/acdha) for reporting.
- **`delete_subfield()` did not actually delete** ([#81](https://github.com/dchud/mrrc/issues/81), [#82](https://github.com/dchud/mrrc/pull/82)): Was returning the value without removing the subfield. Added Rust `Field::delete_subfield()` method, exposed via PyO3, and updated the Python wrapper to delegate. Now matches pymarc behavior.
- **`recovery_mode` validation for authority/holdings readers** ([#82](https://github.com/dchud/mrrc/pull/82)): Authority and holdings readers now reject invalid `recovery_mode` values with `ValueError` instead of silently defaulting to strict.

### Changed

- **Simplified control field API**: Removed `get_control_field_values()` accessor from `Record` — redundant with direct access to the public `control_fields` map. `get_control_field()` remains as a convenience for non-repeatable tags (001, 003, 005, 008).
- **Reduced code duplication across Python bindings and Rust core** ([#82](https://github.com/dchud/mrrc/pull/82)):
  - Extracted `_wrap_field()` and `_wrap_record()` helpers in the Python wrapper, replacing 16+ inconsistent wrapping patterns.
  - Extracted shared `reader_helpers.rs` for PyO3 reader backends, reducing ~225 lines of copy-paste between authority and holdings readers.
  - Extracted `control_field_char_at()` utility for 008 field parsing, replacing 5 duplicated patterns across authority and holdings record types.

### Dependencies

- Bump ruff from 0.15.8 to 0.15.10
- Bump mypy from 1.19.1 to 1.20.1
- Bump pytest from 9.0.2 to 9.0.3
- Bump softprops/action-gh-release from 2 to 3
- Bump actions/upload-pages-artifact from 4 to 5

## [0.7.5] - 2026-04-05

### Added

- **`Record.get(tag)` for pymarc compatibility**: Dict-like `record.get('245')` returns the first matching field or a default value, mirroring pymarc's `Record.get()`. Delegates to existing `get_field()`.
- **`Field.is_control_field()` and `ControlField.is_control_field()` for pymarc compatibility**: Returns `False` on data fields and `True` on control fields, matching pymarc's unified `Field.is_control_field()` API.
- **`Record.__str__` and `Record.__repr__`**: The Python `Record` wrapper now delegates to the Rust implementation instead of showing the default `<mrrc.Record object at 0x...>`. `str(rec)` returns `Record(type=a)` and `repr(rec)` returns `<Record type=a fields=N>`.
- **`SubfieldPatternQuery` negation flag** ([#63](https://github.com/dchud/mrrc/issues/63), [#73](https://github.com/dchud/mrrc/pull/73)): `SubfieldPatternQuery("020", "a", r"^978-", negate=True)` pushes pattern inversion into Rust so non-matching fields never cross the FFI boundary. Thanks to [@acdha@code4lib.social](https://code4lib.social/@acdha) for suggesting this.
- **`SubfieldValueQuery` negation flag** ([#64](https://github.com/dchud/mrrc/issues/64), [#74](https://github.com/dchud/mrrc/pull/74)): `SubfieldValueQuery("650", "a", "History", negate=True)` pushes value inversion into Rust. Works with both exact and partial matching.
- **Comprehensive pymarc API compatibility** ([#71](https://github.com/dchud/mrrc/issues/71), [#72](https://github.com/dchud/mrrc/pull/72)): Full drop-in replacement for pymarc. Thanks to [@mistersql@mastodon.social](https://mastodon.social/@mistersql) for highlighting several of these compatibility gaps. Changes include:
  - **Record accessors as `@property`**: All 17 record accessors (`title`, `author`, `isbn`, `issn`, `subjects`, `location`, `notes`, `publisher`, `uniform_title`, `sudoc`, `issn_title`, `issnl`, `pubyear`, `series`, `physical_description`, plus aliases `physicaldescription`, `uniformtitle`, `addedentries`) are now properties, matching pymarc's `record.title` syntax.
  - **Unified `ControlField` into `Field`**: `Field('001', data='12345')` creates a control field. `ControlField` remains as a backward-compatible subclass. Control field content accessed via `.data` attribute.
  - **`Record['xxx']` raises `KeyError`** for missing tags (use `record.get(tag)` for safe access), matching pymarc behavior.
  - **`Record.as_marc()` / `as_marc21()`**: Returns ISO 2709 bytes.
  - **`Record.as_json()` / `as_dict()`**: pymarc-compatible MARC-in-JSON serialization.
  - **`Field.value()`**: Space-joined subfield values.
  - **`Field.format_field()`**: Human-readable text representation.
  - **`Field.as_marc()` / `as_marc21()`**: Field-level binary serialization.
  - **`Field.add_subfield(code, value, pos=N)`**: Positional insert support.
  - **`Field.linkage_occurrence_num()`**: Extract $6 linkage info.
  - **`Field.convert_legacy_subfields()`**: Classmethod for old flat-list format.
  - **`add_field(*fields)`**: Accepts multiple fields at once.
  - **`remove_field(*fields)`**: Accepts multiple fields, returns None.
  - **`remove_fields(*tags)`**: Bulk removal by tag.
  - **`add_ordered_field(*fields)`**: Tag-sorted insert.
  - **`add_grouped_field(*fields)`**: Insert after same-tag group.
  - **`parse_xml_to_array()`**, **`parse_json_to_array()`**, **`map_records()`**: Module-level convenience functions.
  - **MARC constants**: `LEADER_LEN`, `DIRECTORY_ENTRY_LEN`, `END_OF_FIELD`, `END_OF_RECORD`, `SUBFIELD_INDICATOR`, `MARC_XML_NS`, `MARC_XML_SCHEMA`.
  - **Exception hierarchy**: `MrrcException` base class with `RecordLengthInvalid`, `RecordLeaderInvalid`, `BaseAddressInvalid`, `BaseAddressNotFound`, `RecordDirectoryInvalid`, `EndOfRecordNotFound`, `FieldNotFound`, `FatalReaderError`, and `BadSubfieldCodeWarning`.
  - **`pubyear` returns `str`** (not `int`), matching pymarc.

### Breaking Changes

- **Record accessors are now `@property`**: `record.title()` → `record.title`. Affects all 17 accessors: `title`, `author`, `isbn`, `issn`, `issn_title`, `issnl`, `subjects`, `notes`, `location`, `series`, `sudoc`, `publisher`, `pubyear`, `physical_description`, `uniform_title`, plus new `physicaldescription`, `uniformtitle`, `addedentries`.
- **`pubyear` returns `str`**: Previously returned `Optional[int]`, now returns `Optional[str]` to match pymarc.
- **`Record['xxx']` raises `KeyError` for missing tags**: Previously returned `None`. Use `record.get(tag)` for safe access that returns `None`.
- **`ControlField` unified into `Field`**: `ControlField` is now a subclass of `Field`. Control field content is in `.data` (not `.value`). `Field('001', data='12345')` is the preferred constructor.
- **`remove_field()` returns `None`**: Previously returned a list of removed fields.
- **`add_field()` / `remove_field()` accept `*args`**: Signature changed from single field to `*fields`. Existing single-arg calls still work.
- **Default Field indicators changed from `'0'` to `' '`**: Matches pymarc. Only affects fields created without explicit indicators.

### Changed

- **`SubfieldPatternQuery` and `SubfieldValueQuery` marked `#[non_exhaustive]`** ([#74](https://github.com/dchud/mrrc/pull/74)): Prevents external struct literal construction, requiring use of constructors. Future field additions will not be semver-breaking.

### Dependencies

- Bump pygments from 2.19.2 to 2.20.0 ([#67](https://github.com/dchud/mrrc/pull/67))
- Bump ruff from 0.15.4 to 0.15.8
- Bump actions/deploy-pages from 4 to 5
- Bump codecov/codecov-action from 5 to 6

### Fixed

- **`mrrc.__version__` now reports the correct version**: Previously hardcoded as `"0.1.0"` in both `mrrc/__init__.py` and `src-python/src/lib.rs`. Now derived from `Cargo.toml` at compile time via `env!("CARGO_PKG_VERSION")`, eliminating the need to manually update version strings during releases.

### Documentation

- **Migration guide updated**: Added `record.get()` and `field.is_control_field()` examples to `docs/guides/migration-from-pymarc.md`, reflecting closer pymarc API parity.
- **All docs and examples updated for pymarc API compatibility**: Record accessors now shown as properties (`record.title` not `record.title()`), control field access uses `.data`, missing-field access patterns use `record.get()` or try/except, and new methods/constants/exceptions are documented throughout the API reference, migration guide, quickstart, tutorials, and runnable examples.

## [0.7.4] - 2026-03-04

### Added

- **Proptest binary round-trip property test** ([#39](https://github.com/dchud/mrrc/issues/39), [#42](https://github.com/dchud/mrrc/pull/42)): New `tests/properties.rs` uses proptest to generate arbitrary structurally valid MARC records and verify that serialization to ISO 2709 and parsing back produces identical results. Covers leader fields, control fields, and data fields with indicators and subfields.
- **Cargo-semver-checks CI workflow** ([#37](https://github.com/dchud/mrrc/issues/37), [#40](https://github.com/dchud/mrrc/pull/40)): New `.github/workflows/semver.yml` runs `cargo-semver-checks` on PRs that touch `src/` or `Cargo.toml` to catch accidental semver-breaking changes.
- **Nightly Miri CI workflow** ([#38](https://github.com/dchud/mrrc/issues/38), [#41](https://github.com/dchud/mrrc/pull/41)): New `.github/workflows/miri.yml` runs `cargo +nightly miri test --lib` on a daily schedule to detect undefined behavior.
- **`serialization_never_panics` property test** ([#44](https://github.com/dchud/mrrc/issues/44), [#46](https://github.com/dchud/mrrc/pull/46)): New proptest verifying `MarcWriter` never panics or errors on any generated record, catching serialization bugs the round-trip test might miss.
- **Deduplicate control field tags in proptest** ([#43](https://github.com/dchud/mrrc/issues/43), [#45](https://github.com/dchud/mrrc/pull/45)): `arb_record()` now uses a `HashSet` to skip duplicate control field tags, ensuring generated records are structurally valid per MARC.

### Changed

- **`check.sh --quick` now skips Python tests** ([#53](https://github.com/dchud/mrrc/issues/53), [#56](https://github.com/dchud/mrrc/pull/56)): `--quick` previously skipped the maturin build but still ran Python tests against a stale extension. Now `--quick` runs rustfmt, clippy, ruff, Rust tests, and doc tests (~10s). Full mode adds doc check, audit, maturin build, and Python tests (~16s).

### Fixed

- **Serialization functions now accept wrapped Python Records** ([#57](https://github.com/dchud/mrrc/issues/57), [#58](https://github.com/dchud/mrrc/pull/58)): `record_to_json()`, `record_to_xml()`, `record_to_marcjson()`, `record_to_mods()`, `record_to_dublin_core()`, and `record_to_dublin_core_xml()` previously rejected `Record` objects returned by `xml_to_record()` and other deserialization functions with `TypeError: 'Record' object is not an instance of 'Record'`. The PyO3 functions expected the raw `_mrrc.Record` type but received the Python wrapper `mrrc.Record`. All six functions now use a shared `extract_record()` helper that accepts both types, matching the pattern already used by `record_to_csv()`.
- **`record_to_dublin_core_xml` now exported from Python wrapper** ([#59](https://github.com/dchud/mrrc/pull/59)): The function existed in the Rust extension (`_mrrc`) but was missing from `mrrc/__init__.py` imports and `__all__`, making it inaccessible as `mrrc.record_to_dublin_core_xml()`.
- **Skip rayon tests under Miri** ([#47](https://github.com/dchud/mrrc/issues/47), [#49](https://github.com/dchud/mrrc/pull/49)): Annotate 4 `rayon_parser_pool` tests with `#[cfg_attr(miri, ignore)]` to work around a known stacked borrows violation in `crossbeam-epoch` 0.9.18 ([crossbeam-rs/crossbeam#1181](https://github.com/crossbeam-rs/crossbeam/issues/1181)). Tracking re-enablement in [#48](https://github.com/dchud/mrrc/issues/48).
- **Arithmetic overflow panic on malformed leaders** ([#32](https://github.com/dchud/mrrc/issues/32)): `MarcReader`, `AuthorityReader`, and `HoldingsReader` now return `Err(MarcError::InvalidLeader)` instead of panicking when `record_length` or `data_base_address` in the leader is less than 24. New `Leader::validate_for_reading()` method performs the check in all three binary readers.
- **Python examples used Python file I/O instead of Rust I/O** ([#53](https://github.com/dchud/mrrc/issues/53), [#54](https://github.com/dchud/mrrc/pull/54)): All Python examples now pass file path strings to `MARCReader`/`MARCWriter` instead of `open()` file objects, using the Rust I/O backend which releases the GIL. Updated type stub docstring examples to match.

### Documentation

- **Agent docs overhaul** ([#50](https://github.com/dchud/mrrc/pull/50)): New `CLAUDE.md` with project overview, key files, build/test commands, and architecture reference. Rewritten `AGENTS.md` replaces inception-era framing, migrates all `bd` references to `br`.
- **Docs navigability improvements** ([#51](https://github.com/dchud/mrrc/pull/51)): Added `_mrrc.pyi` type stub admonition to Python API reference page. Moved Design and History under Contributing in mkdocs nav.
- **Context7 configuration** ([#52](https://github.com/dchud/mrrc/pull/52)): New `context7.json` configuring documentation indexing for source, bindings, Python package, docs, and examples.
- **Migrate bd references to br** ([#55](https://github.com/dchud/mrrc/pull/55)): Migrated `.github/copilot-instructions.md` and `docs/contributing/release-procedure.md` from `bd` to `br` commands, with explicit `br sync --flush-only` + git commit workflow.
- Updated migration guide (`docs/guides/migration-from-pymarc.md`) to recommend path-based `MARCReader` input instead of Python file objects, with comments explaining GIL release and multi-thread parallelism benefits.

## [0.7.3] - 2026-02-26

### Added

- **Standard MARCXML support** ([#15](https://github.com/dchud/mrrc/issues/15)): `xml_to_record()` and `record_to_xml()` / `to_xml()` now produce and parse standard LOC MARCXML. Output includes XML declaration, `xmlns="http://www.loc.gov/MARC21/slim"` namespace, and `tag`/`ind1`/`ind2`/`code` as XML attributes (not child elements). Parsing accepts all three namespace forms: default namespace (`<record xmlns="...">`), prefix namespace (`<marc:record xmlns:marc="...">`), and no namespace (`<record>`). New `xml_to_records()` function parses `<collection>` wrappers containing multiple records. Rust module renamed from `xml` to `marcxml` (`record_to_marcxml()`, `marcxml_to_record()`, `marcxml_to_records()`); Python API names unchanged for pymarc compatibility.
- **`get_linked_fields()` for 880 alternate script field linkage** ([#19](https://github.com/dchud/mrrc/issues/19)): New pymarc-compatible `record.get_linked_fields(field)` returns all 880 fields linked via subfield $6 occurrence matching. Also adds `get_linked_field()` (singular), `get_original_field()` (reverse lookup from 880 to original), and `get_field_pairs(tag)` (original + 880 tuples). Fixed `LinkageInfo` parser to handle real MARC script identification codes: `(2` (Hebrew), `(3` (Arabic), `$1` (CJK), `(N` (Cyrillic), `(S` (Greek). Includes tests for Hebrew, Arabic, Chinese, Russian, and Greek scripts plus linkage across multiple field types (title, author, publisher, subject, series, notes, added entries).
- **`MARCReader.backend_type` property**: Exposes the reader's I/O backend as a string (`"rust_file"`, `"cursor"`, or `"python_file"`) for diagnostics and testing.
- **Dependabot configuration**: Enabled automated dependency updates for `cargo`, `uv`, and `github-actions` ecosystems (weekly schedule).
- **GIL release verification tests**: Deterministic tests proving `py.detach()` actually releases the GIL during record parsing. Uses `sys.setswitchinterval(100.0)` to suppress automatic GIL switching, then verifies a background counter thread makes progress — which can only happen if the GIL is explicitly released. Covers all three backends: `rust_file`, `cursor`, and `python_file`.

### Changed

- **Removed 22 duplicate inherent methods from `Record`** ([#26](https://github.com/dchud/mrrc/issues/26)): Helper methods like `title()`, `publisher()`, `isbn()`, etc. were defined both as inherent methods on `Record` and on the `RecordHelpers` trait. The inherent methods shadowed the trait, causing maintenance burden and confusion. Removed all duplicates; callers now use the `RecordHelpers` trait (already re-exported in the public API). Rust users may need to add `use mrrc::RecordHelpers;` if not already imported.
- **Replaced flaky GIL release test with deterministic verification** ([#3](https://github.com/dchud/mrrc/issues/3)): The `test_pathlib_path_threading_equivalent_to_str` test used timing-based speedup ratios to verify GIL release, which was unreliable on shared CI runners. Replaced with `TestBackendTypeRouting` (6 tests asserting each input type routes to the correct backend) and `TestPathlibThreadingCorrectness` (2 tests verifying parallel reads produce correct results without speedup assertions).
- **quick-xml 0.31 → 0.39** ([#7](https://github.com/dchud/mrrc/pull/7)): Migrated MODS XML parser to quick-xml 0.39 API. `BytesText::unescape()` replaced by `decode()` with explicit entity resolution via new `Event::GeneralRef` handler (predefined XML entities and numeric character references). `Reader::trim_text()` moved to `Reader::config_mut().trim_text()`.
- **pyo3 0.27 → 0.28** ([#8](https://github.com/dchud/mrrc/pull/8)): Migrated PyO3 bindings to 0.28 API for free-threaded Python support. `Python::with_gil()` → `Python::attach()`, `Python::assume_gil_acquired()` → `Python::assume_attached()`. Added `from_py_object` annotation to all 10 `#[pyclass]` types implementing `Clone`.
- **thiserror 1.0 → 2.0** ([#6](https://github.com/dchud/mrrc/pull/6)): Updated error derive macro.
- **nom 7.1 → 8.0** ([#5](https://github.com/dchud/mrrc/pull/5)): Updated parser combinator library.
- **ruff 0.14.14 → 0.15.2** ([#9](https://github.com/dchud/mrrc/pull/9)): Updated Python linter.
- **GitHub Actions updates**: actions/checkout 4 → 6 ([#14](https://github.com/dchud/mrrc/pull/14)), codecov/codecov-action 4 → 5 ([#13](https://github.com/dchud/mrrc/pull/13)), astral-sh/setup-uv 4 → 7 ([#12](https://github.com/dchud/mrrc/pull/12)), actions/download-artifact 4 → 7 ([#11](https://github.com/dchud/mrrc/pull/11)), moonrepo/setup-rust 0 → 1 ([#10](https://github.com/dchud/mrrc/pull/10)), actions/setup-python 5 → 6 ([#24](https://github.com/dchud/mrrc/pull/24)), actions/upload-artifact 4 → 6 ([#23](https://github.com/dchud/mrrc/pull/23)), actions/upload-pages-artifact 3 → 4 ([#22](https://github.com/dchud/mrrc/pull/22)), actions/cache 4 → 5 ([#21](https://github.com/dchud/mrrc/pull/21)), softprops/action-gh-release 1 → 2 ([#20](https://github.com/dchud/mrrc/pull/20)).

### Fixed

- **Format conversion functions now return wrapped Python objects** ([#16](https://github.com/dchud/mrrc/issues/16)): `marcjson_to_record()`, `json_to_record()`, `xml_to_record()`, `mods_to_record()`, and `mods_collection_to_records()` previously returned raw Rust `PyRecord` objects. Field subscripting (`field['a']`), `get_subfields()`, and leader indexing (`leader[9]`) all failed on these records. Now all format conversion functions wrap results in the Python `Record` class, matching the behavior of records from `MARCReader`.
- **`publisher()` now checks 264 (RDA) as fallback** ([#17](https://github.com/dchud/mrrc/issues/17)): Previously only returned $b from field 260. Now falls back to field 264 with indicator2='1' (publication) for RDA-cataloged records. Also updated `place_of_publication()`, `publication_date()`, `publication_info()`, and `publication_year()` / `pubyear()` to check 264 as fallback, matching pymarc behavior.
- **`subjects()` now covers all 6XX fields** ([#18](https://github.com/dchud/mrrc/issues/18)): Previously only returned $a values from field 650 (topical term). Now matches pymarc's `subjects()` coverage: 600, 610, 611, 630, 648, 650, 651, 653, 654, 655, 656, 657, 658, 662, 690-691, 696-699. Also updated `subjects_with_subdivision()` and `subjects_with_note()` to search all subject fields.
- **Removed flaky timing assertion from `test_backend_comparison_1k`**: The benchmark's `assert median_rustfile < median_pythonfile * 2` failed intermittently on shared CI runners due to disk I/O variance. Timing data is still printed for informational purposes.

### Documentation

- Updated Python quick example in `docs/index.md` to use path string instead of file I/O wrapper, demonstrating the GIL-releasing code path.

## [0.7.2] - 2026-02-11

### Added

- **MODS XML read support**: `mods_to_record(xml_str)` and `mods_collection_to_records(xml_str)` parse MODS XML (single record or `<modsCollection>`) into MARC `Record` objects. Covers `titleInfo`, `name`, `subject`, `originInfo`, `physicalDescription`, `abstract`, `note`, `genre`, `classification`, `location`, `relatedItem`, `recordInfo`, `identifier`, `accessCondition`, `tableOfContents`, `targetAudience`, and `language` elements with conformance tests using LOC-derived fixtures.
- **Cross-compiled Linux wheels** ([mrrc-experiments#16](https://github.com/dchud/mrrc-experiments/issues/16)): Release workflow now builds `aarch64` and `i686` Linux wheels via maturin-action Docker cross-compilation, increasing wheel count from 15 to 25 per release.
- **Field constructor `subfields=` and `indicators=` kwargs** ([mrrc-experiments#15](https://github.com/dchud/mrrc-experiments/issues/15)): `Field` now accepts `subfields=` (list of `Subfield`) and `indicators=` (list/tuple of two strings) as keyword arguments, enabling inline construction matching pymarc style: `Field('245', indicators=['0', '1'], subfields=[Subfield('a', 'Title')])`. The Rust binding already supported this signature; the Python wrapper now passes through both kwargs.
- **Record constructor `fields=` kwarg** ([mrrc-experiments#15](https://github.com/dchud/mrrc-experiments/issues/15)): `Record` now accepts an optional `fields=` keyword argument (list of `Field`), enabling inline record construction: `Record(fields=[Field(...), ...])`. This goes beyond pymarc parity (pymarc's `Record.__init__` does not accept `fields=`) as a UX improvement. `Record()` with no arguments also now works, defaulting to `Leader()`.
- **Constructor kwargs tests**: 8 new tests in `TestConstructorKwargs` covering `indicators=`, `subfields=`, `fields=`, combined usage, full inline construction, and backward compatibility.
- **PEP 561 `py.typed` marker**: Added to root `mrrc/` package so type checkers recognize shipped type stubs.
- **Project layout documentation**: New `docs/contributing/project-layout.md` describes the three-layer architecture (core Rust → PyO3 bindings → Python package), how maturin and the Cargo workspace connect them, directory structure, build commands, and common development workflows.
- **Pre-push git hook**: `.githooks/pre-push` runs `.cargo/check.sh` before every push, preventing code from reaching CI that would fail local checks. Opt-in via `git config core.hooksPath .githooks`.
- **`check.sh --quick` flag**: Skips docs, audit, and maturin build for faster inner-loop iteration (keeps fmt, clippy, tests, ruff).

### Changed

- **GitHub releases now include changelog notes**: `python-release.yml` auto-extracts the relevant CHANGELOG.md section and includes it in the GitHub Release body. Backfilled v0.7.1 release notes.
- **Pymarc compatibility tests fully enabled**: Removed 10 `pytest.skip` guards from `test_pymarc_compatibility.py` — all features (to_json, to_xml, to_marcjson, to_dublin_core, to_marc21, writer, roundtrip, test data) were already implemented. 88/88 tests now pass with 0 skipped.
- **Documentation updated for inline construction**: Quickstart, API reference, migration guide, writing tutorial, and examples now show `Field(subfields=..., indicators=...)` and `Record(fields=...)` as the primary construction pattern, with `add_subfield()`/`add_field()` as the incremental alternative. Migration guide updated to reflect `Record()` no longer requires an explicit `Leader` argument and field creation is now closer to pymarc.
- **Type stubs updated**: `Field.__new__` and `Record.__new__` in `.pyi` now include the new keyword-only parameters.
- **Type stubs enriched**: Merged rich docstrings into `mrrc/_mrrc.pyi` from the old `src-python` stubs — Leader properties with MARC position descriptions, MARCReader GIL/concurrency/thread-safety docs, MARCWriter context manager protocol, BibframeConfig setter methods and property getters, RdfGraph `parse()`/`triples()`/`__len__()`. Added `mods_to_record` and `mods_collection_to_records` stubs.
- **Removed stale `src-python/python/` directory**: Deleted redundant Python package directory (1,235 lines) left over from the Phase 5 layout migration. Root `mrrc/` is now the sole Python package location.
- **Test suite audit**: Deleted 5 redundant/inert files from `src-python/tests/`, migrated 4 test files to `tests/python/` (backend parity, type detection, rayon parser pool, record boundary scanner), extracted 1 pipeline regression test. Deleted `src-python/tests/` entirely. Python test count: 364 → 433.
- **Ruff lint compliance**: Fixed 76 ruff errors across `mrrc/` and `tests/python/` (bare except, unused imports/variables, walrus operator patterns, f-string placeholders, true/false comparisons). Added ruff check step to `check.sh`.
- **Rust integration tests in check.sh**: Changed `cargo test --lib` to `cargo test --lib --tests`, adding 17 integration test files (bibframe, mods, field query, concurrent GIL, etc.) to the local CI script.
- **CI workflow alignment with check.sh**: Closed gaps where CI was missing checks that check.sh runs locally. Added clippy on mrrc-python to `lint.yml` (was only linting mrrc core). Added ruff Python lint job to `lint.yml` (via `uvx`). Fixed rustdoc scope from `--package mrrc` to `--all` to include PyO3 bindings. Changed `test.yml` from `--lib --bins` to `--lib --tests --package mrrc`, adding 15+ integration test files that were skipped in CI. Removed unused mypy/pyright install from `python-build.yml` test-wheels job. Upgraded `actions/cache` from v3 to v4 across all workflows.
- **CI efficiency improvements**: Switched all `pip install` calls to `uv pip install --system` across `python-build.yml`, `benchmark-python.yml`, and `python-release.yml` for faster dependency resolution. Consolidated three separate cargo cache actions (registry, index, build) into single multi-path caches across five workflows. Switched cargo-audit and cargo-tarpaulin from `cargo install` (compiles from source) to pre-built binaries via `taiki-e/install-action`. Combined two tarpaulin runs into one (`--out Xml Html`). Upgraded `codecov/codecov-action` from v3 to v4, `actions/setup-python` from v4 to v5. Removed redundant `pip install maturin` and single-element Rust version matrix from `build.yml`.

### Fixed

- **Rust formatting**: Applied `cargo fmt` to `src/mods.rs` and `src-python/src/formats.rs` (pre-existing match arm brace style).
- **Clippy doc lint**: Escaped `OCoLC` in `tests/mods_conformance_tests.rs` doc comment to satisfy `clippy::doc_markdown`.
- **Flaky CI benchmark**: `test_bytesio_vs_file_isolation` switched from mean to median for I/O overhead measurement, preventing single-iteration CI runner spikes from failing the test. Removed hard assertion (test value is diagnostic, not a correctness gate).

## [0.7.1] - 2026-02-10

### Changed

- **BREAKING: Removed 7 experimental serialization formats** ([mrrc-experiments#19](https://github.com/dchud/mrrc-experiments/pull/19)): Deleted protobuf, arrow, parquet, flatbuffers, messagepack, cbor, and avro support (~15,100 lines across 78 files). These added significant build complexity and dependency weight without proven adoption. Only ISO 2709 and BIBFRAME remain.
- **BIBFRAME promoted to core**: BIBFRAME conversion is now always-compiled rather than feature-gated (`format-bibframe`), reflecting its importance as the LOC linked data format. Dependencies `oxrdfio` and `oxrdf` are now non-optional.
- **Python bindings simplified**: Removed format-specific classes (ProtobufReader, ArrowWriter, etc.), `mrrc/analytics.py`, and format submodules. Simplified `read()`/`write()` helpers and `__init__.py` exports.
- **Clean repo history**: Recreated repository as `dchud/mrrc` with a clean two-commit history, replacing `dchud/mrrc-experiments`. Build artifacts, large test fixtures, and accumulated cruft from the experimental phase are no longer in git history.
- **Test fixtures**: Removed 25MB `100k_records.mrc` from repo; gitignored to prevent re-commit. Regenerable via `scripts/generate_benchmark_fixtures.py`. Benchmark tests depending on 100k fixture removed.
- **Build simplification**: Removed `build.rs` (was only for protobuf/flatbuffers code generation), `src/generated/`, `proto/`, and ~20 dependencies from `Cargo.toml`.
- **Local CI script**: Updated `.cargo/check.sh` to use `uv run` for maturin and pytest steps instead of manual venv activation.
- **100k fixture scrub**: Removed remaining references to `100k_records.mrc` from scripts, benches, CI workflow names, and public docs (benchmarks, contributing, tests README). Fixture generation script no longer produces the 100k file.
- **Developer docs standardized on uv**: Replaced manual venv activation and bare `maturin develop`/`pytest` commands with `uv sync`/`uv run` equivalents across installation, development setup, testing, and migration guides.
- **Release procedure updated**: Refreshed for OIDC trusted publishing (replaces `PYPI_API_TOKEN`), added `installation.md` and `quickstart-rust.md` to version-bump checklist.
- **Rust dependency version in docs**: Updated hardcoded `mrrc = "0.6"` to `"0.7"` in installation and quickstart guides.

### Fixed

- **Flaky benchmark test**: `test_backend_comparison_1k` now uses 5 iterations with median instead of 3 iterations with mean, making it resilient to single-iteration CI runner I/O spikes.
- **CI: Python release workflow**: Added `actions/setup-python` before `maturin-action` to properly set Python version; `python-version` is not a valid input for maturin-action
- **CI: Python release OIDC publishing**: Removed `password:` secret from `pypa/gh-action-pypi-publish` step; action auto-detects OIDC token from `id-token: write` permission
- **CI: Re-enabled ASAN memory-safety workflow**: Upstream Rust issue (rust-lang/rust#144168) that caused zerocopy nightly incompatibility has been resolved
- **CI: ASAN workflow zerocopy_derive fix**: Removed `-Zavoid-dev-deps` flag which caused zerocopy_derive proc-macro resolution failures on nightly
- **CI: CodSpeed benchmark integration**: Re-linked repository with CodSpeed after clean repo creation
- **Beads: Gitignore rotated daemon logs**: Added `daemon-*.log.gz` pattern to `.beads/.gitignore` and removed accidentally committed 2.7MB rotated log file

## [0.7.0] - 2026-02-05

### Added

#### Testing (2026-02-04)
- **pathlib.Path GIL Release Tests**: Added tests verifying that `pathlib.Path` objects achieve the same GIL release behavior as string paths
  - `test_pathlib_path_sequential_baseline`: Baseline sequential reading with Path objects
  - `test_pathlib_path_threading_equivalent_to_str`: Verifies Path and str have equivalent threading speedup (within 30%)
  - `test_pathlib_vs_str_equivalent_performance`: Confirms both use the same RustFile backend
  - Location: `tests/python/test_pathlib_gil_release.py`

### Changed

#### Python Version Support (2026-02-03)
- **Dropped Python 3.9 support**: Python 3.9 reached end-of-life in October 2025
- **Added Python 3.13 support**: Stable release from October 2024
- **Added Python 3.14 support**: Stable release from October 2025
- Updated minimum Python version from 3.9 to 3.10 in pyproject.toml, CI workflows, and documentation
- Note: RHEL 9 users should use `dnf module enable python3.11` or containers for Python 3.10+

### Fixed

#### Security (2026-02-03)
- **bytes crate upgrade**: Upgraded `bytes` from 1.9.0 to 1.11.1 to address RUSTSEC-2026-0007

#### CI/CD Improvements (2026-02-03)
- **CodSpeed Rust Benchmark Integration** (PR #8): Added continuous performance tracking for Rust benchmarks
  - Integrated `codspeed-criterion-compat` v4.3.0 as drop-in replacement for criterion
  - New `benchmark-rust.yml` workflow runs Rust benchmarks via `cargo codspeed`
  - Added CodSpeed badge to README.md
- **Benchmark Workflow Naming Consistency**: Renamed workflows for clarity
  - `python-benchmark.yml` → `benchmark-python.yml` ("Benchmarks: Python")
  - `codspeed.yml` → `benchmark-rust.yml` ("Benchmarks: Rust")
  - Groups benchmark workflows alphabetically, makes target language clear at a glance

#### CI/CD Fixes (2026-02-02)
- **CodSpeed Action v4 Migration**: Fixed benchmark workflow after upgrading from deprecated v1
  - Added required `mode: walltime` parameter for standard CI runners
  - CodSpeed v4 now requires explicit mode selection ('simulation' or 'walltime')

### Documentation

#### Documentation Updates (2026-02-04)
- **Fixed benchmark documentation links**: Updated internal links in `index.md`, `query-dsl.md`, and `architecture.md` to use lowercase filenames
- **Testbed Design Document** (`docs/design/ideas-for-test-projects.md`):
  - Added comprehensive state management design (YAML + SQLite hybrid)
  - Added interaction models section (centralized vs local/private testing)
  - Added repository growth timeline and pruning strategies
  - Added MkDocs documentation structure with Divio-style organization
  - Updated project phases (7 phases including documentation and initial data)
  - Consolidated duplicate sections for cleaner document structure

#### Documentation Improvements (2026-02-02)
- **File Renames and Reorganization**:
  - Renamed `streaming-large-files.md` → `working-with-large-files.md` to better reflect content scope
  - Renamed benchmark docs to lowercase convention: `RESULTS.md` → `results.md`, `FAQ.md` → `faq.md`, `README.md` → `index.md`
  - Updated all cross-references and mkdocs.yml navigation
- **Fixed Broken Links**:
  - Fixed README.md tutorial links that were returning 404s
  - Updated to point to actual tutorial paths (`tutorials/python/reading-records/`, `tutorials/rust/reading-records/`)
- **Formatting Fixes**:
  - Fixed bullet list formatting in `threading-python.md`, `performance-tuning.md`, `results.md`, `migration-from-pymarc.md`
  - Added blank lines after colons before bullet lists (required by some markdown parsers)
- **Content Improvements**:
  - Added uv example to `installation.md` for building from source
  - Added hardware info (2025 MacBook Air M4) to `performance-tuning.md`
  - Added Rust/Python format name columns to bibframe RDF serialization table
  - Added "Same?" column to `migration-from-pymarc.md` API comparison tables for clarity
  - Removed emojis from `migration-from-pymarc.md` section headers
  - Fixed `encoding.md`: removed non-existent `detect_encoding` import, noted `EncodingValidator` is Rust-only
  - Simplified `python-api.md` format note (all formats bundled in wheel, no extra steps)
  - Added feature-gated format installation link to `formats.md`

#### Documentation Reorganization (2026-01-29)
- **Complete restructure using Material for MkDocs**:
  - New tabbed navigation with Getting Started, Tutorials, Guides, Reference, Examples sections
  - Light/dark theme toggle with persistent preference
  - Built-in search with instant results
  - Mobile-responsive layout
- **Getting Started**:
  - `quickstart-python.md`: 5-minute Python quickstart with installation, reading, writing examples
  - `quickstart-rust.md`: 5-minute Rust quickstart with Cargo setup and basic operations
- **Tutorials** (split from monolithic guides):
  - Python tutorials: reading-records, writing-records, querying-fields, format-conversion, concurrency (5 files)
  - Rust tutorials: reading-records, writing-records, querying-fields, format-conversion, concurrency (5 files)
- **Reference Documentation**:
  - `marc-primer.md`: MARC record structure explained for non-librarians (leader, directory, fields, indicators, subfields)
  - `encoding.md`: Character encoding reference (MARC-8, UTF-8, escape sequences, detection)
  - `python-api.md`: Complete Python API reference (Record, Field, MARCReader, formats, BIBFRAME)
  - `rust-api.md`: Complete Rust API reference (types, builders, traits, feature flags)
- **Examples Index**: Organized links to 29 example files by category (Python/Rust)
- **README Reduction**: Streamlined from 897 to 115 lines per documentation best practices
- **Archived Legacy Docs**: Moved superseded files to `docs/history/legacy-docs-superseded/`

## [0.6.0] - 2026-01-29

### Added

#### BIBFRAME Conversion (2026-01-28, updated 2026-01-29)
- **Bidirectional MARC ↔ BIBFRAME conversion** (`format-bibframe` feature):
  - `marc_to_bibframe()`: Convert MARC bibliographic records to BIBFRAME 2.0 RDF graphs
  - `bibframe_to_marc()`: Reverse conversion from BIBFRAME RDF back to MARC records
  - Full implementation of LOC MARC21 to BIBFRAME 2.0 Conversion Specifications
  - Support for all MARC content types: books, serials, music, maps, visual materials
- **Python BIBFRAME Bindings** (2026-01-29):
  - `mrrc.marc_to_bibframe(record, config)`: Python wrapper for MARC→BIBFRAME conversion
  - `mrrc.bibframe_to_marc(graph)`: Python wrapper for BIBFRAME→MARC conversion
  - `mrrc.BibframeConfig`: Configuration class with all options exposed
  - `mrrc.RdfGraph`: RDF graph class with `serialize()` method for all formats
  - `mrrc.RdfFormat`: Enum for output format selection (RdfXml, NTriples, Turtle, JsonLd)
- **Hub/Expression Support** (2026-01-29):
  - MARC 240 (Uniform Title) creates bf:Hub for expression-level grouping
  - Work → hasExpression → Hub → hasInstance → Instance linking pattern
  - Hub → expressionOf → Work reverse relationship
  - Supports translations, versions, and other expression-level variants
- **Item Creation from Holdings** (2026-01-29):
  - MARC 852 (Location) creates bf:Item entities linked to Instance
  - Call number from $h/$i, sublocation from $b, barcode from $p
  - MARC 876-878 (Item Information) support for detailed holdings
  - Multiple Items per Instance with sequential URI generation
- **Classification Support** (2026-01-29):
  - MARC 050 → bf:ClassificationLcc (Library of Congress Classification)
  - MARC 060 → bf:ClassificationNlm (National Library of Medicine)
  - MARC 080 → bf:ClassificationUdc (Universal Decimal Classification)
  - MARC 082 → bf:ClassificationDdc (Dewey Decimal Classification)
  - MARC 084 → bf:Classification (Other schemes with source in $2)
  - classificationPortion ($a) and itemPortion ($b) properties
- **RDF Serialization Formats**:
  - RDF/XML (W3C standard, maximum interoperability)
  - N-Triples (simple line-based triple format)
  - Turtle (human-readable, compact RDF syntax)
  - JSON-LD (modern web standard, JSON representation)
- **Configuration Options** (`BibframeConfig`):
  - Custom base URI for generated resources
  - Output format selection
  - Authority linking control
  - BFLC extension support
- **Comprehensive Test Suite** (111 tests):
  - 48 unit tests for individual MARC→BIBFRAME mappings (including Hub, Item, Classification)
  - 26 validation tests for BIBFRAME 2.0 ontology compliance
  - 16 integration tests with real-world record types
  - 16 round-trip tests documenting acceptable data loss
  - 5 baseline comparison tests against official LOC tool
- **Examples**:
  - Rust: `marc_to_bibframe.rs`, `bibframe_to_marc.rs`, `bibframe_batch.rs`
  - Python: `marc_to_bibframe.py`, `bibframe_roundtrip.py`, `bibframe_config.py`
- **Documentation**:
  - README section with conversion examples
  - API documentation with docstrings
  - Known limitations and data loss documentation

### Changed

#### Field Insertion Order Preservation (2026-01-14)
- **Record fields now preserve insertion order** instead of sorting by tag
  - Uses `IndexMap` for field storage in `Record`, `AuthorityRecord`, and `HoldingsRecord`
  - Enables round-trip fidelity: serialization and deserialization now preserve original field order
  - Field iteration via `fields()` and `control_fields_iter()` returns fields in insertion order
  - **Breaking change for code assuming tag-sorted order**: Use `fields_by_tag()` for explicit tag-based access
- **Unplanned changes during implementation:**
  - `fields_in_range()` now iterates and filters (was using `BTreeMap::range()`)
  - `remove_fields_by_tag()` now uses `shift_remove()` to preserve order of remaining fields
- **Performance impact**: ~17-22% regression in record parsing benchmarks (acceptable trade-off for round-trip fidelity)
  - Baseline: 9.5ms for 10k records → After: 11.3ms for 10k records
  - Still significantly faster than pymarc (~4x improvement maintained)

#### Performance Optimizations (2026-01-19)
- **SmallVec for Subfield Storage**:
  - Replaced `Vec<Subfield>` with `SmallVec<[Subfield; 4]>` for subfield arrays
  - Targets common case: 85-90% of real-world MARC records have ≤4 subfields per field
  - Performance gain: **+4.6% roundtrip throughput** (read+write cycle)
  - Zero-copy inline storage for typical records, automatic heap spillover for large records
  - Maintains API compatibility - transparent to users
- **Parse Digits Optimization**:
  - Eliminated string allocations in `parse_digits()` parser combinator
  - Reduced numeric field parsing overhead with direct byte-based validation
  - Contributes to overall **+6.0% combined optimization gain** (measured across parse + serialize pipeline)

### Added

#### New Binary Format Support (2026-01-22)
- **Apache Arrow Columnar Format** (`format-arrow` feature):
  - `ArrowWriter`: Converts MARC records to Arrow RecordBatch for analytics workflows
  - `ArrowReader`: Reads Arrow IPC files back to MARC records with full fidelity
  - Preserves field sequence and control field data through round-trip conversion
  - Performance: 96% compression ratio, ~865k records/second throughput
  - Enables SQL queries over MARC data via DuckDB and Polars integration
- **FlatBuffers Zero-Copy Format** (`format-flatbuffers` feature):
  - `FlatbuffersWriter`: Serializes MARC records to FlatBuffers binary format
  - `FlatbuffersReader`: Zero-copy deserialization without parsing overhead
  - Ideal for memory-constrained environments and streaming applications
  - Performance: 64% memory savings vs JSON, 1M+ records/second zero-copy access
- **MessagePack Compact Binary** (`format-messagepack` feature):
  - `MessagePackReader`/`MessagePackWriter` for compact binary serialization
  - 25% smaller than JSON with equivalent structure preservation
  - Cross-platform interoperability with 50+ language implementations
  - Performance: ~750k records/second throughput
- **FormatReader/FormatWriter Traits**: Unified interface enabling consistent API across all formats

#### Python Format Bindings (2026-01-22)
- **All Formats Available in Python**: Arrow, FlatBuffers, MessagePack, and Protobuf readers/writers
- **Format-Agnostic Helpers**:
  - `mrrc.read(path_or_data, format=None)`: Auto-detect format from file extension or content
  - `mrrc.write(records, path, format=None)`: Write records to any supported format
- **Type Stubs** (`.pyi` files): Full IDE autocompletion and type checking support
- **`mrrc/formats/` Package**: Organized modules for format-specific operations
  - `mrrc.formats.marc`: ISO 2709 reading/writing
  - `mrrc.formats.protobuf`: Protobuf serialization with schema evolution
  - `mrrc.formats.arrow`: Arrow/Parquet operations with analytics integration
  - `mrrc.formats.flatbuffers`: FlatBuffers zero-copy access
  - `mrrc.formats.messagepack`: MessagePack compact binary for APIs

#### Analytics Integration (2026-01-22)
- **`mrrc.analytics` Module**: SQL and DataFrame operations over MARC data
  - `to_duckdb(records)`: Create DuckDB relation for SQL queries
  - `to_polars(records)`: Create Polars DataFrame for data analysis
  - Enables filtering, aggregation, and joins across millions of records
- **`export_to_parquet()` Helper**: Convert Arrow tables to Parquet files for data lakes
- **Integration Examples**: Working examples demonstrating analytics workflows

#### Documentation Expansion (2026-01-22)
- **New User Guides**:
  - `INSTALLATION_GUIDE.md`: Complete installation instructions for Python and Rust, feature flags, platform-specific notes, troubleshooting
  - `FORMAT_SELECTION_GUIDE.md`: Decision tree for choosing the right format, comparison matrix, use case recommendations
  - `PYTHON_TUTORIAL.md`: Comprehensive Python tutorial covering reading, writing, field access, format conversion, Query DSL
  - `RUST_TUTORIAL.md`: Complete Rust tutorial with builder pattern, traits, parallel processing with Rayon
  - `STREAMING_GUIDE.md`: Large file handling patterns, O(1) memory streaming, parallel processing strategies
- **Format Support Matrix**: Added comprehensive format comparison table to README.md
- **Enhanced Python Docstrings**: All format modules include use cases, performance notes, and working examples

#### Format Evaluation & Strategy (2026-01-19)
- **Binary Format Comparison**: Evaluated MessagePack, CBOR, Avro, Arrow, FlatBuffers against library requirements
- **Strategy Documentation**: Decision rationale and benchmarks archived in `docs/history/format-research/`

### Fixed

- **Example Code Quality**: Resolved clippy warnings in example code for clean builds
- **Code Formatting**: Applied rustfmt to all recent additions for consistency

### Documentation Updates

- **Benchmark Documentation Accuracy** (2026-01-19):
  - Refreshed all benchmark measurements with latest performance data
  - Clarified that reported numbers are post-warm-up (JIT stabilized)
  - Updated comparison baselines to reflect recent optimizations
- **Architecture Documentation**: Enhanced documentation structure in docs/design/

## [0.4.0] - 2026-01-09

### Added

#### Query DSL Python Bindings (2026-01-11)
- **Python Query DSL**: Exposed Rust Query DSL to Python with full feature parity
  - `FieldQuery`: Builder pattern for complex field matching (tag, indicators, subfields)
  - `TagRangeQuery`: Match fields within a tag range (e.g., 600-699 for all subjects)
  - `SubfieldPatternQuery`: Regex matching on subfield values
  - `SubfieldValueQuery`: Exact or partial string matching on subfield values
- **Record Query Methods**: New methods on Record for advanced field searching
  - `fields_by_indicator(tag, indicator1=None, indicator2=None)`: Filter by indicators
  - `fields_in_range(start_tag, end_tag)`: Find fields within a tag range
  - `fields_matching(query)`: Use FieldQuery objects for complex matching
  - `fields_matching_range(query)`: Use TagRangeQuery for range-based matching
  - `fields_matching_pattern(query)`: Regex matching via SubfieldPatternQuery
  - `fields_matching_value(query)`: String matching via SubfieldValueQuery
- **Query DSL Documentation**: Comprehensive guide at docs/QUERY_DSL.md covering:
  - Philosophy: Why multiple query types (performance, clarity)
  - All query types with examples
  - Practical cataloging scenarios (LCSH filtering, ISBN-13, subject analysis)
  - Comparison table vs pymarc's get_fields()
- **Query DSL Tests**: 42 new tests in tests/python/test_query_dsl.py

#### Developer Experience (2026-01-11)
- **Unified Testing Workflow**: Single command (`.cargo/check.sh`) for full pre-push verification (~30s)
  - Runs rustfmt, clippy, documentation, security audit, maturin build, and Python tests
  - Uses pytest marker-based test selection (`-m "not benchmark"`) to run 314 core tests in ~6s
  - Excludes 61 benchmark tests from default run (available via `pytest -m benchmark`)
  - Documented in AGENTS.md with command reference table and CI alignment
- **CI Workflow Fixes**:
  - Fixed ASAN memory-safety workflow: Exclude dev-dependencies (zerocopy) that have nightly Rust incompatibility
  - Fixed coverage workflow: Exclude PyO3 bindings package to avoid Python linker errors in tarpaulin

#### API Standardization (2026-01-12)
- **Format Conversion API Naming**: Standardized across all format modules for consistency
  - Added `record_to_csv()` for single-record CSV export (delegates to `records_to_csv()`)
  - Documented `records_to_csv()` plural pattern: semantically correct for batch-oriented tabular format
  - All format modules now follow: `record_to_format()` (single) + `records_to_format()` (batch where applicable)
- **CSV Export to Python**: Exposed Rust CSV functions to Python bindings
  - `record_to_csv(record)`: Export single record to CSV format
  - `records_to_csv(records)`: Export multiple records to CSV (batch mode)
  - `records_to_csv_filtered(records, filter_fn)`: Export with custom field filtering (filter_fn takes tag string, returns bool)
  - Handles both direct PyRecord and wrapped Record instances for flexibility
  - Completes API consistency with other format converters (JSON, XML, MARCJSON, Dublin Core, MODS)

#### Code Quality & Cleanup (2026-01-09)
- **Memory Safety CI**: Added ASAN (AddressSanitizer) workflow for nightly memory safety checks
   - Runs on schedule (daily) and manual dispatch
   - Validates no memory safety issues in core library
   - Local `.cargo/check.sh --memory-checks` option for developers with nightly toolchain
- **Phase Reference Removal**: Cleaned up implementation-plan-internal references from all user-facing code
   - Removed "Phase A-H" and gate nomenclature from source code comments
   - Removed from Python wrapper module documentation
   - Removed from test file names and test documentation
   - Removed from example code
   - Codebase now approachable to new users unfamiliar with development history
- **Test File Reorganization**:
   - `test_h_gate_benchmarking.py` → `test_parallel_benchmarking.py`
   - `test_h5_integration.py` → `test_integration.py`
   - `test_queue_state_machine_c2.py` → `test_queue_state_machine.py`
   - `test_memory_profiling_c4.py` → `test_memory_profiling.py`

### Previously Added (2026-01-08)

#### Full pymarc API Parity ✅
- **Leader Value Validation Helpers**: Complete MARC 21 leader position reference implementation
  - `Leader.get_valid_values(position)` - Returns dict of valid values for each leader position
  - `Leader.is_valid_value(position, value)` - Validates values per MARC 21 specification
  - `Leader.get_value_description(position, value)` - Gets human-readable descriptions
  - Support for positions 5 (Record Status), 6 (Type of Record), 7 (Bibliographic Level), 17 (Encoding Level), 18 (Cataloging Form)
- **Indicators as Tuple-Like Object**: Full support for pymarc-compatible indicator access
  - `field.indicators[0]` and `field.indicators[1]` indexing
  - `field.indicators` unpacking support
  - Backward compatibility with `field.indicator1` and `field.indicator2` properties
- **Control Field Access Pattern**: Support for pymarc's `record['001'].value` pattern
  - ControlField wrapper with `.value` property for control fields (001-009)
  - Both `record['001'].value` and `record.control_field('001')` patterns work identically
  - Backward compatibility maintained

#### Documentation
- **pymarc API Parity Plan**: Moved completed work to docs/history/ for archival
- **API Compatibility Summary**: Historical record of all 7 API gaps identified and resolved
- **README Enhancement**: Updated to highlight full pymarc API compatibility as primary feature
- **Migration Guide**: Comprehensive guide showing all compatible patterns with minimal migration path

### Changed

- **README**: Removed "nearly" qualifier - now describes "full" pymarc API compatibility
- **Python Test Suite**: Expanded from 88+ to comprehensive coverage of all parity features
- **Documentation Structure**: Organized completed parity work into history directory

### Performance

- No performance regressions - all existing benchmarks maintained
- Leader validation methods are zero-copy (dictionary lookups only)

### Technical Details

- **MARC 21 Reference Data**: All leader position value mappings per MARC 21 specification
- **API Stability**: Python wrapper API now guaranteed stable for pymarc compatibility
- **Backward Compatibility**: All new features fully backward compatible with existing code

## [0.3.1] - 2026-01-07

### Added

#### CI/CD Improvements
- **Python Wheel Build Workflow**: Fixed multi-platform wheel building for Ubuntu, macOS, and Windows
  - Configured maturin to use `-i pythonX.Y` flag for manylinux Python version selection (per maturin docs)
  - Fixed Windows glob expansion in test step using PowerShell conditionals
  - Removed deprecated mypy/pyright strict type checking (to be addressed in future typing effort)
  - Wheels now build and test successfully for Python 3.9, 3.10, 3.11, and 3.12 across all platforms

### Changed

#### Documentation Updates
- **Performance Reference Table** (benchmarks/RESULTS.md): Updated to use pymarc as baseline (1.0x) for clearer speedup comparison
- **Real-World Performance Scenarios**: Standardized table formats across all four scenarios for consistent speedup and time-saved metrics
- **PERFORMANCE.md Executive Summary**: Updated to clarify recommended strategies (ProducerConsumerPipeline for single-file, ThreadPoolExecutor for multi-file)
- **Documentation Audit**: Moved completed audit to docs/history/ for archival

## [0.3.0] - 2026-01-06

### Added

#### GIL Release & Concurrency (Phase A-F)
- **GIL Release during I/O**: Python wrapper now releases GIL during record parsing for true multi-thread parallelism
- **Three-Phase Pattern**: Robust pattern separates Python object access (GIL held) from CPU-intensive parsing (GIL released)
- **Measured Performance**: 2.04x speedup on 2 threads, 3.20x on 4 threads (Phase H benchmarking)
- **BatchedMarcReader**: Queue-based buffering reduces GIL contention from N to N/100 operations
- **SmallVec Optimization**: 4 KB inline buffer avoids allocations for ~85-90% of MARC records

#### Parallel I/O Backend (Phase H)
- **ReaderBackend Enum**: Unified reader supporting multiple input types with automatic selection
  - RustFile: Pure Rust file I/O (zero GIL overhead)
  - CursorBackend: In-memory bytes (zero GIL overhead)
  - PythonFile: Python file objects (GIL-managed)
- **File Path Support**: Direct file path input bypasses Python I/O layer entirely
- **Bytes/Bytearray Support**: In-memory MARC data via CursorBackend
- **Automatic Detection**: Input type automatically detected, optimal backend selected

#### Documentation (Phase G)
- **PERFORMANCE.md**: Comprehensive guide with threading patterns, benchmarking methods, and tuning recommendations
- **Threading Examples**: Practical concurrent_reading.py and concurrent_writing.py examples
- **API Documentation**: Updated PyMarcReader/PyMarcWriter docstrings with threading guidance
- **README Threading Section**: Concrete speedup numbers (2.04x for 2 threads, 3.20x for 4)
- **Benchmark Documentation**: Updated docs/benchmarks/ with Phase H results

### Changed

- **Python Wrapper API**: No breaking changes (fully backward compatible)
- **Performance Profile**: Single-thread throughput stable, multi-thread speedup now available
- **Reader Construction**: Accepts str/Path/bytes/bytearray in addition to file objects

### Fixed

- **GIL Release Bug (Phase B)**: SmallVec copy pattern properly avoids borrow checker violations
- **Error Handling (Phase B)**: ParseError conversion happens after GIL re-acquisition
- **GIL Crossing (Phase C)**: py.detach() correctly releases GIL during Phase 2 parsing

### Performance

- **Threading Speedup**: 2.04x (2 threads), 3.20x (4 threads) vs sequential reading
- **Memory Overhead**: SmallVec buffering <3% overhead vs single-threaded
- **GIL Contention**: Reduced from linear to O(n/100) with BatchedMarcReader
- **File I/O**: Pure Rust backend (file paths) eliminates all GIL overhead

### Technical Details

- **Phase H Integration**: Producer-consumer pipeline with Rayon parallel record scanning
- **Backpressure**: Queue-based buffering prevents runaway producer threads
- **Thread Safety**: Each thread requires own reader instance (not Send/Sync by design)
- **Efficiency**: pymrrc 92% efficient vs pure Rust Rayon baseline

## [0.2.0] - 2025-12-31

### Added

#### Python Integration (Phase 2-3)
- **PyO3 Python Wrapper**: Full Python bindings via PyO3/Maturin
- **Python Parallel Benchmarks**: Threading and multiprocessing performance analysis
- **pymarc Compliance Test Suite**: 75+ tests validating compatibility with pymarc API
- **Memory Usage Profiling**: Benchmarks comparing Python wrapper overhead vs Rust native
- **Context Manager Support**: Python `with` statement support for file I/O

#### Performance & Benchmarking (Phase 1-4)
- **Rust Parallel Benchmarks**: Performance testing with rayon for concurrent MARC processing
- **Rayon Parser Pool (H.4b)**: Parallel batch processing functions via Rayon thread pool
  - `parse_batch_parallel()` - Unlimited parallel parsing with dynamic work distribution
  - `parse_batch_parallel_limited()` - Bounded parallel parsing respecting configured thread pool
  - Record boundary scanning and parallel record assembly
  - Thread-safe batch processing for large MARC files
- **Comprehensive Benchmark Suite**: 
  - 1K/10K/100K record read performance
  - Field access overhead measurements
  - JSON/XML serialization performance
  - Roundtrip (read+write) benchmarks
  - Sequential vs parallel processing comparison
- **Benchmark Results Documentation**: Real measured performance data with pymarc comparisons
- **CI Optimization**: Caching and performance gate integration

#### Documentation & Organization
- **Design Documentation**: Architecture and design decision documentation
- **Benchmarking Documentation**: Feasibility studies and performance analysis
- **Executive Summaries**: High-level overview of parallel processing capabilities
- **Reorganized Docs**: Consolidated design/ and history/ into docs/ hierarchy
- **Examples**: Real-world usage patterns demonstrating library features
- **Migration Guide**: Comprehensive guide highlighting near-100% API compatibility with pymarc

#### API Enhancements
- **Record.to_marc21()**: Convert records back to ISO 2709 binary format
- **Enhanced Python API**: Full feature parity with Rust API in Python wrapper
- **MARCWriter Fixes**: Fixed write() method and improved record serialization

### Fixed
- Fixed deprecated PyO3 type alias warning
- Fixed 20+ clippy linting violations in benchmark files (missing semicolons in closure statements)
- Suppressed benchmark-specific documentation warnings at file level
- Fixed MARCWriter record serialization issues
- Cleaned up CI pipeline to pass all quality gates
- **H.4a Bug Fix**: RecordBoundaryScanner now correctly scans for 0x1D (record terminator) instead of 0x1E (field terminator) per ISO 2709 specification

### Changed
- Improved documentation structure and organization
- Enhanced Python test coverage with comprehensive pymarc compatibility suite

## [0.1.0] - 2025-12-28

### Added

#### Core Features
- **ISO 2709 Binary Format**: Full read/write support for MARC records in the standard binary interchange format
- **Record Types**: Support for three MARC record types:
  - Bibliographic records (standard MARC records)
  - Authority records (Type Z) for standardized headings and cross-references
  - Holdings records (Types x/y/v/u) for item location and enumeration data
- **Builder Pattern API**: Fluent, idiomatic Rust interface for record construction
- **Field Access API**: Comprehensive methods for reading, filtering, and iterating over fields
  - `fields_by_tag()` - Get fields by tag
  - `fields_by_indicator()` - Filter by indicators
  - `fields_in_range()` - Get fields within a tag range
  - `fields_with_subfield()` - Get fields containing specific subfields

#### Advanced Query DSL (Phase 1-3)
- **FieldQuery Builder**: Complex criteria-based field matching (tags, indicators, subfields)
- **TagRangeQuery**: Range-based field lookups (e.g., 600-699)
- **Subfield Pattern Matching**: Regex-based field filtering
- **Linked Field Navigation**: Support for MARC 880 (Alternate Graphical Representation) fields
  - Parse linkage information from subfield 6
  - Bidirectional lookups between original and 880 fields
- **Authority Control Helpers**: Query traits for authority-specific operations
- **Format-Specific Traits**: 
  - `BibliographicQueries` for bibliographic records
  - `AuthorityQueries` for authority records
  - `HoldingsQueries` for holdings records

#### Serialization Formats
- **JSON**: Generic JSON representation with fields as keys
- **MARCJSON**: Standard JSON-LD format for MARC records
- **XML**: XML representation with proper field/subfield structure
- **CSV**: Tabular export format for spreadsheet applications
- **Dublin Core**: Simplified 15-element metadata schema
- **MODS**: Metadata Object Description Schema for detailed descriptions

#### Character Encoding Support
- **MARC-8 (Legacy)**: Full support for legacy MARC-8 encoding with:
  - Basic Latin (ASCII)
  - ANSEL Extended Latin with diacritical marks
  - Hebrew (ESC ) 2)
  - Arabic (ESC ) 3, 4)
  - Cyrillic (ESC ( N)
  - Greek (ESC ( S)
  - Subscripts/Superscripts/Special character sets
  - East Asian (Chinese, Japanese, Korean via EACC)
- **UTF-8 (Modern)**: Full Unicode support for modern MARC records
- **Automatic Detection**: Encoding detection from MARC leader position 9

#### Helper Methods
- **RecordHelpers Trait**: Available on all record types via blanket implementation
  - `title()` - Extract main title
  - `author()` / `authors()` - Extract author names
  - `subjects()` - Extract subject headings
  - `isbns()` - Extract ISBN values
  - `issns()` - Extract ISSN values
  - `publication_info()` - Extract publication details
  - Record type helpers: `is_book()`, `is_music()`, `is_map()`, `is_serial()`, `is_audiovisual()`, `is_electronic_resource()`

#### API Refactoring
- **MarcRecord Trait**: Common interface for all record types (control field operations)
- **GenericRecordBuilder<T>**: Unified builder for all record types
- **FieldCollection Trait**: Standardized field collection management
- Unified field storage pattern across Record, AuthorityRecord, and HoldingsRecord

#### Error Handling & Recovery
- **MarcError Type**: Comprehensive error handling for MARC operations
- **Recovery Mode**: Graceful handling of truncated/malformed MARC records
- **Result Type**: Convenient `Result<T>` alias for library operations

#### Testing
- 282+ comprehensive unit and integration tests
- Test data with:
  - Simple bibliographic records
  - Music scores
  - Records with control fields (008)
  - Multiple records in one file
  - Authority records
  - Holdings records
  - Multilingual records
  - MARC-8 encoded records

#### Documentation
- Comprehensive API documentation with doc comments
- Module-level documentation with examples
- Examples directory with real-world usage patterns:
  - Creating records with builders
  - Reading and querying fields
  - Converting between formats
  - Working with authority and holdings records
  - MARC-8 encoding demonstration
  - Multilingual record handling
  - CSV export

### Design Principles

1. **Rust-Idiomatic**: Leverages iterators, Result types, and ownership patterns naturally
2. **Zero-Copy Where Possible**: Efficient memory usage for large record sets
3. **Format Flexibility**: Support for multiple serialization formats out of the box
4. **Compatibility**: Maintains data fidelity with pymarc and standard MARC tools
5. **Extensible**: Trait-based architecture allows easy addition of new query types and formats

### Known Limitations

None known at this time. The following have been resolved:
- ✓ Field indicator validation with MARC21 semantics (implemented in 0.1.0)
- ✓ MARC-8 combining character handling with Unicode NFC normalization (implemented in 0.1.0)

### Technical Details

- **Minimum Rust Version**: 1.70
- **Dependencies**: serde, serde_json, regex, unicode-normalization
- **License**: MIT

## Project Information

- **Repository**: https://github.com/dchud/mrrc
- **Documentation**: https://docs.rs/mrrc
- **Crates.io**: https://crates.io/crates/mrrc
- **Issue Tracking**: GitHub Issues and Beads issue tracking system
- **Status**: Active development, experimental (APIs may change)
