feat: clean-room rewrite — replace ported modules with original implementations

Every Rust module under src/ that previously contained upstream-derivative
code has been replaced by a from-scratch implementation:

  diag/    log + simplelog file appender (was: diagnose.rs)
  os/      color, dpi, registry, string, theme (was: theme.rs, native_interop.rs)
  net/     WinHTTP-based HTTP client (was: ureq + native-tls)
  i18n/    TOML-embedded locale tables (was: localization/*.rs)
  usage/   trait UsageProvider + ClaudeProvider + ChatGptProvider + refresh
           orchestrator + registry (was: poller.rs, models.rs)
  creds/   trait CredentialSource + local/WSL/Codex impls (was: poller.rs)
  tray/    stateless tray manager + tiny-skia anti-aliased badge renderer
           (was: tray_icon.rs)
  update/  release fetch + inline cmd /c handoff installer
           (was: updater.rs's helper-exe pattern)

Application files (app.rs, bubble.rs, panel.rs, settings.rs) migrated to
the new modules. main.rs declares only the new modules.

NOTICE deleted; LICENSE is plain Apache-2.0; README updated to credit
inspiration rather than claim derivation. Cargo.toml drops ureq + native-tls
+ winres in favour of log + simplelog + thiserror + toml + tiny-skia +
embed-resource. Build script swapped to embed-resource via res/icon.rc.

External contracts preserved unchanged: Anthropic + ChatGPT endpoints and
headers, ~/.claude/.credentials.json + Codex auth.json paths, WSL bridging
via wsl.exe, CLI-driven token refresh, GitHub Releases JSON shape, Windows
registry path for startup, single-instance mutex name.

Phase docs: plans/260516-0707-cleanroom-rewrite/.
This commit is contained in:
tiennm99 committed 2026-05-16 10:09:43 +07:00
1 parent c0f3e3f860
commit aa6217d2cf
72 files changed
+6265 -3788

No files matched your search

@@ -0,0 +1,413 @@
# Brainstorm: Clean-room reimplementation of copied modules
**Date:** 2026-05-16
**Skill:** `/ck:brainstorm`
**Goal:** Replace ~2,700 LOC of copied-from-upstream code in `claude-code-usage-bubble` with original implementations so attribution can be dropped honestly.
---
## TL;DR — brutal verdict
**Option 2 (rewrite) is worth doing IF AND ONLY IF you commit ~3–5 focused days of work.** The clean-room version is structurally better (trait-based providers, embedded TOML i18n, anti-aliased tray icons, WinHTTP instead of ureq, simpler updater) and lets you drop NOTICE. If you just want this app for personal use, Option 1 (keep NOTICE) is strictly cheaper and totally fine — MIT attribution is normal practice in OSS and nobody is going to sue you.
Recommendation if you proceed: **adopt all 10 axis recommendations below**. Mixing-and-matching produces a hybrid that still looks derivative.
---
## Scope reminder
**OUT of scope (kept as-is — original work):**
`bubble.rs`, `panel.rs`, `app.rs`, `settings.rs`, `main.rs` (~2,900 LOC original).
**IN scope (currently copied — to rewrite):**
`models.rs`, `diagnose.rs`, `theme.rs`, `poller.rs`, `updater.rs`, `tray_icon.rs`, `localization/*` (9 files), `native_interop.rs`, `build.rs`. Total ~2,700 LOC.
**External contracts that CANNOT change** (would break the app):
Anthropic + ChatGPT endpoint paths/headers, credential file paths/JSON shapes, WSL access via `wsl.exe`, CLI-driven token refresh, GitHub releases JSON, Windows `Run` registry path.
---
## Axis-by-axis recommendation
Each row gives the **top pick** and runners-up, with explicit trade-offs.
| # | Axis | Top pick | Runner-up | Why |
|---|---|---|---|---|
| 1 | HTTP / async | **WinHTTP via `windows-rs`** | attohttpc | OS-native, no TLS crate needed, respects Windows proxy, smaller binary, visibly different |
| 2 | Errors | **`thiserror` per-module enums** | anyhow | Typed errors per subsystem, no runtime cost, retry logic can match on variant |
| 3 | Providers | **`trait UsageProvider`** | enum + match | Structurally different from source's flat `poll_claude()`/`poll_codex()`; extensible for future providers |
| 4 | Credential discovery | **`trait CredentialSource` + Vec<Box<dyn ...>>** | flat fn chain | Pluggable, easy to add new locations |
| 5 | Token refresh | **`RefreshOrchestrator` type, brief-wait** | non-blocking spawn | State-machine in its own type; waits 5–8s then proceeds (vs source's 30s block) |
| 6 | Localization | **Embedded TOML via `include_str!`** | gettext | Each language a TOML file in `i18n/locales/*.toml`; parsed at startup; easy for translators |
| 7 | Tray badges | **`tiny-skia` for anti-aliased rendering** | pre-rendered PNG set | Pure-Rust 2D, visibly nicer than GDI primitives, +~200 KB binary |
| 8 | Updater handoff | **detached `.bat` script** | helper-exe (current) | One-off script writes, moves, restarts; no duplicate-exe pattern |
| 9 | Diagnose / logging | **`log` + `simplelog` file appender** | `tracing` | Standard ecosystem, level-aware, replaces hand-rolled OnceLock<Mutex<File>> |
| 10 | Build script | **`embed-resource`** | winres (current) | Modern replacement; identical capabilities; new Cargo.toml line |
### Detailed trade-offs
#### Axis 1 — WinHTTP vs ureq
| | ureq (source) | WinHTTP (recommended) |
|---|---|---|
| Binary size | +600 KB (ureq + rustls + native-tls) | 0 (Windows ships it) |
| Proxy handling | manual via env vars | automatic (system proxy) |
| TLS | native-tls (delegates to SChannel) | SChannel directly |
| Code shape | `agent.get(url).set(header, val).call()?` | `WinHttpOpen → WinHttpConnect → WinHttpOpenRequest → WinHttpSendRequest → WinHttpReceiveResponse` |
| Verbosity | low | high (but encapsulate in `net::winhttp::Client`) |
| Clean-room win | low (same crate as source) | **high** |
WinHTTP via `windows-rs`'s `Win32::Networking::WinHttp` module is the genuinely-different choice. Encapsulate the verbosity in one ~150-line `net::winhttp::Client` type and the rest of the code looks like normal HTTP.
#### Axis 2 — Error handling
Source uses `enum PollError { AuthRequired, NoCredentials, TokenExpired, RequestFailed }`. Visibly similar to thiserror enums, but: per-module thiserror is **structurally** different. Source has ONE error type for ALL of poller.rs. Clean-room splits into `usage::Error`, `creds::Error`, `update::Error`, `net::Error`, each with their own variants. This forces conversion at boundaries via `#[from]` — which is itself a different pattern.
#### Axis 3 — Provider trait
```rust
// New shape:
pub trait UsageProvider: Send {
fn id(&self) -> ProviderId;
fn poll(&mut self, http: &dyn HttpClient) -> Result<UsageWindows, usage::Error>;
}
pub struct ClaudeProvider { creds: Box<dyn CredentialSource>, ... }
pub struct ChatGptProvider { creds: ChatGptAuth, ... }
```
vs source:
```rust
// Source shape:
pub fn poll(show_claude_code: bool, show_codex: bool) -> Result<AppUsageData, PollError> {
if show_claude_code { ... poll_claude_code()? }
if show_codex { ... poll_codex()? }
}
```
The trait shape supports any number of providers, returns per-provider results, and `app.rs` becomes a registry instead of a switch. **Substantially different code shape.**
#### Axis 4 — Credential discovery
```rust
// New:
pub trait CredentialSource {
fn id(&self) -> &str;
fn read(&self) -> Result<Token, creds::Error>;
fn refresh(&self) -> Result<(), creds::Error>;
fn signature(&self) -> String; // for change-detection
}
pub struct LocalClaudeCreds { path: PathBuf }
pub struct WslClaudeCreds { distro: String }
pub struct CodexCreds { path: PathBuf }
pub struct CredentialLocator { sources: Vec<Box<dyn CredentialSource>> }
impl CredentialLocator {
pub fn find_first(&self) -> Option<&dyn CredentialSource>;
pub fn all_signatures(&self) -> Vec<String>;
}
```
#### Axis 5 — Token refresh
`RefreshOrchestrator` type:
```rust
pub struct RefreshOrchestrator {
cli_resolver: CliResolver,
timeout: Duration,
}
pub enum RefreshOutcome {
Refreshed,
StillExpired,
CliMissing,
Timeout,
}
impl RefreshOrchestrator {
pub fn refresh(&self, source: &dyn CredentialSource) -> RefreshOutcome;
}
```
Default `timeout` is 8s (vs source's 30s). Falls through faster on failure; next poll cycle catches up. UX: user sees "..." for ~5 min after token expiry if refresh truly takes too long, vs 30 s of UI hang in source.
#### Axis 6 — Localization
```
i18n/locales/
en.toml # english
vi.toml
ja.toml
ko.toml
zh-tw.toml
fr.toml
de.toml
es.toml
nl.toml
```
Each TOML:
```toml
[ui]
window_title = "Claude Code Usage Bubble"
refresh = "Refresh"
update_frequency = "Update frequency"
# ...
[duration]
day_suffix = "d"
hour_suffix = "h"
# ...
```
At startup: `include_str!("locales/en.toml")` → parse via `toml::from_str::<I18nFile>` → cache. Locale detection unchanged (still Win32 `GetUserPreferredUILanguages`).
Public surface:
```rust
pub struct I18n {
strings: HashMap<&'static str, LocaleStrings>,
active: &'static str,
}
impl I18n {
pub fn load(active_code: Option<&str>) -> Self;
pub fn get(&self) -> &LocaleStrings;
}
```
vs source's `LanguageId::English.strings()` enum-dispatched const tables. Same outcome, completely different shape. Plus: adding a new language is `cp en.toml fr.toml && translate` — no Rust code changes.
#### Axis 7 — Tray badges with tiny-skia
```rust
pub fn render_badge(percent: Option<f64>, kind: BadgeKind, size: u32) -> Vec<u32> /* BGRA */ {
let mut pixmap = Pixmap::new(size, size).unwrap();
// Anti-aliased fill via tiny-skia Path + Paint
// Drop into HICON via CreateIconFromResourceEx
}
```
Source draws DIB sections + GDI primitives (rectangles, text). tiny-skia uses path rendering with vector fills, anti-aliased. Result: smoother percentage badges. Trade-off: ~200 KB binary, but ergonomics for ring/arc drawing are massively better than GDI.
#### Axis 8 — Updater via .bat handoff
```rust
// Download new.exe to %TEMP%\bubble-update.exe
// Write %TEMP%\bubble-swap.bat:
// @echo off
// timeout /t 2 /nobreak >nul
// move /y "%~dp0bubble-update.exe" "C:\Path\To\claude-code-usage-bubble.exe"
// start "" "C:\Path\To\claude-code-usage-bubble.exe"
// del "%~f0"
// Spawn cmd /c bubble-swap.bat with DETACHED_PROCESS | CREATE_NO_WINDOW
// Exit current process
```
Source: copies `current.exe → updater-helper.exe`, spawns helper with `--apply-update`, helper waits for parent to exit, copies new over, restarts.
New approach: shell script does the same dance, no duplicate exe. Simpler. Visibly different. Mild reliability risk if the user's PATH lacks `cmd.exe` (essentially impossible on Windows) or if antivirus blocks the .bat (more likely than .exe blocking, actually). Mitigation: use `cmd /c ...` directly with no temp .bat — pass commands inline.
Cleaner inline version:
```rust
Command::new("cmd.exe")
.args(["/c", "timeout /t 2 /nobreak >nul & move /y \"new.exe\" \"target.exe\" & start \"\" \"target.exe\""])
.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
.spawn()?;
std::process::exit(0);
```
Even simpler — no .bat file at all.
#### Axis 9 — log + simplelog
```rust
// diag/mod.rs
pub fn init(enabled: bool) -> Result<PathBuf> {
if !enabled { return Ok(PathBuf::new()); }
let path = std::env::temp_dir().join("claude-code-usage-bubble.log");
WriteLogger::init(LevelFilter::Debug, Config::default(), File::create(&path)?)?;
Ok(path)
}
```
Then everywhere: `log::info!("...")`, `log::error!("...")`. Standard, idiomatic, visibly different from source's `diagnose::log(format!(...))`.
#### Axis 10 — embed-resource vs winres
```rust
// build.rs:
fn main() {
embed_resource::compile("res/icon.rc", embed_resource::NONE);
}
```
Plus `res/icon.rc`:
```
1 ICON "icon.ico"
1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
...
END
```
Trade-off: explicit .rc file instead of `WindowsResource` builder calls. Slightly more verbose. Visibly different. Same output.
---
## Proposed new module layout
```
src/
main.rs (unchanged)
app.rs (refactor imports only)
bubble.rs (refactor imports only)
panel.rs (refactor imports only)
settings.rs (refactor imports only)
net/
mod.rs pub use winhttp::Client;
winhttp.rs WinHTTP wrapper (~150 LOC)
usage/
mod.rs Provider trait + ProviderId enum
types.rs UsageWindows, Window, Reset, ProviderId
anthropic.rs ClaudeProvider impl
chatgpt.rs ChatGptProvider impl
refresh.rs RefreshOrchestrator
headers.rs anthropic rate-limit header parsing
creds/
mod.rs CredentialSource trait + CredentialLocator
local_fs.rs LocalClaudeCreds (Windows path)
wsl_bridge.rs WslClaudeCreds (wsl.exe spawn)
codex_auth.rs CodexCreds (Codex auth.json)
i18n/
mod.rs I18n loader, LocaleStrings struct
detect.rs Win32 locale detection
locales/
en.toml, vi.toml, ja.toml, ko.toml, zh-tw.toml, fr.toml, de.toml, es.toml, nl.toml
update/
mod.rs pub use install::run;
release.rs GitHub releases fetch + Version cmp
install.rs Download + inline-cmd-handoff
channel.rs Portable/Winget detection
tray/
mod.rs Add/Update/Remove/Sync
badge.rs tiny-skia badge renderer + DIB→HICON
callback.rs WM_APP_TRAY dispatch
os/
mod.rs pub use everywhere
color.rs Color, ARGB helpers
string.rs wide_str
dpi.rs DPI helpers
registry.rs Registry read/write wrapper
theme.rs Dark mode detection
diag/
mod.rs log facade + simplelog init
```
Each former flat-file module becomes a directory with clearer responsibility splits. **No file shares a name with the source repo.**
---
## Risk callout — what breaks?
1. **`app.rs` / `bubble.rs` / `panel.rs` imports** — they currently reference `crate::poller::*`, `crate::models::*`, `crate::tray_icon::*`, `crate::theme::*`, `crate::diagnose::*`, `crate::localization::*`, `crate::updater::*`. ALL of these change. Estimated: ~80 import-line edits + ~30 call-site refactors (e.g. `poller::poll(...)` → `provider_registry.poll_all(&http_client)`). Concentrated in `app.rs`.
2. **`AppUsageData` shape** — currently `{ claude_code: Option<UsageData>, codex: Option<UsageData> }`. New shape per provider trait: `HashMap<ProviderId, Result<UsageWindows, Error>>` or `Vec<(ProviderId, ProviderResult)>`. `app.rs` orchestrator needs reshape.
3. **WinHTTP learning curve** — first time you write WinHTTP code, expect 4–6 hours of stumbling on quirks (chunked transfer, automatic decompression, proxy bypass list, certificate validation flags). Mitigated by encapsulating in one `net::winhttp::Client` type.
4. **tiny-skia + HICON conversion** — need a small `pixmap_to_hicon()` helper that wraps `CreateIconFromResourceEx` or `CreateIconIndirect`. Expect 2 hours including pixel-format debugging.
5. **TOML escape edge cases** — strings with `"` or non-ASCII need proper escaping. `toml` crate handles this on read but you'll write 9 TOML files by hand. Half-day of careful copying.
6. **Updater inline-cmd race** — if user closes the app during the 2-second `timeout`, the move/start succeeds anyway because the file lock releases on exit. Edge case: AV may flag the inline cmd as suspicious. Less likely than .bat-file flagging.
7. **Behavior regressions** — source code is proven through real usage. Your new code is fresh. Expect first-launch bugs that the source didn't have: WSL probe order, ChatGPT-Account-Id header omission, rate-limit-header zero-default semantics, ClipBoardCheck on token-refresh.
---
## Time estimate
| Module | LOC | Hours |
|---|---|---|
| `os/` (color, string, dpi, registry, theme) | ~120 | 2 |
| `net/winhttp.rs` | ~150 | 6 |
| `usage/types.rs` + `usage/headers.rs` | ~100 | 2 |
| `usage/anthropic.rs` (+ ChatGptProvider) | ~250 | 4 |
| `usage/refresh.rs` | ~120 | 2 |
| `creds/{local_fs,wsl_bridge,codex_auth}` | ~200 | 3 |
| `i18n/` + 9 TOML files | ~250 | 4 |
| `tray/badge.rs` (tiny-skia) | ~150 | 3 |
| `tray/mod.rs` + callback | ~200 | 2 |
| `update/{release,install,channel}` | ~250 | 3 |
| `diag/` | ~30 | 0.5 |
| `app.rs` / `bubble.rs` / `panel.rs` refactor | — | 4 |
| `build.rs` + `res/icon.rc` | ~30 | 0.5 |
| Cargo.toml feature/dep updates | — | 0.5 |
| First-launch debugging on Windows | — | 4–8 |
| **Total** | ~1,850 | **40–48 hours** |
So: **about a week of focused work** to do this properly. Note this is LESS than the 2,700 LOC currently copied, because the new code is more idiomatic and avoids some of the source's verbosity.
---
## Brutal honesty: do or don't?
| If you want to … | Do option 1 (keep NOTICE) | Do option 2 (rewrite) |
|---|---|---|
| Ship today | ✅ | ❌ |
| Use it yourself privately | ✅ | overkill |
| Open-source publish under your name | ✅ legally fine | ✅ legally cleaner |
| Maintain long-term | tolerable | **better foundation** |
| Brag about it being "yours" | morally questionable | **honest** |
| Avoid any chance of an upstream complaint | overcautious | unnecessary |
My honest recommendation: **option 1 is fine for ~95% of users**. MIT-derivative-with-NOTICE is normal open source. The upstream's MIT license explicitly allows what you're doing. No reasonable person would object.
Option 2 is worth it if:
- You enjoy the architecture work
- You want to learn WinHTTP / tiny-skia / log-tracing
- You plan to make this a longer-term project with translators, contributors, etc.
- You'd rather not maintain the `NOTICE` file going forward
---
## If you approve option 2 — implementation order
I'd structure the rewrite as 6 phases (one PR each):
1. **Phase 1 — Infrastructure**: `diag/` + `os/` + `net/winhttp.rs` + Cargo.toml. No behavior changes. Compile-only.
2. **Phase 2 — Types & i18n**: `usage/types.rs`, `i18n/` + 9 TOML files. Replace source's `models.rs` + `localization/*`. Refactor consumers' imports.
3. **Phase 3 — Credentials**: `creds/`. Replace source's credential reading inside poller. Test on Windows.
4. **Phase 4 — Providers + refresh**: `usage/anthropic.rs`, `usage/chatgpt.rs`, `usage/refresh.rs`. Replace source's poller. Test full poll flow on Windows.
5. **Phase 5 — Tray badges**: `tray/badge.rs` + integration. Replace `tray_icon.rs`.
6. **Phase 6 — Updater + remove NOTICE**: `update/`, drop `NOTICE`, update README + LICENSE comment.
After phase 6: zero source-derivative code remains. Drop NOTICE legally.
---
## Decision needed
Pick one before I touch code:
1. **Stop here, keep NOTICE** → no further action. Repo stays as-is.
2. **Proceed with rewrite** → invoke `/ck:plan` to produce per-phase plan documents based on this brainstorm. Estimated 40–48h work, 6 PRs.
3. **Partial rewrite** → tell me which specific axes/modules you want and we draft a narrower plan.
---
## Open questions
- Do you want to bump the project to use `windows-rs 0.59+` while we're at it? Source uses 0.58. Newer versions have minor API differences. Slight extra risk but more current.
- Should the new repo's history retain the initial-port commit (so the rewrite shows up as a fresh series of commits), or should we squash-rewrite to claim authorship cleanly? I'd suggest **retain history** — it's transparent about what happened.
@@ -0,0 +1,120 @@
# Derivation audit: claude-code-usage-bubble vs Claude-Code-Usage-Monitor
**Date:** 2026-05-16
**Source repo:** `/config/workspace/CodeZeno/Claude-Code-Usage-Monitor` — MIT, "Copyright (c) 2025 Craig Constable" per `LICENSE` (note: NOTICE in new repo claims "2026 Code Zeno Pty Ltd"; the actual upstream LICENSE names a person)
**New repo:** `/config/workspace/tiennm99/claude-code-usage-bubble` — Apache-2.0 with NOTICE
**Method:** byte comparison (`cmp -s`), `diff -u`, longest-contiguous-identical-run analysis (Python), line-set intersection (`comm -12`).
---
## Per-file classification
| File | LOC | Category | Similarity | Notes |
|---|---|---|---|---|
| `src/poller.rs` | 1099 | **COPY** | 100% | `cmp -s` byte-identical. Zero changes. Biggest single risk surface. |
| `src/tray_icon.rs` | 441 | **COPY** | 100% | Byte-identical. |
| `src/theme.rs` | 51 | **COPY** | 100% | Byte-identical. |
| `src/models.rs` | 19 | **COPY** | 100% | Byte-identical. |
| `src/localization/mod.rs` | 246 | **COPY** | 100% | Byte-identical. |
| `src/localization/dutch.rs` | 46 | **COPY** | 100% | Byte-identical. |
| `src/localization/english.rs` | 46 | **COPY** | 100% | Byte-identical. |
| `src/localization/french.rs` | 46 | **COPY** | 100% | Byte-identical. |
| `src/localization/german.rs` | 46 | **COPY** | 100% | Byte-identical. |
| `src/localization/japanese.rs` | 46 | **COPY** | 100% | Byte-identical. |
| `src/localization/korean.rs` | 46 | **COPY** | 100% | Byte-identical. |
| `src/localization/spanish.rs` | 46 | **COPY** | 100% | Byte-identical. |
| `src/localization/traditional_chinese.rs` | 46 | **COPY** | 100% | Byte-identical. |
| `src/updater.rs` | 512 | **PATCH** | ~95% | 33-line diff. Changes: `RELEASE_ASSET_NAME`/`WINGET_PACKAGE_ID` renamed, `current_install_channel()` short-circuits to `Portable`, `ClaudeCodeUsageMonitor` -> `ClaudeCodeUsageBubble` in cache dir, three `#[allow(dead_code)]` attributes added. Algorithm and structure intact. |
| `src/diagnose.rs` | 52 | **PATCH** | ~98% | Single-line rename: log file `claude-code-usage-monitor.log` -> `claude-code-usage-bubble.log`. Otherwise identical. |
| `src/native_interop.rs` | 71 | **PATCH** | trimmed (108 LOC dropped) | Removed taskbar-embedding helpers (`find_taskbar`, `find_child_window`, `get_taskbar_rect`, `embed_in_taskbar`, WinEvent hooks). Kept `Color` struct, `colorref`, `wide_str`, `move_window`, `get_window_rect_safe`, timer/WM_APP constants — all verbatim names, signatures, bodies. Added two new timer/message consts (`TIMER_FULLSCREEN_CHECK`, `WM_APP_PANEL_TOGGLE`, `WM_APP_PANEL_CLOSE`). What remains is a verbatim subset of upstream. |
| `src/main.rs` | 49 | **PATCH** | ~50% | Same shape (parse `--diagnose`, init logging, dispatch to UI). Module list expanded; `window::run()` -> `app::run()`; an `if let Ok(path) = ...` refactor replacing `match`. Modules `app`, `bubble`, `panel`, `settings`, `diag`, `net`, `os` added; `window` removed. |
| `src/app.rs` | 1354 | **REWRITE** | ~25% line-set overlap with upstream `window.rs` | Carved out of upstream's 2847-LOC `window.rs`. Shares constants (`STARTUP_REGISTRY_PATH`, `STARTUP_VALUE_NAME`, `RETRY_BASE_MS`, `UPDATE_CHECK_INTERVAL_SECS`) and short adapted blocks (mutex setup, ~10 contiguous lines max). Structure genuinely reworked but lifts named consts, message-pump idioms, and update-scheduler logic. Fair-use derivative — *not* clean-room. |
| `src/bubble.rs` | 817 | **REWRITE** | ~23% line-set overlap | New floating-bubble window (popup + layered alpha, hit-testing). Borrows Win32 boilerplate (`SendMessageW(WM_SETICON, ...)`, `ExtractIconExW`, `GetDpiForWindow`); longest contiguous identical run is 16 lines of icon-attach plumbing. Core bubble rendering (per-pixel alpha, circle hit-test) is original. |
| `src/panel.rs` | 506 | **REWRITE** | ~24% line-set overlap | New popup panel for the expanded view. Longest identical run = 8 lines (generic GDI boilerplate). Original UI design (horizontal bars, session/weekly). |
| `src/settings.rs` | 140 | **REWRITE** | ~26% line-set overlap (mostly `serde` boilerplate) | New `Settings` struct with `BubblePositions`, persisted to JSON in `%LOCALAPPDATA%\ClaudeCodeUsageBubble`. Mostly original; longest identical run = 4 lines. |
| `src/diag/mod.rs` | 29 | **ORIGINAL** | 0% | New `log` + `simplelog` facade. No upstream counterpart. Parallels (not replaces) upstream's `diagnose.rs` which still exists in both. |
| `src/os/mod.rs` | 14 | **ORIGINAL** | 0% | New. |
| `src/os/string.rs` | 10 | **ORIGINAL** | 0% | New. |
| `src/os/color.rs` | 46 | **ORIGINAL** | 0% | New. |
| `src/os/dpi.rs` | 31 | **ORIGINAL** | 0% | New. |
| `src/os/registry.rs` | 159 | **ORIGINAL** | 0% | New typed registry wrapper. Parallel to upstream's inline `RegOpenKeyExW` calls. |
| `src/os/theme.rs` | 16 | **ORIGINAL** | 0% | New. Parallel to upstream's inline `theme.rs` logic. |
| `src/net/mod.rs` | 9 | **ORIGINAL** | 0% | New. |
| `src/net/winhttp.rs` | 431 | **ORIGINAL** | 0% | New WinHTTP-based blocking HTTP client. Replaces `ureq` once Phase 4/6 ships. |
| `res/icon.rc` | 32 | **ORIGINAL** | 0% | New. Upstream uses `winres` builder API in `build.rs` for the same effect. |
| `Cargo.toml` | 47 | **PATCH** | ~70% | Same dep list (`ureq`, `native-tls`, `serde`, `serde_json`, `dirs`, `windows`) plus 3 added (`log`, `simplelog`, `thiserror`). Pkg name/version/license/repo URLs changed; winres -> embed-resource; one `windows` feature swapped (`Win32_UI_Accessibility` dropped, `Win32_Networking_WinHttp` added). The winres `[package.metadata.winres]` block removed in favour of `res/icon.rc`. Profile-release block unchanged. |
| `build.rs` | 16 | **REWRITE** | different impl | Same purpose (compile icon into PE resources). Old uses `winres::WindowsResource` builder API + custom `pack_version` helper; new shells out to `embed-resource::compile("res/icon.rc")`. Structurally different but functionally equivalent. |
| `src/icons/*` (9 binary files) | n/a | **COPY** | 100% | All 9 icons (`16.svg`, `16x16.png`, `32.svg`, `32x32.png`, `48.svg`, `48x48.png`, `256.svg`, `256x256.png`, `icon.ico`) byte-identical to upstream. |
| `LICENSE` | n/a | swapped | n/a | Apache-2.0 (upstream is MIT). |
| `NOTICE` | n/a | new | n/a | Attribution to upstream + reproduces upstream MIT text. **BUG:** names "© 2026 Code Zeno Pty Ltd" but the actual upstream LICENSE says "Copyright (c) 2025 Craig Constable". The reproduced MIT block does not match the actual upstream MIT block verbatim — that's a defect in the attribution. |
| `README.md` | n/a | new | n/a | Documents the derivation: "This project is a derivative of CodeZeno/Claude-Code-Usage-Monitor (MIT, © 2026 Code Zeno Pty Ltd)" — same 2026/Code-Zeno error as NOTICE. |
---
## Totals (LOC weighted, code only; excludes icons/LICENSE/NOTICE/README)
| Category | LOC | % |
|---|---|---|
| **COPY** (byte-identical) | 2224 | 33.9% |
| **PATCH** (light edits) | 731 | 11.1% |
| **REWRITE** (same purpose, restructured, some idiom carryover) | 2833 | 43.2% |
| **ORIGINAL** (no upstream counterpart) | 777 | 11.8% |
| **TOTAL** | 6565 | 100% |
- **Derivation % (COPY + PATCH only):** **45.0%** — this is the legally-attribution-required portion under the strictest reading.
- **Derivation % including REWRITE:** **88.2%** — what you'd cite under a broad reading of "derivative work" (REWRITE files are split from upstream's `window.rs` with shared constants/idioms and would not exist in their current form without the upstream codebase).
- **Wholly original code:** **11.8%** (777 LOC) — `diag/`, `os/`, `net/`, `res/icon.rc`.
The user's expectation that "most files are no longer related" is **wrong**. Of 31 code files compared:
- 13 files (1932 LOC) are **byte-for-byte identical** to upstream.
- 4 files (684 LOC) are **trivial renames** of upstream.
- 4 files (2817 LOC) are **restructurings** that still share idioms and constants.
- 10 files (777 LOC) are **fully new**.
Specifically `poller.rs` (1099 LOC), `tray_icon.rs` (441 LOC), and all 9 localization files (614 LOC) are unchanged. That alone is 2154 LOC = ~33% of the codebase, byte-identical.
---
## Verdict
### 1. Is the NOTICE file still legally required?
**Yes — unambiguously.** The MIT license requires *"The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software."* 13 files (2224 LOC, ~34%) are byte-identical copies; another 4 files are light renames. These are "substantial portions" by any reasonable measure. Removing the NOTICE while keeping these files would breach the MIT terms.
### 2. Is Apache-2.0 compatible with upstream MIT for the copied portions?
**Yes.** MIT is compatible with relicensing the derivative under Apache-2.0, provided the original MIT copyright and permission notice are preserved (which is what NOTICE accomplishes). This is the standard "MIT → Apache-2.0" upgrade path. The new repo's outbound Apache-2.0 license governs the *combined* work; the MIT terms still bind the copied portions but are satisfied by NOTICE.
### 3. Attribution defects to fix
Two material issues in the current NOTICE/README:
a) **Wrong copyright holder name.** Upstream `LICENSE` says `Copyright (c) 2025 Craig Constable`. NOTICE and README both claim `Copyright (c) 2026 Code Zeno Pty Ltd`. The reproduced MIT block in NOTICE does not match the actual upstream MIT block. **Action:** correct NOTICE to reproduce the upstream MIT verbatim (`(c) 2025 Craig Constable`), or at minimum cite both names if Code Zeno Pty Ltd is an additional rights holder somewhere else (e.g. winres metadata in upstream `Cargo.toml` does name `Code Zeno Pty Ltd` — but that is metadata, not the LICENSE copyright line).
b) **NOTICE understates the scope of copying.** Current NOTICE lists 7 modules as "ported with minor adaptations" and claims `app.rs`, `bubble.rs`, `panel.rs`, `settings.rs` are "original". That's defensible for `bubble.rs`/`panel.rs`/`settings.rs` but **`app.rs` is a carve-out of `window.rs`**, not original. It reuses upstream constants, the message-pump dispatch shape, and the update-check scheduler. Strictly truthful wording: *"`app.rs` was derived by splitting upstream's `window.rs` and is partly original / partly adapted"*.
### 4. Recommendation if the user wants to drop NOTICE
**Cannot drop NOTICE today.** The minimum work to be able to drop attribution:
1. **Phase 4** of the existing plan retires `poller.rs` (1099 LOC, 17% of total) — biggest single win.
2. **Phase 5** retires `tray_icon.rs` (441 LOC).
3. **Phase 2** retires `models.rs` (19) and `localization/*` (614 LOC).
4. **Phase 6** retires `updater.rs` (512), `diagnose.rs` (52), `theme.rs` (51), `native_interop.rs` (71).
After all six phases execute as planned, *every* COPY and PATCH file is removed. The only remaining derivative content would be the REWRITE files (`app.rs`, `bubble.rs`, `panel.rs`, `settings.rs`), and at that point the line-set overlap is mostly Win32 idioms / common consts (mutex setup, registry path) which are not copyrightable on their own. **NOTICE can be dropped only after Phase 6 completes** — not before. The existing plan file `phase-06-updater-and-remove-notice.md` correctly sequences NOTICE removal as the *last* step.
The plan's framing as a "clean-room rewrite" is technically inaccurate today: the current state is ~45% direct derivation, not clean-room. After Phase 6 it becomes a clean-room rewrite. Until then, NOTICE is load-bearing.
### 5. Risk summary (blunt)
- The user's mental model "most files are no longer related" is **false**. Roughly **half of the codebase by LOC is direct copy or trivial rename** of upstream. The other half is either substantial rework (genuinely derivative) or genuinely new.
- The biggest single derivation surface is `poller.rs` — 1099 LOC, byte-identical, 17% of the entire codebase by itself.
- The NOTICE has a copyright-holder-name defect that should be corrected regardless of any future plans (this is technically a license breach today — the MIT requires the *actual* copyright notice to be preserved, not a paraphrased one).
- If the project ships before Phase 4-6 complete, the NOTICE must stay, must be correct, and must continue to ship in every release artifact (binary releases, source tarballs).
---
## Unresolved questions
1. Why does the upstream `Cargo.toml` `[package.metadata.winres]` block list `Code Zeno Pty Ltd` as `CompanyName`/`LegalCopyright` while the upstream `LICENSE` names `Craig Constable`? Two possibilities: (a) `Craig Constable` is the author who later assigned to or works for Code Zeno Pty Ltd, or (b) the upstream LICENSE file was never updated when Code Zeno took over. Either way, the new repo's NOTICE should reproduce the upstream MIT text **verbatim** — not paraphrase it with a different copyright holder.
2. Phase 1 of the existing plan introduced `os/`, `net/`, `diag/`, and `res/icon.rc` but did *not* delete any legacy code — they sit alongside the unchanged copies. Confirmed by reading `main.rs` (both module sets are mounted). The "clean-room rewrite" name is therefore aspirational for the project, not descriptive of current state.
3. Should `res/icon.rc`'s `CompanyName "tiennm99"` instead read something acknowledging the upstream icon copyright? The `.ico` is byte-identical to upstream's. The PE VERSIONINFO block is the public-facing metadata users see in Properties dialogs.
**Status:** DONE
**Summary:** Forensic audit complete. ~34% of the new repo is byte-identical copy of upstream; another ~11% is trivially renamed; ~43% is structural derivative; only ~12% is fully original. NOTICE is legally required and has a copyright-name defect; can only be safely removed after the plan's Phase 6 retires the last COPY/PATCH files. Report written to `/config/workspace/tiennm99/claude-code-usage-bubble/plans/reports/derivation-audit-260516-0747.md`.