Skip to content

fix: wire advocacy phases into every language config#40

Merged
samtuckerdavis merged 1 commit into
mainfrom
fix/advocacy-scan-persistence-2026-05-14
May 13, 2026
Merged

fix: wire advocacy phases into every language config#40
samtuckerdavis merged 1 commit into
mainfrom
fix/advocacy-scan-persistence-2026-05-14

Conversation

@samtuckerdavis
Copy link
Copy Markdown

@samtuckerdavis samtuckerdavis commented May 13, 2026

Summary

desloppify scan was silently skipping all 3 advocacy detectors (advocacy_language, advocacy_security, advocacy_tool_presence) on 9 of 11 languages. Only javascript and go had the wiring. python, typescript, csharp, cxx, dart, gdscript, rust, and every generic-only language never ran advocacy phases — defeating the fork's core differentiator.

How it surfaced

E2E test on the merged sync (#39): scan a Python file with "kill two birds with one stone" + "whitelist"

  • desloppify status showed the Advocacy language dimension exists with "10 checks" (so the detector was registered in the catalog)
  • but desloppify show advocacy_language returned 0 issues
  • direct call to detect_advocacy_language() on the same file found 4 instances with full suggestion/alternative data

So detector code was fine — the bug was upstream of detection: the phase wasn't being scheduled.

Root cause

Each LangConfig.phases list is built manually per language. The two languages that had advocacy wiring did it inconsistently:

  • javascript: appended after generic_lang() returned (cfg.phases.append(detector_phase_advocacy_language()))
  • go: inlined in the phases list

Every other language config (and every generic_lang-built plugin) was just missing the phase entries. Nobody had a single source of truth for "all OP languages should run advocacy."

Fix

Move advocacy wiring into shared_subjective_duplicates_tail() — the helper that every LangConfig.phases list (and the generic_lang builder) already calls. One canonical location.

New helper shared_advocacy_phases() returns the 3 phases; the tail prepends them before Subjective review. Removed the now-duplicate manual wiring in javascript and go.

Also added the 3 advocacy labels to SHARED_PHASE_LABELS in capabilities.py so capability_report stops bucketing them under "linting" in the desloppify langs output.

Verification

E2E rerun post-fix on the same Python test file:

[15/20] Advocacy language...
         4 instances → 4 issues
[16/20] Advocacy security...
         0 instances → 0 issues
[17/20] Advocacy tools...
         0 instances → 0 issues

And desloppify show advocacy_language now lists:

  • T2 [high] "kill two birds with one stone" → use "accomplish two things at once" instead (idioms)
  • T2 [high] "whitelist/blacklist" → use "allowlist/denylist" instead (terminology)

Regression coverage

New test_advocacy_phase_coverage.py parametrizes over all 8 dedicated LangConfigs + asserts on generic_lang. Each must contain all 3 advocacy phase labels. Would have caught the original bug — adding it forces future language-config additions to go through shared_subjective_duplicates_tail() (or otherwise prove they include advocacy).

Updated test_phase_builders.py for the new tail shape (length 3 → 6, with advocacy phases at positions 0–2).

Test plan

  • CI green (lint, typecheck, arch-contracts, ci-contracts, tests-core, tests-full, package-smoke, CodeQL, Code Quality Gate)
  • CodeRabbit pass
  • Verify new regression test runs in CI

Summary by CodeRabbit

  • New Features

    • Standardized advocacy detection phases across all language plugins, ensuring consistent checks for language, security, and tools best practices.
  • Tests

    • Added comprehensive test coverage to verify all language configurations include advocacy detection phases.

`desloppify scan` was silently skipping advocacy_language, advocacy_security,
and advocacy_tool_presence detectors on 9 of 11 languages — only javascript
and go had manual wiring. Confirmed via e2e: scan a Python file containing
"kill two birds with one stone" + "whitelist" → status shows the Advocacy
language dimension exists with "10 checks" but 0 findings; direct call to
detect_advocacy_language() on the same file finds 4 instances.

Root cause: each LangConfig.phases list is built manually. javascript
appended advocacy phases via `cfg.phases.append(...)` after generic_lang
returned; go inlined them in its phases list. python, typescript, csharp,
cxx, dart, gdscript, rust, and every generic-only language never had them.

Fix: move advocacy wiring into the shared tail (shared_subjective_duplicates_tail)
that every language config already calls. One canonical location, one
shared helper (shared_advocacy_phases), no per-language churn when phases
evolve. Removed the now-duplicate manual wiring in javascript and go.

Also: added the 3 advocacy phase labels to SHARED_PHASE_LABELS in
capabilities.py so capability_report stops bucketing them under "linting"
in the langs command output.

Verified post-fix: scan on the same Python test file now prints
`[15/20] Advocacy language... 4 instances → 4 issues` and
`show advocacy_language` returns the persisted findings with full
suggestion/alternative data. All existing tests pass (after updating
test_phase_builders.py assertions for the new tail shape).

Regression test added: test_advocacy_phase_coverage.py asserts every
dedicated LangConfig AND generic_lang include all 3 advocacy phases.
Would have caught the original bug.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0aa49e8-4cdd-4652-a253-0552d25a8a79

📥 Commits

Reviewing files that changed from the base of the PR and between 8ef608d and aeb4e49.

📒 Files selected for processing (6)
  • desloppify/languages/_framework/base/phase_builders.py
  • desloppify/languages/_framework/generic_support/capabilities.py
  • desloppify/languages/go/__init__.py
  • desloppify/languages/javascript/__init__.py
  • desloppify/tests/lang/common/test_advocacy_phase_coverage.py
  • desloppify/tests/lang/common/test_phase_builders.py
💤 Files with no reviewable changes (1)
  • desloppify/languages/go/init.py

📝 Walkthrough

Walkthrough

This PR consolidates advocacy detector phase wiring by introducing a shared shared_advocacy_phases() helper and updating shared_subjective_duplicates_tail() to include advocacy phases by default. Per-language advocacy phase imports and appends are removed from Go and JavaScript, and test coverage is added to verify all language configurations receive advocacy phases.

Changes

Advocacy Phase Consolidation Campaign

Layer / File(s) Summary
Shared advocacy phase builder and tail ordering
desloppify/languages/_framework/base/phase_builders.py
New shared_advocacy_phases() function lazily imports and constructs the three advocacy detector phases. Updated shared_subjective_duplicates_tail() to prepend advocacy phases before subjective review, shifting the tail composition contract and exported the new helper via __all__.
Capability labels for shared advocacy phases
desloppify/languages/_framework/generic_support/capabilities.py
Three advocacy-related phase labels—"Advocacy language", "Advocacy security", "Advocacy tools"—added to exported SHARED_PHASE_LABELS constant for capability reporting.
Language-specific wiring cleanup
desloppify/languages/go/__init__.py, desloppify/languages/javascript/__init__.py
Removed per-language direct imports of advocacy phase detectors and cfg.phases.append() calls; advocacy phases now flow through the shared shared_subjective_duplicates_tail() wiring. Added comments in JavaScript module clarifying the source.
Phase composition and coverage validation
desloppify/tests/lang/common/test_phase_builders.py, desloppify/tests/lang/common/test_advocacy_phase_coverage.py
Updated default tail test to expect six phases (three advocacy + three existing). Added parametrized regression test that verifies all dedicated language configs and generic plugin include all advocacy labels. Adjusted slow-phase assertions to match new tail structure.

Sequence Diagram

Not applicable—this is a refactoring consolidation with no new feature control flow or multi-component interactions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

breaking-change

🚥 Pre-merge checks | ✅ 8
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately describes the main change: consolidating advocacy phase wiring across all language configurations via shared infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No Hardcoded Secrets Or Credentials ✅ Passed Search across all modified non-test files found no hardcoded secrets, API keys, tokens, passwords, or credentials. Only phase labels and detector module paths present.
No Speciesist Idioms ✅ Passed All modified files scanned for prohibited speciesist idioms. Zero violations found.
No Tier 3 Data Committed ✅ Passed No Tier 3 data found. Changes contain framework code, tests, and generic detectors—no activist identities, PII, legal strategy, operational security details, investigation records, or credentials.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/advocacy-scan-persistence-2026-05-14
  • 🛠️ fix NAV violations

Comment @coderabbitai help to get the list of available commands and usage tips.

@samtuckerdavis samtuckerdavis merged commit 73b2e22 into main May 13, 2026
12 checks passed
@samtuckerdavis samtuckerdavis deleted the fix/advocacy-scan-persistence-2026-05-14 branch May 13, 2026 22:25
samtuckerdavis added a commit that referenced this pull request May 13, 2026
After PR #40, running the full suite on Windows surfaced 34 failures that
weren't visible in CI (Linux). All were Windows-specific quirks falling
into five categories. Each was root-caused and fixed without changing
Linux behavior. Full suite now green on Windows Python 3.12:
  Before: 34 failed, 6796 passed
  After:  0 failed, 6830 passed, 6 skipped

## Path-separator normalization (production code, 7 files)

Several modules built prefix strings with `os.sep` ("\\" on Windows) and
then compared against POSIX-style paths emitted by other tools or test
fixtures. The mismatched separators caused silent miss-matches. Each fix
normalizes both sides to "/" for comparison while preserving native
separators in the output where production callers expect them.

  - `engine/detectors/coupling.py::_rel_to_root` — also strip drive letter
    when root resolved to `C:/...` but the caller still passes POSIX-rooted
    prefix strings (e.g. tests using `/project/...`).
  - `engine/detectors/coverage/mapping_analysis.py::_build_prod_by_module` —
    normalize prod_file before prefix-strip; on Windows the mixed-slash
    inputs were missing the project-root prefix and producing module names
    like `.repo.pkg.util` instead of `pkg.util`.
  - `engine/detectors/test_coverage/discovery.py::_normalize_graph_paths,
    _discover_scorable_and_tests` — same pattern; also kept native-
    separator output for callers that round-trip native paths
    (test_normalize_graph_paths_converts_absolute_to_relative).
  - `engine/policy/zones.py::_match_pattern` — normalize both pattern and
    rel_path to forward slashes. Without this, an absolute Windows path
    pattern (`C:\...\test_creds.py`) hit the "exact basename" branch and
    returned False, causing the security detector to scan files in TEST
    zones it should have skipped.
  - `languages/_framework/treesitter/imports/resolvers_backend.py` —
    `os.path.normpath` around every returned candidate (go/rust/java/
    kotlin/cxx/csharp/dart/scala/swift). Forward-slashed src-root strings
    like "src/main/java" combined with native scan_path produced mixed
    separators ("C:\...\src/main/java\com\acme\App.java") that didn't
    match the platform-native expected paths.
  - `languages/_framework/treesitter/imports/resolvers_scripts.py::
    resolve_js_import` — normpath each candidate before isfile check.
    The "/index.js" suffix on a Windows path produced mixed separators.
  - `languages/javascript/test_coverage.py::resolve_import_spec` —
    normalize candidate after `os.path.normpath` for set-membership check
    against POSIX-keyed production files.

## Test path/separator fixes (3 tests)

  - `test_state_path_from_explicit_state_arg` — `Path.as_posix()` for
    cross-platform string equality.
  - `test_external_adapters.py::TestCollectExcludeDirs::test_includes_*,
    test_deduplicates` — use `os.path.basename` instead of rsplit("/")
    which split on the wrong separator on Windows.
  - `test_writes_to_file` (holistic_review remediation) — explicit
    `encoding="utf-8"` on the read; locale default (cp1252 on Windows)
    mangled the middle-dot unicode the writer emits as UTF-8.

## Skip Unix-only test path

  - `test_timeout_kills_jscpd_process_group` — `pytest.mark.skipif` on
    `hasattr(os, 'getpgid')`. The kill-by-process-group path is POSIX
    only; on Windows the timeout handling uses TerminateProcess, which
    has its own test.

## chmod assertion gated by platform

  - `test_orchestrator_pipeline_writes_exact_cli_helper` — skip the
    `st_mode & 0o111` exec-bit assert on Windows. Windows uses ACLs,
    not POSIX mode bits; the helper is invoked via bash/PYTHONPATH and
    doesn't need the exec bit set on Windows.

## cmd /c wrapping (5 tests, 1 cross-cutting fixture)

  - On Windows when an executable (acli, codex) isn't on PATH,
    `_resolve_executable` returns `["cmd", "/c", name]`, then
    `_wrap_cmd_c` collapses everything after `/c` into a single string
    for cmd.exe's tokenizer. Correct production behavior — but it
    obscures the per-arg list shape several tests assert on.
  - For 3 rovodev tests + 1 codex_batch test: per-test monkeypatch of
    `_resolve_executable` to return `[name]` directly.
  - For 12 `TestCmdReviewPrepare::test_do_run_batches_*` tests: added a
    class-level autouse fixture that does the same. Plus added
    `**kwargs` to all 7 `fake_subprocess_run` signatures + `fake_run` in
    `test_run_codex_batch_writes_live_status_before_completion`. The
    runner adds `input=stdin_text` to `subprocess.run` kwargs whenever
    `_prompt_via_stdin(prompt)` is true — which is always on Windows
    (`return sys.platform == "win32" or len(prompt) > _PROMPT_ARG_MAX_CHARS`).
    Without **kwargs, the fakes raised TypeError, hit
    `_run_via_subprocess`'s except branch, and returned exit_code=1.

None of these touch Linux behavior. CI on Linux 3.11 was already green;
this brings Windows 3.12 to parity.

Co-authored-by: Original Gary <276612211+OpenGaryBot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants