branchproof 0.6.0 → 0.8.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +279 -27
  4. data/doc/Branchproof/Analyzer.md +2 -2
  5. data/doc/Branchproof/CLI.md +4 -0
  6. data/doc/Branchproof/ComparisonReport.md +4 -0
  7. data/doc/Branchproof/Constraints/Solver.md +21 -0
  8. data/doc/Branchproof/Constraints.md +98 -0
  9. data/doc/Branchproof/CoverageIndex.md +6 -0
  10. data/doc/Branchproof/DecisionSyntax.md +22 -0
  11. data/doc/Branchproof/DecisionTable.md +153 -0
  12. data/doc/Branchproof/FlowInstrumentation.md +7 -0
  13. data/doc/Branchproof/FocusedReport.md +5 -2
  14. data/doc/Branchproof/Instrumenter.md +1 -0
  15. data/doc/Branchproof/Loader.md +0 -4
  16. data/doc/Branchproof/MinitestAdapter.md +0 -3
  17. data/doc/Branchproof/Report.md +18 -0
  18. data/doc/Branchproof/Runtime.md +23 -0
  19. data/doc/Branchproof/RuntimeFlow.md +27 -0
  20. data/doc/Branchproof/SavedReport.md +21 -0
  21. data/doc/Branchproof/Source.md +1 -0
  22. data/doc/Branchproof.md +9 -2
  23. data/doc/CHANGELOG.md +45 -0
  24. data/doc/README.md +279 -27
  25. data/lib/branchproof/analyzer.rb +222 -54
  26. data/lib/branchproof/cli.rb +28 -19
  27. data/lib/branchproof/comparison.rb +207 -19
  28. data/lib/branchproof/comparison_report.rb +49 -1
  29. data/lib/branchproof/constraints.rb +363 -0
  30. data/lib/branchproof/coverage_index.rb +120 -3
  31. data/lib/branchproof/decision_syntax.rb +310 -0
  32. data/lib/branchproof/decision_table.rb +377 -0
  33. data/lib/branchproof/evidence.rb +101 -36
  34. data/lib/branchproof/flow_instrumentation.rb +107 -0
  35. data/lib/branchproof/focused_report.rb +161 -26
  36. data/lib/branchproof/instrumenter.rb +65 -39
  37. data/lib/branchproof/limits.rb +4 -1
  38. data/lib/branchproof/loader.rb +18 -6
  39. data/lib/branchproof/minimizer.rb +18 -13
  40. data/lib/branchproof/minitest_adapter.rb +11 -16
  41. data/lib/branchproof/records.rb +2 -0
  42. data/lib/branchproof/report.rb +470 -66
  43. data/lib/branchproof/runtime.rb +25 -30
  44. data/lib/branchproof/runtime_flow.rb +58 -0
  45. data/lib/branchproof/saved_report.rb +438 -13
  46. data/lib/branchproof/source.rb +239 -41
  47. data/lib/branchproof/version.rb +1 -1
  48. data/lib/branchproof/worker.rb +1 -4
  49. data/lib/branchproof.rb +2 -0
  50. data/llms.txt +9 -2
  51. data/sig/branchproof.rbs +54 -1
  52. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5d48afceb9c6608abf5ebdb2aa6f402dd11b75f43421b49686cc49fd95641c3
4
- data.tar.gz: 647789d869c3a01f1809eacc871df99e4e0f81c7d8e70ffdb27228f4fd5c797e
3
+ metadata.gz: c76fa40f75fbd8122fc0fbb45a1c9d460fa3478f783951674e0e4c2707d44175
4
+ data.tar.gz: b4b0b88c1a0d5e61797ab52174f1841c57f1dde359d405dc1205bcff27810968
5
5
  SHA512:
6
- metadata.gz: b5f87cd4aee8588f7a86ba6a91730b7b4a787c256a0485f6dc278acbd948a5a1399669d0e2903371b28f1b7058e5339efb85fdb13d31110944e6462bdfe55b6a
7
- data.tar.gz: fb81f01ba2e42014364959afcdce9c8cd218611299d8bbd27a0a9fd5ed1ce1f2d311993f834b9f960934deadab659848257010717c2bdcc6814ea01870f985e2
6
+ metadata.gz: c32db269cece96c843093cc72accffd3921e56eb2a6b974b61c2d87cd696444ffd0d7fff25ed50ac3c5cadf2e6c316ccfd99aaf4f6c28344112c30451cf8e22a
7
+ data.tar.gz: 54a46ff6c7a753e2015bdb814d13fc576bc2d9bd88fa18d0bb4bee8f1f939fbcea62b9f006d31a3c5e99ac0d9c95de8a8bbea8d43db05c1a767c3ff828430f8b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.8.0] - 2026-09-17
4
+
5
+ - Derive a reduced decision table for every supported Boolean decision from its
6
+ `AND`/`OR`/`NOT`/atom structure, preserving Ruby short-circuit semantics with
7
+ an explicit `dont_care` value instead of exhaustive Cartesian expansion.
8
+ - Give every rule a stable identity derived from the decision, its normalized
9
+ condition vector, the expected outcome, and the table schema version, so
10
+ saved reports compare across runs, test order, and Minitest seeds.
11
+ - Overlay the existing run's observations onto the rules, attribute covered
12
+ rules to their Minitest tests, and describe uncovered rules as condition-value
13
+ requirements. No additional test execution is performed.
14
+ - Report Decision Table Coverage as its own criterion in the coverage ladder,
15
+ per decision and in aggregate, independently from MC/DC in both directions.
16
+ - Add conservative reachability: `observed`, `unknown`, and
17
+ `statically_impossible` with stable reason codes. Constraint analysis version 2
18
+ requires safe source constraints before excluding rules, keeping arbitrary
19
+ comparison receivers, mutation, and unordered numeric cases unknown.
20
+ - Exclude statically impossible rules from coverage denominators while keeping
21
+ them visible, and let runtime evidence withdraw an impossibility claim with a
22
+ `constraint_model_conflict` diagnostic.
23
+ - Add `--view decision-tables`, which honours `--missing-only` and never lists
24
+ an impossible rule as a missing obligation.
25
+ - Bound table derivation with the new `max_conditions_for_decision_table` and
26
+ `decision_table_rules_per_decision` limits.
27
+ - Persist the table, rule identities, coverage, attribution, and reachability in
28
+ schema `1.3` reports, and distinguish rule-coverage changes from reachability
29
+ changes in offline comparison.
30
+ - Enforce exact rule-limit boundaries and index runtime rule matching rather
31
+ than scanning all observations per rule.
32
+ - Add `--no-reachability` to keep all generated rules as coverage obligations.
33
+ - Align the JSON regression flag with decision-table CLI failures, report
34
+ analysis-version changes, and validate saved rule identities and evidence.
35
+ - Keep uncalculated decision locations and reasons in missing-only reports;
36
+ label `unless` and `until` outcomes as predicate values.
37
+
38
+ ## [0.7.0] - 2026-09-10
39
+
40
+ - Discover Boolean loop predicates, subjectless case candidates, standalone
41
+ short-circuit expressions, and pattern predicates through Prism.
42
+ - Analyze unary NOT and keyword `and`/`or` with Ruby's parsed precedence.
43
+ - Attribute selected paths for ordinary case, unguarded case/in, safe navigation,
44
+ and conditional assignments without adding them to MC/DC denominators.
45
+ - Expose decision kinds, contexts, alternative evidence, and explicit unsupported
46
+ constructs in schema 1.2 reports while retaining older saved-report support.
47
+
3
48
  ## [0.6.0] - 2026-09-10
4
49
 
5
50
  - Report Decision, Condition, and Condition/Decision Coverage alongside
data/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Branchproof
2
2
 
3
- Branchproof measures modified condition/decision coverage (MC/DC) from one
4
- serial Minitest run. It inventories supported `if`, `unless`, `elsif`,
5
- modifier, and ordinary ternary (`?:`) decisions, records observed vectors,
6
- and reports independence evidence, missing counterpart constraints, and
7
- smaller supporting test sets.
3
+ Branchproof measures decision, condition, modified condition/decision (MC/DC),
4
+ and decision-table coverage from one serial Minitest run. It discovers Ruby
5
+ decisions through Prism, records their runtime paths, and attributes evidence
6
+ to tests. Boolean decisions receive the coverage ladder; `case`, pattern
7
+ alternatives, safe navigation, and conditional assignments receive alternative
8
+ coverage.
8
9
 
9
10
  The gem and primary command are named `branchproof`. The `mcdc` command and
10
11
  `MCDC` namespace remain compatibility aliases with the same behavior.
@@ -128,7 +129,7 @@ json` when a consumer needs the complete identifiers and versioned schema.
128
129
 
129
130
  ### Coverage ladder
130
131
 
131
- Each successful `analyze` run calculates four criteria from the same completed
132
+ Each successful `analyze` run calculates five criteria from the same completed
132
133
  observations. The report shows their status for each supported decision and
133
134
  aggregate counts with explicit denominators:
134
135
 
@@ -138,6 +139,7 @@ aggregate counts with explicit denominators:
138
139
  | Condition | Every atomic condition evaluated both true and false | Two required truth values per supported condition |
139
140
  | Condition/Decision | Both Decision and Condition Coverage hold for a decision | Supported decisions |
140
141
  | MC/DC | Every condition has an independence witness pair | Supported conditions |
142
+ | Decision Table | Every executable logical rule was exercised | Non-impossible generated rules |
141
143
 
142
144
  For `logged_in? && admin?`, observations `[F-] => F` and `[TT] => T` give:
143
145
 
@@ -146,6 +148,7 @@ Decision PASS
146
148
  Condition FAIL (3/4 values observed)
147
149
  Condition/Decision FAIL
148
150
  MC/DC FAIL (1/2 conditions proven)
151
+ Decision Table FAIL (2/3 rules covered)
149
152
  ```
150
153
 
151
154
  The skipped `admin?` in `[F-]` counts as neither true nor false. Conditions
@@ -155,6 +158,12 @@ for `logged_in?`; `admin?` still needs `[TF] => F`.
155
158
 
156
159
  Decision and Condition Coverage are calculated independently from the captured
157
160
  evidence. Condition/Decision requires both; MC/DC adds independence evidence.
161
+ Decision Table Coverage is calculated independently of all of them: MC/DC asks
162
+ whether each condition can independently affect the outcome, while Decision
163
+ Table Coverage asks whether each logical rule was exercised. Neither status is
164
+ inferred from the other, so `MC/DC PASS` with `Decision Table FAIL` and the
165
+ reverse are both valid results.
166
+
158
167
  Unsupported decisions are excluded from every denominator, while
159
168
  unexecuted supported decisions remain in scope. Empty denominators are N/A.
160
169
 
@@ -170,6 +179,184 @@ under `analysis.decisions[].coverage`, and condition value evidence under
170
179
  pairs, counterpart constraints, and raw vectors remain available. Statuses
171
180
  distinguish `covered`, `partial`, `unexecuted`, and `unsupported` results.
172
181
 
182
+ ### Decision table coverage
183
+
184
+ For a Boolean decision Branchproof can represent with `AND`, `OR`, `NOT`, and
185
+ atomic conditions, it derives a reduced decision table statically from the
186
+ Boolean structure and then overlays the runtime evidence of the same run. No
187
+ application code is executed while the table is derived, and no extra test run
188
+ is performed while it is overlaid.
189
+
190
+ Rules come directly from the short-circuit evaluation paths of the Ruby expression,
191
+ so a condition the interpreter would
192
+ skip appears as an explicit don't-care (`-`) rather than as two separate rules.
193
+ Exhaustive Boolean expansion is deferred; it is not required to calculate coverage
194
+ and is never performed on the reporting path.
195
+ For `premium? && (admin? || owner?)`:
196
+
197
+ ```text
198
+ Decision Table: 3/4 rules covered (75.0%)
199
+ Rule premium? admin? owner? Result Status
200
+ R1 F - - F COVERED
201
+ R2 T T - T COVERED
202
+ R3 T F T T MISSING
203
+ R4 T F F F COVERED
204
+ R1 tests: UserAccessTest#test_free_user
205
+ R2 tests: UserAccessTest#test_admin
206
+ R3
207
+ Need:
208
+ premium? = truthy
209
+ admin? = falsey
210
+ owner? = truthy
211
+ Expected decision:
212
+ true
213
+ Reachability:
214
+ unknown
215
+ R4 tests: UserAccessTest#test_denied
216
+ ```
217
+
218
+ A missing rule describes condition values only. It does not claim which
219
+ application inputs would produce them.
220
+
221
+ An observation matches a rule when every required condition value matches and
222
+ the decision outcome matches. Conditions Ruby skipped can only line up with
223
+ don't-care positions; they never satisfy a required true or false. Every rule
224
+ carries a stable identity derived from the decision, its normalized condition
225
+ vector, the expected outcome, and the table schema version — never from a test
226
+ name, an observation order, or the Minitest seed, so saved reports compare
227
+ across runs.
228
+
229
+ Decision tables are derived for `if`, `unless`, `elsif`, ternary, `while`,
230
+ `until`, subjectless `case`/`when`, and supported Boolean pattern guards.
231
+ Multi-way `case`/`when`, `case`/`in` alternatives, safe navigation, conditional
232
+ assignment, and exception handling keep their alternative coverage model and
233
+ produce no Boolean table.
234
+ For `unless` and `until`, the outcome is the predicate value, not whether the
235
+ body executes; the terminal report labels it accordingly.
236
+
237
+ #### Reachability and impossible rules
238
+
239
+ Each rule carries a reachability status: `observed`, `unknown`, or
240
+ `statically_impossible`. Branchproof proves impossibility or leaves
241
+ reachability unknown. It never infers impossibility from a missing test, a
242
+ missing observation, application conventions, Rails validations, database
243
+ constraints, comments, or method names.
244
+
245
+ Constraint analysis version 2 requires evidence that a constraint is safe before
246
+ using it to exclude a rule. A variable name and a numeric literal do not prove
247
+ that the receiver is a number, that its comparison methods use built-in semantics,
248
+ or that its value stays unchanged. Numeric, equality, and nil-check expressions
249
+ are still normalized for inspection, but unproven source constraints remain
250
+ `unknown`. The standalone constraint solver describes its explicit model, not
251
+ arbitrary Ruby objects.
252
+
253
+ Literal truth values can establish impossibility without invoking application
254
+ methods. For `age && false`, the rule requiring the literal `false` to be truthy
255
+ cannot execute:
256
+
257
+ ```text
258
+ Decision Table: 2/2 rules covered (100.0%)
259
+ Statically impossible rules excluded: 1
260
+ Rule age false Result Status
261
+ R1 F - F COVERED
262
+ R2 T F F COVERED
263
+ R3 T T T EXCLUDED
264
+ R3 TT => T
265
+ Status:
266
+ EXCLUDED
267
+ Reachability:
268
+ STATICALLY IMPOSSIBLE
269
+ Reason:
270
+ conflicting Boolean literal requirements
271
+ ```
272
+
273
+ Impossible rules stay visible in the full report but leave the coverage
274
+ denominator. Reason codes are stable: `conflicting_numeric_bounds`,
275
+ `conflicting_equalities`, `equality_outside_numeric_range`, `nil_conflict`, and
276
+ `boolean_literal_conflict`. Human-readable messages may change independently.
277
+
278
+ Runtime evidence is authoritative. If an observation matches a rule the static
279
+ model called impossible, the rule becomes `observed`, the impossibility claim is
280
+ withdrawn, the rule returns to the denominator, and a `constraint_model_conflict`
281
+ diagnostic records the disagreement.
282
+
283
+ For example, `age > 10 && age < 5` stays unknown without a proven domain;
284
+ custom comparison methods can make both comparisons true. Similarly,
285
+ `x > 10 && (x = 0) && x < 5` can execute successfully. `Float::NAN` also prevents
286
+ treating a false comparison as its ordered complement. Runtime evidence can cover
287
+ these rules, but lack of evidence cannot exclude them. Ruby truthiness remains
288
+ distinct from Boolean equality: only `false` and `nil` are falsey.
289
+
290
+ Use `analyze --no-reachability` to disable all static exclusions, including literal
291
+ proofs. Every generated rule then remains an obligation; reports show
292
+ `Reachability: not analyzed`. The mode is persisted with the report and considered
293
+ when comparing analysis contexts. Reading a saved report preserves its original
294
+ analysis; it does not recalculate it under a different mode.
295
+
296
+ #### Decision table views and limits
297
+
298
+ `--view decision-tables` groups the terminal report by decision table, and
299
+ `--missing-only` narrows it to uncovered, non-impossible rules while retaining
300
+ uncalculated decisions with their location and reason. `decision_tables` is an
301
+ accepted compatibility spelling for the view:
302
+
303
+ ```sh
304
+ bundle exec branchproof analyze 'lib/**/*.rb' \
305
+ --view decision-tables \
306
+ --missing-only
307
+ ```
308
+
309
+ Tables grow exponentially with condition count, so
310
+ `max_conditions_for_decision_table` (default 12) and
311
+ `decision_table_rules_per_decision` (default 4096) bound the derivation. A
312
+ decision above either limit reports `Decision Table: NOT CALCULATED` with the
313
+ reason `decision_table_condition_limit_exceeded` or
314
+ `decision_table_rule_limit_exceeded` instead of a partial table.
315
+
316
+ JSON stores the table under `analysis.decisions[].decision_table` with its
317
+ `schema_version`, `constraint_analysis_version`, rules, rule identities, rule
318
+ coverage, test attribution, reachability, and reachability reason. This abbreviated
319
+ example omits counts and evidence bookkeeping fields; full reports also retain
320
+ rule indexes, vector IDs, unattributed counts, and withdrawn-impossibility details:
321
+
322
+ ```json
323
+ {
324
+ "decision_table": {
325
+ "status": "calculated",
326
+ "schema_version": 1,
327
+ "constraint_analysis_version": 2,
328
+ "rules": [
329
+ {
330
+ "id": "8f1c...",
331
+ "label": "R1",
332
+ "conditions": ["false", "dont_care", "dont_care"],
333
+ "outcome": false,
334
+ "coverage": "covered",
335
+ "reachability": "observed",
336
+ "tests": ["..."]
337
+ },
338
+ {
339
+ "id": "3ad0...",
340
+ "label": "R3",
341
+ "conditions": ["true", "false", "true"],
342
+ "outcome": true,
343
+ "coverage": "missing",
344
+ "reachability": "unknown",
345
+ "tests": []
346
+ }
347
+ ]
348
+ }
349
+ }
350
+ ```
351
+
352
+ Aggregate counts live under `analysis.coverage.decision_table` and keep rule
353
+ coverage and fully covered decisions as distinct metrics:
354
+
355
+ ```text
356
+ DT (Decision table coverage): 83.9% (47/56 rules)
357
+ Decision tables fully covered: 13/18 decisions
358
+ ```
359
+
173
360
  ### Find missing cases
174
361
 
175
362
  Use `--missing-only` to focus the terminal report on conditions that still
@@ -231,7 +418,7 @@ analysis even at Level 1. A failed, unsupported, or incomplete run is reported
231
418
  with its status and diagnostics and cannot become a successful coverage
232
419
  result by changing the display level.
233
420
 
234
- ### Focused condition and test views
421
+ ### Focused condition, test, and decision-table views
235
422
 
236
423
  Use `--view conditions` to group the report by condition. Each condition shows
237
424
  its expression, decision, and project-relative source location with the
@@ -253,6 +440,10 @@ conditions. A test that evaluates both Boolean values is evidence of execution;
253
440
  it is a proof contributor only when the analyzer's independent witness pair
254
441
  uses its observations.
255
442
 
443
+ Use `--view decision-tables` to group the report by decision table. See
444
+ [Decision table coverage](#decision-table-coverage) for the rule, reachability,
445
+ and attribution detail it renders.
446
+
256
447
  `--view` changes terminal grouping and does not change instrumentation or test
257
448
  execution. JSON output always contains the complete evidence document, so an
258
449
  explicit view cannot be combined with `--format json`. The `mcdc` executable
@@ -308,7 +499,12 @@ patterns is valid comparison context, and seed differences are disclosed.
308
499
 
309
500
  `compare` exits 0 for a complete comparison, including one with coverage
310
501
  changes; `--fail-on-regression` exits 1 when a complete comparable run loses
311
- proof. Invalid input or an incomplete comparison exits 2, which takes
502
+ MC/DC proof or decision-table rule coverage. The JSON `regression` flag includes
503
+ either kind of loss; `regressions` retains the MC/DC count and
504
+ `decision_table_regressions` supplies the separate rule-loss count. Changes to
505
+ table schemas, constraint-analysis versions, or reachability modes are reported
506
+ as analysis context changes rather than silently treated as unchanged analysis.
507
+ Invalid input or an incomplete comparison exits 2, which takes
312
508
  precedence. Reports are explicit snapshots: comparison never creates history,
313
509
  promotes a baseline, or overwrites either input.
314
510
 
@@ -321,33 +517,89 @@ short-circuited or masked rather than fixed to the same observed values. For exa
321
517
  short-circuits it once, but does not prove `right`; `[TF]` is also required.
322
518
  The condition and test views preserve that distinction.
323
519
 
324
- ### Supported conditional forms
520
+ ### Supported decision forms
325
521
 
326
- Ordinary Ruby ternaries use the same predicate instrumentation and `&&`/`||`
327
- condition trees as supported `if` decisions. For example:
522
+ Every decision has a stable `kind` and `context` in JSON. The Boolean ladder
523
+ applies to the following forms:
328
524
 
329
- ```ruby
330
- value = ready ? false : true
331
- ```
332
-
333
- Branchproof records `ready` as the ternary predicate. The decision outcome is
334
- therefore the truth value of `ready`, even though the selected branch returns
335
- `false` or `true`; branch selection, returned values, object identity, and
336
- evaluation order are unchanged. Ternaries nested inside other predicates are
337
- also inventoried at their own level, including all executed nested levels.
338
- Expanding the supported syntax increases the eligible-condition denominator,
339
- so percentages should be compared with that changed scope in mind.
340
-
341
- The existing predicate exclusions and analysis limits still apply. Keyword
342
- `and`/`or` expressions, contextual syntax, unsafe or ambiguous predicates,
343
- and limit overflows remain diagnostics rather than eligible coverage.
525
+ | Construct | Kind | Context |
526
+ | --- | --- | --- |
527
+ | `if`, modifier `if`, `elsif`, `unless`, ternary | `boolean` | `if`, `elsif`, `unless`, `ternary` |
528
+ | `while`, `until`, including modifier and post-test loops | `boolean` | `while`, `until` |
529
+ | Each subjectless `case` candidate | `boolean` | `case_when` |
530
+ | Standalone `value in pattern` | `boolean` | `pattern_in` |
531
+ | Evaluated pattern guard predicate | `boolean` | `pattern_guard` |
532
+ | Standalone `&&`, `||`, `and`, `or` | `boolean` | `short_circuit` |
533
+
534
+ Prism determines precedence. `!` and `not` appear as NOT nodes in the Boolean
535
+ tree; their operands remain the conditions. Short-circuited operands remain
536
+ not evaluated. A Boolean subtree already decomposed in a decision is not
537
+ inventoried again as a standalone decision.
538
+
539
+ Loop outcomes describe the predicate as written: an `until` predicate that
540
+ returns true ends the loop. Every predicate evaluation receives an execution
541
+ ID. Repeated equivalent executions aggregate into a vector's `count`, retaining
542
+ the supporting tests. Ternary outcomes likewise describe the predicate, not
543
+ the value returned by the chosen branch.
544
+
545
+ Other constructs use alternative coverage, separate from MC/DC:
546
+
547
+ | Construct | Kind | Context | Required alternatives |
548
+ | --- | --- | --- | --- |
549
+ | `case subject` | `multiway` | `case` | Each `when` candidate and `else` (or implicit no-match path) |
550
+ | `case/in` | `pattern` | `case_in` | Each pattern clause and explicit `else`, if present |
551
+ | `receiver&.method` | `implicit` | `safe_navigation` | Receiver nil / non-nil |
552
+ | `lhs ||= rhs` | `implicit` | `or_assignment` | RHS skipped / executed |
553
+ | `lhs &&= rhs` | `implicit` | `and_assignment` | RHS skipped / executed |
554
+
555
+ Each safe-navigation operation in a chain is a distinct decision. Assignment
556
+ instrumentation preserves Ruby's native local, instance, class, global,
557
+ constant, method, and indexed assignment operations, including receiver and
558
+ index evaluation order. Safe navigation distinguishes nil from false.
559
+
560
+ For multiway decisions, vector values mean selected (`true`), evaluated but
561
+ not selected (`false`), and skipped (`null`). Later alternatives remain skipped
562
+ when an earlier candidate matches. Implicit vectors record the selected path
563
+ and its unselected complement. Their `outcome` is a selection marker, not the
564
+ truthiness of the application's return value. Reports label these as paths,
565
+ not Boolean outcomes. Each alternative exposes selected, not-selected, and
566
+ skipped evidence with test and vector IDs. The alternative denominator is the
567
+ number of supported selectable alternatives; these decisions do not enter
568
+ Boolean-ladder or MC/DC denominators.
569
+
570
+ A `case/in` without `else` retains Ruby's native no-match exception. An execution
571
+ that fails before choosing a branch is aborted, not counted as a selected
572
+ alternative. Selected branches and assignment paths remain observed even when
573
+ their bodies or right-hand sides subsequently raise or return.
574
+
575
+ Unsupported syntax stays visible and outside coverage denominators. Current
576
+ exclusions include guarded `case/in` (`unsupported_pattern_guard`), dynamic
577
+ `when` splats (`unsupported_case_splat`), safe-navigation compound assignment
578
+ (`unsupported_assignment_target`), and rescue alternatives
579
+ (`unsupported_rescue_control_flow`). A guard predicate can still supply Boolean
580
+ evidence when Ruby evaluates it; an unsupported guarded case does not claim
581
+ pattern-match coverage from that evidence. Flip-flops remain
582
+ `unsupported_flip_flop`. Decisions inside `defined?`, contextual regular
583
+ expressions, heredocs, unsafe predicates, and limit overflows retain explicit
584
+ exclusions. Ruby-defined custom `!` methods keep their runtime behavior;
585
+ evidence that contradicts Boolean negation is rejected instead of proving
586
+ coverage with an invalid logical model.
587
+
588
+ New reports use schema `1.3`; saved schema `1.0`, `1.1`, and `1.2` reports
589
+ remain readable. Comparison distinguishes decision-table coverage movement
590
+ (`rule coverage gained`, `rule coverage lost`) from analysis movement
591
+ (`rule reachability changed`), and treats a structurally changed decision as a
592
+ changed decision-table context instead of guessing which old rule a new rule
593
+ corresponds to. Expanded discovery changes coverage denominators, so compare reports
594
+ with their supported syntax scope in mind.
344
595
 
345
596
  ### Limits
346
597
 
347
598
  `--limits` accepts a JSON object containing positive integer overrides. The
348
599
  available keys are `conditions_per_decision`, `vectors_per_decision`,
349
600
  `owner_associations_per_run`, `tests_per_run`, `exact_candidates`,
350
- `exact_search_nodes`, and `constraint_search_states`.
601
+ `exact_search_nodes`, `constraint_search_states`,
602
+ `max_conditions_for_decision_table`, and `decision_table_rules_per_decision`.
351
603
 
352
604
  ```json
353
605
  {
@@ -16,11 +16,11 @@ Not documented.
16
16
  ### `call()` <a id="method-i-call"></a> <a id="call-instance_method"></a>
17
17
  Not documented.
18
18
 
19
- ### `initialize(inventory:, evidence:, limits:)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
19
+ ### `initialize(inventory:, evidence:, limits:, reachability: = true)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
20
20
  - **@return** [Analyzer] a new instance of Analyzer
21
21
 
22
22
  ### `missing(decision_id:, condition_index:)` <a id="method-i-missing"></a> <a id="missing-instance_method"></a>
23
23
  Not documented.
24
24
 
25
- ### `pair?(decision_id:, condition_index:, left:, right:)` <a id="method-i-pair-3F"></a> <a id="pair?-instance_method"></a>
25
+ ### `pair?(decision_id:, condition_index:, left:, right:, masks: = nil)` <a id="method-i-pair-3F"></a> <a id="pair?-instance_method"></a>
26
26
  - **@return** [Boolean]
@@ -7,6 +7,10 @@
7
7
 
8
8
  Coordinates source inventory, isolated test execution, and report output.
9
9
 
10
+ ## Constants
11
+ ### `VIEWS` <a id="constant-VIEWS"></a> <a id="VIEWS-constant"></a>
12
+ Not documented.
13
+
10
14
  ## Public Instance Methods
11
15
  ### `call(argv)` <a id="method-i-call"></a> <a id="call-instance_method"></a>
12
16
  Not documented.
@@ -7,6 +7,10 @@
7
7
 
8
8
  Renders the offline document returned by Comparison.
9
9
 
10
+ ## Constants
11
+ ### `RULE_LABELS` <a id="constant-RULE_LABELS"></a> <a id="RULE_LABELS-constant"></a>
12
+ Not documented.
13
+
10
14
  ## Public Instance Methods
11
15
  ### `exit_code(fail_on_regression: = false)` <a id="method-i-exit_code"></a> <a id="exit_code-instance_method"></a>
12
16
  Not documented.
@@ -0,0 +1,21 @@
1
+ # Class Branchproof::Constraints::Solver <a id="class-Branchproof-Constraints-Solver"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | Object |
6
+ | **Defined in** | lib/branchproof/constraints.rb |
7
+
8
+ Accumulates the constraints one decision-table rule requires and reports the
9
+ first proven contradiction. Subjects never interact with each other.
10
+
11
+ ## Public Instance Methods
12
+ ### `add(constraint, truth)` <a id="method-i-add"></a> <a id="add-instance_method"></a>
13
+ Returns a reason code when the rule became unsatisfiable, otherwise nil.
14
+
15
+ ### `add_prepared(constraint, truth)` <a id="method-i-add_prepared"></a> <a id="add_prepared-instance_method"></a>
16
+ Adds a constraint that has already been symbolized and validated by
17
+ <code>usable?</code>. Source inventory can use this path after preparing each
18
+ leaf once instead of repeating normalization for every solver state.
19
+
20
+ ### `initialize()` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
21
+ - **@return** [Solver] a new instance of Solver
@@ -0,0 +1,98 @@
1
+ # Module Branchproof::Constraints <a id="module-Branchproof-Constraints"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/branchproof/constraints.rb |
6
+
7
+ Normalizes simple atomic conditions into subject/operator/value records and
8
+ decides, conservatively, whether a set of required condition truths can hold
9
+ at the same time.
10
+
11
+ The module never evaluates application code. It only inspects the syntax of a
12
+ condition, and it answers "contradictory" only when the contradiction follows
13
+ from the normalized constraints alone. Every other shape stays unrepresented,
14
+ which leaves the owning decision-table rule reachability `unknown`.
15
+
16
+ ## Constants
17
+ ### `COMPARISON_OPERATORS` <a id="constant-COMPARISON_OPERATORS"></a> <a id="COMPARISON_OPERATORS-constant"></a>
18
+ Not documented.
19
+
20
+ ### `FALSEY_TYPES` <a id="constant-FALSEY_TYPES"></a> <a id="FALSEY_TYPES-constant"></a>
21
+ Not documented.
22
+
23
+ ### `FLIPPED` <a id="constant-FLIPPED"></a> <a id="FLIPPED-constant"></a>
24
+ Not documented.
25
+
26
+ ### `LITERAL_TYPES` <a id="constant-LITERAL_TYPES"></a> <a id="LITERAL_TYPES-constant"></a>
27
+ Not documented.
28
+
29
+ ### `NUMERIC_OPERATORS` <a id="constant-NUMERIC_OPERATORS"></a> <a id="NUMERIC_OPERATORS-constant"></a>
30
+ Not documented.
31
+
32
+ ### `NUMERIC_TYPES` <a id="constant-NUMERIC_TYPES"></a> <a id="NUMERIC_TYPES-constant"></a>
33
+ Not documented.
34
+
35
+ ### `OPERATORS` <a id="constant-OPERATORS"></a> <a id="OPERATORS-constant"></a>
36
+ Not documented.
37
+
38
+ ### `REASONS` <a id="constant-REASONS"></a> <a id="REASONS-constant"></a>
39
+ Not documented.
40
+
41
+ ### `REASON_MESSAGES` <a id="constant-REASON_MESSAGES"></a> <a id="REASON_MESSAGES-constant"></a>
42
+ Not documented.
43
+
44
+ ### `SUBJECT_KINDS` <a id="constant-SUBJECT_KINDS"></a> <a id="SUBJECT_KINDS-constant"></a>
45
+ Not documented.
46
+
47
+ ### `VERSION` <a id="constant-VERSION"></a> <a id="VERSION-constant"></a>
48
+ Not documented.
49
+
50
+ ## Public Class Methods
51
+ ### `comparison_constraint(node, name)` <a id="method-c-comparison_constraint"></a> <a id="comparison_constraint-class_method"></a>
52
+ Not documented.
53
+
54
+ ### `for_node(node)` <a id="method-c-for_node"></a> <a id="for_node-class_method"></a>
55
+ Derives the normalized constraint of one atomic condition, or nil when the
56
+ expression is outside the supported vocabulary.
57
+
58
+ ### `literal_for(node)` <a id="method-c-literal_for"></a> <a id="literal_for-class_method"></a>
59
+ String equality stays unsupported in v1 so that encoding and mutability
60
+ questions cannot turn into an impossibility claim.
61
+
62
+ ### `message(reason)` <a id="method-c-message"></a> <a id="message-class_method"></a>
63
+ Not documented.
64
+
65
+ ### `mixed_numeric_literals?(left, right)` <a id="method-c-mixed_numeric_literals-3F"></a> <a id="mixed_numeric_literals?-class_method"></a>
66
+ - **@return** [Boolean]
67
+
68
+ ### `nil_constraint(node)` <a id="method-c-nil_constraint"></a> <a id="nil_constraint-class_method"></a>
69
+ Not documented.
70
+
71
+ ### `numeric?(literal)` <a id="method-c-numeric-3F"></a> <a id="numeric?-class_method"></a>
72
+ - **@return** [Boolean]
73
+
74
+ ### `same_literal?(left, right)` <a id="method-c-same_literal-3F"></a> <a id="same_literal?-class_method"></a>
75
+ - **@return** [Boolean]
76
+
77
+ ### `simple_call?(node)` <a id="method-c-simple_call-3F"></a> <a id="simple_call?-class_method"></a>
78
+ - **@return** [Boolean]
79
+
80
+ ### `single_argument(node)` <a id="method-c-single_argument"></a> <a id="single_argument-class_method"></a>
81
+ Not documented.
82
+
83
+ ### `subject_for(node)` <a id="method-c-subject_for"></a> <a id="subject_for-class_method"></a>
84
+ Only unambiguously identifiable storage locations become subjects. Method-call
85
+ receivers stay unsupported: a repeated call may return a different value or
86
+ have side effects (see the v1 constraint scope).
87
+
88
+ ### `subject_key(subject)` <a id="method-c-subject_key"></a> <a id="subject_key-class_method"></a>
89
+ Not documented.
90
+
91
+ ### `symbolize(value)` <a id="method-c-symbolize"></a> <a id="symbolize-class_method"></a>
92
+ Not documented.
93
+
94
+ ### `usable?(constraint)` <a id="method-c-usable-3F"></a> <a id="usable?-class_method"></a>
95
+ - **@return** [Boolean]
96
+
97
+ ### `valid_literal_value?(literal)` <a id="method-c-valid_literal_value-3F"></a> <a id="valid_literal_value?-class_method"></a>
98
+ - **@return** [Boolean]
@@ -8,6 +8,9 @@
8
8
  Derives condition- and test-oriented rows from one report document.
9
9
 
10
10
  ## Attributes
11
+ ### `alternatives` [R] <a id="attribute-i-alternatives"></a> <a id="alternatives-instance_method"></a>
12
+ Returns the value of attribute alternatives.
13
+
11
14
  ### `conditions` [R] <a id="attribute-i-conditions"></a> <a id="conditions-instance_method"></a>
12
15
  Returns the value of attribute conditions.
13
16
 
@@ -15,5 +18,8 @@ Returns the value of attribute conditions.
15
18
  Returns the value of attribute tests.
16
19
 
17
20
  ## Public Instance Methods
21
+ ### `decision_tables()` <a id="method-i-decision_tables"></a> <a id="decision_tables-instance_method"></a>
22
+ Not documented.
23
+
18
24
  ### `initialize(document:)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
19
25
  - **@return** [CoverageIndex] a new instance of CoverageIndex
@@ -0,0 +1,22 @@
1
+ # Module Branchproof::DecisionSyntax <a id="module-Branchproof-DecisionSyntax"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/branchproof/decision_syntax.rb |
6
+
7
+ Discovers control-flow expressions whose truth is not represented by an
8
+ ordinary Prism IfNode. The records intentionally contain byte ranges and
9
+ scalar metadata only; Prism nodes must not escape the source pass.
10
+
11
+ ## Constants
12
+ ### `AND_WRITE_NODE_CLASSES` <a id="constant-AND_WRITE_NODE_CLASSES"></a> <a id="AND_WRITE_NODE_CLASSES-constant"></a>
13
+ Not documented.
14
+
15
+ ### `OR_WRITE_NODE_CLASSES` <a id="constant-OR_WRITE_NODE_CLASSES"></a> <a id="OR_WRITE_NODE_CLASSES-constant"></a>
16
+ Not documented.
17
+
18
+ ## Public Instance Methods
19
+ ### `flow_decisions_for(program, bytes, source_id, file_reasons = [], encoding = "UTF-8", nodes: = nil)` <a id="method-i-flow_decisions_for"></a> <a id="flow_decisions_for-instance_method"></a>
20
+ nodes: flow-decision nodes already collected by a caller's own AST walk
21
+ (Source merges this discovery into one pass). Falls back to its own walk when
22
+ nothing is passed in, so this method still works standalone.