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.
- checksums.yaml +4 -4
- data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
- data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
- data/TODO.bugs/03-builder-mutable-state.md +43 -0
- data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
- data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
- data/TODO.bugs/06-collection-registry-single-source.md +53 -0
- data/TODO.bugs/07-require-relative-cleanup.md +42 -0
- data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
- data/TODO.bugs/09-parser-split.md +53 -0
- data/TODO.bugs/10-to-s-override.md +42 -0
- data/TODO.bugs/11-parser-class-variables.md +39 -0
- data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
- data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
- data/TODO.bugs/14-model-formatting-leak.md +30 -0
- data/TODO.bugs/15-expression-children-macro.md +27 -0
- data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
- data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
- data/TODO.bugs/18-const-get-private-constants.md +30 -0
- data/TODO.bugs/19-format-methods-public.md +22 -0
- data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
- data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
- data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
- data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
- data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
- data/TODO.bugs/25-debug-puts-in-production.md +21 -0
- data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
- data/TODO.bugs/27-package-build-god-method.md +19 -0
- data/TODO.bugs/28-package-god-class.md +30 -0
- data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
- data/TODO.bugs/30-unicode-map-extraction.md +19 -0
- data/TODO.bugs/README.md +43 -0
- data/TODO.max-perf/01-restore-ci-green.md +29 -0
- data/TODO.max-perf/02-streaming-parse-path.md +31 -0
- data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
- data/TODO.max-perf/04-benchmark-harness.md +28 -0
- data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
- data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
- data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
- data/TODO.max-perf/08-builder-build-perf.md +45 -0
- data/TODO.max-perf/09-grammar-cold-start.md +25 -0
- data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
- data/TODO.max-perf/11-ci-green-closeout.md +25 -0
- data/TODO.max-perf/12-require-boot-profile.md +25 -0
- data/TODO.max-perf/13-key-conversion-specs.md +26 -0
- data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
- data/benchmark/srl_benchmark.rb +76 -17
- data/docs/_tutorials/formatting-schemas.adoc +89 -0
- data/docs/_tutorials/index.adoc +10 -0
- data/docs/lychee.toml +3 -0
- data/expressir.gemspec +4 -2
- data/lib/expressir/cli.rb +3 -0
- data/lib/expressir/commands/changes_import_eengine.rb +0 -6
- data/lib/expressir/commands/changes_validate.rb +0 -2
- data/lib/expressir/commands/clean.rb +1 -1
- data/lib/expressir/commands/coverage.rb +6 -2
- data/lib/expressir/commands/file_violations.rb +70 -0
- data/lib/expressir/commands/format.rb +1 -1
- data/lib/expressir/commands/non_ascii_character.rb +49 -0
- data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
- data/lib/expressir/commands/package.rb +143 -187
- data/lib/expressir/commands/validate_ascii.rb +0 -510
- data/lib/expressir/commands/version.rb +1 -1
- data/lib/expressir/commands.rb +3 -0
- data/lib/expressir/coverage.rb +49 -117
- data/lib/expressir/express/ast_key_converter.rb +114 -0
- data/lib/expressir/express/builder.rb +71 -194
- data/lib/expressir/express/builder_context.rb +22 -0
- data/lib/expressir/express/builders/expression_builder.rb +16 -16
- data/lib/expressir/express/cache.rb +35 -8
- data/lib/expressir/express/error.rb +24 -0
- data/lib/expressir/express/formatter.rb +33 -18
- data/lib/expressir/express/formatters/declarations_formatter.rb +24 -2
- data/lib/expressir/express/formatters/remark_formatter.rb +75 -2
- data/lib/expressir/express/formatters/statements_formatter.rb +20 -3
- data/lib/expressir/express/grammar/parser.rb +705 -0
- data/lib/expressir/express/grammar.rb +11 -0
- data/lib/expressir/express/node_position_index.rb +215 -0
- data/lib/expressir/express/parallel_files.rb +229 -0
- data/lib/expressir/express/parser.rb +138 -950
- data/lib/expressir/express/pretty_formatter.rb +23 -5
- data/lib/expressir/express/remark_attacher.rb +497 -581
- data/lib/expressir/express/remark_scanner.rb +84 -19
- data/lib/expressir/express/schema_block_scanner.rb +137 -0
- data/lib/expressir/express/schema_source_formatter.rb +15 -0
- data/lib/expressir/express/scope_resolver.rb +223 -0
- data/lib/expressir/express/source_formatter.rb +15 -0
- data/lib/expressir/express/streaming_builder.rb +147 -176
- data/lib/expressir/express.rb +10 -6
- data/lib/expressir/model/concerns.rb +3 -0
- data/lib/expressir/model/data_types/generic_entity.rb +6 -6
- data/lib/expressir/model/declarations/entity.rb +5 -0
- data/lib/expressir/model/declarations/function.rb +5 -0
- data/lib/expressir/model/declarations/procedure.rb +5 -0
- data/lib/expressir/model/declarations/rule.rb +6 -0
- data/lib/expressir/model/declarations/schema.rb +21 -8
- data/lib/expressir/model/declarations/type.rb +3 -0
- data/lib/expressir/model/exp_file.rb +2 -0
- data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -0
- data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -0
- data/lib/expressir/model/expressions/binary_expression.rb +1 -0
- data/lib/expressir/model/expressions/entity_constructor.rb +1 -0
- data/lib/expressir/model/expressions/function_call.rb +1 -0
- data/lib/expressir/model/expressions/interval.rb +1 -0
- data/lib/expressir/model/expressions/query_expression.rb +2 -0
- data/lib/expressir/model/expressions/unary_expression.rb +1 -0
- data/lib/expressir/model/model_element.rb +133 -14
- data/lib/expressir/model/remark_format.rb +17 -0
- data/lib/expressir/model/remark_info.rb +31 -3
- data/lib/expressir/model/remark_placement.rb +29 -0
- data/lib/expressir/model/repository.rb +18 -5
- data/lib/expressir/model/statements/alias.rb +2 -0
- data/lib/expressir/model/statements/assignment.rb +4 -0
- data/lib/expressir/model/statements/case.rb +21 -0
- data/lib/expressir/model/statements/case_action.rb +1 -0
- data/lib/expressir/model/statements/compound.rb +4 -0
- data/lib/expressir/model/statements/escape.rb +3 -0
- data/lib/expressir/model/statements/if.rb +5 -0
- data/lib/expressir/model/statements/null.rb +3 -0
- data/lib/expressir/model/statements/procedure_call.rb +3 -0
- data/lib/expressir/model/statements/repeat.rb +3 -0
- data/lib/expressir/model/statements/return.rb +3 -0
- data/lib/expressir/model/statements/skip.rb +3 -0
- data/lib/expressir/model.rb +12 -3
- data/lib/expressir/package/builder.rb +2 -2
- data/lib/expressir/version.rb +6 -1
- data/lib/expressir.rb +56 -3
- metadata +81 -7
- 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.
|
data/TODO.bugs/README.md
ADDED
|
@@ -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.
|