Skip to content

refactor(abc,cyclomatic): declare the remaining paying constructs by role across languages #1695

Closed

Description

@dekobon

Objective

Declare the remaining paying constructs — Comparison and ShortCircuit for the other twenty languages, then EagerLogical, Ternary, CaseArm, Guard, NullSafe — one role at a time across all languages, every consumer of the role in the same change. Zero drift per role.

Umbrella: R00: #1690 · Planning ID: R05: #1695
Completion prerequisites: R04: #1694 Completion of R04: #1694 includes a recorded go decision.

Why this exists

R04: #1694 proved the shape on three languages; the trails span twenty. The order is by role across languages, not by language across roles, because that is the direction the (b)-chains run — one construct found in one language and re-found in the next (#1383 → #1527 for the branch arm; #1470 → #1522 → #1530 → #1536 for word-spelled operators) — and because migrating every consumer of a role together is what stops a half-migrated role becoming the next disagreement (#1529: the Rule-9 chain walker left on the deleted terminal list in the file whose slot had just been rewritten).

Documented policy differences stay as terms in the root expressions: Bash &&/|| are not ABC conditions (#1536); Groovy's elvis counts once per token, the Kotlin reading; Ruby and/or are short-circuit; Kotlin's infix and/or/xor are eager — both operands are evaluated — and decision-counting consumers add no chain increment for them. EagerLogical adds recognition without changing current counts or Halstead classification.

Implementation steps

  • Comparison and ShortCircuit across the remaining languages, including word spellings (Perl eq/lt/cmp, Ruby ===/=~, Groovy ===/=~/<=>/in/instanceof) and the text-gated Tcl/iRules BinopExpr operator read. Dialect templates for js-family and tcl-family. Groovy's row, as the worked example of a node/token pairing that is comment-held today (abc/groovy.rs): owners SpaceshipExpression, IdentityExpression, RegexFindExpression, RegexMatchExpression projected to their operator token; payers LTEQGT, EQEQEQ, BANGEQEQ, EQTILDE, EQEQTILDE; self-paying MembershipExpression, InstanceofExpression.
  • EagerLogical: Kotlin infix and/or/xor, Perl and PHP xor. fix(metrics): Perl xor and Bash -a/-o miss metric parity #1536 treats xor as a value, not a chain: no Rule-9 chain increment; its boolean slot pays under the ordinary slot rule (abc/php.rs, abc/perl.rs). Preserve each language's existing Halstead classification: Perl/PHP xor remains an operator; Kotlin infix names remain identifier operands. EagerLogical adds recognition, not a billing change. Bash -a/-o exclusions must name the role and preserve their existing classification elsewhere.
  • Ternary (? under the ternary node only — fix(abc): generic type-parameter <> counts as conditions in Java and Groovy #1274's wildcard case; Bash arithmetic ternary; elvis as NullSafe or Ternary per the existing per-language decision, recorded on the row).
  • CaseArm and Guard (C# when_clause, Java Guard, Ruby IfGuard/UnlessGuard, Rust match-arm if; default/_ arms excluded with the lesson-11 reason).
  • NullSafe (?., &., ??, ?->): preserve per-language consumer policy. Optional navigation and null coalescing need not have identical consumers; JS-family cognitive includes ?? and ??=. Record the distinct structural forms and consumer projections instead of imposing a cyclomatic-only rule (fix(js-family): normalize optional chaining across cyclomatic and Halstead #281, fix(cyclomatic): safe-navigation not counted for Ruby (&.) and Groovy (?./??.); stale grammar comment #452).
  • After each role: delete the per-language lists it replaces inside the consumer boundary; bypass gate passes. Hand-written tests that pin behaviour (not membership) stay.

Tests and edge cases

  • slot_pays_tests.rs and tests/parity/cyclomatic_cross_language_parity.rs unchanged and passing per role.
  • Per role, per language that has the construct: the R04: #1694 input set (slot-only, token-only, outside-slot).
  • Zero drift per role; both snapshot forms; plain run. A sibling found wrong while writing its row is a separate commit with its own baseline and submodule bump, never inside the migration commit.

Acceptance criteria

  • Every (language, paying-role) pair has rows or absent(reason); no pending cells remain for these roles.
  • The "must agree with those arms exactly" comment in src/metrics/abc.rs is rewritten to say the agreement is declared in the row and checked by the census — not deleted, because the slot driver still has to be called, and that is what the hand-written behaviour tests guard.

Verification and completion evidence

  • Per-role, per-language command + output table; snapshot counts; BCA_GATE: line; behaviour-change commits listed with issue numbers.

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

    Done — every paying role declared in every language

    Branch refactor/1694-1695-paying-roles (not pushed; 28 commits on bdd70eb9, R04 through c59fcd55). Per role, one migration commit and one review-fix commit, each with a fresh-context review:

    Role Commits Census (pending)
    Comparison + ShortCircuit, 20 langs 87f67091…cc6829a7 (8), fix f9ba4188 495 → 457
    EagerLogical 8738dd1d, fix 7122d8b0 → 448
    Ternary c50bcf69, fix 4be5dab6 → 430
    CaseArm + Guard dd90dbf6, fix e972fa4f → 400
    NullSafe 6c89e00d, fix acf4dfb9 → 390
    Branch review a6099245 (Elixir Halstead scaling), cf62aa9e (feature isolation), c58ffad4 (CHANGELOG)

    No cell of the seven paying roles is pending in any of the 25 languages; coverage baseline −535 ids over the step; bypass baseline 86 → 84 (Java's two guard sites); rustfmt-bail −7.

  4. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Specification corrections, recorded on the rows:

    • Python's & / | / ^ on two bools and Bash's [ ] -a / -o are eager, not absent (both cells were absent; now owns [] with the kinds excludesd and the reason — Bash's -a/-o stay the ShortCircuit row's text-gated payer because every consumer scores them as the &&/|| they spell, fix(metrics): Perl xor and Bash -a/-o miss metric parity #1536).
    • Java's Guard was R01's pays never proof row; cyclomatic pays the guard node, so the owner is the paying projection. pays never now has no live user (the DSL keeps the form).
    • C#'s when is also a _reserved_identifier at the pin (int when = 1;), so the Guard row's owners are the clause nodes and the token is excludesd, billing through the byte-less arm.
    • C#'s is and Java's instanceof payers are gated to their type-test owner; JS/MozJS ? and PHP </> are ungated — each because error recovery strands the token under an ERROR, and the arm being replaced counted it (or not). Both directions were caught by the recovery sweep or review, never by the corpus.
    • Python's Ternary row said cyclomatic does not count it; it does, through the if keyword. Cyclomatic now reads the row at the node and declines the keyword under it.
    • C#'s ConditionalAccessExpression is pays_itself beside the ?? payer: one node is one ?, the counting consumers pay the node, and Halstead bills the bare ? through the byte-less arm. scores_itself reads only the coalescing owner — reading the access too dropped !(a?.b)'s negated-operand condition (found by the NullSafe review; fixture and row added).
  5. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Gates. Both snapshot forms (forced-pass and plain) exit 0 with no stranded snapshot files; the output submodule is untouched. The pre-commit gate reports pass on every commit; chain-audit exits 0; the scaling benchmark passes, including the two new probes cyclomatic/tcl-wide-switch (0.93) and halstead/elixir-repeated-guards (0.98); the per-language membership comparison against origin/main loses no build; single-feature library builds for rust, go, elixir, tcl and kotlin are green (the last caught case_arms gated per language while the tables importing it are not, fixed in cf62aa9e).

  6. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Zero drift. Every ops and metrics output was byte-compared against the R04 head binary on every population after the final commit: Bash 144 files, Cpp 1048 (both dialects), C# 22, Java 12, JavaScript 389 (both dialects), Python 73, Rust 173, TypeScript 8, TSX 1, plus a fixture per language spelling every row kind: 0 of 6,638 outputs differ.

  7. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Recovery sweep. Every fixture truncated at each line boundary, at sixteen offsets, and one byte after every operator punctuation (R04 and R05 fixtures, both binaries): 4,388 outputs, 0 differing.

  8. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Drifts found in flight, each fixed before its commit landed (none accepted as a snapshot):

    • a macro && reaching Rust's ABC chain walker (two serde corpus files)
    • a self-paying owner billed to Halstead (Groovy negated membership)
    • Perl's rewrite exception applied to a comparison against a substitution's result
    • C#'s is, JS/MozJS ?, PHP's orderings and iRules' switch arm stranded under an ERROR
  9. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor
    • Tcl arms named for / switch losing their command reading
    • C#'s conditional access paying at node and token
    • a negated C# conditional access losing its negated-operand condition

    Two complexity regressions found by review, both fixed with a scaling probe: Tcl's per-arm rescan of the switch's options, now memoised per word list; and Halstead asking Elixir's guard-position climb once per when, now a hand-written reader as Rust's and Bash's are.

  10. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Gate predicates the rows read moved into the parse crate: the Elixir decision module (a move) and the new case_arms module. Perturbations (one payer or gate removed per language per role): every one failed at least one test; counts are in each commit message. The final branch review recorded no further findings beyond the Elixir scaling fix. Not pushed; no PR opened.

  11. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Representation added to the row macro: a conditional self-paying owner, a text-gated payer, a first-declared-payer-child fallback for the projection where the grammar names no field, and empty owner and payer constants on an absent row. The Python reader reads each of them.

  12. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    All acceptance items ticked; evidence in the comments above. Closing as complete. The branch refactor/1694-1695-paying-roles (cf62aa9e) is not pushed and no PR is opened — that is the next step, outside this issue's scope.

  13. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Superseded names (cleanup after close). Four cleanup commits landed on the branch after this issue closed, so several names cited above no longer exist at HEAD:

    • c_family_gates!, cpp_family_gates!'s operator fns, js_family_operator_gates!, js_family_null_safe_gate!, tcl_family_gates!, c_family_case_arm_gate!, js_family_case_arm_gate!, cpp_family_operator_in, and every per-language comparison_operator / short_circuit_operator / null_coalescing_operator / elvis_operator / defined_or_operator / under_* / is_case_label / safe_navigation gate fn — replaced by the DSL's owner holds_payer and payer under P / under [P, Q] (06a68274, 894059a6). cpp_operator_is_applied is cpp_family_operator_is_applied.
    • js_abc_compute! / ts_abc_compute! → one js_family_abc_compute!; Mozcpp's ABC/cognitive impls → cpp_family_abc! / cpp_family_cognitive!; C and ObjC cyclomatic → impl_cyclomatic_c_family_rows! with extra = [..].
    • Halstead reads rows through pays_at_token_kind; scores_itself leads with rows_own!; owns_via(payer, …) replaces owns && project == K.
    • Coverage delta stated as −535 above is −487 over the whole branch (−451 for R05 alone).
  14. dekobon commented on Oct 24, 2026

    @dekobon
    OwnerAuthor

    Post-close review found two zero-drift breaks, both on recovery shapes the branch's sweep could not reach (it cut after operator punctuation, not after keywords): Groovy's stranded instanceof ({ o instanceof } at end of input) paid an ABC condition, and Elixir's when named in &when/2 billed as a Halstead operator. Fixed in af4fc7cb (Instanceof under InstanceofExpression; the Elixir reader declines an operator_identifier parent), each with a regression row verified by perturbation. The recovery sweep now cuts after every word as well: 14,588 outputs, 0 differing; corpus oracle 0 of 6,638. Lesson 102 in docs/development/lessons_learned.md records why the sweep missed them.

    Also in that batch: project() reads its field by grammar id (cached per row), Perl ABC's scores_itself scans once, and two pre-existing quadratics the rows became the reader of — Elixir repeated guards under cyclomatic/ABC, Python and chains under cognitive — are linear, with probes.

  15. dekobon commented on Oct 25, 2026

    @dekobon
    OwnerAuthor

    R05 verified on the current base (7425561f) before R06/R08 start.

    Traceability correction. The comments above say the branch refactor/1694-1695-paying-roles was "not pushed, no PR". It has since been rebased onto the 955da8d5..44184ad7 getter-fix batch and fast-forwarded into main; the branch ref no longer exists and the hashes cited above (c59fcd55, cf62aa9e, af4fc7cb, …) are dangling. The same commits on main are f23fb7bc..c65df636 (R04 + R05, 28 commits), ac3f7781 (the rebased twin of the post-close af4fc7cb; same message, only rebase context differs), and 6c9818cf..7425561f (five later post-close commits: review findings, the Kotlin-only gate, lesson 102, the Perl/Elixir guard-chain memo, the Bash/Elixir owner-gate tests). main == origin/main == 7425561f.

    Endpoint behavioural verification (44184ad7, the revision immediately before the rebased R04/R05 sequence, vs 7425561f; binaries built from each in separate worktrees). This establishes that the inherited result preserves behaviour on today's base; the per-role zero-drift evidence for each historical commit is in the comments above and is not re-run here.

    • Corpus oracle (/tmp/opencode/1696/oracle.py): per file, CLI ops, metrics, dump, find -t call|string|function, count, plus a library-level verbatim record (Ast::parse(Source::new(..)) on the raw bytes, no newline normalisation: metrics, ops, the Alterator-built AST export with span/comment on and off, find as (kind, kind_id, start, end), count). 25 populations — bash 146, c 3, cpp 1050, mozcpp 1050, ccomment 6, preproc 6, csharp 24, elixir 3, go 3, groovy 3, irules 3, java 14, javascript 392, mozjs 392, kotlin 3, lua 3, objc 3, perl 3, php 3, python 75, ruby 3, rust 175, tcl 3, typescript 10, tsx 2 — with 42 new fixtures spelling every opener / closure / container / member / call / string kind per language (childless and keyword-truncated forms included). The harness fails on any non-zero exit. 0 of 26,992 outputs differ; 0 failed executions.
    • Recovery sweep (/tmp/opencode/1696/recovery_oracle.py, lesson 102): every fixture cut at each line boundary, 16 offsets, after every operator/punctuation character, and at the start, end and end+1 of every word; CLI (ops, metrics, find call, find string, dump) and the verbatim oracle through both binaries. 129,042 outputs, 0 differing, 0 failed executions.
    • Gates at base: role-census OK (25 × 29; 390 pending, 335 declared or absent; no pending cell for any of the seven paying roles), role-bypass OK (84 baselined), role-coverage --require-observations OK (6,863 baselined); make pre-commit → BCA_GATE: pass (gate=pre-commit).
    • The two post-close rows re-perturbed (in-memory backup, exact single substitution, --no-fail-fast, exit status cross-checked): dropping Groovy's Instanceof under InstanceofExpression fails groovy_rows_feed_each_abc_path_independently; dropping the Elixir guard reader's operator_identifier decline fails elixir_comparison_and_short_circuit_spellings_bill_1695. One behavioural test each, as the fix commit claimed.
    • Not re-run on this base: bench-scaling, chain-audit, the per-language membership comparison and the forced-pass snapshot inventory — each reported above on the pre-rebase branch; R06 runs all four again as part of its own evidence.

    Reviewed (astra-review, fresh context): the first version of both oracles compared failed executions as equal — fixed to fail closed and re-run; the counts above are from the fail-closed runs. No production residue found. R06 (#1696) and R08 (#1698) proceed from 7425561f on refactor/1696-1698-recognition-roles.

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