Skip to content

Commit 8c47409

Browse files
authored
Merge branch 'main' into fix/media-controls-false-positives
2 parents 3e45626 + 69c6254 commit 8c47409

File tree

12 files changed

+361
-18
lines changed

12 files changed

+361
-18
lines changed

CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@ test(ego-lint): add fixture for deprecated ByteArray usage
138138
- **Obfuscation regex pitfalls**: `[a-z]\d+` catches unicode escapes (`\u2013`); `re.IGNORECASE` on guard patterns catches `console.debug` when matching `DEBUG`
139139
- **Git history rewritten 2026-02-27**: `git filter-repo` removed internal files. All SHAs before that date are invalid
140140

141+
## Repository Workflow
142+
143+
- **Squash merge only** — merge commits and rebase merges are disabled. Every PR becomes a single commit on main with the PR title (conventional commit) and PR body as the commit message
144+
- **Required status check**: `test` must pass before merging (enforced by `main-protection` ruleset). Admin can bypass for hotfixes
145+
- **Auto-merge**: Enabled — can be activated per-PR to merge automatically when CI passes
146+
- **Branch protection**: `main` is protected against deletion and force push via repository ruleset (not classic branch protection)
147+
- **Labels**: `false-positive`, `new-rule`, `severity-change`, `ego-lint`, `ego-review` (plus GitHub defaults)
148+
149+
## Development Workflow
150+
151+
- **Issue first**: Create a GitHub issue describing the problem or improvement before starting work. Label with `false-positive`, `new-rule`, or `severity-change` as appropriate
152+
- **Branch per issue**: Create a branch from `main` named `fix/<short-description>`, `feat/<short-description>`, or `docs/<short-description>`
153+
- **One concern per PR**: Each PR should address a single logical concern (one fix, one feature, one doc update). Split multi-concern work into separate PRs
154+
- **PR closes issue**: Include `Closes #N` in the PR description to auto-close the issue on merge
155+
- **Tests before PR**: Run `bash tests/run-tests.sh` and verify all assertions pass before pushing
156+
141157
## Releasing
142158

143159
release-please automates versioning, CHANGELOG updates, git tags, and GitHub Releases:

docs/internal/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ Run the full ego-submit pipeline against real extensions to surface false positi
1414
| [Blur my Shell](field-test-blur-my-shell.md) | 2026-03-01 | 49 | 7,743 | 21→7 | 7 | 14 | GObject.registerClass, src/ layout |
1515
| [GSConnect](field-test-gsconnect.md) | 2026-03-02 | 65 | 24,680 | 19→13 | 10 | 6 | Service daemon context, JSDoc noise |
1616
| [V-Shell](field-test-v-shell.md) | 2026-03-02 | 28 | 19,201 | 23→3 | 3 | 0 | Multi-version compat guards; all initial FPs resolved |
17-
| [Dash to Panel](field-test-dash-to-panel.md) | 2026-03-03 | 17 | 16,583 | 19→16 | 10 | 9 | Comment FPs, version-compat guards |
17+
| [Dash to Panel](field-test-dash-to-panel.md) | 2026-03-03 | 17 | 16,583 | 19→16† | 10 | 9 | Comment FPs, version-compat guards |
18+
| [Tiling Shell](field-test-tiling-shell.md) | 2026-03-03 | 64 | 11,371 | 21→4 | 4 | 0 | Compiled TS, keybindings, constructors-from-enable |
19+
| [Media Controls](field-test-media-controls.md) | 2026-03-04 | 17 | ~5,000 | 246W/3F→31W/6F | 6 | 0 | MPRIS D-Bus, onDestroy, src/ layout, provenance |
20+
21+
† Tiling Shell session #29 fixes (constructor-only-in-extension.js) resolve 4 of Dash to Panel's init/shell-modification FP FAILs; re-run pending.
1822

1923
### Cumulative Calibration Lessons
2024

@@ -43,7 +47,15 @@ Patterns discovered across field tests — encode here so future rules avoid rep
4347
*WARN noise:*
4448

4549
12. **License header URLs are the largest single source of false R-SEC-03 WARNs**: GPL boilerplate with `gnu.org/licenses` URLs triggered the external-URL rule. Now suppressed by guard pattern.
46-
13. **JSDoc documentation creates massive R-SLOP-01/02 WARN noise on mature projects**: GSConnect (222 WARNs) and Dash to Panel both showed that real JSDoc on mature code triggers AI slop heuristics. Provenance scoring (`quality/code-provenance >= 4`) now gates R-SLOP-01/02 post-filter.
50+
13. **JSDoc documentation creates massive R-SLOP-01/02 WARN noise on mature projects**: GSConnect (222 WARNs) and Dash to Panel both showed that real JSDoc on mature code triggers AI slop heuristics. Provenance scoring (`quality/code-provenance >= 3`) now gates R-SLOP-01/02 post-filter.
51+
52+
*Compiled TypeScript patterns:*
53+
54+
14. **Helper file constructors called from enable() are not init-time violations**: Tiling Shell had 13 FP FAILs from Shell globals in class constructors that are only ever instantiated from `enable()`. Constructor checks are now restricted to `extension.js` only — matching the existing `GOBJECT_CONSTRUCTORS` guard.
55+
15. **Arrow function definitions at module scope are lazy**: `const fn = () => Main.layoutManager.monitors` doesn't execute at module load time. The function body is deferred until the first call.
56+
16. **Same-line ternary guards need `guard-window: 1`**: Version compat patterns like `api.exists ? old(args) : new()` have the guard and deprecated API on the same line. `guard-window: 1` (minimum valid value; the current line is always included in the lookback window) handles this.
57+
17. **Single long lines ≠ minification**: Compiled TypeScript may have one or two very long lines (keyboard constant chains, export lists) in otherwise readable files. The minified-js check now requires 3+ lines > 500 chars.
58+
18. **Compiled TypeScript (esbuild) generates systematic WARN noise**: `var` declarations from `__defProp`/`__publicField` helpers (R-DEPR-09), verbose parameter names (R-SLOP-38), and many small classes without `destroy()` methods (resource-tracking/no-destroy-method). These are build artifacts, not author issues.
4759

4860
## Other Artifacts
4961

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Field Test: Media Controls
2+
3+
**Extension**: Media Controls (`mediacontrols@cliffniff.github.com`)
4+
**Source**: https://github.com/cliffniff/media-controls
5+
**Category**: Audio/media control (MPRIS)
6+
**Downloads**: ~437,000
7+
**GNOME versions**: 46, 47, 48, 49
8+
**Codebase**: 17 JS files, 5,064 lines (hand-written JavaScript, no TypeScript compilation)
9+
**Layout**: `src/` directory (modern build-system layout)
10+
**License**: GPL-2.0-or-later
11+
12+
## Why This Extension
13+
14+
First audio/media extension tested. Exercises under-represented patterns:
15+
- D-Bus proxy lifecycle (MPRIS bus interaction, 3 proxies per player)
16+
- Signal-heavy architecture (41 manual .connect() calls)
17+
- `src/` directory layout (build-system pattern, metadata.json in src/)
18+
- Extensive TypeScript-compatible JSDoc typing strategy
19+
- Custom St.Widget subclasses with GObject.registerClass
20+
21+
## Architecture Overview
22+
23+
- **extension.js** (787 LOC): Main entry point, player discovery via D-Bus NameOwnerChanged, keybinding lifecycle
24+
- **PlayerProxy.js** (609 LOC): MPRIS D-Bus wrapper — 3 proxies per player (Mpris, MprisPlayer, Properties), change-listener Map, fallback polling
25+
- **PanelButton.js** (1,236 LOC): Panel button + popup menu UI, extends PanelMenu.Button, `onDestroy()` cleanup via destroy signal
26+
- **ScrollingLabel.js** (391 LOC): Animated scrolling text widget, extends St.ScrollView, `destroy()` override with timer cleanup
27+
- **MenuSlider.js** (278 LOC): Track position slider, extends St.BoxLayout, `onDestroy()` via destroy signal connection
28+
- **prefs.js** (393 LOC): Preferences with AppChooser, BlacklistedPlayers, ElementList, LabelList widgets
29+
- **types/dbus.js** (239 LOC): Full MPRIS D-Bus type definitions in JSDoc
30+
31+
Import segregation is clean: shell helpers in `helpers/shell/`, prefs helpers in `helpers/prefs/`, shared utils in `utils/`.
32+
33+
## Raw Results
34+
35+
```
36+
432 checks — 135 passed, 3 failed, 246 warnings, 48 skipped
37+
Exit code: 1
38+
```
39+
40+
### Results by Check Name
41+
42+
| Check | Count | Severity | Assessment |
43+
|-------|-------|----------|------------|
44+
| R-SLOP-02 (JSDoc @returns) | 103 | WARN | **FP** — intentional TypeScript-compatible JSDoc |
45+
| R-SLOP-01 (JSDoc @param) | 70 | WARN | **FP** — same |
46+
| resource-tracking/no-destroy-method | 45 | WARN | **FP**`onDestroy()` not recognized as cleanup |
47+
| R-SLOP-11 (GLib.source_remove) | 10 | WARN | **TP** — should use `GLib.Source.remove()` |
48+
| R-SLOP-17 (typeof method guard) | 3 | WARN | **Debatable** — defensive check for optional API methods |
49+
| lifecycle/prototype-override | 2 | WARN | **TP** — MprisSource.prototype._addPlayer not restored |
50+
| metadata/exists | 1 | FAIL | **FP** — metadata.json in `src/`, no fallback |
51+
| file-structure/metadata.json | 1 | FAIL | **FP** — same |
52+
| css/shell-class-override | 1 | FAIL | **TP**`.panel-button` overrides Shell theme class |
53+
| lifecycle/signal-balance | 1 | WARN | **Needs analysis** — 41 connects vs 10 disconnects |
54+
| lifecycle/async-destroyed-guard | 1 | WARN | **TP** |
55+
| lifecycle/soup-session-abort | 1 | WARN | **TP** — no abort() on disable |
56+
| async/no-cancellable | 1 | WARN | **TP** — async calls without Gio.Cancellable |
57+
| async/missing-cancellable | 1 | WARN | **TP**_async() without cancellable |
58+
| quality/constructor-resources | 1 | WARN | **TP** — .connect() in AppChooser constructor |
59+
| quality/comment-density | 1 | WARN | **TP advisory** — utils/common.js 45% comments |
60+
| quality/private-api | 1 | WARN | **TP** — Main.panel private API access |
61+
| quality/excessive-null-checks | 1 | WARN | **TP advisory** — 117 null checks, ratio 0.025 |
62+
| R-QUAL-33 (Gio._promisify) | 1 | WARN | **TP advisory** — 4 files with undocumented promisify |
63+
64+
**Total**: 218 FP (89%), 28 TP/debatable (11%)
65+
66+
## False Positive Analysis
67+
68+
### 1. R-SLOP-01/02 JSDoc Flood (173 WARNs)
69+
70+
**Root cause**: Provenance score 3, threshold >= 4 for suppression.
71+
72+
Media Controls uses intentional TypeScript-compatible JSDoc throughout all 17 files. The project includes `jsconfig.json`, `@girs/*` type packages, and a dedicated `types/dbus.js` with full MPRIS type definitions. This is clearly a deliberate typing strategy, not AI slop.
73+
74+
The provenance score of 3 means "strong hand-written indicators" (domain vocabulary: 12 hits, nontrivial algorithms: 40 hits, consistent naming). The threshold of >= 4 is too conservative for JSDoc suppression.
75+
76+
**Fix**: Lower provenance threshold to >= 3.
77+
78+
### 2. resource-tracking/no-destroy-method (45 WARNs)
79+
80+
**Root cause**: `build-resource-graph.py` recognizes `destroy()`, `disable()`, and `_destroy*()` as cleanup methods, but NOT `onDestroy()`.
81+
82+
Both MenuSlider.js and PanelButton.js use `this.connect("destroy", this.onDestroy.bind(this))` to register cleanup handlers. The `onDestroy()` method contains proper signal disconnection, timer removal, and null assignments. The resource graph doesn't recognize this pattern.
83+
84+
- MenuSlider.js: 8 resource warnings, has `onDestroy()` at line 250
85+
- PanelButton.js: 37 resource warnings, has `onDestroy()` at line 1186
86+
87+
**Fix**: Add `onDestroy()` recognition in build-resource-graph.py cleanup method detection.
88+
89+
### 3. metadata.json Missing (2 FAILs)
90+
91+
**Root cause**: ego-lint.sh has `src/` fallback for extension.js but NOT for metadata.json. check-metadata.py also lacks the fallback.
92+
93+
Media Controls uses a build-system layout with all source in `src/` — metadata.json lives at `src/metadata.json`, which is standard for extensions using gnome-extensions pack with a build step.
94+
95+
**Fix**: Add consistent `src/` fallback for metadata.json in ego-lint.sh and check-metadata.py.
96+
97+
## True Positives
98+
99+
### Lifecycle Issues
100+
- **prototype-override** (2): `MprisSource.prototype._addPlayer` is monkey-patched but the original is not saved/restored in `disable()`. Genuine concern for multi-extension compatibility.
101+
- **soup-session-abort** (1): Soup.Session created for album art fetching but no `.abort()` in cleanup path. Pending HTTP requests will continue after disable.
102+
- **async-destroyed-guard** (1): Multiple async/await chains without `_destroyed` guard. Extension could act on stale state after disable if async operations complete late.
103+
- **signal-balance** (1): 41 manual `.connect()` vs 10 `.disconnect()`. Needs deeper analysis — some may use `.connectObject()` auto-management, some may be on child widgets (auto-cleaned on destroy).
104+
105+
### Async Safety
106+
- **no-cancellable + missing-cancellable** (2): D-Bus proxy creation and file operations use async without `Gio.Cancellable`. These should be cancellable via disable() to prevent stale state.
107+
108+
### CSS
109+
- **shell-class-override** (1 FAIL): `.panel-button` in stylesheet.css overrides GNOME Shell's panel button styling for ALL panel buttons, not just this extension's. Also uses `.popup-menu-container` and `.popup-menu-box` (not flagged but same concern).
110+
111+
### Code Quality
112+
- **constructor-resources** (1): AppChooser.js:51 has `.connect()` in constructor. Since this is a prefs widget (not extension lifecycle), the impact is limited but still worth noting.
113+
- **GLib.source_remove** (10): Uses non-idiomatic `GLib.source_remove()` instead of `GLib.Source.remove()`. Functional but not correct GJS API.
114+
115+
## Calibration Lessons
116+
117+
1. **`onDestroy()` is a common cleanup pattern** — build-resource-graph.py should recognize it alongside `destroy()`, `disable()`, and `_destroy*()`
118+
2. **Provenance threshold of 4 is too conservative for JSDoc suppression** — score 3 is sufficient to indicate hand-written code, especially when JSDoc is extensive and consistent
119+
3. **`src/` layout support remains inconsistent** — extension.js has fallback, metadata.json does not. Need systematic audit of all src/ fallbacks
120+
4. **TypeScript-compatible JSDoc is becoming more common** — popular extensions like Media Controls use it intentionally. The R-SLOP-01/02 rules need better calibration for this pattern
121+
5. **D-Bus proxy lifecycle is well-handled** — no false positives from D-Bus-specific checks. The MPRIS pattern (3 proxies per player, NameOwnerChanged tracking) exercises these checks well
122+
6. **CSS shell-class-override works correctly** — caught legitimate `.panel-button` override. However, it missed `.popup-menu-container` and `.popup-menu-box` (potential gap in the shell class list)
123+
124+
## Comparison with Prior Tests
125+
126+
| Metric | Media Controls | Avg of Prior 8 |
127+
|--------|---------------|----------------|
128+
| FP rate (pre-fix) | 89% | ~40-76% |
129+
| Dominant FP cause | JSDoc + onDestroy | Varied |
130+
| True positive rate | 11% | ~24-60% |
131+
| Security issues | 0 | 0-6 |
132+
| Code provenance | 3/5 | 2-4/5 |
133+
134+
The high FP rate is driven by two specific gaps. After fixes, the extension's true positive findings are meaningful and actionable.

0 commit comments

Comments
 (0)