Skip to content

refactor(abc,cyclomatic): declare Comparison and ShortCircuit in Java, Python and C++ with every consumer — go/no-go #1694

Closed

Description

@dekobon

Objective

Declare Comparison and ShortCircuit — owners, payers, projection — in Java, Python and C++, and route every consumer that reads either (ABC's token arm and scores_itself, cyclomatic's short-circuit arm, cognitive's boolean-sequence arm, the getter's operator arm) through the rows in one change. Zero drift. This is the architectural go/no-go: R05: #1695–R11: #1701 do not start until it records "go".

Umbrella: R00: #1690 · Planning ID: R04: #1694
Completion prerequisites: R03: #1693

Status: implemented on branch refactor/1694-1695-paying-roles at c59fcd55 (eight commits on bdd70eb9; not pushed). Decision: go — recorded on #1690. Zero drift measured on the corpora, the fixtures and a recovery-tree sweep; see the completion comment for the evidence tables and the perturbation table.

Adjustments to this body, recorded during implementation (each a specification correction, not a behaviour change):

  • Java's instanceof payer is the keyword, gated to its instanceof_expression, not the self-paying expression node: Halstead bills the keyword, ABC billed the node, and one payer must serve both. The gate matters — a keyword error recovery strands under an ERROR would otherwise score a condition the node never did.
  • Halstead has a named complement. Getter::get_paying_role (new, emitted by paying_roles!) reads the rows between get_keyword_role and the byte-less get_op_type. Each language's byte-less arm keeps the spellings the row's application gate leaves out (Java generic < / > and recovered tokens; Python loop in; C++ overload names, reference declarators, requires conjunctions) under a role-bypass: allow naming them. For those kinds the row is not what makes Halstead bill them. This is the per-language escape the acceptance criteria asked to be named.
  • cpp_operator_is_applied lives in the AST crate (roles::c_family::cpp_family_operator_is_applied) so the rows can gate on it; the applied_if macro escape is gone.
  • Representation: role_table! output is pub; fragments splice into pays lists with inline gates; project() falls back to the owner's first declared-payer child when the grammar names no field. The C-family ABC slot helpers take a ScoresItself parameter so C / Objective-C keep the name-keyed predicate until R05.
  • Contract 5's TS/TSX fold landed here too (ts_family_modifiers! / ts_family_decl_keywords!).

Why this exists, and why it is the go/no-go

The deepest (b)-class chain in the tracker is the ABC slot/token pair: five languages, five issues, every sweep clearing the next victim (#1383 → #1421 → #1453 → #1523 → #1527). The machinery that landed to end it — #1526's count_boolean_slot — produced four more within two days (#1529, #1533, #1527, #1537), because it centralised the rule and left scores_itself and wrapper_operand as per-language lists that must agree with a third list by comment (src/metrics/abc.rs, "must agree with those arms exactly").

The exploratory inventory identified owner/token pairings but did not establish either new live drift or exhaustive agreement; the required agreement is between ABC's slot predicate and its own counting paths. That is what a construct row is intended to declare once. The design review's blocking finding was that a flat kind role cannot serve both — scores_itself receives BinaryExpression, the token arm fires on LT — so this package is where owners/payers/projection is proven or found wanting.

Two roles rather than one, because Comparison alone touches no cyclomatic arm (comparisons are not cyclomatic decisions) and would leave the three-consumer case unproven. ShortCircuit is read by all of ABC, cyclomatic and cognitive. The three languages cover the hard cases: Java needs the projection and keeps its AssignmentExpression recursion as a named per-language extra; Python has an owner-only, never-paying BooleanOperator for ABC that cyclomatic does count via the And/Or tokens (cyclomatic/python.rs) — the same row, two policy expressions; C++ has ConstraintReach (requires suppresses every decision in its subtree — #1525) and spells && in fold expressions, where cpp_operator_is_applied counts the fold once through its operator field.

Implementation steps

  • roles/java.rs, roles/python.rs, roles/cpp.rs (via R03: #1693's template, with Cpp extras): full Comparison and ShortCircuit rows. Java: LT/GT gated under binary_expression (fix(abc): generic type-parameter <> counts as conditions in Java and Groovy #1274), InstanceofExpression self-paying (decide: four uncounted boolean operands, and whether terminal sets should be token arms #1461). Python: ComparisonOperator self-paying; BooleanOperator owner, And/Or payers. C++: comparison and &&/|| tokens gated by the existing cpp_operator_is_applied (binary and fold parents; reuse, do not re-derive).
  • Root policy for the three languages: abc::scores_itself = owns_Comparison(projected) || owns_ShortCircuit || owns_Ternary || <lang>_extra(expr) where the extra is the existing assignment-recursion fn; the ABC token arm reads pays_at(Leaf) for Java and C++ and pays_at(Wrapper) for Python; Python's ABC expression omits pays_ShortCircuit (owner-only, fix(abc): if predicate scores 0 for -x, this, *p across langs #1526) while its cyclomatic expression includes it; the root's cyclomatic expression reads pays_at(Leaf); cognitive's compute_booleans_keyed keeps its keyed projection — C++ maps and and && to one sequence key (cognitive/cpp.rs), so the row supplies the kinds and the per-language key fn stays beside the table; the root's Halstead expression reads pays_at(Leaf) and its ABC expression for Python reads pays_at(Wrapper) — the ComparisonOperator wrapper — which is the per-consumer split the fixture exists for. Every other role's arms are untouched.
  • ConstraintReach unchanged; a comment at the call site states the row is consulted under it.
  • Delete the three languages' comparison and short-circuit lists from abc/*.rs, cyclomatic/*.rs, cognitive/*.rs and the getter; bypass gate passes with no new exemptions.

Tests and edge cases

  • src/metrics/abc/slot_pays_tests.rs rows for the three languages unchanged and passing — they carry cyclomatic as an oracle ABC did not write.
  • Lesson 50's independent-path inputs per language: a bare-identifier predicate (only the slot pays), a comparison predicate (only the token pays), a comparison outside any slot (boolean b = x < y; — only the token pays, no slot to compensate), a C++ fold (a && ... && b), a requires (A<T> && B<T>) whose && must score zero, and a mixed-spelling C++ chain a and b && c whose cognitive boolean-sequence score is unchanged.
  • Perturbations, each with the observed failing tests recorded (do not require exclusivity — the slot can compensate for a dropped token inside a predicate, which is why the outside-slot fixture exists): (1) drop LT from Java's payers; (2) projection returns None; (3) BinaryExpression listed as a payer — census rejects at expansion.
  • Zero drift on the corpus for Java, Python, C++: metric JSON and space trees byte-identical; both snapshot forms counted; plain run; no .snap.new under tests/repositories/big-code-analysis-output/.
  • make bench-scaling on the comparison-chain and &&-chain probes; make chain-audit.

Acceptance criteria

  • Every consumer of the two roles in the three languages reads the row; no comparison or short-circuit kind matched inside the consumer boundary for them.
  • Zero drift evidenced per language with commands and output.
  • A go/no-go note on the umbrella: did owners/payers/projection hold for the projection case, the owner-only-for-one-consumer case, the fold case and the suppression case? Name any per-language escape needed beyond the named extra. R05: #1695+ proceed only on a recorded "go".

Verification and completion evidence

  • Perturbation table (three rows, observed failures each); corpus diff commands and snapshot counts; bench-scaling exponents; chain-audit pass; BCA_GATE: line.

Activity

  1. added
    refactorCode maintainability / tech-debt cleanup (no behaviour change)
    on Oct 23, 2026
  2. dekobon commented on Oct 23, 2026

    @dekobon
    OwnerAuthor

    Tracking is ready

    The body now links the master issue and the work packages it references. Native parent and blocked-by relationships match the listed completion prerequisites. Implementation and acceptance checkboxes remain open; prerequisite closure and the R04: #1694 go decision govern execution order.

  3. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    R04 is implemented on refactor/1694-1695-paying-roles (not pushed), eight commits on bdd70eb9, eca909a0 through c59fcd55: the representation (eca909a0), Java (283ce8cd), Python (ad636c57), the C++ dialects (4c129579), the TS/TSX keyword fold contract 5 assigned here (fd300ade), the changelog, and two review follow-ups (cdf5df46, c59fcd55).

    Specification corrections, recorded before the rows were written

    • Java's instanceof payer is the keyword, not the expression. The body says InstanceofExpression self-paying (R01's proof row). Halstead bills the instanceof keyword and ABC billed the node; one payer has to serve both, and the keyword is it — Instanceof gated_by under_instanceof_expression, the expression an ordinary owner. One pays per test, plain or Java 16 pattern form; the count is unchanged. The gate is not decoration: error recovery strands a keyword whose expression it lost (return x instanceof at end of input) under an ERROR, and the ungated payer scored a condition there that the expression arm never did (review finding, fixed in c59fcd55).
    • Halstead reads the rows through a new hook, with a named complement. Getter::get_paying_role (emitted by paying_roles!(lang) from the two rows) is read between get_keyword_role and the byte-less get_op_type on every path the walk takes. A row gates its payers on application — the gate the counting consumers need — but Halstead bills the non-applying spellings too: Java's generic < / > and a recovered token, Python's loop in, C++'s operator<, int&& x, void f() &&, a requires-clause conjunction. Those stay in each byte-less arm under a role-bypass: allow(Comparison, ShortCircuit) exemption naming them. For those kinds the row is not what makes Halstead bill them — the vocabulary is the union of row and arm. This is the per-language escape the acceptance criterion asked to be named; the pilot proves shared recognition for the three decision consumers plus a vocabulary-preserving Halstead union, not the elimination of every independent membership list.
    • cpp_operator_is_applied moved to the AST crate as roles::c_family::cpp_family_operator_is_applied, since the rows gate on it; the root's applied_if escape in impl_cyclomatic_c_family! is gone and the C++ dialects use impl_cyclomatic_c_family_rows!.
    • Representation extensions: role_table! output is pub (the root crate reads roles::<lang>::<role>); ..frag!() splices into pays [..] lists with inline gates (LT gated_by applied,), the gate resolving at the splicing row, read by utils/role_tables.py too; project() falls back to the owner's first declared-payer child when the grammar names no field (Java's instanceof_expression). Node::count_specific_ancestors takes impl Fn for check.
    • C and Objective-C share the C-family ABC slot helpers with C++, so the helpers take a ScoresItself parameter: Cpp/Mozcpp pass row-keyed closures, C/Objc keep the name-keyed predicate until R05.

    The four hard cases

    case held? how
    projection (Java) yes BinaryExpression → operator field; InstanceofExpression → the instanceof child by the payer fallback; projection_lands_on_each_owners_paying_child pins both forms
    owner-only for one consumer (Python) yes ABC reads comparison::pays_at_wrapper and short_circuit::pays_at_leaf only to find the chain whose operand slots pay; cyclomatic reads the same leaf and counts it; python_rows_feed_each_abc_path_independently has both policies side by side with cyclomatic as oracle
    fold (C++) yes FoldExpression is an owner of both rows, gated on its operator field; the payer's applied gate admits only the field token, so a binary fold pays once; (ts && ...), `(...
    suppression (C++) yes ConstraintReach untouched; the rows are read under it with the call-site comment saying a parenthesised constraint is an ordinary binary_expression only the clause above can disqualify; requires A<T> && B<T>, requires (A<T> && B<T>) and static_assert(requires { a && b; }) score zero in the tests

    Where the representation bent: the project fallback embeds "first declared-payer child" in the macro rather than a per-owner projector — adequate for every owner here, and a named per-row projector is the right move before a text-gated or multi-token construct needs one (R05 note). scores_itself asks the rows about a peeled descendant with Ancestors::unknown(); sound because none of the twelve owner gates reads ancestors (each reads the node's operator field or is unconditional), documented at each call site, and not a licence for an ancestor-gated owner row later.

    Zero drift

    /tmp/opencode/1694/oracle.py runs bca ops --no-config --language L -O json and bca metrics likewise over every corpus file plus one fixture per language spelling every row kind, before (bdd70eb9) and after each commit, diff -rq of the trees:

    language files outputs differing
    java 12 24 0
    python 73 146 0
    cpp 1048 2096 0
    mozcpp (the C++ population run as --language mozcpp) 1048 2096 0
    typescript + tsx (6 files, each as both dialects) 6 24 0

    Recovery sweep (recovery_oracle.py): each fixture truncated at every line boundary and sixteen byte offsets, both binaries, ops + metrics, every dialect: 780 outputs, 0 differing. INSTA_FORCE_PASS=1 cargo test --workspace --all-features --no-fail-fast → 6346 passed, 0 .snap.new, 0 .pending-snap, output submodule clean; plain run identical.

    Gates

    Bypass baseline 125 → 86 (every Java and Python comparison / short-circuit site; the C++ sites were name-keyed and invisible to it), remove-only. Census 501 → 495 pending. Coverage −36 (&&, ||, instanceof in Java; and, or, boolean_operator in Python; 15 ids per C++ dialect). Six new role-bypass: allow exemptions, all the Halstead complement arms or the C++ cognitive sequence key, none for membership. rustfmt-bail +8, all in the two cyclomatic *_rows! macro bodies (cause 2, metavariable patterns) and the Java getter guard arm.

    make pre-commit on c59fcd55 → BCA_GATE: pass (gate=pre-commit). make chain-audit 4261 passed. make bench-scaling: all 82 probes within bound; cyclomatic/cpp-and-chain 1.01, cognitive/cpp-and-chain 1.02, abc/cpp-and-chain 0.99, halstead/nested-not 1.00. Feature isolation with -p big-code-analysis under --features java|python|cpp|mozcpp|go (the last compiles none of the new tests). make check-test-lang-gates-compare COMPARE_REF=origin/main OK.

    Perturbations (observed failures, --no-fail-fast, lib targets + parity)

    perturbation failing tests
    Java: drop LT from the payers 4 — role_rows, java_assignments_with_conditions, java_generic_angles_are_one_pair_1559, comparison_row_separates_the_owner_from_its_token
    Java: project returns None 22 across abc (14), cognitive (4), roles (2), slot_pays, C# parity
    Java: BinaryExpression listed as a payer compile error at expansion (owner / payer disjointness)
    Java: drop LTEQ 1 — java_comparison_and_short_circuit_spellings_bill_1694 (Halstead)
    Java: drop Instanceof 6 — role_rows, slot_pays, two abc tests, two roles tests
    Java: Instanceof ungated 1 — the truncated-input row
    Python: drop In 1 — bare_in_and_is_pay_only_under_the_wrapper
    Python: drop LTEQ 1 — the Halstead spelling test
    Python: drop Or 13 — abc (5), cognitive (7), halstead (1)
    C++ (both dialects): drop LTEQGT 7 — role_rows ×2, three abc, two parity
    C++: drop And 15 — abc, cognitive, cyclomatic, the alternative-token suite, parity
    C++: applied true for every parent 17 — abc, cyclomatic, halstead, ops, parity
    C++: FoldExpression dropped from ShortCircuit owners 5 — slot_pays ×2, cognitive fold ×2, parity

    Reviews: astra-review after the Java slice (four findings, fixed in cdf5df46: a bypass-gate hole, project unable to reach instanceof, a recovery-tree Halstead drift for <, a missing Python self-test) and after completion (three findings, fixed in c59fcd55: the instanceof recovery drift above, a cached-kind hole in the forwarding-macro exemption, two overstated test comments). The second review's verdict was "not go for that HEAD" on the two drift / gate findings and "the four hard cases otherwise hold"; both are fixed and re-measured.

  4. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Completed on refactor/1694-1695-paying-roles (c59fcd55), reviewed twice with go recorded on #1690; the follow-on R05 (#1695) is done on the same branch, which carries both. Closing as complete; the branch is not pushed and lands with #1695.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refactorCode maintainability / tech-debt cleanup (no behaviour change)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions