diff --git a/.agents/.impeccable.md b/.agents/.impeccable.md new file mode 100644 index 0000000..139d38a --- /dev/null +++ b/.agents/.impeccable.md @@ -0,0 +1,29 @@ +## Project Context + +### What imgstry is +A TypeScript image-processing engine. Pixel-level operations (filters, kernels, +splines, curves, color-space conversions) over `Uint8ClampedArray` buffers. +Runs in browser (canvas / offscreen / worker) and Node platforms. Distributed +as a library for downstream UIs (see `imgstry-ui`). + +### Design Goals +- **Pure pixel math** — operations are deterministic, side-effect free, and + test-friendly. Same input buffer + same op = same output bytes. +- **In-place mutation contract** — pipeline ops mutate the source buffer + intentionally to avoid copy churn. Callers clone upstream if they need + immutability. Never break this contract silently. +- **Platform-thin** — `core/` knows nothing about DOM, workers, or fs. All + environment binding lives under `platform/browser` and `platform/node`. +- **Fast & honest benchmarks** — `bench/` measures real pipelines, not + micro-ops. Treat regressions as bugs. + +### Non-goals +- No GPU/WebGL backend. CPU typed arrays only. +- No image format parsing (PNG/JPEG codecs); inputs are decoded buffers. +- No bundled UI. UI lives in `imgstry-ui`. + +### Architecture in one breath +`core/processor` orchestrates the pipeline; `core/operation` defines per-op +contracts; `kernel/` holds matrix kernels + collections; `pixel/` exposes +pixel-level primitives; `platform/*` adapters wire the engine to a host +runtime; `utils/` is leaf math. diff --git a/.agents/rules/code-principles.md b/.agents/rules/code-principles.md new file mode 100644 index 0000000..a4dfeaf --- /dev/null +++ b/.agents/rules/code-principles.md @@ -0,0 +1,83 @@ +--- +trigger: glob +globs: '**' +description: 'Functional programming, immutability, simplicity, and function design principles.' +applyTo: '**' +--- + +# Code Principles + +## Functional Programming + +- Prefer pure functions: same input -> same output. +- Side effects live at the boundaries (platform adapters, IO, the pipeline + runner). Math and color logic stays pure. +- **Exception: the pipeline mutates pixel buffers by contract** — this is the + hot path. Document any mutation outside that contract with a one-line comment + explaining why. + +## Immutability + +- `const` over `let` whenever possible. +- Use `map` / `filter` / `reduce` instead of mutating loops, **except** in + per-pixel hot loops where a `for` loop over `Uint8ClampedArray` indices is + measurably faster and idiomatic. Bench before swapping. +- Use TypeScript `readonly`: `Readonly`, `ReadonlyArray`. + +## Early Exits + +Guard clauses over nested `if`s. + +```ts +function applyOp(op: Operation | null, buf: Uint8ClampedArray) { + if (!op) return buf; + if (op.disabled) return buf; + if (buf.length === 0) return buf; + return op.kernel(buf); +} +``` + +## Function Design + +- Single responsibility per function. +- 3+ parameters -> single object parameter. +- Pass dependencies as parameters (don't reach for module-level singletons + inside leaf functions). + +**Bad** + +```ts +function blur(buf, radius, sigma, channels) { /* ... */ } +``` + +**Good** + +```ts +function blur({ buffer, radius, sigma, channels }: BlurParams) { /* ... */ } +``` + +## Iteration Patterns + +- Use `map` / `filter` / `reduce` for collections of operations, points, + layers. +- Use index loops for per-pixel work; comment with the rationale only if + surprising (e.g. branchless variant chosen for SIMD-friendliness). + +## Type Safety + +- Strict TS; no `any`. If a foreign API leaks `any`, narrow at the boundary. +- No non-null assertions (`!`). Handle `null` / `undefined` explicitly. +- Prefer `array.at(i)` over `array[i]` when the index may be out of range. +- Buffer indexing inside known-bounds loops is fine. + +## Simplicity + +- Prefer the small obvious solution over the clever one. +- Three similar lines beat a premature abstraction. +- If a helper appears in 2+ places, consider lifting it to `utils/`. + +## Hot-path Discipline + +- Allocations in per-pixel loops are bugs. Pre-allocate LUTs and reuse them. +- Avoid object literals / closures inside per-pixel iteration. +- When in doubt, write the bench first. diff --git a/.agents/rules/core.md b/.agents/rules/core.md new file mode 100644 index 0000000..891fc99 --- /dev/null +++ b/.agents/rules/core.md @@ -0,0 +1,56 @@ +--- +trigger: glob +globs: 'source/core/**' +description: 'Patterns and conventions for source/core — operations, pipeline, processor, layers, splines.' +applyTo: 'source/core/**' +--- + +# Core Guidelines + +## Scope + +`source/core/` is the heart of the engine: the processor, the pipeline runner, +operation contracts, layers, points, splines, and core types. + +## Hard Rules + +- **No platform code.** No `document`, `window`, `OffscreenCanvas`, `Worker`, + `fs`. Anything that touches the host runtime belongs in + `source/platform/`. +- **No DOM-typed imports.** If a type comes from `lib.dom.d.ts`, it stops at + the platform boundary. +- **Pure where possible.** The pipeline runner is the only intentional + mutator; everything else stays referentially transparent. + +## Operations + +- Each operation exports a factory returning an `Operation` shape (name, + parameters, kernel/pixel function). +- Operations are data, not behavior bound to a class. Keep them serialisable + so the worker thread can replay them. +- New operations live next to their family (point ops in `point/`, curve ops + in `spline/`, etc.). + +## Pipeline + +- The runner consumes `Operation[]` and a buffer. It does not know what an + op does — only its contract. +- Do not branch on operation names inside the runner. Add capability flags on + the operation shape if a new behavior is needed. + +## Layers + +- Layers compose multiple pipelines. A layer must declare its blend mode and + must not assume buffer ownership beyond its own scope. + +## Splines & Points + +- Inputs to operations; never mutated post-construction. +- Provide derived data (LUTs, sampled arrays) via pure helpers in `utils/` or + inside `spline/` itself. + +## Worker Bridge + +- `imgstry.thread.ts` serialises `Operation[]` to a worker. +- New operations must round-trip cleanly. If an op carries a non-cloneable + field (functions, class instances), redesign it before merging. diff --git a/.agents/rules/implementation.md b/.agents/rules/implementation.md new file mode 100644 index 0000000..94f67d3 --- /dev/null +++ b/.agents/rules/implementation.md @@ -0,0 +1,61 @@ +--- +trigger: glob +globs: '**' +description: 'General implementation approach for imgstry source code.' +applyTo: '**' +--- + +# Implementation Guidelines + +## Before Writing Code + +- Search for an existing pattern first. This codebase has conventions. +- Check `core/`, `kernel/`, `pixel/`, `utils/` for a similar helper before + introducing a new one. +- Prefer linking to a real file as the example, not an abstract description. + +## Naming Conventions + +- **PascalCase**: types, interfaces, classes (rare). +- **camelCase**: functions, variables, methods, file names of helpers. +- **ALL_CAPS**: module-level constants representing fixed values (e.g. + `MAX_CHANNELS`). Not for ordinary `const`s. +- **dot-namespacing**: `imgstry.processor.ts`, `imgstry.operation.ts` is the + existing convention for core surfaces. Keep it. + +## TypeScript Standards + +- Strict mode on. +- No `any`. Narrow at boundaries; never propagate. +- Public types live next to or below the module they describe; truly shared + types go in `source/types/`. +- Use `readonly` on input arrays / structures that ops must not mutate + (curves, points, kernels). The pixel buffer is the exception. + +## Module Layout + +- One responsibility per file. If a file exceeds ~200 lines of logic without + obvious cohesion, split it. +- Re-export public surface through the nearest `index.ts`; do not import from + internal sub-paths across subsystems. + +## Tooling + +- Format: rely on the repo's existing formatter / ESLint config. Do not switch + formatters. +- Type-check before claiming done: `npm run check`. +- Tests: `npm run test:run` (single pass). Watch mode: `npm test`. + +## Performance Discipline + +- Bench before optimising. Anecdotes are not data. +- A "perf" commit must include a numeric delta in the message body or the + bench output linked from the PR. +- Do not regress benches without explicit justification (correctness fix, + feature addition with clear win, etc.). + +## Rule Frontmatter + +- YAML frontmatter values use **single quotes**. Do not change `'` to `"`. +- Keep `applyTo` populated; downstream tooling (Copilot, Antigravity, Claude) + reads it. diff --git a/.agents/rules/kernel.md b/.agents/rules/kernel.md new file mode 100644 index 0000000..bf84946 --- /dev/null +++ b/.agents/rules/kernel.md @@ -0,0 +1,37 @@ +--- +trigger: glob +globs: 'source/kernel/**' +description: 'Patterns for source/kernel — matrix kernels and typed collections.' +applyTo: 'source/kernel/**' +--- + +# Kernel Guidelines + +## Scope + +`source/kernel/` holds convolution kernels and typed collections used by +operations. + +## Kernels + +- Kernels are immutable, numeric, and named. +- Provide both the kernel data and the radius / normalization metadata; do not + recompute it at the call site. +- New kernels live in their own file under `kernel/` with a default-export + factory or a named constant — match the existing surface, do not invent a + new convention per file. + +## Collections + +- Typed collections wrap typed arrays with index-safe access helpers. +- Do not leak the underlying buffer outside the collection unless explicitly + exposed via a method named `buffer` / `raw`. +- Mutation methods must be named such that they clearly mutate (`set`, + `fill`, `swap`). Read-only methods return new values. + +## Performance + +- Kernels live in the hot path. Avoid allocations in their factories beyond + the one-time setup. +- Prefer flat `Float32Array` / `Int32Array` over nested arrays for + cache-friendly traversal. diff --git a/.agents/rules/platform.md b/.agents/rules/platform.md new file mode 100644 index 0000000..7d05b2e --- /dev/null +++ b/.agents/rules/platform.md @@ -0,0 +1,54 @@ +--- +trigger: glob +globs: 'source/platform/**' +description: 'Platform-specific bindings for browser and node — canvas, offscreen, worker, fs adapters.' +applyTo: 'source/platform/**' +--- + +# Platform Guidelines + +## Scope + +`source/platform/` adapts the platform-agnostic core to a host runtime. + +``` +platform/ + browser/ # canvas, offscreen, worker handle + node/ # node-specific bindings +``` + +## Hard Rules + +- **Core may not depend on platform.** Imports flow platform -> core, never + core -> platform. +- Every platform module owns its own resources. If you create it, you destroy + it (workers, canvases, file handles). +- Feature-detect before using a host API. Fall back gracefully (e.g. main + thread when `Worker` is missing). + +## Worker Thread + +- The worker handle is typed as `Worker` — keep it tight. +- Messages crossing the boundary are structured-cloneable. Pipelines arrive + as `Operation[]`; the worker replays them through the same processor used + on the main thread. +- Never embed closures in messages. + +## Canvas / Offscreen + +- Buffer extraction goes through a thin adapter so tests can substitute + `ImageData`-like fixtures. +- Do not reach into the DOM from inside an op — request the buffer at the + edge, apply the pipeline, write back. + +## Node Adapter + +- Node binding mirrors the browser surface where it makes sense + (`processor.from(buffer)`, etc.). Divergence in API shape is a code smell; + keep parity. + +## Tests + +- Browser platform tests live next to their target and run under jsdom (see + `test/unit/browser/`). +- Node platform tests run under the node environment. diff --git a/.agents/rules/project.md b/.agents/rules/project.md new file mode 100644 index 0000000..3376dc0 --- /dev/null +++ b/.agents/rules/project.md @@ -0,0 +1,82 @@ +--- +trigger: glob +globs: '**' +description: 'Core project overview, structure, architecture, and tooling for imgstry. Applies to all files.' +applyTo: '**' +--- + +# Project Guidelines + +## Tech Stack + +- TypeScript (strict mode), targeting both browser and Node. +- Vite for dev/build, Vitest for tests, ESLint for linting. +- `Uint8ClampedArray` is the canonical pixel buffer. +- Bench harness in `bench/`. + +## Project Structure + +``` +source/ + core/ + imgstry.editor.ts # high-level editor facade + imgstry.operation.ts # operation contracts + imgstry.processor.ts # pipeline orchestrator + imgstry.thread.ts # worker-thread bridge + layer/ # layer composition + pipeline/ # pipeline runner + point/ # point/curve types + spline/ # spline interpolation + types/ # shared core types + kernel/ # matrix kernels + typed collections + pixel/ # pixel-level primitives + platform/ + browser/ # canvas, offscreen, worker bindings + node/ # node-side bindings + types/ # public type exports + utils/ # pure math/color helpers + index.ts # public entry +test/ # vitest specs grouped by surface +bench/ # benchmark runners +declarations/ # generated .d.ts (do not edit) +``` + +## Architecture Patterns + +### `core/` is platform-agnostic +Core code references only typed arrays and pure math. No `document`, no +`window`, no `fs`. Environment-specific work goes through `platform/`. + +### Pipeline operates by mutation +Operations mutate the pixel buffer in place. The processor owns the buffer +lifecycle. Callers needing immutability clone upstream. + +### Operations declare their contract +Every op exports a typed factory + a `Operation` shape (name, args, kernel or +pixel function). Do not bypass the contract with ad-hoc closures. + +### Worker offloading is opt-in +`imgstry.thread.ts` routes a pipeline through a Web Worker when the host +supports it. The same `Operation[]` runs identically on main thread or worker. + +### Splines and points are value types +`spline/` and `point/` produce immutable curve descriptors. They are inputs to +operations, never mutated post-construction. + +## Commit Standards + +Follow [Conventional Commits](https://www.conventionalcommits.org/). + +- Types: `feat`, `fix`, `perf`, `refactor`, `test`, `chore`, `docs`, `build`, `ci`. +- Scope often matches subsystem: `feat(core):`, `perf(spline,color):`, + `fix(worker):`. +- Breaking changes use the `!` marker: + `perf(processor)!: rewrite batch around a typed-array LUT pipeline`. + +## Tooling + +- **Test**: `npm test` (Vitest watch) / `npm run test:run` (single run). +- **Lint**: `npm run lint`, autofix via `npm run lint:fix`. +- **Type-check**: `npm run check`. +- **Build**: `npm run build` (Vite library mode). +- **Bench**: see `bench/` runners; do not regress without justification. diff --git a/.agents/rules/testing.md b/.agents/rules/testing.md new file mode 100644 index 0000000..b2319fc --- /dev/null +++ b/.agents/rules/testing.md @@ -0,0 +1,60 @@ +--- +trigger: glob +globs: '**/*.{test,spec}.ts' +description: 'Testing philosophy, layout, and conventions for imgstry.' +applyTo: '**/*.{test,spec}.ts' +--- + +# Testing Guidelines + +## Framework + +Vitest in jsdom (for browser-platform tests) and Node environments. See +`vitest.config.ts`. + +## File Organization + +- Tests live under `test/`, grouped by surface (`test/unit/`, `test/color/`, + `test/utils/`, etc.). +- Resources / fixtures live in `test/resources/`. +- Browser-targeted tests live alongside their browser surface (e.g. + `test/unit/browser/imgstry.offscreen.test.ts`). + +## Running Tests + +```bash +npm test # watch mode +npm run test:run # single run +npm run coverage # coverage report +``` + +## Testing Philosophy + +- **Test pure functions first.** Pixel math, color conversions, splines, and + kernels are deterministic — assert exact outputs against known fixtures. +- **Behavior, not implementation.** Don't assert on internal call sequences. +- **Reference fixtures, not snapshots.** Image fixtures live in + `test/resources/`; compare buffers byte-by-byte (or with a small + channel-wise tolerance when documented). +- **Platform tests run on the platform they target.** Browser bindings under + jsdom; node bindings against the node runtime. + +## Pixel Comparison + +- Equality: byte-for-byte over `Uint8ClampedArray`. +- Tolerance comparisons require a justification comment naming the source of + the tolerance (rounding mode, color-space drift, etc.) and a numeric bound. + +## Conventions + +- Top-level `describe` names the unit under test, optionally with a kind + prefix: `describe('processor: pipeline', ...)`, + `describe('color: hsv -> rgb', ...)`. +- Nested `describe` groups scenarios. +- `it` titles read as sentences starting with `'should ...'`. + +## What NOT to Mock + +- Do not mock typed arrays. +- Do not mock pure math helpers — call them. +- Mock only platform boundaries (canvas, worker handle, fs). diff --git a/.agents/rules/utils.md b/.agents/rules/utils.md new file mode 100644 index 0000000..e6c5bc1 --- /dev/null +++ b/.agents/rules/utils.md @@ -0,0 +1,38 @@ +--- +trigger: glob +globs: 'source/utils/**' +description: 'Conventions for source/utils — pure math, color, and helper functions.' +applyTo: 'source/utils/**' +--- + +# Utils Guidelines + +## Scope + +`source/utils/` holds pure helpers: math, color-space conversions, clamping, +interpolation primitives. + +## Hard Rules + +- **Pure.** No side effects. No DOM. No mutation of inputs. +- One responsibility per file. Name the file after the export. +- No platform-specific imports. + +## Color Conversions + +- Input ranges are documented at the function level (0–255, 0–1, 0–360). +- Round-trip conversions (e.g. `rgb -> hsv -> rgb`) must converge within a + documented tolerance. + +## Clamping & Sampling + +- Clamp helpers return the clamped value; they never throw on out-of-range + input. +- Sampling helpers assume sorted inputs unless documented otherwise. + +## Imports + +- Utils may not import from `core/`, `platform/`, or `kernel/`. They are + leaves. +- Re-exports flow up through `index.ts` only when a helper is part of the + public surface. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..c72c6b7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,3 @@ +{ + "enabledPlugins": {} +} diff --git a/.github/instructions/code-principles.instructions.md b/.github/instructions/code-principles.instructions.md new file mode 120000 index 0000000..a3ccd25 --- /dev/null +++ b/.github/instructions/code-principles.instructions.md @@ -0,0 +1 @@ +../../.agents/rules/code-principles.md \ No newline at end of file diff --git a/.github/instructions/core.instructions.md b/.github/instructions/core.instructions.md new file mode 120000 index 0000000..a7a9e84 --- /dev/null +++ b/.github/instructions/core.instructions.md @@ -0,0 +1 @@ +../../.agents/rules/core.md \ No newline at end of file diff --git a/.github/instructions/implementation.instructions.md b/.github/instructions/implementation.instructions.md new file mode 120000 index 0000000..b16ace2 --- /dev/null +++ b/.github/instructions/implementation.instructions.md @@ -0,0 +1 @@ +../../.agents/rules/implementation.md \ No newline at end of file diff --git a/.github/instructions/kernel.instructions.md b/.github/instructions/kernel.instructions.md new file mode 120000 index 0000000..f78d099 --- /dev/null +++ b/.github/instructions/kernel.instructions.md @@ -0,0 +1 @@ +../../.agents/rules/kernel.md \ No newline at end of file diff --git a/.github/instructions/platform.instructions.md b/.github/instructions/platform.instructions.md new file mode 120000 index 0000000..e8117a6 --- /dev/null +++ b/.github/instructions/platform.instructions.md @@ -0,0 +1 @@ +../../.agents/rules/platform.md \ No newline at end of file diff --git a/.github/instructions/project.instructions.md b/.github/instructions/project.instructions.md new file mode 120000 index 0000000..5e1be41 --- /dev/null +++ b/.github/instructions/project.instructions.md @@ -0,0 +1 @@ +../../.agents/rules/project.md \ No newline at end of file diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md new file mode 120000 index 0000000..057b0a4 --- /dev/null +++ b/.github/instructions/testing.instructions.md @@ -0,0 +1 @@ +../../.agents/rules/testing.md \ No newline at end of file diff --git a/.github/instructions/utils.instructions.md b/.github/instructions/utils.instructions.md new file mode 120000 index 0000000..065c80e --- /dev/null +++ b/.github/instructions/utils.instructions.md @@ -0,0 +1 @@ +../../.agents/rules/utils.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..97834d4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Always-loaded core (small, project-wide) + +@.agents/rules/project.md + +@.agents/rules/code-principles.md + +@.agents/rules/implementation.md + +@.agents/rules/testing.md + +# Domain rules - load on demand + +Domain-specific rules are NOT auto-imported to keep baseline context small. +Read them when the work touches the matching area. CLAUDE.md routes the +mapping; the rule files live at `.agents/rules/`: + +- `core.md` - processor, pipeline, operations, layers, splines (source/core) +- `kernel.md` - convolution kernels and typed collections (source/kernel) +- `platform.md` - browser / node host bindings (source/platform) +- `utils.md` - pure math, color, helpers (source/utils) + +Read with the Read tool when the task enters the domain. Re-read after long +gaps if context was compacted. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2c22417 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,16 @@ +Before implementing anything, identify which domain you are working in and +read the corresponding rule file from `.agents/rules/` (only the core rules +auto-load via AGENTS.md; domain rules load on demand): + +- Processor, pipeline, operations, layers, splines (`source/core/**`): read + core.md +- Convolution kernels and typed collections (`source/kernel/**`): read + kernel.md +- Browser / node host bindings (`source/platform/**`): read platform.md +- Pure math, color, helpers (`source/utils/**`): read utils.md +- Tests (`test/**`, `*.test.ts`, `*.spec.ts`): testing.md (already loaded as + core) +- All other source code: project.md, code-principles.md, implementation.md + (always-on baseline, already loaded as core). + +@AGENTS.md