Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions .planning/codebase/ARCHITECTURE.md

Large diffs are not rendered by default.

207 changes: 207 additions & 0 deletions .planning/codebase/CONCERNS.md

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions .planning/codebase/CONVENTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Coding Conventions

**Analysis Date:** 2026-05-07

## Language & Runtime Baseline

- **Pure PowerShell 7.5+ on .NET 9.** No PowerShell 5.1 fallbacks anywhere — `#requires -Version 7.5` at the top of script entry points.
- **No `Add-Type` shims that only compile under Windows PowerShell.** Where P/Invoke is needed, the `Add-Type @"…"@` block must compile under pwsh 7.5 on .NET 9. The `Core` region must remain free of `Add-Type` so it loads on Linux pwsh.
- **Single-file shape is a headline product feature.** All production code lives in `SnipIT.ps1`. Do not split into modules without an explicit board reversal (see `CLAUDE.md` "Repo layout").
- **Strict mode + Stop on error** in test harnesses (`Set-StrictMode -Version Latest`, `$ErrorActionPreference = 'Stop'`). Production script uses `$ErrorActionPreference = 'Stop'` selectively around side-effectful sections.

## Region Layout

`SnipIT.ps1` is organised by `#region`/`#endregion` markers. Order is load-bearing — the `Core` region must precede the `if ($CoreOnly) { return }` guard so headless tests can dot-source it.

| Region | Purpose | Cross-platform? |
|---|---|---|
| `Core` | Pure logic, no UI / no Win32 | Yes (Linux pwsh testable) |
| `Bootstrap` | Install paths, diag ring, log writers | Windows-leaning |
| `PInvoke` | `Add-Type` blocks for `user32.dll` / `gdi32.dll` / `kernel32.dll` | Windows only |
| `Capture` | Screen / window / region capture | Windows only |
| `Preview` | WPF Fluent preview window + annotation editor | Windows only |
| `Tray` | System tray icon + context menu | Windows only |
| `Main` | Single-instance mutex + main message loop | Windows only |

**Rule:** Any new pure helper (no UI, no Win32, no filesystem side-effects) goes in the `Core` region — `Test-SnipIT.ps1` picks it up automatically via `-CoreOnly`. Source: `SnipIT.ps1:16` (`#region Core`), `SnipIT.ps1:319-320` (`if ($CoreOnly) { return }`).

## Naming Patterns

**Functions:**
- `Verb-Noun` PascalCase using [PowerShell approved verbs](https://learn.microsoft.com/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands).
- Examples from `Core`: `Get-DragRectangle`, `Test-IsClickVsDrag`, `Get-LoupeSourceRect`, `Get-LoupePosition`, `Get-DefaultSnipFilename`, `Resolve-SaveImagePath`, `Test-CaptureRectValid`, `Get-CropBounds`, `Copy-AnnotationList`, `Test-IsSelfWindowHandle`, `Resolve-WindowCaptureTarget`, `Invoke-CaptureLoop`.
- `Test-*` for predicate / validation helpers; `Get-*` for value computation; `Invoke-*` for control-flow / side-effect orchestrators; `Resolve-*` for canonicalisation; `Copy-*` for deep-clone helpers.

**Files:**
- Production: `SnipIT.ps1` (PascalCase, single file).
- Tests: `Test-SnipIT.ps1`, `Test-SnipIT-Interactive.ps1` (verb-prefixed PascalCase, sibling to production).
- Scripts: lowercase-hyphenated under `scripts/` (e.g., `setup-git-signed.sh`).

**Variables:**
- Local / parameter: PascalCase (`$AnchorX`, `$VsWidth`, `$LoupeHeight`).
- Script-scope state: `$script:Pass`, `$script:Fail`, `$script:Failures`, `$script:Results`.
- Closure handles passed through `-TestAction`: PascalCase property bag (`$kit.LayoutScale`, `$kit.SetZoom`, `$kit.ZoomText`).

## Function Signatures

**Hard rule:** every function with > 1 parameter uses `[CmdletBinding()]` + `param()`. Mandatory parameters are explicit:

```powershell
function Resolve-SaveImagePath {
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string]$Path,
[Parameter(Mandatory)] [string]$BaseDirectory
)
...
}
```

Single-arg helpers may use a bare `param([type]$Name)` (e.g., `Get-DefaultSnipFilename` at `SnipIT.ps1:91`, `Get-ImageFormatNameFromPath` at `SnipIT.ps1:96`).

Use typed parameters everywhere. `[AllowNull()]` / `[AllowEmptyCollection()]` are used deliberately when nulls are valid input (see `Copy-AnnotationList` at `SnipIT.ps1:221`).

## Named-Closure Pattern (WPF event wiring)

Preview-window mouse / keyboard handlers are **named closures captured at window-creation time**, not bodies attached directly to WPF events.

**Why:** `Test-SnipIT-Interactive.ps1` needs to drive every code path without simulating real OS input. The integration harness invokes the closures directly via the `$kit` handle bag returned through `-TestAction`.

**Pattern:**

```powershell
# Inside Show-PreviewWindow / Build-PreviewWindow:
$beginPan = { param($e) ... } # named closure
$pickColor = { param($e) ... }
$handleMouseDown = { param($s, $e) ... }

# WPF event handlers are one-line wrappers:
$canvas.Add_MouseDown({ & $handleMouseDown @args })

# Test-mode hook exposes them through $kit:
if ($TestAction) {
$kit = @{
SetZoom = $setZoom
ZoomBy = $zoomBy
BeginPan = $beginPan
...
}
& $TestAction $kit
}
```

Sources: `SnipIT.ps1:1013-1018` (`Show-PreviewWindow` `[scriptblock]$TestAction` parameter), `SnipIT.ps1:2185-2224` (`$TestAction` invocation site), `Test-SnipIT-Interactive.ps1:78-122` (driving `$kit.SetZoom`, `$kit.ZoomBy`, `$kit.LayoutScale`, `$kit.ZoomText`).

**Rule:** Any new preview-window event handler ships as a named closure first, with the WPF `Add_*` registration as a one-line wrapper. Add the closure to the `$kit` bag if it covers a code path tests must exercise.

## Test-Mode Hooks

**`SNIPIT_TEST_MODE=1` environment variable** short-circuits all global side effects in `SnipIT.ps1`:

- Skips the single-instance mutex (`SnipIT.ps1:381`).
- Skips tray icon registration.
- Skips global hotkey registration.
- Skips the main `Dispatcher.Run()` loop (`SnipIT.ps1:2437`: `if ($env:SNIPIT_TEST_MODE) { return }`).

This lets `Test-SnipIT-Interactive.ps1` dot-source the full script (no `-CoreOnly`) on Windows and exercise WPF code without the app actually launching.

**`-CoreOnly` switch parameter** on `SnipIT.ps1` itself (`SnipIT.ps1:14`: `param([switch]$CoreOnly)`) is the Linux-pwsh path. After the `Core` region loads, the script early-returns at line 320 (`if ($CoreOnly) { return }`) so no Windows-only `Add-Type` blocks ever execute.

**Rule:** Any new global side-effect (timer registration, file watcher, P/Invoke setup, network call) must be guarded by an `if (-not $env:SNIPIT_TEST_MODE)` check. Any new pure helper that does not need Windows must be placed above the `-CoreOnly` early-return so the headless suite picks it up.

## Code Style

**Formatting:**
- 4-space indent, no tabs.
- K&R-style braces (`function Foo {` on the same line; closing brace on its own line).
- One blank line between top-level functions; no blank lines inside `param()` blocks.

**Comment-based help:** present at the top of `SnipIT.ps1` and on most `Core` functions. Inline comments explain non-obvious branches (DPI math, virtual-screen origin handling, loupe edge-clamping) — see `Get-LoupeSourceRect` and `Resolve-WindowCaptureTarget`.

**Linting:** `PSScriptAnalyzer` Error-severity is the merge gate (`.github/workflows/security.yml` `psscriptanalyzer` job, lines 73-102). Warning-severity findings are surfaced but non-blocking. **Fix the code, do not lower the gate.**

## Error Handling

- `try { … } catch { … }` around any P/Invoke call, file I/O, or WPF dispatcher work.
- Errors surface to the user via the diag ring + `last-error.txt` in the install home (see `Write-SnipDiag` at `SnipIT.ps1:332`). The diag ring is bounded — no unbounded log buffer.
- The capture loop **owns** disposal of `System.Drawing.Bitmap` (RAN-14 contract): the preview disposes on close, the loop creates a fresh bitmap each iteration via the `CaptureFactory` closure. Do not pre-allocate, do not double-dispose.

## Imports & P/Invoke Surface

- `Add-Type @"…"@` blocks live exclusively in the `PInvoke` region (`SnipIT.ps1:411-472`).
- `using namespace` directives are not used — fully-qualified type names instead (e.g., `[System.Drawing.Bitmap]`, `[System.Windows.Window]`).
- Every imported native API is reviewed for input-handle validation per `shared/runbooks/engineering-standards.md` §5.2 — never pass user-controlled HWNDs without owner-check (see `Test-IsSelfWindowHandle` and `Resolve-WindowCaptureTarget`).

## File / Path Handling

- User-supplied save paths go through `Resolve-Path` + canonical-form check before write (`Resolve-SaveImagePath` at `SnipIT.ps1:105`).
- Image format is derived from extension via `Get-ImageFormatNameFromPath`.
- The install flow is the only on-disk side-effect outside the user's chosen save path; install paths are computed centrally in `Get-InstallPaths` (`SnipIT.ps1:146`).

## Comments

- Use comments to record **invariants and contracts**, not to narrate the code.
- The RAN-14 capture-loop ownership contract, the RAN-15 SnipIT-window exclusion, and the test-mode short-circuit invariants are all flagged inline. New invariants get the same treatment.
- TODO / FIXME comments are not used in production code. Open a Paperclip ticket (`RAN-XX`) and link it in the PR.

## Module Design

- No exported module — `SnipIT.ps1` is dot-sourced by tests and run directly by users.
- The function set inside `Core` is the de-facto public API for the headless suite. Adding / renaming / removing a `Core` function is a tested change: update both `Test-SnipIT.ps1` and any caller in the script.

## Git & Signing

- All commits on `main` are **signed** (SSH key registered as both auth + signing on GitHub).
- Run `scripts/setup-git-signed.sh` once per fresh worktree to apply local git config; the script honours your existing global signing setup (ssh / openpgp / x509).
- Branch protection blocks force-push and direct-push to `main`. Squash-merge is the only allowed merge style.
- Conventional-commit subjects (`feat:`, `fix:`, `refactor:`, `chore(RAN-XX):`, `docs:`). Link Paperclip tickets as `[RAN-XX](/RAN/issues/RAN-XX)` in PR descriptions.

---

*Convention analysis: 2026-05-07*
158 changes: 158 additions & 0 deletions .planning/codebase/INTEGRATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# External Integrations

**Analysis Date:** 2026-05-07

snipIT is an offline desktop application with no runtime network calls, no API keys, and no telemetry. "Integrations" here means **OS-level surfaces the script binds to at runtime** plus **GitHub-side surfaces** consumed by CI and the security/best-practices posture.

## OS-Level Runtime Integrations (Windows 11)

### Win32 P/Invoke surfaces (`SnipIT.ps1:411-471`, `Native` static class)

| Surface | DLL | Used for | Functions |
|---|---|---|---|
| Hotkey registration | `user32.dll` | Three global hotkeys on a hidden message-only form | `RegisterHotKey`, `UnregisterHotKey` (`SnipIT.ps1:2528-2534`, cleanup `2616-2618`) |
| Window discovery / smart capture | `user32.dll` | Hover-to-highlight uses cursor → top-level window resolution | `WindowFromPoint`, `GetAncestor` (root), `GetForegroundWindow`, `GetWindowRect`, `GetCursorPos` |
| SnipIT chrome hide-during-capture | `user32.dll` | Hide own widget/preview/tray windows so they don't bake into the snip (RAN-15 fix) | `IsWindowVisible`, `ShowWindow` (`SW_HIDE`, `SW_SHOWNA` to restore without stealing focus) |
| DWM extended frame bounds | `dwmapi.dll` | Capture true window rect minus drop shadow | `DwmGetWindowAttribute` with `DWMWA_EXTENDED_FRAME_BOUNDS` (`SnipIT.ps1:925-927, 2305-2306`) |
| Mica backdrop / dark mode | `dwmapi.dll` | Win11 Fluent preview chrome | `DwmSetWindowAttribute` with `DWMWA_SYSTEMBACKDROP_TYPE` = `DWMSBT_MAINWINDOW` (Mica) and `DWMWA_USE_IMMERSIVE_DARK_MODE` |
| Per-monitor DPI v2 | `user32.dll` | Accurate capture at 100/125/150/200% across mixed-DPI monitors | `SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)` (`SnipIT.ps1:470`) |
| GDI handle cleanup | `gdi32.dll` | Free `HBITMAP` returned by `Bitmap.GetHbitmap()` | `DeleteObject` |
| Console hide | `kernel32.dll`, `user32.dll` | Hide stray console window when launched from non-pwsh shell | `GetConsoleWindow`, `ShowWindow` (`SnipIT.ps1:358-359`) |

### WPF surfaces

- **`System.Windows.Markup.XamlReader.Load`** parses inline XAML strings to construct: smart-overlay window (`SnipIT.ps1:775`), capture overlay (`856`), preview window (`1338`), floating widget (`2387`).
- **`System.Windows.Clipboard`** - `SetImage` writes the flattened (annotations-baked-in) snip (`SnipIT.ps1:2148`); `SetText` is a fallback messaging path (`1362`).
- **Preview-window architecture** uses named closures (`$beginPan`, `$pickColor`, `$handleMouseDown`, etc.) captured at window creation. Tests drive every code path through `Show-PreviewWindow -TestAction { ... }` (`SnipIT.ps1:1018, 2185-2224`).

### System tray + hotkey form (`SnipIT.ps1:2433-2596`)

- `System.Windows.Forms.NotifyIcon` (`2539`) with a `ContextMenuStrip` (`2547`) — full menu (capture modes, open snips folder, about, uninstall, exit).
- Hidden `NativeWindow` subclass (compiled inline at `SnipIT.ps1:2491` via `Add-Type -TypeDefinition $nativeWindowSrc`) receives `WM_HOTKEY` messages.
- Three global hotkeys: `Ctrl+Shift+S` (smart), `Ctrl+Shift+F` (full), `Ctrl+Shift+W` (active window).
- `System.Windows.Forms.Application.Run()` is the main message pump (`SnipIT.ps1:2601`).

### Single-instance mutex

- `System.Threading.Mutex` named `Local\SnipIT-SingleInstance` (or similar per session), created at `SnipIT.ps1:383`.
- A second launch shows a friendly notification and exits — *unless* `SNIPIT_TEST_MODE=1` is set, in which case the mutex is skipped entirely (`SnipIT.ps1:381`).
- Released and disposed in the cleanup region (`SnipIT.ps1:2621-2623`).

### File-system / shell integrations (first-run install, `SnipIT.ps1:547-611`)

- `WScript.Shell` COM object (`557`) creates two `.lnk` shortcuts:
- Desktop shortcut → `~/Desktop/SnipIT.lnk`
- Auto-start shortcut → `shell:startup` so the app launches at login
- Self-copies to `snipIT-Home/SnipIT.ps1` next to the source script.
- Generates `SnipIT.ico` on the fly via the Icon-generation region (`SnipIT.ps1:474-545`).
- Default save path: `~/Pictures/Snips/snip-yyyyMMdd-HHmmss.{png|jpg|bmp}`.
- **No registry writes outside `HKCU`. No admin elevation. No UAC prompt.**

## Data Storage

**Databases:** None.

**File Storage:**
- Local filesystem only — `~/Pictures/Snips/` for saved snips, `snipIT-Home/` for the installed copy.

**Caching:** None. No persisted preferences in the current version (rebindable hotkeys / default folder / widget position are on the roadmap per `README.md:181-188`).

## Authentication & Identity

Not applicable — single-user desktop app, no remote services, no accounts.

## APIs & External Services

Not applicable. No SDK clients, no HTTP calls, no webhooks (incoming or outgoing).

## Monitoring & Observability

**Error Tracking:** None.

**Logs:** No log file. Errors surface as in-app notifications (tray balloon) or are swallowed by `try { ... } catch {}` blocks around best-effort cleanup operations.

**Telemetry:** None — explicitly out of scope per `~/.claude/rules/build.md` (no auto-updater, no telemetry by default).

## CI/CD & Deployment

**Hosting:** GitHub repository at <https://github.com/RandomCodeSpace/snipIT> (public, MIT-licensed, secret-scanning + push-protection enabled).

**Distribution:** No published binary. The script *is* the package — install path is `git clone` + run, or download `SnipIT.ps1` directly. Per CLAUDE.md "Known not-a-pass" note, this is why Scorecard `Packaging` does not pass and is by design.

**CI Pipeline (GitHub Actions, all actions SHA-pinned):**

| Workflow | File | Trigger | What it does |
|---|---|---|---|
| `test` | `.github/workflows/test.yml` | push to `main`, PR to `main`, manual | Headless `Test-SnipIT.ps1` on Linux + Windows; AST parse-only on Windows |
| `Security (OSS-CLI)` | `.github/workflows/security.yml` | push, PR, weekly Mon 04:21 UTC | Trivy + Semgrep + PSScriptAnalyzer + Gitleaks + jscpd + SBOM (six independent jobs, fail-fast off) |
| `Scorecard supply-chain security` | `.github/workflows/scorecard.yml` | push to `main`, weekly Mon 06:00 UTC, manual | OpenSSF Scorecard analysis, SARIF → Security tab + public dashboard |

**Hardening:**
- Top-level `permissions: read-all` in every workflow per Scorecard `Token-Permissions`.
- `step-security/harden-runner` with `egress-policy: audit` in the Scorecard workflow (`.github/workflows/scorecard.yml:33-37`).
- `actions/checkout` and every other action pinned by commit SHA — Dependabot opens routine bumps.
- `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"` env in `test.yml` to opt into Node 24 ahead of the June 2026 Node 20 deprecation.

## GitHub-Side Surfaces

### Dependabot (`.github/dependabot.yml`)

- Single ecosystem: `github-actions`, weekly Mondays 08:00 UTC.
- Grouped — all action bumps land in one PR (`groups.actions.patterns: ["*"]`).
- `open-pull-requests-limit: 5`. Labels: `type:dependencies`, `area:ci`. Commit prefix `chore(actions)`.
- Repo-level **Dependabot security updates** enabled separately via `gh api` (per `.github/dependabot.yml:12-13`).
- No npm / pip / Maven manifests today, so `github-actions` is the only ecosystem.

### OpenSSF Scorecard

- Workflow: `.github/workflows/scorecard.yml`
- Engine: `ossf/scorecard-action@v2.4.3` (SHA `4eaacf0543bb3f2c246792bd56e8cdeffafb205a`)
- Output: SARIF → GitHub Security tab + <https://securityscorecards.dev/viewer/?uri=github.com/RandomCodeSpace/snipIT>
- Posture: best-effort, non-blocking. `passing` Best Practices is the only OpenSSF gate that blocks merge (per `CLAUDE.md`).
- Stretch target: ≥ 8.0 / 10.

### OpenSSF Best Practices

- Project ID: **12647**
- Public page: <https://www.bestpractices.dev/en/projects/12647>
- README badge: `https://www.bestpractices.dev/projects/12647/badge`
- In-repo self-assessment: `.bestpractices.json` (target `passing`)
- **Hard merge gate:** any PR that would fail a passing-level criterion is blocked at review.

### Branch-protection / signed commits

- `main` requires signed commits (SSH key registered as both auth + signing key).
- One-shot setup: `scripts/setup-git-signed.sh` (run once per worktree).
- Scorecard `Branch-Protection`, `Code-Review` (TechLead via Codex), `Signed-Releases` (`tag.gpgsign=true`) all configured per CLAUDE.md.

### Issue tracker

- **Paperclip** (not GitHub Issues for active work). Issue prefix `RAN` (e.g., RAN-54, RAN-15, RAN-66).
- Cross-project parent issue for OpenSSF rollout: [RAN-50](/RAN/issues/RAN-50).
- Paperclip Project ID for snipIT: `e6a01833-e7df-4068-849c-6a7c5154b70c`.
- Linking convention in PR descriptions and code comments: `[RAN-XX](/RAN/issues/RAN-XX)`.

## Environment Configuration

**Required env vars (runtime):** None for normal operation.

**Optional env vars:**
- `SNIPIT_TEST_MODE=1` — disables mutex, tray, hotkeys, and the main message-loop so harnesses can dot-source the script.

**Required env vars (CI):**
- `GITHUB_TOKEN` (auto-provided) — used by `gh release download` in the Gitleaks job (`.github/workflows/security.yml:111`).
- `GITLEAKS_VERSION: 8.30.1` (workflow-scoped) — pins the Gitleaks CLI release.

**Secrets location:** No secrets in repo. Secret scanning + push protection on. Gitleaks scans full git history on every PR.

## Webhooks & Callbacks

**Incoming:** None.

**Outgoing:** None.

GitHub repo webhook surface is empty — Scorecard `Webhooks` check passes by absence (per CLAUDE.md "Configured-for-pass checks").

---

*Integration audit: 2026-05-07*
Loading
Loading