expressir 2.4.0 → 2.4.2

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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
  3. data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
  4. data/TODO.bugs/03-builder-mutable-state.md +43 -0
  5. data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
  6. data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
  7. data/TODO.bugs/06-collection-registry-single-source.md +53 -0
  8. data/TODO.bugs/07-require-relative-cleanup.md +42 -0
  9. data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
  10. data/TODO.bugs/09-parser-split.md +53 -0
  11. data/TODO.bugs/10-to-s-override.md +42 -0
  12. data/TODO.bugs/11-parser-class-variables.md +39 -0
  13. data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
  14. data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
  15. data/TODO.bugs/14-model-formatting-leak.md +30 -0
  16. data/TODO.bugs/15-expression-children-macro.md +27 -0
  17. data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
  18. data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
  19. data/TODO.bugs/18-const-get-private-constants.md +30 -0
  20. data/TODO.bugs/19-format-methods-public.md +22 -0
  21. data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
  22. data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
  23. data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
  24. data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
  25. data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
  26. data/TODO.bugs/25-debug-puts-in-production.md +21 -0
  27. data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
  28. data/TODO.bugs/27-package-build-god-method.md +19 -0
  29. data/TODO.bugs/28-package-god-class.md +30 -0
  30. data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
  31. data/TODO.bugs/30-unicode-map-extraction.md +19 -0
  32. data/TODO.bugs/README.md +43 -0
  33. data/TODO.max-perf/01-restore-ci-green.md +29 -0
  34. data/TODO.max-perf/02-streaming-parse-path.md +31 -0
  35. data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
  36. data/TODO.max-perf/04-benchmark-harness.md +28 -0
  37. data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
  38. data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
  39. data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
  40. data/TODO.max-perf/08-builder-build-perf.md +45 -0
  41. data/TODO.max-perf/09-grammar-cold-start.md +25 -0
  42. data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
  43. data/TODO.max-perf/11-ci-green-closeout.md +25 -0
  44. data/TODO.max-perf/12-require-boot-profile.md +25 -0
  45. data/TODO.max-perf/13-key-conversion-specs.md +26 -0
  46. data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
  47. data/benchmark/srl_benchmark.rb +76 -17
  48. data/docs/_tutorials/formatting-schemas.adoc +89 -0
  49. data/docs/_tutorials/index.adoc +10 -0
  50. data/docs/lychee.toml +3 -0
  51. data/expressir.gemspec +4 -2
  52. data/lib/expressir/cli.rb +3 -0
  53. data/lib/expressir/commands/changes_import_eengine.rb +0 -6
  54. data/lib/expressir/commands/changes_validate.rb +0 -2
  55. data/lib/expressir/commands/clean.rb +1 -1
  56. data/lib/expressir/commands/coverage.rb +6 -2
  57. data/lib/expressir/commands/file_violations.rb +70 -0
  58. data/lib/expressir/commands/format.rb +1 -1
  59. data/lib/expressir/commands/non_ascii_character.rb +49 -0
  60. data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
  61. data/lib/expressir/commands/package.rb +143 -187
  62. data/lib/expressir/commands/validate_ascii.rb +0 -510
  63. data/lib/expressir/commands/version.rb +1 -1
  64. data/lib/expressir/commands.rb +3 -0
  65. data/lib/expressir/coverage.rb +49 -117
  66. data/lib/expressir/express/ast_key_converter.rb +114 -0
  67. data/lib/expressir/express/builder.rb +71 -194
  68. data/lib/expressir/express/builder_context.rb +22 -0
  69. data/lib/expressir/express/builders/expression_builder.rb +16 -16
  70. data/lib/expressir/express/cache.rb +35 -8
  71. data/lib/expressir/express/error.rb +24 -0
  72. data/lib/expressir/express/formatter.rb +33 -18
  73. data/lib/expressir/express/formatters/declarations_formatter.rb +24 -2
  74. data/lib/expressir/express/formatters/remark_formatter.rb +75 -2
  75. data/lib/expressir/express/formatters/statements_formatter.rb +20 -3
  76. data/lib/expressir/express/grammar/parser.rb +705 -0
  77. data/lib/expressir/express/grammar.rb +11 -0
  78. data/lib/expressir/express/node_position_index.rb +215 -0
  79. data/lib/expressir/express/parallel_files.rb +229 -0
  80. data/lib/expressir/express/parser.rb +138 -950
  81. data/lib/expressir/express/pretty_formatter.rb +23 -5
  82. data/lib/expressir/express/remark_attacher.rb +497 -581
  83. data/lib/expressir/express/remark_scanner.rb +84 -19
  84. data/lib/expressir/express/schema_block_scanner.rb +137 -0
  85. data/lib/expressir/express/schema_source_formatter.rb +15 -0
  86. data/lib/expressir/express/scope_resolver.rb +223 -0
  87. data/lib/expressir/express/source_formatter.rb +15 -0
  88. data/lib/expressir/express/streaming_builder.rb +147 -176
  89. data/lib/expressir/express.rb +10 -6
  90. data/lib/expressir/model/concerns.rb +3 -0
  91. data/lib/expressir/model/data_types/generic_entity.rb +6 -6
  92. data/lib/expressir/model/declarations/entity.rb +5 -0
  93. data/lib/expressir/model/declarations/function.rb +5 -0
  94. data/lib/expressir/model/declarations/procedure.rb +5 -0
  95. data/lib/expressir/model/declarations/rule.rb +6 -0
  96. data/lib/expressir/model/declarations/schema.rb +21 -8
  97. data/lib/expressir/model/declarations/type.rb +3 -0
  98. data/lib/expressir/model/exp_file.rb +2 -0
  99. data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -0
  100. data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -0
  101. data/lib/expressir/model/expressions/binary_expression.rb +1 -0
  102. data/lib/expressir/model/expressions/entity_constructor.rb +1 -0
  103. data/lib/expressir/model/expressions/function_call.rb +1 -0
  104. data/lib/expressir/model/expressions/interval.rb +1 -0
  105. data/lib/expressir/model/expressions/query_expression.rb +2 -0
  106. data/lib/expressir/model/expressions/unary_expression.rb +1 -0
  107. data/lib/expressir/model/model_element.rb +133 -14
  108. data/lib/expressir/model/remark_format.rb +17 -0
  109. data/lib/expressir/model/remark_info.rb +31 -3
  110. data/lib/expressir/model/remark_placement.rb +29 -0
  111. data/lib/expressir/model/repository.rb +18 -5
  112. data/lib/expressir/model/statements/alias.rb +2 -0
  113. data/lib/expressir/model/statements/assignment.rb +4 -0
  114. data/lib/expressir/model/statements/case.rb +21 -0
  115. data/lib/expressir/model/statements/case_action.rb +1 -0
  116. data/lib/expressir/model/statements/compound.rb +4 -0
  117. data/lib/expressir/model/statements/escape.rb +3 -0
  118. data/lib/expressir/model/statements/if.rb +5 -0
  119. data/lib/expressir/model/statements/null.rb +3 -0
  120. data/lib/expressir/model/statements/procedure_call.rb +3 -0
  121. data/lib/expressir/model/statements/repeat.rb +3 -0
  122. data/lib/expressir/model/statements/return.rb +3 -0
  123. data/lib/expressir/model/statements/skip.rb +3 -0
  124. data/lib/expressir/model.rb +12 -3
  125. data/lib/expressir/package/builder.rb +2 -2
  126. data/lib/expressir/version.rb +6 -1
  127. data/lib/expressir.rb +56 -3
  128. metadata +81 -7
  129. data/lib/expressir/express/transformer/remark_handling.rb +0 -194
@@ -0,0 +1,28 @@
1
+ # 16 — PrettyFormatter duplicates declaration formatting from DeclarationsFormatter
2
+
3
+ **Priority:** P1 (DRY violation)
4
+ **Status:** ACCEPTED — duplication is intentional specialization
5
+
6
+ ## Problem
7
+
8
+ `PrettyFormatter` redefines 5 `format_declarations_*` methods with
9
+ indentation and provenance-header differences from the base
10
+ `DeclarationsFormatter`.
11
+
12
+ ## Decision
13
+
14
+ The duplication is **accepted** as intentional specialization.
15
+ PrettyFormatter adds provenance headers (`format_provenance_header`),
16
+ scope-body helpers (`format_scope_body`, `format_scope_footer`), and
17
+ different indentation handling that the base formatter does not need.
18
+
19
+ A full unification would require parameterizing the base formatter with
20
+ indent-width, provenance support, and scope-body rendering — adding
21
+ complexity to the common path to serve a specialized variant. The
22
+ deletion test fails: removing PrettyFormatter's overrides would
23
+ concentrate complexity into the base formatter, making the common path
24
+ harder to reason about.
25
+
26
+ The duplication is bounded (5 methods, ~100 lines) and the two
27
+ formatters evolve independently (PrettyFormatter adds features the
28
+ base does not need).
@@ -0,0 +1,28 @@
1
+ # 17 — SNAKE_CASE_CACHE is a mutable module constant
2
+
3
+ **Priority:** P1 (unusual pattern)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/builder.rb:32`:
8
+
9
+ ```ruby
10
+ SNAKE_CASE_CACHE = {} # rubocop:disable Style/MutableConstant
11
+ ```
12
+
13
+ A module-level constant that is mutated in place at line 265 via `||=`.
14
+ It grows unboundedly across the process lifetime and violates the frozen-
15
+ constant convention the rest of the codebase follows.
16
+
17
+ ## Fix
18
+
19
+ Either:
20
+ - Document it explicitly as an intentionally-mutable static-conversion
21
+ cache and wrap it in a dedicated `SnakeCaseCache` class with a clear API.
22
+ - Replace with `Thread.current[:snake_case_cache] ||= {}` for thread-local
23
+ bounded growth.
24
+
25
+ ## Acceptance
26
+
27
+ - No `rubocop:disable Style/MutableConstant` needed.
28
+ - Cache behavior unchanged.
@@ -0,0 +1,30 @@
1
+ # 18 — const_get to access private constants from formatter modules
2
+
3
+ **Priority:** P2 (fragile access pattern)
4
+
5
+ ## Problem
6
+
7
+ Formatter modules use `self.class.const_get(:INDENT)`,
8
+ `self.class.const_get(:OPERATOR_PRECEDENCE)`, etc. to access constants
9
+ declared `private_constant` on `Formatter`. ~10 call sites across:
10
+
11
+ - `formatters/declarations_formatter.rb:129,235,304,360,535`
12
+ - `formatters/expressions_formatter.rb:41`
13
+ - `formatters/data_types_formatter.rb:131,265,304`
14
+ - `formatters/supertype_expressions_formatter.rb:13`
15
+
16
+ String-keyed reflective access to bypass visibility makes the code harder
17
+ to grep and reason about.
18
+
19
+ ## Fix
20
+
21
+ Remove `private_constant` on `INDENT`, `INDENT_CHAR`, `OPERATOR_PRECEDENCE`,
22
+ and `SUPERTYPE_OPERATOR_PRECEDENCE` since the formatter modules are
23
+ implementation details of Formatter anyway. Replace `const_get(:X)` with
24
+ direct `X` references.
25
+
26
+ ## Acceptance
27
+
28
+ - `grep -rn "const_get" lib/expressir/express/` returns nothing.
29
+ - No `private_constant` on format constants.
30
+ - All formatter specs pass.
@@ -0,0 +1,22 @@
1
+ # 19 — All format methods must be public due to format_registry dispatch
2
+
3
+ **Priority:** P2 (encapsulation)
4
+
5
+ ## Problem
6
+
7
+ `Formatter#format` dispatches via `public_send(handler, node)` at
8
+ `lib/expressir/express/formatter.rb:88`. This forces ~60 `format_*`
9
+ methods across 9 formatter modules to be public, even though they are only
10
+ ever called internally via the registry.
11
+
12
+ ## Fix
13
+
14
+ Replace `public_send(handler, node)` with `send(handler, node)` in
15
+ `Formatter#format` (the handler comes from the trusted registry, not user
16
+ input), then mark all `format_*` methods as `private`.
17
+
18
+ ## Acceptance
19
+
20
+ - `Formatter#format` uses `send` not `public_send`.
21
+ - Format methods are private in all 9 formatter modules.
22
+ - `Formatter.new.public_methods` does not include any `format_*` method.
@@ -0,0 +1,20 @@
1
+ # 20 — Coverage.find_nested_entities repeats 6-collection pattern three times
2
+
3
+ **Priority:** P2 (DRY violation)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/coverage.rb:404-477` — `find_nested_entities` has three
8
+ near-identical `when` branches for Function, Rule, and Procedure, each
9
+ concatenating the same six collections and recursing.
10
+
11
+ ## Fix
12
+
13
+ Replace the three branches with a single loop over
14
+ `node.class.collection_attributes_list` (from the `collection_attributes`
15
+ macro) and recurse into children.
16
+
17
+ ## Acceptance
18
+
19
+ - `find_nested_entities` has no model-class-specific case branches.
20
+ - All coverage specs pass.
@@ -0,0 +1,21 @@
1
+ # 21 — OPERATOR_TOKENS in Builder is a secondary dispatch mechanism
2
+
3
+ **Priority:** P2 (parallel dispatch)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/builder.rb:51-59` — 24-element `Set` of operator
8
+ token symbols, used at line 87 to decide whether to skip a nil-returning
9
+ handler. This set must be kept in sync with the grammar.
10
+
11
+ ## Fix
12
+
13
+ Instead of maintaining a hand-curated set, check whether the handler
14
+ returns nil for ANY first-key handler when the hash has multiple keys, not
15
+ just for operator tokens. This eliminates the need for the set.
16
+
17
+ ## Acceptance
18
+
19
+ - `OPERATOR_TOKENS` constant removed.
20
+ - Builder still handles multi-key hash AST correctly.
21
+ - All parser + builder specs pass.
@@ -0,0 +1,32 @@
1
+ # 22 — Builder fast-path methods are trivial wrappers
2
+
3
+ **Priority:** P2 (dead code surface)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/builder.rb:232-259` — Six methods that each just
8
+ call `build_node(:symbol, data)`, adding no logic:
9
+
10
+ ```ruby
11
+ def build_term(data) = build_node(:term, data)
12
+ def build_factor(data) = build_node(:factor, data)
13
+ def build_simple_factor(data) = build_node(:simple_factor, data)
14
+ def build_primary(data) = build_node(:primary, data)
15
+ def build_expression(data) = build_node(:expression, data)
16
+ def build_simple_expression(data) = build_node(:simple_expression, data)
17
+ ```
18
+
19
+ Called ~25 times across the builders. The indirection provides no value
20
+ since `build_node` is equally readable.
21
+
22
+ ## Fix
23
+
24
+ Inline the calls to `build_node(:term, ...)`, etc. Remove the six wrapper
25
+ methods. Reduce the Builder public surface.
26
+
27
+ ## Acceptance
28
+
29
+ - `build_term`, `build_factor`, `build_simple_factor`, `build_primary`,
30
+ `build_expression`, `build_simple_expression` removed from Builder.
31
+ - All call sites updated to `build_node(:term, ...)`.
32
+ - All parser + builder specs pass.
@@ -0,0 +1,21 @@
1
+ # 23 — Coverage maps are manual inverses
2
+
3
+ **Priority:** P2 (DRY violation)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/coverage.rb:7-52` — `ENTITY_TYPE_MAP` and
8
+ `CLASS_TO_EXPRESS_TYPE_MAP` are 20-entry frozen hashes that are exact
9
+ inverses of each other. Adding or renaming a type requires updating both.
10
+
11
+ ## Fix
12
+
13
+ Generate one map from the other at load time, or use a single canonical
14
+ list and derive both directions via `.to_h` and `.invert`.
15
+
16
+ ## Acceptance
17
+
18
+ - Only one source-of-truth map exists.
19
+ - Both `ENTITY_TYPE_MAP` and `CLASS_TO_EXPRESS_TYPE_MAP` (or their
20
+ successors) are derived from the single source.
21
+ - All coverage specs pass.
@@ -0,0 +1,19 @@
1
+ # 24 — StreamingBuilder convert_ast_format is 190 lines with 5 levels of nesting
2
+
3
+ **Priority:** P3 (complexity; defer unless time allows)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/streaming_builder.rb:241-429` —
8
+ `convert_ast_format` is a 190-line method with deeply nested conditionals
9
+ for merging Parsanol's single-key-hash sequences.
10
+
11
+ ## Fix
12
+
13
+ Split into named sub-methods for each pattern: merge-sequence,
14
+ repetition-unzip, str-concat.
15
+
16
+ ## Acceptance
17
+
18
+ - No method in StreamingBuilder exceeds 30 lines.
19
+ - All streaming specs pass.
@@ -0,0 +1,21 @@
1
+ # 25 — Debug puts statements in production code
2
+
3
+ **Priority:** P3 (cleanup)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/express/streaming_builder.rb:115,123,447-453` — `puts
8
+ "DEBUG ..."` guarded by `ENV["DEBUG_STREAMING"]`.
9
+
10
+ Using `puts` for debug logging means messages go to stdout mixed with
11
+ program output. No way to control log level programmatically.
12
+
13
+ ## Fix
14
+
15
+ Use `Logger` or `Expressir::Benchmark.trace` for debug output. Or remove
16
+ the statements if they are stale.
17
+
18
+ ## Acceptance
19
+
20
+ - `grep -rn "puts.*DEBUG" lib/` returns nothing.
21
+ - Streaming builder still works in debug mode (if retained).
@@ -0,0 +1,21 @@
1
+ # 26 — GenericEntity#children misplaced (P0 BUG)
2
+
3
+ **Priority:** P0 (correctness bug)
4
+ **Status:** FIXED
5
+
6
+ ## Problem
7
+
8
+ `lib/expressir/model/data_types/generic_entity.rb` had the `children`
9
+ method defined OUTSIDE the `GenericEntity` class body — it was on the
10
+ `DataTypes` module instead. Calling `GenericEntity.new.children` would
11
+ raise NoMethodError.
12
+
13
+ ## Fix
14
+
15
+ Moved `def children` inside the `GenericEntity` class body (before the
16
+ final `end`).
17
+
18
+ ## Acceptance
19
+
20
+ - `GenericEntity.new.children` no longer raises.
21
+ - All tests pass.
@@ -0,0 +1,19 @@
1
+ # 27 — Package#build is a 195-line god method
2
+
3
+ **Priority:** P1 (god method)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/commands/package.rb:94-288` — `Package#build` handles
8
+ manifest validation, auto-resolution, repo building, validation, packaging,
9
+ and error formatting in one method.
10
+
11
+ ## Fix
12
+
13
+ Extract `build_from_manifest`, `build_from_auto_resolution`,
14
+ `run_validation` as private methods.
15
+
16
+ ## Acceptance
17
+
18
+ - No method exceeds 40 lines.
19
+ - All package specs pass.
@@ -0,0 +1,30 @@
1
+ # 28 — Package command is a 1266-line god class
2
+
3
+ **Priority:** P2 (god class)
4
+ **Status:** ACCEPTED — Thor command pattern
5
+
6
+ ## Problem
7
+
8
+ `lib/expressir/commands/package.rb` is 1266 lines with 30 methods: display
9
+ logic (tree, info, search, list), package building, validation output,
10
+ colorization, and element introspection.
11
+
12
+ ## Decision
13
+
14
+ The god class is **accepted** as the Thor command pattern. Thor commands
15
+ are defined as methods within a single class with `desc`/`long_desc`/`option`
16
+ declarations. Extracting display methods to a separate module would:
17
+
18
+ 1. Separate `desc` declarations from method implementations — confusing
19
+ for Thor users who expect `desc` immediately before `def`.
20
+ 2. Risk breaking Thor's dispatch (Thor finds methods via
21
+ `instance_method_defined?` on the class; included-module methods
22
+ work but are non-standard for Thor commands).
23
+ 3. Move code without concentrating complexity — the deletion test fails:
24
+ the display methods and their helpers earn their keep in the command
25
+ class because they ARE the command's interface.
26
+
27
+ The Package#build method was already split (TODO 27) into focused
28
+ sub-methods. The remaining methods (info, validate, extract, list,
29
+ search, tree) are each 30-60 lines — individual methods are well-sized;
30
+ only the file is large.
@@ -0,0 +1,24 @@
1
+ # 29 — ValidateAscii is a 604-line god class with inline inner classes
2
+
3
+ **Priority:** P2 (god class)
4
+ **Status:** DONE
5
+
6
+ ## Problem
7
+
8
+ `validate_ascii.rb` was 549 lines with 3 sibling classes defined inline:
9
+ NonAsciiCharacter, FileViolations, NonAsciiViolationCollection.
10
+
11
+ ## Fix
12
+
13
+ Extracted the three inner classes to separate files:
14
+ - `lib/expressir/commands/non_ascii_character.rb` (50 lines)
15
+ - `lib/expressir/commands/file_violations.rb` (71 lines)
16
+ - `lib/expressir/commands/non_ascii_violation_collection.rb` (349 lines)
17
+
18
+ `validate_ascii.rb` now contains only the ValidateAscii command class
19
+ (94 lines). Autoloads added to `lib/expressir/commands.rb`.
20
+
21
+ ## Acceptance
22
+
23
+ - validate_ascii.rb under 100 lines.
24
+ - All 26 validate_ascii specs pass.
@@ -0,0 +1,19 @@
1
+ # 30 — build_unicode_to_asciimath_map is 80 lines of data inside a method
2
+
3
+ **Priority:** P2 (data-as-code)
4
+
5
+ ## Problem
6
+
7
+ `lib/expressir/commands/validate_ascii.rb:436-517` —
8
+ `build_unicode_to_asciimath_map` is an 80-line hash literal defined inside
9
+ a method body. Should be a frozen constant.
10
+
11
+ ## Fix
12
+
13
+ Extract to `lib/expressir/express/unicode_mapping.rb` as a frozen
14
+ constant.
15
+
16
+ ## Acceptance
17
+
18
+ - The hash is a module-level frozen constant.
19
+ - All validate_ascii specs pass.
@@ -0,0 +1,43 @@
1
+ # TODO.bugs
2
+
3
+ Track all architectural and code-cleanliness work.
4
+
5
+ ## Status
6
+
7
+ **All 30 TODOs resolved.** 24 implemented, 4 accepted as design decisions
8
+ (14, 16) or deferred with clear reasoning (28, 29).
9
+
10
+ ## Index
11
+
12
+ | # | Priority | Status | Title |
13
+ | --- | --- | --- | --- |
14
+ | [01](01-stale-transformer-autoload.md) | P0 | done | Delete stale `Transformer` autoload |
15
+ | [02](02-parser-class-instance-vars.md) | P0 | done | Replace class-level mutation in `Parser` |
16
+ | [03](03-builder-mutable-state.md) | P1 | done | Thread `BuilderContext` |
17
+ | [04](04-formatter-public-send-dispatch.md) | P1 | done | Registration-time guard |
18
+ | [05](05-anonymous-formatter-subclass.md) | P1 | done | Cache named formatter classes |
19
+ | [06](06-collection-registry-single-source.md) | P1 | done | Consolidate `SCOPE_DECL_COLLECTIONS` |
20
+ | [07](07-require-relative-cleanup.md) | P2 | done | Replace `require_relative` with autoload |
21
+ | [08](08-require-expressir-in-commands.md) | P2 | done | Remove `require "expressir/..."` |
22
+ | [09](09-parser-split.md) | P2 | done | Split parser.rb into grammar + scanner + facade |
23
+ | [10](10-to-s-override.md) | P2 | done | Rename `to_s` to `format` |
24
+ | [11](11-parser-class-variables.md) | P3 | done | Replace `@@` class variables |
25
+ | [12](12-marker-modules-vs-registry.md) | P3 | done | `collection_attributes` macro |
26
+ | [13](13-string-literal-scanner-limitation.md) | P1 | done | `:in_string` scanner state |
27
+ | [14](14-model-formatting-leak.md) | P1 | accepted | Model formatting — domain coupling |
28
+ | [15](15-expression-children-macro.md) | P1 | done | `child_attributes` macro |
29
+ | [16](16-pretty-formatter-duplication.md) | P1 | accepted | PrettyFormatter — intentional specialization |
30
+ | [17](17-snake-case-cache-mutable-constant.md) | P1 | done | Thread-local snake_case cache |
31
+ | [18](18-const-get-private-constants.md) | P2 | done | Remove `private_constant` |
32
+ | [19](19-format-methods-public.md) | P2 | done | `send` dispatch for private methods |
33
+ | [20](20-coverage-nested-entities-dedup.md) | P2 | done | Coverage uses `collection_attributes_list` |
34
+ | [21](21-operator-tokens-secondary-dispatch.md) | P2 | done | Remove OPERATOR_TOKENS set |
35
+ | [22](22-builder-fast-path-wrappers.md) | P2 | done | Remove fast-path wrappers |
36
+ | [23](23-coverage-inverse-maps.md) | P2 | done | Derive maps from `TYPE_PAIRS` |
37
+ | [24](24-streaming-builder-complexity.md) | P3 | done | Split `convert_ast_format` |
38
+ | [25](25-debug-puts-in-production.md) | P3 | done | Remove debug puts |
39
+ | [26](26-generic-entity-children-misplaced.md) | P0 | done | Fix misplaced `children` method |
40
+ | [27](27-package-build-god-method.md) | P1 | done | Split `Package#build` |
41
+ | [28](28-package-god-class.md) | P2 | deferred | Package 1266-line god class |
42
+ | [29](29-validate-ascii-god-class.md) | P2 | deferred | ValidateAscii god class |
43
+ | [30](30-unicode-map-extraction.md) | P2 | done | Extract unicode map to constant |
@@ -0,0 +1,29 @@
1
+ # TODO.max-perf/01 — Restore CI green on main
2
+
3
+ ## Context
4
+
5
+ CI on main (`0ef38e1`, 2026-09-17) fails on the Windows matrix: yeptris
6
+ 0.6.3.3's mingw build raises `NameError: uninitialized constant
7
+ Yeptris::FFI::NODE_SCALAR` from `yeptris/node.rb` whenever `to_yaml` runs.
8
+ Expressir's gemspec constraint (`~> 0.6.1`, from PR #346) allows it, and the
9
+ Gemfile.lock is gitignored, so CI resolves the newest release at run time.
10
+
11
+ yeptris has since released 0.6.4.1 and 0.6.5.1.
12
+
13
+ ## Work
14
+
15
+ - [x] Reproduce the failing spec groups locally on the newest yeptris
16
+ (`package_fixtures_spec`, `package_spec`, `cache_spec`): 84/84 green on
17
+ 0.6.5.1
18
+ - [x] Re-run the failed rake workflow on main (resolves 0.6.5.1)
19
+ - [ ] Confirm the rake workflow on main is green
20
+ - [x] 0.6.5.1 does NOT fix mingw (same NameError, all Windows jobs) — filed
21
+ leptris/yeptris#318 with the CI repro
22
+ - [x] expressir-side guard: `Expressir.select_serialization_engines` pins the
23
+ portable :standard YAML/JSON adapters on `Gem.win_platform?` at boot, so
24
+ lutaml-model's autodetection never loads the broken mingw build; Unix
25
+ keeps yeptris. Spec: spec/expressir/engine_selection_spec.rb
26
+
27
+ ## Acceptance
28
+
29
+ rake workflow green on main across the full platform matrix.
@@ -0,0 +1,31 @@
1
+ # TODO.max-perf/02 — Fix or retire the streaming parse path
2
+
3
+ ## Context
4
+
5
+ `Parser.from_exp(use_streaming: true)` — documented as the maximum-performance
6
+ path (construct-by-construct building via `Parsanol::Native.parse_with_builder`,
7
+ no full intermediate AST) — fails on every schema tested with
8
+ `Error::SchemaParseFailure` on parsanol 1.3.28 and 1.3.30. The feature is dead
9
+ weight until it works: callers cannot opt into it, and it skews the perf
10
+ picture (the fastest intended path is the broken one).
11
+
12
+ ## Work
13
+
14
+ - [x] Reproduced on a 5-line schema: `parse_fresh(schema_grammar, block)` fails
15
+ at end-of-input — parse_fresh has no packrat memoization, which EXPRESS
16
+ requires (parsanol-ruby#52)
17
+ - [x] The true streaming path (`from_exp_streaming_builder`) is unreachable:
18
+ its gate uses `defined?(Parsanol::Native.parse_with_builder)`, which is
19
+ always nil for the extension's dynamically-dispatched methods; calling it
20
+ directly yields empty models (builder protocol drift vs parsanol 1.3.30)
21
+ - [x] Removed both dead paths; `use_streaming: true` now raises
22
+ `Error::StreamingUnsupportedError` with a precise message
23
+ - [x] Spec: spec/expressir/express/streaming_spec.rb
24
+ - [x] Upstream: covered by parsanol-ruby#59 roadmap (ractor-safe / stable
25
+ parse_with_builder unlocks a rewrite of this path)
26
+
27
+ ## Acceptance
28
+
29
+ Either `use_streaming: true` produces an equal model with a green spec, or
30
+ the breakage is precisely documented upstream and the path is explicitly
31
+ marked unsupported.
@@ -0,0 +1,27 @@
1
+ # TODO.max-perf/03 — CLI parallel opt-in and multi-file loader consolidation
2
+
3
+ ## Context
4
+
5
+ The fork worker pool (PRs #349/#351) is strictly opt-in for library users.
6
+ The expressir CLI is application mode — our process — so it can opt in.
7
+ Today `commands/coverage.rb` calls `Parser.from_files` without
8
+ `max_processes` (sequential), and `commands/package.rb` calls
9
+ `Model::Repository.from_files` — a *second* multi-file loading path
10
+ duplicating the same orchestration (MECE violation).
11
+
12
+ ## Work
13
+
14
+ - [x] `--max-processes` on `coverage` (default 4, Thor layer in cli.rb) and
15
+ `package build` (default 1 — strict error semantics preserved)
16
+ - [x] `ParallelFiles.run` gained `strict:` — re-raises SchemaParseFailure,
17
+ preserving Repository.from_files' fail-fast contract
18
+ - [x] `Model::Repository.from_files` now delegates its parse loop to
19
+ ParallelFiles (shared orchestration, strict mode); tolerant
20
+ Parser.from_files behavior unchanged
21
+ - [x] Specs: repository parallel == sequential (ids + order), strict raise,
22
+ coverage/package suites green (57 + 57 examples)
23
+
24
+ ## Acceptance
25
+
26
+ One multi-file loading pipeline; CLI bulk loads parse in parallel by
27
+ default; library default remains sequential.
@@ -0,0 +1,28 @@
1
+ # TODO.max-perf/04 — Repair the SRL benchmark harness
2
+
3
+ ## Context
4
+
5
+ `benchmark/srl_benchmark.rb` has two defects found during the Sept 2026
6
+ validation:
7
+
8
+ 1. The "Ruby Parser" pass calls `from_file(file, skip_references: true)`
9
+ without `use_native: false`. Since native-became-default (2026-03-25),
10
+ both passes run the native parser — the head-to-head comparison is
11
+ mislabeled and meaningless.
12
+ 2. Per-file `Timeout.timeout(30)` cannot interrupt the native FFI parse
13
+ (GVL held), so pathological files hang the benchmark instead of timing
14
+ out — observed as a 25-minute stall on an 8.6KB schema.
15
+
16
+ ## Work
17
+
18
+ - [x] Ruby-parser pass (and warmup) now pass `use_native: false`
19
+ - [x] `parse_file_isolated`: per-file fork, length-prefixed Marshal results,
20
+ TERM→KILL wall-clock escalation — pathological files terminate
21
+ - [x] `SRL_PATH` env override for reproducible smoke runs
22
+ - [x] Verified: 3-fixture run prints genuine Ruby-vs-Native numbers with
23
+ truthful labels and terminates cleanly
24
+
25
+ ## Acceptance
26
+
27
+ A short benchmark run prints genuine Ruby-vs-Native numbers, terminates on
28
+ pathological files, and labels passes truthfully.
@@ -0,0 +1,22 @@
1
+ # TODO.max-perf/05 — Parallel-parsing fidelity specs
2
+
3
+ ## Context
4
+
5
+ Parallel `from_files` equality is currently asserted on schema ids only.
6
+ The pool crosses a fork boundary via Marshal; deep structural equality is
7
+ the real contract. The progress-block regression merged in #349 (double
8
+ `schemas` extraction) proved the block/caller paths need dedicated specs.
9
+
10
+ ## Work
11
+
12
+ - [x] Deep-equality spec: `to_hash` of parallel repository == sequential
13
+ - [x] Progress-block contract specs on both paths (schemas is an Array of
14
+ Declarations::Schema)
15
+ - [x] Error-path specs: skip-vs-raise by mode (tolerant/strict), including
16
+ the strict-mode re-raise of SchemaParseFailure
17
+ - [x] Gate specs: nil/1/<3 files and fork-less platforms never fork
18
+
19
+ ## Acceptance
20
+
21
+ All fidelity specs green; `to_hash` of a parallel repository equals the
22
+ sequential one for a multi-fixture corpus.
@@ -0,0 +1,41 @@
1
+ # TODO.max-perf/06 — Ruby-side (Builder) CPU audit on the current stack
2
+
3
+ ## Context
4
+
5
+ After parsanol 1.3.30 (~2.5x further parse speedup), the native parse share
6
+ collapsed and the Ruby side — AstTransformer → Builder → lutaml-model
7
+ instantiation → remark attachment — is now the dominant parse cost
8
+ (sampled 2026-09-17: 72/28 Rust/Ruby on 1.3.28; native got ~2.5x faster
9
+ afterwards). March data ("removing Ruby transform: 2.5%") is obsolete.
10
+
11
+ Related landed work: f32f85f "eliminate allocation storms in remark
12
+ attachment" (another session, on main), 4c49e3a per-remark rescan guards.
13
+
14
+ ## Work
15
+
16
+ Results (CPU-time, parsanol 1.3.30, 4 mid-size SRL schemas):
17
+
18
+ | phase | total | share |
19
+ |---|---:|---:|
20
+ | native parse (parse_native) | 1.595s | 50% |
21
+ | Builder.build_with_remarks | 1.530s | 48% |
22
+ | reference resolution | 0.080s | 2% |
23
+
24
+ Builder split: model build (`Builder.build`) 1.12s vs remark attachment
25
+ 0.06s — remark attachment is already cheap (f32f85f); the cost is
26
+ AST-hash → lutaml-model instantiation.
27
+
28
+ - [x] Phase timing recorded (above)
29
+ - [x] Builder sub-phase split recorded (above)
30
+ - [x] Next target identified precisely: `Builder.build` per-node path —
31
+ `cached_snake_case` / `fast_convert_keys` string allocations and
32
+ lutaml-model constructor overhead (thousands of attribute setters).
33
+ C stack sampling cannot name Ruby methods (vm_exec_core only);
34
+ instrument inside Builder.build or add stackprof as a dev dependency
35
+ before optimizing. Not refactored blindly — no safe evident win
36
+ without that instrumentation.
37
+
38
+ ## Acceptance
39
+
40
+ A numbers table attributing Ruby-side parse CPU to phases, plus either a
41
+ measured improvement or a precise, evidence-backed next target.
@@ -0,0 +1,27 @@
1
+ # TODO.max-perf/07 — Upstream parsanol roadmap (research 2022–2026)
2
+
3
+ ## Context
4
+
5
+ The field's fastest grammar engines compile grammars instead of interpreting
6
+ them. Mapping to parsanol (all upstream work; expressir only consumes):
7
+
8
+ | Technique | Precedent | parsanol mapping |
9
+ |---|---|---|
10
+ | JIT grammar compilation + tag-dispatched structure switching + cross-grammar cache reuse (6x) | XGrammar-2 (arXiv:2601.04426) | BYTE_DISPATCH is conceptually TagDispatch; compile to a dispatch table per atom |
11
+ | Grammar → generated code (production precedent) | tree-sitter (precompiled C, incremental reparse), CPython's generated PEG parser | generate Rust from the EXPRESS grammar instead of walking atom structs |
12
+ | Skip packrat tables via automata | Pest discussion #1081 | memoization-free automata for the deterministic fragments; keep packrat only where ambiguity needs it |
13
+ | SIMD byte classification (GB/s lexing) | simdjson, Lemire's vectorized classification (arXiv:2503.01662) | vectorize keyword/delimiter classification in the lexer tier |
14
+ | Incremental re-parse | tree-sitter, gpeg (SLE'21) | re-parse only dirty schema blocks — pairs with expressir's SchemaBlockScanner |
15
+
16
+ ## Work
17
+
18
+ - [x] Filed parsanol-ruby#59 with the mapping, profile evidence, citations,
19
+ and the Ractor-safe entry point item (the only parallelism option on
20
+ Windows MRI and fork-hostile embedders)
21
+ - [x] Consumer unlocks noted: incremental reparse pairs with expressir's
22
+ SchemaBlockScanner; compiled grammar cuts cold-start (~2,300 atoms
23
+ serialized per boot); ractor-safe parse replaces the fork pool
24
+
25
+ ## Acceptance
26
+
27
+ Upstream issue filed; expressir-side dependencies on each technique noted.