xlsxrb 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -0
  3. data/CHANGELOG.md +54 -0
  4. data/README.md +1 -0
  5. data/Rakefile +66 -0
  6. data/benchmark.rb +3 -1
  7. data/docs/ARCHITECTURE.md +1 -1
  8. data/docs/DEVELOPMENT.md +6 -2
  9. data/docs/MUTATION_TESTING.md +52 -0
  10. data/docs/PEER_LIBRARIES.md +14 -14
  11. data/docs/QUALITY_ASSURANCE.md +1 -0
  12. data/docs/assets/benchmark_results.svg +49 -49
  13. data/docs/visual/VisualGallery.md +38 -27
  14. data/docs/wasm/ruby.wasm +0 -0
  15. data/lib/xlsxrb/chart_builder.rb +18 -18
  16. data/lib/xlsxrb/dsl_helpers.rb +116 -0
  17. data/lib/xlsxrb/elements/cell.rb +122 -34
  18. data/lib/xlsxrb/elements/column.rb +27 -3
  19. data/lib/xlsxrb/elements/row.rb +27 -8
  20. data/lib/xlsxrb/elements/workbook.rb +16 -14
  21. data/lib/xlsxrb/elements/worksheet.rb +17 -1
  22. data/lib/xlsxrb/ooxml/cfb.rb +9 -6
  23. data/lib/xlsxrb/ooxml/crypto/agile.rb +5 -5
  24. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +784 -0
  25. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +3478 -0
  26. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +1796 -0
  27. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +428 -0
  28. data/lib/xlsxrb/ooxml/reader/listeners.rb +4 -6446
  29. data/lib/xlsxrb/ooxml/utils.rb +5 -6
  30. data/lib/xlsxrb/ooxml/workbook_writer.rb +287 -226
  31. data/lib/xlsxrb/ooxml/worksheet_parser.rb +115 -208
  32. data/lib/xlsxrb/ooxml/worksheet_writer.rb +122 -9
  33. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +1602 -0
  34. data/lib/xlsxrb/ooxml/writer/features_xml.rb +731 -0
  35. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +348 -0
  36. data/lib/xlsxrb/ooxml/writer.rb +7 -2645
  37. data/lib/xlsxrb/ooxml/xml_builder.rb +11 -2
  38. data/lib/xlsxrb/ooxml/zip_generator.rb +26 -5
  39. data/lib/xlsxrb/ooxml/zip_writer.rb +31 -38
  40. data/lib/xlsxrb/stream_row.rb +30 -6
  41. data/lib/xlsxrb/stream_sheet.rb +2 -8
  42. data/lib/xlsxrb/stream_writer.rb +186 -133
  43. data/lib/xlsxrb/style_builder.rb +10 -1
  44. data/lib/xlsxrb/version.rb +1 -1
  45. data/lib/xlsxrb/worksheet_builder.rb +16 -50
  46. data/lib/xlsxrb.rb +1 -0
  47. data/sig/generated/xlsxrb/chart_builder.rbs +36 -36
  48. data/sig/generated/xlsxrb/dsl_helpers.rbs +60 -0
  49. data/sig/generated/xlsxrb/elements/cell.rbs +166 -12
  50. data/sig/generated/xlsxrb/elements/column.rbs +32 -21
  51. data/sig/generated/xlsxrb/elements/row.rbs +94 -14
  52. data/sig/generated/xlsxrb/elements/workbook.rbs +95 -10
  53. data/sig/generated/xlsxrb/elements/worksheet.rbs +9 -0
  54. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +173 -0
  55. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +400 -0
  56. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +442 -0
  57. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +80 -0
  58. data/sig/generated/xlsxrb/ooxml/reader/listeners.rbs +0 -1072
  59. data/sig/generated/xlsxrb/ooxml/utils.rbs +2 -0
  60. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +17 -1
  61. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +16 -2
  62. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +4 -0
  63. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +79 -0
  64. data/sig/generated/xlsxrb/ooxml/writer/features_xml.rbs +83 -0
  65. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +53 -0
  66. data/sig/generated/xlsxrb/ooxml/writer.rbs +6 -185
  67. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +7 -0
  68. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +15 -0
  69. data/sig/generated/xlsxrb/stream_row.rbs +3 -0
  70. data/sig/generated/xlsxrb/stream_writer.rbs +7 -1
  71. data/sig/generated/xlsxrb/style_builder.rbs +7 -0
  72. metadata +19 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f865582e17bc03ede4f300f348f6166a666d82cd088f42a501a6518979a7b999
4
- data.tar.gz: 25ba8105a6ca0318aa9bb4766a0de59da4818d18d226d212f1ec941fde3082c9
3
+ metadata.gz: 15a818d0e9e58b9ada1a11c90db9f9baa55fc76b3a7e62303d33a61f2e6f9896
4
+ data.tar.gz: c7b5f4af516d9911a1ee37ef5413939e8425bc270304900132e0962fad18905d
5
5
  SHA512:
6
- metadata.gz: 6e35bcdabcff60af9084c9860fcf1838abfb507065a2eabb1004f40f16ab6cf6f7a11d136bbd83e97f46f0e460b906601db1584ef23b00c05076bfc240417baf
7
- data.tar.gz: 8d4597dad8fe327f4ad887934a7f2bc18fcd65b606ef265ab38bcef0a0ae2565dc09f9e7f04dd8dc5bf7c3403fadc2a265d3ce074c9fd623489d8f4004065802
6
+ metadata.gz: 9deec47109a21a62aafb4a93efcee64f61f5ffb4566fd009cd1d5fdb9557e1193a53155d60ff7e4cc565317d49fb4ce8b3b89542234dff6a900981cd9220d14a
7
+ data.tar.gz: b536ef03e3ae01c49ff8e00c693d86fb72a9248cae1ccf93802962127d9b86ef676b5b17ca4b896613961bab8869139644e439542b1bca5adb6a7452d971f124
data/.mutant.yml ADDED
@@ -0,0 +1,8 @@
1
+ usage: opensource
2
+ requires:
3
+ - test_helper
4
+ - xlsxrb
5
+ includes:
6
+ - lib
7
+ - test
8
+ integration: test-unit
data/CHANGELOG.md CHANGED
@@ -1,3 +1,57 @@
1
+ ## [0.1.12] - 2026-09-20
2
+
3
+ ### Added
4
+ - **Mutation Testing Framework (`mutant`)**:
5
+ - Full-scale integration of mutant test suite targeting the Functional Core (10 classes and modules, 38 subjects).
6
+ - Achieved **100.00% Kill Rate (1,091 / 1,091 mutations killed, 0 alive)** without exclusions.
7
+ - Added dedicated root configuration [`.mutant.yml`](.mutant.yml) for streamlined CLI execution (`bundle exec mutant run -- '...'`).
8
+ - Integrated `bundle exec rake mutant:pure` into GitHub Actions CI pipeline (`main.yml`), guaranteeing mutation-tested quality on every push and pull request.
9
+ - Added comprehensive architecture and testing guide in [`docs/MUTATION_TESTING.md`](docs/MUTATION_TESTING.md).
10
+ - **Pre-Push Quality Verification Script (`bin/pre-push`)**: Added automated pre-push verification script running RuboCop, Steep type checking, RBS sync validation, Unit & Contract tests, RBS runtime type validation, and pure logic mutation testing, with automatic hook installation in `bin/setup`.
11
+
12
+ ### Changed
13
+ - **Functional Core & Imperative Shell Architecture**:
14
+ - Purified core business logic and domain entities into deterministic, side-effect-free class methods and predicates:
15
+ - Extracted coordinate and validation predicates: `Elements::Cell.valid_value?`, `Cell.valid_coordinates?`, `Cell.calculate_column_letter`, `Cell.calculate_column_index`.
16
+ - Extracted domain validation predicates: `Elements::Row.valid_index?`, `Elements::Column.valid_index?`, `Elements::Worksheet.valid_name?`.
17
+ - Replaced date/time serial calculations in `Ooxml::Utils` with pure mathematical conversions (Julian Day math for `date_to_serial`, `divmod` arithmetic for `serial_to_datetime`, and rigorous 1900 leap year bug handling).
18
+ - Extracted binary packing utilities in `Ooxml::ZipGenerator` (`le16`, `le32`, and bitfield arithmetic for `dos_datetime`).
19
+ - Extracted pure XML character escaping logic in `Ooxml::XmlBuilder.escape`.
20
+ - Modularized `Ooxml::Writer` into domain-specific mixin modules (`DrawingXml`, `FeaturesXml`, `StylesXml`).
21
+ - Extracted shared DSL parameter normalization and range expansion into `Xlsxrb::DslHelpers`.
22
+ - Modularized reader SAX listeners into domain-specific parser components.
23
+
24
+ ### Fixed
25
+ - **Boundary Validation & Error Reporting**:
26
+ - Strictly validate row number digits in `Elements::Cell.parse_ref` to reject malformed cell coordinate strings (e.g. `A0`, `A01`).
27
+ - Harmonized supported value type checks and nil/empty handling across `Elements::Cell.validate`.
28
+ - Corrected maximum row index boundary error messages in `Elements::Row.validate`.
29
+ - Hardened cryptographic stream boundary checks and corrupted header handling in Standard and Agile encryption modes.
30
+ - **RBS Runtime Type Validation**:
31
+ - Expanded `Elements::Workbook#initialize`, `#sheet`, and `[]` type annotations to accept `StreamSheet` instances used by streaming readers.
32
+ - Made update block parameter optional in `Elements::Workbook#update_sheet` to allow testing missing block validation.
33
+ - Supported `Numeric` types (Float and Integer) in `ChartBuilder` and `SeriesBuilder` kwargs (`width`, etc.).
34
+ - Handled `RBS::Test::Tester::TypeError` in unit tests verifying block return type validation.
35
+
36
+ ## [0.1.11] - 2026-08-19
37
+
38
+ ### Performance
39
+ - **Sub-Second Streaming Write (< 1.0s / 1,000,000 cells)**:
40
+ - Achieved sub-second streaming write performance (**0.97 s** median for 1M cells) while maintaining 100% full SST (Shared String Table) XML deduplication and ISO/IEC 29500 compatibility.
41
+ - Implemented 1-Pass Direct ZIP streaming pipeline, eliminating all intermediate tempfiles, disk seek-backs, and extra deflate flush cycles.
42
+ - Added dedicated unstyled fast-path in `WorksheetWriter#write_row_values` with precomputed coordinate and integer lookup tables.
43
+ - Pre-registered default date/time formatting styles, completely eliminating redundant per-row scan loops.
44
+ - Streamlined `ZipWriter` instance variable lookups to eliminate per-chunk hash overhead.
45
+ - Added `alias << row` to `WorksheetProxy` and `StreamWriter` for idiomatic Ruby streaming.
46
+ - **High-Speed Streaming Read (1.61s / 1,000,000 cells)**:
47
+ - Reduced streaming read execution time from **3.40 s** down to **1.61 s** (over 2x speedup).
48
+ - Refactored `Elements::Cell` into an optimized lightweight class with `Cell.fast_create`, reducing 1M cell instantiation overhead by 5.2x (0.92s → 0.17s) while maintaining full pattern matching (`deconstruct`, `deconstruct_keys`) and immutability.
49
+ - Replaced per-byte Ruby scanning in `WorksheetParser.fast_scan_cells_direct` with an Onigmo C-level regular expression scanner (`CELL_FAST_RE`), eliminating 100,000 intermediate XML substring allocations and nested byte loops.
50
+ - Optimized `StreamRow#cells` to populate cell arrays via direct block traversal, eliminating Enumerator object allocations.
51
+
52
+ ### Documentation
53
+ - Updated benchmark results, performance comparisons, and linear-scale SVG charts across `README.md` and `docs/PEER_LIBRARIES.md`.
54
+
1
55
  ## [0.1.10] - 2026-08-19
2
56
 
3
57
  ### Added
data/README.md CHANGED
@@ -166,6 +166,7 @@ Backed by an enterprise-grade QA architecture to guarantee absolute reliability:
166
166
  * **Official Microsoft Open XML SDK Validation**: Validates generated OOXML structures against Microsoft's official SDK.
167
167
  * **Visual Regression Testing (VRT)**: Headless LibreOffice Calc pixel-by-pixel rendering checks.
168
168
  * **Contract & Round-Trip Tests**: Verifies parity between Streaming and In-Memory APIs and round-trip read/write accuracy.
169
+ * **Mutation Testing (Mutant)**: 100% mutant kill rate (1,091/1,091 mutations across 38 subjects) on pure algorithms, predicates, and coordinates ([docs/MUTATION_TESTING.md](docs/MUTATION_TESTING.md)).
169
170
  * **Security & DoS Protection**: Formula injection mitigation and ZIP bomb protection.
170
171
 
171
172
  For full architectural details, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/QUALITY_ASSURANCE.md](docs/QUALITY_ASSURANCE.md).
data/Rakefile CHANGED
@@ -462,6 +462,12 @@ task doc: %i[wasm fetch_assets] do
462
462
  FileUtils.mkdir_p("doc/docs/visual/files")
463
463
  FileUtils.cp_r(Dir.glob("docs/visual/files/*"), "doc/docs/visual/files")
464
464
 
465
+ FileUtils.mkdir_p("doc/docs/assets")
466
+ FileUtils.cp_r(Dir.glob("docs/assets/*"), "doc/docs/assets")
467
+
468
+ # Alias index.html as README_md.html for links referencing README.md
469
+ FileUtils.cp("doc/index.html", "doc/README_md.html") if File.exist?("doc/index.html")
470
+
465
471
  # --- WebAssembly Playground Integration ---
466
472
  puts "Integrating WebAssembly Playground..."
467
473
 
@@ -565,3 +571,63 @@ desc "Run static type checking with Steep"
565
571
  task typecheck: :sig do
566
572
  sh "bundle exec steep check"
567
573
  end
574
+
575
+ namespace :mutant do
576
+ desc "Run mutation testing on pure logic and algorithms (coordinates, serial, conversion)"
577
+ task :pure do
578
+ default_subjects = [
579
+ "Xlsxrb::Elements::CoordinateAccess#cells",
580
+ "Xlsxrb::Elements::CoordinateAccess#[]",
581
+ "Xlsxrb::Elements::Cell#ref",
582
+ "Xlsxrb::Elements::Cell#to_i",
583
+ "Xlsxrb::Elements::Cell#to_f",
584
+ "Xlsxrb::Elements::Cell#content",
585
+ "Xlsxrb::Elements::Cell#to_s",
586
+ "Xlsxrb::Elements::Cell#valid?",
587
+ "Xlsxrb::Elements::Cell.calculate_column_letter",
588
+ "Xlsxrb::Elements::Cell.calculate_column_index",
589
+ "Xlsxrb::Elements::Cell.valid_coordinates?",
590
+ "Xlsxrb::Elements::Cell.valid_value?",
591
+ "Xlsxrb::Ooxml::Utils.serial_to_date",
592
+ "Xlsxrb::Ooxml::Utils.date_to_serial",
593
+ "Xlsxrb::Ooxml::Utils.datetime_to_serial",
594
+ "Xlsxrb::Ooxml::Utils.serial_to_datetime",
595
+ "Xlsxrb::Ooxml::ZipGenerator.le16",
596
+ "Xlsxrb::Ooxml::ZipGenerator.le32",
597
+ "Xlsxrb::StreamRow#valid?",
598
+ "Xlsxrb::StreamRow#unmapped_data",
599
+ "Xlsxrb::StreamRow#errors",
600
+ "Xlsxrb::StreamRow#values",
601
+ "Xlsxrb::Elements::Worksheet.valid_name?",
602
+ "Xlsxrb::Elements::Worksheet#valid?",
603
+ "Xlsxrb::Elements::Worksheet#load",
604
+ "Xlsxrb::Ooxml::Cfb::Reader.cfb?",
605
+ "Xlsxrb::Ooxml::Cfb::DirEntry#stream?",
606
+ "Xlsxrb::Ooxml::Cfb::DirEntry#root?",
607
+ "Xlsxrb::Ooxml::Cfb::DirEntry#storage?",
608
+ "Xlsxrb::Elements::Column.valid_index?",
609
+ "Xlsxrb::Elements::Column.validate",
610
+ "Xlsxrb::Elements::Column#valid?",
611
+ "Xlsxrb::Elements::Row.valid_index?",
612
+ "Xlsxrb::Elements::Row#valid?",
613
+ "Xlsxrb::Elements::Workbook#valid?",
614
+ "Xlsxrb::Ooxml::XmlBuilder.escape",
615
+ "Xlsxrb::Ooxml::ZipGenerator.dos_datetime",
616
+ "Xlsxrb::DslHelpers.normalize_column_indices"
617
+ ].join(" ")
618
+ subjects = ENV["MUTANT_SUBJECTS"] || default_subjects
619
+ test_files = [
620
+ "./test/xlsxrb/elements_test.rb",
621
+ "./test/xlsxrb/ooxml_test.rb",
622
+ "./test/xlsxrb/ooxml/utils_test.rb",
623
+ "./test/xlsxrb/ooxml/crypto_test.rb",
624
+ "./test/xlsxrb/ooxml/cfb_test.rb",
625
+ "./test/zip_generator_test.rb",
626
+ "./test/xlsxrb/stream_row_test.rb",
627
+ "./test/xlsxrb/style_builder_test.rb",
628
+ "./test/xlsxrb/dsl_helpers_test.rb"
629
+ ].select { |f| File.exist?(f) }
630
+ requires = test_files.map { |f| "-r #{f}" }.join(" ")
631
+ sh "bundle exec mutant run --usage opensource #{requires} -- #{subjects}"
632
+ end
633
+ end
data/benchmark.rb CHANGED
@@ -9,6 +9,7 @@ require "bundler/inline"
9
9
  puts "Ensuring benchmark peer ecosystem gems are available (bundler/inline)..."
10
10
  gemfile(true) do
11
11
  source "https://rubygems.org"
12
+ gem "rubyzip", ">= 2.3.0"
12
13
  gem "caxlsx", "4.5.0"
13
14
  gem "xlsxtream", "3.1.0"
14
15
  gem "fast_excel", "0.5.0", platform: :mri
@@ -187,7 +188,7 @@ RUNNER_SCRIPT = <<~'RUBY'
187
188
  when ["xlsxrb_inmemory", "read"]
188
189
  require_relative "lib/xlsxrb"
189
190
  measure do
190
- wb = Xlsxrb.read(filename)
191
+ wb = Xlsxrb.read(filename).load
191
192
  count = 0
192
193
  wb.sheets.each do |sheet|
193
194
  sheet.rows.each do |row|
@@ -237,6 +238,7 @@ RUNNER_SCRIPT = <<~'RUBY'
237
238
  end
238
239
  end
239
240
  when ["simple_xlsx_reader", "read"]
241
+ gem "rubyzip", "~> 1.3.0"
240
242
  require "simple_xlsx_reader"
241
243
  measure do
242
244
  doc = SimpleXlsxReader.open(filename)
data/docs/ARCHITECTURE.md CHANGED
@@ -509,6 +509,6 @@ To ensure library robustness and consistency across execution paths, we organize
509
509
  - Run via: `bundle exec rake test:e2e`
510
510
 
511
511
  4. **Visual Examples & VRT (`test/visual/`):**
512
- - **Living Documentation:** Compiles visual DSL scripts under `examples/visual/` into the [Visual Examples Gallery](visual/README.md).
512
+ - **Living Documentation:** Compiles visual DSL scripts under `examples/visual/` into the [Visual Examples Gallery](visual/VisualGallery.md).
513
513
  - **Visual Regression Testing:** Renders the generated spreadsheets into PNG files using headless LibreOffice Calc, and calculates pixel differences against reference baselines using ImageMagick.
514
514
  - Run via: `bundle exec rake test:visual`
data/docs/DEVELOPMENT.md CHANGED
@@ -32,7 +32,11 @@ To run the different tiers of our testing strategy:
32
32
  ```bash
33
33
  bundle exec rake test:visual
34
34
  ```
35
- 7. **Run All Tests:**
35
+ 7. **Mutation Testing (Pure Logic & Algorithms):**
36
+ ```bash
37
+ bundle exec rake mutant:pure
38
+ ```
39
+ 8. **Run All Tests:**
36
40
  ```bash
37
41
  bundle exec rake test
38
42
  ```
@@ -41,7 +45,7 @@ To run the different tiers of our testing strategy:
41
45
 
42
46
  ## Development Workflow
43
47
 
44
- High-level API expansion follows the Facade rules documented in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). In short: if a low-level writer feature is stable, the default expectation is that it should eventually be exposed through the high-level DSL as well, with consistent naming, both streaming and in-memory coverage, backward-compatible options/block forms where practical, and matching Facade-level tests.
48
+ High-level API expansion follows the Facade rules documented in [ARCHITECTURE.md](ARCHITECTURE.md). In short: if a low-level writer feature is stable, the default expectation is that it should eventually be exposed through the high-level DSL as well, with consistent naming, both streaming and in-memory coverage, backward-compatible options/block forms where practical, and matching Facade-level tests.
45
49
 
46
50
  To ensure systematic progress, reliable round-trip compatibility, and strict adherence to the ECMA-376 specification, we follow this iterative development cycle for each new feature:
47
51
 
@@ -0,0 +1,52 @@
1
+ # Mutation Testing in xlsxrb
2
+
3
+ To ensure enterprise-grade reliability and avoid "shallow test coverage" (where line coverage is high but assertions are missing or weak), `xlsxrb` adopts **Mutation Testing** via [`mbj/mutant`](https://github.com/mbj/mutant) with `test-unit` integration.
4
+
5
+ As of the current release, the test suite achieves **100.00% kill rate (1,091 / 1,091 mutations killed, 0 alive)** across all 38 pure functional subjects.
6
+
7
+ ## Motivation & Strategy
8
+
9
+ Standard code coverage measures whether lines of code were executed during tests. However, it cannot guarantee that the tests will fail if the code's behavior is altered or corrupted. Mutation testing systematically modifies source code (introducing "mutants" such as flipping conditionals, substituting constants, or removing statements) and checks whether existing tests catch the mutation ("kill" the mutant).
10
+
11
+ ### Targeted Scope: Functional Core, Pure Predicates & Byte Encoding
12
+
13
+ Running mutation testing across the entire codebase (which includes heavy file I/O, ZIP streaming, and large XML parsing) is computationally expensive and prone to creating brittle tests for streaming writers. Therefore, `xlsxrb` follows the **Functional Core / Imperative Shell** architecture, focusing mutation testing strictly on pure functions, boundary predicates, coordinate conversions, and binary encoders:
14
+
15
+ 1. **Coordinates & Cell Conversions (`Xlsxrb::Elements::Cell`, `CoordinateAccess`)**:
16
+ - Column letters to 0-based indices (`"A"` ↔ `0`, `"Z"` ↔ `25`, `"AA"` ↔ `26`, up to `"XFD"` ↔ `16383`).
17
+ - Pure predicate checks (`Cell.valid_coordinates?`, `Cell.valid_value?`, `Cell#valid?`).
18
+ - String, integer, and float conversions (`Cell#to_s`, `Cell#to_i`, `Cell#to_f`, `Cell#content`, `Cell#ref`).
19
+ - Coordinate access references and cell sorting (`CoordinateAccess#cells`, `CoordinateAccess#[]`).
20
+ 2. **Worksheet, Column, Row & Workbook Invariants (`Xlsxrb::Elements::*`, `Xlsxrb::StreamRow`)**:
21
+ - Worksheet name validation (`Worksheet.valid_name?` with 1..31 characters and forbidden character rules `\ / ? * [ ]`).
22
+ - Column and row bounds (`Column.valid_index?`, `Column.validate`, `Column#valid?`, `Row.valid_index?`, `Row#valid?`).
23
+ - Workbook structure validation (`Workbook#valid?`).
24
+ - Streaming row data integrity (`StreamRow#valid?`, `StreamRow#unmapped_data`, `StreamRow#errors`, `StreamRow#values`).
25
+ 3. **Serial Value & DateTime Conversions (`Xlsxrb::Ooxml::Utils`)**:
26
+ - Julian Day ↔ 1900-based serial number conversions (`date_to_serial`, `serial_to_date`).
27
+ - Time fractions and fractional days arithmetic (`datetime_to_serial`, `serial_to_datetime`).
28
+ - Excel 1900 leap year bug handling.
29
+ 4. **Binary Parsing & Bitfield Packings (`Xlsxrb::Ooxml::ZipGenerator`, `Xlsxrb::Ooxml::Cfb`)**:
30
+ - Little-endian 16/32-bit byte serialization (`ZipGenerator.le16`, `ZipGenerator.le32`).
31
+ - MS-DOS packed datetime calculation with arithmetic bitfield combinations (`ZipGenerator.dos_datetime`).
32
+ - Compound File Binary (CFB) magic header recognition (`Cfb::Reader.cfb?`) and directory entry predicates (`DirEntry#stream?`, `#root?`, `#storage?`).
33
+ 5. **XML Character Escaping (`Xlsxrb::Ooxml::XmlBuilder`)**:
34
+ - Pure escaping of XML special characters (`XmlBuilder.escape`) with object identity preservation for clean strings.
35
+ 6. **DSL Normalization (`Xlsxrb::DslHelpers`)**:
36
+ - Column indices, range, and alphabet normalization (`DslHelpers.normalize_column_indices`).
37
+
38
+ ### Excluded Scope (Non-Targets)
39
+
40
+ - **Streaming XML Writers & Parsers**: Validated via Microsoft Open XML SDK schema checks and round-trip parsing tests rather than mutant to avoid brittle assertions.
41
+ - **Visual Regression Testing (VRT)**: Headless rendering with LibreOffice Calc is validated via image diffs in CI.
42
+ - **Cryptographic Key Stretching**: Password hashing with 100,000 spin iterations is validated by specific contract tests to avoid slow feedback cycles.
43
+
44
+ ## Running Mutation Testing
45
+
46
+ ```bash
47
+ # Run full mutation testing suite (38 subjects, ~35 seconds)
48
+ bundle exec rake mutant:pure
49
+
50
+ # Or run mutant against a specific class or method using .mutant.yml configuration
51
+ bundle exec mutant run -- 'Xlsxrb::Elements::Cell.valid_value?'
52
+ ```
@@ -88,25 +88,25 @@ The following benchmarks evaluate processing **1,000,000 cells** (100,000 rows
88
88
 
89
89
  | Library | Version | Model | String Storage | Time (Median) | Time (Mean) | Peak Memory | GC Count |
90
90
  | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
91
- | **xlsxtream** | 3.1.0 | Streaming | Inline String | **1.19 s** | 1.20 s | **18.2 MB** | 1072.0 |
92
- | **xlsxrb (Streaming)** | - | Streaming | SST (Shared) | **1.73 s** | 1.65 s | 94.4 MB | 39.0 |
93
- | **fast_excel** | 0.5.0 | Streaming | SST (Shared) | 1.89 s | 1.89 s | 148.2 MB | 245.0 |
94
- | **xlsxrb (In-Memory)** | - | In-Memory | SST (Shared) | 3.84 s | 3.83 s | 278.3 MB | 32.0 |
95
- | **write_xlsx** | 1.15.0 | In-Memory | SST (Shared) | 4.32 s | 4.34 s | 201.2 MB | 33.0 |
96
- | **caxlsx** | 4.5.0 | In-Memory | Inline String | 5.15 s | 5.12 s | 188.6 MB | 23.0 |
97
- | **rubyXL** | 3.4.38 | In-Memory | Inline String | 38.81 s | 37.82 s | 2186.8 MB | 103.0 |
91
+ | **xlsxrb (Streaming)** | - | Streaming | SST (Shared) | **0.95 s** | **0.95 s** | 67.6 MB | **34.0** |
92
+ | **xlsxtream** | 3.1.0 | Streaming | Inline String | 1.16 s | 1.17 s | **17.0 MB** | 323.0 |
93
+ | **fast_excel** | 0.5.0 | Streaming | SST (Shared) | 2.02 s | 2.14 s | 148.3 MB | 210.0 |
94
+ | **xlsxrb (In-Memory)** | - | In-Memory | SST (Shared) | 2.42 s | 2.57 s | 209.0 MB | 16.0 |
95
+ | **write_xlsx** | 1.15.0 | In-Memory | SST (Shared) | 4.39 s | 4.42 s | 217.4 MB | 25.0 |
96
+ | **caxlsx** | 4.5.0 | In-Memory | Inline String | 5.28 s | 5.28 s | 177.4 MB | 24.0 |
97
+ | **rubyXL** | 3.4.38 | In-Memory | Inline String | 43.13 s | 43.65 s | 2167.2 MB | 101.0 |
98
98
 
99
99
  ### Read Performance (1,000,000 cells)
100
100
 
101
101
  | Library | Version | Model | Time (Median) | Time (Mean) | Peak Memory | GC Count |
102
102
  | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
103
- | **xlsxrb (Streaming)** | - | Streaming | **3.17 s** | 3.22 s | 91.4 MB | 43.0 |
104
- | **simple_xlsx_reader** | 5.1.0 | Streaming | 4.48 s | 4.45 s | **38.5 MB** | 1669.0 |
105
- | **xlsxrb (In-Memory)** | - | In-Memory | 5.71 s | 5.89 s | 224.8 MB | 63.0 |
106
- | **creek** | 2.6.3 | Streaming | 8.14 s | 8.02 s | 834.6 MB | 477.0 |
107
- | **xsv** | 1.4.1 | Streaming | 14.61 s | 14.50 s | 76.1 MB | 2224.0 |
108
- | **roo** | 3.0.0 | Streaming | 15.69 s | 13.36 s | 119.7 MB | 441.0 |
109
- | **rubyXL** | 3.4.38 | In-Memory | 37.13 s | 40.35 s | 2537.6 MB | 146.0 |
103
+ | **xlsxrb (Streaming)** | - | Streaming | **1.73 s** | **1.72 s** | 136.2 MB | **45.0** |
104
+ | **xlsxrb (In-Memory)** | - | In-Memory | 4.33 s | 4.46 s | 304.3 MB | 77.0 |
105
+ | **simple_xlsx_reader** | 5.1.0 | Streaming | 4.38 s | 4.41 s | **32.8 MB** | 918.0 |
106
+ | **creek** | 2.6.3 | Streaming | 7.12 s | 7.05 s | 840.1 MB | 419.0 |
107
+ | **roo** | 3.0.0 | Streaming | 10.95 s | 10.33 s | 123.4 MB | 196.0 |
108
+ | **xsv** | 1.4.1 | Streaming | 14.56 s | 14.59 s | 73.6 MB | 2028.0 |
109
+ | **rubyXL** | 3.4.38 | In-Memory | 30.74 s | 30.78 s | 2289.6 MB | 146.0 |
110
110
 
111
111
  ---
112
112
 
@@ -15,6 +15,7 @@ Below is an overview of the inspection mechanisms, when they run, the quality at
15
15
  | **Unit & Contract Tests** | `rake test:unit test:contract` | ⭕ | ⭕ | - | Accuracy / Functional Reqs | Dynamic Analysis (Assertions) | Method specification violations, unexpected return values, edge-case failures. |
16
16
  | **Runtime Type Validation (RBS::Test)** | `rake test:rbs` | △ (Opt-in) | ⭕ | - | Type Safety (Dynamic) | Dynamic Analysis (Runtime Hooks) | Type errors slipping past static checks, divergence between RBS docs and implementation. |
17
17
  | **Property-Based Testing (PBT)** | `rake test:pbt` | ⭕ | ⭕ | - | Robustness / Exhaustiveness | Automated Random Generation | Crashes caused by "unexpected inputs" (e.g., empty strings, huge numbers, special symbols like `=`). |
18
+ | **Mutation Testing (Mutant)** | `rake mutant:pure` | ⭕ | ⭕ | - | Test Suite Rigor / Detection Power | Fault Injection / Mutation Analysis (100% kill on 1,091 mutants) | Shallow tests, unasserted edge cases, and surviving mutant bugs in pure algorithms, predicates, and coordinates logic. |
18
19
  | **Security Validation (DoS Protection)** | Included in `rake test:unit` | ⭕ | ⭕ | - | Availability / Safety | Dynamic Analysis (Malicious Input) | Memory/disk exhaustion from ZIP bombs, infinite parsing loops from malformed files. |
19
20
  | **Concurrency Validation (Thread/Ractor)** | Included in `rake test:unit` | ⭕ | ⭕ | - | Thread Safety | Dynamic Analysis (Parallel Execution) | Global variable pollution, data mixing during concurrent request processing. |
20
21
  | **XSD Schema Validation** | Included in `rake test:unit` | ⭕ | ⭕ | - | Compatibility / Compliance | Structural Validation | "We found a problem with some content in it" errors when opening in Excel. |
@@ -46,40 +46,40 @@
46
46
  <line x1="380.0" y1="65" x2="380.0" y2="320" class="grid"/>
47
47
  <text x="380.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">40s</text>
48
48
 
49
- <!-- xlsxtream 3.1.0: 1.19s -->
50
- <text x="150" y="85" text-anchor="end" class="label">xlsxtream 3.1.0</text>
51
- <rect x="160" y="72" width="6.5" height="18" rx="3" fill="#64748b"/>
52
- <text x="174.5" y="85" class="value">1.19 s</text>
49
+ <!-- xlsxrb (Streaming): 0.95s -->
50
+ <text x="150" y="85" text-anchor="end" class="label-accent">xlsxrb (Streaming)</text>
51
+ <rect x="160" y="72" width="5.2" height="18" rx="3" fill="#3b82f6"/>
52
+ <text x="173.2" y="85" class="value">0.95 s</text>
53
53
 
54
- <!-- xlsxrb (Streaming): 1.73s -->
55
- <text x="150" y="120" text-anchor="end" class="label-accent">xlsxrb (Streaming)</text>
56
- <rect x="160" y="107" width="9.5" height="18" rx="3" fill="#3b82f6"/>
57
- <text x="177.5" y="120" class="value">1.73 s</text>
54
+ <!-- xlsxtream 3.1.0: 1.16s -->
55
+ <text x="150" y="120" text-anchor="end" class="label">xlsxtream 3.1.0</text>
56
+ <rect x="160" y="107" width="6.4" height="18" rx="3" fill="#64748b"/>
57
+ <text x="174.4" y="120" class="value">1.16 s</text>
58
58
 
59
- <!-- fast_excel 0.5.0: 1.89s -->
59
+ <!-- fast_excel 0.5.0: 2.02s -->
60
60
  <text x="150" y="155" text-anchor="end" class="label">fast_excel 0.5.0</text>
61
- <rect x="160" y="142" width="10.4" height="18" rx="3" fill="#64748b"/>
62
- <text x="178.4" y="155" class="value">1.89 s</text>
61
+ <rect x="160" y="142" width="11.1" height="18" rx="3" fill="#64748b"/>
62
+ <text x="179.1" y="155" class="value">2.02 s</text>
63
63
 
64
- <!-- xlsxrb (In-Memory): 3.84s -->
64
+ <!-- xlsxrb (In-Memory): 2.42s -->
65
65
  <text x="150" y="190" text-anchor="end" class="label-accent">xlsxrb (In-Memory)</text>
66
- <rect x="160" y="177" width="21.1" height="18" rx="3" fill="#3b82f6"/>
67
- <text x="189.1" y="190" class="value">3.84 s</text>
66
+ <rect x="160" y="177" width="13.3" height="18" rx="3" fill="#3b82f6"/>
67
+ <text x="181.3" y="190" class="value">2.42 s</text>
68
68
 
69
- <!-- write_xlsx 1.15.0: 4.32s -->
69
+ <!-- write_xlsx 1.15.0: 4.39s -->
70
70
  <text x="150" y="225" text-anchor="end" class="label">write_xlsx 1.15.0</text>
71
- <rect x="160" y="212" width="23.8" height="18" rx="3" fill="#64748b"/>
72
- <text x="191.8" y="225" class="value">4.32 s</text>
71
+ <rect x="160" y="212" width="24.1" height="18" rx="3" fill="#64748b"/>
72
+ <text x="192.1" y="225" class="value">4.39 s</text>
73
73
 
74
- <!-- caxlsx 4.5.0: 5.15s -->
74
+ <!-- caxlsx 4.5.0: 5.28s -->
75
75
  <text x="150" y="260" text-anchor="end" class="label">caxlsx 4.5.0</text>
76
- <rect x="160" y="247" width="28.3" height="18" rx="3" fill="#64748b"/>
77
- <text x="196.3" y="260" class="value">5.15 s</text>
76
+ <rect x="160" y="247" width="29.0" height="18" rx="3" fill="#64748b"/>
77
+ <text x="197.0" y="260" class="value">5.28 s</text>
78
78
 
79
- <!-- rubyXL 3.4.38: 38.81s -->
79
+ <!-- rubyXL 3.4.38: 43.13s -->
80
80
  <text x="150" y="295" text-anchor="end" class="label">rubyXL 3.4.38</text>
81
- <rect x="160" y="282" width="213.5" height="18" rx="3" fill="#64748b"/>
82
- <text x="381.5" y="295" class="value">38.81 s</text>
81
+ <rect x="160" y="282" width="220.0" height="18" rx="3" fill="#64748b"/>
82
+ <text x="388.0" y="295" class="value">43.13 s</text>
83
83
  </g>
84
84
 
85
85
  <!-- Right: Read Performance -->
@@ -103,42 +103,42 @@
103
103
  <line x1="380.0" y1="65" x2="380.0" y2="320" class="grid"/>
104
104
  <text x="380.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">40s</text>
105
105
 
106
- <!-- xlsxrb (Streaming): 3.17s -->
106
+ <!-- xlsxrb (Streaming): 1.73s -->
107
107
  <text x="150" y="85" text-anchor="end" class="label-accent">xlsxrb (Streaming)</text>
108
- <rect x="160" y="72" width="17.4" height="18" rx="3" fill="#3b82f6"/>
109
- <text x="185.4" y="85" class="value">3.17 s</text>
108
+ <rect x="160" y="72" width="9.5" height="18" rx="3" fill="#3b82f6"/>
109
+ <text x="177.5" y="85" class="value">1.73 s</text>
110
110
 
111
- <!-- simple_xlsx_reader: 4.48s -->
112
- <text x="150" y="120" text-anchor="end" class="label">simple_xlsx_reader</text>
113
- <rect x="160" y="107" width="24.6" height="18" rx="3" fill="#64748b"/>
114
- <text x="192.6" y="120" class="value">4.48 s</text>
111
+ <!-- xlsxrb (In-Memory): 4.33s -->
112
+ <text x="150" y="120" text-anchor="end" class="label-accent">xlsxrb (In-Memory)</text>
113
+ <rect x="160" y="107" width="23.8" height="18" rx="3" fill="#3b82f6"/>
114
+ <text x="191.8" y="120" class="value">4.33 s</text>
115
115
 
116
- <!-- xlsxrb (In-Memory): 5.71s -->
117
- <text x="150" y="155" text-anchor="end" class="label-accent">xlsxrb (In-Memory)</text>
118
- <rect x="160" y="142" width="31.4" height="18" rx="3" fill="#3b82f6"/>
119
- <text x="199.4" y="155" class="value">5.71 s</text>
116
+ <!-- simple_xlsx_reader: 4.38s -->
117
+ <text x="150" y="155" text-anchor="end" class="label">simple_xlsx_reader</text>
118
+ <rect x="160" y="142" width="24.1" height="18" rx="3" fill="#64748b"/>
119
+ <text x="192.1" y="155" class="value">4.38 s</text>
120
120
 
121
- <!-- creek 2.6.3: 8.14s -->
121
+ <!-- creek 2.6.3: 7.12s -->
122
122
  <text x="150" y="190" text-anchor="end" class="label">creek 2.6.3</text>
123
- <rect x="160" y="177" width="44.8" height="18" rx="3" fill="#64748b"/>
124
- <text x="212.8" y="190" class="value">8.14 s</text>
123
+ <rect x="160" y="177" width="39.2" height="18" rx="3" fill="#64748b"/>
124
+ <text x="207.2" y="190" class="value">7.12 s</text>
125
125
 
126
- <!-- xsv 1.4.1: 14.61s -->
127
- <text x="150" y="225" text-anchor="end" class="label">xsv 1.4.1</text>
128
- <rect x="160" y="212" width="80.4" height="18" rx="3" fill="#64748b"/>
129
- <text x="248.4" y="225" class="value">14.61 s</text>
126
+ <!-- roo 3.0.0: 10.95s -->
127
+ <text x="150" y="225" text-anchor="end" class="label">roo 3.0.0</text>
128
+ <rect x="160" y="212" width="60.2" height="18" rx="3" fill="#64748b"/>
129
+ <text x="228.2" y="225" class="value">10.95 s</text>
130
130
 
131
- <!-- roo 3.0.0: 15.69s -->
132
- <text x="150" y="260" text-anchor="end" class="label">roo 3.0.0</text>
133
- <rect x="160" y="247" width="86.3" height="18" rx="3" fill="#64748b"/>
134
- <text x="254.3" y="260" class="value">15.69 s</text>
131
+ <!-- xsv 1.4.1: 14.56s -->
132
+ <text x="150" y="260" text-anchor="end" class="label">xsv 1.4.1</text>
133
+ <rect x="160" y="247" width="80.1" height="18" rx="3" fill="#64748b"/>
134
+ <text x="248.1" y="260" class="value">14.56 s</text>
135
135
 
136
- <!-- rubyXL 3.4.38: 37.13s -->
136
+ <!-- rubyXL 3.4.38: 30.74s -->
137
137
  <text x="150" y="295" text-anchor="end" class="label">rubyXL 3.4.38</text>
138
- <rect x="160" y="282" width="204.2" height="18" rx="3" fill="#64748b"/>
139
- <text x="372.2" y="295" class="value">37.13 s</text>
138
+ <rect x="160" y="282" width="169.1" height="18" rx="3" fill="#64748b"/>
139
+ <text x="337.1" y="295" class="value">30.74 s</text>
140
140
  </g>
141
141
 
142
142
  <!-- Footer -->
143
- <text x="480" y="455" text-anchor="middle" class="footer">Tested on Ruby 3.4+ in isolated subprocesses • Full GC and peak memory metrics in docs/PEER_LIBRARIES.md</text>
143
+ <text x="480" y="455" text-anchor="middle" class="footer">Tested on Ruby 4.0+ in isolated subprocesses • Full GC and peak memory metrics in docs/PEER_LIBRARIES.md</text>
144
144
  </svg>