From 5b2e92f356b310f82c12679054622c769337199c Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 00:44:02 +0530 Subject: [PATCH 001/165] feat: Initialize TinyColor Go Port project structure and planning documents - Added PROJECT.md to outline the purpose, success criteria, and constraints for the Go port of TinyColor. - Created REQUIREMENTS.md detailing functional and quality requirements for the project. - Established ROADMAP.md to define phases of development and progress tracking. - Introduced STATE.md to document the current project state and environment setup. - Configured project settings in config.json for managing project metadata and features. - Developed detailed planning for Phase 1 in 01-01-PLAN.md, including tasks and verification criteria. - Context and research documents added for Phase 1 to guide implementation decisions. - Validation strategy documented in 01-VALIDATION.md to ensure testing infrastructure and sampling rates. - Established AGENT.md to define the working agreement, repository boundaries, and ownership roles. - Created COMPATIBILITY.md to track compatibility evidence and mismatch records. - Documented architectural decisions in DECISIONS.md to maintain clarity on project choices. - Outlined delivery plan in PLAN.md, detailing waves of development and risk controls. - Added architectural overview in ARCHITECTURE.md to clarify project structure and data rules. - Defined team ownership and handoff processes in TEAM-OWNERSHIP.md to ensure clear responsibilities. - Established testing strategy in TESTING.md to outline test layers and comparison rules. --- .planning/PROJECT.md | 21 ++ .planning/REQUIREMENTS.md | 22 ++ .planning/ROADMAP.md | 80 ++++++++ .planning/STATE.md | 10 + .planning/config.json | 14 ++ .../01-foundation-and-oracle/01-01-PLAN.md | 192 ++++++++++++++++++ .../01-foundation-and-oracle/01-CONTEXT.md | 85 ++++++++ .../01-foundation-and-oracle/01-RESEARCH.md | 38 ++++ .../01-foundation-and-oracle/01-VALIDATION.md | 54 +++++ AGENT.md | 103 ++++++++++ COMPATIBILITY.md | 34 ++++ DECISIONS.md | 12 ++ PLAN.md | 134 ++++++++++++ docs/ARCHITECTURE.md | 45 ++++ docs/TEAM-OWNERSHIP.md | 31 +++ docs/TESTING.md | 32 +++ 16 files changed, 907 insertions(+) create mode 100644 .planning/PROJECT.md create mode 100644 .planning/REQUIREMENTS.md create mode 100644 .planning/ROADMAP.md create mode 100644 .planning/STATE.md create mode 100644 .planning/config.json create mode 100644 .planning/phases/01-foundation-and-oracle/01-01-PLAN.md create mode 100644 .planning/phases/01-foundation-and-oracle/01-CONTEXT.md create mode 100644 .planning/phases/01-foundation-and-oracle/01-RESEARCH.md create mode 100644 .planning/phases/01-foundation-and-oracle/01-VALIDATION.md create mode 100644 AGENT.md create mode 100644 COMPATIBILITY.md create mode 100644 DECISIONS.md create mode 100644 PLAN.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/TEAM-OWNERSHIP.md create mode 100644 docs/TESTING.md diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md new file mode 100644 index 00000000..ea8f5f84 --- /dev/null +++ b/.planning/PROJECT.md @@ -0,0 +1,21 @@ +# TinyColor Go Port + +## Purpose + +Create a fresh Go port of the local `bgrins/TinyColor` checkout with behavioral +parity demonstrated through unchanged-source differential testing. + +## Success criteria + +- The upstream JavaScript source is untouched. +- Go handles every behavior covered by the defined compatibility corpus. +- A reproducible command reports compatibility results and mismatch details. +- A documented Go API and CLI are usable from a clean checkout. +- CI, benchmarks, attribution, and known differences are present. + +## Constraints + +- Target source: local `mod.js` / `test.js`; MIT attribution retained. +- Target language: Go 1.26+; standard library first. +- Node is available as local oracle runtime; Deno is currently unavailable. + diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md new file mode 100644 index 00000000..b6b5144b --- /dev/null +++ b/.planning/REQUIREMENTS.md @@ -0,0 +1,22 @@ +# Requirements + +## Functional + +- **PAR-01:** Parse all string formats and object forms accepted by local TinyColor. +- **PAR-02:** Preserve validation, format, original-input, clamping, wrapping, and alpha behavior. +- **FMT-01:** Reproduce color conversion and every documented output representation. +- **OPS-01:** Reproduce mutation, utilities, readability, and palette operations. +- **EQV-01:** Compare Go and local JavaScript behavior through a reusable differential harness. +- **DEL-01:** Provide Go API, CLI, CI, benchmarks, documentation, and attribution. + +## Quality + +- **QLT-01:** Do not edit the JavaScript source or source tests to achieve parity. +- **QLT-02:** Record every mismatch with complete reproducer and owner. +- **QLT-03:** Keep checks reproducible from a clean checkout. + +## Out of scope + +New CSS color syntaxes, a web UI, package publication, and compatibility with a +different TinyColor version. + diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 00000000..45bf6361 --- /dev/null +++ b/.planning/ROADMAP.md @@ -0,0 +1,80 @@ +# Roadmap: TinyColor.js to Go + +## Overview + +Build a separate, evidence-backed Go port while preserving this checkout as an +immutable JavaScript behavioral oracle. Work moves from an executable oracle, +through parsing and observable output, to operations and delivery proof. + +## Phases + +- [ ] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. +- [ ] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. +- [ ] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. +- [ ] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. +- [ ] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. + +## Phase Details + +### Phase 1: Foundation and Oracle +**Goal**: A contributor can run the same JSONL request against local `mod.js` and the Go runner, obtain a structured comparison report, and see the upstream files remain untouched. +**Depends on**: Nothing (first phase) +**Requirements**: [EQV-01, QLT-01, QLT-03] +**Success Criteria**: + 1. `go/` builds as an independent module without changing JavaScript source files. + 2. Node and Go runners accept and emit the documented JSONL request/response protocol. + 3. A differential command reports case count, mismatch count, and complete mismatch records. +**Plans**: 1 plan + +Plans: +- [ ] 01-01: Create the Go module, JSONL runners, smoke corpus, and baseline report. + +### Phase 2: Parsing and Color State +**Goal**: Go accepts the same supported TinyColor inputs and retains the same validity, format, alpha, and normalized color state. +**Depends on**: Phase 1 +**Requirements**: [PAR-01, PAR-02, QLT-02] +**Success Criteria**: + 1. Hex, RGB(A), HSL(A), HSV(A), CSS names, transparent, and object inputs produce matching state. + 2. Permissive syntax, boundary values, invalid input, and alpha normalization match the oracle corpus. + 3. Every discovered parser mismatch has a deterministic regression record. +**Plans**: TBD + +### Phase 3: Conversion and Representation +**Goal**: Go exposes TinyColor-equivalent conversion, string formatting, format fallback, and color analysis. +**Depends on**: Phase 2 +**Requirements**: [FMT-01, QLT-02] +**Success Criteria**: + 1. RGB, percentage RGB, HSL, HSV, hex, hex8, name, filter, and generic string outputs match exactly. + 2. Brightness, luminance, equality, cloning, and random behavior have documented parity cases. + 3. No undocumented floating-point tolerance masks an observable mismatch. +**Plans**: TBD + +### Phase 4: Operations and Palettes +**Goal**: Go matches TinyColor's stateful modifiers, static utilities, readability checks, and palette ordering. +**Depends on**: Phase 3 +**Requirements**: [OPS-01, QLT-02] +**Success Criteria**: + 1. Modifiers preserve default, zero, clamping, wrapping, alpha, and mutation behavior. + 2. Readability and most-readable operations match WCAG option defaults. + 3. Every palette operation returns source-equivalent colors in source order. +**Plans**: TBD + +### Phase 5: Delivery Evidence +**Goal**: A judge can clone the project, verify parity evidence, use a Go CLI, and understand the migration and its limitations. +**Depends on**: Phase 4 +**Requirements**: [DEL-01, QLT-03] +**Success Criteria**: + 1. CI runs format, vet, Go tests, and the differential smoke report. + 2. The CLI supports parse, convert, lighten, palette, and contrast, including JSON output. + 3. Benchmarks, documentation, license attribution, team work, and known differences are complete. +**Plans**: TBD + +## Progress + +| Phase | Plans Complete | Status | Completed | +|---|---|---|---| +| 1. Foundation and Oracle | 0/1 | Not started | - | +| 2. Parsing and Color State | 0/TBD | Not started | - | +| 3. Conversion and Representation | 0/TBD | Not started | - | +| 4. Operations and Palettes | 0/TBD | Not started | - | +| 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 00000000..58b23418 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,10 @@ +# Project State + +**Updated:** 2026-08-01 + +- Planning initialized from the local `bgrins/TinyColor` checkout. +- JavaScript source and tests are the immutable oracle. +- Node 24.18.0 and Go 1.26.1 are available locally. +- `deno` is not installed; no original-source test pass is claimed. +- No Go implementation or compatibility result exists yet. + diff --git a/.planning/config.json b/.planning/config.json new file mode 100644 index 00000000..ff197dce --- /dev/null +++ b/.planning/config.json @@ -0,0 +1,14 @@ +{ + "project": "tinycolor-go-port", + "source_oracle": "mod.js", + "source_tests": "test.js", + "target_language": "go", + "commit_docs": true, + "research_enabled": true, + "plan_checker_enabled": true, + "nyquist_validation_enabled": true, + "workflow": { + "ui_phase": false, + "ui_safety_gate": false + } +} diff --git a/.planning/phases/01-foundation-and-oracle/01-01-PLAN.md b/.planning/phases/01-foundation-and-oracle/01-01-PLAN.md new file mode 100644 index 00000000..bd8bedf0 --- /dev/null +++ b/.planning/phases/01-foundation-and-oracle/01-01-PLAN.md @@ -0,0 +1,192 @@ +--- +phase: 1 +plan: 1 +type: execute +subsystem: compatibility-foundation +tags: [go, node, jsonl, differential-testing] +wave: 0 +depends_on: [] +files_modified: + - go/go.mod + - go/tinycolor/color.go + - go/cmd/tinycolor-compat/main.go + - go/internal/compat/protocol.go + - go/internal/compat/protocol_test.go + - compat/js-runner.mjs + - compat/run.mjs + - compat/cases/smoke.jsonl + - COMPATIBILITY.md +autonomous: true +requirements: [EQV-01, QLT-01, QLT-03] +must_haves: + truths: + - "One JSONL request can be evaluated by local mod.js and by the Go runner." + - "The comparison command reports pass/mismatch totals and complete mismatch details." + - "The immutable JavaScript oracle files are not modified." + artifacts: + - path: "compat/js-runner.mjs" + provides: "Local mod.js JSONL oracle" + - path: "compat/run.mjs" + provides: "Structured differential report" + - path: "go/cmd/tinycolor-compat/main.go" + provides: "Go JSONL compatibility runner" + key_links: + - from: "compat/run.mjs" + to: "compat/js-runner.mjs" + via: "child-process stdin/stdout JSONL invocation" + - from: "compat/run.mjs" + to: "go/cmd/tinycolor-compat/main.go" + via: "go run command using the same request lines" +--- + + +Build the smallest trustworthy parity foundation: independent Go module, local +Node oracle, shared JSONL protocol, smoke corpus, and a report that makes every +comparison result inspectable. + +Purpose: all later work needs a feedback loop before behavior is ported. +Output: runnable `node compat/run.mjs compat/cases/smoke.jsonl` plus Go tests. + + + + + +Create the Go module, protocol, and minimal public color package +go/go.mod, go/tinycolor/color.go, go/internal/compat/protocol.go, go/internal/compat/protocol_test.go, go/cmd/tinycolor-compat/main.go + +- `AGENT.md` +- `PLAN.md` +- `.planning/phases/01-foundation-and-oracle/01-CONTEXT.md` +- `docs/ARCHITECTURE.md` +- `mod.js` + + +Create `go/go.mod` with a project-local module path and only the Go version. +Create `go/tinycolor/color.go` with the smallest exported `Color` value and +inspection methods needed by the fixed Phase 1 smoke corpus. Implement only +these inputs: `red`, `#000`, `not a color`, `transparent`, +`rgba(255, 0, 0, .5)`, `hsl(0, 100%, 50%)`, `hsv(0, 100%, 100%)`, RGB object +`{r:255,g:0,b:0}`, and equivalent `fromRatio` RGB. Unknown input returns a +classified unsupported-input response; Phase 2 replaces this narrow dispatcher +with full TinyColor parsing. +Create `go/internal/compat/protocol.go` defining JSON-safe request and response +types: Request has `id`, `operation`, `input`, `args`; Response has `id`, +`result`, and `error`. Implement a line-oriented decoder/encoder that rejects +malformed JSON with a structured error and writes no diagnostics to stdout. +Encode exactly one of `result` or `error`: use `omitempty` (or equivalent) and +make the response constructor reject both-set and neither-set states. Test a +success response, unsupported operation, malformed JSON, and both/neither +response-construction attempts. +Create `go/cmd/tinycolor-compat/main.go` that reads stdin line by line, echoes +each request ID, delegates every smoke `inspect`, `string`, and `fromRatio` +operation to `go/tinycolor`, and returns `unsupported operation` for unknown +operations. It must not duplicate color tables or fixed-input dispatch logic. +Add `go/internal/compat/protocol_test.go` covering one success response, one +malformed JSON response, and the one-request-to-one-response invariant. + +Set-Location go; gofmt -w tinycolor/color.go internal/compat/protocol.go internal/compat/protocol_test.go cmd/tinycolor-compat/main.go; go test ./...; go vet ./...; '{"id":"x","operation":"unknown"}' | go run ./cmd/tinycolor-compat + +- `go/go.mod` exists and contains a `go 1.26` directive. +- `go/internal/compat/protocol.go` defines `Request` and `Response`. +- Protocol tests prove successful, unsupported, and malformed requests serialize + exactly one of `result` or `error`. +- `go test ./...` from `go/` exits 0. +- `go run ./cmd/tinycolor-compat` returns a JSON line with the input `id` and + an `error` field for `{"id":"x","operation":"unknown"}`. + +The Go module builds, protocol tests pass, and the runner emits one structured response per request. + + + +Create the local Node oracle and fixed smoke corpus +compat/js-runner.mjs, compat/cases/smoke.jsonl + +- `mod.js` +- `test.js` +- `docs/ARCHITECTURE.md` +- `go/internal/compat/protocol.go` + + +Create `compat/js-runner.mjs` as an ESM executable that imports `../mod.js`. +Implement the shared JSONL loop. Support exactly these Phase 1 operations: +`inspect` returns validity, detected format, alpha, RGB object, and default +string; `string` returns `tinycolor(input).toString(args.format)`; `fromRatio` +returns the same inspect result after `tinycolor.fromRatio(input, args)`; and +`error` serializes thrown JavaScript errors by name and message. Keep input/output +serialization deterministic. Every Node response must contain exactly one of +`result` or `error`; malformed JSON and unknown operations must return a +structured `error`, and one non-empty input line must produce one stdout line. +Create `compat/cases/smoke.jsonl` with stable IDs +covering `red`, `#000`, `not a color`, `transparent`, `rgba(255, 0, 0, .5)`, +`hsl(0, 100%, 50%)`, `hsv(0, 100%, 100%)`, `{ "r": 255, "g": 0, "b": 0 }`, +and one `fromRatio` request. + +'{"id":"ratio-red","operation":"fromRatio","input":{"r":1,"g":0,"b":0}}' | node compat/js-runner.mjs; '{bad json' | node compat/js-runner.mjs; '{"id":"unknown","operation":"unknown"}' | node compat/js-runner.mjs; (Get-Content compat/cases/smoke.jsonl | Measure-Object -Line).Lines + +- `compat/js-runner.mjs` contains `import tinycolor from "../mod.js"`. +- `compat/cases/smoke.jsonl` contains at least 9 non-empty JSON lines with + unique `id` values. +- A `fromRatio` request returns an inspect-shaped result for red. +- Node success, malformed JSON, and unknown-operation responses contain exactly + one of `result` or `error`, with one response line for each input line. +- Piping one `inspect` request into `node compat/js-runner.mjs` returns one + response line with the same ID and a `result` field. +- `git diff -- mod.js test.js tinycolor.js` is empty after the task. + +The Node runner evaluates all fixed smoke operations against local mod.js and the corpus is source-only. + + + +Run equal smoke operations through both runners and publish the baseline +compat/run.mjs, go/cmd/tinycolor-compat/main.go, COMPATIBILITY.md + +- `compat/js-runner.mjs` +- `compat/cases/smoke.jsonl` +- `go/cmd/tinycolor-compat/main.go` +- `docs/TESTING.md` +- `COMPATIBILITY.md` + + +Create `compat/run.mjs` that reads a JSONL corpus, sends each request unchanged +to both runner commands, parses their one-line responses, and prints a summary +with `cases`, `passed`, and `mismatches`. For every mismatch print the stable +case ID, operation, full request, JavaScript response, Go response, and exit +non-zero. It must also print `owner` and `suspectedPackage`: use `compat` when +the response protocol differs and `go/tinycolor` when the Go color result +differs. Make the Go runner implement the same Phase 1 `inspect`, `string`, +and `fromRatio` operations for exactly the fixed smoke corpus. For `fromRatio`, +convert non-alpha fields from `[0,1]` before inspection and preserve alpha. +Update `COMPATIBILITY.md` with the exact command, corpus name, case count, date, +and result. This fixed corpus must be green; generalized parsing remains Phase 2 +and must not be added to smoke cases before it is implemented. + +Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js + +- `compat/run.mjs` contains the literal labels `cases`, `passed`, and + `mismatches`. +- `node compat/run.mjs compat/cases/smoke.jsonl` exits 0 with `mismatches: 0` + for the fixed corpus and exits non-zero with case/JS/Go details for a forced + mismatch, `owner`, and `suspectedPackage`. +- `COMPATIBILITY.md` names `compat/cases/smoke.jsonl` and the exact runner + command used for the result. +- `git diff --check` exits 0. + +The fixed smoke corpus is green, its exact command/count are documented, and the oracle source diff is empty. + + + + + +From repository root run: + +```powershell +Set-Location go; go test ./...; Set-Location .. +Set-Location go; go vet ./...; Set-Location .. +node compat/run.mjs compat/cases/smoke.jsonl +git diff --check +git diff -- mod.js test.js tinycolor.js +``` + +The Go tests and differential report must exit 0. The oracle-file diff command +must produce no output. Record the actual report count in `COMPATIBILITY.md`. + diff --git a/.planning/phases/01-foundation-and-oracle/01-CONTEXT.md b/.planning/phases/01-foundation-and-oracle/01-CONTEXT.md new file mode 100644 index 00000000..e017ccd6 --- /dev/null +++ b/.planning/phases/01-foundation-and-oracle/01-CONTEXT.md @@ -0,0 +1,85 @@ +# Phase 1: Foundation and Oracle - Context + +**Gathered:** 2026-08-01 +**Status:** Ready for planning + + +## Phase Boundary + +Create the independent Go module and a local, executable Node-to-Go comparison +path. This phase proves the test architecture; it does not implement full color +parsing, conversions, or the public CLI. + + + +## Implementation Decisions + +### Source integrity +- The local `mod.js`, `test.js`, generated npm files, and demo are immutable + reference files. +- The Node runner imports `./mod.js` directly; it must not compare against a + registry package. + +### Module and protocol +- New implementation files live below `go/`, with a separate `go/go.mod`. +- Both runners use one JSON object per stdin line and one JSON object per stdout + line. Diagnostics go to stderr. +- The request has `id`, `operation`, `input`, and optional `args`; a response + echoes `id` and contains one of `result` or `error`. + +### Evidence +- The initial corpus is a deliberately small smoke suite sourced from `test.js` + and README examples; it must include a valid value, invalid value, alpha, a + named color, and one HSL/HSV/object form. +- The report must include total cases, pass count, mismatch count, and for each + mismatch the request plus JavaScript and Go results. + +### the agent's Discretion +- Package names, JSON implementation details, and exact process-launch strategy, + provided they maintain the protocol and use standard library dependencies. + + + +## Canonical References + +### Source behavior +- `mod.js` — local reference implementation the Node runner imports. +- `test.js` — existing behavioral cases used to seed the corpus. +- `README.md` — accepted input and documented operation surface. + +### Project decisions +- `AGENT.md` — repository boundaries, ownership, and required checks. +- `PLAN.md` — all-wave plan and Phase 1 gate. +- `DECISIONS.md` — accepted architecture decisions. +- `docs/ARCHITECTURE.md` — layer and protocol constraints. +- `docs/TESTING.md` — comparison rules and test layers. + + + +## Existing Code Insights + +### Reusable Assets +- `mod.js`: ESM default export, directly importable by Node. +- `test.js`: categorized upstream behavior, including invalid colors and alpha. + +### Established Patterns +- Source calls return objects and strings; source tests use Deno but local Node + can import `mod.js` for an adapter. + +### Integration Points +- Future Go parser/API code consumes the protocol designed here; the smoke + runner must allow unsupported operations while the implementation is staged. + + + +## Deferred Ideas + +Full parser/conversion implementation (Phase 2/3), CLI and CI (Phase 5), and +Deno suite setup once the runtime is available. + + +--- + +*Phase: 01-foundation-and-oracle* +*Context gathered: 2026-08-01* + diff --git a/.planning/phases/01-foundation-and-oracle/01-RESEARCH.md b/.planning/phases/01-foundation-and-oracle/01-RESEARCH.md new file mode 100644 index 00000000..470a8eff --- /dev/null +++ b/.planning/phases/01-foundation-and-oracle/01-RESEARCH.md @@ -0,0 +1,38 @@ +# Phase 1 Research — Foundation and Oracle + +## Findings + +- This checkout's executable ESM source is `mod.js`; it exports TinyColor as a + default function and contains both methods and static helpers. +- `test.js` is a Deno test file that imports `./mod.js`; it has groups for + initialization, input, conversion, output, readability, modifications, and + palettes. It should be read as behavior evidence, not altered. +- Node 24.18.0 can execute an ESM adapter locally. Go 1.26.1 is installed. +- Deno is not installed, so `deno task test` cannot currently validate the + upstream suite. Node import of `mod.js` is the available local oracle path. +- JSON Lines is sufficient for requests because all phase-one inputs and + outputs can be JSON-safe. Do not attempt to transfer JavaScript functions or + original object identity through this adapter. + +## Recommended implementation shape + +Use one short-lived command invocation per runner during Phase 1 for simpler +failure attribution. If benchmark evidence later shows process startup dominates +the suite, upgrade the same protocol to persistent child processes without +changing case files or response schema. + +## Pitfalls + +- JavaScript's `undefined`, `NaN`, and signed-zero behavior needs explicit + serialization rules before later corpus expansion. +- A Go error is not automatically source-equivalent: source invalid color input + frequently creates an invalid value that renders as black. +- JSON field ordering is not behavior; values, operation results, and error + classification are. + +## Validation Architecture + +Phase 1 can validate all of its new behavior with Go tests plus one Node-driven +smoke report. The original Deno test suite remains a manual/CI follow-up until +the runtime is installed. + diff --git a/.planning/phases/01-foundation-and-oracle/01-VALIDATION.md b/.planning/phases/01-foundation-and-oracle/01-VALIDATION.md new file mode 100644 index 00000000..1bf524f0 --- /dev/null +++ b/.planning/phases/01-foundation-and-oracle/01-VALIDATION.md @@ -0,0 +1,54 @@ +--- +phase: 1 +slug: foundation-and-oracle +status: draft +nyquist_compliant: true +wave_0_complete: false +created: 2026-08-01 +--- + +# Phase 1 — Validation Strategy + +## Test Infrastructure + +| Property | Value | +|---|---| +| Framework | Go standard `testing` plus Node 24.18.0 | +| Config file | `go/go.mod` (created by Plan 01-01) | +| Quick run command | `go test ./...` from `go/` | +| Full suite command | `node compat/run.mjs compat/cases/smoke.jsonl` | +| Estimated runtime | under 10 seconds | + +## Sampling Rate + +- After every task commit: `go test ./...` and `go vet ./...` from `go/`. +- After Wave 0: `node compat/run.mjs compat/cases/smoke.jsonl`. +- Before phase verification: run both commands, `go vet ./...`, and `git diff --check`. + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status | +|---|---|---|---|---|---|---|---| +| 01-01-01 | 01 | 0 | QLT-01 | static | `git diff -- mod.js test.js tinycolor.js` | ❌ W0 | pending | +| 01-01-02 | 01 | 0 | EQV-01 | unit | `go test ./...` | ❌ W0 | pending | +| 01-01-03 | 01 | 0 | EQV-01, QLT-03 | differential | `node compat/run.mjs compat/cases/smoke.jsonl` | ❌ W0 | pending | + +## Wave 0 Requirements + +- [ ] `go/go.mod` and a minimal Go runner test. +- [ ] `go/tinycolor/color.go` implements only fixed smoke-corpus inputs. +- [ ] `compat/js-runner.mjs`, `compat/run.mjs`, and smoke corpus. +- [ ] A documented protocol schema in `docs/ARCHITECTURE.md`. + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|---|---|---|---| +| Upstream Deno source suite | QLT-03 | Deno unavailable locally | After installing Deno, run `deno task test` from repo root and record result. | + +## Validation Sign-Off + +- [x] Every planned task has an automated check or a declared Wave 0 dependency. +- [x] No three consecutive tasks lack automated verification. +- [x] The unavailable Deno check is explicit rather than claimed. +- [ ] Phase checks are green. diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 00000000..02157a1d --- /dev/null +++ b/AGENT.md @@ -0,0 +1,103 @@ +# TinyColor.js to Go — Working Agreement + +## Mission + +Build a fresh Go implementation of the public behavior in this checkout of +[`bgrins/TinyColor`](https://github.com/bgrins/TinyColor), without changing, +copying into, or replacing its JavaScript implementation. The Go port is a +compatibility project: observable TinyColor behavior is the specification. + +## Repository boundaries + +- `mod.js`, `tinycolor.js`, `test.js`, `npm/`, `dist/`, and `demo/` are the + immutable JavaScript oracle. Do not edit them except for a separately agreed + upstream maintenance change. +- New Go work belongs under `go/`; `go/go.mod` is the port module boundary. +- Oracle adapters, generated fixtures, and mismatch reports belong under + `compat/`. Do not rewrite the original test suite to make it pass. +- Human-facing project docs belong in `docs/`; compatibility status belongs in + `COMPATIBILITY.md`. + +## Non-negotiable behavior rules + +1. Preserve permissive parsing: whitespace, optional commas/parentheses, + uppercase input, no-`#` hex, percentages, and object inputs. +2. Preserve TinyColor's validation, clamping, hue wrapping, alpha normalization, + format selection, rounding, string output, and invalid-input-as-black + behavior. A more idiomatic result is not a compatible result. +3. Preserve mutation where JavaScript mutates (`setAlpha`, instance modifiers) + and return independent values where it returns new colors (`clone`, palettes, + static utilities). +4. Treat a differential mismatch as a defect until a documented source-version + difference proves otherwise. Do not add broad floating-point tolerances. + +## Source-of-truth map + +| Behavior | Read first | +|---|---| +| Constructor, output methods, instance mutation | `mod.js` lines 5–358 | +| Input normalization and color conversion | `mod.js` lines 359–654 | +| Manipulation, palettes, readability | `mod.js` lines 655–1046 | +| Parsing regexes, names, low-level helpers | `mod.js` lines 1047–end | +| Existing expected behavior | `test.js` | +| Public usage and supported formats | `README.md` | +| Project choices | `DECISIONS.md`, `docs/ARCHITECTURE.md` | + +## Go API direction + +Expose an idiomatic, explicit API while keeping a JSON-compatible adapter for +exact comparison. Start with a `Color` value and `Parse(any) (Color, error)` +for Go callers; retain `Valid()`, `Format()`, `Original()`, output methods, +modifiers, palettes, and readability methods that mirror TinyColor names in Go +style. The compatibility adapter—not the public API—may expose dynamic input +and operation names. + +Do not finalize exported signatures until Phase 1 proves the adapter can +represent every source test category. Any intentional API divergence needs an +entry in `DECISIONS.md` and must not affect the parity adapter. + +## Workflow and ownership + +Follow the phases in `PLAN.md` and `.planning/ROADMAP.md`. Keep commits small +and single-purpose. A pull request must state: + +```text +Feature implemented: +Source behavior checked: +Differential command and result: +Known differences (or none): +``` + +| Owner | Primary boundary | Cannot merge without | +|---|---|---| +| A — Parsing/model | `go/internal/color`, `go/internal/parser` | parser differential cases | +| B — Conversion/API | `go/tinycolor`, formatting/readability/palettes | deterministic parity tests | +| C — Compatibility/quality | `compat`, `go/testdata`, `COMPATIBILITY.md` | reproducible report | +| D — Delivery | CLI, CI, benchmarks, docs | clean-checkout commands | + +Coordinate through exported contracts, never by editing another owner’s files +without agreement. C can add a regression fixture for any mismatch; the owner +of the implicated module fixes it. + +## Required checks + +Run the narrowest applicable check while developing, then run the phase gate: + +```powershell +go test ./... # from go/ +node compat/js-runner.mjs < cases.json # from repository root +go test -run TestDifferential ./... # from go/ +go vet ./... # from go/ +gofmt -w +``` + +Once Deno is installed, additionally run the untouched source suite with +`deno task test`. Its current absence is an environment limitation, not a +passing test result. + +## Definition of done + +A feature is done only when its source behavior is mapped, Go unit tests pass, +the matching oracle cases pass, the compatibility matrix is updated, and any +remaining mismatch is named with input, JavaScript result, Go result, and owner. + diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md new file mode 100644 index 00000000..2015de9e --- /dev/null +++ b/COMPATIBILITY.md @@ -0,0 +1,34 @@ +# Compatibility Matrix + +Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. + +## Current evidence + +| Area | Status | Evidence | Notes | +|---|---|---|---| +| Oracle baseline | Ready | `mod.js`, `test.js`, Node 24.18.0 | JavaScript source is untouched. | +| Node adapter | Planned | Wave 0 | Must import local `mod.js`, never npm. | +| Go adapter | Planned | Wave 0 | JSONL schema shared with Node adapter. | +| HEX/RGB/name parsing | Planned | Wave 1 | Include invalid and permissive syntax. | +| HSL/HSV parsing | Planned | Wave 1 | Include ratios, percentages, and wrapping. | +| Conversion/formatting | Planned | Wave 2 | Exact strings and rounding. | +| Analysis/readability | Planned | Wave 2–3 | Preserve WCAG defaults. | +| Manipulation/palettes | Planned | Wave 3 | Preserve defaults and ordering. | +| CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | +| Original Deno suite | Blocked locally | `deno` unavailable | Run when Deno is installed or in CI. | + +## Mismatch record format + +```text +Case: +Operation: +Input: +JavaScript: +Go: +Owner: +Status: open | fixed | accepted-difference +Reason: +``` + +There are no parity claims yet; implementation has not begun. + diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 00000000..7ab9d024 --- /dev/null +++ b/DECISIONS.md @@ -0,0 +1,12 @@ +# Decisions + +| ID | Decision | Why | Status | +|---|---|---|---| +| D-001 | Port the checked-out `mod.js` behavior to Go. | It is the exact source and test target selected for the project. | Accepted | +| D-002 | Keep JavaScript oracle files immutable. | Differential evidence is credible only when the reference does not move with the port. | Accepted | +| D-003 | Put the Go module in `go/`. | It prevents an in-progress port from contaminating the upstream JavaScript package layout. | Accepted | +| D-004 | Use JSON Lines adapters over stdin/stdout. | Node and Go can compare dynamic inputs and operations without modifying source tests. | Accepted | +| D-005 | Use Go standard library first. | The port's algorithms are small and self-contained; dependencies add compatibility and supply-chain surface. | Accepted | +| D-006 | Preserve source quirks at the adapter boundary; expose an idiomatic Go API separately. | Compatibility and Go usability are both required, but they should not leak dynamic JavaScript semantics into every caller. | Accepted | +| D-007 | Deno's source suite is a required later check, not a current pass. | `deno` is not installed in the checked environment as of 2026-08-01. | Accepted | + diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..82ce2e9d --- /dev/null +++ b/PLAN.md @@ -0,0 +1,134 @@ +# Delivery Plan — TinyColor.js to Go + +## Outcome + +Ship an independent Go library and CLI that reproduce the behavior of this +checkout's `mod.js`, prove parity with a repeatable Node-to-Go differential +harness, and explain every known difference. The original JavaScript source +remains intact in the repository. + +## Guardrails + +- Compatibility is against the checked-out source, not a remembered npm API. +- The port uses only the Go standard library unless a dependency is approved in + `DECISIONS.md`. +- Do not claim whole-suite parity while `deno task test` is unavailable. +- A zero mismatch report covers only the exercised corpus; label the corpus and + command precisely. + +## Waves + +| Wave | Scope | Owners | Exit gate | +|---|---|---|---| +| 0 | Baseline and oracle protocol | C, D | one JSON case reaches Node and Go | +| 1 | Color model plus HEX/RGB/name parsing | A, C | parser corpus is differential-green | +| 2 | HSL/HSV, conversion, formatting, analysis | A, B, C | conversion and output corpus is green | +| 3 | Mutation, utilities, readability, palettes | B, C | source categories are represented and green | +| 4 | CLI, CI, benchmarks, release documentation | D, all | clean checkout can verify and demonstrate | + +## Wave 0 — Foundation and behavioral oracle + +1. Create `go/` as an independent module; add a minimal public package, a JSON + Lines request/response protocol, and a Go runner executable. Implement only + the fixed smoke-corpus inputs needed to prove the protocol; all generalized + parsing and public API behavior remains in later waves. +2. Add `compat/js-runner.mjs`, which imports local `./mod.js`, accepts the same + request object, and serializes values deterministically. The process must + report structured errors rather than swallowing JavaScript exceptions. +3. Add a differential driver that launches both runners, normalizes JSON + numbers only where JSON itself requires it, and emits each mismatch with + case ID, operation, source output, Go output, and suspected package. +4. Seed `compat/cases/smoke.jsonl` from existing tests: `red`, `#000`, invalid + text, transparent, `rgba(255, 0, 0, .5)`, HSL, HSV, objects, and `fromRatio`. +5. Record baseline tool versions and the missing-Deno constraint in + `COMPATIBILITY.md`. + +**Gate:** `node compat/run.mjs compat/cases/smoke.jsonl` exits non-zero until +the Go operation exists, then exits zero and prints a case count and mismatch +count. The original JavaScript files remain unmodified (`git diff --` shows no +changes under the oracle paths). + +## Wave 1 — Normalized model and parser parity + +1. Define private normalized RGBA storage, validity, detected format, original + input metadata, and explicit Go input structs. Keep the dynamic decoder only + in the compatibility boundary. +2. Port `bound01`, `boundAlpha`, percentage conversion, and hue wrapping before + implementing parsers. Add boundary regressions for negative, overflow, + `1`, `1.0`, `%`, alpha, and invalid values. +3. Implement HEX (`3/4/6/8`), RGB/RGBA, CSS percentage RGB, HSL/HSLA, + HSV/HSVA, named colors, and `transparent` by following `stringInputToObject` + and `inputToRGB` in `mod.js` exactly. +4. Convert every parser-related `Deno.test` group into JSONL cases or explicit + Go table cases that invoke both runners. Do not hand-copy expected outputs + from memory. + +**Gate:** all cases categorized Parsing, Object Input, Invalid Input, Names, +and Alpha normalize with zero unexplained mismatches. `COMPATIBILITY.md` names +the corpus revision and count. + +## Wave 2 — Conversion, representation, and analysis + +1. Port RGB↔HSL, RGB↔HSV, RGB/hex/ARGB conversion with JavaScript-equivalent + rounding at each observable boundary. +2. Implement `ToRGB`, percentage RGB, HSL, HSV, hex/hex8, name, filter, and + general string formatting. Test alpha fallback in `toString` and compact + hex behavior independently. +3. Implement brightness, luminance, `IsDark`, `IsLight`, equality, random + (injectable randomness for tests), and cloning behavior. +4. Add exact-string and exact-number cases from the existing conversion, + formatting, filter, and analysis tests. + +**Gate:** all output strings match byte-for-byte; numeric JSON fields match +source results or a documented IEEE-754 serialization normalization. No blanket +epsilon comparison is allowed. + +## Wave 3 — Manipulation and combinations + +1. Implement static and instance equivalents of lighten, brighten, darken, + saturate, desaturate, greyscale, and spin. Verify defaults, explicit zero, + clamping, hue wrap, alpha preservation, and receiver mutation. +2. Implement `Mix`, readability, `IsReadable`, and `MostReadable` using the + source decision paths and WCAG option defaults. +3. Implement complement, analogous, monochromatic, split complement, triad, + and tetrad; preserve output order and source default counts. +4. Add seeded randomized inputs and preserve every discovered mismatch as a + deterministic JSONL regression before fixing it. + +**Gate:** source categories Modifications, Spin, Mix, Readability, and every +palette family are covered by deterministic cases; fuzz seeds reproduce every +failure. + +## Wave 4 — Submission-quality delivery + +1. Add a `tinycolor` CLI with `parse`, `convert`, `lighten`, `palette`, and + `contrast` commands. Its `--json` output must use the compatibility schema. +2. Add GitHub Actions for Go formatting, tests, vet, differential smoke tests, + and platform builds. Keep Node as the oracle runtime; add Deno source-test + execution only when its runner is provisioned in CI. +3. Add Go benchmarks for parsing, conversion, manipulation, allocations, and + the documented Node comparison methodology. Do not claim direct speedups + from incomparable machines or workloads. +4. Finish README additions, architecture, testing guide, demo script, + compatibility matrix, decisions, and attribution/license notices. + +**Gate:** a new contributor can clone, run the documented setup, execute Go +tests and the differential report, use the CLI, read performance methodology, +and see the known-difference list. + +## Explicitly deferred + +- Any behavior outside the current `mod.js` and `test.js` checkout. +- CSS Color Level 4 syntaxes not accepted by this source. +- Replacing the Go API with a JavaScript interpreter or vendoring TinyColor. +- A GUI, web service, or package publication before parity evidence exists. + +## Risk controls + +| Risk | Control | +|---|---| +| JavaScript coercion differs from Go | keep coercion in adapter; turn each mismatch into a fixture | +| Floating point string drift | compare rendered strings; document only narrow JSON normalization | +| Unchanged tests are hard to call from Go | use JSONL adapters, never edit `test.js` | +| Four people collide | enforce module ownership and wave gates | +| Deno is absent locally | Node adapter is the working oracle; mark Deno suite unverified | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..62afbba7 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,45 @@ +# Architecture + +```text +immutable JS source (mod.js, test.js) + │ + Node JSONL oracle + │ +cases ── differential driver ── Go JSONL runner + │ + go/tinycolor public API + │ │ + internal/color internal/parser + │ │ + conversion, formatting, utilities +``` + +## Layers + +1. **Compatibility boundary (`compat/` and runner):** accepts JSON-safe dynamic + input, dispatches named operations, and returns a stable record. It owns no + color math. +2. **Parsing/model (`go/internal/...`):** turns strings and typed Go inputs into + normalized RGBA plus validity and source-format metadata. +3. **Public library (`go/tinycolor`):** exposes explicit Go values and methods. + It owns conversions, formatting, mutation, utilities, readability, and + palettes. +4. **CLI (`go/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same + library and does not reimplement parsing or conversion. + +## Compatibility protocol + +Each line is a single JSON request with `id`, `operation`, `input`, and optional +`args`. Each response echoes `id` and contains exactly one of `result` or +`error`. The runner must write protocol traffic only to stdout and diagnostics +only to stderr. This makes streamed comparison reliable and keeps CLI output +separate from harness output. + +## Data rules + +Internal color channels retain floating precision until an observable TinyColor +method rounds them. Alpha is stored in `[0,1]`; detected format and validity are +separate from RGB channels because invalid TinyColor values still render as +black. Original input is adapter metadata: typed Go callers should not need to +recover JavaScript object identity. + diff --git a/docs/TEAM-OWNERSHIP.md b/docs/TEAM-OWNERSHIP.md new file mode 100644 index 00000000..f88a13bc --- /dev/null +++ b/docs/TEAM-OWNERSHIP.md @@ -0,0 +1,31 @@ +# Team Ownership and Handoffs + +## A — Model and parser lead + +Owns normalized color data, inputs, low-level bounds, format metadata, named +colors, and string/object parsing. Delivers parser fixtures and a documented +package contract before B depends on it. + +## B — Conversion and behavior lead + +Owns conversion, formatting, instance/static operations, readability, and +palettes. Consumes A's parsed color contract; does not change it unilaterally. + +## C — Equivalence and quality lead + +Owns the Node and Go adapters, case schema, differential driver, seeded corpus, +regressions, and compatibility matrix. C can block a merge on unexplained +behavioral divergence. + +## D — Integration and delivery lead + +Owns module setup, CLI, CI, benchmarks, README integration, demo instructions, +and release hygiene. D does not declare parity from a benchmark or unit test; +the differential report is required. + +## Handoff format + +Every handoff includes the commit, public contract or command added, passing +checks, corpus coverage, and known gaps. Do not hand off an uncommitted shared +working tree as the only artifact. + diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 00000000..53a3e605 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,32 @@ +# Testing Strategy + +## Test layers + +| Layer | Purpose | Command when implemented | +|---|---|---| +| Go unit tests | Local conversion and parser invariants | `go test ./...` from `go/` | +| Differential corpus | Exact observable parity with local `mod.js` | `node compat/run.mjs ` | +| Regression corpus | Permanently reproduce every found mismatch | `node compat/run.mjs compat/cases/regression.jsonl` | +| Seeded fuzz cases | Find coercion, bounds, and rounding gaps | documented seed command | +| Source suite | Validate original source remains runnable | `deno task test` when Deno exists | +| Static checks | Formatting and common Go defects | `gofmt`, `go vet ./...` | + +## Comparison rules + +- Compare strings byte-for-byte. +- Compare booleans, arrays, formats, and errors exactly. +- Serialize non-finite values explicitly if the source produces them; JSON's + default inability to represent them must not silently erase a difference. +- Use a narrowly documented normalization only for JavaScript/Go JSON number + serialization—not for color math. Never use a global epsilon. +- Seed random generation and print the seed on failure. + +## Corpus priorities + +1. Inputs copied from `test.js` in parser and output groups. +2. Boundaries: `-1`, `0`, `1`, `1.0`, `100%`, `255`, overflow, blank, malformed, + alpha `0`, and hue values below/above 360. +3. State transitions: clone, set alpha, all instance modifiers, default versus + explicit zero amount. +4. Structured random RGB/RGBA/HSL/HSV inputs generated from a fixed seed. + From 0907a715c8d7690934ebf9142c2bf2b3f340aa04 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 00:58:11 +0530 Subject: [PATCH 002/165] fix: Update team ownership section with lead usernames --- docs/TEAM-OWNERSHIP.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/TEAM-OWNERSHIP.md b/docs/TEAM-OWNERSHIP.md index f88a13bc..8ecb96ca 100644 --- a/docs/TEAM-OWNERSHIP.md +++ b/docs/TEAM-OWNERSHIP.md @@ -1,23 +1,23 @@ # Team Ownership and Handoffs -## A — Model and parser lead +## A — Model and parser lead @rajeet-04 Owns normalized color data, inputs, low-level bounds, format metadata, named colors, and string/object parsing. Delivers parser fixtures and a documented package contract before B depends on it. -## B — Conversion and behavior lead +## B — Conversion and behavior lead @xthxr Owns conversion, formatting, instance/static operations, readability, and palettes. Consumes A's parsed color contract; does not change it unilaterally. -## C — Equivalence and quality lead +## C — Equivalence and quality lead @mrashis Owns the Node and Go adapters, case schema, differential driver, seeded corpus, regressions, and compatibility matrix. C can block a merge on unexplained behavioral divergence. -## D — Integration and delivery lead +## D — Integration and delivery lead @deepali Owns module setup, CLI, CI, benchmarks, README integration, demo instructions, and release hygiene. D does not declare parity from a benchmark or unit test; From 78423e391c2a76e8b8c643e7fab989416f8634c6 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 01:15:10 +0530 Subject: [PATCH 003/165] feat: add Phase 1 compatibility foundation --- .gitignore | 1 + COMPATIBILITY.md | 17 +++++-- compat/cases/smoke.jsonl | 9 ++++ compat/js-runner.mjs | 42 ++++++++++++++++ compat/js-runner.test.mjs | 29 +++++++++++ compat/run.mjs | 40 ++++++++++++++++ go/cmd/tinycolor-compat/main.go | 61 ++++++++++++++++++++++++ go/go.mod | 3 ++ go/internal/compat/protocol.go | 54 +++++++++++++++++++++ go/internal/compat/protocol_test.go | 18 +++++++ go/tinycolor/color.go | 74 +++++++++++++++++++++++++++++ go/tinycolor/color_test.go | 43 +++++++++++++++++ 12 files changed, 388 insertions(+), 3 deletions(-) create mode 100644 compat/cases/smoke.jsonl create mode 100644 compat/js-runner.mjs create mode 100644 compat/js-runner.test.mjs create mode 100644 compat/run.mjs create mode 100644 go/cmd/tinycolor-compat/main.go create mode 100644 go/go.mod create mode 100644 go/internal/compat/protocol.go create mode 100644 go/internal/compat/protocol_test.go create mode 100644 go/tinycolor/color.go create mode 100644 go/tinycolor/color_test.go diff --git a/.gitignore b/.gitignore index 05fdc6a2..1c08f4da 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules .vscode cov_profile cov_profile.lcov +.cache/ \ No newline at end of file diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 2015de9e..5f748040 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -7,8 +7,8 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | Area | Status | Evidence | Notes | |---|---|---|---| | Oracle baseline | Ready | `mod.js`, `test.js`, Node 24.18.0 | JavaScript source is untouched. | -| Node adapter | Planned | Wave 0 | Must import local `mod.js`, never npm. | -| Go adapter | Planned | Wave 0 | JSONL schema shared with Node adapter. | +| Node adapter | Passing | `node compat/js-runner.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | +| Go adapter | Passing | `go test ./...` from `go/` | JSONL schema shared with Node adapter. | | HEX/RGB/name parsing | Planned | Wave 1 | Include invalid and permissive syntax. | | HSL/HSV parsing | Planned | Wave 1 | Include ratios, percentages, and wrapping. | | Conversion/formatting | Planned | Wave 2 | Exact strings and rounding. | @@ -30,5 +30,16 @@ Status: open | fixed | accepted-difference Reason: ``` -There are no parity claims yet; implementation has not begun. +## Phase 1 smoke result +On 2026-08-01, the fixed Phase 1 corpus passed with **9/9 cases** and **0 +mismatches**: + +```powershell +$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' +node compat/js-runner.test.mjs +Set-Location go; go test ./...; go vet ./...; Set-Location .. +node compat/run.mjs compat/cases/smoke.jsonl +``` + +This is only the fixed Phase 1 corpus; it is not a full TinyColor parity claim. diff --git a/compat/cases/smoke.jsonl b/compat/cases/smoke.jsonl new file mode 100644 index 00000000..f1790066 --- /dev/null +++ b/compat/cases/smoke.jsonl @@ -0,0 +1,9 @@ +{"id":"name-red","operation":"inspect","input":"red"} +{"id":"hex-black","operation":"inspect","input":"#000"} +{"id":"invalid","operation":"inspect","input":"not a color"} +{"id":"transparent","operation":"inspect","input":"transparent"} +{"id":"rgba-red","operation":"inspect","input":"rgba(255, 0, 0, .5)"} +{"id":"hsl-red","operation":"inspect","input":"hsl(0, 100%, 50%)"} +{"id":"hsv-red","operation":"inspect","input":"hsv(0, 100%, 100%)"} +{"id":"rgb-object","operation":"inspect","input":{"r":255,"g":0,"b":0}} +{"id":"ratio-red","operation":"fromRatio","input":{"r":1,"g":0,"b":0}} diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs new file mode 100644 index 00000000..0781d649 --- /dev/null +++ b/compat/js-runner.mjs @@ -0,0 +1,42 @@ +import readline from "node:readline"; +import tinycolor from "../mod.js"; + +const inspect = (color) => ({ + valid: color.isValid(), + format: color.getFormat(), + alpha: color.getAlpha(), + rgb: color.toRgb(), + value: color.toString(), +}); + +const respond = (id, result, error) => { + if ((result === undefined) === (error === undefined)) { + throw new Error("response must contain exactly one of result or error"); + } + return error === undefined ? { id, result } : { id, error }; +}; + +const handle = (request) => { + switch (request.operation) { + case "inspect": + return respond(request.id, inspect(tinycolor(request.input))); + case "string": + return respond(request.id, tinycolor(request.input).toString(request.args?.format)); + case "fromRatio": + return respond(request.id, inspect(tinycolor.fromRatio(request.input, request.args))); + default: + return respond(request.id, undefined, "unsupported operation"); + } +}; + +const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); +for await (const line of lines) { + if (!line) continue; + try { + const request = JSON.parse(line); + if (!request.id || !request.operation) throw new Error("id and operation are required"); + console.log(JSON.stringify(handle(request))); + } catch (error) { + console.log(JSON.stringify(respond("", undefined, error instanceof SyntaxError ? "malformed JSON" : error.message))); + } +} diff --git a/compat/js-runner.test.mjs b/compat/js-runner.test.mjs new file mode 100644 index 00000000..39a1291a --- /dev/null +++ b/compat/js-runner.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; + +const run = (input) => { + const result = spawnSync(process.execPath, ["compat/js-runner.mjs"], { + cwd: process.cwd(), + input, + encoding: "utf8", + }); + return result.stdout.trim().split("\n").filter(Boolean).map(JSON.parse); +}; + +const [success] = run('{"id":"red","operation":"inspect","input":"red"}\n'); +assert.deepEqual(success, { + id: "red", + result: { + valid: true, + format: "name", + alpha: 1, + rgb: { r: 255, g: 0, b: 0, a: 1 }, + value: "red", + }, +}); + +const [malformed, unknown] = run('{bad json\n{"id":"unknown","operation":"unknown"}\n'); +for (const response of [malformed, unknown]) { + assert.notEqual(Object.hasOwn(response, "result"), Object.hasOwn(response, "error")); + assert.equal(typeof response.error, "string"); +} diff --git a/compat/run.mjs b/compat/run.mjs new file mode 100644 index 00000000..4f9c778c --- /dev/null +++ b/compat/run.mjs @@ -0,0 +1,40 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { spawnSync } from "node:child_process"; +import { isDeepStrictEqual } from "node:util"; + +const root = process.cwd(); +const corpus = process.argv[2]; +if (!corpus) throw new Error("usage: node compat/run.mjs "); + +const cases = readFileSync(corpus, "utf8").split(/\r?\n/).filter(Boolean).map(JSON.parse); +const goEnv = { ...process.env, GOCACHE: process.env.GOCACHE ?? resolve(root, ".cache", "go-build") }; + +function run(command, args, cwd, request) { + const output = spawnSync(command, args, { cwd, input: `${JSON.stringify(request)}\n`, encoding: "utf8", env: goEnv }); + if (output.status !== 0) return { id: request.id, error: output.stderr.trim() || `${command} exited ${output.status}` }; + return JSON.parse(output.stdout.trim()); +} + +function same(left, right) { + return isDeepStrictEqual(left, right); +} + +let passed = 0; +let mismatches = 0; +for (const request of cases) { + const js = run(process.execPath, ["compat/js-runner.mjs"], root, request); + const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "go"), request); + if (same(js, go)) { + passed++; + continue; + } + mismatches++; + const protocol = Object.hasOwn(js, "result") !== Object.hasOwn(go, "result"); + console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "go/tinycolor" })); +} + +console.log(`cases: ${cases.length}`); +console.log(`passed: ${passed}`); +console.log(`mismatches: ${mismatches}`); +process.exitCode = mismatches === 0 ? 0 : 1; diff --git a/go/cmd/tinycolor-compat/main.go b/go/cmd/tinycolor-compat/main.go new file mode 100644 index 00000000..e8f4835b --- /dev/null +++ b/go/cmd/tinycolor-compat/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "bufio" + "fmt" + "os" + + "github.com/rajeet-04/tinycolor-go/internal/compat" + "github.com/rajeet-04/tinycolor-go/tinycolor" +) + +func main() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + request, err := compat.Decode(line) + if err != nil { + write(compat.Response{ID: "", Error: err.Error()}) + continue + } + write(handle(request)) + } +} + +func handle(request compat.Request) compat.Response { + var ( + color tinycolor.Color + err error + ) + switch request.Operation { + case "inspect", "string": + color, err = tinycolor.FromCompat(request.Input, false) + case "fromRatio": + color, err = tinycolor.FromCompat(request.Input, true) + default: + response, _ := compat.Failure(request.ID, "unsupported operation") + return response + } + if err != nil { + response, _ := compat.Failure(request.ID, err.Error()) + return response + } + if request.Operation == "string" { + response, _ := compat.Success(request.ID, color.String()) + return response + } + response, _ := compat.Success(request.ID, color.Inspect()) + return response +} + +func write(response compat.Response) { + encoded, err := compat.Encode(response) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + fmt.Println(string(encoded)) +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 00000000..fe59f263 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,3 @@ +module github.com/rajeet-04/tinycolor-go + +go 1.26 diff --git a/go/internal/compat/protocol.go b/go/internal/compat/protocol.go new file mode 100644 index 00000000..52ff4fc7 --- /dev/null +++ b/go/internal/compat/protocol.go @@ -0,0 +1,54 @@ +package compat + +import ( + "encoding/json" + "fmt" +) + +type Request struct { + ID string `json:"id"` + Operation string `json:"operation"` + Input any `json:"input"` + Args any `json:"args,omitempty"` +} + +type Response struct { + ID string `json:"id"` + Result any `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +func Success(id string, result any) (Response, error) { + response := Response{ID: id, Result: result} + return response, response.Validate() +} + +func Failure(id, message string) (Response, error) { + response := Response{ID: id, Error: message} + return response, response.Validate() +} + +func (r Response) Validate() error { + if (r.Result == nil) == (r.Error == "") { + return fmt.Errorf("response must contain exactly one of result or error") + } + return nil +} + +func Decode(line []byte) (Request, error) { + var request Request + if err := json.Unmarshal(line, &request); err != nil { + return Request{}, fmt.Errorf("malformed JSON") + } + if request.ID == "" || request.Operation == "" { + return Request{}, fmt.Errorf("id and operation are required") + } + return request, nil +} + +func Encode(response Response) ([]byte, error) { + if err := response.Validate(); err != nil { + return nil, err + } + return json.Marshal(response) +} diff --git a/go/internal/compat/protocol_test.go b/go/internal/compat/protocol_test.go new file mode 100644 index 00000000..336d1921 --- /dev/null +++ b/go/internal/compat/protocol_test.go @@ -0,0 +1,18 @@ +package compat + +import "testing" + +func TestResponseHasExactlyOnePayload(t *testing.T) { + if _, err := Success("one", map[string]any{"ok": true}); err != nil { + t.Fatal(err) + } + if _, err := Failure("one", "bad request"); err != nil { + t.Fatal(err) + } + if err := (Response{ID: "one"}).Validate(); err == nil { + t.Fatal("expected neither payload error") + } + if err := (Response{ID: "one", Result: map[string]any{}, Error: "bad"}).Validate(); err == nil { + t.Fatal("expected both payloads error") + } +} diff --git a/go/tinycolor/color.go b/go/tinycolor/color.go new file mode 100644 index 00000000..70b3bf5d --- /dev/null +++ b/go/tinycolor/color.go @@ -0,0 +1,74 @@ +package tinycolor + +import ( + "fmt" + "math" +) + +// Color is the Phase 1 compatibility value. Phase 2 replaces its fixed input +// decoder with TinyColor's full parser while retaining these observable fields. +type Color struct { + r, g, b int + a float64 + valid bool + format string + value string +} + +func FromCompat(input any, fromRatio bool) (Color, error) { + if fromRatio { + if rgb, ok := input.(map[string]any); ok && number(rgb["r"]) == 1 && number(rgb["g"]) == 0 && number(rgb["b"]) == 0 { + return Color{r: 255, a: 1, valid: true, format: "prgb", value: "rgb(100%, 0%, 0%)"}, nil + } + return Color{}, fmt.Errorf("unsupported input") + } + + switch value := input.(type) { + case string: + switch value { + case "red": + return Color{r: 255, a: 1, valid: true, format: "name", value: "red"}, nil + case "#000": + return Color{a: 1, valid: true, format: "hex", value: "#000000"}, nil + case "not a color": + return Color{a: 1, value: "#000000"}, nil + case "transparent": + return Color{a: 0, valid: true, format: "name", value: "transparent"}, nil + case "rgba(255, 0, 0, .5)": + return Color{r: 255, a: .5, valid: true, format: "rgb", value: "rgba(255, 0, 0, 0.5)"}, nil + case "hsl(0, 100%, 50%)": + return Color{r: 255, a: 1, valid: true, format: "hsl", value: "hsl(0, 100%, 50%)"}, nil + case "hsv(0, 100%, 100%)": + return Color{r: 255, a: 1, valid: true, format: "hsv", value: "hsv(0, 100%, 100%)"}, nil + } + case map[string]any: + if number(value["r"]) == 255 && number(value["g"]) == 0 && number(value["b"]) == 0 { + return Color{r: 255, a: 1, valid: true, format: "rgb", value: "rgb(255, 0, 0)"}, nil + } + } + return Color{}, fmt.Errorf("unsupported input") +} + +func (c Color) Valid() bool { return c.valid } +func (c Color) Format() string { return c.format } +func (c Color) Alpha() float64 { return c.a } +func (c Color) String() string { return c.value } +func (c Color) RGB() map[string]any { + return map[string]any{"r": c.r, "g": c.g, "b": c.b, "a": c.a} +} + +func (c Color) Inspect() map[string]any { + format := any(c.format) + if c.format == "" { + format = false + } + return map[string]any{"valid": c.valid, "format": format, "alpha": c.a, "rgb": c.RGB(), "value": c.String()} +} + +func number(value any) float64 { + number, ok := value.(float64) + if !ok { + return math.NaN() + } + return number +} diff --git a/go/tinycolor/color_test.go b/go/tinycolor/color_test.go new file mode 100644 index 00000000..7819420d --- /dev/null +++ b/go/tinycolor/color_test.go @@ -0,0 +1,43 @@ +package tinycolor + +import "testing" + +func TestPhaseOneInputsMatchTinyColor(t *testing.T) { + tests := []struct { + name string + input any + ratio bool + valid bool + format string + alpha float64 + value string + }{ + {"name", "red", false, true, "name", 1, "red"}, + {"hex", "#000", false, true, "hex", 1, "#000000"}, + {"invalid", "not a color", false, false, "", 1, "#000000"}, + {"transparent", "transparent", false, true, "name", 0, "transparent"}, + {"rgba", "rgba(255, 0, 0, .5)", false, true, "rgb", .5, "rgba(255, 0, 0, 0.5)"}, + {"hsl", "hsl(0, 100%, 50%)", false, true, "hsl", 1, "hsl(0, 100%, 50%)"}, + {"hsv", "hsv(0, 100%, 100%)", false, true, "hsv", 1, "hsv(0, 100%, 100%)"}, + {"rgb object", map[string]any{"r": float64(255), "g": float64(0), "b": float64(0)}, false, true, "rgb", 1, "rgb(255, 0, 0)"}, + {"from ratio", map[string]any{"r": float64(1), "g": float64(0), "b": float64(0)}, true, true, "prgb", 1, "rgb(100%, 0%, 0%)"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + color, err := FromCompat(tt.input, tt.ratio) + if err != nil { + t.Fatal(err) + } + if color.Valid() != tt.valid || color.Format() != tt.format || color.Alpha() != tt.alpha || color.String() != tt.value { + t.Fatalf("got valid=%v format=%q alpha=%v string=%q", color.Valid(), color.Format(), color.Alpha(), color.String()) + } + }) + } +} + +func TestPhaseOneRejectsUnsupportedInput(t *testing.T) { + if _, err := FromCompat("blue", false); err == nil { + t.Fatal("expected unsupported input error") + } +} From 1e15da6c4a4060593923a34f7728f55f10c928b3 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 01:20:26 +0530 Subject: [PATCH 004/165] docs: plan Phase 2 parser parity --- .planning/ROADMAP.md | 16 ++-- .planning/STATE.md | 7 +- .../02-parsing-and-color-state/02-01-PLAN.md | 90 +++++++++++++++++++ .../02-parsing-and-color-state/02-02-PLAN.md | 82 +++++++++++++++++ .../02-parsing-and-color-state/02-CONTEXT.md | 72 +++++++++++++++ .../02-parsing-and-color-state/02-RESEARCH.md | 34 +++++++ .../02-VALIDATION.md | 38 ++++++++ 7 files changed, 331 insertions(+), 8 deletions(-) create mode 100644 .planning/phases/02-parsing-and-color-state/02-01-PLAN.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-02-PLAN.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-CONTEXT.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-RESEARCH.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-VALIDATION.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 45bf6361..da09cbea 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -8,7 +8,7 @@ through parsing and observable output, to operations and delivery proof. ## Phases -- [ ] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. +- [x] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. - [ ] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. - [ ] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. - [ ] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. @@ -24,10 +24,10 @@ through parsing and observable output, to operations and delivery proof. 1. `go/` builds as an independent module without changing JavaScript source files. 2. Node and Go runners accept and emit the documented JSONL request/response protocol. 3. A differential command reports case count, mismatch count, and complete mismatch records. -**Plans**: 1 plan +**Plans**: 1 plan (complete 2026-08-01) Plans: -- [ ] 01-01: Create the Go module, JSONL runners, smoke corpus, and baseline report. +- [x] 01-01: Create the Go module, JSONL runners, smoke corpus, and baseline report. ### Phase 2: Parsing and Color State **Goal**: Go accepts the same supported TinyColor inputs and retains the same validity, format, alpha, and normalized color state. @@ -37,7 +37,11 @@ Plans: 1. Hex, RGB(A), HSL(A), HSV(A), CSS names, transparent, and object inputs produce matching state. 2. Permissive syntax, boundary values, invalid input, and alpha normalization match the oracle corpus. 3. Every discovered parser mismatch has a deterministic regression record. -**Plans**: TBD +**Plans**: 2 plans + +Plans: +- [ ] 02-01: Replace Phase 1 fixed inputs with normalized color/model and HEX/RGB/name parsing. +- [ ] 02-02: Add HSL/HSV/object parsing and parser differential corpus. ### Phase 3: Conversion and Representation **Goal**: Go exposes TinyColor-equivalent conversion, string formatting, format fallback, and color analysis. @@ -73,8 +77,8 @@ Plans: | Phase | Plans Complete | Status | Completed | |---|---|---|---| -| 1. Foundation and Oracle | 0/1 | Not started | - | -| 2. Parsing and Color State | 0/TBD | Not started | - | +| 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | +| 2. Parsing and Color State | 0/2 | Not started | - | | 3. Conversion and Representation | 0/TBD | Not started | - | | 4. Operations and Palettes | 0/TBD | Not started | - | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 58b23418..b10aced1 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -6,5 +6,8 @@ - JavaScript source and tests are the immutable oracle. - Node 24.18.0 and Go 1.26.1 are available locally. - `deno` is not installed; no original-source test pass is claimed. -- No Go implementation or compatibility result exists yet. - +- Phase 1 completed in commit `78423e3`: Go/Node JSONL runners, protocol tests, + and a fixed corpus passed 9/9 with zero mismatches. This is not whole-library + parity. +- Phase 2 is planned next; it replaces the fixed Phase 1 input decoder with the + complete TinyColor parser while preserving the compatibility protocol. diff --git a/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md new file mode 100644 index 00000000..f97563de --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md @@ -0,0 +1,90 @@ +--- +phase: 2 +plan: 1 +type: execute +subsystem: parser-model-rgb +tags: [go, parser, tinycolor, differential-testing] +wave: 1 +depends_on: [] +files_modified: + - go/internal/color/model.go + - go/internal/color/bounds.go + - go/internal/color/model_test.go + - go/internal/parser/parser.go + - go/internal/parser/hex.go + - go/internal/parser/rgb.go + - go/internal/parser/names.go + - go/internal/parser/parser_test.go + - go/tinycolor/color.go + - compat/cases/parser-hex-rgb.jsonl + - COMPATIBILITY.md +autonomous: true +requirements: [PAR-01, PAR-02, QLT-02] +must_haves: + truths: + - "HEX, RGB(A), named colors, transparent, invalid input, and RGB objects match local TinyColor state." + - "Phase 1 JSONL operations remain supported without hard-coded color inputs." + artifacts: + - path: "go/internal/color/model.go" + provides: "Normalized RGBA, validity, and format state" + - path: "go/internal/parser/parser.go" + provides: "Single string/object parser entrypoint" + - path: "compat/cases/parser-hex-rgb.jsonl" + provides: "Exact source-derived parser corpus" +--- + + +Replace the fixed Phase 1 color decoder with the shared normalized model and +the first complete parser slice: bounds, HEX, RGB(A), names, transparent, +invalid input, and RGB objects. + + + + +Create the normalized color model and JavaScript-equivalent bounds +go/internal/color/model.go, go/internal/color/bounds.go, go/internal/color/model_test.go + +- `mod.js` lines 359-442 and 1047-1128 +- `test.js` lines 274-443 and 698-746 +- `go/tinycolor/color.go` + + +Define one private normalized RGBA model with float RGB channels, alpha, +validity, a typed format that can represent source false distinctly from known +format strings, and original input metadata. Preserve source original-input +rules: null and empty become `""`; string case is retained; object input remains +JSON-structurally equal; and FromRatio retains its transformed object. Port `bound01`, `boundAlpha`, percentage detection, +`1.0` handling, clamping, modulo, and near-maximum rounding from `mod.js`. +Write table tests for negative/overflow values, `1`, string `1.0`, percentages, +invalid alpha, alpha zero, the source invalid-black state, lowercase/uppercase/ +mixed strings, objects, empty strings, and null. + +Set-Location go; go test ./internal/color; go vet ./... +All normalized state and bounds tests pass with explicit source-derived boundary values. + + + +Parse HEX, RGB(A), names, transparent, and RGB objects through the shared model +go/internal/parser/parser.go, go/internal/parser/hex.go, go/internal/parser/rgb.go, go/internal/parser/names.go, go/internal/parser/parser_test.go, go/tinycolor/color.go, compat/cases/parser-hex-rgb.jsonl, COMPATIBILITY.md + +- `mod.js` lines 359-442 and 1129-1260 +- `test.js` lines 316-443, 487-697, and 698-746 +- `compat/js-runner.mjs` +- `compat/cases/smoke.jsonl` + + +Implement one parser entrypoint used by `go/tinycolor`; remove fixed-input +switches from the public facade. Accept optional-`#` 3/4/6/8 hex, case-insensitive +trimmed names, transparent, and source-permissive RGB/RGBA fields. Generate or +commit the full local named-color map as Go data derived from `mod.js`. Accept +RGB object fields using the same bounds and alpha rules. Preserve `name`, `hex`, +`hex8`, `rgb`, and `prgb` formats. Add source-derived JSONL cases for every +syntax family, name aliases, invalid strings/objects, percentage RGB, and +alpha boundaries, original strings/objects/null/empty values; retain all Phase 1 smoke cases unchanged. +Extend both JSONL `inspect` results with an `original` field: Node uses +`getOriginalInput()` and Go serializes the model's JSON-safe original value. + +Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/smoke.jsonl +HEX/RGB/name/object differential corpus and the unchanged Phase 1 smoke corpus exit 0 with no mismatches. + + diff --git a/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md new file mode 100644 index 00000000..e3f4981f --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md @@ -0,0 +1,82 @@ +--- +phase: 2 +plan: 2 +type: execute +subsystem: parser-hsl-hsv +tags: [go, parser, hsl, hsv, differential-testing] +wave: 2 +depends_on: [02-01] +files_modified: + - go/internal/parser/hsl.go + - go/internal/parser/hsv.go + - go/internal/parser/object.go + - go/internal/parser/parser_test.go + - go/tinycolor/color.go + - compat/cases/parser.jsonl + - COMPATIBILITY.md +autonomous: true +requirements: [PAR-01, PAR-02, QLT-02] +must_haves: + truths: + - "HSL(A), HSV(A), HSL/HSV objects, and FromRatio produce source-equivalent normalized state." + - "The complete Phase 2 parser corpus and Phase 1 smoke corpus report zero mismatches." + artifacts: + - path: "go/internal/parser/hsl.go" + provides: "HSL(A) parser path to normalized RGB" + - path: "go/internal/parser/hsv.go" + provides: "HSV(A) parser path to normalized RGB" + - path: "compat/cases/parser.jsonl" + provides: "Full Phase 2 parser differential corpus" +--- + + +Finish the input surface by porting HSL/HSV conversion used during parsing, +object precedence, FromRatio transformation, and the full Phase 2 parser corpus. + + + + +Parse HSL(A), HSV(A), typed objects, and FromRatio using source bounds +go/internal/parser/hsl.go, go/internal/parser/hsv.go, go/internal/parser/object.go, go/internal/parser/parser_test.go, go/tinycolor/color.go + +- `mod.js` lines 359-572 and 1129-1260 +- `test.js` lines 274-315, 444-515, 945-1096 +- `go/internal/color/bounds.go` +- `go/internal/parser/parser.go` + + +Port only the RGB conversion math required to normalize parsed HSL/HSV values; +keep output conversion APIs for Phase 3. Match source object precedence RGB, +then HSV, then HSL. Accept CSS-unit HSL/HSV object fields, the permissive +functional syntax, hue wrapping, saturation/lightness/value ratios, and alpha +normalization. Implement FromRatio by converting every non-alpha object field +through the source percentage transformation before parser dispatch. Add exact +tests for source HSL/HSV examples, decimals, percentages, wrapped hues, alpha, +and malformed components. + +Set-Location go; go test ./internal/parser ./tinycolor; go vet ./... +Focused HSL/HSV, object, and FromRatio tests pass and no parser path bypasses the normalized model. + + + +Publish the complete parser differential corpus and compatibility evidence +compat/cases/parser.jsonl, COMPATIBILITY.md + +- `test.js` lines 274-747 and 945-1096 +- `compat/cases/parser-hex-rgb.jsonl` +- `compat/run.mjs` +- `COMPATIBILITY.md` + + +Create `compat/cases/parser.jsonl` from source cases covering ratios, RGB, +percentage RGB, HSL(A), HSV(A), all hex lengths, names, transparent, invalid +input, object precedence, alpha values, whitespace/case variants, and boundary +values. Run it through the unchanged Node/Go runner; record exact case count, +command, and result in `COMPATIBILITY.md`. Every mismatch must retain the +runner's input/JS/Go/owner/suspected-package record until fixed. + +Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +The full parser corpus and Phase 1 smoke corpus exit 0; compatibility evidence names the corpus count and no oracle files changed. + + + diff --git a/.planning/phases/02-parsing-and-color-state/02-CONTEXT.md b/.planning/phases/02-parsing-and-color-state/02-CONTEXT.md new file mode 100644 index 00000000..f06371a3 --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-CONTEXT.md @@ -0,0 +1,72 @@ +# Phase 2: Parsing and Color State - Context + +**Gathered:** 2026-08-01 +**Status:** Ready for planning + + +## Phase Boundary + +Replace the fixed Phase 1 decoder with the complete input behavior in local +`mod.js`: strings, object input, alpha, source format, validity, and normalized +RGBA state. Output formatting beyond the minimal compatibility snapshot belongs +to Phase 3. + + + +## Implementation Decisions + +### Ownership and boundaries +- @rajeet-04 owns `go/internal/color` and `go/internal/parser` for this phase. +- `go/tinycolor` remains a thin public facade over the internal model; do not + move formatting, readability, mutations, or palettes into Phase 2. + +### Source equivalence +- `mod.js` is authoritative for `inputToRGB`, `bound01`, `boundAlpha`, + `isValidCSSUnit`, and `stringInputToObject`. +- Preserve source format values: `rgb`, `prgb`, `hsl`, `hsv`, `hex`, `hex8`, + `name`, and invalid `false` (represented internally without an empty-string + ambiguity). +- Invalid input is a valid construction result with `Valid=false`, black RGB, + alpha 1, and no format; it is not a Go parsing error. + +### Supported inputs +- Strings: case-insensitive/trimming names, transparent, 3/4/6/8 hex with + optional `#`, and the permissive RGB(A)/HSL(A)/HSV(A) grammar from source. +- Objects: RGB, HSL, HSV, optional alpha, CSS-unit values, and `FromRatio`. +- Preserve alpha normalization: malformed, negative, or >1 alpha becomes 1; + numeric zero remains zero. + +### Compatibility evidence +- Use generated JSONL corpus records derived from `test.js`; no edits to the + upstream test file. +- Each mismatch includes input, operation, source result, Go result, owner, and + suspected package. No global float tolerance. + +### the agent's Discretion +- Exact Go private type names and parser helper decomposition. +- Whether name data is generated from `mod.js` during development or checked in + as generated Go data, provided the committed table comes from this checkout. + + + +## Canonical References + +### Upstream behavior +- `mod.js` lines 359-654 — input-to-RGB and conversion paths invoked by parsing. +- `mod.js` lines 1047-1260 — named colors, bounds, regex grammar, and string parser. +- `test.js` lines 274-747 — ratio, RGB, HSL, HEX, HSV, invalid, name, and alpha tests. + +### Project contracts +- `AGENT.md` — ownership and non-negotiable parity behavior. +- `docs/ARCHITECTURE.md` — parser/model and adapter layers. +- `docs/TESTING.md` — comparison rules. +- `.planning/phases/01-foundation-and-oracle/01-01-PLAN.md` — stable protocol that Phase 2 must keep working. + + + +## Deferred Ideas + +Public output formats, analysis, manipulation, palettes, CLI, CI, benchmarks, +and any syntax not accepted by the local source checkout. + + diff --git a/.planning/phases/02-parsing-and-color-state/02-RESEARCH.md b/.planning/phases/02-parsing-and-color-state/02-RESEARCH.md new file mode 100644 index 00000000..fbf9fb8f --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-RESEARCH.md @@ -0,0 +1,34 @@ +# Phase 2 Research — Parsing and Color State + +## Source facts + +- TinyColor parses with `inputToRGB`: strings first become object-like fields, + then RGB has precedence over HSV, which has precedence over HSL. +- `bound01` treats string `1.0` as `100%`, recognizes `%`, clamps before + scaling, returns exactly 1 near the upper bound, and otherwise uses modulo. +- `boundAlpha` uses JavaScript `parseFloat`; non-numeric, negative, and >1 + values become 1. +- Its CSS unit and functional syntax are intentionally permissive: commas and + parentheses are optional, whitespace can separate components, signs and + decimal values are accepted, and matching is case-insensitive after lowercasing. +- Hex permits `#` optionally at 3, 4, 6, and 8 digits. Four/eight digit alpha + converts from hex to `[0,1]` and receives format `hex8`. +- Named colors are the source `names` map; recognized names are converted to + hex but retain source format `name`. `transparent` is special (`0,0,0,0`). +- Invalid colors remain black with alpha 1 and false validity rather than + returning an exception. + +## Design consequence + +Make a private normalized model the only parser output. The public facade and +compatibility runner should consume that model rather than duplicate bounds, +format decisions, or name handling. The model should retain channel precision; +observable rounding belongs to Phase 3. + +## Validation architecture + +Run focused Go unit tests for model/bounds/parser and differential JSONL cases +through the existing Node runner. Use exact rendered/JSON values. Test parser +categories separately so a mismatch names `go/internal/parser` or +`go/internal/color`, not a vague package. + diff --git a/.planning/phases/02-parsing-and-color-state/02-VALIDATION.md b/.planning/phases/02-parsing-and-color-state/02-VALIDATION.md new file mode 100644 index 00000000..fb059c18 --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-VALIDATION.md @@ -0,0 +1,38 @@ +--- +phase: 2 +slug: parsing-and-color-state +status: draft +nyquist_compliant: true +wave_0_complete: false +created: 2026-08-01 +--- + +# Phase 2 — Validation Strategy + +| Property | Value | +|---|---| +| Framework | Go standard `testing` plus Node 24.18.0 | +| Quick run | `go test ./internal/color ./internal/parser` from `go/` | +| Full run | `go test ./...; node compat/run.mjs compat/cases/parser.jsonl` | +| Static checks | `go vet ./...` and no output from `gofmt -d` | + +## Sampling rate + +- After each parser/model task: focused Go tests and `go vet ./...`. +- After Plan 02-01: HEX/RGB/name differential corpus. +- After Plan 02-02: full parser corpus and Phase 1 smoke corpus. + +## Verification map + +| Task | Plan | Requirement | Automated proof | +|---|---|---|---| +| 02-01-01 | 02-01 | PAR-02 | `go test ./internal/color` | +| 02-01-02 | 02-01 | PAR-01 | `node compat/run.mjs compat/cases/parser-hex-rgb.jsonl` | +| 02-02-01 | 02-02 | PAR-01, PAR-02 | `go test ./internal/parser` | +| 02-02-02 | 02-02 | QLT-02 | `node compat/run.mjs compat/cases/parser.jsonl` | + +## Manual-only check + +When Deno is available, run `deno task test`; until then it remains explicitly +unverified and does not substitute for Node differential evidence. + From 2df5fa870d743d3df5dc71cdcce6e56005d495ed Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 01:40:28 +0530 Subject: [PATCH 005/165] feat: complete Phase 2 parser parity --- .gitignore | 3 +- .planning/ROADMAP.md | 10 +- .planning/STATE.md | 7 +- .../02-parsing-and-color-state/02-01-PLAN.md | 28 +-- .../02-parsing-and-color-state/02-02-PLAN.md | 25 ++- .port-mortem.toml | 4 + AGENT.md | 18 +- COMPATIBILITY.md | 33 +++- DECISIONS.md | 3 +- Dockerfile | 10 + Makefile | 10 + PLAN.md | 2 +- PORT.md | 12 ++ bench/methodology.md | 5 + bench/results.json | 4 + compat/cases/parser-hex-rgb.jsonl | 26 +++ compat/cases/parser.jsonl | 23 +++ compat/js-runner.mjs | 1 + compat/run.mjs | 4 +- docs/ARCHITECTURE.md | 9 +- docs/TESTING.md | 3 +- fuzz/README.md | 4 + go/tinycolor/color.go | 74 -------- {go => src}/cmd/tinycolor-compat/main.go | 0 {go => src}/go.mod | 0 src/internal/color/bounds.go | 77 ++++++++ src/internal/color/model.go | 53 ++++++ src/internal/color/model_test.go | 63 ++++++ {go => src}/internal/compat/protocol.go | 0 {go => src}/internal/compat/protocol_test.go | 0 src/internal/parser/hex.go | 41 ++++ src/internal/parser/hsl.go | 13 ++ src/internal/parser/hsv.go | 13 ++ src/internal/parser/names.go | 43 +++++ src/internal/parser/object.go | 22 +++ src/internal/parser/parser.go | 179 ++++++++++++++++++ src/internal/parser/parser_test.go | 75 ++++++++ src/internal/parser/rgb.go | 56 ++++++ src/tinycolor/color.go | 128 +++++++++++++ {go => src}/tinycolor/color_test.go | 7 +- tests/original/README.md | 5 + tests/original/manifest.sha256 | 3 + tests/port/README.md | 4 + .../port/adapter.test.mjs | 1 + 44 files changed, 963 insertions(+), 138 deletions(-) create mode 100644 .port-mortem.toml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 PORT.md create mode 100644 bench/methodology.md create mode 100644 bench/results.json create mode 100644 compat/cases/parser-hex-rgb.jsonl create mode 100644 compat/cases/parser.jsonl create mode 100644 fuzz/README.md delete mode 100644 go/tinycolor/color.go rename {go => src}/cmd/tinycolor-compat/main.go (100%) rename {go => src}/go.mod (100%) create mode 100644 src/internal/color/bounds.go create mode 100644 src/internal/color/model.go create mode 100644 src/internal/color/model_test.go rename {go => src}/internal/compat/protocol.go (100%) rename {go => src}/internal/compat/protocol_test.go (100%) create mode 100644 src/internal/parser/hex.go create mode 100644 src/internal/parser/hsl.go create mode 100644 src/internal/parser/hsv.go create mode 100644 src/internal/parser/names.go create mode 100644 src/internal/parser/object.go create mode 100644 src/internal/parser/parser.go create mode 100644 src/internal/parser/parser_test.go create mode 100644 src/internal/parser/rgb.go create mode 100644 src/tinycolor/color.go rename {go => src}/tinycolor/color_test.go (86%) create mode 100644 tests/original/README.md create mode 100644 tests/original/manifest.sha256 create mode 100644 tests/port/README.md rename compat/js-runner.test.mjs => tests/port/adapter.test.mjs (97%) diff --git a/.gitignore b/.gitignore index 1c08f4da..24a07a3e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules .vscode cov_profile cov_profile.lcov -.cache/ \ No newline at end of file +.cache/ +bin/ diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index da09cbea..ed3c6d69 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -21,7 +21,7 @@ through parsing and observable output, to operations and delivery proof. **Depends on**: Nothing (first phase) **Requirements**: [EQV-01, QLT-01, QLT-03] **Success Criteria**: - 1. `go/` builds as an independent module without changing JavaScript source files. + 1. `src/` builds as an independent module without changing JavaScript source files. 2. Node and Go runners accept and emit the documented JSONL request/response protocol. 3. A differential command reports case count, mismatch count, and complete mismatch records. **Plans**: 1 plan (complete 2026-08-01) @@ -37,11 +37,11 @@ Plans: 1. Hex, RGB(A), HSL(A), HSV(A), CSS names, transparent, and object inputs produce matching state. 2. Permissive syntax, boundary values, invalid input, and alpha normalization match the oracle corpus. 3. Every discovered parser mismatch has a deterministic regression record. -**Plans**: 2 plans +**Plans**: 2 plans (complete 2026-08-01) Plans: -- [ ] 02-01: Replace Phase 1 fixed inputs with normalized color/model and HEX/RGB/name parsing. -- [ ] 02-02: Add HSL/HSV/object parsing and parser differential corpus. +- [x] 02-01: Replace Phase 1 fixed inputs with normalized color/model and HEX/RGB/name parsing. +- [x] 02-02: Add HSL/HSV/object parsing and parser differential corpus. ### Phase 3: Conversion and Representation **Goal**: Go exposes TinyColor-equivalent conversion, string formatting, format fallback, and color analysis. @@ -78,7 +78,7 @@ Plans: | Phase | Plans Complete | Status | Completed | |---|---|---|---| | 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | -| 2. Parsing and Color State | 0/2 | Not started | - | +| 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | | 3. Conversion and Representation | 0/TBD | Not started | - | | 4. Operations and Palettes | 0/TBD | Not started | - | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index b10aced1..e28e1a16 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -9,5 +9,8 @@ - Phase 1 completed in commit `78423e3`: Go/Node JSONL runners, protocol tests, and a fixed corpus passed 9/9 with zero mismatches. This is not whole-library parity. -- Phase 2 is planned next; it replaces the fixed Phase 1 input decoder with the - complete TinyColor parser while preserving the compatibility protocol. +- Phase 2 completed locally: the normalized parser covers HEX, RGB(A), HSL(A), + HSV(A), names, objects, invalid input, and FromRatio. Differential evidence: + 26/26 HEX/RGB/name cases, 23/23 complete parser cases, and 9/9 smoke cases. +- The Go module lives in `src/`; `tests/original/manifest.sha256` pins the + unmodified root JavaScript oracle. diff --git a/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md index f97563de..3910cd67 100644 --- a/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md +++ b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md @@ -7,15 +7,15 @@ tags: [go, parser, tinycolor, differential-testing] wave: 1 depends_on: [] files_modified: - - go/internal/color/model.go - - go/internal/color/bounds.go - - go/internal/color/model_test.go - - go/internal/parser/parser.go - - go/internal/parser/hex.go - - go/internal/parser/rgb.go - - go/internal/parser/names.go - - go/internal/parser/parser_test.go - - go/tinycolor/color.go + - src/internal/color/model.go + - src/internal/color/bounds.go + - src/internal/color/model_test.go + - src/internal/parser/parser.go + - src/internal/parser/hex.go + - src/internal/parser/rgb.go + - src/internal/parser/names.go + - src/internal/parser/parser_test.go + - src/tinycolor/color.go - compat/cases/parser-hex-rgb.jsonl - COMPATIBILITY.md autonomous: true @@ -42,11 +42,11 @@ invalid input, and RGB objects. Create the normalized color model and JavaScript-equivalent bounds -go/internal/color/model.go, go/internal/color/bounds.go, go/internal/color/model_test.go +src/internal/color/model.go, src/internal/color/bounds.go, src/internal/color/model_test.go - `mod.js` lines 359-442 and 1047-1128 - `test.js` lines 274-443 and 698-746 -- `go/tinycolor/color.go` +- `src/tinycolor/color.go` Define one private normalized RGBA model with float RGB channels, alpha, @@ -59,13 +59,13 @@ Write table tests for negative/overflow values, `1`, string `1.0`, percentages, invalid alpha, alpha zero, the source invalid-black state, lowercase/uppercase/ mixed strings, objects, empty strings, and null. -Set-Location go; go test ./internal/color; go vet ./... +Set-Location src; go test ./internal/color; go vet ./... All normalized state and bounds tests pass with explicit source-derived boundary values. Parse HEX, RGB(A), names, transparent, and RGB objects through the shared model -go/internal/parser/parser.go, go/internal/parser/hex.go, go/internal/parser/rgb.go, go/internal/parser/names.go, go/internal/parser/parser_test.go, go/tinycolor/color.go, compat/cases/parser-hex-rgb.jsonl, COMPATIBILITY.md +src/internal/parser/parser.go, src/internal/parser/hex.go, src/internal/parser/rgb.go, src/internal/parser/names.go, src/internal/parser/parser_test.go, src/tinycolor/color.go, compat/cases/parser-hex-rgb.jsonl, COMPATIBILITY.md - `mod.js` lines 359-442 and 1129-1260 - `test.js` lines 316-443, 487-697, and 698-746 @@ -84,7 +84,7 @@ alpha boundaries, original strings/objects/null/empty values; retain all Phase 1 Extend both JSONL `inspect` results with an `original` field: Node uses `getOriginalInput()` and Go serializes the model's JSON-safe original value. -Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/smoke.jsonl +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/smoke.jsonl HEX/RGB/name/object differential corpus and the unchanged Phase 1 smoke corpus exit 0 with no mismatches. diff --git a/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md index e3f4981f..4d956f94 100644 --- a/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md +++ b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md @@ -7,11 +7,11 @@ tags: [go, parser, hsl, hsv, differential-testing] wave: 2 depends_on: [02-01] files_modified: - - go/internal/parser/hsl.go - - go/internal/parser/hsv.go - - go/internal/parser/object.go - - go/internal/parser/parser_test.go - - go/tinycolor/color.go + - src/internal/parser/hsl.go + - src/internal/parser/hsv.go + - src/internal/parser/object.go + - src/internal/parser/parser_test.go + - src/tinycolor/color.go - compat/cases/parser.jsonl - COMPATIBILITY.md autonomous: true @@ -21,9 +21,9 @@ must_haves: - "HSL(A), HSV(A), HSL/HSV objects, and FromRatio produce source-equivalent normalized state." - "The complete Phase 2 parser corpus and Phase 1 smoke corpus report zero mismatches." artifacts: - - path: "go/internal/parser/hsl.go" + - path: "src/internal/parser/hsl.go" provides: "HSL(A) parser path to normalized RGB" - - path: "go/internal/parser/hsv.go" + - path: "src/internal/parser/hsv.go" provides: "HSV(A) parser path to normalized RGB" - path: "compat/cases/parser.jsonl" provides: "Full Phase 2 parser differential corpus" @@ -37,12 +37,12 @@ object precedence, FromRatio transformation, and the full Phase 2 parser corpus. Parse HSL(A), HSV(A), typed objects, and FromRatio using source bounds -go/internal/parser/hsl.go, go/internal/parser/hsv.go, go/internal/parser/object.go, go/internal/parser/parser_test.go, go/tinycolor/color.go +src/internal/parser/hsl.go, src/internal/parser/hsv.go, src/internal/parser/object.go, src/internal/parser/parser_test.go, src/tinycolor/color.go - `mod.js` lines 359-572 and 1129-1260 - `test.js` lines 274-315, 444-515, 945-1096 -- `go/internal/color/bounds.go` -- `go/internal/parser/parser.go` +- `src/internal/color/bounds.go` +- `src/internal/parser/parser.go` Port only the RGB conversion math required to normalize parsed HSL/HSV values; @@ -54,7 +54,7 @@ through the source percentage transformation before parser dispatch. Add exact tests for source HSL/HSV examples, decimals, percentages, wrapped hues, alpha, and malformed components. -Set-Location go; go test ./internal/parser ./tinycolor; go vet ./... +Set-Location src; go test ./internal/parser ./tinycolor; go vet ./... Focused HSL/HSV, object, and FromRatio tests pass and no parser path bypasses the normalized model. @@ -75,8 +75,7 @@ values. Run it through the unchanged Node/Go runner; record exact case count, command, and result in `COMPATIBILITY.md`. Every mismatch must retain the runner's input/JS/Go/owner/suspected-package record until fixed. -Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js The full parser corpus and Phase 1 smoke corpus exit 0; compatibility evidence names the corpus count and no oracle files changed. - diff --git a/.port-mortem.toml b/.port-mortem.toml new file mode 100644 index 00000000..e2f31c43 --- /dev/null +++ b/.port-mortem.toml @@ -0,0 +1,4 @@ +track = "H" +source_url = "https://github.com/bgrins/TinyColor.git" +kickoff_commit = "b49018c9f2dbca313d80d7a4dad25e26143cfe01" +oracle_manifest = "tests/original/manifest.sha256" diff --git a/AGENT.md b/AGENT.md index 02157a1d..63e35f31 100644 --- a/AGENT.md +++ b/AGENT.md @@ -12,9 +12,12 @@ compatibility project: observable TinyColor behavior is the specification. - `mod.js`, `tinycolor.js`, `test.js`, `npm/`, `dist/`, and `demo/` are the immutable JavaScript oracle. Do not edit them except for a separately agreed upstream maintenance change. -- New Go work belongs under `go/`; `go/go.mod` is the port module boundary. +- New Go work belongs under `src/`; `src/go.mod` is the port module boundary. - Oracle adapters, generated fixtures, and mismatch reports belong under `compat/`. Do not rewrite the original test suite to make it pass. +- `tests/original/manifest.sha256` pins the immutable root oracle. Port-owned + adapter tests live in `tests/port/`; differential fuzzing and benchmarks live + in `fuzz/` and `bench/` respectively. - Human-facing project docs belong in `docs/`; compatibility status belongs in `COMPATIBILITY.md`. @@ -70,9 +73,9 @@ Known differences (or none): | Owner | Primary boundary | Cannot merge without | |---|---|---| -| A — Parsing/model | `go/internal/color`, `go/internal/parser` | parser differential cases | -| B — Conversion/API | `go/tinycolor`, formatting/readability/palettes | deterministic parity tests | -| C — Compatibility/quality | `compat`, `go/testdata`, `COMPATIBILITY.md` | reproducible report | +| A — Parsing/model | `src/internal/color`, `src/internal/parser` | parser differential cases | +| B — Conversion/API | `src/tinycolor`, formatting/readability/palettes | deterministic parity tests | +| C — Compatibility/quality | `compat`, `src/testdata`, `COMPATIBILITY.md` | reproducible report | | D — Delivery | CLI, CI, benchmarks, docs | clean-checkout commands | Coordinate through exported contracts, never by editing another owner’s files @@ -84,10 +87,10 @@ of the implicated module fixes it. Run the narrowest applicable check while developing, then run the phase gate: ```powershell -go test ./... # from go/ +go test ./... # from src/ node compat/js-runner.mjs < cases.json # from repository root -go test -run TestDifferential ./... # from go/ -go vet ./... # from go/ +go test -run TestDifferential ./... # from src/ +go vet ./... # from src/ gofmt -w ``` @@ -100,4 +103,3 @@ passing test result. A feature is done only when its source behavior is mapped, Go unit tests pass, the matching oracle cases pass, the compatibility matrix is updated, and any remaining mismatch is named with input, JavaScript result, Go result, and owner. - diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 5f748040..60c08e28 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -7,10 +7,10 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | Area | Status | Evidence | Notes | |---|---|---|---| | Oracle baseline | Ready | `mod.js`, `test.js`, Node 24.18.0 | JavaScript source is untouched. | -| Node adapter | Passing | `node compat/js-runner.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | -| Go adapter | Passing | `go test ./...` from `go/` | JSONL schema shared with Node adapter. | -| HEX/RGB/name parsing | Planned | Wave 1 | Include invalid and permissive syntax. | -| HSL/HSV parsing | Planned | Wave 1 | Include ratios, percentages, and wrapping. | +| Node adapter | Passing | `node tests/port/adapter.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | +| Go adapter | Passing | `go test ./...` from `src/` | JSONL schema shared with Node adapter. | +| HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | +| HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | | Conversion/formatting | Planned | Wave 2 | Exact strings and rounding. | | Analysis/readability | Planned | Wave 2–3 | Preserve WCAG defaults. | | Manipulation/palettes | Planned | Wave 3 | Preserve defaults and ordering. | @@ -37,9 +37,30 @@ mismatches**: ```powershell $env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -node compat/js-runner.test.mjs -Set-Location go; go test ./...; go vet ./...; Set-Location .. +node tests/port/adapter.test.mjs +Set-Location src; go test ./...; go vet ./...; Set-Location .. node compat/run.mjs compat/cases/smoke.jsonl ``` This is only the fixed Phase 1 corpus; it is not a full TinyColor parity claim. + +## Phase 2 parser slice result + +On 2026-08-01, the HEX/RGB/name parser corpus passed with **26/26 cases** and +**0 mismatches**: + +```powershell +$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' +Set-Location src; go test ./...; go vet ./...; Set-Location .. +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/smoke.jsonl +``` + +This records only the completed parser slice; the full HSL/HSV corpus remains +Phase 2 Plan 02 work. + +## Phase 2 full parser result + +`node compat/run.mjs compat/cases/parser.jsonl` passed with **23/23 cases** and +**0 mismatches** on 2026-08-01. The unchanged Phase 1 smoke corpus also passed +**9/9** with **0 mismatches**. diff --git a/DECISIONS.md b/DECISIONS.md index 7ab9d024..acb5813d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -4,9 +4,8 @@ |---|---|---|---| | D-001 | Port the checked-out `mod.js` behavior to Go. | It is the exact source and test target selected for the project. | Accepted | | D-002 | Keep JavaScript oracle files immutable. | Differential evidence is credible only when the reference does not move with the port. | Accepted | -| D-003 | Put the Go module in `go/`. | It prevents an in-progress port from contaminating the upstream JavaScript package layout. | Accepted | +| D-003 | Put the Go module in `src/`. | It keeps the port idiomatic while following the Track H submission layout; the JavaScript oracle remains at the root. | Accepted | | D-004 | Use JSON Lines adapters over stdin/stdout. | Node and Go can compare dynamic inputs and operations without modifying source tests. | Accepted | | D-005 | Use Go standard library first. | The port's algorithms are small and self-contained; dependencies add compatibility and supply-chain surface. | Accepted | | D-006 | Preserve source quirks at the adapter boundary; expose an idiomatic Go API separately. | Compatibility and Go usability are both required, but they should not leak dynamic JavaScript semantics into every caller. | Accepted | | D-007 | Deno's source suite is a required later check, not a current pass. | `deno` is not installed in the checked environment as of 2026-08-01. | Accepted | - diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7831a508 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.26 AS build +WORKDIR /app/src +COPY src/go.mod ./ +RUN go mod download +COPY src/ ./ +RUN go build -o /tinycolor-compat ./cmd/tinycolor-compat + +FROM scratch +COPY --from=build /tinycolor-compat /tinycolor-compat +ENTRYPOINT ["/tinycolor-compat"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..8374bfe3 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: build test + +build: + go -C src build -o ../bin/tinycolor-compat ./cmd/tinycolor-compat + +test: + node tests/port/adapter.test.mjs + go -C src test ./... + go -C src vet ./... + node compat/run.mjs compat/cases/smoke.jsonl diff --git a/PLAN.md b/PLAN.md index 82ce2e9d..84fdeb53 100644 --- a/PLAN.md +++ b/PLAN.md @@ -28,7 +28,7 @@ remains intact in the repository. ## Wave 0 — Foundation and behavioral oracle -1. Create `go/` as an independent module; add a minimal public package, a JSON +1. Create `src/` as an independent module; add a minimal public package, a JSON Lines request/response protocol, and a Go runner executable. Implement only the fixed smoke-corpus inputs needed to prove the protocol; all generalized parsing and public API behavior remains in later waves. diff --git a/PORT.md b/PORT.md new file mode 100644 index 00000000..aec69edb --- /dev/null +++ b/PORT.md @@ -0,0 +1,12 @@ +# TinyColor Go port + +This repository ports the behavior of the pinned `bgrins/TinyColor` checkout +to Go. Build the compatibility CLI with one command: + +```powershell +make build +``` + +Run the current parity checks with `make test`. The immutable JavaScript oracle +stays at the root and its kickoff hashes are recorded in +`tests/original/manifest.sha256`. diff --git a/bench/methodology.md b/bench/methodology.md new file mode 100644 index 00000000..29dd6d56 --- /dev/null +++ b/bench/methodology.md @@ -0,0 +1,5 @@ +# Benchmark methodology + +Measure the compiled compatibility CLI on a quiet host with the same corpus for +each run. Record median and p99 latency, RSS, startup time, and throughput. +Do not compare results across different hosts or Go versions. diff --git a/bench/results.json b/bench/results.json new file mode 100644 index 00000000..42e6257d --- /dev/null +++ b/bench/results.json @@ -0,0 +1,4 @@ +{ + "status": "not_run", + "reason": "Benchmarking is scheduled for the delivery phase." +} diff --git a/compat/cases/parser-hex-rgb.jsonl b/compat/cases/parser-hex-rgb.jsonl new file mode 100644 index 00000000..77774e1f --- /dev/null +++ b/compat/cases/parser-hex-rgb.jsonl @@ -0,0 +1,26 @@ +{"id":"hex-3","operation":"inspect","input":"#f00"} +{"id":"hex-3-no-hash","operation":"inspect","input":"F00"} +{"id":"hex-4","operation":"inspect","input":"#f008"} +{"id":"hex-6","operation":"inspect","input":"ff0000"} +{"id":"hex-8","operation":"inspect","input":"#ff000080"} +{"id":"hex-invalid","operation":"inspect","input":"##123456"} +{"id":"rgb-space","operation":"inspect","input":"rgb 255 0 0"} +{"id":"rgb-paren","operation":"inspect","input":"rgb(255, 0, 0)"} +{"id":"rgba-alpha","operation":"inspect","input":"rgba 255 0 0 .5"} +{"id":"rgb-percent","operation":"inspect","input":"rgb 100% 0% 0%"} +{"id":"rgb-percent-object","operation":"inspect","input":{"r":"90%","g":"45%","b":"0%","a":0.4}} +{"id":"rgb-clamped","operation":"inspect","input":"rgb 300 -1 0"} +{"id":"rgb-alpha-invalid","operation":"inspect","input":{"r":255,"g":20,"b":10,"a":-1}} +{"id":"rgb-alpha-zero","operation":"inspect","input":{"r":255,"g":20,"b":10,"a":0}} +{"id":"name-red","operation":"inspect","input":"red"} +{"id":"name-alias-aqua","operation":"inspect","input":"aqua"} +{"id":"name-trim-case","operation":"inspect","input":" InDiAnReD "} +{"id":"name-grey","operation":"inspect","input":"grey"} +{"id":"transparent","operation":"inspect","input":"transparent"} +{"id":"invalid-string","operation":"inspect","input":"this is not a color"} +{"id":"invalid-object","operation":"inspect","input":{"r":"invalid","g":"invalid","b":"invalid"}} +{"id":"original-empty","operation":"inspect","input":""} +{"id":"original-null","operation":"inspect","input":null} +{"id":"original-object","operation":"inspect","input":{"r":255,"g":0,"b":0}} +{"id":"ratio-white","operation":"fromRatio","input":{"r":1,"g":1,"b":1}} +{"id":"ratio-alpha","operation":"fromRatio","input":{"r":1,"g":0,"b":0,"a":0.5}} diff --git a/compat/cases/parser.jsonl b/compat/cases/parser.jsonl new file mode 100644 index 00000000..ad81e62e --- /dev/null +++ b/compat/cases/parser.jsonl @@ -0,0 +1,23 @@ +{"id":"hsl-red","operation":"inspect","input":"hsl(0, 100%, 50%)"} +{"id":"hsl-decimal","operation":"inspect","input":"hsl 251 100 0.38"} +{"id":"hsla-alpha","operation":"inspect","input":"hsla(251,100%,38%,.5)"} +{"id":"hsl-wrap","operation":"inspect","input":"hsl(-109,100%,50%)"} +{"id":"hsv-red","operation":"inspect","input":"hsv(0,100%,100%)"} +{"id":"hsv-decimal","operation":"inspect","input":"hsv 251.1 .887 .918"} +{"id":"hsva-alpha","operation":"inspect","input":"hsva 251.1 .887 .918 .5"} +{"id":"hsv-wrap","operation":"inspect","input":"hsv(720,100%,100%)"} +{"id":"hsl-object","operation":"inspect","input":{"h":251,"s":100,"l":0.38}} +{"id":"hsv-object","operation":"inspect","input":{"h":251.1,"s":0.887,"v":0.918}} +{"id":"object-precedence-rgb","operation":"inspect","input":{"r":255,"g":0,"b":0,"h":120,"s":100,"v":100}} +{"id":"object-precedence-hsv","operation":"inspect","input":{"h":120,"s":100,"v":100,"l":50}} +{"id":"hsl-invalid","operation":"inspect","input":{"h":"invalid","s":"invalid","l":"invalid"}} +{"id":"hsv-invalid","operation":"inspect","input":{"h":"invalid","s":"invalid","v":"invalid"}} +{"id":"ratio-hsl","operation":"fromRatio","input":{"h":0,"s":1,"l":0.5,"a":0.5}} +{"id":"ratio-hsv","operation":"fromRatio","input":{"h":0,"s":1,"v":1,"a":0.5}} +{"id":"ratio-rgb-alpha-invalid","operation":"fromRatio","input":{"r":1,"g":0,"b":0,"a":10}} +{"id":"hex8","operation":"inspect","input":"#ff000080"} +{"id":"rgb-percent","operation":"inspect","input":"rgb 100% 0% 0%"} +{"id":"name-alias","operation":"inspect","input":"aqua"} +{"id":"transparent","operation":"inspect","input":"transparent"} +{"id":"invalid","operation":"inspect","input":"this is not a color"} +{"id":"original-null","operation":"inspect","input":null} diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index 0781d649..648dc195 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -7,6 +7,7 @@ const inspect = (color) => ({ alpha: color.getAlpha(), rgb: color.toRgb(), value: color.toString(), + original: color.getOriginalInput(), }); const respond = (id, result, error) => { diff --git a/compat/run.mjs b/compat/run.mjs index 4f9c778c..510a208c 100644 --- a/compat/run.mjs +++ b/compat/run.mjs @@ -24,14 +24,14 @@ let passed = 0; let mismatches = 0; for (const request of cases) { const js = run(process.execPath, ["compat/js-runner.mjs"], root, request); - const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "go"), request); + const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "src"), request); if (same(js, go)) { passed++; continue; } mismatches++; const protocol = Object.hasOwn(js, "result") !== Object.hasOwn(go, "result"); - console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "go/tinycolor" })); + console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "src/tinycolor" })); } console.log(`cases: ${cases.length}`); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 62afbba7..b7e471b9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,7 +7,7 @@ immutable JS source (mod.js, test.js) │ cases ── differential driver ── Go JSONL runner │ - go/tinycolor public API + src/tinycolor public API │ │ internal/color internal/parser │ │ @@ -19,12 +19,12 @@ cases ── differential driver ── Go JSONL runner 1. **Compatibility boundary (`compat/` and runner):** accepts JSON-safe dynamic input, dispatches named operations, and returns a stable record. It owns no color math. -2. **Parsing/model (`go/internal/...`):** turns strings and typed Go inputs into +2. **Parsing/model (`src/internal/...`):** turns strings and typed Go inputs into normalized RGBA plus validity and source-format metadata. -3. **Public library (`go/tinycolor`):** exposes explicit Go values and methods. +3. **Public library (`src/tinycolor`):** exposes explicit Go values and methods. It owns conversions, formatting, mutation, utilities, readability, and palettes. -4. **CLI (`go/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same +4. **CLI (`src/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same library and does not reimplement parsing or conversion. ## Compatibility protocol @@ -42,4 +42,3 @@ method rounds them. Alpha is stored in `[0,1]`; detected format and validity are separate from RGB channels because invalid TinyColor values still render as black. Original input is adapter metadata: typed Go callers should not need to recover JavaScript object identity. - diff --git a/docs/TESTING.md b/docs/TESTING.md index 53a3e605..8050b8ac 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -4,7 +4,7 @@ | Layer | Purpose | Command when implemented | |---|---|---| -| Go unit tests | Local conversion and parser invariants | `go test ./...` from `go/` | +| Go unit tests | Local conversion and parser invariants | `go test ./...` from `src/` | | Differential corpus | Exact observable parity with local `mod.js` | `node compat/run.mjs ` | | Regression corpus | Permanently reproduce every found mismatch | `node compat/run.mjs compat/cases/regression.jsonl` | | Seeded fuzz cases | Find coercion, bounds, and rounding gaps | documented seed command | @@ -29,4 +29,3 @@ 3. State transitions: clone, set alpha, all instance modifiers, default versus explicit zero amount. 4. Structured random RGB/RGBA/HSL/HSV inputs generated from a fixed seed. - diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..d021a6ca --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,4 @@ +# Differential fuzzing + +Phase 3 adds a seeded generator here. Until then, the checked-in JSONL corpora +in `compat/cases/` are the reproducible differential input set. diff --git a/go/tinycolor/color.go b/go/tinycolor/color.go deleted file mode 100644 index 70b3bf5d..00000000 --- a/go/tinycolor/color.go +++ /dev/null @@ -1,74 +0,0 @@ -package tinycolor - -import ( - "fmt" - "math" -) - -// Color is the Phase 1 compatibility value. Phase 2 replaces its fixed input -// decoder with TinyColor's full parser while retaining these observable fields. -type Color struct { - r, g, b int - a float64 - valid bool - format string - value string -} - -func FromCompat(input any, fromRatio bool) (Color, error) { - if fromRatio { - if rgb, ok := input.(map[string]any); ok && number(rgb["r"]) == 1 && number(rgb["g"]) == 0 && number(rgb["b"]) == 0 { - return Color{r: 255, a: 1, valid: true, format: "prgb", value: "rgb(100%, 0%, 0%)"}, nil - } - return Color{}, fmt.Errorf("unsupported input") - } - - switch value := input.(type) { - case string: - switch value { - case "red": - return Color{r: 255, a: 1, valid: true, format: "name", value: "red"}, nil - case "#000": - return Color{a: 1, valid: true, format: "hex", value: "#000000"}, nil - case "not a color": - return Color{a: 1, value: "#000000"}, nil - case "transparent": - return Color{a: 0, valid: true, format: "name", value: "transparent"}, nil - case "rgba(255, 0, 0, .5)": - return Color{r: 255, a: .5, valid: true, format: "rgb", value: "rgba(255, 0, 0, 0.5)"}, nil - case "hsl(0, 100%, 50%)": - return Color{r: 255, a: 1, valid: true, format: "hsl", value: "hsl(0, 100%, 50%)"}, nil - case "hsv(0, 100%, 100%)": - return Color{r: 255, a: 1, valid: true, format: "hsv", value: "hsv(0, 100%, 100%)"}, nil - } - case map[string]any: - if number(value["r"]) == 255 && number(value["g"]) == 0 && number(value["b"]) == 0 { - return Color{r: 255, a: 1, valid: true, format: "rgb", value: "rgb(255, 0, 0)"}, nil - } - } - return Color{}, fmt.Errorf("unsupported input") -} - -func (c Color) Valid() bool { return c.valid } -func (c Color) Format() string { return c.format } -func (c Color) Alpha() float64 { return c.a } -func (c Color) String() string { return c.value } -func (c Color) RGB() map[string]any { - return map[string]any{"r": c.r, "g": c.g, "b": c.b, "a": c.a} -} - -func (c Color) Inspect() map[string]any { - format := any(c.format) - if c.format == "" { - format = false - } - return map[string]any{"valid": c.valid, "format": format, "alpha": c.a, "rgb": c.RGB(), "value": c.String()} -} - -func number(value any) float64 { - number, ok := value.(float64) - if !ok { - return math.NaN() - } - return number -} diff --git a/go/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go similarity index 100% rename from go/cmd/tinycolor-compat/main.go rename to src/cmd/tinycolor-compat/main.go diff --git a/go/go.mod b/src/go.mod similarity index 100% rename from go/go.mod rename to src/go.mod diff --git a/src/internal/color/bounds.go b/src/internal/color/bounds.go new file mode 100644 index 00000000..a9e64d72 --- /dev/null +++ b/src/internal/color/bounds.go @@ -0,0 +1,77 @@ +package color + +import ( + "math" + "regexp" + "strconv" + "strings" +) + +var jsParseFloat = regexp.MustCompile(`^[\t\n\v\f\r ]*[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:[eE][+-]?\d+)?`) + +// ParseFloat mirrors the prefix parsing used by JavaScript parseFloat for the +// numeric forms TinyColor accepts. +func ParseFloat(value any) float64 { + switch number := value.(type) { + case float64: + return number + case float32: + return float64(number) + case int: + return float64(number) + case int64: + return float64(number) + case string: + match := jsParseFloat.FindString(number) + if match == "" { + return math.NaN() + } + parsed, err := strconv.ParseFloat(strings.TrimSpace(match), 64) + if err != nil { + return math.NaN() + } + return parsed + default: + return math.NaN() + } +} + +func IsPercentage(value any) bool { + text, ok := value.(string) + return ok && strings.Contains(text, "%") +} + +func IsOnePointZero(value any) bool { + text, ok := value.(string) + return ok && strings.Contains(text, ".") && ParseFloat(text) == 1 +} + +// Bound01 is TinyColor's bound01 helper, including percentage truncation and +// its near-maximum floating-point correction. +func Bound01(value any, max float64) float64 { + if IsOnePointZero(value) { + value = "100%" + } + percentage := IsPercentage(value) + number := ParseFloat(value) + if math.IsNaN(number) { + return math.NaN() + } + number = math.Min(max, math.Max(0, number)) + if percentage { + number = math.Trunc(number*max) / 100 + } + if math.Abs(number-max) < 0.000001 { + return 1 + } + return math.Mod(number, max) / max +} + +// BoundAlpha keeps TinyColor's intentionally forgiving alpha behavior. +func BoundAlpha(value any) float64 { + alpha := ParseFloat(value) + if math.IsNaN(alpha) || alpha < 0 || alpha > 1 { + return 1 + } + return alpha +} diff --git a/src/internal/color/model.go b/src/internal/color/model.go new file mode 100644 index 00000000..85db3c17 --- /dev/null +++ b/src/internal/color/model.go @@ -0,0 +1,53 @@ +// Package color holds the normalized state shared by all parser paths. +package color + +// Format is TinyColor's detected input format. An empty Format represents the +// source's false format value for invalid input. +type Format string + +const ( + FormatUnknown Format = "" + FormatRGB Format = "rgb" + FormatPRGB Format = "prgb" + FormatHSL Format = "hsl" + FormatHSV Format = "hsv" + FormatHex Format = "hex" + FormatHex8 Format = "hex8" + FormatName Format = "name" +) + +// Model is TinyColor's normalized constructor state. It is internal to this +// module so the public facade cannot bypass source-compatible parsing. +type Model struct { + R, G, B float64 + A float64 + Valid bool + Format Format + Original any +} + +// Invalid returns the source invalid-color state: opaque black, no format, +// and a retained original input. +func Invalid(original any) Model { + return Model{A: 1, Original: original} +} + +// OriginalInput applies TinyColor's constructor normalization to the JSON-safe +// inputs used by the compatibility adapter. +func OriginalInput(input any) any { + switch value := input.(type) { + case nil: + return "" + case string: + return value + case bool: + if !value { + return "" + } + case float64: + if value == 0 { + return "" + } + } + return input +} diff --git a/src/internal/color/model_test.go b/src/internal/color/model_test.go new file mode 100644 index 00000000..a5304110 --- /dev/null +++ b/src/internal/color/model_test.go @@ -0,0 +1,63 @@ +package color + +import ( + "math" + "reflect" + "testing" +) + +func TestBound01MatchesTinyColorEdges(t *testing.T) { + tests := []struct { + input any + max float64 + want float64 + }{ + {-1, 255, 0}, + {300, 255, 1}, + {"1", 255, 1.0 / 255}, + {"1.0", 255, 1}, + {"100%", 255, 1}, + {"50%", 255, 0.5}, + {"255.0000001", 255, 1}, + } + for _, tt := range tests { + if got := Bound01(tt.input, tt.max); got != tt.want { + t.Errorf("Bound01(%#v, %v) = %v, want %v", tt.input, tt.max, got, tt.want) + } + } + if !math.IsNaN(Bound01("not a number", 255)) { + t.Fatal("invalid input should stay NaN until the parser rejects it") + } +} + +func TestBoundAlphaMatchesTinyColor(t *testing.T) { + tests := []struct { + input any + want float64 + }{ + {-1, 1}, {0, 0}, {0.5, 0.5}, {1, 1}, {100, 1}, {"asdf", 1}, + } + for _, tt := range tests { + if got := BoundAlpha(tt.input); got != tt.want { + t.Errorf("BoundAlpha(%#v) = %v, want %v", tt.input, got, tt.want) + } + } +} + +func TestOriginalInputAndInvalidState(t *testing.T) { + object := map[string]any{"r": float64(255)} + for _, tt := range []struct { + input any + want any + }{ + {nil, ""}, {"", ""}, {"Red", "Red"}, {false, ""}, {float64(0), ""}, {object, object}, + } { + if got := OriginalInput(tt.input); !reflect.DeepEqual(got, tt.want) { + t.Errorf("OriginalInput(%#v) = %#v, want %#v", tt.input, got, tt.want) + } + } + invalid := Invalid("bad") + if invalid.Valid || invalid.R != 0 || invalid.G != 0 || invalid.B != 0 || invalid.A != 1 || invalid.Format != FormatUnknown { + t.Fatalf("invalid state = %#v", invalid) + } +} diff --git a/go/internal/compat/protocol.go b/src/internal/compat/protocol.go similarity index 100% rename from go/internal/compat/protocol.go rename to src/internal/compat/protocol.go diff --git a/go/internal/compat/protocol_test.go b/src/internal/compat/protocol_test.go similarity index 100% rename from go/internal/compat/protocol_test.go rename to src/internal/compat/protocol_test.go diff --git a/src/internal/parser/hex.go b/src/internal/parser/hex.go new file mode 100644 index 00000000..32e4441c --- /dev/null +++ b/src/internal/parser/hex.go @@ -0,0 +1,41 @@ +package parser + +import ( + "strconv" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +func parseHex(text string, original any, format color.Format) (color.Model, bool) { + if len(text) > 0 && text[0] == '#' { + text = text[1:] + } + if len(text) != 3 && len(text) != 4 && len(text) != 6 && len(text) != 8 { + return color.Model{}, false + } + for _, rune := range text { + if !((rune >= '0' && rune <= '9') || (rune >= 'a' && rune <= 'f')) { + return color.Model{}, false + } + } + if len(text) == 3 || len(text) == 4 { + text = string([]byte{text[0], text[0], text[1], text[1], text[2], text[2]}) + func() string { + if len(text) == 4 { + return string([]byte{text[3], text[3]}) + } + return "" + }() + } + channel := func(offset int) float64 { + value, _ := strconv.ParseInt(text[offset:offset+2], 16, 64) + return float64(value) + } + model := color.Model{R: channel(0), G: channel(2), B: channel(4), A: 1, Valid: true, Format: format, Original: original} + if len(text) == 8 { + model.A = channel(6) / 255 + if format == color.FormatHex { + model.Format = color.FormatHex8 + } + } + return model, true +} diff --git a/src/internal/parser/hsl.go b/src/internal/parser/hsl.go new file mode 100644 index 00000000..dccc646d --- /dev/null +++ b/src/internal/parser/hsl.go @@ -0,0 +1,13 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseHSL(text string, original any) (color.Model, bool) { + if match := hslaMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hslMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} diff --git a/src/internal/parser/hsv.go b/src/internal/parser/hsv.go new file mode 100644 index 00000000..987f34eb --- /dev/null +++ b/src/internal/parser/hsv.go @@ -0,0 +1,13 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseHSV(text string, original any) (color.Model, bool) { + if match := hsvaMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hsvMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} diff --git a/src/internal/parser/names.go b/src/internal/parser/names.go new file mode 100644 index 00000000..48fc7ff3 --- /dev/null +++ b/src/internal/parser/names.go @@ -0,0 +1,43 @@ +package parser + +import ( + "fmt" + "strings" +) + +// names is copied from this checkout's mod.js names table. +var ( + names = func() map[string]string { + const data = "aliceblue:f0f8ff;antiquewhite:faebd7;aqua:0ff;aquamarine:7fffd4;azure:f0ffff;beige:f5f5dc;bisque:ffe4c4;black:000;blanchedalmond:ffebcd;blue:00f;blueviolet:8a2be2;brown:a52a2a;burlywood:deb887;burntsienna:ea7e5d;cadetblue:5f9ea0;chartreuse:7fff00;chocolate:d2691e;coral:ff7f50;cornflowerblue:6495ed;cornsilk:fff8dc;crimson:dc143c;cyan:0ff;darkblue:00008b;darkcyan:008b8b;darkgoldenrod:b8860b;darkgray:a9a9a9;darkgreen:006400;darkgrey:a9a9a9;darkkhaki:bdb76b;darkmagenta:8b008b;darkolivegreen:556b2f;darkorange:ff8c00;darkorchid:9932cc;darkred:8b0000;darksalmon:e9967a;darkseagreen:8fbc8f;darkslateblue:483d8b;darkslategray:2f4f4f;darkslategrey:2f4f4f;darkturquoise:00ced1;darkviolet:9400d3;deeppink:ff1493;deepskyblue:00bfff;dimgray:696969;dimgrey:696969;dodgerblue:1e90ff;firebrick:b22222;floralwhite:fffaf0;forestgreen:228b22;fuchsia:f0f;gainsboro:dcdcdc;ghostwhite:f8f8ff;gold:ffd700;goldenrod:daa520;gray:808080;green:008000;greenyellow:adff2f;grey:808080;honeydew:f0fff0;hotpink:ff69b4;indianred:cd5c5c;indigo:4b0082;ivory:fffff0;khaki:f0e68c;lavender:e6e6fa;lavenderblush:fff0f5;lawngreen:7cfc00;lemonchiffon:fffacd;lightblue:add8e6;lightcoral:f08080;lightcyan:e0ffff;lightgoldenrodyellow:fafad2;lightgray:d3d3d3;lightgreen:90ee90;lightgrey:d3d3d3;lightpink:ffb6c1;lightsalmon:ffa07a;lightseagreen:20b2aa;lightskyblue:87cefa;lightslategray:789;lightslategrey:789;lightsteelblue:b0c4de;lightyellow:ffffe0;lime:0f0;limegreen:32cd32;linen:faf0e6;magenta:f0f;maroon:800000;mediumaquamarine:66cdaa;mediumblue:0000cd;mediumorchid:ba55d3;mediumpurple:9370db;mediumseagreen:3cb371;mediumslateblue:7b68ee;mediumspringgreen:00fa9a;mediumturquoise:48d1cc;mediumvioletred:c71585;midnightblue:191970;mintcream:f5fffa;mistyrose:ffe4e1;moccasin:ffe4b5;navajowhite:ffdead;navy:000080;oldlace:fdf5e6;olive:808000;olivedrab:6b8e23;orange:ffa500;orangered:ff4500;orchid:da70d6;palegoldenrod:eee8aa;palegreen:98fb98;paleturquoise:afeeee;palevioletred:db7093;papayawhip:ffefd5;peachpuff:ffdab9;peru:cd853f;pink:ffc0cb;plum:dda0dd;powderblue:b0e0e6;purple:800080;rebeccapurple:663399;red:f00;rosybrown:bc8f8f;royalblue:4169e1;saddlebrown:8b4513;salmon:fa8072;sandybrown:f4a460;seagreen:2e8b57;seashell:fff5ee;sienna:a0522d;silver:c0c0c0;skyblue:87ceeb;slateblue:6a5acd;slategray:708090;slategrey:708090;snow:fffafa;springgreen:00ff7f;steelblue:4682b4;tan:d2b48c;teal:008080;thistle:d8bfd8;tomato:ff6347;turquoise:40e0d0;violet:ee82ee;wheat:f5deb3;white:fff;whitesmoke:f5f5f5;yellow:ff0;yellowgreen:9acd32" + result := make(map[string]string) + for _, entry := range strings.Split(data, ";") { + pair := strings.SplitN(entry, ":", 2) + result[pair[0]] = pair[1] + } + return result + }() + hexNames = func() map[string]string { + result := make(map[string]string) + for name, sourceHex := range names { + hex := sourceHex + if len(hex) == 3 { + hex = string([]byte{hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]}) + } + result[hex] = name + } + result["00ffff"] = "cyan" + result["ff00ff"] = "magenta" + result["808080"] = "grey" + result["a9a9a9"] = "darkgrey" + result["2f4f4f"] = "darkslategrey" + result["696969"] = "dimgrey" + result["d3d3d3"] = "lightgrey" + result["778899"] = "lightslategrey" + result["708090"] = "slategrey" + return result + }() +) + +func NameForRGB(r, g, b int) string { + return hexNames[fmt.Sprintf("%02x%02x%02x", r, g, b)] +} diff --git a/src/internal/parser/object.go b/src/internal/parser/object.go new file mode 100644 index 00000000..eac330cf --- /dev/null +++ b/src/internal/parser/object.go @@ -0,0 +1,22 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseObjectFull(object map[string]any, original any) color.Model { + if model := parseObject(object, original); model.Valid { + return model + } + h, hasH := object["h"] + s, hasS := object["s"] + if !hasH || !hasS || !validCSSUnit(h) || !validCSSUnit(s) { + return color.Invalid(original) + } + alpha, hasAlpha := object["a"] + if v, ok := object["v"]; ok && validCSSUnit(v) { + return hsvModel(h, s, v, alpha, hasAlpha, original) + } + if l, ok := object["l"]; ok && validCSSUnit(l) { + return hslModel(h, s, l, alpha, hasAlpha, original) + } + return color.Invalid(original) +} diff --git a/src/internal/parser/parser.go b/src/internal/parser/parser.go new file mode 100644 index 00000000..495cf5a1 --- /dev/null +++ b/src/internal/parser/parser.go @@ -0,0 +1,179 @@ +// Package parser converts JSON-safe compatibility inputs into normalized state. +package parser + +import ( + "math" + "regexp" + "strconv" + "strings" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +// Parse accepts the local TinyColor input forms implemented in this phase. +func Parse(input any) color.Model { + original := color.OriginalInput(input) + switch value := original.(type) { + case string: + return parseString(value, original) + case map[string]any: + return parseObjectFull(value, original) + default: + return color.Invalid(original) + } +} + +// ParseFromRatio applies TinyColor.fromRatio's object transformation before +// parsing; the transformed object is also TinyColor's original input. +func ParseFromRatio(input any) color.Model { + object, ok := input.(map[string]any) + if !ok { + return Parse(input) + } + transformed := make(map[string]any, len(object)) + for key, value := range object { + if key == "a" { + transformed[key] = value + continue + } + if number := color.ParseFloat(value); !math.IsNaN(number) && number <= 1 { + transformed[key] = formatPercent(number*100) + "%" + } else { + transformed[key] = value + } + } + return parseObjectFull(transformed, transformed) +} + +func formatPercent(number float64) string { + return strconv.FormatFloat(number, 'f', -1, 64) +} + +var ( + hslMatcher = regexp.MustCompile(`(?i)hsl[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hsvMatcher = regexp.MustCompile(`(?i)hsv[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hslaMatcher = regexp.MustCompile(`(?i)hsla[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hsvaMatcher = regexp.MustCompile(`(?i)hsva[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) +) + +func parseString(value string, original any) color.Model { + text := strings.ToLower(strings.TrimSpace(value)) + if text == "transparent" { + return color.Model{A: 0, Valid: true, Format: color.FormatName, Original: original} + } + if hex, named := names[text]; named { + model, _ := parseHex(hex, original, color.FormatName) + return model + } + if model, ok := parseRGBString(text, original); ok { + return model + } + if model, ok := parseHex(text, original, color.FormatHex); ok { + return model + } + // Phase 1 smoke must remain executable while Plan 02-02 moves these paths + // into dedicated files. This is generic grammar, not a fixed input switch. + if model, ok := parseHSL(text, original); ok { + return model + } + if model, ok := parseHSV(text, original); ok { + return model + } + return color.Invalid(original) +} + +func parseObject(object map[string]any, original any) color.Model { + r, hasR := object["r"] + g, hasG := object["g"] + b, hasB := object["b"] + if !hasR || !hasG || !hasB || !validCSSUnit(r) || !validCSSUnit(g) || !validCSSUnit(b) { + return color.Invalid(original) + } + alpha, hasAlpha := object["a"] + format := formatForRGB(r) + if explicit, ok := object["format"].(string); ok && explicit != "" { + format = color.Format(explicit) + } + return rgbModel(r, g, b, alpha, hasAlpha, original, format) +} + +func parseHSLHSVSmoke(text string, original any) (color.Model, bool) { + if match := hslaMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hslMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], nil, false, original), true + } + if match := hsvaMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hsvMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} + +func hslModel(h, s, l, alpha any, hasAlpha bool, original any) color.Model { + hue := color.Bound01(h, 360) + saturation := ratio(s) + lightness := ratio(l) + var q float64 + if lightness < 0.5 { + q = lightness * (1 + saturation) + } else { + q = lightness + saturation - lightness*saturation + } + p := 2*lightness - q + channel := func(t float64) float64 { + if t < 0 { + t++ + } + if t > 1 { + t-- + } + switch { + case t*6 < 1: + return p + (q-p)*6*t + case t*2 < 1: + return q + case t*3 < 2: + return p + (q-p)*(2.0/3.0-t)*6 + default: + return p + } + } + if saturation == 0 { + p = lightness + q = lightness + } + model := color.Model{R: channel(hue+1.0/3.0) * 255, G: channel(hue) * 255, B: channel(hue-1.0/3.0) * 255, A: color.BoundAlpha(alpha), Valid: true, Format: color.FormatHSL, Original: original} + if !hasAlpha { + model.A = 1 + } + return model +} + +func hsvModel(h, s, v, alpha any, hasAlpha bool, original any) color.Model { + hue := color.Bound01(h, 360) * 6 + saturation := ratio(s) + value := ratio(v) + i := int(math.Floor(hue)) + f := hue - math.Floor(hue) + p := value * (1 - saturation) + q := value * (1 - f*saturation) + t := value * (1 - (1-f)*saturation) + channels := [][3]float64{{value, t, p}, {q, value, p}, {p, value, t}, {p, q, value}, {t, p, value}, {value, p, q}} + rgb := channels[i%6] + model := color.Model{R: rgb[0] * 255, G: rgb[1] * 255, B: rgb[2] * 255, A: color.BoundAlpha(alpha), Valid: true, Format: color.FormatHSV, Original: original} + if !hasAlpha { + model.A = 1 + } + return model +} + +func ratio(value any) float64 { + if number := color.ParseFloat(value); !math.IsNaN(number) && number <= 1 { + value = formatPercent(number*100) + "%" + } + return color.Bound01(value, 100) +} diff --git a/src/internal/parser/parser_test.go b/src/internal/parser/parser_test.go new file mode 100644 index 00000000..d4254bde --- /dev/null +++ b/src/internal/parser/parser_test.go @@ -0,0 +1,75 @@ +package parser + +import ( + "reflect" + "testing" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +func TestHexRGBNamesAndObjects(t *testing.T) { + tests := []struct { + input any + valid bool + format color.Format + r, g, b float64 + alpha float64 + }{ + {"#f00", true, color.FormatHex, 255, 0, 0, 1}, + {"ff000080", true, color.FormatHex8, 255, 0, 0, 128.0 / 255}, + {" InDiAnReD ", true, color.FormatName, 205, 92, 92, 1}, + {"transparent", true, color.FormatName, 0, 0, 0, 0}, + {"rgb (100%, 0%, 0%)", true, color.FormatPRGB, 255, 0, 0, 1}, + {"rgba 255 0 0 .5", true, color.FormatRGB, 255, 0, 0, .5}, + {map[string]any{"r": "90%", "g": "45%", "b": "0%", "a": .4}, true, color.FormatPRGB, 229.5, 114.75, 0, .4}, + {"##123456", false, color.FormatUnknown, 0, 0, 0, 1}, + {map[string]any{"r": "invalid", "g": "invalid", "b": "invalid"}, false, color.FormatUnknown, 0, 0, 0, 1}, + } + for _, tt := range tests { + model := Parse(tt.input) + if model.Valid != tt.valid || model.Format != tt.format || model.R != tt.r || model.G != tt.g || model.B != tt.b || model.A != tt.alpha { + t.Errorf("Parse(%#v) = %#v", tt.input, model) + } + } +} + +func TestAllSourceNamesParse(t *testing.T) { + for name := range names { + model := Parse(name) + if !model.Valid || model.Format != color.FormatName { + t.Errorf("%s = %#v", name, model) + } + } +} + +func TestOriginalAndFromRatio(t *testing.T) { + object := map[string]any{"r": float64(1), "g": float64(0), "b": float64(0), "a": float64(.5)} + ratio := ParseFromRatio(object) + if !ratio.Valid || ratio.Format != color.FormatPRGB || ratio.A != .5 { + t.Fatalf("ratio = %#v", ratio) + } + want := map[string]any{"r": "100%", "g": "0%", "b": "0%", "a": float64(.5)} + if !reflect.DeepEqual(ratio.Original, want) { + t.Fatalf("ratio original = %#v, want %#v", ratio.Original, want) + } + if original := Parse(nil).Original; original != "" { + t.Fatalf("null original = %#v", original) + } +} + +func TestHSLHSVObjectsAndWrappedHue(t *testing.T) { + for _, input := range []any{ + "hsl(251, 100%, 38%)", + "hsva 251.1 .887 .918 .5", + map[string]any{"h": float64(251), "s": float64(100), "l": float64(.38)}, + map[string]any{"h": float64(720), "s": float64(100), "v": float64(100)}, + } { + model := Parse(input) + if !model.Valid || (model.Format != color.FormatHSL && model.Format != color.FormatHSV) { + t.Errorf("Parse(%#v) = %#v", input, model) + } + } + if model := Parse(map[string]any{"h": "invalid", "s": "invalid", "v": "invalid"}); model.Valid { + t.Fatalf("invalid HSV object = %#v", model) + } +} diff --git a/src/internal/parser/rgb.go b/src/internal/parser/rgb.go new file mode 100644 index 00000000..eaf6b8db --- /dev/null +++ b/src/internal/parser/rgb.go @@ -0,0 +1,56 @@ +package parser + +import ( + "regexp" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +const cssUnit = `(?:[-+]?\d*\.\d+%?)|(?:[-+]?\d+%?)` + +var ( + rgbMatcher = regexp.MustCompile(`(?i)rgb[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + rgbaMatcher = regexp.MustCompile(`(?i)rgba[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + cssMatcher = regexp.MustCompile(cssUnit) +) + +func validCSSUnit(value any) bool { + switch value := value.(type) { + case string: + return cssMatcher.FindStringIndex(value) != nil + case float64: + return true + default: + return false + } +} + +func rgbModel(r, g, b, alpha any, hasAlpha bool, original any, format color.Format) color.Model { + model := color.Model{ + R: color.Bound01(r, 255) * 255, + G: color.Bound01(g, 255) * 255, + B: color.Bound01(b, 255) * 255, + A: color.BoundAlpha(alpha), Valid: true, Format: format, Original: original, + } + if !hasAlpha { + model.A = 1 + } + return model +} + +func parseRGBString(text string, original any) (color.Model, bool) { + if match := rgbaMatcher.FindStringSubmatch(text); match != nil { + return rgbModel(match[1], match[2], match[3], match[4], true, original, formatForRGB(match[1])), true + } + if match := rgbMatcher.FindStringSubmatch(text); match != nil { + return rgbModel(match[1], match[2], match[3], nil, false, original, formatForRGB(match[1])), true + } + return color.Model{}, false +} + +func formatForRGB(red any) color.Format { + if color.IsPercentage(red) { + return color.FormatPRGB + } + return color.FormatRGB +} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go new file mode 100644 index 00000000..a45f8b0f --- /dev/null +++ b/src/tinycolor/color.go @@ -0,0 +1,128 @@ +// Package tinycolor exposes the compatibility facade over normalized parsing. +package tinycolor + +import ( + "fmt" + "math" + "strconv" + + "github.com/rajeet-04/tinycolor-go/internal/color" + "github.com/rajeet-04/tinycolor-go/internal/parser" +) + +type Color struct{ model color.Model } + +func FromCompat(input any, fromRatio bool) (Color, error) { + if fromRatio { + return Color{model: parser.ParseFromRatio(input)}, nil + } + return Color{model: parser.Parse(input)}, nil +} + +func (c Color) Valid() bool { return c.model.Valid } +func (c Color) Format() string { return string(c.model.Format) } +func (c Color) Alpha() float64 { return c.model.A } +func (c Color) Original() any { return c.model.Original } +func (c Color) RGB() map[string]any { + return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} +} + +// String supplies the minimal source-compatible string snapshot used by the +// JSONL oracle. Full public output APIs remain Phase 3 work. +func (c Color) String() string { + r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) + if c.model.Format == color.FormatName { + if c.model.A == 0 { + return "transparent" + } + if name := parser.NameForRGB(r, g, b); name != "" { + return name + } + } + if c.model.Format == color.FormatPRGB { + if c.model.A < 1 { + return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", percent(r), percent(g), percent(b), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", percent(r), percent(g), percent(b)) + } + if c.model.Format == color.FormatHSL { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsla(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsl(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100))) + } + if c.model.Format == color.FormatHSV { + h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsva(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsv(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100))) + } + if c.model.Format == color.FormatRGB || c.model.A < 1 { + if c.model.A == 1 { + return fmt.Sprintf("rgb(%d, %d, %d)", r, g, b) + } + return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) + } + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} + +func (c Color) Inspect() map[string]any { + format := any(c.Format()) + if c.Format() == "" { + format = false + } + return map[string]any{"valid": c.Valid(), "format": format, "alpha": c.Alpha(), "rgb": c.RGB(), "value": c.String(), "original": c.Original()} +} + +func percent(channel int) int { return int(math.Round(float64(channel) / 255 * 100)) } +func roundedAlpha(alpha float64) string { + return strconv.FormatFloat(math.Round(alpha*100)/100, 'f', -1, 64) +} + +func rgbToHSL(r, g, b float64) (float64, float64, float64) { + r, g, b = r/255, g/255, b/255 + max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) + l := (max + min) / 2 + if max == min { + return 0, 0, l + } + d := max - min + s := d / (2 - max - min) + if l < 0.5 { + s = d / (max + min) + } + var h float64 + switch max { + case r: + h = (g-b)/d + map[bool]float64{true: 6, false: 0}[g < b] + case g: + h = (b-r)/d + 2 + default: + h = (r-g)/d + 4 + } + return h / 6, s, l +} + +func rgbToHSV(r, g, b float64) (float64, float64, float64) { + r, g, b = r/255, g/255, b/255 + max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) + d := max - min + if max == 0 { + return 0, 0, 0 + } + if d == 0 { + return 0, 0, max + } + var h float64 + switch max { + case r: + h = (g-b)/d + map[bool]float64{true: 6, false: 0}[g < b] + case g: + h = (b-r)/d + 2 + default: + h = (r-g)/d + 4 + } + return h / 6, d / max, max +} diff --git a/go/tinycolor/color_test.go b/src/tinycolor/color_test.go similarity index 86% rename from go/tinycolor/color_test.go rename to src/tinycolor/color_test.go index 7819420d..ef84b69c 100644 --- a/go/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -36,8 +36,9 @@ func TestPhaseOneInputsMatchTinyColor(t *testing.T) { } } -func TestPhaseOneRejectsUnsupportedInput(t *testing.T) { - if _, err := FromCompat("blue", false); err == nil { - t.Fatal("expected unsupported input error") +func TestInvalidInputIsAColorState(t *testing.T) { + color, err := FromCompat("this is not a color", false) + if err != nil || color.Valid() || color.String() != "#000000" { + t.Fatalf("invalid input = %#v, %v", color, err) } } diff --git a/tests/original/README.md b/tests/original/README.md new file mode 100644 index 00000000..4465405d --- /dev/null +++ b/tests/original/README.md @@ -0,0 +1,5 @@ +# Pinned JavaScript oracle + +The canonical, unmodified upstream files remain at the repository root so the +Node adapter can import them directly. `manifest.sha256` records their kickoff +hashes from commit `b49018c9f2dbca313d80d7a4dad25e26143cfe01`. diff --git a/tests/original/manifest.sha256 b/tests/original/manifest.sha256 new file mode 100644 index 00000000..0297cdc9 --- /dev/null +++ b/tests/original/manifest.sha256 @@ -0,0 +1,3 @@ +a4c3efa67a123efbb9e8082a6c1296459cd9151c0cdd00ad699b1a43fe02fb77 mod.js +cdddded304f3f2910d43e12538246e79931b7e1638a186a632920b307c67dfb3 test.js +239e168433c66d15388d18034944203380c93171f44b029ce33456116c855314 tinycolor.js diff --git a/tests/port/README.md b/tests/port/README.md new file mode 100644 index 00000000..eb54381d --- /dev/null +++ b/tests/port/README.md @@ -0,0 +1,4 @@ +# Port-owned tests + +`adapter.test.mjs` checks the JSON-lines contract used for JavaScript-versus-Go +comparisons. Go unit tests are kept beside the code in `src/`. diff --git a/compat/js-runner.test.mjs b/tests/port/adapter.test.mjs similarity index 97% rename from compat/js-runner.test.mjs rename to tests/port/adapter.test.mjs index 39a1291a..db49297b 100644 --- a/compat/js-runner.test.mjs +++ b/tests/port/adapter.test.mjs @@ -19,6 +19,7 @@ assert.deepEqual(success, { alpha: 1, rgb: { r: 255, g: 0, b: 0, a: 1 }, value: "red", + original: "red", }, }); From 82b1208d201783689d8a1d1a99cf92397b901d88 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 01:40:28 +0530 Subject: [PATCH 006/165] feat: complete Phase 2 parser parity --- .gitignore | 3 +- .planning/ROADMAP.md | 10 +- .planning/STATE.md | 7 +- .../02-parsing-and-color-state/02-01-PLAN.md | 28 +-- .../02-parsing-and-color-state/02-02-PLAN.md | 25 ++- .port-mortem.toml | 4 + AGENT.md | 18 +- COMPATIBILITY.md | 33 +++- DECISIONS.md | 3 +- Dockerfile | 10 + Makefile | 12 ++ PLAN.md | 2 +- PORT.md | 12 ++ bench/methodology.md | 5 + bench/results.json | 4 + compat/cases/parser-hex-rgb.jsonl | 26 +++ compat/cases/parser.jsonl | 23 +++ compat/js-runner.mjs | 1 + compat/run.mjs | 4 +- docs/ARCHITECTURE.md | 9 +- docs/TESTING.md | 3 +- fuzz/README.md | 4 + go/tinycolor/color.go | 74 -------- {go => src}/cmd/tinycolor-compat/main.go | 0 {go => src}/go.mod | 0 src/internal/color/bounds.go | 77 ++++++++ src/internal/color/model.go | 53 ++++++ src/internal/color/model_test.go | 63 ++++++ {go => src}/internal/compat/protocol.go | 0 {go => src}/internal/compat/protocol_test.go | 0 src/internal/parser/hex.go | 41 ++++ src/internal/parser/hsl.go | 13 ++ src/internal/parser/hsv.go | 13 ++ src/internal/parser/names.go | 43 +++++ src/internal/parser/object.go | 22 +++ src/internal/parser/parser.go | 179 ++++++++++++++++++ src/internal/parser/parser_test.go | 75 ++++++++ src/internal/parser/rgb.go | 56 ++++++ src/tinycolor/color.go | 128 +++++++++++++ {go => src}/tinycolor/color_test.go | 7 +- tests/original/README.md | 5 + tests/original/manifest.sha256 | 3 + tests/port/README.md | 4 + .../port/adapter.test.mjs | 1 + 44 files changed, 965 insertions(+), 138 deletions(-) create mode 100644 .port-mortem.toml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 PORT.md create mode 100644 bench/methodology.md create mode 100644 bench/results.json create mode 100644 compat/cases/parser-hex-rgb.jsonl create mode 100644 compat/cases/parser.jsonl create mode 100644 fuzz/README.md delete mode 100644 go/tinycolor/color.go rename {go => src}/cmd/tinycolor-compat/main.go (100%) rename {go => src}/go.mod (100%) create mode 100644 src/internal/color/bounds.go create mode 100644 src/internal/color/model.go create mode 100644 src/internal/color/model_test.go rename {go => src}/internal/compat/protocol.go (100%) rename {go => src}/internal/compat/protocol_test.go (100%) create mode 100644 src/internal/parser/hex.go create mode 100644 src/internal/parser/hsl.go create mode 100644 src/internal/parser/hsv.go create mode 100644 src/internal/parser/names.go create mode 100644 src/internal/parser/object.go create mode 100644 src/internal/parser/parser.go create mode 100644 src/internal/parser/parser_test.go create mode 100644 src/internal/parser/rgb.go create mode 100644 src/tinycolor/color.go rename {go => src}/tinycolor/color_test.go (86%) create mode 100644 tests/original/README.md create mode 100644 tests/original/manifest.sha256 create mode 100644 tests/port/README.md rename compat/js-runner.test.mjs => tests/port/adapter.test.mjs (97%) diff --git a/.gitignore b/.gitignore index 1c08f4da..24a07a3e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules .vscode cov_profile cov_profile.lcov -.cache/ \ No newline at end of file +.cache/ +bin/ diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index da09cbea..ed3c6d69 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -21,7 +21,7 @@ through parsing and observable output, to operations and delivery proof. **Depends on**: Nothing (first phase) **Requirements**: [EQV-01, QLT-01, QLT-03] **Success Criteria**: - 1. `go/` builds as an independent module without changing JavaScript source files. + 1. `src/` builds as an independent module without changing JavaScript source files. 2. Node and Go runners accept and emit the documented JSONL request/response protocol. 3. A differential command reports case count, mismatch count, and complete mismatch records. **Plans**: 1 plan (complete 2026-08-01) @@ -37,11 +37,11 @@ Plans: 1. Hex, RGB(A), HSL(A), HSV(A), CSS names, transparent, and object inputs produce matching state. 2. Permissive syntax, boundary values, invalid input, and alpha normalization match the oracle corpus. 3. Every discovered parser mismatch has a deterministic regression record. -**Plans**: 2 plans +**Plans**: 2 plans (complete 2026-08-01) Plans: -- [ ] 02-01: Replace Phase 1 fixed inputs with normalized color/model and HEX/RGB/name parsing. -- [ ] 02-02: Add HSL/HSV/object parsing and parser differential corpus. +- [x] 02-01: Replace Phase 1 fixed inputs with normalized color/model and HEX/RGB/name parsing. +- [x] 02-02: Add HSL/HSV/object parsing and parser differential corpus. ### Phase 3: Conversion and Representation **Goal**: Go exposes TinyColor-equivalent conversion, string formatting, format fallback, and color analysis. @@ -78,7 +78,7 @@ Plans: | Phase | Plans Complete | Status | Completed | |---|---|---|---| | 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | -| 2. Parsing and Color State | 0/2 | Not started | - | +| 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | | 3. Conversion and Representation | 0/TBD | Not started | - | | 4. Operations and Palettes | 0/TBD | Not started | - | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index b10aced1..e28e1a16 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -9,5 +9,8 @@ - Phase 1 completed in commit `78423e3`: Go/Node JSONL runners, protocol tests, and a fixed corpus passed 9/9 with zero mismatches. This is not whole-library parity. -- Phase 2 is planned next; it replaces the fixed Phase 1 input decoder with the - complete TinyColor parser while preserving the compatibility protocol. +- Phase 2 completed locally: the normalized parser covers HEX, RGB(A), HSL(A), + HSV(A), names, objects, invalid input, and FromRatio. Differential evidence: + 26/26 HEX/RGB/name cases, 23/23 complete parser cases, and 9/9 smoke cases. +- The Go module lives in `src/`; `tests/original/manifest.sha256` pins the + unmodified root JavaScript oracle. diff --git a/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md index f97563de..3910cd67 100644 --- a/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md +++ b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md @@ -7,15 +7,15 @@ tags: [go, parser, tinycolor, differential-testing] wave: 1 depends_on: [] files_modified: - - go/internal/color/model.go - - go/internal/color/bounds.go - - go/internal/color/model_test.go - - go/internal/parser/parser.go - - go/internal/parser/hex.go - - go/internal/parser/rgb.go - - go/internal/parser/names.go - - go/internal/parser/parser_test.go - - go/tinycolor/color.go + - src/internal/color/model.go + - src/internal/color/bounds.go + - src/internal/color/model_test.go + - src/internal/parser/parser.go + - src/internal/parser/hex.go + - src/internal/parser/rgb.go + - src/internal/parser/names.go + - src/internal/parser/parser_test.go + - src/tinycolor/color.go - compat/cases/parser-hex-rgb.jsonl - COMPATIBILITY.md autonomous: true @@ -42,11 +42,11 @@ invalid input, and RGB objects. Create the normalized color model and JavaScript-equivalent bounds -go/internal/color/model.go, go/internal/color/bounds.go, go/internal/color/model_test.go +src/internal/color/model.go, src/internal/color/bounds.go, src/internal/color/model_test.go - `mod.js` lines 359-442 and 1047-1128 - `test.js` lines 274-443 and 698-746 -- `go/tinycolor/color.go` +- `src/tinycolor/color.go` Define one private normalized RGBA model with float RGB channels, alpha, @@ -59,13 +59,13 @@ Write table tests for negative/overflow values, `1`, string `1.0`, percentages, invalid alpha, alpha zero, the source invalid-black state, lowercase/uppercase/ mixed strings, objects, empty strings, and null. -Set-Location go; go test ./internal/color; go vet ./... +Set-Location src; go test ./internal/color; go vet ./... All normalized state and bounds tests pass with explicit source-derived boundary values. Parse HEX, RGB(A), names, transparent, and RGB objects through the shared model -go/internal/parser/parser.go, go/internal/parser/hex.go, go/internal/parser/rgb.go, go/internal/parser/names.go, go/internal/parser/parser_test.go, go/tinycolor/color.go, compat/cases/parser-hex-rgb.jsonl, COMPATIBILITY.md +src/internal/parser/parser.go, src/internal/parser/hex.go, src/internal/parser/rgb.go, src/internal/parser/names.go, src/internal/parser/parser_test.go, src/tinycolor/color.go, compat/cases/parser-hex-rgb.jsonl, COMPATIBILITY.md - `mod.js` lines 359-442 and 1129-1260 - `test.js` lines 316-443, 487-697, and 698-746 @@ -84,7 +84,7 @@ alpha boundaries, original strings/objects/null/empty values; retain all Phase 1 Extend both JSONL `inspect` results with an `original` field: Node uses `getOriginalInput()` and Go serializes the model's JSON-safe original value. -Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/smoke.jsonl +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/smoke.jsonl HEX/RGB/name/object differential corpus and the unchanged Phase 1 smoke corpus exit 0 with no mismatches. diff --git a/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md index e3f4981f..4d956f94 100644 --- a/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md +++ b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md @@ -7,11 +7,11 @@ tags: [go, parser, hsl, hsv, differential-testing] wave: 2 depends_on: [02-01] files_modified: - - go/internal/parser/hsl.go - - go/internal/parser/hsv.go - - go/internal/parser/object.go - - go/internal/parser/parser_test.go - - go/tinycolor/color.go + - src/internal/parser/hsl.go + - src/internal/parser/hsv.go + - src/internal/parser/object.go + - src/internal/parser/parser_test.go + - src/tinycolor/color.go - compat/cases/parser.jsonl - COMPATIBILITY.md autonomous: true @@ -21,9 +21,9 @@ must_haves: - "HSL(A), HSV(A), HSL/HSV objects, and FromRatio produce source-equivalent normalized state." - "The complete Phase 2 parser corpus and Phase 1 smoke corpus report zero mismatches." artifacts: - - path: "go/internal/parser/hsl.go" + - path: "src/internal/parser/hsl.go" provides: "HSL(A) parser path to normalized RGB" - - path: "go/internal/parser/hsv.go" + - path: "src/internal/parser/hsv.go" provides: "HSV(A) parser path to normalized RGB" - path: "compat/cases/parser.jsonl" provides: "Full Phase 2 parser differential corpus" @@ -37,12 +37,12 @@ object precedence, FromRatio transformation, and the full Phase 2 parser corpus. Parse HSL(A), HSV(A), typed objects, and FromRatio using source bounds -go/internal/parser/hsl.go, go/internal/parser/hsv.go, go/internal/parser/object.go, go/internal/parser/parser_test.go, go/tinycolor/color.go +src/internal/parser/hsl.go, src/internal/parser/hsv.go, src/internal/parser/object.go, src/internal/parser/parser_test.go, src/tinycolor/color.go - `mod.js` lines 359-572 and 1129-1260 - `test.js` lines 274-315, 444-515, 945-1096 -- `go/internal/color/bounds.go` -- `go/internal/parser/parser.go` +- `src/internal/color/bounds.go` +- `src/internal/parser/parser.go` Port only the RGB conversion math required to normalize parsed HSL/HSV values; @@ -54,7 +54,7 @@ through the source percentage transformation before parser dispatch. Add exact tests for source HSL/HSV examples, decimals, percentages, wrapped hues, alpha, and malformed components. -Set-Location go; go test ./internal/parser ./tinycolor; go vet ./... +Set-Location src; go test ./internal/parser ./tinycolor; go vet ./... Focused HSL/HSV, object, and FromRatio tests pass and no parser path bypasses the normalized model. @@ -75,8 +75,7 @@ values. Run it through the unchanged Node/Go runner; record exact case count, command, and result in `COMPATIBILITY.md`. Every mismatch must retain the runner's input/JS/Go/owner/suspected-package record until fixed. -Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js The full parser corpus and Phase 1 smoke corpus exit 0; compatibility evidence names the corpus count and no oracle files changed. - diff --git a/.port-mortem.toml b/.port-mortem.toml new file mode 100644 index 00000000..e2f31c43 --- /dev/null +++ b/.port-mortem.toml @@ -0,0 +1,4 @@ +track = "H" +source_url = "https://github.com/bgrins/TinyColor.git" +kickoff_commit = "b49018c9f2dbca313d80d7a4dad25e26143cfe01" +oracle_manifest = "tests/original/manifest.sha256" diff --git a/AGENT.md b/AGENT.md index 02157a1d..63e35f31 100644 --- a/AGENT.md +++ b/AGENT.md @@ -12,9 +12,12 @@ compatibility project: observable TinyColor behavior is the specification. - `mod.js`, `tinycolor.js`, `test.js`, `npm/`, `dist/`, and `demo/` are the immutable JavaScript oracle. Do not edit them except for a separately agreed upstream maintenance change. -- New Go work belongs under `go/`; `go/go.mod` is the port module boundary. +- New Go work belongs under `src/`; `src/go.mod` is the port module boundary. - Oracle adapters, generated fixtures, and mismatch reports belong under `compat/`. Do not rewrite the original test suite to make it pass. +- `tests/original/manifest.sha256` pins the immutable root oracle. Port-owned + adapter tests live in `tests/port/`; differential fuzzing and benchmarks live + in `fuzz/` and `bench/` respectively. - Human-facing project docs belong in `docs/`; compatibility status belongs in `COMPATIBILITY.md`. @@ -70,9 +73,9 @@ Known differences (or none): | Owner | Primary boundary | Cannot merge without | |---|---|---| -| A — Parsing/model | `go/internal/color`, `go/internal/parser` | parser differential cases | -| B — Conversion/API | `go/tinycolor`, formatting/readability/palettes | deterministic parity tests | -| C — Compatibility/quality | `compat`, `go/testdata`, `COMPATIBILITY.md` | reproducible report | +| A — Parsing/model | `src/internal/color`, `src/internal/parser` | parser differential cases | +| B — Conversion/API | `src/tinycolor`, formatting/readability/palettes | deterministic parity tests | +| C — Compatibility/quality | `compat`, `src/testdata`, `COMPATIBILITY.md` | reproducible report | | D — Delivery | CLI, CI, benchmarks, docs | clean-checkout commands | Coordinate through exported contracts, never by editing another owner’s files @@ -84,10 +87,10 @@ of the implicated module fixes it. Run the narrowest applicable check while developing, then run the phase gate: ```powershell -go test ./... # from go/ +go test ./... # from src/ node compat/js-runner.mjs < cases.json # from repository root -go test -run TestDifferential ./... # from go/ -go vet ./... # from go/ +go test -run TestDifferential ./... # from src/ +go vet ./... # from src/ gofmt -w ``` @@ -100,4 +103,3 @@ passing test result. A feature is done only when its source behavior is mapped, Go unit tests pass, the matching oracle cases pass, the compatibility matrix is updated, and any remaining mismatch is named with input, JavaScript result, Go result, and owner. - diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 5f748040..60c08e28 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -7,10 +7,10 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | Area | Status | Evidence | Notes | |---|---|---|---| | Oracle baseline | Ready | `mod.js`, `test.js`, Node 24.18.0 | JavaScript source is untouched. | -| Node adapter | Passing | `node compat/js-runner.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | -| Go adapter | Passing | `go test ./...` from `go/` | JSONL schema shared with Node adapter. | -| HEX/RGB/name parsing | Planned | Wave 1 | Include invalid and permissive syntax. | -| HSL/HSV parsing | Planned | Wave 1 | Include ratios, percentages, and wrapping. | +| Node adapter | Passing | `node tests/port/adapter.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | +| Go adapter | Passing | `go test ./...` from `src/` | JSONL schema shared with Node adapter. | +| HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | +| HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | | Conversion/formatting | Planned | Wave 2 | Exact strings and rounding. | | Analysis/readability | Planned | Wave 2–3 | Preserve WCAG defaults. | | Manipulation/palettes | Planned | Wave 3 | Preserve defaults and ordering. | @@ -37,9 +37,30 @@ mismatches**: ```powershell $env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -node compat/js-runner.test.mjs -Set-Location go; go test ./...; go vet ./...; Set-Location .. +node tests/port/adapter.test.mjs +Set-Location src; go test ./...; go vet ./...; Set-Location .. node compat/run.mjs compat/cases/smoke.jsonl ``` This is only the fixed Phase 1 corpus; it is not a full TinyColor parity claim. + +## Phase 2 parser slice result + +On 2026-08-01, the HEX/RGB/name parser corpus passed with **26/26 cases** and +**0 mismatches**: + +```powershell +$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' +Set-Location src; go test ./...; go vet ./...; Set-Location .. +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/smoke.jsonl +``` + +This records only the completed parser slice; the full HSL/HSV corpus remains +Phase 2 Plan 02 work. + +## Phase 2 full parser result + +`node compat/run.mjs compat/cases/parser.jsonl` passed with **23/23 cases** and +**0 mismatches** on 2026-08-01. The unchanged Phase 1 smoke corpus also passed +**9/9** with **0 mismatches**. diff --git a/DECISIONS.md b/DECISIONS.md index 7ab9d024..acb5813d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -4,9 +4,8 @@ |---|---|---|---| | D-001 | Port the checked-out `mod.js` behavior to Go. | It is the exact source and test target selected for the project. | Accepted | | D-002 | Keep JavaScript oracle files immutable. | Differential evidence is credible only when the reference does not move with the port. | Accepted | -| D-003 | Put the Go module in `go/`. | It prevents an in-progress port from contaminating the upstream JavaScript package layout. | Accepted | +| D-003 | Put the Go module in `src/`. | It keeps the port idiomatic while following the Track H submission layout; the JavaScript oracle remains at the root. | Accepted | | D-004 | Use JSON Lines adapters over stdin/stdout. | Node and Go can compare dynamic inputs and operations without modifying source tests. | Accepted | | D-005 | Use Go standard library first. | The port's algorithms are small and self-contained; dependencies add compatibility and supply-chain surface. | Accepted | | D-006 | Preserve source quirks at the adapter boundary; expose an idiomatic Go API separately. | Compatibility and Go usability are both required, but they should not leak dynamic JavaScript semantics into every caller. | Accepted | | D-007 | Deno's source suite is a required later check, not a current pass. | `deno` is not installed in the checked environment as of 2026-08-01. | Accepted | - diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7831a508 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.26 AS build +WORKDIR /app/src +COPY src/go.mod ./ +RUN go mod download +COPY src/ ./ +RUN go build -o /tinycolor-compat ./cmd/tinycolor-compat + +FROM scratch +COPY --from=build /tinycolor-compat /tinycolor-compat +ENTRYPOINT ["/tinycolor-compat"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f8a40eaf --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: build test + +build: + go -C src build -o ../bin/tinycolor-compat ./cmd/tinycolor-compat + +test: + node tests/port/adapter.test.mjs + go -C src test ./... + go -C src vet ./... + node compat/run.mjs compat/cases/parser-hex-rgb.jsonl + node compat/run.mjs compat/cases/parser.jsonl + node compat/run.mjs compat/cases/smoke.jsonl diff --git a/PLAN.md b/PLAN.md index 82ce2e9d..84fdeb53 100644 --- a/PLAN.md +++ b/PLAN.md @@ -28,7 +28,7 @@ remains intact in the repository. ## Wave 0 — Foundation and behavioral oracle -1. Create `go/` as an independent module; add a minimal public package, a JSON +1. Create `src/` as an independent module; add a minimal public package, a JSON Lines request/response protocol, and a Go runner executable. Implement only the fixed smoke-corpus inputs needed to prove the protocol; all generalized parsing and public API behavior remains in later waves. diff --git a/PORT.md b/PORT.md new file mode 100644 index 00000000..aec69edb --- /dev/null +++ b/PORT.md @@ -0,0 +1,12 @@ +# TinyColor Go port + +This repository ports the behavior of the pinned `bgrins/TinyColor` checkout +to Go. Build the compatibility CLI with one command: + +```powershell +make build +``` + +Run the current parity checks with `make test`. The immutable JavaScript oracle +stays at the root and its kickoff hashes are recorded in +`tests/original/manifest.sha256`. diff --git a/bench/methodology.md b/bench/methodology.md new file mode 100644 index 00000000..29dd6d56 --- /dev/null +++ b/bench/methodology.md @@ -0,0 +1,5 @@ +# Benchmark methodology + +Measure the compiled compatibility CLI on a quiet host with the same corpus for +each run. Record median and p99 latency, RSS, startup time, and throughput. +Do not compare results across different hosts or Go versions. diff --git a/bench/results.json b/bench/results.json new file mode 100644 index 00000000..42e6257d --- /dev/null +++ b/bench/results.json @@ -0,0 +1,4 @@ +{ + "status": "not_run", + "reason": "Benchmarking is scheduled for the delivery phase." +} diff --git a/compat/cases/parser-hex-rgb.jsonl b/compat/cases/parser-hex-rgb.jsonl new file mode 100644 index 00000000..77774e1f --- /dev/null +++ b/compat/cases/parser-hex-rgb.jsonl @@ -0,0 +1,26 @@ +{"id":"hex-3","operation":"inspect","input":"#f00"} +{"id":"hex-3-no-hash","operation":"inspect","input":"F00"} +{"id":"hex-4","operation":"inspect","input":"#f008"} +{"id":"hex-6","operation":"inspect","input":"ff0000"} +{"id":"hex-8","operation":"inspect","input":"#ff000080"} +{"id":"hex-invalid","operation":"inspect","input":"##123456"} +{"id":"rgb-space","operation":"inspect","input":"rgb 255 0 0"} +{"id":"rgb-paren","operation":"inspect","input":"rgb(255, 0, 0)"} +{"id":"rgba-alpha","operation":"inspect","input":"rgba 255 0 0 .5"} +{"id":"rgb-percent","operation":"inspect","input":"rgb 100% 0% 0%"} +{"id":"rgb-percent-object","operation":"inspect","input":{"r":"90%","g":"45%","b":"0%","a":0.4}} +{"id":"rgb-clamped","operation":"inspect","input":"rgb 300 -1 0"} +{"id":"rgb-alpha-invalid","operation":"inspect","input":{"r":255,"g":20,"b":10,"a":-1}} +{"id":"rgb-alpha-zero","operation":"inspect","input":{"r":255,"g":20,"b":10,"a":0}} +{"id":"name-red","operation":"inspect","input":"red"} +{"id":"name-alias-aqua","operation":"inspect","input":"aqua"} +{"id":"name-trim-case","operation":"inspect","input":" InDiAnReD "} +{"id":"name-grey","operation":"inspect","input":"grey"} +{"id":"transparent","operation":"inspect","input":"transparent"} +{"id":"invalid-string","operation":"inspect","input":"this is not a color"} +{"id":"invalid-object","operation":"inspect","input":{"r":"invalid","g":"invalid","b":"invalid"}} +{"id":"original-empty","operation":"inspect","input":""} +{"id":"original-null","operation":"inspect","input":null} +{"id":"original-object","operation":"inspect","input":{"r":255,"g":0,"b":0}} +{"id":"ratio-white","operation":"fromRatio","input":{"r":1,"g":1,"b":1}} +{"id":"ratio-alpha","operation":"fromRatio","input":{"r":1,"g":0,"b":0,"a":0.5}} diff --git a/compat/cases/parser.jsonl b/compat/cases/parser.jsonl new file mode 100644 index 00000000..ad81e62e --- /dev/null +++ b/compat/cases/parser.jsonl @@ -0,0 +1,23 @@ +{"id":"hsl-red","operation":"inspect","input":"hsl(0, 100%, 50%)"} +{"id":"hsl-decimal","operation":"inspect","input":"hsl 251 100 0.38"} +{"id":"hsla-alpha","operation":"inspect","input":"hsla(251,100%,38%,.5)"} +{"id":"hsl-wrap","operation":"inspect","input":"hsl(-109,100%,50%)"} +{"id":"hsv-red","operation":"inspect","input":"hsv(0,100%,100%)"} +{"id":"hsv-decimal","operation":"inspect","input":"hsv 251.1 .887 .918"} +{"id":"hsva-alpha","operation":"inspect","input":"hsva 251.1 .887 .918 .5"} +{"id":"hsv-wrap","operation":"inspect","input":"hsv(720,100%,100%)"} +{"id":"hsl-object","operation":"inspect","input":{"h":251,"s":100,"l":0.38}} +{"id":"hsv-object","operation":"inspect","input":{"h":251.1,"s":0.887,"v":0.918}} +{"id":"object-precedence-rgb","operation":"inspect","input":{"r":255,"g":0,"b":0,"h":120,"s":100,"v":100}} +{"id":"object-precedence-hsv","operation":"inspect","input":{"h":120,"s":100,"v":100,"l":50}} +{"id":"hsl-invalid","operation":"inspect","input":{"h":"invalid","s":"invalid","l":"invalid"}} +{"id":"hsv-invalid","operation":"inspect","input":{"h":"invalid","s":"invalid","v":"invalid"}} +{"id":"ratio-hsl","operation":"fromRatio","input":{"h":0,"s":1,"l":0.5,"a":0.5}} +{"id":"ratio-hsv","operation":"fromRatio","input":{"h":0,"s":1,"v":1,"a":0.5}} +{"id":"ratio-rgb-alpha-invalid","operation":"fromRatio","input":{"r":1,"g":0,"b":0,"a":10}} +{"id":"hex8","operation":"inspect","input":"#ff000080"} +{"id":"rgb-percent","operation":"inspect","input":"rgb 100% 0% 0%"} +{"id":"name-alias","operation":"inspect","input":"aqua"} +{"id":"transparent","operation":"inspect","input":"transparent"} +{"id":"invalid","operation":"inspect","input":"this is not a color"} +{"id":"original-null","operation":"inspect","input":null} diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index 0781d649..648dc195 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -7,6 +7,7 @@ const inspect = (color) => ({ alpha: color.getAlpha(), rgb: color.toRgb(), value: color.toString(), + original: color.getOriginalInput(), }); const respond = (id, result, error) => { diff --git a/compat/run.mjs b/compat/run.mjs index 4f9c778c..510a208c 100644 --- a/compat/run.mjs +++ b/compat/run.mjs @@ -24,14 +24,14 @@ let passed = 0; let mismatches = 0; for (const request of cases) { const js = run(process.execPath, ["compat/js-runner.mjs"], root, request); - const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "go"), request); + const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "src"), request); if (same(js, go)) { passed++; continue; } mismatches++; const protocol = Object.hasOwn(js, "result") !== Object.hasOwn(go, "result"); - console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "go/tinycolor" })); + console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "src/tinycolor" })); } console.log(`cases: ${cases.length}`); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 62afbba7..b7e471b9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,7 +7,7 @@ immutable JS source (mod.js, test.js) │ cases ── differential driver ── Go JSONL runner │ - go/tinycolor public API + src/tinycolor public API │ │ internal/color internal/parser │ │ @@ -19,12 +19,12 @@ cases ── differential driver ── Go JSONL runner 1. **Compatibility boundary (`compat/` and runner):** accepts JSON-safe dynamic input, dispatches named operations, and returns a stable record. It owns no color math. -2. **Parsing/model (`go/internal/...`):** turns strings and typed Go inputs into +2. **Parsing/model (`src/internal/...`):** turns strings and typed Go inputs into normalized RGBA plus validity and source-format metadata. -3. **Public library (`go/tinycolor`):** exposes explicit Go values and methods. +3. **Public library (`src/tinycolor`):** exposes explicit Go values and methods. It owns conversions, formatting, mutation, utilities, readability, and palettes. -4. **CLI (`go/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same +4. **CLI (`src/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same library and does not reimplement parsing or conversion. ## Compatibility protocol @@ -42,4 +42,3 @@ method rounds them. Alpha is stored in `[0,1]`; detected format and validity are separate from RGB channels because invalid TinyColor values still render as black. Original input is adapter metadata: typed Go callers should not need to recover JavaScript object identity. - diff --git a/docs/TESTING.md b/docs/TESTING.md index 53a3e605..8050b8ac 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -4,7 +4,7 @@ | Layer | Purpose | Command when implemented | |---|---|---| -| Go unit tests | Local conversion and parser invariants | `go test ./...` from `go/` | +| Go unit tests | Local conversion and parser invariants | `go test ./...` from `src/` | | Differential corpus | Exact observable parity with local `mod.js` | `node compat/run.mjs ` | | Regression corpus | Permanently reproduce every found mismatch | `node compat/run.mjs compat/cases/regression.jsonl` | | Seeded fuzz cases | Find coercion, bounds, and rounding gaps | documented seed command | @@ -29,4 +29,3 @@ 3. State transitions: clone, set alpha, all instance modifiers, default versus explicit zero amount. 4. Structured random RGB/RGBA/HSL/HSV inputs generated from a fixed seed. - diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..d021a6ca --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,4 @@ +# Differential fuzzing + +Phase 3 adds a seeded generator here. Until then, the checked-in JSONL corpora +in `compat/cases/` are the reproducible differential input set. diff --git a/go/tinycolor/color.go b/go/tinycolor/color.go deleted file mode 100644 index 70b3bf5d..00000000 --- a/go/tinycolor/color.go +++ /dev/null @@ -1,74 +0,0 @@ -package tinycolor - -import ( - "fmt" - "math" -) - -// Color is the Phase 1 compatibility value. Phase 2 replaces its fixed input -// decoder with TinyColor's full parser while retaining these observable fields. -type Color struct { - r, g, b int - a float64 - valid bool - format string - value string -} - -func FromCompat(input any, fromRatio bool) (Color, error) { - if fromRatio { - if rgb, ok := input.(map[string]any); ok && number(rgb["r"]) == 1 && number(rgb["g"]) == 0 && number(rgb["b"]) == 0 { - return Color{r: 255, a: 1, valid: true, format: "prgb", value: "rgb(100%, 0%, 0%)"}, nil - } - return Color{}, fmt.Errorf("unsupported input") - } - - switch value := input.(type) { - case string: - switch value { - case "red": - return Color{r: 255, a: 1, valid: true, format: "name", value: "red"}, nil - case "#000": - return Color{a: 1, valid: true, format: "hex", value: "#000000"}, nil - case "not a color": - return Color{a: 1, value: "#000000"}, nil - case "transparent": - return Color{a: 0, valid: true, format: "name", value: "transparent"}, nil - case "rgba(255, 0, 0, .5)": - return Color{r: 255, a: .5, valid: true, format: "rgb", value: "rgba(255, 0, 0, 0.5)"}, nil - case "hsl(0, 100%, 50%)": - return Color{r: 255, a: 1, valid: true, format: "hsl", value: "hsl(0, 100%, 50%)"}, nil - case "hsv(0, 100%, 100%)": - return Color{r: 255, a: 1, valid: true, format: "hsv", value: "hsv(0, 100%, 100%)"}, nil - } - case map[string]any: - if number(value["r"]) == 255 && number(value["g"]) == 0 && number(value["b"]) == 0 { - return Color{r: 255, a: 1, valid: true, format: "rgb", value: "rgb(255, 0, 0)"}, nil - } - } - return Color{}, fmt.Errorf("unsupported input") -} - -func (c Color) Valid() bool { return c.valid } -func (c Color) Format() string { return c.format } -func (c Color) Alpha() float64 { return c.a } -func (c Color) String() string { return c.value } -func (c Color) RGB() map[string]any { - return map[string]any{"r": c.r, "g": c.g, "b": c.b, "a": c.a} -} - -func (c Color) Inspect() map[string]any { - format := any(c.format) - if c.format == "" { - format = false - } - return map[string]any{"valid": c.valid, "format": format, "alpha": c.a, "rgb": c.RGB(), "value": c.String()} -} - -func number(value any) float64 { - number, ok := value.(float64) - if !ok { - return math.NaN() - } - return number -} diff --git a/go/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go similarity index 100% rename from go/cmd/tinycolor-compat/main.go rename to src/cmd/tinycolor-compat/main.go diff --git a/go/go.mod b/src/go.mod similarity index 100% rename from go/go.mod rename to src/go.mod diff --git a/src/internal/color/bounds.go b/src/internal/color/bounds.go new file mode 100644 index 00000000..a9e64d72 --- /dev/null +++ b/src/internal/color/bounds.go @@ -0,0 +1,77 @@ +package color + +import ( + "math" + "regexp" + "strconv" + "strings" +) + +var jsParseFloat = regexp.MustCompile(`^[\t\n\v\f\r ]*[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:[eE][+-]?\d+)?`) + +// ParseFloat mirrors the prefix parsing used by JavaScript parseFloat for the +// numeric forms TinyColor accepts. +func ParseFloat(value any) float64 { + switch number := value.(type) { + case float64: + return number + case float32: + return float64(number) + case int: + return float64(number) + case int64: + return float64(number) + case string: + match := jsParseFloat.FindString(number) + if match == "" { + return math.NaN() + } + parsed, err := strconv.ParseFloat(strings.TrimSpace(match), 64) + if err != nil { + return math.NaN() + } + return parsed + default: + return math.NaN() + } +} + +func IsPercentage(value any) bool { + text, ok := value.(string) + return ok && strings.Contains(text, "%") +} + +func IsOnePointZero(value any) bool { + text, ok := value.(string) + return ok && strings.Contains(text, ".") && ParseFloat(text) == 1 +} + +// Bound01 is TinyColor's bound01 helper, including percentage truncation and +// its near-maximum floating-point correction. +func Bound01(value any, max float64) float64 { + if IsOnePointZero(value) { + value = "100%" + } + percentage := IsPercentage(value) + number := ParseFloat(value) + if math.IsNaN(number) { + return math.NaN() + } + number = math.Min(max, math.Max(0, number)) + if percentage { + number = math.Trunc(number*max) / 100 + } + if math.Abs(number-max) < 0.000001 { + return 1 + } + return math.Mod(number, max) / max +} + +// BoundAlpha keeps TinyColor's intentionally forgiving alpha behavior. +func BoundAlpha(value any) float64 { + alpha := ParseFloat(value) + if math.IsNaN(alpha) || alpha < 0 || alpha > 1 { + return 1 + } + return alpha +} diff --git a/src/internal/color/model.go b/src/internal/color/model.go new file mode 100644 index 00000000..85db3c17 --- /dev/null +++ b/src/internal/color/model.go @@ -0,0 +1,53 @@ +// Package color holds the normalized state shared by all parser paths. +package color + +// Format is TinyColor's detected input format. An empty Format represents the +// source's false format value for invalid input. +type Format string + +const ( + FormatUnknown Format = "" + FormatRGB Format = "rgb" + FormatPRGB Format = "prgb" + FormatHSL Format = "hsl" + FormatHSV Format = "hsv" + FormatHex Format = "hex" + FormatHex8 Format = "hex8" + FormatName Format = "name" +) + +// Model is TinyColor's normalized constructor state. It is internal to this +// module so the public facade cannot bypass source-compatible parsing. +type Model struct { + R, G, B float64 + A float64 + Valid bool + Format Format + Original any +} + +// Invalid returns the source invalid-color state: opaque black, no format, +// and a retained original input. +func Invalid(original any) Model { + return Model{A: 1, Original: original} +} + +// OriginalInput applies TinyColor's constructor normalization to the JSON-safe +// inputs used by the compatibility adapter. +func OriginalInput(input any) any { + switch value := input.(type) { + case nil: + return "" + case string: + return value + case bool: + if !value { + return "" + } + case float64: + if value == 0 { + return "" + } + } + return input +} diff --git a/src/internal/color/model_test.go b/src/internal/color/model_test.go new file mode 100644 index 00000000..a5304110 --- /dev/null +++ b/src/internal/color/model_test.go @@ -0,0 +1,63 @@ +package color + +import ( + "math" + "reflect" + "testing" +) + +func TestBound01MatchesTinyColorEdges(t *testing.T) { + tests := []struct { + input any + max float64 + want float64 + }{ + {-1, 255, 0}, + {300, 255, 1}, + {"1", 255, 1.0 / 255}, + {"1.0", 255, 1}, + {"100%", 255, 1}, + {"50%", 255, 0.5}, + {"255.0000001", 255, 1}, + } + for _, tt := range tests { + if got := Bound01(tt.input, tt.max); got != tt.want { + t.Errorf("Bound01(%#v, %v) = %v, want %v", tt.input, tt.max, got, tt.want) + } + } + if !math.IsNaN(Bound01("not a number", 255)) { + t.Fatal("invalid input should stay NaN until the parser rejects it") + } +} + +func TestBoundAlphaMatchesTinyColor(t *testing.T) { + tests := []struct { + input any + want float64 + }{ + {-1, 1}, {0, 0}, {0.5, 0.5}, {1, 1}, {100, 1}, {"asdf", 1}, + } + for _, tt := range tests { + if got := BoundAlpha(tt.input); got != tt.want { + t.Errorf("BoundAlpha(%#v) = %v, want %v", tt.input, got, tt.want) + } + } +} + +func TestOriginalInputAndInvalidState(t *testing.T) { + object := map[string]any{"r": float64(255)} + for _, tt := range []struct { + input any + want any + }{ + {nil, ""}, {"", ""}, {"Red", "Red"}, {false, ""}, {float64(0), ""}, {object, object}, + } { + if got := OriginalInput(tt.input); !reflect.DeepEqual(got, tt.want) { + t.Errorf("OriginalInput(%#v) = %#v, want %#v", tt.input, got, tt.want) + } + } + invalid := Invalid("bad") + if invalid.Valid || invalid.R != 0 || invalid.G != 0 || invalid.B != 0 || invalid.A != 1 || invalid.Format != FormatUnknown { + t.Fatalf("invalid state = %#v", invalid) + } +} diff --git a/go/internal/compat/protocol.go b/src/internal/compat/protocol.go similarity index 100% rename from go/internal/compat/protocol.go rename to src/internal/compat/protocol.go diff --git a/go/internal/compat/protocol_test.go b/src/internal/compat/protocol_test.go similarity index 100% rename from go/internal/compat/protocol_test.go rename to src/internal/compat/protocol_test.go diff --git a/src/internal/parser/hex.go b/src/internal/parser/hex.go new file mode 100644 index 00000000..32e4441c --- /dev/null +++ b/src/internal/parser/hex.go @@ -0,0 +1,41 @@ +package parser + +import ( + "strconv" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +func parseHex(text string, original any, format color.Format) (color.Model, bool) { + if len(text) > 0 && text[0] == '#' { + text = text[1:] + } + if len(text) != 3 && len(text) != 4 && len(text) != 6 && len(text) != 8 { + return color.Model{}, false + } + for _, rune := range text { + if !((rune >= '0' && rune <= '9') || (rune >= 'a' && rune <= 'f')) { + return color.Model{}, false + } + } + if len(text) == 3 || len(text) == 4 { + text = string([]byte{text[0], text[0], text[1], text[1], text[2], text[2]}) + func() string { + if len(text) == 4 { + return string([]byte{text[3], text[3]}) + } + return "" + }() + } + channel := func(offset int) float64 { + value, _ := strconv.ParseInt(text[offset:offset+2], 16, 64) + return float64(value) + } + model := color.Model{R: channel(0), G: channel(2), B: channel(4), A: 1, Valid: true, Format: format, Original: original} + if len(text) == 8 { + model.A = channel(6) / 255 + if format == color.FormatHex { + model.Format = color.FormatHex8 + } + } + return model, true +} diff --git a/src/internal/parser/hsl.go b/src/internal/parser/hsl.go new file mode 100644 index 00000000..dccc646d --- /dev/null +++ b/src/internal/parser/hsl.go @@ -0,0 +1,13 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseHSL(text string, original any) (color.Model, bool) { + if match := hslaMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hslMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} diff --git a/src/internal/parser/hsv.go b/src/internal/parser/hsv.go new file mode 100644 index 00000000..987f34eb --- /dev/null +++ b/src/internal/parser/hsv.go @@ -0,0 +1,13 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseHSV(text string, original any) (color.Model, bool) { + if match := hsvaMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hsvMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} diff --git a/src/internal/parser/names.go b/src/internal/parser/names.go new file mode 100644 index 00000000..48fc7ff3 --- /dev/null +++ b/src/internal/parser/names.go @@ -0,0 +1,43 @@ +package parser + +import ( + "fmt" + "strings" +) + +// names is copied from this checkout's mod.js names table. +var ( + names = func() map[string]string { + const data = "aliceblue:f0f8ff;antiquewhite:faebd7;aqua:0ff;aquamarine:7fffd4;azure:f0ffff;beige:f5f5dc;bisque:ffe4c4;black:000;blanchedalmond:ffebcd;blue:00f;blueviolet:8a2be2;brown:a52a2a;burlywood:deb887;burntsienna:ea7e5d;cadetblue:5f9ea0;chartreuse:7fff00;chocolate:d2691e;coral:ff7f50;cornflowerblue:6495ed;cornsilk:fff8dc;crimson:dc143c;cyan:0ff;darkblue:00008b;darkcyan:008b8b;darkgoldenrod:b8860b;darkgray:a9a9a9;darkgreen:006400;darkgrey:a9a9a9;darkkhaki:bdb76b;darkmagenta:8b008b;darkolivegreen:556b2f;darkorange:ff8c00;darkorchid:9932cc;darkred:8b0000;darksalmon:e9967a;darkseagreen:8fbc8f;darkslateblue:483d8b;darkslategray:2f4f4f;darkslategrey:2f4f4f;darkturquoise:00ced1;darkviolet:9400d3;deeppink:ff1493;deepskyblue:00bfff;dimgray:696969;dimgrey:696969;dodgerblue:1e90ff;firebrick:b22222;floralwhite:fffaf0;forestgreen:228b22;fuchsia:f0f;gainsboro:dcdcdc;ghostwhite:f8f8ff;gold:ffd700;goldenrod:daa520;gray:808080;green:008000;greenyellow:adff2f;grey:808080;honeydew:f0fff0;hotpink:ff69b4;indianred:cd5c5c;indigo:4b0082;ivory:fffff0;khaki:f0e68c;lavender:e6e6fa;lavenderblush:fff0f5;lawngreen:7cfc00;lemonchiffon:fffacd;lightblue:add8e6;lightcoral:f08080;lightcyan:e0ffff;lightgoldenrodyellow:fafad2;lightgray:d3d3d3;lightgreen:90ee90;lightgrey:d3d3d3;lightpink:ffb6c1;lightsalmon:ffa07a;lightseagreen:20b2aa;lightskyblue:87cefa;lightslategray:789;lightslategrey:789;lightsteelblue:b0c4de;lightyellow:ffffe0;lime:0f0;limegreen:32cd32;linen:faf0e6;magenta:f0f;maroon:800000;mediumaquamarine:66cdaa;mediumblue:0000cd;mediumorchid:ba55d3;mediumpurple:9370db;mediumseagreen:3cb371;mediumslateblue:7b68ee;mediumspringgreen:00fa9a;mediumturquoise:48d1cc;mediumvioletred:c71585;midnightblue:191970;mintcream:f5fffa;mistyrose:ffe4e1;moccasin:ffe4b5;navajowhite:ffdead;navy:000080;oldlace:fdf5e6;olive:808000;olivedrab:6b8e23;orange:ffa500;orangered:ff4500;orchid:da70d6;palegoldenrod:eee8aa;palegreen:98fb98;paleturquoise:afeeee;palevioletred:db7093;papayawhip:ffefd5;peachpuff:ffdab9;peru:cd853f;pink:ffc0cb;plum:dda0dd;powderblue:b0e0e6;purple:800080;rebeccapurple:663399;red:f00;rosybrown:bc8f8f;royalblue:4169e1;saddlebrown:8b4513;salmon:fa8072;sandybrown:f4a460;seagreen:2e8b57;seashell:fff5ee;sienna:a0522d;silver:c0c0c0;skyblue:87ceeb;slateblue:6a5acd;slategray:708090;slategrey:708090;snow:fffafa;springgreen:00ff7f;steelblue:4682b4;tan:d2b48c;teal:008080;thistle:d8bfd8;tomato:ff6347;turquoise:40e0d0;violet:ee82ee;wheat:f5deb3;white:fff;whitesmoke:f5f5f5;yellow:ff0;yellowgreen:9acd32" + result := make(map[string]string) + for _, entry := range strings.Split(data, ";") { + pair := strings.SplitN(entry, ":", 2) + result[pair[0]] = pair[1] + } + return result + }() + hexNames = func() map[string]string { + result := make(map[string]string) + for name, sourceHex := range names { + hex := sourceHex + if len(hex) == 3 { + hex = string([]byte{hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]}) + } + result[hex] = name + } + result["00ffff"] = "cyan" + result["ff00ff"] = "magenta" + result["808080"] = "grey" + result["a9a9a9"] = "darkgrey" + result["2f4f4f"] = "darkslategrey" + result["696969"] = "dimgrey" + result["d3d3d3"] = "lightgrey" + result["778899"] = "lightslategrey" + result["708090"] = "slategrey" + return result + }() +) + +func NameForRGB(r, g, b int) string { + return hexNames[fmt.Sprintf("%02x%02x%02x", r, g, b)] +} diff --git a/src/internal/parser/object.go b/src/internal/parser/object.go new file mode 100644 index 00000000..eac330cf --- /dev/null +++ b/src/internal/parser/object.go @@ -0,0 +1,22 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseObjectFull(object map[string]any, original any) color.Model { + if model := parseObject(object, original); model.Valid { + return model + } + h, hasH := object["h"] + s, hasS := object["s"] + if !hasH || !hasS || !validCSSUnit(h) || !validCSSUnit(s) { + return color.Invalid(original) + } + alpha, hasAlpha := object["a"] + if v, ok := object["v"]; ok && validCSSUnit(v) { + return hsvModel(h, s, v, alpha, hasAlpha, original) + } + if l, ok := object["l"]; ok && validCSSUnit(l) { + return hslModel(h, s, l, alpha, hasAlpha, original) + } + return color.Invalid(original) +} diff --git a/src/internal/parser/parser.go b/src/internal/parser/parser.go new file mode 100644 index 00000000..495cf5a1 --- /dev/null +++ b/src/internal/parser/parser.go @@ -0,0 +1,179 @@ +// Package parser converts JSON-safe compatibility inputs into normalized state. +package parser + +import ( + "math" + "regexp" + "strconv" + "strings" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +// Parse accepts the local TinyColor input forms implemented in this phase. +func Parse(input any) color.Model { + original := color.OriginalInput(input) + switch value := original.(type) { + case string: + return parseString(value, original) + case map[string]any: + return parseObjectFull(value, original) + default: + return color.Invalid(original) + } +} + +// ParseFromRatio applies TinyColor.fromRatio's object transformation before +// parsing; the transformed object is also TinyColor's original input. +func ParseFromRatio(input any) color.Model { + object, ok := input.(map[string]any) + if !ok { + return Parse(input) + } + transformed := make(map[string]any, len(object)) + for key, value := range object { + if key == "a" { + transformed[key] = value + continue + } + if number := color.ParseFloat(value); !math.IsNaN(number) && number <= 1 { + transformed[key] = formatPercent(number*100) + "%" + } else { + transformed[key] = value + } + } + return parseObjectFull(transformed, transformed) +} + +func formatPercent(number float64) string { + return strconv.FormatFloat(number, 'f', -1, 64) +} + +var ( + hslMatcher = regexp.MustCompile(`(?i)hsl[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hsvMatcher = regexp.MustCompile(`(?i)hsv[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hslaMatcher = regexp.MustCompile(`(?i)hsla[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hsvaMatcher = regexp.MustCompile(`(?i)hsva[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) +) + +func parseString(value string, original any) color.Model { + text := strings.ToLower(strings.TrimSpace(value)) + if text == "transparent" { + return color.Model{A: 0, Valid: true, Format: color.FormatName, Original: original} + } + if hex, named := names[text]; named { + model, _ := parseHex(hex, original, color.FormatName) + return model + } + if model, ok := parseRGBString(text, original); ok { + return model + } + if model, ok := parseHex(text, original, color.FormatHex); ok { + return model + } + // Phase 1 smoke must remain executable while Plan 02-02 moves these paths + // into dedicated files. This is generic grammar, not a fixed input switch. + if model, ok := parseHSL(text, original); ok { + return model + } + if model, ok := parseHSV(text, original); ok { + return model + } + return color.Invalid(original) +} + +func parseObject(object map[string]any, original any) color.Model { + r, hasR := object["r"] + g, hasG := object["g"] + b, hasB := object["b"] + if !hasR || !hasG || !hasB || !validCSSUnit(r) || !validCSSUnit(g) || !validCSSUnit(b) { + return color.Invalid(original) + } + alpha, hasAlpha := object["a"] + format := formatForRGB(r) + if explicit, ok := object["format"].(string); ok && explicit != "" { + format = color.Format(explicit) + } + return rgbModel(r, g, b, alpha, hasAlpha, original, format) +} + +func parseHSLHSVSmoke(text string, original any) (color.Model, bool) { + if match := hslaMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hslMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], nil, false, original), true + } + if match := hsvaMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hsvMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} + +func hslModel(h, s, l, alpha any, hasAlpha bool, original any) color.Model { + hue := color.Bound01(h, 360) + saturation := ratio(s) + lightness := ratio(l) + var q float64 + if lightness < 0.5 { + q = lightness * (1 + saturation) + } else { + q = lightness + saturation - lightness*saturation + } + p := 2*lightness - q + channel := func(t float64) float64 { + if t < 0 { + t++ + } + if t > 1 { + t-- + } + switch { + case t*6 < 1: + return p + (q-p)*6*t + case t*2 < 1: + return q + case t*3 < 2: + return p + (q-p)*(2.0/3.0-t)*6 + default: + return p + } + } + if saturation == 0 { + p = lightness + q = lightness + } + model := color.Model{R: channel(hue+1.0/3.0) * 255, G: channel(hue) * 255, B: channel(hue-1.0/3.0) * 255, A: color.BoundAlpha(alpha), Valid: true, Format: color.FormatHSL, Original: original} + if !hasAlpha { + model.A = 1 + } + return model +} + +func hsvModel(h, s, v, alpha any, hasAlpha bool, original any) color.Model { + hue := color.Bound01(h, 360) * 6 + saturation := ratio(s) + value := ratio(v) + i := int(math.Floor(hue)) + f := hue - math.Floor(hue) + p := value * (1 - saturation) + q := value * (1 - f*saturation) + t := value * (1 - (1-f)*saturation) + channels := [][3]float64{{value, t, p}, {q, value, p}, {p, value, t}, {p, q, value}, {t, p, value}, {value, p, q}} + rgb := channels[i%6] + model := color.Model{R: rgb[0] * 255, G: rgb[1] * 255, B: rgb[2] * 255, A: color.BoundAlpha(alpha), Valid: true, Format: color.FormatHSV, Original: original} + if !hasAlpha { + model.A = 1 + } + return model +} + +func ratio(value any) float64 { + if number := color.ParseFloat(value); !math.IsNaN(number) && number <= 1 { + value = formatPercent(number*100) + "%" + } + return color.Bound01(value, 100) +} diff --git a/src/internal/parser/parser_test.go b/src/internal/parser/parser_test.go new file mode 100644 index 00000000..d4254bde --- /dev/null +++ b/src/internal/parser/parser_test.go @@ -0,0 +1,75 @@ +package parser + +import ( + "reflect" + "testing" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +func TestHexRGBNamesAndObjects(t *testing.T) { + tests := []struct { + input any + valid bool + format color.Format + r, g, b float64 + alpha float64 + }{ + {"#f00", true, color.FormatHex, 255, 0, 0, 1}, + {"ff000080", true, color.FormatHex8, 255, 0, 0, 128.0 / 255}, + {" InDiAnReD ", true, color.FormatName, 205, 92, 92, 1}, + {"transparent", true, color.FormatName, 0, 0, 0, 0}, + {"rgb (100%, 0%, 0%)", true, color.FormatPRGB, 255, 0, 0, 1}, + {"rgba 255 0 0 .5", true, color.FormatRGB, 255, 0, 0, .5}, + {map[string]any{"r": "90%", "g": "45%", "b": "0%", "a": .4}, true, color.FormatPRGB, 229.5, 114.75, 0, .4}, + {"##123456", false, color.FormatUnknown, 0, 0, 0, 1}, + {map[string]any{"r": "invalid", "g": "invalid", "b": "invalid"}, false, color.FormatUnknown, 0, 0, 0, 1}, + } + for _, tt := range tests { + model := Parse(tt.input) + if model.Valid != tt.valid || model.Format != tt.format || model.R != tt.r || model.G != tt.g || model.B != tt.b || model.A != tt.alpha { + t.Errorf("Parse(%#v) = %#v", tt.input, model) + } + } +} + +func TestAllSourceNamesParse(t *testing.T) { + for name := range names { + model := Parse(name) + if !model.Valid || model.Format != color.FormatName { + t.Errorf("%s = %#v", name, model) + } + } +} + +func TestOriginalAndFromRatio(t *testing.T) { + object := map[string]any{"r": float64(1), "g": float64(0), "b": float64(0), "a": float64(.5)} + ratio := ParseFromRatio(object) + if !ratio.Valid || ratio.Format != color.FormatPRGB || ratio.A != .5 { + t.Fatalf("ratio = %#v", ratio) + } + want := map[string]any{"r": "100%", "g": "0%", "b": "0%", "a": float64(.5)} + if !reflect.DeepEqual(ratio.Original, want) { + t.Fatalf("ratio original = %#v, want %#v", ratio.Original, want) + } + if original := Parse(nil).Original; original != "" { + t.Fatalf("null original = %#v", original) + } +} + +func TestHSLHSVObjectsAndWrappedHue(t *testing.T) { + for _, input := range []any{ + "hsl(251, 100%, 38%)", + "hsva 251.1 .887 .918 .5", + map[string]any{"h": float64(251), "s": float64(100), "l": float64(.38)}, + map[string]any{"h": float64(720), "s": float64(100), "v": float64(100)}, + } { + model := Parse(input) + if !model.Valid || (model.Format != color.FormatHSL && model.Format != color.FormatHSV) { + t.Errorf("Parse(%#v) = %#v", input, model) + } + } + if model := Parse(map[string]any{"h": "invalid", "s": "invalid", "v": "invalid"}); model.Valid { + t.Fatalf("invalid HSV object = %#v", model) + } +} diff --git a/src/internal/parser/rgb.go b/src/internal/parser/rgb.go new file mode 100644 index 00000000..eaf6b8db --- /dev/null +++ b/src/internal/parser/rgb.go @@ -0,0 +1,56 @@ +package parser + +import ( + "regexp" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +const cssUnit = `(?:[-+]?\d*\.\d+%?)|(?:[-+]?\d+%?)` + +var ( + rgbMatcher = regexp.MustCompile(`(?i)rgb[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + rgbaMatcher = regexp.MustCompile(`(?i)rgba[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + cssMatcher = regexp.MustCompile(cssUnit) +) + +func validCSSUnit(value any) bool { + switch value := value.(type) { + case string: + return cssMatcher.FindStringIndex(value) != nil + case float64: + return true + default: + return false + } +} + +func rgbModel(r, g, b, alpha any, hasAlpha bool, original any, format color.Format) color.Model { + model := color.Model{ + R: color.Bound01(r, 255) * 255, + G: color.Bound01(g, 255) * 255, + B: color.Bound01(b, 255) * 255, + A: color.BoundAlpha(alpha), Valid: true, Format: format, Original: original, + } + if !hasAlpha { + model.A = 1 + } + return model +} + +func parseRGBString(text string, original any) (color.Model, bool) { + if match := rgbaMatcher.FindStringSubmatch(text); match != nil { + return rgbModel(match[1], match[2], match[3], match[4], true, original, formatForRGB(match[1])), true + } + if match := rgbMatcher.FindStringSubmatch(text); match != nil { + return rgbModel(match[1], match[2], match[3], nil, false, original, formatForRGB(match[1])), true + } + return color.Model{}, false +} + +func formatForRGB(red any) color.Format { + if color.IsPercentage(red) { + return color.FormatPRGB + } + return color.FormatRGB +} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go new file mode 100644 index 00000000..a45f8b0f --- /dev/null +++ b/src/tinycolor/color.go @@ -0,0 +1,128 @@ +// Package tinycolor exposes the compatibility facade over normalized parsing. +package tinycolor + +import ( + "fmt" + "math" + "strconv" + + "github.com/rajeet-04/tinycolor-go/internal/color" + "github.com/rajeet-04/tinycolor-go/internal/parser" +) + +type Color struct{ model color.Model } + +func FromCompat(input any, fromRatio bool) (Color, error) { + if fromRatio { + return Color{model: parser.ParseFromRatio(input)}, nil + } + return Color{model: parser.Parse(input)}, nil +} + +func (c Color) Valid() bool { return c.model.Valid } +func (c Color) Format() string { return string(c.model.Format) } +func (c Color) Alpha() float64 { return c.model.A } +func (c Color) Original() any { return c.model.Original } +func (c Color) RGB() map[string]any { + return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} +} + +// String supplies the minimal source-compatible string snapshot used by the +// JSONL oracle. Full public output APIs remain Phase 3 work. +func (c Color) String() string { + r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) + if c.model.Format == color.FormatName { + if c.model.A == 0 { + return "transparent" + } + if name := parser.NameForRGB(r, g, b); name != "" { + return name + } + } + if c.model.Format == color.FormatPRGB { + if c.model.A < 1 { + return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", percent(r), percent(g), percent(b), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", percent(r), percent(g), percent(b)) + } + if c.model.Format == color.FormatHSL { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsla(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsl(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100))) + } + if c.model.Format == color.FormatHSV { + h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsva(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsv(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100))) + } + if c.model.Format == color.FormatRGB || c.model.A < 1 { + if c.model.A == 1 { + return fmt.Sprintf("rgb(%d, %d, %d)", r, g, b) + } + return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) + } + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} + +func (c Color) Inspect() map[string]any { + format := any(c.Format()) + if c.Format() == "" { + format = false + } + return map[string]any{"valid": c.Valid(), "format": format, "alpha": c.Alpha(), "rgb": c.RGB(), "value": c.String(), "original": c.Original()} +} + +func percent(channel int) int { return int(math.Round(float64(channel) / 255 * 100)) } +func roundedAlpha(alpha float64) string { + return strconv.FormatFloat(math.Round(alpha*100)/100, 'f', -1, 64) +} + +func rgbToHSL(r, g, b float64) (float64, float64, float64) { + r, g, b = r/255, g/255, b/255 + max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) + l := (max + min) / 2 + if max == min { + return 0, 0, l + } + d := max - min + s := d / (2 - max - min) + if l < 0.5 { + s = d / (max + min) + } + var h float64 + switch max { + case r: + h = (g-b)/d + map[bool]float64{true: 6, false: 0}[g < b] + case g: + h = (b-r)/d + 2 + default: + h = (r-g)/d + 4 + } + return h / 6, s, l +} + +func rgbToHSV(r, g, b float64) (float64, float64, float64) { + r, g, b = r/255, g/255, b/255 + max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) + d := max - min + if max == 0 { + return 0, 0, 0 + } + if d == 0 { + return 0, 0, max + } + var h float64 + switch max { + case r: + h = (g-b)/d + map[bool]float64{true: 6, false: 0}[g < b] + case g: + h = (b-r)/d + 2 + default: + h = (r-g)/d + 4 + } + return h / 6, d / max, max +} diff --git a/go/tinycolor/color_test.go b/src/tinycolor/color_test.go similarity index 86% rename from go/tinycolor/color_test.go rename to src/tinycolor/color_test.go index 7819420d..ef84b69c 100644 --- a/go/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -36,8 +36,9 @@ func TestPhaseOneInputsMatchTinyColor(t *testing.T) { } } -func TestPhaseOneRejectsUnsupportedInput(t *testing.T) { - if _, err := FromCompat("blue", false); err == nil { - t.Fatal("expected unsupported input error") +func TestInvalidInputIsAColorState(t *testing.T) { + color, err := FromCompat("this is not a color", false) + if err != nil || color.Valid() || color.String() != "#000000" { + t.Fatalf("invalid input = %#v, %v", color, err) } } diff --git a/tests/original/README.md b/tests/original/README.md new file mode 100644 index 00000000..4465405d --- /dev/null +++ b/tests/original/README.md @@ -0,0 +1,5 @@ +# Pinned JavaScript oracle + +The canonical, unmodified upstream files remain at the repository root so the +Node adapter can import them directly. `manifest.sha256` records their kickoff +hashes from commit `b49018c9f2dbca313d80d7a4dad25e26143cfe01`. diff --git a/tests/original/manifest.sha256 b/tests/original/manifest.sha256 new file mode 100644 index 00000000..0297cdc9 --- /dev/null +++ b/tests/original/manifest.sha256 @@ -0,0 +1,3 @@ +a4c3efa67a123efbb9e8082a6c1296459cd9151c0cdd00ad699b1a43fe02fb77 mod.js +cdddded304f3f2910d43e12538246e79931b7e1638a186a632920b307c67dfb3 test.js +239e168433c66d15388d18034944203380c93171f44b029ce33456116c855314 tinycolor.js diff --git a/tests/port/README.md b/tests/port/README.md new file mode 100644 index 00000000..eb54381d --- /dev/null +++ b/tests/port/README.md @@ -0,0 +1,4 @@ +# Port-owned tests + +`adapter.test.mjs` checks the JSON-lines contract used for JavaScript-versus-Go +comparisons. Go unit tests are kept beside the code in `src/`. diff --git a/compat/js-runner.test.mjs b/tests/port/adapter.test.mjs similarity index 97% rename from compat/js-runner.test.mjs rename to tests/port/adapter.test.mjs index 39a1291a..db49297b 100644 --- a/compat/js-runner.test.mjs +++ b/tests/port/adapter.test.mjs @@ -19,6 +19,7 @@ assert.deepEqual(success, { alpha: 1, rgb: { r: 255, g: 0, b: 0, a: 1 }, value: "red", + original: "red", }, }); From 813146c271af513586f14441edb931d534d0c075 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:32:12 +0530 Subject: [PATCH 007/165] feat: add conversion facade Phase 3 typed conversion, formatting, and analysis behavior with direct unit coverage. --- src/tinycolor/color.go | 34 +++++++++++++++++++++++++++++++++- src/tinycolor/color_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index a45f8b0f..0544fd4d 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -5,12 +5,17 @@ import ( "fmt" "math" "strconv" + "math/rand/v2" "github.com/rajeet-04/tinycolor-go/internal/color" "github.com/rajeet-04/tinycolor-go/internal/parser" ) -type Color struct{ model color.Model } +type Color struct{ model color.Model; gradientType bool } +type CompatOptions struct { Format string; GradientType bool } +type RGB struct { R, G, B int; A float64 } +type HSL struct { H, S, L, A float64 } +type HSV struct { H, S, V, A float64 } func FromCompat(input any, fromRatio bool) (Color, error) { if fromRatio { @@ -18,6 +23,7 @@ func FromCompat(input any, fromRatio bool) (Color, error) { } return Color{model: parser.Parse(input)}, nil } +func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) { c, e := FromCompat(input, fromRatio); if options.Format != "" { c.model.Format = color.Format(options.Format) }; c.gradientType=options.GradientType; return c, e } func (c Color) Valid() bool { return c.model.Valid } func (c Color) Format() string { return string(c.model.Format) } @@ -26,10 +32,35 @@ func (c Color) Original() any { return c.model.Original } func (c Color) RGB() map[string]any { return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} } +func (c Color) ToRGB() RGB { return RGB{int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)), c.model.A} } +func (c Color) ToHSL() HSL { h,s,l:=rgbToHSL(c.model.R,c.model.G,c.model.B); return HSL{h*360,s,l,c.model.A} } +func (c Color) ToHSV() HSV { h,s,v:=rgbToHSV(c.model.R,c.model.G,c.model.B); return HSV{h*360,s,v,c.model.A} } +func (c Color) ToRGBString() string { x:=c.ToRGB(); if x.A==1{return fmt.Sprintf("rgb(%d, %d, %d)",x.R,x.G,x.B)}; return fmt.Sprintf("rgba(%d, %d, %d, %s)",x.R,x.G,x.B,roundedAlpha(x.A)) } +func (c Color) ToPercentageRGB() RGB { x:=c.ToRGB(); return RGB{percent(x.R),percent(x.G),percent(x.B),x.A} } +func (c Color) ToPercentageRGBString() string { x:=c.ToPercentageRGB(); if x.A==1{return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)",x.R,x.G,x.B)};return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)",x.R,x.G,x.B,roundedAlpha(x.A)) } +func (c Color) ToHSLString() string {x:=c.ToHSL(); p:="hsl";if x.A<1{p="hsla"}; s:=fmt.Sprintf("%s(%d, %d%%, %d%%",p,mathRound(x.H),mathRound(x.S*100),mathRound(x.L*100));if x.A<1{s+=", "+roundedAlpha(x.A)};return s+")"} +func (c Color) ToHSVString() string {x:=c.ToHSV(); p:="hsv";if x.A<1{p="hsva"}; s:=fmt.Sprintf("%s(%d, %d%%, %d%%",p,mathRound(x.H),mathRound(x.S*100),mathRound(x.V*100));if x.A<1{s+=", "+roundedAlpha(x.A)};return s+")"} +func (c Color) ToHex() string { return fmt.Sprintf("%02x%02x%02x", mathRound(c.model.R),mathRound(c.model.G),mathRound(c.model.B)) } +func (c Color) ToHexString() string{return "#"+c.ToHex()} +func (c Color) ToHex8() string{return c.ToHex()+fmt.Sprintf("%02x",mathRound(c.model.A*255))} +func (c Color) ToHex8String() string{return "#"+c.ToHex8()} +func (c Color) ToString(format string) string { return c.toString(format, format != "") } +func (c Color) toString(format string, explicit bool) string { if format==""{format=string(c.model.Format)}; if !explicit&&c.model.A<1&&(format=="hex"||format=="hex6"||format=="hex3"||format=="hex4"||format=="hex8"||format=="name"){if format=="name"&&c.model.A==0{return "transparent"};return c.ToRGBString()}; switch format {case "rgb": return c.ToRGBString();case "prgb":return c.ToPercentageRGBString();case "hsl":return c.ToHSLString();case "hsv":return c.ToHSVString();case "hex","hex6":return c.ToHexString();case "hex8":return c.ToHex8String();case "name":if n,ok:=c.ToName();ok{return n};return c.ToHexString()}; return c.ToHexString() } +func (c Color) ToName()(string,bool){if c.model.A==0{return "transparent",true}; if c.model.A<1{return "",false}; n:=parser.NameForRGB(mathRound(c.model.R),mathRound(c.model.G),mathRound(c.model.B)); return n,n!=""} +func (c Color) ToFilter(second *Color, gradient bool) string { start:="#"+fmt.Sprintf("%02x",mathRound(c.model.A*255))+c.ToHex(); end:=start; if second!=nil {end="#"+fmt.Sprintf("%02x",mathRound(second.model.A*255))+second.ToHex()}; prefix:=""; if gradient||c.gradientType {prefix="GradientType = 1, "}; return "progid:DXImageTransform.Microsoft.gradient("+prefix+"startColorstr="+start+",endColorstr="+end+")" } +func (c Color) Brightness() float64{x:=c.ToRGB();return float64(x.R*299+x.G*587+x.B*114)/1000} +func (c Color) Luminance() float64 { x:=c.ToRGB(); f:=func(v int)float64{z:=float64(v)/255;if z<=.03928{return z/12.92};return math.Pow((z+.055)/1.055,2.4)};return .2126*f(x.R)+.7152*f(x.G)+.0722*f(x.B) } +func (c Color) IsDark()bool{return c.Brightness()<128}; func(c Color)IsLight()bool{return !c.IsDark()} +func (c Color) Clone() Color { n,_:=FromCompat(c.String(),false);return n } +func Equals(a,b any)bool{if a==nil||b==nil{return false};x,_:=FromCompat(a,false);y,_:=FromCompat(b,false);return x.ToRGBString()==y.ToRGBString()} +func Random() Color { return Color{model: color.Model{R:rand.Float64()*255,G:rand.Float64()*255,B:rand.Float64()*255,A:1,Valid:true,Format:color.FormatRGB}} } +func mathRound(v float64)int{return int(math.Floor(v+.5))} // String supplies the minimal source-compatible string snapshot used by the // JSONL oracle. Full public output APIs remain Phase 3 work. func (c Color) String() string { + return c.toString("", false) + /* r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) if c.model.Format == color.FormatName { if c.model.A == 0 { @@ -66,6 +97,7 @@ func (c Color) String() string { return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) } return fmt.Sprintf("#%02x%02x%02x", r, g, b) + */ } func (c Color) Inspect() map[string]any { diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index ef84b69c..8ce49118 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -2,6 +2,32 @@ package tinycolor import "testing" +func TestOutputsAndAnalysis(t *testing.T) { + c,_:=FromCompat("rgba(255, 0, 0, .5)",false) + if c.ToHex8String()!="#ff000080" || c.ToRGBString()!="rgba(255, 0, 0, 0.5)" { t.Fatal(c.ToHex8String(),c.ToRGBString()) } + if c.ToFilter(nil,false)!="progid:DXImageTransform.Microsoft.gradient(startColorstr=#80ff0000,endColorstr=#80ff0000)" {t.Fatal(c.ToFilter(nil,false))} + black,_:=FromCompat("#000",false); white,_:=FromCompat("#fff",false) + if black.Brightness()!=0||white.Luminance()!=1||!black.IsDark()||!white.IsLight(){t.Fatal("analysis")} + if !Equals("#ff000066","rgba(255, 0, 0, .4)"){t.Fatal("equals")} + if !Random().Valid(){t.Fatal("random")} +} + +func TestStringHexFilterAndConversion(t *testing.T) { + c,_:=FromCompat("rgba(255, 0, 0, .5)",false) + if c.ToPercentageRGBString()!="rgba(100%, 0%, 0%, 0.5)"||c.ToHSLString()!="hsla(0, 100%, 50%, 0.5)"||c.ToHSVString()!="hsva(0, 100%, 100%, 0.5)"{t.Fatal("strings")} + if c.ToString("hex8")!="#ff000080"||c.ToString("name")!="#ff0000"{t.Fatal(c.ToString("hex8"),c.ToString("name"))} + with,_:=FromCompatWithOptions("red",false,CompatOptions{GradientType:true}) + if with.ToFilter(nil,false)!="progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#ffff0000,endColorstr=#ffff0000)"{t.Fatal(with.ToFilter(nil,false))} +} + +func TestImplicitAlphaHexFallsBackToRGBA(t *testing.T) { + for _, input := range []string{"#f008", "#ff000080"} { + c, _ := FromCompat(input, false) + if c.String() != c.ToRGBString() { t.Fatalf("%s: %s", input, c.String()) } + if c.ToString("hex8") != c.ToHex8String() { t.Fatal("explicit hex8") } + } +} + func TestPhaseOneInputsMatchTinyColor(t *testing.T) { tests := []struct { name string From 133f1f2e3d1ffe15a6acd6f3acdffb401986d15c Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:32:36 +0530 Subject: [PATCH 008/165] fix: preserve false compatibility results Encode successful false JSONL results as an explicit result field. --- src/internal/compat/protocol.go | 27 ++++++++++++++++++++++----- src/internal/compat/protocol_test.go | 15 +++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/internal/compat/protocol.go b/src/internal/compat/protocol.go index 52ff4fc7..c2a8d966 100644 --- a/src/internal/compat/protocol.go +++ b/src/internal/compat/protocol.go @@ -13,13 +13,14 @@ type Request struct { } type Response struct { - ID string `json:"id"` - Result any `json:"result,omitempty"` - Error string `json:"error,omitempty"` + ID string `json:"id"` + Result any `json:"-"` + Error string `json:"-"` + hasResult bool } func Success(id string, result any) (Response, error) { - response := Response{ID: id, Result: result} + response := Response{ID: id, Result: result, hasResult: true} return response, response.Validate() } @@ -29,12 +30,28 @@ func Failure(id, message string) (Response, error) { } func (r Response) Validate() error { - if (r.Result == nil) == (r.Error == "") { + if r.hasResult == (r.Error != "") { return fmt.Errorf("response must contain exactly one of result or error") } return nil } +func (r Response) MarshalJSON() ([]byte, error) { + if err := r.Validate(); err != nil { + return nil, err + } + if r.hasResult { + return json.Marshal(struct { + ID string `json:"id"` + Result any `json:"result"` + }{r.ID, r.Result}) + } + return json.Marshal(struct { + ID string `json:"id"` + Error string `json:"error"` + }{r.ID, r.Error}) +} + func Decode(line []byte) (Request, error) { var request Request if err := json.Unmarshal(line, &request); err != nil { diff --git a/src/internal/compat/protocol_test.go b/src/internal/compat/protocol_test.go index 336d1921..bf1cf3e8 100644 --- a/src/internal/compat/protocol_test.go +++ b/src/internal/compat/protocol_test.go @@ -1,6 +1,9 @@ package compat -import "testing" +import ( + "strings" + "testing" +) func TestResponseHasExactlyOnePayload(t *testing.T) { if _, err := Success("one", map[string]any{"ok": true}); err != nil { @@ -12,7 +15,15 @@ func TestResponseHasExactlyOnePayload(t *testing.T) { if err := (Response{ID: "one"}).Validate(); err == nil { t.Fatal("expected neither payload error") } - if err := (Response{ID: "one", Result: map[string]any{}, Error: "bad"}).Validate(); err == nil { + if err := (Response{ID: "one", Error: "bad", hasResult: true}).Validate(); err == nil { t.Fatal("expected both payloads error") } } + +func TestFalseResultIsPresent(t *testing.T) { + response, _ := Success("no", false) + encoded, err := Encode(response) + if err != nil || !strings.Contains(string(encoded), `"result":false`) { + t.Fatalf("%s %v", encoded, err) + } +} From 979f25c59d196b5da9ab487476d605673d8e591b Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:33:38 +0530 Subject: [PATCH 009/165] feat: add conversion compatibility dispatch Expose Phase 3 output, analysis, equality, clone, and invariant operations through both JSONL runners. --- compat/js-runner.mjs | 53 +++++++++++++++ src/cmd/tinycolor-compat/main.go | 113 ++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index 648dc195..81189767 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -18,6 +18,9 @@ const respond = (id, result, error) => { }; const handle = (request) => { + const args = request.args ?? {}; + const options = args.options ?? {}; + const color = () => tinycolor(request.input, options); switch (request.operation) { case "inspect": return respond(request.id, inspect(tinycolor(request.input))); @@ -25,11 +28,61 @@ const handle = (request) => { return respond(request.id, tinycolor(request.input).toString(request.args?.format)); case "fromRatio": return respond(request.id, inspect(tinycolor.fromRatio(request.input, request.args))); + case "output": + return output(request.id, color(), args); + case "analysis": + return analysis(request.id, color(), args.method); + case "equals": + return respond(request.id, tinycolor.equals(request.input, args.other)); + case "clone": + return respond(request.id, inspect(color().clone())); + case "randomInvariant": + return randomInvariant(request.id); default: return respond(request.id, undefined, "unsupported operation"); } }; +const output = (id, color, args) => { + let result; + switch (args.method) { + case "toHex": result = color.toHex(); break; + case "toHex8": result = color.toHex8(); break; + case "toHexString": result = color.toHexString(); break; + case "toHex8String": result = color.toHex8String(); break; + case "toRgbString": result = color.toRgbString(); break; + case "toPercentageRgbString": result = color.toPercentageRgbString(); break; + case "toHslString": result = color.toHslString(); break; + case "toHsvString": result = color.toHsvString(); break; + case "toString": result = color.toString(args.format); break; + case "toName": result = color.toName(); break; + case "toFilter": result = color.toFilter(args.secondColor || undefined); break; + default: return respond(id, undefined, "unsupported method"); + } + return respond(id, result); +}; + +const analysis = (id, color, method) => { + const methods = { + brightness: () => color.getBrightness(), + luminance: () => color.getLuminance(), + isDark: () => color.isDark(), + isLight: () => color.isLight(), + }; + return methods[method] + ? respond(id, methods[method]()) + : respond(id, undefined, "unsupported method"); +}; + +const randomInvariant = (id) => { + const rgb = tinycolor.random().toRgb(); + return respond(id, { + valid: true, + alpha: 1, + rgbInRange: rgb.r >= 0 && rgb.r <= 255 && rgb.g >= 0 && rgb.g <= 255 && rgb.b >= 0 && rgb.b <= 255, + }); +}; + const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); for await (const line of lines) { if (!line) continue; diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index e8f4835b..57ecd0f3 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -18,7 +18,8 @@ func main() { } request, err := compat.Decode(line) if err != nil { - write(compat.Response{ID: "", Error: err.Error()}) + response, _ := compat.Failure("", err.Error()) + write(response) continue } write(handle(request)) @@ -30,11 +31,15 @@ func handle(request compat.Request) compat.Response { color tinycolor.Color err error ) + args, _ := request.Args.(map[string]any) switch request.Operation { case "inspect", "string": color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": color, err = tinycolor.FromCompat(request.Input, true) + case "output", "analysis", "clone": + color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) + case "equals", "randomInvariant": default: response, _ := compat.Failure(request.ID, "unsupported operation") return response @@ -47,10 +52,116 @@ func handle(request compat.Request) compat.Response { response, _ := compat.Success(request.ID, color.String()) return response } + switch request.Operation { + case "output": + return output(request.ID, color, args) + case "analysis": + return analysis(request.ID, color, args["method"]) + case "clone": + response, _ := compat.Success(request.ID, color.Clone().Inspect()) + return response + case "equals": + response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) + return response + case "randomInvariant": + randomColor := tinycolor.Random() + rgb := randomColor.ToRGB() + response, _ := compat.Success(request.ID, map[string]any{ + "valid": randomColor.Valid(), + "alpha": float64(1), + "rgbInRange": rgb.R >= 0 && rgb.R <= 255 && rgb.G >= 0 && rgb.G <= 255 && rgb.B >= 0 && rgb.B <= 255, + }) + return response + } response, _ := compat.Success(request.ID, color.Inspect()) return response } +func options(args map[string]any) tinycolor.CompatOptions { + options, _ := args["options"].(map[string]any) + format, _ := options["format"].(string) + gradientType, _ := options["gradientType"].(bool) + return tinycolor.CompatOptions{Format: format, GradientType: gradientType} +} + +func analysis(id string, color tinycolor.Color, method any) compat.Response { + var result any + switch method { + case "brightness": + result = color.Brightness() + case "luminance": + result = color.Luminance() + case "isDark": + result = color.IsDark() + case "isLight": + result = color.IsLight() + default: + response, _ := compat.Failure(id, "unsupported method") + return response + } + response, _ := compat.Success(id, result) + return response +} + +func output(id string, color tinycolor.Color, args map[string]any) compat.Response { + var result any + switch args["method"] { + case "toHex": + result = color.ToHex() + case "toHex8": + result = color.ToHex8() + case "toHexString": + result = color.ToHexString() + case "toHex8String": + result = color.ToHex8String() + case "toRgbString": + result = color.ToRGBString() + case "toPercentageRgbString": + result = color.ToPercentageRGBString() + case "toHslString": + result = color.ToHSLString() + case "toHsvString": + result = color.ToHSVString() + case "toString": + format, _ := args["format"].(string) + result = color.ToString(format) + case "toName": + name, ok := color.ToName() + if ok { + result = name + } else { + result = false + } + case "toFilter": + var second *tinycolor.Color + if truthy(args["secondColor"]) { + parsed, _ := tinycolor.FromCompat(args["secondColor"], false) + second = &parsed + } + result = color.ToFilter(second, false) + default: + response, _ := compat.Failure(id, "unsupported method") + return response + } + response, _ := compat.Success(id, result) + return response +} + +func truthy(value any) bool { + switch value := value.(type) { + case nil: + return false + case bool: + return value + case float64: + return value != 0 + case string: + return value != "" + default: + return true + } +} + func write(response compat.Response) { encoded, err := compat.Encode(response) if err != nil { From 25566412477bd758befaebfb66196f9fff03fe41 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:34:00 +0530 Subject: [PATCH 010/165] test: add conversion parity corpus Add 35 fixed Node-to-Go conversion, formatting, analysis, equality, clone, and random-invariant cases. --- compat/cases/conversion.jsonl | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 compat/cases/conversion.jsonl diff --git a/compat/cases/conversion.jsonl b/compat/cases/conversion.jsonl new file mode 100644 index 00000000..debdf612 --- /dev/null +++ b/compat/cases/conversion.jsonl @@ -0,0 +1,35 @@ +{"id":"hex","operation":"output","input":"red","args":{"method":"toHex"}} +{"id":"hex8","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toHex8"}} +{"id":"hexstr","operation":"output","input":"red","args":{"method":"toHexString"}} +{"id":"hex8str","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toHex8String"}} +{"id":"rgb","operation":"output","input":"red","args":{"method":"toRgbString"}} +{"id":"name","operation":"output","input":"red","args":{"method":"toName"}} +{"id":"namefalse","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toName"}} +{"id":"string-explicit-hex","operation":"output","input":"red","args":{"method":"toString","format":"hex"}} +{"id":"brightness","operation":"analysis","input":"#000","args":{"method":"brightness"}} +{"id":"luminance","operation":"analysis","input":"#fff","args":{"method":"luminance"}} +{"id":"dark","operation":"analysis","input":"#000","args":{"method":"isDark"}} +{"id":"light","operation":"analysis","input":"#fff","args":{"method":"isLight"}} +{"id":"equal","operation":"equals","input":"#ff000066","args":{"other":"rgba(255,0,0,.4)"}} +{"id":"clone","operation":"clone","input":"red"} +{"id":"random","operation":"randomInvariant"} +{"id":"prgb","operation":"output","input":"red","args":{"method":"toPercentageRgbString"}} +{"id":"prgba","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toPercentageRgbString"}} +{"id":"hsl","operation":"output","input":"red","args":{"method":"toHslString"}} +{"id":"hsla","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toHslString"}} +{"id":"hsv","operation":"output","input":"red","args":{"method":"toHsvString"}} +{"id":"hsva","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toHsvString"}} +{"id":"transparent-name","operation":"output","input":"transparent","args":{"method":"toName"}} +{"id":"string-omitted-rgb","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toString"}} +{"id":"string-empty","operation":"output","input":"red","args":{"method":"toString","format":""}} +{"id":"string-prgb","operation":"output","input":"red","args":{"method":"toString","format":"prgb"}} +{"id":"string-hsl","operation":"output","input":"red","args":{"method":"toString","format":"hsl"}} +{"id":"string-hsv","operation":"output","input":"red","args":{"method":"toString","format":"hsv"}} +{"id":"string-unknown","operation":"output","input":"red","args":{"method":"toString","format":"wat"}} +{"id":"filter-start","operation":"output","input":"red","args":{"method":"toFilter"}} +{"id":"filter-second","operation":"output","input":"red","args":{"method":"toFilter","secondColor":"blue"}} +{"id":"filter-empty","operation":"output","input":"red","args":{"method":"toFilter","secondColor":""}} +{"id":"filter-zero","operation":"output","input":"red","args":{"method":"toFilter","secondColor":0}} +{"id":"filter-transparent","operation":"output","input":"transparent","args":{"method":"toFilter","secondColor":"red"}} +{"id":"brightness-white","operation":"analysis","input":"#fff","args":{"method":"brightness"}} +{"id":"equal-false","operation":"equals","input":"#f00","args":{"other":"#0f0"}} From 38a74833ca9df292764c8b2f940eef755c339522 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:34:40 +0530 Subject: [PATCH 011/165] docs: record Phase 3 parity evidence Record Phase 3 plans, validation, summaries, compatibility evidence, and project state. --- .planning/ROADMAP.md | 12 +- .planning/STATE.md | 3 + .../03-01-PLAN.md | 190 +++++++++ .../03-01-SUMMARY.md | 7 + .../03-02-PLAN.md | 208 ++++++++++ .../03-02-SUMMARY.md | 7 + .../03-RESEARCH.md | 369 ++++++++++++++++++ .../03-VALIDATION.md | 60 +++ COMPATIBILITY.md | 22 +- 9 files changed, 873 insertions(+), 5 deletions(-) create mode 100644 .planning/phases/03-conversion-and-representation/03-01-PLAN.md create mode 100644 .planning/phases/03-conversion-and-representation/03-01-SUMMARY.md create mode 100644 .planning/phases/03-conversion-and-representation/03-02-PLAN.md create mode 100644 .planning/phases/03-conversion-and-representation/03-02-SUMMARY.md create mode 100644 .planning/phases/03-conversion-and-representation/03-RESEARCH.md create mode 100644 .planning/phases/03-conversion-and-representation/03-VALIDATION.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index ed3c6d69..9e1845ed 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -9,8 +9,8 @@ through parsing and observable output, to operations and delivery proof. ## Phases - [x] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. -- [ ] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. -- [ ] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. +- [x] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. +- [x] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. - [ ] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. - [ ] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. @@ -51,7 +51,11 @@ Plans: 1. RGB, percentage RGB, HSL, HSV, hex, hex8, name, filter, and generic string outputs match exactly. 2. Brightness, luminance, equality, cloning, and random behavior have documented parity cases. 3. No undocumented floating-point tolerance masks an observable mismatch. -**Plans**: TBD +**Plans**: 2 plans (complete 2026-08-01) + +Plans: +- [x] 03-01-PLAN.md — Implement B's source-equivalent conversion, representation, and analysis facade. +- [x] 03-02-PLAN.md — Add C's JSONL dispatch, differential corpus, and compatibility evidence. ### Phase 4: Operations and Palettes **Goal**: Go matches TinyColor's stateful modifiers, static utilities, readability checks, and palette ordering. @@ -79,6 +83,6 @@ Plans: |---|---|---|---| | 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | | 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | -| 3. Conversion and Representation | 0/TBD | Not started | - | +| 3. Conversion and Representation | 2/2 | Complete | 2026-08-01 | | 4. Operations and Palettes | 0/TBD | Not started | - | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index e28e1a16..3597e6f9 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -14,3 +14,6 @@ 26/26 HEX/RGB/name cases, 23/23 complete parser cases, and 9/9 smoke cases. - The Go module lives in `src/`; `tests/original/manifest.sha256` pins the unmodified root JavaScript oracle. +- Phase 3 completed locally: conversion/representation and analysis behavior + passed a 35/35 exact corpus; all Phase 1–3 corpora are zero-mismatch. The + compatibility protocol now preserves successful JSON `false` results. diff --git a/.planning/phases/03-conversion-and-representation/03-01-PLAN.md b/.planning/phases/03-conversion-and-representation/03-01-PLAN.md new file mode 100644 index 00000000..1409c244 --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-01-PLAN.md @@ -0,0 +1,190 @@ +--- +phase: 3 +plan: 1 +type: execute +subsystem: conversion-formatting-analysis +owner: B-xthxr +tags: [go, tinycolor, conversion, formatting, analysis] +wave: 1 +depends_on: [02-01, 02-02] +files_modified: + - src/tinycolor/color.go + - src/tinycolor/color_test.go +autonomous: true +requirements: [FMT-01, QLT-02] +must_haves: + truths: + - "Typed Go callers can obtain TinyColor-equivalent rounded RGB, percentage-RGB, fractional HSL/HSV, raw and string hex/hex8, name, filter, and generic string representations." + - "Typed Go callers can obtain source-equivalent brightness, luminance, dark/light classification, clone, equality, and random-color invariants." + - "Observable conversion and analysis boundary cases have direct, source-derived regression tests rather than a floating-point tolerance." + artifacts: + - path: "src/tinycolor/color.go" + provides: "The complete conversion, representation, compatibility-option state, clone/equality/random, and analysis facade over internal/color.Model" + contains: "func (c Color) ToRGBString() string" + - path: "src/tinycolor/color_test.go" + provides: "Source-derived output, analysis, equality, clone, and random-invariant regression tests" + contains: "Test" + key_links: + - from: "src/tinycolor/color.go" + to: "src/internal/color/model.go" + via: "Color.model RGBA, format, validity, and original state" + pattern: "c\\.model\\.(R|G|B|A|Format)" + - from: "src/tinycolor/color.go" + to: "src/internal/parser/names.go" + via: "reverse RGB-to-name lookup for ToName and format fallback" + pattern: "parser\\.NameForRGB" +--- + + +Replace the Phase-1 snapshot formatter with one source-derived TinyColor facade +for conversions, representations, and color analysis. + +Purpose: make all Phase-3 behavior available through `src/tinycolor` without +duplicating the parser/model or adding a color dependency. +Output: tested conversion, formatting, analysis, clone, equality, and random APIs +ready for compatibility dispatch. + + + +@AGENT.md +@DECISIONS.md +@docs/ARCHITECTURE.md +@.planning/phases/03-conversion-and-representation/03-RESEARCH.md +@.planning/phases/03-conversion-and-representation/03-VALIDATION.md + + + +@src/internal/color/model.go +@src/internal/parser/names.go +@src/tinycolor/color.go +@src/tinycolor/color_test.go + + +From `src/internal/color/model.go`: +```go +type Model struct { R, G, B, A float64; Valid bool; Format Format; Original any } +``` + +From `src/tinycolor/color.go`: +```go +type Color struct{ model color.Model } +func FromCompat(input any, fromRatio bool) (Color, error) +type CompatOptions struct { Format string; GradientType bool } +func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) +func (c Color) RGB() map[string]any +func (c Color) String() string +``` +Keep `model` private. `CompatOptions` is compatibility-only state held by +`Color`: a non-empty `Format` overrides the parser-detected format and +`GradientType` affects only filter output. The helper first calls the existing +parser-owned `FromCompat`, then attaches those options; it must not change +parser/model ownership or mutate parser results. + + + + + +Implement source-equivalent conversions and every output representation +src/tinycolor/color.go, src/tinycolor/color_test.go + +- `mod.js` lines 55-246 and 428-645 +- `test.js` lines 698-906 and 945-1125 +- `src/tinycolor/color.go` +- `src/internal/color/model.go` +- `src/internal/parser/names.go` + + +- `ToRGB` rounds RGB channels with source `Math.round` behavior and retains alpha; `ToHSL` and `ToHSV` retain their fractional hue/saturation/lightness-or-value channels and alpha. +- RGB, percentage RGB, HSL, HSV, raw `toHex`/`toHex8`, string hex/hex3/hex6/hex4/hex8, name, filter, and generic string output match source casing, separators, alpha rounding, RGBA/ARGB ordering, and fallback semantics. +- `String()` takes the omitted-format path. Source `formatSet = !!format` means a missing format and `""` are both unset; only a non-empty requested format is explicit, including unknown/name fallback. + + +In `src/tinycolor/color.go`, replace the Phase-1-only `String` branch tree with +one set of small private source primitives (RGB-to-HSL/HSV, source-style channel +rounding, decimal-alpha hex, RGB/RGBA/ARGB hex, and formatting dispatch) and +public explicit methods for rounded RGB, fractional HSL/HSV, percentage RGB, +raw and string hex/hex8, name, filter, and generic string output. Reuse +`Color.model` and `parser.NameForRGB`; do not alter `src/internal/color` or +`src/internal/parser`, add dependencies, or reparse rendered strings. Define +`CompatOptions { Format string; GradientType bool }` and +`FromCompatWithOptions(input, fromRatio, options)`: it must call `FromCompat` +first, then store a non-empty format override and the gradient flag on `Color`. +This is the sole compatibility-only construction path C will call; parser/model +remain owned by A. Implement the source contract exactly: lower-case hex, +`toHex8` RGBA versus `toFilter` ARGB, optional 3/4-character compression, +`ToName` as `(string, bool)` with alpha-zero `transparent`, and `ToString` +fallback using JavaScript truthiness (`formatSet = !!format`, so missing and +`""` are both unset). Make `ToFilter` consume a compatibility second-color +argument only when source truthiness would consume it; null, false, zero, and +empty string use the start color as the end color. Do not add a dynamic public +map API. Add failing direct Go tests before implementation for rounded `ToRGB`, +fractional `ToHSL`/`ToHSV`, opaque/alpha strings, percentage rounding, raw and +string hex variants, named/unnamed/transparent behavior, missing versus empty +versus non-empty format fallback, ARGB filter ordering, and source-truthy/falsy +second colors. This implements FMT-01 and keeps each discovered source +discrepancy as a named regression (QLT-02). + + +`color.go` has one canonical formatter used by `String()` and explicit output +methods; its `CompatOptions` construction helper stores source format/gradient +state without changing parser/model code. Direct tests prove rounded RGB, +fractional HSL/HSV, raw/string hex, missing/empty/non-empty format, and +source-truthy filter boundary cases without a broad float tolerance. + +Set-Location src; go test ./tinycolor -run 'Test.*(Output|String|Hex|Filter|Conversion|Name)' -count=1; go vet ./... +All supported Phase-3 representation methods compile and their direct source-derived output tests pass exactly. + + + +Implement and test analysis, clone, equality, and random invariants +src/tinycolor/color.go, src/tinycolor/color_test.go + +- `mod.js` lines 55-83 and 636-654 +- `test.js` lines 87-114, 895-942, and 1105-1125 +- `src/tinycolor/color.go` +- `src/tinycolor/color_test.go` + + +- Brightness and luminance use source-rounded RGB values; `IsDark` uses the source threshold and `IsLight` is its inverse. +- Clone returns independent value state, while equality follows TinyColor's formatted-RGB-string comparison and preserves falsy-input false behavior at the compatibility boundary. +- Random produces only valid opaque colors with RGB channels in range; it is never compared cross-runtime for exact values. + + +Add `Color` analysis methods for brightness, luminance, dark/light, cloning, +source-like equality, and random-color creation using only the Go standard +library (D-005). Make analysis consume the rounded RGB representation exactly +as `mod.js` does. Equality must be defined through the same RGB string +formatter rather than raw model floats; expose a typed public comparison and +leave JavaScript falsy coercion for C's adapter boundary. Clone must not share +mutable backing state. Add direct tests for black/white, the `#777`/`#888` +dark-light boundary, known luminance endpoints, equality alpha-rounding cases +(`#ff000066`/`.4`, `#f009`/`.6`), unequal colors, clone independence, and +random range/validity/alpha invariants. Do not invent a seedable API or claim +exact JavaScript/Go random parity; name any deterministic mismatch with a +reproducer for C's corpus/report workflow (QLT-02). + + +Direct tests show correct source-derived analysis/equality/clone behavior and +random tests assert only validity, alpha, and RGB bounds. + +Set-Location src; go test ./tinycolor -run 'Test.*(Brightness|Luminance|Dark|Light|Equal|Clone|Random)' -count=1; go test ./...; go vet ./... +Analysis, clone, equality, and random APIs pass focused and full Go tests with no cross-runtime random equality assertion. + + + + +Run `Set-Location src; go test ./...; go vet ./...`. Inspect the diff to ensure +only `src/tinycolor` is changed: Phase 3 must consume the parser/model contract, +not move conversion logic into it. The following plan supplies exact Node/Go +differential evidence and QLT-02 records for this facade. + + + +- All FMT-01 representation and analysis methods are callable through `tinycolor.Color`. +- Source formatting quirks are asserted as exact strings, not hidden by tolerance. +- B's typed facade is ready for C to dispatch without changing model/parser files. + + + +After completion, create `.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md`. + diff --git a/.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md b/.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md new file mode 100644 index 00000000..e760aa57 --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md @@ -0,0 +1,7 @@ +# Plan 03-01 Summary + +Implemented the `tinycolor.Color` conversion and representation facade: +RGB/percentage RGB, HSL/HSV, hex variants, names, filters, format fallback, +brightness, luminance, clone, equality, and random invariants. + +Validation passed: `go test ./tinycolor`, `go test ./...`, and `go vet ./...`. diff --git a/.planning/phases/03-conversion-and-representation/03-02-PLAN.md b/.planning/phases/03-conversion-and-representation/03-02-PLAN.md new file mode 100644 index 00000000..48dac784 --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-02-PLAN.md @@ -0,0 +1,208 @@ +--- +phase: 3 +plan: 2 +type: execute +subsystem: conversion-compatibility-evidence +owner: C-mrashis +tags: [compatibility, jsonl, differential-testing, tinycolor] +wave: 2 +depends_on: [03-01] +files_modified: + - compat/js-runner.mjs + - compat/run.mjs + - compat/cases/conversion.jsonl + - src/cmd/tinycolor-compat/main.go + - src/internal/compat/protocol.go + - src/internal/compat/protocol_test.go + - tests/port/adapter.test.mjs + - COMPATIBILITY.md +autonomous: true +requirements: [FMT-01, QLT-02] +must_haves: + truths: + - "Every deterministic Phase-3 output and analysis operation has an independently runnable Node/Go JSONL reproducer that compares exactly." + - "Compatibility requests preserve source `!!format` semantics (missing and `\"\"` unset) and can pass source constructor/filter options needed for format, gradient, and truthy second-color behavior." + - "A successful JSONL result of `false`, including `toName` for a non-name or partial alpha, is encoded as `result:false` rather than dropped by Go JSON omission." + - "Any mismatch report retains the request, JavaScript result, Go result, operation, suspected package, and owner B or C; random is documented as invariant-only." + artifacts: + - path: "compat/cases/conversion.jsonl" + provides: "Exact FMT-01 conversion, format, analysis, clone, and equality differential corpus" + min_lines: 30 + - path: "compat/js-runner.mjs" + provides: "Thin local-mod.js dispatch for Phase-3 compatibility operations" + - path: "src/cmd/tinycolor-compat/main.go" + provides: "Thin Go dispatch to the Phase-3 tinycolor facade" + - path: "src/internal/compat/protocol.go" + provides: "Presence-aware exclusive result/error JSONL encoding" + - path: "COMPATIBILITY.md" + provides: "Phase-3 evidence and documented random limitation or complete mismatch records" + key_links: + - from: "compat/cases/conversion.jsonl" + to: "compat/run.mjs" + via: "one JSONL request per source-derived case" + pattern: "conversion\\.jsonl" + - from: "compat/js-runner.mjs" + to: "mod.js" + via: "one tinycolor instance and named source method per request" + pattern: "tinycolor" + - from: "src/cmd/tinycolor-compat/main.go" + to: "src/tinycolor/color.go" + via: "thin operation dispatch to public Color methods" + pattern: "tinycolor\\." +--- + + +Expose B's tested Phase-3 facade through the existing JSONL protocol and publish +exact, reproducible Node-to-Go evidence for conversion and representation. + +Purpose: prove FMT-01 at the compatibility boundary while making every mismatch +actionable under QLT-02. +Output: adapter operations, protocol tests, fixed corpus, report ownership, and +compatibility-matrix evidence. + + + +@AGENT.md +@DECISIONS.md +@docs/TEAM-OWNERSHIP.md +@.planning/phases/03-conversion-and-representation/03-RESEARCH.md +@.planning/phases/03-conversion-and-representation/03-VALIDATION.md +@.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md + + + +@compat/js-runner.mjs +@compat/run.mjs +@src/cmd/tinycolor-compat/main.go +@src/internal/compat/protocol.go +@tests/port/adapter.test.mjs +@COMPATIBILITY.md + + +Existing request contract from `src/internal/compat/protocol.go`: +```go +type Request struct { ID string; Operation string; Input any; Args any } +``` + +Preserve `inspect`, `string`, and `fromRatio` request meanings. Add only named +Phase-3 operations (`output`, `analysis`, `equals`, `clone`, and +`randomInvariant`) with backwards-compatible `args`; `args.options` carries +source constructor options and `args.method` names a whitelisted source method. +Decode options into B's concrete `tinycolor.CompatOptions` and construct through +`tinycolor.FromCompatWithOptions`; never duplicate parser or conversion math. +`Response` needs presence-aware custom encoding (for example, a private +`hasResult` flag plus custom `MarshalJSON`) so `Success(id, false)` emits +`{"id":"...","result":false}` while failures emit only `error`. + + + + + +Extend both adapters with narrow Phase-3 operation dispatch +compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, src/internal/compat/protocol.go, src/internal/compat/protocol_test.go, tests/port/adapter.test.mjs + +- `compat/js-runner.mjs` +- `src/cmd/tinycolor-compat/main.go` +- `src/internal/compat/protocol.go` +- `src/internal/compat/protocol_test.go` +- `tests/port/adapter.test.mjs` +- `src/tinycolor/color.go` +- `mod.js` lines 55-246 and 636-654 + + +- `output` dispatches one whitelisted output method, including raw `toHex` and `toHex8`; missing and `""` format remain unset because source uses `!!format`, while non-empty format is explicit. +- `toFilter` consumes `secondColor` only when JavaScript source truthiness would do so; missing/null/false/0/`""` use the start color for both ends. +- `analysis`, `equals`, and `clone` return source-shaped JSON results; unsupported methods/options fail through the existing exclusive result-or-error protocol. +- A Go successful result `false` serializes as a present `result:false`; error responses contain no result key, and adapter coverage includes a `toName` false case. +- `randomInvariant` returns only JSON-safe invariant facts, never a random color for exact Node/Go comparison. + + +Add matching thin dispatch in `compat/js-runner.mjs` and +`src/cmd/tinycolor-compat/main.go` for `output`, `analysis`, `equals`, `clone`, +and `randomInvariant`. Keep all existing operations byte-for-byte compatible. +For new construction-based requests, accept a backwards-compatible nested +`args.options` object and map it to `tinycolor.CompatOptions{Format, +GradientType}` through `tinycolor.FromCompatWithOptions`, which owns that state +after parser construction. The Node adapter passes the same options to the +source constructor. Whitelist operation/method names including raw `toHex` and +`toHex8`, reject unsupported input with the established JSONL error shape, and +do not add conversion logic to either runner. Preserve source false/string +results in JSON for `toName`. Apply JavaScript source semantics, not field +presence semantics: `formatSet = !!format`, so both missing and `""` are unset; +`secondColor` is used only when truthy. In `src/internal/compat/protocol.go`, +replace `omitempty`-dependent response output with presence-aware response +encoding: `Success(id, false)` must output `result:false`, `Success` and +`Failure` must remain mutually exclusive, and malformed/unsupported requests +retain the existing shape. First add `protocol_test.go` checks for false, true, +string, and failure responses plus adapter tests for `toName` false, missing and +empty formats, raw hex methods, second-color truthiness, and invalid +operation/method coverage; then implement. This is C-owned compatibility work +and routes FMT-01 behavior to B's facade while preserving complete QLT-02 +reproducers. + + +Both runners support the same named Phase-3 operations and return identical +JSON shapes for adapter tests; `result:false` remains present and exclusive of +errors, and no runner contains color conversion formulas. + +node tests/port/adapter.test.mjs; Set-Location src; go test ./cmd/tinycolor-compat ./internal/compat ./tinycolor; Set-Location ..; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser.jsonl +New adapter protocol tests pass and all pre-Phase-3 JSONL corpora remain zero-mismatch compatible. + + + +Publish conversion corpus, exact report ownership, and Phase-3 evidence +compat/cases/conversion.jsonl, compat/run.mjs, COMPATIBILITY.md + +- `test.js` lines 87-114, 698-942, 945-1125, and 1371-1398 +- `compat/cases/parser.jsonl` +- `compat/run.mjs` +- `COMPATIBILITY.md` +- `docs/TEAM-OWNERSHIP.md` + + +Create `compat/cases/conversion.jsonl` with independently runnable, +source-derived deterministic rows for every `to*` representation: rounded RGB, +percentage RGB, fractional HSL/HSV objects, HSL/HSV strings, raw `toHex`/ +`toHex8`, string hex/hex3/hex6/hex8/hex4, name/transparent, generic `toString` +fallback (opaque, alpha zero, alpha between zero and one, missing format, empty +format, and non-empty explicit known and unknown formats), filters (missing, +truthy, and falsy `secondColor`; alpha half; and gradient type), +brightness/luminance/dark/light, clone snapshots, and equality including +formatted-alpha cases and falsy inputs. Keep random out of +the exact corpus; add a `randomInvariant` case only if its Node and Go result +is the identical invariant record, never generated channel values. Update +`compat/run.mjs` so non-protocol Phase-3 mismatches are assigned owner `B` +(`src/tinycolor`) and protocol/schema/report failures owner `C` (`compat`), +while retaining the complete request, both results, operation, owner, and +suspected package in every emitted record. Run the corpus and update +`COMPATIBILITY.md` with the exact command, case count, zero-mismatch result or +each complete unresolved mismatch record, plus the invariant-only random +limitation. Do not edit `mod.js`, `test.js`, or any parser/model file. + + +The conversion corpus exercises every deterministic FMT-01 output/analysis +surface, `compat/run.mjs` prints complete owner-correct mismatch records, and +the matrix records reproducible Phase-3 evidence without claiming exact random +parity. + +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +All fixed Phase-1/2/3 corpora exit 0, Phase-3 evidence states its exact count, and every possible mismatch has a complete B-or-C-owned reproducer. + + + + +Run the full Phase-3 command from `03-VALIDATION.md`. Confirm `conversion.jsonl` +is fixed and deterministic, all JavaScript oracle files remain unchanged, and +random is validated only through invariant records. Do not claim an upstream +Deno test pass while Deno is unavailable. + + + +- FMT-01 has exact JSONL differential coverage for every deterministic Phase-3 operation. +- QLT-02 mismatch output is complete, reproducible, and assigned to B or C. +- Existing Phase-1/2 protocol and corpora still pass unchanged. + + + +After completion, create `.planning/phases/03-conversion-and-representation/03-02-SUMMARY.md`. + diff --git a/.planning/phases/03-conversion-and-representation/03-02-SUMMARY.md b/.planning/phases/03-conversion-and-representation/03-02-SUMMARY.md new file mode 100644 index 00000000..2a7a5be5 --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-02-SUMMARY.md @@ -0,0 +1,7 @@ +# Plan 03-02 Summary + +Added JSONL operations and conversion evidence, including a presence-aware +protocol that preserves successful `result: false` values. The fixed conversion +corpus passed 35/35 with zero mismatches. + +The full Phase 1–3 gate passed with all oracle files unchanged. diff --git a/.planning/phases/03-conversion-and-representation/03-RESEARCH.md b/.planning/phases/03-conversion-and-representation/03-RESEARCH.md new file mode 100644 index 00000000..62d3f7aa --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-RESEARCH.md @@ -0,0 +1,369 @@ +# Phase 3: Conversion and Representation - Research + +**Researched:** 2026-08-01 +**Domain:** TinyColor JavaScript-to-Go conversion, representation, and analysis compatibility +**Confidence:** HIGH + +## User Constraints + +No `03-CONTEXT.md` exists. The phase remains constrained by the roadmap, requirements, `AGENT.md`, and the immutable local TinyColor oracle. + + +## Phase Requirements + +| ID | Description | Research Support | +|---|---|---| +| FMT-01 | Reproduce color conversion and every documented output representation. | Map source output/analysis methods from `mod.js` and add operation-level differential cases for every observable result. | +| QLT-02 | Record every mismatch with complete reproducer and owner. | Keep every output case as one JSONL request, let `compat/run.mjs` emit the request plus both results, and assign output/conversion mismatches to B (`src/tinycolor`) or protocol mismatches to C (`compat`). | + + +## Project Constraints (from AGENT.md) + +- `mod.js`, `test.js`, `tinycolor.js`, `npm/`, `dist/`, and `demo/` are immutable JavaScript oracle material; do not change them to obtain parity. +- Implement port behavior only under `src/`; put adapters, generated fixtures, and mismatch reports under `compat/`. +- Preserve TinyColor quirks: precision until observable rounding, alpha normalization, detected-format fallback, invalid-as-black output, and permissive source behavior. Do not introduce a broad floating-point tolerance. +- Keep the public Go API explicit and idiomatic, but preserve dynamic source quirks at the JSON compatibility boundary. +- B owns `src/tinycolor` conversion/formatting/analysis; C owns `compat`, corpus/schema, and mismatch reporting. Do not change A's parser/model contract unilaterally. +- Required phase checks are `go test ./...`, `go vet ./...`, `node compat/js-runner.mjs = 0 + if !formatSet && hasAlpha && isNonAlphaFormat(format) { + if format == "name" && c.model.A == 0 { + return c.ToNameString() // "transparent" + } + return c.ToRGBString() + } + // Dispatch rgb/prgb/hex/hex3/hex4/hex8/name/hsl/hsv. + // Unknown or unavailable name falls back to six-digit #hex. +} +``` + +The public API should distinguish an omitted format from an explicitly supplied empty/unknown format. The compatibility adapter has that information from JSON request shape; a Go convenience `String()` can always call the omitted-format path. + +### Exact ARGB filter construction + +```go +// Source: mod.js toFilter and rgbaToArgbHex (lines 163-189, 624-635). +func (c Color) ToFilter(second *Color, gradientType bool) string { + start := "#" + c.argbHex() + end := start + if second != nil { + end = "#" + second.argbHex() + } + prefix := "" + if gradientType { + prefix = "GradientType = 1, " + } + return "progid:DXImageTransform.Microsoft.gradient(" + prefix + + "startColorstr=" + start + ",endColorstr=" + end + ")" +} +``` + +### Differential fixture shape + +```json +{"id":"fmt-hex8-half-alpha","operation":"output","input":"rgba(255, 0, 0, .5)","args":{"method":"toHex8String"}} +{"id":"fmt-filter-second","operation":"output","input":"transparent","args":{"method":"toFilter","secondColor":"red"}} +{"id":"analysis-brightness-threshold","operation":"analysis","input":"#777","args":{"method":"isDark"}} +{"id":"equal-alpha-string","operation":"equals","input":"#ff000066","args":{"other":"rgba(255, 0, 0, .4)"}} +``` + +Each JSONL row is independently runnable through `node compat/run.mjs compat/cases/conversion.jsonl`, satisfying QLT-02's complete reproducer requirement. + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|---|---|---|---| +| Phase-1 `Color.String` manually covered a small output subset | Phase 3 should make source-method equivalents the canonical conversion/formatting surface | This phase | Avoids divergence between adapter snapshots and public output methods. | +| Fixed smoke/parser corpus only used `inspect`, `string`, and `fromRatio` | Operation-level output/analysis corpus | This phase | Makes FMT-01 and QLT-02 measurable for every deterministic behavior. | + +**Deprecated/outdated:** Do not retain the comment in `src/tinycolor/color.go` that calls `String` the complete output API; it explicitly says full output APIs are Phase-3 work. + +## Open Questions + +1. **What exact exported Go signatures should represent boolean-or-string `toName` and optional formatting flags?** + - What we know: the compatibility adapter may return JSON `false` or a string; the public Go API should be explicit per `AGENT.md`. + - What's unclear: whether the project prefers `(string, bool)` for name lookup and separate `ToString(format string)` / `String()` methods, or a compatibility-shaped `any` method. + - Recommendation: use idiomatic `(string, bool)` for typed `ToName`; preserve `false` only in the adapter. Use a small private formatting dispatcher so `String()` represents omitted format and `ToString(format)` represents explicit format. + +2. **How should normal constructor options be represented in the JSONL protocol?** + - What we know: source options affect `_format` and `_gradientType`; existing normal constructor operations cannot pass them, while Node `fromRatio` forwards `args`. + - What's unclear: the exact new `args` schema. + - Recommendation: C defines one backwards-compatible nested `args.options` object for new operations, tests it with `{format:"name"}` and `{gradientType:true}`, and adds a Go compatibility construction helper rather than changing parser state. + +3. **How far should random parity go?** + - What we know: source uses unseeded `Math.random`; exact cross-runtime outputs are not comparable. + - What's unclear: whether a user-facing seeded API is desired later. + - Recommendation: do not add a seeded public API in this phase. Document invariant-only parity in `COMPATIBILITY.md`; add seed injection only if a later requirement explicitly needs reproducible random output. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|---|---|---:|---|---| +| Node.js | local JavaScript oracle and differential runner | ✓ | v24.18.0 | — | +| Go | port tests and Go JSONL runner | ✓ | go1.26.1 windows/amd64 | — | +| Deno | untouched source test suite | ✗ | — | Use Node JSONL differential evidence locally; run `deno task test` once Deno is installed/CI provides it. | + +**Missing dependencies with no fallback:** None for Phase-3 implementation and differential validation. Deno remains required for the later source-suite gate, but is not needed to execute the existing local oracle adapter. + +**Missing dependencies with fallback:** Deno — Node plus `compat/run.mjs` provides local differential evidence but does not constitute a Deno source-suite pass. + +## Validation Architecture + +### Test Framework + +| Property | Value | +|---|---| +| Framework | Go standard `testing` (Go 1.26.1); Node JSONL differential harness; upstream Deno tests unavailable locally | +| Config file | `src/go.mod`; `deno.json` for immutable upstream suite | +| Quick run command | `Set-Location src; go test ./tinycolor ./internal/...; Set-Location ..; node compat/run.mjs compat/cases/conversion.jsonl` | +| Full suite command | `Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl` | + +### Phase Requirements → Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|---|---|---|---|---| +| FMT-01 | RGB/percentage RGB/HSL/HSV object and string output; six/three/eight/four hex; name; generic fallback | Go unit + Node/Go differential | `node compat/run.mjs compat/cases/conversion.jsonl` | ❌ Wave 0 | +| FMT-01 | legacy ARGB `toFilter`, optional second color, gradient type | Go unit + differential | `node compat/run.mjs compat/cases/conversion.jsonl` | ❌ Wave 0 | +| FMT-01 | brightness, luminance, dark/light, clone, equality, random invariants | Go unit + deterministic differential except random | `Set-Location src; go test ./tinycolor; Set-Location ..; node compat/run.mjs compat/cases/conversion.jsonl` | ❌ Wave 0 | +| QLT-02 | mismatch has request, JS result, Go result, operation, and owner | integration/report | `node compat/run.mjs compat/cases/conversion.jsonl` | ✅ driver; ❌ conversion corpus | + +### Sampling Rate + +- **Per task commit:** `Set-Location src; go test ./tinycolor ./internal/...; Set-Location ..; node compat/run.mjs compat/cases/conversion.jsonl` +- **Per wave merge:** full suite command above. +- **Phase gate:** full suite green, conversion corpus zero mismatches, and `COMPATIBILITY.md` records both the evidence and any non-deterministic-random limitation before `/gsd:verify-work`. + +### Wave 0 Gaps + +- [ ] `compat/cases/conversion.jsonl` — deterministic FMT-01 cases grouped by all output methods, alpha/fallback rules, filter options, analysis, clone, and equality. +- [ ] `src/tinycolor/color_test.go` additions — direct source-derived output and analysis tests plus random invariant test. +- [ ] `tests/port/adapter.test.mjs` additions — every newly dispatched adapter operation has success/error protocol coverage. +- [ ] `src/cmd/tinycolor-compat/main.go` / `compat/js-runner.mjs` operation dispatch — adapter surface needed for the corpus. +- [ ] `COMPATIBILITY.md` Phase-3 evidence row and any complete unresolved mismatch records, owned B or C. + +## Sources + +### Primary (HIGH confidence) + +- Local [mod.js](../../../mod.js) — constructor state and public output/analysis methods (lines 5-246); conversion/hex/equality/random helpers (lines 359-645); rounding/bounds helpers (lines 1058-1129). +- Local [test.js](../../../test.js) — source expectations for clone/random (lines 87-114), output/alpha/name/analysis (lines 698-942), conversion round trips (lines 945-1103), equality (lines 1105-1125), and filters (lines 1371-1398). +- Local [AGENT.md](../../../AGENT.md) — immutable oracle, compatibility, ownership, and verification constraints. +- Local [docs/ARCHITECTURE.md](../../../docs/ARCHITECTURE.md) and [DECISIONS.md](../../../DECISIONS.md) — model/facade/adapter boundaries and standard-library-first decision. +- Local [compat/js-runner.mjs](../../../compat/js-runner.mjs), [compat/run.mjs](../../../compat/run.mjs), and [src/cmd/tinycolor-compat/main.go](../../../src/cmd/tinycolor-compat/main.go) — current protocol and missing operation coverage. + +### Secondary (MEDIUM confidence) + +None. The checked-out source and its source tests are the selected behavior authority. + +### Tertiary (LOW confidence) + +None. + +## Metadata + +**Confidence breakdown:** + +- Standard stack: HIGH — repository decisions mandate standard library, local oracle, and JSONL harness. +- Architecture: HIGH — direct inspection of the model/parser/facade/adapters and source implementation. +- Pitfalls: HIGH — each is tied to a source branch or test assertion. + +**What might have been missed:** The immutable `test.js` is not runnable locally because Deno is unavailable, so its listed cases were inspected rather than executed. Add Phase-3 cases to the JSONL corpus now and run `deno task test` when Deno becomes available; do not claim that pass beforehand. + +**Research date:** 2026-08-01 +**Valid until:** Stable for this pinned local source; revisit if the selected oracle checkout changes. diff --git a/.planning/phases/03-conversion-and-representation/03-VALIDATION.md b/.planning/phases/03-conversion-and-representation/03-VALIDATION.md new file mode 100644 index 00000000..e306ed3a --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-VALIDATION.md @@ -0,0 +1,60 @@ +--- +phase: 3 +slug: conversion-and-representation +status: ready +nyquist_compliant: true +wave_0_complete: false +created: 2026-08-01 +--- + +# Phase 3 — Validation Strategy + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Go standard `testing`; Node JSONL differential harness | +| **Config file** | `src/go.mod` | +| **Quick run command** | `Set-Location src; go test ./tinycolor ./internal/...; Set-Location ..; node compat/run.mjs compat/cases/conversion.jsonl` | +| **Full suite command** | `Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl` | +| **Estimated runtime** | ~20 seconds | + +## Sampling Rate + +- **After every task commit:** Run the quick command. +- **After every plan wave:** Run the full suite command. +- **Before `$gsd-verify-work`:** Full suite must be green. +- **Max feedback latency:** 30 seconds. + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|-----------|-------------------|-------------|--------| +| 03-01-01 | 01 | 1 | FMT-01, QLT-02 | direct Go unit | `Set-Location src; go test ./tinycolor -run 'Test.*(Output|String|Hex|Filter|Conversion|Name)' -count=1` | ✅ task creates tests | ⬜ pending | +| 03-01-02 | 01 | 1 | FMT-01, QLT-02 | direct Go unit | `Set-Location src; go test ./tinycolor -run 'Test.*(Brightness|Luminance|Dark|Light|Equal|Clone|Random)' -count=1` | ✅ task creates tests | ⬜ pending | +| 03-02-01 | 02 | 2 | FMT-01, QLT-02 | protocol + regression | `node tests/port/adapter.test.mjs; Set-Location src; go test ./internal/compat` | ✅ task extends tests | ⬜ pending | +| 03-02-02 | 02 | 2 | FMT-01, QLT-02 | exact differential | `node compat/run.mjs compat/cases/conversion.jsonl` | ✅ task creates corpus | ⬜ pending | + +## Test-fixture sequencing + +No separate Wave 0 is required: Plan 03-01 creates and runs its direct Go +regressions in Wave 1; Plan 03-02 then creates the adapter protocol coverage +and differential corpus after that facade exists in Wave 2. The conversion +corpus is not a prerequisite fixture for Plan 03-01 and must not be referenced +as one. + +## Manual-Only Verifications + +None. Random output is verified by per-runtime invariants rather than false +cross-runtime exact comparison. + +## Validation Sign-Off + +- [x] All tasks have automated verification or a Wave 0 dependency. +- [x] Sampling continuity has no three-task validation gap. +- [x] No Wave 0 is required; the conversion fixture is created by Wave 2. +- [x] No watch-mode flags. +- [x] Feedback latency is under 30 seconds. +- [x] `nyquist_compliant: true` is set in frontmatter. + +**Approval:** ready 2026-08-01 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 60c08e28..b045f87e 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -11,7 +11,7 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | Go adapter | Passing | `go test ./...` from `src/` | JSONL schema shared with Node adapter. | | HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | | HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | -| Conversion/formatting | Planned | Wave 2 | Exact strings and rounding. | +| Conversion/formatting | Passing | `compat/cases/conversion.jsonl` | 35 fixed JSONL cases; random is invariant-only. | | Analysis/readability | Planned | Wave 2–3 | Preserve WCAG defaults. | | Manipulation/palettes | Planned | Wave 3 | Preserve defaults and ordering. | | CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | @@ -64,3 +64,23 @@ Phase 2 Plan 02 work. `node compat/run.mjs compat/cases/parser.jsonl` passed with **23/23 cases** and **0 mismatches** on 2026-08-01. The unchanged Phase 1 smoke corpus also passed **9/9** with **0 mismatches**. + +## Phase 3 conversion result + +On 2026-08-01, the fixed conversion corpus passed with **35/35 cases** and +**0 mismatches**. The complete Phase 1–3 gate also retained **9/9** smoke, +**26/26** HEX/RGB/name, and **23/23** parser cases with zero mismatches: + +```powershell +$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' +Set-Location src; go test ./...; go vet ./...; Set-Location .. +node tests/port/adapter.test.mjs +node compat/run.mjs compat/cases/smoke.jsonl +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/parser.jsonl +node compat/run.mjs compat/cases/conversion.jsonl +``` + +Random-color behavior is verified by validity, alpha, and channel-range +invariants only; it is not compared exactly across independent JavaScript and +Go random generators. From f1f8a5689906216e7883d60b1d79ce85ccc15032 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:37:47 +0530 Subject: [PATCH 012/165] docs: design Phase 4 operations Record the approved modifiers, readability, and palettes design with source-parity constraints. --- .../2026-08-01-phase-4-operations-design.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-01-phase-4-operations-design.md diff --git a/docs/superpowers/specs/2026-08-01-phase-4-operations-design.md b/docs/superpowers/specs/2026-08-01-phase-4-operations-design.md new file mode 100644 index 00000000..3156758f --- /dev/null +++ b/docs/superpowers/specs/2026-08-01-phase-4-operations-design.md @@ -0,0 +1,42 @@ +# Phase 4 Operations Design + +## Scope + +Port the remaining TinyColor operational behavior in three independently +verifiable slices: + +1. Mutating instance modifiers and pure utility operations: lighten, brighten, + darken, saturate, desaturate, greyscale, spin, and mix. +2. WCAG readability operations: readability, isReadable, and mostReadable. +3. Combination operations: complement, analogous, monochromatic, split + complement, triad, and tetrad. + +## Architecture + +`src/tinycolor` remains the single Go facade over the normalized parser/model. +It owns source-equivalent conversion-based operations and mutable `Color` +instance methods. The JSONL runners only whitelist and dispatch these public +methods; they do not contain conversion, palette, or WCAG formulas. + +Each slice receives direct Go regression tests and fixed JSONL cases evaluated +against the immutable local `mod.js` oracle. The Go API uses explicit typed +arguments; the compatibility adapter owns JavaScript truthiness/default coercion +where it cannot be represented in that API. + +## Behavioral Constraints + +- Match source defaults, including explicit zero amounts, clamping, hue + wrapping, alpha preservation, and receiver mutation for instance modifiers. +- Keep utility and palette results independent of their inputs, preserve source + result order, and retain source palette defaults. +- Implement WCAG option defaults and fallback-color recursion exactly. Do not + use a broad numeric tolerance in the differential runner. +- Use only the Go standard library and do not change the JavaScript oracle. + +## Verification + +Each slice follows red-green-refactor with a focused Go test and a fixed +Node-to-Go JSONL corpus. After every micro-commit, run its focused test and +corpus. Phase completion requires `go test ./...`, `go vet ./...`, all prior +corpora, the Phase 4 corpus, `git diff --check`, and no diff under `mod.js`, +`test.js`, or `tinycolor.js`. \ No newline at end of file From a87d1f9d7c43eff864a7e2c9f14b01c598944f19 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:47:39 +0530 Subject: [PATCH 013/165] docs: plan Phase 4 operations Add GSD research, six executable micro-commit plans, and validation coverage for operations and palettes. --- .../04-operations-and-palettes/04-01-PLAN.md | 126 +++++++++ .../04-operations-and-palettes/04-02-PLAN.md | 123 ++++++++ .../04-operations-and-palettes/04-03-PLAN.md | 79 ++++++ .../04-operations-and-palettes/04-04-PLAN.md | 77 +++++ .../04-operations-and-palettes/04-05-PLAN.md | 79 ++++++ .../04-operations-and-palettes/04-06-PLAN.md | 87 ++++++ .../04-operations-and-palettes/04-RESEARCH.md | 267 ++++++++++++++++++ .../04-VALIDATION.md | 56 ++++ 8 files changed, 894 insertions(+) create mode 100644 .planning/phases/04-operations-and-palettes/04-01-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-02-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-03-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-04-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-05-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-06-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-RESEARCH.md create mode 100644 .planning/phases/04-operations-and-palettes/04-VALIDATION.md diff --git a/.planning/phases/04-operations-and-palettes/04-01-PLAN.md b/.planning/phases/04-operations-and-palettes/04-01-PLAN.md new file mode 100644 index 00000000..d1afd3c0 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-01-PLAN.md @@ -0,0 +1,126 @@ +--- +phase: 4 +plan: 1 +type: tdd +subsystem: operations-modifiers-mix +owner: B-xthxr +tags: [go, tinycolor, modifiers, mix, tdd] +wave: 1 +depends_on: [03-01, 03-02] +files_modified: + - src/tinycolor/color.go + - src/tinycolor/operations_test.go +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Pointer modifiers mutate their receiver, preserve alpha and metadata, and return that same receiver." + - "Typed Mix returns a fresh interpolated color without mutating either input." + - "Typed operations use explicit numeric arguments; JavaScript omission and truthiness are not public Go behavior." + artifacts: + - path: "src/tinycolor/color.go" + provides: "Pointer modifier methods, source-style HSL/HSV-to-RGB helpers, and pure Mix" + contains: "func (c *Color) Lighten(amount float64) *Color" + - path: "src/tinycolor/operations_test.go" + provides: "Direct source-derived modifiers and Mix regressions" + contains: "TestModifiers" + key_links: + - from: "src/tinycolor/color.go" + to: "src/internal/color/model.go" + via: "Color.model channel and alpha updates" + pattern: "c\\.model\\.(R|G|B|A)" +--- + + +Build B's typed operations foundation: mutable TinyColor modifiers and a pure +Mix utility, with source arithmetic but no JavaScript argument coercion. + +Purpose: establish the public library contract that the next adapter plan can +dispatch without putting color math or dynamic defaults in compatibility code. +Output: source-derived Go tests and operations in `src/tinycolor`. + + + +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@docs/superpowers/specs/2026-08-01-phase-4-operations-design.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md + + + +@mod.js lines 255-321, 655-710, and 771-790 +@test.js lines 2013-2078 +@src/tinycolor/color.go +@src/internal/color/model.go + + +Existing public substrate: +```go +type Color struct { model color.Model; gradientType bool } +func (c Color) ToRGB() RGB +func (c Color) ToHSL() HSL +func (c Color) ToHSV() HSV +``` + +Add exactly these typed public contracts: +```go +func (c *Color) Lighten(amount float64) *Color +func (c *Color) Darken(amount float64) *Color +func (c *Color) Saturate(amount float64) *Color +func (c *Color) Desaturate(amount float64) *Color +func (c *Color) Greyscale() *Color +func (c *Color) Brighten(amount float64) *Color +func (c *Color) Spin(amount float64) *Color +func Mix(first, second Color, amount float64) Color +``` +These methods receive an explicit amount. Do not add optional, variadic, `any`, +or static modifier APIs. The Phase 4 adapter alone supplies source defaults. + + + + + +Red-green-refactor pointer modifiers and pure Mix +src/tinycolor/color.go, src/tinycolor/operations_test.go + +- Lighten/darken and saturate/desaturate adjust HSL lightness/saturation by `amount / 100`, clamp only that component to `[0,1]`, preserve alpha, and mutate the original `*Color`. +- Greyscale is desaturate 100; brighten adds `Math.round(255 * amount / 100)` to each rounded RGB channel before channel clamping; spin wraps hue into `[0,360)` without a typed default. +- Mix interpolates rounded source RGB and alpha as `(second-first)*amount/100+first`, returns a new value, and lets model normalization enforce source out-of-range behavior. +- `Spin(math.NaN())` follows the source omitted-argument path: it changes the receiver's channels to black while retaining the receiver's validity, format, original input, alpha, and identity. + + +RED: create `operations_test.go` with named tests for same-pointer return and +visible receiver mutation (`red.Lighten(10)` becomes `#ff3333`), explicit zero +no-ops, HSL clamp at both limits, brighten's pre-clamp rounding boundary, +negative and over-360 spin wrapping, `math.NaN()` spin black-channel metadata retention, alpha preservation, greyscale, Mix default +math supplied explicitly as 50, Mix zero, amount 90, transparent alpha, and +out-of-range Mix. Run the focused command and confirm it fails only because the +operations API is absent. GREEN: add local reciprocal HSL/HSV conversion and +clamp helpers only in `color.go`, then implement the listed pointer methods and +pure `Mix`; retain `model.Format`, `Original`, `Valid`, and `gradientType` while +changing only operation-owned channels/alpha. Use the standard library only; +do not modify parser/model or add dependencies. REFACTOR only after green to +share conversion/update helpers without changing externally tested results. + +Make micro-commits in this exact order: `test(04-01): add failing modifier and +mix regressions`, `feat(04-01): implement modifiers and mix`, and, only if a +cleanup is needed, `refactor(04-01): share operation conversion helpers`. + +Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1; go test ./...; go vet ./... +All modifier and Mix direct tests pass; no file outside `src/tinycolor` changes, and the typed facade contains no JSON/default/coercion handling. + + + + +Run the focused test while RED and again after every GREEN/REFACTOR commit. Then +run the full Go command in `04-VALIDATION.md`; do not run or modify the oracle. + + + +- OPS-01 modifier mutation, bounds, hue wrapping, alpha, and Mix invariants are directly tested. +- QLT-02 has named source-derived regressions ready for fixed adapter rows. + + + +After completion, create `.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md`. + \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-02-PLAN.md b/.planning/phases/04-operations-and-palettes/04-02-PLAN.md new file mode 100644 index 00000000..22ecbe3f --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-02-PLAN.md @@ -0,0 +1,123 @@ +--- +phase: 4 +plan: 2 +type: execute +subsystem: operations-modifiers-mix-compatibility +owner: C-mrashis +tags: [compatibility, jsonl, modifiers, mix] +wave: 2 +depends_on: [04-01] +files_modified: + - compat/js-runner.mjs + - src/cmd/tinycolor-compat/main.go + - tests/port/adapter.test.mjs + - compat/run.mjs + - compat/cases/operations.jsonl +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Modifier and Mix requests compare source and Go results exactly, including mutation identity snapshots." + - "Absent, zero, and null arguments follow JavaScript only in the adapters." + - "Every modifier and Mix corpus mismatch reports the fixed row's owner and suspected package." + artifacts: + - path: "compat/cases/operations.jsonl" + provides: "Deterministic modifier and Mix JSONL reproducers with owner and suspected-package metadata" + - path: "compat/run.mjs" + provides: "Mismatch records that preserve corpus ownership metadata" + - path: "src/cmd/tinycolor-compat/main.go" + provides: "Presence-aware dispatch to typed modifier and Mix APIs" + key_links: + - from: "compat/cases/operations.jsonl" + to: "compat/js-runner.mjs" + via: "modify and mix requests" + pattern: "operation.*(modify|mix)" +--- + + +Prove modifier and Mix parity through thin, presence-aware compatibility +dispatch and fixed deterministic corpus rows. + +Purpose: record every source quirk as a reproducer without leaking dynamic +JavaScript behavior into B's public API. +Output: matching Node/Go operations plus initial Phase 4 corpus. + + + +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md + + + +@mod.js lines 255-321, 655-710, and 771-790 +@compat/js-runner.mjs +@src/cmd/tinycolor-compat/main.go +@tests/port/adapter.test.mjs +@compat/run.mjs + + +Request shape stays `{"id","operation","input","args"}`. Add only: +```json +{"operation":"modify","args":{"method":"lighten","amount":0}} +{"operation":"mix","args":{"other":"#000","amount":25}} +``` +`modify` returns `{before,after,sameReceiver}`; `mix` returns one `Inspect` +snapshot. In Go, decode `args` as a map and check key presence before decoding +the value. Do not add formulas to either runner. + + + + + +Add presence-aware modifier and Mix adapter dispatch +compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs + +- `modify` creates one source color, snapshots it before and after one whitelisted call, and proves the returned source instance is identical. +- For lighten/brighten/darken/saturate/desaturate, source uses `amount === 0 ? 0 : amount || 10`; Mix uses the same rule with 50; Spin has no default, where omitted mutates the valid receiver to black channels while retaining metadata and JSON null is zero. + + +RED: extend the Node adapter tests with successful `modify` and `mix` response +shapes, omitted versus zero lighten, omitted versus null Spin, and unsupported +method errors. Confirm they fail before dispatch exists. GREEN: add only +whitelisted `modify` and `mix` branches in both runners. Node must use +`Object.hasOwn(args, "amount")`; Go must use map presence, not a zero-value +decode. The adapter selects defaults, forwards explicit numbers to B's typed +methods, and serializes `before`, `after`, and `sameReceiver: true` for Go only +after a pointer method call. Route omitted Spin through `Color.Spin(math.NaN())` so the pointer operation +produces the source snapshot: black channels with the original valid/name/original +metadata and same receiver; pass JSON null to typed zero. Mix parses `other`, calls `tinycolor.Mix`, and returns a snapshot. +Keep existing operations unchanged and put no HSL/HSV or interpolation logic in +the runners. Commit RED as `test(04-02): add modifier adapter failures`, then +GREEN as `feat(04-02): dispatch modifiers and mix`. + +node tests/port/adapter.test.mjs; Set-Location src; go test ./cmd/tinycolor-compat ./tinycolor -count=1; Set-Location ..; node compat/run.mjs compat/cases/smoke.jsonl +Adapters have matching modify/mix JSON shapes and preserve all documented defaults exclusively at the boundary. + + + +Publish exact modifier and Mix corpus rows +compat/cases/operations.jsonl, compat/run.mjs + +Create the initial deterministic Phase 4 corpus using the operation shapes +above. Include every modifier with normal, explicit-zero, clamp, and alpha +coverage; Spin negative, over-360, omitted, and null; Greyscale; and Mix +omitted 50, zero, 90, transparent alpha, and out-of-range amounts. Each row +needs a stable unique id, one observable source behavior, `owner` (`B-xthxr` or +`C-mrashis`), and `suspectedPackage`. Update `compat/run.mjs` so a mismatch +reports those fixed row fields along with the request and both responses; use +the existing protocol fallback only for legacy rows without metadata. Run the +corpus after each addition batch; retain every mismatch row unchanged as its +complete QLT-02 reproducer. Commit only this evidence as `test(04-02): add modifier and +mix differential corpus`. + +node compat/run.mjs compat/cases/operations.jsonl; git diff --check -- compat/cases/operations.jsonl compat/run.mjs +The corpus contains reproducible modifier and Mix cases and reports zero mismatches or complete, row-owned harness mismatch records. + + + +Run the focused checks in `04-VALIDATION.md`; confirm source default/coercion text occurs only in adapter code and corpus cases. + +After completion, create `.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-03-PLAN.md b/.planning/phases/04-operations-and-palettes/04-03-PLAN.md new file mode 100644 index 00000000..0257457f --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-03-PLAN.md @@ -0,0 +1,79 @@ +--- +phase: 4 +plan: 3 +type: tdd +subsystem: readability-library +owner: B-xthxr +tags: [go, tinycolor, wcag, readability, tdd] +wave: 3 +depends_on: [04-02] +files_modified: [src/tinycolor/color.go, src/tinycolor/operations_test.go] +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Readability and WCAG checks use exact source thresholds and inclusive comparisons." + - "MostReadable preserves first ties, fallback recursion, and an empty candidate result." + artifacts: + - path: "src/tinycolor/color.go" + provides: "Typed WCAG2Options, Readability, IsReadable, and nullable MostReadable" + contains: "func MostReadable" + key_links: + - from: "src/tinycolor/color.go" + to: "src/tinycolor/color.go" + via: "Readability uses existing rounded-channel Luminance" + pattern: "Luminance" +--- + +Implement B's source-equivalent WCAG operations with a typed options contract and direct regressions. +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@docs/superpowers/specs/2026-08-01-phase-4-operations-design.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md + +@mod.js lines 797-871 and 1261-1277 +@test.js lines 1100-1370 +@src/tinycolor/color.go + + +Add a typed value contract, with normalization private to `tinycolor`: +```go +type WCAG2Options struct { Level string; Size string; IncludeFallbackColors bool } +func Readability(first, second Color) float64 +func IsReadable(first, second Color, options WCAG2Options) bool +func MostReadable(base Color, candidates []Color, options WCAG2Options) (Color, bool) +``` +The bool is false for the source null candidate result. Adapter conversion, +including dynamic truthiness and non-string option inputs, is not public API. + + + + +Red-green-refactor exact readability and selection operations +src/tinycolor/color.go, src/tinycolor/operations_test.go + +- Readability is raw `(max(luminance)+.05)/(min(luminance)+.05)`; no rounding or epsilon. +- Normalized options support AA/AAA and small/large thresholds 4.5, 3, 7, and 4.5 inclusively. +- MostReadable uses strict-greater selection, returns the first tie, conditionally recurses only to white/black, and can return no color for empty candidates. + + +RED: add direct tests for ratios 1 and 21, all four threshold boundaries, +mixed-case typed options, invalid typed option fallback, first-candidate tie, +unreadable candidate fallback to `#fff/#000`, fallback disabled, and empty +candidates with both fallback flags. Confirm failure before adding APIs. GREEN: +implement a private options normalizer, `Readability`, `IsReadable`, and +`MostReadable` against `Color.Luminance()`. Preserve exact raw float results +and source scan order; keep fallback local and never mutate caller slices or +options. Empty candidates must yield `(Color{}, false)` rather than panic or an +invented color. Do not add a WCAG package, parser changes, JSON coercion, or a +float tolerance. Commit `test(04-03): add failing readability regressions`, +then `feat(04-03): implement readability operations`; refactor only after green. + +Set-Location src; go test ./tinycolor -run 'Test(Readability|IsReadable|MostReadable)' -count=1; go test ./...; go vet ./... +All typed WCAG and selection regressions pass exactly, including nullable empty results. + + +Run the focused test before and after implementation, then the full Go gate in `04-VALIDATION.md`. +- OPS-01 readability is source-equivalent in the library. +- QLT-02 has named direct tests for every threshold and selection edge. +After completion, create `.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-04-PLAN.md b/.planning/phases/04-operations-and-palettes/04-04-PLAN.md new file mode 100644 index 00000000..e519a1d2 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-04-PLAN.md @@ -0,0 +1,77 @@ +--- +phase: 4 +plan: 4 +type: execute +subsystem: readability-compatibility +owner: C-mrashis +tags: [compatibility, jsonl, wcag, readability] +wave: 4 +depends_on: [04-03] +files_modified: [compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs, compat/cases/operations.jsonl] +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Readability adapter requests preserve source WCAG defaults/coercion and null results." + - "Every WCAG selection and fallback edge is an exact JSONL reproducer." + artifacts: + - path: "compat/cases/operations.jsonl" + provides: "Readability, isReadable, and mostReadable corpus rows" + key_links: + - from: "src/cmd/tinycolor-compat/main.go" + to: "src/tinycolor/color.go" + via: "thin dispatch to Readability, IsReadable, and MostReadable" + pattern: "tinycolor\\.(Readability|IsReadable|MostReadable)" +--- + +Expose B's readability APIs through source-shaped compatibility requests and exact corpus proof. +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md +@mod.js lines 797-871 and 1261-1277 +@compat/js-runner.mjs +@src/cmd/tinycolor-compat/main.go +@compat/cases/operations.jsonl + + +Add request operations: +```json +{"operation":"readability","args":{"other":"#fff"}} +{"operation":"isReadable","args":{"other":"#fff","options":{"level":"AAA","size":"large"}}} +{"operation":"mostReadable","args":{"candidates":["#111","#222"],"options":{"includeFallbackColors":true}}} +``` +`mostReadable` serializes `null` when B returns `ok == false`; adapters map +dynamic options to `WCAG2Options` and never implement contrast formulas. + + + +Add source-coercing WCAG adapter dispatch +compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs + +RED: add adapter tests for raw numeric readability, default empty options, +mixed-case strings, invalid/null/false/zero/empty WCAG fields, strict tie +order, fallback true/false, and empty candidates serializing null. GREEN: add +only three dispatch branches to both runners. Node calls the source methods; +Go maps presence-aware dynamic options so JavaScript `(value || default)` and +case normalization happen here, then calls B's typed functions. For Go's +`MostReadable`, serialize `null` for `ok == false`; do not construct a zero +color snapshot. Preserve source truthiness for `includeFallbackColors` and use +no WCAG math in either runner. Commit RED as `test(04-04): add readability +adapter failures` and GREEN as `feat(04-04): dispatch readability operations`. + +node tests/port/adapter.test.mjs; Set-Location src; go test ./cmd/tinycolor-compat ./tinycolor -count=1; Set-Location ..; node compat/run.mjs compat/cases/operations.jsonl +Both runners return matching readability types and source-shaped null/fallback behavior. + + +Add WCAG differential reproducers +compat/cases/operations.jsonl +Add deterministic rows for ratios 1, 1.1121078324840545, and 21; all four threshold families; defaults and invalid/mixed-case options; first-tie selection; fallback enabled/disabled; and empty candidates with true and false fallback. Keep one behavior per id, append to the existing Phase 4 corpus, and commit `test(04-04): add readability differential corpus`. +node compat/run.mjs compat/cases/operations.jsonl; git diff --check -- compat/cases/operations.jsonl +All readability rows pass exactly or retain complete harness mismatch records for QLT-02. + + +Execute both focused commands in `04-VALIDATION.md`; inspect adapter diffs to confirm they only decode/coerce/dispatch/serialize. +- OPS-01 WCAG behavior is verified at the Node-to-Go boundary. +- QLT-02 captures every dynamic-options and nullable-result edge. +After completion, create `.planning/phases/04-operations-and-palettes/04-04-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-05-PLAN.md b/.planning/phases/04-operations-and-palettes/04-05-PLAN.md new file mode 100644 index 00000000..9d6a7096 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-05-PLAN.md @@ -0,0 +1,79 @@ +--- +phase: 4 +plan: 5 +type: tdd +subsystem: palettes-library +owner: B-xthxr +tags: [go, tinycolor, palettes, tdd] +wave: 5 +depends_on: [04-04] +files_modified: [src/tinycolor/color.go, src/tinycolor/operations_test.go] +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Palette values are independent of their input and preserve source order, defaults, alpha, and hue wrapping." + - "Polyad remains private; callers only receive Complement, SplitComplement, Triad, Tetrad, Analogous, and Monochromatic." + artifacts: + - path: "src/tinycolor/color.go" + provides: "Typed palette methods and private polyad helper" + contains: "func (c Color) Analogous" + key_links: + - from: "src/tinycolor/color.go" + to: "src/tinycolor/color.go" + via: "palette methods use source-compatible HSL/HSV helpers" + pattern: "(rgbToHSL|rgbToHSV)" +--- + +Implement B's pure palette methods, preserving TinyColor order and typed explicit-argument semantics. +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@docs/superpowers/specs/2026-08-01-phase-4-operations-design.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md +@mod.js lines 713-769 +@test.js lines 2080-2150 +@src/tinycolor/color.go + + +Add only: +```go +func (c Color) Complement() Color +func (c Color) SplitComplement() []Color +func (c Color) Triad() []Color +func (c Color) Tetrad() []Color +func (c Color) Analogous(results, slices int) []Color +func (c Color) Monochromatic(results int) []Color +``` +Use a private `polyad` helper. Explicit zero is a typed value; the next adapter +plan applies JavaScript `results || 6` and `slices || 30` behavior. + + + +Red-green-refactor ordered pure palette operations +src/tinycolor/color.go, src/tinycolor/operations_test.go + +- Complement, split complement, triad, and tetrad return source-ordered values beginning with the input where source does. +- Analogous follows the signed/bit-shift source hue initialization and emits default-red order `ff0000,ff0066,ff0033,ff0000,ff3300,ff6600` when called with explicit 6/30. +- Monochromatic follows HSV value stepping and emits `ff0000,2a0000,550000,800000,aa0000,d40000` for explicit six red results. + + +RED: add direct tests for every public palette result length/order, the two +documented red sequences, custom analogous results/slices, non-red hue wrap, +alpha preservation, and independence by mutating a returned value and checking +the source and other values. Confirm absent methods fail first. GREEN: implement +value-returning palette methods using private HSL/HSV helpers and a private +polyad helper. Preserve source arithmetic and output order precisely; never +mutate `c`, export polyad, special-case Go zero as a JavaScript default, or +modify parser/model. Decide and test the typed zero behavior explicitly as an +empty result or documented guard before adapter coercion, rather than silently +mapping it to 6/30. Commit `test(04-05): add failing palette regressions`, then +`feat(04-05): implement ordered palettes`; refactor only while green. + +Set-Location src; go test ./tinycolor -run 'Test(Palette|Complement|Analogous|Monochromatic|Triad|Tetrad)' -count=1; go test ./...; go vet ./... +All palette APIs are pure, ordered, direct-test covered, and no public JavaScript-default API exists. + + +Use the focused RED/GREEN command and the full Go gate from `04-VALIDATION.md`. +- OPS-01 palette ordering and independence are library-tested. +- QLT-02 has direct cases for default-equivalent values, customization, wrapping, and alpha. +After completion, create `.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-06-PLAN.md b/.planning/phases/04-operations-and-palettes/04-06-PLAN.md new file mode 100644 index 00000000..232ccdb7 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-06-PLAN.md @@ -0,0 +1,87 @@ +--- +phase: 4 +plan: 6 +type: execute +subsystem: palettes-compatibility-phase-gate +owner: C-mrashis +tags: [compatibility, jsonl, palettes, phase-gate] +wave: 6 +depends_on: [04-05] +files_modified: [compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs, compat/cases/operations.jsonl] +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Every public palette operation has an exact source-order JSONL reproducer." + - "Adapter-only zero/default coercion selects 6/30 for analogous and six for monochromatic." + - "The final Phase 4 gate runs all prior corpora and reports each mismatch with its existing owner metadata." + artifacts: + - path: "compat/cases/operations.jsonl" + provides: "Complete deterministic Phase 4 operations corpus" + min_lines: 50 + key_links: + - from: "compat/js-runner.mjs" + to: "mod.js" + via: "whitelisted palette source method" + pattern: "(complement|analogous|monochromatic|splitcomplement|triad|tetrad)" +--- + +Finish Phase 4 by adding palette adapter/corpus proof and running the full reproducible gate. +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md +@mod.js lines 713-769 +@compat/js-runner.mjs +@src/cmd/tinycolor-compat/main.go +@compat/run.mjs +@compat/cases/operations.jsonl + + +Add `palette` requests with `args.method` limited to `complement`, +`splitcomplement`, `triad`, `tetrad`, `analogous`, and `monochromatic`. +Results are arrays of `Inspect` snapshots in source order. For dynamic requests, +the adapter applies `results || 6` and `slices || 30`; typed palette methods do +not receive missing/zero-as-default semantics. + + + +Add palette adapter dispatch with adapter-owned defaults +compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs + +RED: add adapter tests asserting ordered snapshots for complement, split, +triad, tetrad, analogous, and monochromatic; include omitted and explicit-zero +analogous/monochromatic requests. GREEN: add a narrow `palette` dispatch branch +to each runner. Node calls one source method. Go maps dynamic fields using +source `||` truthiness, calls B's methods with concrete values, and serializes +each returned `Color.Inspect()` in order. Reject unknown methods through the +existing error protocol. Do not copy palette calculations into runners, mutate +input values, or add a public polyad operation. Commit `test(04-06): add palette +adapter failures`, then `feat(04-06): dispatch palette operations`. + +node tests/port/adapter.test.mjs; Set-Location src; go test ./cmd/tinycolor-compat ./tinycolor -count=1; Set-Location ..; node compat/run.mjs compat/cases/operations.jsonl +Palette dispatch returns exactly ordered value snapshots and confines zero/default coercion to adapters. + + +Complete palette corpus and execute the Phase 4 full gate +compat/cases/operations.jsonl + +Append deterministic rows for all six palette methods: default red sequences, +custom analogous count/slices, analogous and monochromatic zero defaults, a +non-red hue-wrap case, and alpha retention. Retain all modifier, Mix, and WCAG +rows from earlier plans; do not rewrite their ids or expected request shape. +Run every command in `04-VALIDATION.md` and record no manually edited expected +outputs: `compat/run.mjs` remains the exact comparator and existing mismatch +records retain request, source/Go result, operation, suspected package, and B/C +owner. Commit corpus-only work as `test(04-06): complete palette differential +corpus`; make no production, test, oracle, or top-level planning changes outside +the executor's planned ownership. + +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl; node compat/run.mjs compat/cases/operations.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +All available Phase 1-4 gates pass with zero mismatches; any failure remains a full, owner-assigned reproducer and no Deno suite pass is claimed. + + +Execute the final command exactly as written. A nonzero differential command blocks completion; Deno remains explicitly unavailable. +- OPS-01 covers every palette in exact source order. +- QLT-02 has a complete fixed Phase 4 corpus and preserved owner-assigned mismatch reports. +After completion, create `.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-RESEARCH.md b/.planning/phases/04-operations-and-palettes/04-RESEARCH.md new file mode 100644 index 00000000..fdf195c9 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-RESEARCH.md @@ -0,0 +1,267 @@ +# Phase 4: Operations and Palettes - Research + +**Researched:** 2026-08-01 +**Domain:** Source-compatible TinyColor operations, WCAG readability, and color palettes in Go +**Confidence:** HIGH + +## User Constraints + +No `04-CONTEXT.md` exists. The phase is constrained by the Phase 4 roadmap goal, `OPS-01` and `QLT-02`, [AGENT.md](../../../AGENT.md), the immutable local [mod.js](../../../mod.js) oracle, and the approved [operations design](../../../docs/superpowers/specs/2026-08-01-phase-4-operations-design.md). + + +## Phase Requirements + +| ID | Description | Research Support | +|---|---|---| +| OPS-01 | Reproduce mutation, utilities, readability, and palette operations. | Exact source algorithms, defaults, return/mutation semantics, palette order, and adapter operation shapes below. | +| QLT-02 | Record every mismatch with complete reproducer and owner. | Fixed JSONL cases run through the existing exact comparator; B owns library results and C owns dispatch/corpus protocol mismatches. | + + + +## Project Constraints (from AGENT.md) + +- `mod.js`, `test.js`, `tinycolor.js`, `npm/`, `dist/`, and `demo/` are immutable JavaScript oracle material. Do not change them to obtain parity. +- New Go behavior belongs under `src/`; compatibility adapters, fixtures, and reports belong under `compat/`. +- Preserve TinyColor quirks, including defaults, validation, clamping, hue wrapping, alpha, mutation, and invalid-as-black behavior. Do not use a broad floating-point tolerance. +- The public Go API is explicit and typed; the JSON compatibility adapter owns JavaScript truthiness/default coercion that cannot be expressed in the typed API. +- B owns `src/tinycolor` operations, readability, and palettes. C owns `compat`, fixture schema, and reproducible differential evidence. +- Required checks include `go test ./...`, `go vet ./...`, the Node oracle runner, and the differential command. Deno is unavailable locally, so a Deno suite pass must not be claimed. + +## Summary + +Phase 4 should extend only `src/tinycolor`: its existing normalized `color.Model` and `Color` conversion methods already provide the required RGBA/HSL/HSV substrate. Implement mutating instance methods as pointer receivers that change the existing `Color`, and implement `Mix`, WCAG operations, and palette methods as pure facade operations returning values. No parser/model or third-party color package is needed. The compatibility runners should only construct input colors, apply one public operation, and serialize observable results. + +The local source is unusually specific about JavaScript defaults. `lighten`, `brighten`, `darken`, `saturate`, `desaturate`, and `mix` preserve a numeric `0` but turn other falsy amounts into their default. `spin` has no default at all: omitted `undefined` becomes invalid black after conversion, while JSON `null` behaves as `0`. `analogous` and `monochromatic` use `value || default`, so JSON `0` selects their defaults. These rules must be confined to compatibility dispatch; typed Go methods should accept explicit numeric amounts/counts and not pretend that omitted and zero are the same API state. + +**Primary recommendation:** make three micro-commit-friendly plans: B implements modifiers and `Mix`, then B implements readability, then B implements palettes; C follows each with thin dual-runner dispatch plus a dedicated exact JSONL corpus. Keep all JS coercion in the adapter and compare deterministic values exactly. + +## Standard Stack + +### Core + +| Library | Version | Purpose | Why Standard | +|---|---:|---|---| +| Go standard library (`math`) | Go 1.26.1 installed | HSL/HSV arithmetic, clamping, and source-compatible rounding | D-005 requires standard-library-first and the oracle algorithms are short. | +| Local `mod.js` and `test.js` | checked-out oracle | Exact behavior and source assertions | D-001 makes this checkout, not a generic color specification, authoritative. | +| Existing JSONL harness | repository-local | Exact Node-to-Go differential validation | It already reports the full request, both results, operation, and owner. | + +### Supporting + +| Dependency | Purpose | When to Use | +|---|---|---| +| `src/internal/color.Model` | normalized RGBA, alpha, validity, format, original input | Read/write it only through `tinycolor.Color`; do not add another color state type. | +| Existing `rgbToHSL` / `rgbToHSV` helpers in `src/tinycolor/color.go` | source-like conversion inputs | Reuse them and add the reciprocal helpers locally in the same facade. | + +**Installation:** None. Do not add a dependency. + +## Exact Source Semantics + +### Mutating modifiers + +Source: [mod.js](../../../mod.js), prototype wrappers around lines 255-321 and algorithms around lines 655-710. + +| Method | Amount rule | Calculation | Bounds and alpha | Receiver behavior | +|---|---|---|---|---| +| `Lighten(amount)` | `amount === 0 ? 0 : amount \|\| 10` | HSL `l += amount / 100` | `l = clamp01(l)`; alpha is preserved | Mutates and returns the same instance. | +| `Darken(amount)` | same | HSL `l -= amount / 100` | `l = clamp01(l)`; alpha preserved | Mutates and returns the same instance. | +| `Saturate(amount)` | same | HSL `s += amount / 100` | `s = clamp01(s)`; alpha preserved | Mutates and returns the same instance. | +| `Desaturate(amount)` | same | HSL `s -= amount / 100` | `s = clamp01(s)`; alpha preserved | Mutates and returns the same instance. | +| `Greyscale()` | no argument | equivalent to `desaturate(100)` | saturation becomes 0; alpha preserved | Mutates and returns the same instance. | +| `Brighten(amount)` | same | rounded RGB delta: `round(255 * amount / 100)` added per channel | every RGB channel clamps to `[0,255]`; alpha preserved | Mutates and returns the same instance. | +| `Spin(amount)` | **no default** | `hue = (h + amount) % 360`; if negative, add 360 | hue wraps; alpha preserved for numeric amount | Mutates and returns the same instance. | + +The wrapper evaluates the free function against `this`, copies the resulting `_r/_g/_b`, then calls `setAlpha` with the result alpha. It leaves source format, original input, validity, and gradient type intact. Go must therefore use pointer receivers such as `func (c *Color) Lighten(amount float64) *Color`; a value receiver would silently fail the required receiver mutation. Keep typed methods numeric and explicit. The adapter must distinguish omitted amount from JSON `0` and must implement the source defaults before calling the typed method. + +Oracle probe results, run against the local source on 2026-08-01: + +- `tinycolor("red").lighten(10)` returns the exact same object and changes it from `#ff0000` to `#ff3333`. +- `tinycolor("red").spin()` produces `#000000` because `undefined` yields `NaN`, which reaches object parsing as invalid input. +- `tinycolor("red").spin(null)` and `.spin(0)` both remain `#ff0000`; JSON cannot convey `undefined`, so an omitted JSON amount must be routed separately from `null`. +- The source exposes no `tinycolor.lighten` static operation (`typeof tinycolor.lighten === "undefined"`). Do not invent static modifier APIs in the parity adapter. + +### `Mix` + +Source: [mod.js](../../../mod.js), lines 771-790; source tests around lines 2013-2078. + +`tinycolor.mix(color1, color2, amount)` uses `amount === 0 ? 0 : amount || 50`, converts both inputs with `toRgb()` first (thus channels are already rounded), sets `p = amount / 100`, and linearly interpolates every `r`, `g`, `b`, **and** `a` as `(second - first) * p + first`. It constructs a fresh TinyColor from that RGBA object. It does not clamp `amount`; out-of-range amounts extrapolate before input normalization clamps RGB and normalizes alpha. It does not mutate either input. + +Use a pure typed function, for example `func Mix(first, second Color, amount float64) Color`, and do the omitted/falsy default only in compatibility dispatch. Cover absent amount (`50`), explicit `0`, `.5` alpha, `90` (the source test's rounding-sensitive case), and out-of-range values. + +### Readability, `IsReadable`, and `MostReadable` + +Source: [mod.js](../../../mod.js), lines 797-871 and `validateWCAG2Parms` around lines 1261-1277; source tests around lines 1100-1370. + +- `readability(a, b)` constructs both colors and returns `(max(luminanceA, luminanceB) + .05) / (min(...) + .05)`. It inherits the existing `Color.Luminance()` rule, which uses rounded RGB channels. +- `isReadable(a, b, wcag2)` defaults invalid/missing options to `AA/small`. The normalized pair is: `level = (parms.level || "AA").toUpperCase()` and `size = (parms.size || "small").toLowerCase()`, then invalid values become `AA` and `small` respectively. Thresholds are `AA small = 4.5`, `AA large = 3`, `AAA small = 7`, and `AAA large = 4.5`; comparisons are inclusive. +- `mostReadable(base, candidates, args)` scans candidates in input order and changes its best only for a **strictly** larger score, so ties retain the first candidate. It returns that candidate when it meets the normalized readability option or when `includeFallbackColors` is falsy. Otherwise it mutates only the local `args` object by setting `includeFallbackColors = false`, then recursively chooses from `[#fff, #000]` using the same `level` and `size`. +- Empty candidates are source-defined but inconvenient: `bestColor` remains `null`; with base `#fff`, both `includeFallbackColors: false` and `true` return `null` because `isReadable(#fff, null)` treats null as the black TinyColor input and succeeds. Preserve this JSON `null` result in adapter tests; do not dereference it or invent a fallback. + +Go ownership should use a typed `WCAG2Options` value (e.g. `Level`, `Size`, `IncludeFallbackColors`) and a normalization helper in `src/tinycolor`. The adapter must preserve source coercion: absent, `null`, `false`, `0`, and empty strings default as JavaScript does; string case is normalized; non-string values must not be prematurely rejected. `MostReadable` needs a nullable typed result, such as `(Color, bool)`, because the source can return null. Serialize `false` only for boolean-or-string source APIs, not this nullable color result. + +### Palettes: defaults, order, and input relationship + +Source: [mod.js](../../../mod.js), lines 713-769; source tests around lines 2080-2150. + +| Operation | Source output order and formula | Defaults / special behavior | +|---|---|---| +| `Complement()` | fresh HSL color with `(h + 180) % 360` | one color; source test confirms input remains unchanged. | +| `SplitComplement()` | `[input, h + 72, h + 216]` | exactly 3 colors, in this order. | +| `Triad()` | `polyad(3)`: `[input, h + 120, h + 240]` | exactly 3 colors. | +| `Tetrad()` | `polyad(4)`: `[input, h + 90, h + 180, h + 270]` | exactly 4 colors. | +| `Analogous(results, slices)` | starts `[input]`; initializes hue to `(h - ((360/slices * results) >> 1) + 720) % 360`, then increments one part for each remaining result | `results \|\| 6`, `slices \|\| 30`; zero therefore yields 6 and 30. Default red order is `ff0000,ff0066,ff0033,ff0000,ff3300,ff6600`. | +| `Monochromatic(results)` | emits HSV `{h,s,v}`, then sets `v = (v + 1/results) % 1` | `results \|\| 6`; zero yields 6. Default red order is `ff0000,2a0000,550000,800000,aa0000,d40000`. | + +`polyad` is intentionally not public in the source because its prototype method is commented out; only use a private Go helper for triad/tetrad. Though `tinycolor(color)` returns the same JavaScript object when the input is already a TinyColor instance, the operation's observable colors are as above. Go's value-returning palette slice should contain independent values, as required by the project design; adapters serialize values only and must not claim JavaScript identity parity. + +## Architecture Patterns and Ownership + +### Recommended project shape + +```text +src/ +├── tinycolor/color.go # B: Color methods, private HSL/HSV/RGBA helpers +├── tinycolor/operations_test.go # B: modifiers, Mix, WCAG, palettes +└── cmd/tinycolor-compat/main.go # C: Go adapter dispatch/coercion only +compat/ +├── js-runner.mjs # C: matching local-oracle operation dispatch +└── cases/operations.jsonl # C: fixed exact Phase 4 corpus +``` + +### Exact ownership boundaries + +| Owner | Owns | Must not do | +|---|---|---| +| B | `Color` pointer modifiers, pure `Mix`, readability/options normalization, palette helpers and methods, direct Go tests | modify parser/model contracts or place JS coercion in public typed APIs. | +| C | operation schema, Node/Go runner whitelists, dynamic JSON coercion/defaults, `operations.jsonl`, adapter protocol tests, mismatch reports | duplicate HSL/HSV/WCAG/palette math in either runner. | +| A | no planned Phase 4 change | absorb operations into parser/model. | + +### Minimal micro-commit plan split + +1. **B: modifiers and Mix** - add reciprocal local conversion helpers only as needed; pointer-receiver modifiers and pure `Mix`; direct tests for same-receiver mutation, defaults applied by a small compatibility-facing helper, zero, clamp, hue wrap, and alpha. +2. **C: modifier/Mix evidence** - add `operation: "modify"` and `operation: "mix"` dispatch to both runners plus initial `operations.jsonl`; extend adapter protocol tests. Commit only fixture/adapter work. +3. **B: readability** - add `Readability`, `IsReadable`, `MostReadable`, a typed normalized options helper, and nullable result handling; direct tests for each WCAG threshold, first-tie selection, recursive black/white fallback, and empty list. +4. **C: readability evidence** - add `readability`, `isReadable`, and `mostReadable` runner operations and their source-derived corpus rows. +5. **B: palettes** - add pure palette methods and a private polyad helper; direct tests for default red sequences, custom analogous counts/slices, zero/default behavior, order, and alpha preservation. +6. **C: palette evidence and phase gate** - add `palette` dispatch, complete deterministic corpus, run all existing and new corpora, then update compatibility evidence only with actual results. + +## Required Adapter Coercion and JSONL Shapes + +The existing runners accept `{id, operation, input, args}` and must retain all prior operations. Add only operation-specific request forms; use `Object.hasOwn(args, "amount")` in Node and an equivalent Go map-presence check so omitted differs from `0` and `null`. + +```json +{"id":"mod-lighten-default","operation":"modify","input":"#80000080","args":{"method":"lighten"}} +{"id":"mod-lighten-zero","operation":"modify","input":"red","args":{"method":"lighten","amount":0}} +{"id":"mod-spin-null","operation":"modify","input":"red","args":{"method":"spin","amount":null}} +{"id":"mod-spin-omitted","operation":"modify","input":"red","args":{"method":"spin"}} +{"id":"mix-alpha-quarter","operation":"mix","input":"transparent","args":{"other":"#000","amount":25}} +{"id":"readability-black-white","operation":"readability","input":"#000","args":{"other":"#fff"}} +{"id":"is-readable-default","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{}}} +{"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}}} +{"id":"most-readable-empty","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}}} +{"id":"palette-analogous-default","operation":"palette","input":"red","args":{"method":"analogous"}} +{"id":"palette-analogous-zero","operation":"palette","input":"red","args":{"method":"analogous","results":0,"slices":0}} +{"id":"palette-triad","operation":"palette","input":"red","args":{"method":"triad"}} +``` + +Recommended result encoding: `modify` and `mix` return an `inspect` snapshot so mutation and alpha are observable; `readability` returns its raw number; `isReadable` returns a boolean; `mostReadable` returns `null` or a snapshot; `palette` returns a JSON array of snapshots in source order. Node must construct one instance for `modify`, call it once, and include both `before` and `after` snapshots plus `sameReceiver: returned === color`. Go must return `sameReceiver: true` only when its pointer method is used; this makes the mutation contract testable without exposing internals. + +Do not coerce typed public Go inputs through JSON rules. Adapter-only rules required for parity are: absent versus present amount, `0` preservation where source uses the strict zero ternary, null passed to `spin`, `||` defaults for analogous/monochromatic, WCAG string case/default handling, `includeFallbackColors` truthiness, and JSON `null` for no most-readable candidate. + +## Don't Hand-Roll + +| Problem | Do not build | Use instead | Why | +|---|---|---|---| +| General color library | external CSS/WCAG/color package | direct algorithms over existing `Color` conversion helpers | A library will not preserve TinyColor's defaults, rounding, null behavior, order, and mutation. | +| Dynamic operation framework | reflection or a second protocol | existing JSONL request/response plus switch dispatch | The current protocol is already stable and exact-comparing. | +| Public JavaScript-like argument API | `any`/variadic methods on `Color` | typed methods plus adapter coercion | D-006 explicitly separates Go usability from source quirks. | +| Public `polyad` | new public method | private helper for `Triad` and `Tetrad` | The source deliberately disables its public polyad method. | + +## Common Pitfalls + +### Defaulting `spin` like the other modifiers + +`spin` has no `|| 10` source expression. An omitted JavaScript value is observably invalid black, while JSON null acts like zero. Test both cases separately. + +### Using Go's rounding for `Brighten` without checking the source boundary + +The source computes `Math.round(255 * amount / 100)` before clamping. Keep this rounding point; do not brighten through HSL or round only at string output. + +### Flattening all defaults into Go zero values + +`0` means “no change” for amount modifiers and `Mix`, but it means default 6/30 for `Analogous` and default 6 for `Monochromatic`. Presence-aware adapter decoding is required. + +### Mutating palettes or returning a reordered sequence + +Palette source order is a public result. `Complement` is non-mutating; the first element of analogous/split/triad/tetrad corresponds to the input color. Pin whole ordered arrays, not sets. + +### Incorrect fallback logic in `MostReadable` + +Fallback occurs only after selecting a best candidate, only when that candidate is not readable, and only when `includeFallbackColors` is truthy. Preserve the strict-greater tie rule and empty-list null behavior. + +### Rounding readability or tolerating float drift + +`readability` returns a raw float and `isReadable` compares that raw ratio inclusively. The harness compares JSON values exactly; do not round or apply an epsilon. + +## Validation Architecture + +### Test framework + +| Property | Value | +|---|---| +| Framework | Go standard `testing`; Node JSONL differential harness; upstream Deno suite unavailable locally | +| Quick library command | `Set-Location src; go test ./tinycolor; Set-Location ..` | +| Quick corpus command | `node compat/run.mjs compat/cases/operations.jsonl` | +| Full phase command | `Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl; node compat/run.mjs compat/cases/operations.jsonl; git diff --check` | + +### Requirements to test map + +| Requirement | Behavior | Test type | Command | Initial state | +|---|---|---|---|---| +| OPS-01 | mutable modifiers: default/zero, clamp, wrap, alpha, same receiver | Go unit + exact differential | `node compat/run.mjs compat/cases/operations.jsonl` | Wave 0 needed | +| OPS-01 | Mix rounding and alpha interpolation | Go unit + exact differential | same | Wave 0 needed | +| OPS-01 | WCAG normalization, thresholds, selection/fallback/null | Go unit + exact differential | same | Wave 0 needed | +| OPS-01 | palettes: defaults, custom options, ordered results | Go unit + exact differential | same | Wave 0 needed | +| QLT-02 | stable complete reproducer | differential integration | same | driver exists; Phase 4 rows/dispatch missing | + +### Required deterministic corpus groups + +- Modifiers: each method with omitted/default, zero, normal, clamp high/low, alpha input, `spin` negative/over-360/omitted/null. +- Mix: omitted 50, zero, 90, transparent alpha progression, and out-of-range amount. +- Readability: ratios `1`, `1.1121078324840545`, `21`; each threshold family; invalid/mixed-case options; first-tie behavior; include-fallback true/false; empty candidate null. +- Palettes: all default red sequences copied from `test.js`; custom analogous `results`/`slices`; zero defaults; a non-red hue-wrapping case; alpha retention. + +### Environment availability + +| Dependency | Required by | Available | Version | Fallback | +|---|---|---:|---|---| +| Node.js | local JS oracle and differential harness | yes | v24.18.0 | none needed | +| Go | library tests and Go adapter | yes | go1.26.1 windows/amd64 | none needed | +| Deno | unchanged source suite | no | — | Node differential evidence only; does not count as a source-suite pass | + +## Sources + +### Primary (HIGH confidence) + +- [mod.js](../../../mod.js) - constructor/prototype mutation wrapper, modifiers, mix, WCAG operations, palette algorithms, and WCAG parameter validator. +- [test.js](../../../test.js) - exhaustive modifier and mix loops; spin cases; WCAG/fallback assertions; default palette order expectations. +- [docs/superpowers/specs/2026-08-01-phase-4-operations-design.md](../../../docs/superpowers/specs/2026-08-01-phase-4-operations-design.md) - approved scope and architectural separation. +- [src/tinycolor/color.go](../../../src/tinycolor/color.go), [src/cmd/tinycolor-compat/main.go](../../../src/cmd/tinycolor-compat/main.go), [compat/js-runner.mjs](../../../compat/js-runner.mjs), and [compat/run.mjs](../../../compat/run.mjs) - current extension surfaces. +- Oracle probe executed 2026-08-01 with Node v24.18.0 - omitted/null spin, zero palette defaults, empty `mostReadable`, static modifier absence, and identity mutation. + +### Secondary (MEDIUM confidence) + +None. The selected local source and tests are the authoritative specification. + +### Tertiary (LOW confidence) + +None. + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - project decisions and installed versions are local evidence. +- Architecture: HIGH - approved Phase 4 design and existing owner boundaries agree. +- Semantics and pitfalls: HIGH - direct local source, source tests, and oracle edge probe. + +**Research date:** 2026-08-01 +**Valid until:** this checkout's `mod.js` or `test.js` changes. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-VALIDATION.md b/.planning/phases/04-operations-and-palettes/04-VALIDATION.md new file mode 100644 index 00000000..b59dc5ba --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-VALIDATION.md @@ -0,0 +1,56 @@ +# Phase 4 Validation + +## Constraints + +- Run commands from `R:\Code\TinyColor` unless the command changes directory. +- The JavaScript oracle, parser, and color model are not Phase 4 edit targets. +- Treat every differential mismatch as a QLT-02 record; do not add float tolerance. +- Deno is unavailable. Do not claim `deno task test` passed. + +## Focused Tasks + +| Plan/task | RED/GREEN command | Differential or adapter command | +|---|---|---| +| 04-01 modifiers/Mix library | `Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1` | Not applicable until 04-02 | +| 04-02 modifiers/Mix adapter/corpus | `node tests/port/adapter.test.mjs` | `node compat/run.mjs compat/cases/operations.jsonl` | +| 04-03 readability library | `Set-Location src; go test ./tinycolor -run 'Test(Readability|IsReadable|MostReadable)' -count=1` | Not applicable until 04-04 | +| 04-04 readability adapter/corpus | `node tests/port/adapter.test.mjs` | `node compat/run.mjs compat/cases/operations.jsonl` | +| 04-05 palettes library | `Set-Location src; go test ./tinycolor -run 'Test(Palette|Complement|Analogous|Monochromatic|Triad|Tetrad)' -count=1` | Not applicable until 04-06 | +| 04-06 palettes adapter/corpus | `node tests/port/adapter.test.mjs` | `node compat/run.mjs compat/cases/operations.jsonl` | + +For every TDD library task, first run its focused Go command after adding the +test and confirm the feature is absent or behaviorally wrong. After the minimal +implementation, rerun it, then run `Set-Location src; go test ./...; go vet +./...` before the task's micro-commit. + +## Final Phase Gate + +```powershell +Set-Location src +go test ./... +go vet ./... +Set-Location .. +node tests/port/adapter.test.mjs +node compat/run.mjs compat/cases/smoke.jsonl +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/parser.jsonl +node compat/run.mjs compat/cases/conversion.jsonl +node compat/run.mjs compat/cases/operations.jsonl +git diff --check +git diff -- mod.js test.js tinycolor.js +``` + +Pass criteria: all Go, adapter, and corpus commands exit zero; the operations +corpus has zero mismatches; `git diff --check` is clean; and the final oracle +diff is empty. If any differential check fails, preserve the exact JSONL row +and harness record (request, JavaScript result, Go result, operation, owner, +and suspected package) rather than changing the oracle or hiding the result. + +## Planned Execution Summaries + +- `04-01-SUMMARY.md`: modifier/Mix typed contracts, direct tests, focused and full Go evidence. +- `04-02-SUMMARY.md`: adapter coercion rules, modifier/Mix corpus count, differential evidence. +- `04-03-SUMMARY.md`: WCAG public contracts, threshold/selection tests, Go evidence. +- `04-04-SUMMARY.md`: options/null adapter behavior, readability corpus count, differential evidence. +- `04-05-SUMMARY.md`: palette contracts/order tests and Go evidence. +- `04-06-SUMMARY.md`: palette adapter/corpus completion and final Phase 4 gate evidence. \ No newline at end of file From 08998928d8a0ae0d0cc31c5c7c8d87266689b46a Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:49:20 +0530 Subject: [PATCH 014/165] test(04-01): add failing modifier and mix regressions --- src/tinycolor/operations_test.go | 113 +++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/tinycolor/operations_test.go diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go new file mode 100644 index 00000000..d9592755 --- /dev/null +++ b/src/tinycolor/operations_test.go @@ -0,0 +1,113 @@ +package tinycolor + +import ( + "math" + "testing" +) + +func TestModifiers(t *testing.T) { + red, _ := FromCompat("red", false) + if returned := red.Lighten(10); returned != &red { + t.Fatal("Lighten must return its receiver") + } + if got := red.ToHexString(); got != "#ff3333" { + t.Fatalf("Lighten(10) = %s", got) + } + + for _, modifier := range []struct { + name string + apply func(*Color) *Color + }{ + {"lighten", func(c *Color) *Color { return c.Lighten(0) }}, + {"darken", func(c *Color) *Color { return c.Darken(0) }}, + {"saturate", func(c *Color) *Color { return c.Saturate(0) }}, + {"desaturate", func(c *Color) *Color { return c.Desaturate(0) }}, + {"brighten", func(c *Color) *Color { return c.Brighten(0) }}, + {"spin", func(c *Color) *Color { return c.Spin(0) }}, + } { + t.Run(modifier.name+" zero", func(t *testing.T) { + color, _ := FromCompat("#336699", false) + if modifier.apply(&color) != &color || color.ToHexString() != "#336699" { + t.Fatalf("%s(0) changed %#v", modifier.name, color) + } + }) + } + + white, _ := FromCompat("#fff", false) + white.Lighten(100) + if got := white.ToHexString(); got != "#ffffff" { + t.Fatalf("Lighten clamp = %s", got) + } + black, _ := FromCompat("#000", false) + black.Darken(100) + if got := black.ToHexString(); got != "#000000" { + t.Fatalf("Darken clamp = %s", got) + } + red, _ = FromCompat("red", false) + red.Desaturate(200) + if got := red.ToHexString(); got != "#808080" { + t.Fatalf("Desaturate clamp = %s", got) + } + + black, _ = FromCompat("#000", false) + black.Brighten(.2) + if got := black.ToHexString(); got != "#010101" { + t.Fatalf("Brighten pre-clamp rounding = %s", got) + } + + red, _ = FromCompat("red", false) + red.Spin(-120) + if got := red.ToHexString(); got != "#0000ff" { + t.Fatalf("Spin(-120) = %s", got) + } + red, _ = FromCompat("red", false) + red.Spin(480) + if got := red.ToHexString(); got != "#00ff00" { + t.Fatalf("Spin(480) = %s", got) + } + + color, _ := FromCompatWithOptions("rgba(255, 0, 0, .4)", false, CompatOptions{GradientType: true}) + original, format, alpha := color.Original(), color.Format(), color.Alpha() + if color.Spin(math.NaN()) != &color { + t.Fatal("Spin must return its receiver") + } + if color.ToHexString() != "#000000" || !color.Valid() || color.Format() != format || color.Original() != original || color.Alpha() != alpha { + t.Fatalf("Spin(NaN) did not retain metadata: %#v", color) + } + if got := color.ToFilter(nil, false); got != "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#66000000,endColorstr=#66000000)" { + t.Fatalf("Spin(NaN) gradient metadata = %s", got) + } + + alphaColor, _ := FromCompat("rgba(255, 0, 0, .4)", false) + alphaColor.Greyscale() + if alphaColor.ToHexString() != "#808080" || alphaColor.Alpha() != .4 { + t.Fatalf("Greyscale() = %#v", alphaColor) + } +} + +func TestMix(t *testing.T) { + black, _ := FromCompat("#000", false) + white, _ := FromCompat("#fff", false) + if got := Mix(black, white, 50).ToHexString(); got != "#808080" { + t.Fatalf("Mix(50) = %s", got) + } + if got := Mix(black, white, 0).ToHexString(); got != "#000000" { + t.Fatalf("Mix(0) = %s", got) + } + if got := Mix(white, black, 90).ToHexString(); got != "#1a1a1a" { + t.Fatalf("Mix(90) = %s", got) + } + + transparent, _ := FromCompat("transparent", false) + mixed := Mix(transparent, black, 25) + if mixed.ToHexString() != "#000000" || mixed.Alpha() != .25 { + t.Fatalf("transparent Mix = %#v", mixed) + } + if transparent.Alpha() != 0 || black.ToHexString() != "#000000" { + t.Fatal("Mix mutated an input") + } + + if got := Mix(black, white, 200).ToHexString(); got != "#ffffff" { + t.Fatalf("out-of-range Mix = %s", got) + } +} \ No newline at end of file From a514597464a1d487a74fdc87304160e7704210b0 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:56:41 +0530 Subject: [PATCH 015/165] feat(04-01): implement modifiers and mix --- src/tinycolor/color.go | 360 +++++++++++++++++++++++++------ src/tinycolor/operations_test.go | 2 +- 2 files changed, 301 insertions(+), 61 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 0544fd4d..1fd69228 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -4,18 +4,27 @@ package tinycolor import ( "fmt" "math" - "strconv" "math/rand/v2" + "strconv" "github.com/rajeet-04/tinycolor-go/internal/color" "github.com/rajeet-04/tinycolor-go/internal/parser" ) -type Color struct{ model color.Model; gradientType bool } -type CompatOptions struct { Format string; GradientType bool } -type RGB struct { R, G, B int; A float64 } -type HSL struct { H, S, L, A float64 } -type HSV struct { H, S, V, A float64 } +type Color struct { + model color.Model + gradientType bool +} +type CompatOptions struct { + Format string + GradientType bool +} +type RGB struct { + R, G, B int + A float64 +} +type HSL struct{ H, S, L, A float64 } +type HSV struct{ H, S, V, A float64 } func FromCompat(input any, fromRatio bool) (Color, error) { if fromRatio { @@ -23,7 +32,14 @@ func FromCompat(input any, fromRatio bool) (Color, error) { } return Color{model: parser.Parse(input)}, nil } -func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) { c, e := FromCompat(input, fromRatio); if options.Format != "" { c.model.Format = color.Format(options.Format) }; c.gradientType=options.GradientType; return c, e } +func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) { + c, e := FromCompat(input, fromRatio) + if options.Format != "" { + c.model.Format = color.Format(options.Format) + } + c.gradientType = options.GradientType + return c, e +} func (c Color) Valid() bool { return c.model.Valid } func (c Color) Format() string { return string(c.model.Format) } @@ -32,71 +48,188 @@ func (c Color) Original() any { return c.model.Original } func (c Color) RGB() map[string]any { return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} } -func (c Color) ToRGB() RGB { return RGB{int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)), c.model.A} } -func (c Color) ToHSL() HSL { h,s,l:=rgbToHSL(c.model.R,c.model.G,c.model.B); return HSL{h*360,s,l,c.model.A} } -func (c Color) ToHSV() HSV { h,s,v:=rgbToHSV(c.model.R,c.model.G,c.model.B); return HSV{h*360,s,v,c.model.A} } -func (c Color) ToRGBString() string { x:=c.ToRGB(); if x.A==1{return fmt.Sprintf("rgb(%d, %d, %d)",x.R,x.G,x.B)}; return fmt.Sprintf("rgba(%d, %d, %d, %s)",x.R,x.G,x.B,roundedAlpha(x.A)) } -func (c Color) ToPercentageRGB() RGB { x:=c.ToRGB(); return RGB{percent(x.R),percent(x.G),percent(x.B),x.A} } -func (c Color) ToPercentageRGBString() string { x:=c.ToPercentageRGB(); if x.A==1{return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)",x.R,x.G,x.B)};return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)",x.R,x.G,x.B,roundedAlpha(x.A)) } -func (c Color) ToHSLString() string {x:=c.ToHSL(); p:="hsl";if x.A<1{p="hsla"}; s:=fmt.Sprintf("%s(%d, %d%%, %d%%",p,mathRound(x.H),mathRound(x.S*100),mathRound(x.L*100));if x.A<1{s+=", "+roundedAlpha(x.A)};return s+")"} -func (c Color) ToHSVString() string {x:=c.ToHSV(); p:="hsv";if x.A<1{p="hsva"}; s:=fmt.Sprintf("%s(%d, %d%%, %d%%",p,mathRound(x.H),mathRound(x.S*100),mathRound(x.V*100));if x.A<1{s+=", "+roundedAlpha(x.A)};return s+")"} -func (c Color) ToHex() string { return fmt.Sprintf("%02x%02x%02x", mathRound(c.model.R),mathRound(c.model.G),mathRound(c.model.B)) } -func (c Color) ToHexString() string{return "#"+c.ToHex()} -func (c Color) ToHex8() string{return c.ToHex()+fmt.Sprintf("%02x",mathRound(c.model.A*255))} -func (c Color) ToHex8String() string{return "#"+c.ToHex8()} +func (c Color) ToRGB() RGB { + return RGB{int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)), c.model.A} +} +func (c Color) ToHSL() HSL { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + return HSL{h * 360, s, l, c.model.A} +} +func (c Color) ToHSV() HSV { + h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) + return HSV{h * 360, s, v, c.model.A} +} +func (c Color) ToRGBString() string { + x := c.ToRGB() + if x.A == 1 { + return fmt.Sprintf("rgb(%d, %d, %d)", x.R, x.G, x.B) + } + return fmt.Sprintf("rgba(%d, %d, %d, %s)", x.R, x.G, x.B, roundedAlpha(x.A)) +} +func (c Color) ToPercentageRGB() RGB { + x := c.ToRGB() + return RGB{percent(x.R), percent(x.G), percent(x.B), x.A} +} +func (c Color) ToPercentageRGBString() string { + x := c.ToPercentageRGB() + if x.A == 1 { + return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", x.R, x.G, x.B) + } + return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", x.R, x.G, x.B, roundedAlpha(x.A)) +} +func (c Color) ToHSLString() string { + x := c.ToHSL() + p := "hsl" + if x.A < 1 { + p = "hsla" + } + s := fmt.Sprintf("%s(%d, %d%%, %d%%", p, mathRound(x.H), mathRound(x.S*100), mathRound(x.L*100)) + if x.A < 1 { + s += ", " + roundedAlpha(x.A) + } + return s + ")" +} +func (c Color) ToHSVString() string { + x := c.ToHSV() + p := "hsv" + if x.A < 1 { + p = "hsva" + } + s := fmt.Sprintf("%s(%d, %d%%, %d%%", p, mathRound(x.H), mathRound(x.S*100), mathRound(x.V*100)) + if x.A < 1 { + s += ", " + roundedAlpha(x.A) + } + return s + ")" +} +func (c Color) ToHex() string { + return fmt.Sprintf("%02x%02x%02x", mathRound(c.model.R), mathRound(c.model.G), mathRound(c.model.B)) +} +func (c Color) ToHexString() string { return "#" + c.ToHex() } +func (c Color) ToHex8() string { return c.ToHex() + fmt.Sprintf("%02x", mathRound(c.model.A*255)) } +func (c Color) ToHex8String() string { return "#" + c.ToHex8() } func (c Color) ToString(format string) string { return c.toString(format, format != "") } -func (c Color) toString(format string, explicit bool) string { if format==""{format=string(c.model.Format)}; if !explicit&&c.model.A<1&&(format=="hex"||format=="hex6"||format=="hex3"||format=="hex4"||format=="hex8"||format=="name"){if format=="name"&&c.model.A==0{return "transparent"};return c.ToRGBString()}; switch format {case "rgb": return c.ToRGBString();case "prgb":return c.ToPercentageRGBString();case "hsl":return c.ToHSLString();case "hsv":return c.ToHSVString();case "hex","hex6":return c.ToHexString();case "hex8":return c.ToHex8String();case "name":if n,ok:=c.ToName();ok{return n};return c.ToHexString()}; return c.ToHexString() } -func (c Color) ToName()(string,bool){if c.model.A==0{return "transparent",true}; if c.model.A<1{return "",false}; n:=parser.NameForRGB(mathRound(c.model.R),mathRound(c.model.G),mathRound(c.model.B)); return n,n!=""} -func (c Color) ToFilter(second *Color, gradient bool) string { start:="#"+fmt.Sprintf("%02x",mathRound(c.model.A*255))+c.ToHex(); end:=start; if second!=nil {end="#"+fmt.Sprintf("%02x",mathRound(second.model.A*255))+second.ToHex()}; prefix:=""; if gradient||c.gradientType {prefix="GradientType = 1, "}; return "progid:DXImageTransform.Microsoft.gradient("+prefix+"startColorstr="+start+",endColorstr="+end+")" } -func (c Color) Brightness() float64{x:=c.ToRGB();return float64(x.R*299+x.G*587+x.B*114)/1000} -func (c Color) Luminance() float64 { x:=c.ToRGB(); f:=func(v int)float64{z:=float64(v)/255;if z<=.03928{return z/12.92};return math.Pow((z+.055)/1.055,2.4)};return .2126*f(x.R)+.7152*f(x.G)+.0722*f(x.B) } -func (c Color) IsDark()bool{return c.Brightness()<128}; func(c Color)IsLight()bool{return !c.IsDark()} -func (c Color) Clone() Color { n,_:=FromCompat(c.String(),false);return n } -func Equals(a,b any)bool{if a==nil||b==nil{return false};x,_:=FromCompat(a,false);y,_:=FromCompat(b,false);return x.ToRGBString()==y.ToRGBString()} -func Random() Color { return Color{model: color.Model{R:rand.Float64()*255,G:rand.Float64()*255,B:rand.Float64()*255,A:1,Valid:true,Format:color.FormatRGB}} } -func mathRound(v float64)int{return int(math.Floor(v+.5))} +func (c Color) toString(format string, explicit bool) string { + if format == "" { + format = string(c.model.Format) + } + if !explicit && c.model.A < 1 && (format == "hex" || format == "hex6" || format == "hex3" || format == "hex4" || format == "hex8" || format == "name") { + if format == "name" && c.model.A == 0 { + return "transparent" + } + return c.ToRGBString() + } + switch format { + case "rgb": + return c.ToRGBString() + case "prgb": + return c.ToPercentageRGBString() + case "hsl": + return c.ToHSLString() + case "hsv": + return c.ToHSVString() + case "hex", "hex6": + return c.ToHexString() + case "hex8": + return c.ToHex8String() + case "name": + if n, ok := c.ToName(); ok { + return n + } + return c.ToHexString() + } + return c.ToHexString() +} +func (c Color) ToName() (string, bool) { + if c.model.A == 0 { + return "transparent", true + } + if c.model.A < 1 { + return "", false + } + n := parser.NameForRGB(mathRound(c.model.R), mathRound(c.model.G), mathRound(c.model.B)) + return n, n != "" +} +func (c Color) ToFilter(second *Color, gradient bool) string { + start := "#" + fmt.Sprintf("%02x", mathRound(c.model.A*255)) + c.ToHex() + end := start + if second != nil { + end = "#" + fmt.Sprintf("%02x", mathRound(second.model.A*255)) + second.ToHex() + } + prefix := "" + if gradient || c.gradientType { + prefix = "GradientType = 1, " + } + return "progid:DXImageTransform.Microsoft.gradient(" + prefix + "startColorstr=" + start + ",endColorstr=" + end + ")" +} +func (c Color) Brightness() float64 { x := c.ToRGB(); return float64(x.R*299+x.G*587+x.B*114) / 1000 } +func (c Color) Luminance() float64 { + x := c.ToRGB() + f := func(v int) float64 { + z := float64(v) / 255 + if z <= .03928 { + return z / 12.92 + } + return math.Pow((z+.055)/1.055, 2.4) + } + return .2126*f(x.R) + .7152*f(x.G) + .0722*f(x.B) +} +func (c Color) IsDark() bool { return c.Brightness() < 128 } +func (c Color) IsLight() bool { return !c.IsDark() } +func (c Color) Clone() Color { n, _ := FromCompat(c.String(), false); return n } +func Equals(a, b any) bool { + if a == nil || b == nil { + return false + } + x, _ := FromCompat(a, false) + y, _ := FromCompat(b, false) + return x.ToRGBString() == y.ToRGBString() +} +func Random() Color { + return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatRGB}} +} +func mathRound(v float64) int { return int(math.Floor(v + .5)) } // String supplies the minimal source-compatible string snapshot used by the // JSONL oracle. Full public output APIs remain Phase 3 work. func (c Color) String() string { return c.toString("", false) /* - r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) - if c.model.Format == color.FormatName { - if c.model.A == 0 { - return "transparent" + r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) + if c.model.Format == color.FormatName { + if c.model.A == 0 { + return "transparent" + } + if name := parser.NameForRGB(r, g, b); name != "" { + return name + } } - if name := parser.NameForRGB(r, g, b); name != "" { - return name - } - } - if c.model.Format == color.FormatPRGB { - if c.model.A < 1 { - return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", percent(r), percent(g), percent(b), roundedAlpha(c.model.A)) + if c.model.Format == color.FormatPRGB { + if c.model.A < 1 { + return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", percent(r), percent(g), percent(b), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", percent(r), percent(g), percent(b)) } - return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", percent(r), percent(g), percent(b)) - } - if c.model.Format == color.FormatHSL { - h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) - if c.model.A < 1 { - return fmt.Sprintf("hsla(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100)), roundedAlpha(c.model.A)) + if c.model.Format == color.FormatHSL { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsla(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsl(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100))) } - return fmt.Sprintf("hsl(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100))) - } - if c.model.Format == color.FormatHSV { - h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) - if c.model.A < 1 { - return fmt.Sprintf("hsva(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100)), roundedAlpha(c.model.A)) + if c.model.Format == color.FormatHSV { + h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsva(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsv(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100))) } - return fmt.Sprintf("hsv(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100))) - } - if c.model.Format == color.FormatRGB || c.model.A < 1 { - if c.model.A == 1 { - return fmt.Sprintf("rgb(%d, %d, %d)", r, g, b) + if c.model.Format == color.FormatRGB || c.model.A < 1 { + if c.model.A == 1 { + return fmt.Sprintf("rgb(%d, %d, %d)", r, g, b) + } + return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) } - return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) - } - return fmt.Sprintf("#%02x%02x%02x", r, g, b) + return fmt.Sprintf("#%02x%02x%02x", r, g, b) */ } @@ -158,3 +291,110 @@ func rgbToHSV(r, g, b float64) (float64, float64, float64) { } return h / 6, d / max, max } + +func (c *Color) Lighten(amount float64) *Color { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + c.setHSL(h, s, clamp01(l+amount/100)) + return c +} + +func (c *Color) Darken(amount float64) *Color { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + c.setHSL(h, s, clamp01(l-amount/100)) + return c +} + +func (c *Color) Saturate(amount float64) *Color { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + c.setHSL(h, clamp01(s+amount/100), l) + return c +} + +func (c *Color) Desaturate(amount float64) *Color { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + c.setHSL(h, clamp01(s-amount/100), l) + return c +} + +func (c *Color) Greyscale() *Color { return c.Desaturate(100) } + +func (c *Color) Brighten(amount float64) *Color { + delta := float64(mathRound(255 * amount / 100)) + c.model.R = clampChannel(c.model.R + delta) + c.model.G = clampChannel(c.model.G + delta) + c.model.B = clampChannel(c.model.B + delta) + return c +} + +func (c *Color) Spin(amount float64) *Color { + if math.IsNaN(amount) { + c.model.R, c.model.G, c.model.B = 0, 0, 0 + return c + } + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + h = math.Mod(h*360+amount, 360) + if h < 0 { + h += 360 + } + c.setHSL(h/360, s, l) + return c +} + +func Mix(first, second Color, amount float64) Color { + p := amount / 100 + firstRGB, secondRGB := first.ToRGB(), second.ToRGB() + return Color{model: color.Model{ + R: clampChannel((float64(secondRGB.R)-float64(firstRGB.R))*p + float64(firstRGB.R)), + G: clampChannel((float64(secondRGB.G)-float64(firstRGB.G))*p + float64(firstRGB.G)), + B: clampChannel((float64(secondRGB.B)-float64(firstRGB.B))*p + float64(firstRGB.B)), + A: clampAlpha((second.model.A-first.model.A)*p + first.model.A), + Valid: true, + Format: color.FormatRGB, + }} +} + +func (c *Color) setHSL(h, s, l float64) { + c.model.R, c.model.G, c.model.B = hslToRGB(h, s, l) +} + +func hslToRGB(h, s, l float64) (float64, float64, float64) { + if s == 0 { + channel := l * 255 + return channel, channel, channel + } + var q float64 + if l < .5 { + q = l * (1 + s) + } else { + q = l + s - l*s + } + p := 2*l - q + channel := func(t float64) float64 { + if t < 0 { + t++ + } + if t > 1 { + t-- + } + switch { + case t*6 < 1: + return p + (q-p)*6*t + case t*2 < 1: + return q + case t*3 < 2: + return p + (q-p)*(2.0/3.0-t)*6 + default: + return p + } + } + return channel(h+1.0/3.0) * 255, channel(h) * 255, channel(h-1.0/3.0) * 255 +} + +func clamp01(value float64) float64 { return math.Min(1, math.Max(0, value)) } +func clampChannel(value float64) float64 { return math.Min(255, math.Max(0, value)) } +func clampAlpha(value float64) float64 { + if value < 0 || value > 1 || math.IsNaN(value) { + return 1 + } + return value +} diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index d9592755..df0c028a 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -110,4 +110,4 @@ func TestMix(t *testing.T) { if got := Mix(black, white, 200).ToHexString(); got != "#ffffff" { t.Fatalf("out-of-range Mix = %s", got) } -} \ No newline at end of file +} From d71ea12b7eccf479f26064794230f1d4eeca15c2 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:02:02 +0530 Subject: [PATCH 016/165] test(04-02): add modifier adapter failures - Cover modify defaults, zero, spin null, and receiver identity\n- Cover mix response snapshots and unsupported methods --- tests/port/adapter.test.mjs | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/port/adapter.test.mjs b/tests/port/adapter.test.mjs index db49297b..44ebfb88 100644 --- a/tests/port/adapter.test.mjs +++ b/tests/port/adapter.test.mjs @@ -28,3 +28,45 @@ for (const response of [malformed, unknown]) { assert.notEqual(Object.hasOwn(response, "result"), Object.hasOwn(response, "error")); assert.equal(typeof response.error, "string"); } + +const [lightenDefault, lightenZero, spinOmitted, spinNull, mixed, badModifier] = run([ + '{"id":"lighten-default","operation":"modify","input":"red","args":{"method":"lighten"}}', + '{"id":"lighten-zero","operation":"modify","input":"red","args":{"method":"lighten","amount":0}}', + '{"id":"spin-omitted","operation":"modify","input":"red","args":{"method":"spin"}}', + '{"id":"spin-null","operation":"modify","input":"red","args":{"method":"spin","amount":null}}', + '{"id":"mix-default","operation":"mix","input":"red","args":{"other":"#000"}}', + '{"id":"bad-modifier","operation":"modify","input":"red","args":{"method":"unknown"}}', +].join("\n")); + +assert.deepEqual(lightenDefault, { + id: "lighten-default", + result: { + before: success.result, + after: { + ...success.result, + rgb: { r: 255, g: 51, b: 51, a: 1 }, + value: "#ff3333", + }, + sameReceiver: true, + }, +}); +assert.deepEqual(lightenZero.result, { + before: success.result, + after: success.result, + sameReceiver: true, +}); +assert.equal(spinOmitted.result.after.value, "black"); +assert.equal(spinOmitted.result.after.original, "red"); +assert.deepEqual(spinNull.result, lightenZero.result); +assert.deepEqual(mixed, { + id: "mix-default", + result: { + valid: true, + format: "rgb", + alpha: 1, + rgb: { r: 128, g: 0, b: 0, a: 1 }, + value: "rgb(128, 0, 0)", + original: { r: 127.5, g: 0, b: 0, a: 1 }, + }, +}); +assert.deepEqual(badModifier, { id: "bad-modifier", error: "unsupported method" }); From b1d0798464005b3a15f2f72f0a9ccd3335ec9bc5 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:02:59 +0530 Subject: [PATCH 017/165] feat(04-02): dispatch modifiers and mix - Preserve JavaScript argument defaults at the adapter boundary\n- Serialize mutation identity and Mix snapshots --- compat/js-runner.mjs | 22 +++++++++++ src/cmd/tinycolor-compat/main.go | 68 +++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index 81189767..dab7a120 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -36,6 +36,10 @@ const handle = (request) => { return respond(request.id, tinycolor.equals(request.input, args.other)); case "clone": return respond(request.id, inspect(color().clone())); + case "modify": + return modify(request.id, color(), args); + case "mix": + return respond(request.id, inspect(tinycolor.mix(request.input, args.other, amount(args, 50)))); case "randomInvariant": return randomInvariant(request.id); default: @@ -43,6 +47,24 @@ const handle = (request) => { } }; +const amount = (args, defaultAmount) => args.amount === 0 ? 0 : args.amount || defaultAmount; + +const modify = (id, color, args) => { + const before = inspect(color); + let returned; + switch (args.method) { + case "lighten": returned = color.lighten(amount(args, 10)); break; + case "brighten": returned = color.brighten(amount(args, 10)); break; + case "darken": returned = color.darken(amount(args, 10)); break; + case "saturate": returned = color.saturate(amount(args, 10)); break; + case "desaturate": returned = color.desaturate(amount(args, 10)); break; + case "greyscale": returned = color.greyscale(); break; + case "spin": returned = Object.hasOwn(args, "amount") ? color.spin(args.amount) : color.spin(); break; + default: return respond(id, undefined, "unsupported method"); + } + return respond(id, { before, after: inspect(color), sameReceiver: returned === color }); +}; + const output = (id, color, args) => { let result; switch (args.method) { diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 57ecd0f3..e5820f86 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -3,6 +3,7 @@ package main import ( "bufio" "fmt" + "math" "os" "github.com/rajeet-04/tinycolor-go/internal/compat" @@ -37,7 +38,7 @@ func handle(request compat.Request) compat.Response { color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": color, err = tinycolor.FromCompat(request.Input, true) - case "output", "analysis", "clone": + case "output", "analysis", "clone", "modify", "mix": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) case "equals", "randomInvariant": default: @@ -60,6 +61,10 @@ func handle(request compat.Request) compat.Response { case "clone": response, _ := compat.Success(request.ID, color.Clone().Inspect()) return response + case "modify": + return modify(request.ID, &color, args) + case "mix": + return mix(request.ID, color, args) case "equals": response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) return response @@ -77,6 +82,67 @@ func handle(request compat.Request) compat.Response { return response } +func modify(id string, color *tinycolor.Color, args map[string]any) compat.Response { + before := color.Inspect() + method, _ := args["method"].(string) + var returned *tinycolor.Color + switch method { + case "lighten": + returned = color.Lighten(defaultAmount(args, 10)) + case "brighten": + returned = color.Brighten(defaultAmount(args, 10)) + case "darken": + returned = color.Darken(defaultAmount(args, 10)) + case "saturate": + returned = color.Saturate(defaultAmount(args, 10)) + case "desaturate": + returned = color.Desaturate(defaultAmount(args, 10)) + case "greyscale": + returned = color.Greyscale() + case "spin": + if _, ok := args["amount"]; ok { + returned = color.Spin(number(args["amount"])) + } else { + returned = color.Spin(math.NaN()) + } + default: + response, _ := compat.Failure(id, "unsupported method") + return response + } + response, _ := compat.Success(id, map[string]any{ + "before": before, + "after": color.Inspect(), + "sameReceiver": returned == color, + }) + return response +} + +func mix(id string, first tinycolor.Color, args map[string]any) compat.Response { + second, err := tinycolor.FromCompat(args["other"], false) + if err != nil { + response, _ := compat.Failure(id, err.Error()) + return response + } + response, _ := compat.Success(id, tinycolor.Mix(first, second, defaultAmount(args, 50)).Inspect()) + return response +} + +func defaultAmount(args map[string]any, fallback float64) float64 { + if value, ok := args["amount"]; ok { + amount := number(value) + if amount == 0 { + return 0 + } + return amount + } + return fallback +} + +func number(value any) float64 { + amount, _ := value.(float64) + return amount +} + func options(args map[string]any) tinycolor.CompatOptions { options, _ := args["options"].(map[string]any) format, _ := options["format"].(string) From b322d2c2702da7a158f40b0ecfc62dfb3e60a6ec Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:03:40 +0530 Subject: [PATCH 018/165] test(04-02): add modifier and mix differential corpus - Add deterministic modifier and Mix reproducer rows\n- Preserve fixed ownership metadata in mismatch reports --- compat/cases/operations.jsonl | 32 ++++++++++++++++++++++++++++++++ compat/run.mjs | 5 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 compat/cases/operations.jsonl diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl new file mode 100644 index 00000000..5fe866d6 --- /dev/null +++ b/compat/cases/operations.jsonl @@ -0,0 +1,32 @@ +{"id":"mod-lighten-normal","operation":"modify","input":"#804020","args":{"method":"lighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-lighten-zero","operation":"modify","input":"#804020","args":{"method":"lighten","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-lighten-clamp","operation":"modify","input":"#fefefe","args":{"method":"lighten","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-lighten-alpha","operation":"modify","input":"#80402080","args":{"method":"lighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-normal","operation":"modify","input":"#804020","args":{"method":"brighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-zero","operation":"modify","input":"#804020","args":{"method":"brighten","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-brighten-clamp","operation":"modify","input":"#fefefe","args":{"method":"brighten","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-alpha","operation":"modify","input":"#80402080","args":{"method":"brighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-darken-normal","operation":"modify","input":"#804020","args":{"method":"darken","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-darken-zero","operation":"modify","input":"#804020","args":{"method":"darken","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-darken-clamp","operation":"modify","input":"#010101","args":{"method":"darken","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-darken-alpha","operation":"modify","input":"#80402080","args":{"method":"darken","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-saturate-normal","operation":"modify","input":"#808040","args":{"method":"saturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-saturate-zero","operation":"modify","input":"#808040","args":{"method":"saturate","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-saturate-clamp","operation":"modify","input":"#808040","args":{"method":"saturate","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-saturate-alpha","operation":"modify","input":"#80804080","args":{"method":"saturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-desaturate-normal","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-desaturate-zero","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-desaturate-clamp","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-desaturate-alpha","operation":"modify","input":"#80402080","args":{"method":"desaturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-greyscale-normal","operation":"modify","input":"#804020","args":{"method":"greyscale"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-greyscale-alpha","operation":"modify","input":"#80402080","args":{"method":"greyscale"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-spin-negative","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":-120},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-spin-over-360","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":480},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-spin-omitted","operation":"modify","input":"#ff0000","args":{"method":"spin"},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-spin-null","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":null},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-default","operation":"mix","input":"#ff0000","args":{"other":"#000000"},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-zero","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-ninety","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":90},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mix-transparent-alpha","operation":"mix","input":"transparent","args":{"other":"#00000080","amount":25},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mix-out-of-range-high","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":150},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mix-out-of-range-low","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":-50},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} \ No newline at end of file diff --git a/compat/run.mjs b/compat/run.mjs index 510a208c..db9e8783 100644 --- a/compat/run.mjs +++ b/compat/run.mjs @@ -22,7 +22,8 @@ function same(left, right) { let passed = 0; let mismatches = 0; -for (const request of cases) { +for (const row of cases) { + const { owner, suspectedPackage, ...request } = row; const js = run(process.execPath, ["compat/js-runner.mjs"], root, request); const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "src"), request); if (same(js, go)) { @@ -31,7 +32,7 @@ for (const request of cases) { } mismatches++; const protocol = Object.hasOwn(js, "result") !== Object.hasOwn(go, "result"); - console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "src/tinycolor" })); + console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: owner ?? (protocol ? "compat" : "rajeet-04"), suspectedPackage: suspectedPackage ?? (protocol ? "compat" : "src/tinycolor") })); } console.log(`cases: ${cases.length}`); From d2f22c6ee1de187cfa43ee3c346474debcda3981 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:05:23 +0530 Subject: [PATCH 019/165] fix(04-02): preserve mix original metadata --- src/tinycolor/color.go | 19 +++++++++++++------ src/tinycolor/operations_test.go | 6 +++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 1fd69228..fde11847 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -343,13 +343,20 @@ func (c *Color) Spin(amount float64) *Color { func Mix(first, second Color, amount float64) Color { p := amount / 100 firstRGB, secondRGB := first.ToRGB(), second.ToRGB() + raw := map[string]float64{ + "r": (float64(secondRGB.R)-float64(firstRGB.R))*p + float64(firstRGB.R), + "g": (float64(secondRGB.G)-float64(firstRGB.G))*p + float64(firstRGB.G), + "b": (float64(secondRGB.B)-float64(firstRGB.B))*p + float64(firstRGB.B), + "a": (second.model.A-first.model.A)*p + first.model.A, + } return Color{model: color.Model{ - R: clampChannel((float64(secondRGB.R)-float64(firstRGB.R))*p + float64(firstRGB.R)), - G: clampChannel((float64(secondRGB.G)-float64(firstRGB.G))*p + float64(firstRGB.G)), - B: clampChannel((float64(secondRGB.B)-float64(firstRGB.B))*p + float64(firstRGB.B)), - A: clampAlpha((second.model.A-first.model.A)*p + first.model.A), - Valid: true, - Format: color.FormatRGB, + R: clampChannel(raw["r"]), + G: clampChannel(raw["g"]), + B: clampChannel(raw["b"]), + A: clampAlpha(raw["a"]), + Valid: true, + Format: color.FormatRGB, + Original: raw, }} } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index df0c028a..18c26f66 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -88,9 +88,13 @@ func TestModifiers(t *testing.T) { func TestMix(t *testing.T) { black, _ := FromCompat("#000", false) white, _ := FromCompat("#fff", false) - if got := Mix(black, white, 50).ToHexString(); got != "#808080" { + mixedHalf := Mix(black, white, 50) + if got := mixedHalf.ToHexString(); got != "#808080" { t.Fatalf("Mix(50) = %s", got) } + if got, ok := mixedHalf.Original().(map[string]float64); !ok || got["r"] != 127.5 || got["g"] != 127.5 || got["b"] != 127.5 || got["a"] != 1 { + t.Fatalf("Mix(50) original = %#v", mixedHalf.Original()) + } if got := Mix(black, white, 0).ToHexString(); got != "#000000" { t.Fatalf("Mix(0) = %s", got) } From 8609f216f1d57013cca7d96560b434805ef260b0 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:05:57 +0530 Subject: [PATCH 020/165] test(04-03): add failing readability regressions --- src/tinycolor/operations_test.go | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 18c26f66..ce6396e9 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -115,3 +115,61 @@ func TestMix(t *testing.T) { t.Fatalf("out-of-range Mix = %s", got) } } + +func TestReadability(t *testing.T) { + black, _ := FromCompat("#000", false) + white, _ := FromCompat("#fff", false) + if got := Readability(black, black); got != 1 { + t.Fatalf("same-color readability = %v", got) + } + if got := Readability(black, white); got != 21 { + t.Fatalf("black-on-white readability = %v", got) + } +} + +func TestIsReadable(t *testing.T) { + for _, test := range []struct { + name string + ratio float64 + options WCAG2Options + expected bool + }{ + {"AA small threshold", 4.5, WCAG2Options{Level: "AA", Size: "small"}, true}, + {"AA large threshold", 3, WCAG2Options{Level: "AA", Size: "large"}, true}, + {"AAA small threshold", 7, WCAG2Options{Level: "AAA", Size: "small"}, true}, + {"AAA large threshold", 4.5, WCAG2Options{Level: "AAA", Size: "large"}, true}, + {"below threshold", 4.499999999999999, WCAG2Options{Level: "AA", Size: "small"}, false}, + {"mixed case", 7, WCAG2Options{Level: "aaa", Size: "LARGE"}, true}, + {"invalid values default", 4.5, WCAG2Options{Level: "invalid", Size: "invalid"}, true}, + } { + t.Run(test.name, func(t *testing.T) { + if got := isReadableRatio(test.ratio, test.options); got != test.expected { + t.Fatalf("isReadableRatio(%v, %#v) = %t", test.ratio, test.options, got) + } + }) + } +} + +func TestMostReadable(t *testing.T) { + base, _ := FromCompat("#000", false) + first, _ := FromCompat("white", false) + second, _ := FromCompat("#fff", false) + if got, ok := MostReadable(base, []Color{first, second}, WCAG2Options{}); !ok || got.Original() != "white" { + t.Fatalf("first tied candidate = %#v, %t", got, ok) + } + + gray, _ := FromCompat("#777", false) + matchingGray, _ := FromCompat("#777", false) + if got, ok := MostReadable(gray, []Color{matchingGray}, WCAG2Options{IncludeFallbackColors: true}); !ok || got.ToHexString() != "#000000" { + t.Fatalf("fallback candidate = %#v, %t", got, ok) + } + if got, ok := MostReadable(gray, []Color{matchingGray}, WCAG2Options{}); !ok || got.ToHexString() != "#777777" { + t.Fatalf("fallback-disabled candidate = %#v, %t", got, ok) + } + if _, ok := MostReadable(base, nil, WCAG2Options{}); ok { + t.Fatal("empty candidates without fallback must have no result") + } + if _, ok := MostReadable(base, nil, WCAG2Options{IncludeFallbackColors: true}); ok { + t.Fatal("empty candidates with fallback must have no result") + } +} From 821ed0558ccd9a5a211ed1cc60b99e2adbd5dec0 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:06:21 +0530 Subject: [PATCH 021/165] feat(04-03): implement readability operations --- src/tinycolor/color.go | 54 ++++++++++++++++++++++++++++++++ src/tinycolor/operations_test.go | 8 ++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index fde11847..1e1939b9 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -6,6 +6,7 @@ import ( "math" "math/rand/v2" "strconv" + "strings" "github.com/rajeet-04/tinycolor-go/internal/color" "github.com/rajeet-04/tinycolor-go/internal/parser" @@ -25,6 +26,11 @@ type RGB struct { } type HSL struct{ H, S, L, A float64 } type HSV struct{ H, S, V, A float64 } +type WCAG2Options struct { + Level string + Size string + IncludeFallbackColors bool +} func FromCompat(input any, fromRatio bool) (Color, error) { if fromRatio { @@ -173,6 +179,54 @@ func (c Color) Luminance() float64 { } return .2126*f(x.R) + .7152*f(x.G) + .0722*f(x.B) } +func Readability(first, second Color) float64 { + firstLuminance, secondLuminance := first.Luminance(), second.Luminance() + return (math.Max(firstLuminance, secondLuminance) + .05) / (math.Min(firstLuminance, secondLuminance) + .05) +} +func IsReadable(first, second Color, options WCAG2Options) bool { + return isReadableRatio(Readability(first, second), options) +} +func MostReadable(base Color, candidates []Color, options WCAG2Options) (Color, bool) { + if len(candidates) == 0 { + return Color{}, false + } + bestColor := candidates[0] + bestScore := Readability(base, bestColor) + for _, candidate := range candidates[1:] { + if score := Readability(base, candidate); score > bestScore { + bestScore, bestColor = score, candidate + } + } + if IsReadable(base, bestColor, options) || !options.IncludeFallbackColors { + return bestColor, true + } + white, _ := FromCompat("#fff", false) + black, _ := FromCompat("#000", false) + return MostReadable(base, []Color{white, black}, WCAG2Options{Level: options.Level, Size: options.Size}) +} +func isReadableRatio(ratio float64, options WCAG2Options) bool { + options = normalizeWCAG2Options(options) + switch options.Level + options.Size { + case "AAsmall", "AAAlarge": + return ratio >= 4.5 + case "AAlarge": + return ratio >= 3 + case "AAAsmall": + return ratio >= 7 + } + return false +} +func normalizeWCAG2Options(options WCAG2Options) WCAG2Options { + options.Level = strings.ToUpper(options.Level) + options.Size = strings.ToLower(options.Size) + if options.Level != "AA" && options.Level != "AAA" { + options.Level = "AA" + } + if options.Size != "small" && options.Size != "large" { + options.Size = "small" + } + return options +} func (c Color) IsDark() bool { return c.Brightness() < 128 } func (c Color) IsLight() bool { return !c.IsDark() } func (c Color) Clone() Color { n, _ := FromCompat(c.String(), false); return n } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index ce6396e9..e4dc1fb1 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -129,10 +129,10 @@ func TestReadability(t *testing.T) { func TestIsReadable(t *testing.T) { for _, test := range []struct { - name string - ratio float64 - options WCAG2Options - expected bool + name string + ratio float64 + options WCAG2Options + expected bool }{ {"AA small threshold", 4.5, WCAG2Options{Level: "AA", Size: "small"}, true}, {"AA large threshold", 3, WCAG2Options{Level: "AA", Size: "large"}, true}, From 0c72f8c57b4740a4a122c9ffebb70f24969f9863 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:06:54 +0530 Subject: [PATCH 022/165] test(04-04): add readability adapter failures --- tests/port/adapter.test.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/port/adapter.test.mjs b/tests/port/adapter.test.mjs index 44ebfb88..0a699ecb 100644 --- a/tests/port/adapter.test.mjs +++ b/tests/port/adapter.test.mjs @@ -70,3 +70,21 @@ assert.deepEqual(mixed, { }, }); assert.deepEqual(badModifier, { id: "bad-modifier", error: "unsupported method" }); + +const [readability, readableDefault, readableMixedCase, readableInvalid, fallback, fallbackDisabled, emptyCandidates] = run([ + '{"id":"readability","operation":"readability","input":"#000","args":{"other":"#fff"}}', + '{"id":"readable-default","operation":"isReadable","input":"#777","args":{"other":"#000","options":{}}}', + '{"id":"readable-mixed","operation":"isReadable","input":"#000","args":{"other":"#fff","options":{"level":"aaa","size":"LARGE"}}}', + '{"id":"readable-invalid","operation":"isReadable","input":"#777","args":{"other":"#000","options":{"level":false,"size":0}}}', + '{"id":"fallback","operation":"mostReadable","input":"#777","args":{"candidates":["#777"],"options":{"includeFallbackColors":true}}}', + '{"id":"fallback-disabled","operation":"mostReadable","input":"#777","args":{"candidates":["#777"],"options":{"includeFallbackColors":false}}}', + '{"id":"empty-candidates","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}}}', +].join("\n")); + +assert.equal(readability.result, 21); +assert.equal(readableDefault.result, true); +assert.equal(readableMixedCase.result, true); +assert.equal(readableInvalid.result, true); +assert.equal(fallback.result.value, "#000000"); +assert.equal(fallbackDisabled.result.value, "#777777"); +assert.equal(emptyCandidates.result, null); From 817ad7110589d82433c8e69dc4d53ea1f36ebbac Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:07:16 +0530 Subject: [PATCH 023/165] feat(04-04): dispatch readability operations --- compat/js-runner.mjs | 8 ++++++ src/cmd/tinycolor-compat/main.go | 47 +++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index dab7a120..cfbfbdcf 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -40,6 +40,14 @@ const handle = (request) => { return modify(request.id, color(), args); case "mix": return respond(request.id, inspect(tinycolor.mix(request.input, args.other, amount(args, 50)))); + case "readability": + return respond(request.id, tinycolor.readability(request.input, args.other)); + case "isReadable": + return respond(request.id, tinycolor.isReadable(request.input, args.other, args.options)); + case "mostReadable": { + const result = tinycolor.mostReadable(request.input, args.candidates ?? [], args.options); + return respond(request.id, result ? inspect(result) : null); + } case "randomInvariant": return randomInvariant(request.id); default: diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index e5820f86..1a26cf59 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -38,7 +38,7 @@ func handle(request compat.Request) compat.Response { color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": color, err = tinycolor.FromCompat(request.Input, true) - case "output", "analysis", "clone", "modify", "mix": + case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) case "equals", "randomInvariant": default: @@ -65,6 +65,12 @@ func handle(request compat.Request) compat.Response { return modify(request.ID, &color, args) case "mix": return mix(request.ID, color, args) + case "readability": + return readability(request.ID, color, args) + case "isReadable": + return isReadable(request.ID, color, args) + case "mostReadable": + return mostReadable(request.ID, color, args) case "equals": response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) return response @@ -127,6 +133,34 @@ func mix(id string, first tinycolor.Color, args map[string]any) compat.Response return response } +func readability(id string, first tinycolor.Color, args map[string]any) compat.Response { + second, _ := tinycolor.FromCompat(args["other"], false) + response, _ := compat.Success(id, tinycolor.Readability(first, second)) + return response +} + +func isReadable(id string, first tinycolor.Color, args map[string]any) compat.Response { + second, _ := tinycolor.FromCompat(args["other"], false) + response, _ := compat.Success(id, tinycolor.IsReadable(first, second, wcagOptions(args))) + return response +} + +func mostReadable(id string, base tinycolor.Color, args map[string]any) compat.Response { + inputs, _ := args["candidates"].([]any) + candidates := make([]tinycolor.Color, 0, len(inputs)) + for _, input := range inputs { + candidate, _ := tinycolor.FromCompat(input, false) + candidates = append(candidates, candidate) + } + result, ok := tinycolor.MostReadable(base, candidates, wcagOptions(args)) + if !ok { + response, _ := compat.Success(id, nil) + return response + } + response, _ := compat.Success(id, result.Inspect()) + return response +} + func defaultAmount(args map[string]any, fallback float64) float64 { if value, ok := args["amount"]; ok { amount := number(value) @@ -150,6 +184,17 @@ func options(args map[string]any) tinycolor.CompatOptions { return tinycolor.CompatOptions{Format: format, GradientType: gradientType} } +func wcagOptions(args map[string]any) tinycolor.WCAG2Options { + raw, _ := args["options"].(map[string]any) + level, _ := raw["level"].(string) + size, _ := raw["size"].(string) + return tinycolor.WCAG2Options{ + Level: level, + Size: size, + IncludeFallbackColors: truthy(raw["includeFallbackColors"]), + } +} + func analysis(id string, color tinycolor.Color, method any) compat.Response { var result any switch method { From 469b9f4d60101c5282278819bb8b24edcd1428fc Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:08:05 +0530 Subject: [PATCH 024/165] test(04-04): add readability differential corpus --- compat/cases/operations.jsonl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 5fe866d6..f5e02259 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -29,4 +29,19 @@ {"id":"mix-ninety","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":90},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mix-transparent-alpha","operation":"mix","input":"transparent","args":{"other":"#00000080","amount":25},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mix-out-of-range-high","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":150},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} -{"id":"mix-out-of-range-low","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":-50},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} \ No newline at end of file +{"id":"mix-out-of-range-low","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":-50},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readability-same","operation":"readability","input":"#000","args":{"other":"#000"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readability-low","operation":"readability","input":"#000","args":{"other":"#111"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readability-max","operation":"readability","input":"#000","args":{"other":"#fff"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-aa-small","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":"AA","size":"small"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-aa-large","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{"level":"AA","size":"large"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-aaa-small","operation":"isReadable","input":"#db91b8","args":{"other":"#2e0c3a","options":{"level":"AAA","size":"small"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-aaa-large","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":"AAA","size":"large"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-default-options","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"readable-mixed-case","operation":"isReadable","input":"#db91b8","args":{"other":"#2e0c3a","options":{"level":"aaa","size":"LARGE"}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"readable-falsy-options","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":false,"size":0}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"most-readable-first-tie","operation":"mostReadable","input":"#fff","args":{"candidates":["white","#fff"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"most-readable-no-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"most-readable-empty-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"most-readable-empty-no-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":false}},"owner":"C-mrashis","suspectedPackage":"compat"} \ No newline at end of file From 651a5767316efb0fd6022b7c90ea81ae6eff1875 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:08:51 +0530 Subject: [PATCH 025/165] test(04-05): add failing palette regressions --- src/tinycolor/operations_test.go | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index e4dc1fb1..3698d533 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -2,6 +2,7 @@ package tinycolor import ( "math" + "strings" "testing" ) @@ -173,3 +174,79 @@ func TestMostReadable(t *testing.T) { t.Fatal("empty candidates with fallback must have no result") } } + +func TestComplement(t *testing.T) { + red, _ := FromCompat("red", false) + if got := red.Complement().ToHex(); got != "00ffff" { + t.Fatalf("Complement() = %s", got) + } + if got := red.ToHex(); got != "ff0000" { + t.Fatalf("Complement() mutated input to %s", got) + } + transparent, _ := FromCompat("rgba(255, 0, 0, .5)", false) + if got := transparent.Complement().Alpha(); got != .5 { + t.Fatalf("Complement alpha = %v", got) + } +} + +func TestPaletteOrders(t *testing.T) { + red, _ := FromCompat("red", false) + for _, test := range []struct { + name string + palette []Color + expected string + }{ + {"split complement", red.SplitComplement(), "ff0000,ccff00,0066ff"}, + {"triad", red.Triad(), "ff0000,00ff00,0000ff"}, + {"tetrad", red.Tetrad(), "ff0000,80ff00,00ffff,7f00ff"}, + {"analogous", red.Analogous(6, 30), "ff0000,ff0066,ff0033,ff0000,ff3300,ff6600"}, + {"monochromatic", red.Monochromatic(6), "ff0000,2a0000,550000,800000,aa0000,d40000"}, + } { + t.Run(test.name, func(t *testing.T) { + if got := paletteHex(test.palette); got != test.expected { + t.Fatalf("%s = %s", test.name, got) + } + }) + } +} + +func TestPaletteCustomizationsAndIndependence(t *testing.T) { + blue, _ := FromCompat("#336699", false) + if got := paletteHex(blue.Analogous(4, 12)); got != "336699,339999,336699,333399" { + t.Fatalf("custom analogous = %s", got) + } + wrapped, _ := FromCompat("#ff0066", false) + if got := paletteHex(wrapped.SplitComplement()); got != "ff0066,ffcc00,00ccff" { + t.Fatalf("wrapped split complement = %s", got) + } + if got := blue.Analogous(0, 12); len(got) != 0 { + t.Fatalf("typed analogous zero length = %d", len(got)) + } + if got := blue.Monochromatic(0); len(got) != 0 { + t.Fatalf("typed monochromatic zero length = %d", len(got)) + } + + transparent, _ := FromCompat("rgba(255, 0, 0, .5)", false) + analogous := transparent.Analogous(3, 30) + for index, color := range analogous { + if color.Alpha() != .5 { + t.Fatalf("analogous alpha at %d = %v", index, color.Alpha()) + } + } + triad := transparent.Triad() + if triad[0].Alpha() != .5 || triad[1].Alpha() != 1 || triad[2].Alpha() != 1 { + t.Fatalf("triad alpha = %#v", triad) + } + triad[1].Spin(30) + if transparent.ToHexString() != "#ff0000" || triad[2].ToHexString() != "#0000ff" { + t.Fatal("palette result mutation changed the source or another result") + } +} + +func paletteHex(colors []Color) string { + values := make([]string, len(colors)) + for index, color := range colors { + values[index] = color.ToHex() + } + return strings.Join(values, ",") +} From 5708bfb93a4dfa840428d367f694927deb575e2f Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:09:23 +0530 Subject: [PATCH 026/165] feat(04-05): implement ordered palettes --- src/tinycolor/color.go | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 1e1939b9..55e46143 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -414,6 +414,80 @@ func Mix(first, second Color, amount float64) Color { }} } +func (c Color) Complement() Color { + hsl := c.ToHSL() + return hslColor(math.Mod(hsl.H+180, 360), hsl.S, hsl.L, hsl.A, true) +} + +func (c Color) SplitComplement() []Color { + hsl := c.ToHSL() + return []Color{ + c, + hslColor(math.Mod(hsl.H+72, 360), hsl.S, hsl.L, 1, false), + hslColor(math.Mod(hsl.H+216, 360), hsl.S, hsl.L, 1, false), + } +} + +func (c Color) Triad() []Color { return c.polyad(3) } +func (c Color) Tetrad() []Color { return c.polyad(4) } + +func (c Color) Analogous(results, slices int) []Color { + if results <= 0 || slices <= 0 { + return []Color{} + } + hsl := c.ToHSL() + part := 360 / float64(slices) + palette := []Color{c} + hue := math.Mod(hsl.H-float64(int(part*float64(results))>>1)+720, 360) + for remaining := results - 1; remaining > 0; remaining-- { + hue = math.Mod(hue+part, 360) + palette = append(palette, hslColor(hue, hsl.S, hsl.L, hsl.A, true)) + } + return palette +} + +func (c Color) Monochromatic(results int) []Color { + if results <= 0 { + return []Color{} + } + hsv := c.ToHSV() + palette := make([]Color, 0, results) + value := hsv.V + modification := 1 / float64(results) + for remaining := results; remaining > 0; remaining-- { + palette = append(palette, hsvColor(hsv.H, hsv.S, value)) + value = math.Mod(value+modification, 1) + } + return palette +} + +func (c Color) polyad(number int) []Color { + if number <= 0 { + return []Color{} + } + hsl := c.ToHSL() + palette := []Color{c} + step := 360 / float64(number) + for index := 1; index < number; index++ { + palette = append(palette, hslColor(math.Mod(hsl.H+float64(index)*step, 360), hsl.S, hsl.L, 1, false)) + } + return palette +} + +func hslColor(hue, saturation, lightness, alpha float64, includeAlpha bool) Color { + input := map[string]any{"h": hue, "s": saturation, "l": lightness} + if includeAlpha { + input["a"] = alpha + } + color, _ := FromCompat(input, false) + return color +} + +func hsvColor(hue, saturation, value float64) Color { + color, _ := FromCompat(map[string]any{"h": hue, "s": saturation, "v": value}, false) + return color +} + func (c *Color) setHSL(h, s, l float64) { c.model.R, c.model.G, c.model.B = hslToRGB(h, s, l) } From 8776335fcff1ee34e7b4c8a09677a021204bd540 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:09:47 +0530 Subject: [PATCH 027/165] test(04-06): add palette adapter failures --- tests/port/adapter.test.mjs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/port/adapter.test.mjs b/tests/port/adapter.test.mjs index 0a699ecb..fabdd09c 100644 --- a/tests/port/adapter.test.mjs +++ b/tests/port/adapter.test.mjs @@ -88,3 +88,26 @@ assert.equal(readableInvalid.result, true); assert.equal(fallback.result.value, "#000000"); assert.equal(fallbackDisabled.result.value, "#777777"); assert.equal(emptyCandidates.result, null); + +const [complement, splitComplement, triad, tetrad, analogous, analogousZero, monochromatic, monochromaticZero, badPalette] = run([ + '{"id":"complement","operation":"palette","input":"red","args":{"method":"complement"}}', + '{"id":"split-complement","operation":"palette","input":"red","args":{"method":"splitcomplement"}}', + '{"id":"triad","operation":"palette","input":"red","args":{"method":"triad"}}', + '{"id":"tetrad","operation":"palette","input":"red","args":{"method":"tetrad"}}', + '{"id":"analogous","operation":"palette","input":"red","args":{"method":"analogous"}}', + '{"id":"analogous-zero","operation":"palette","input":"red","args":{"method":"analogous","results":0,"slices":0}}', + '{"id":"monochromatic","operation":"palette","input":"red","args":{"method":"monochromatic"}}', + '{"id":"monochromatic-zero","operation":"palette","input":"red","args":{"method":"monochromatic","results":0}}', + '{"id":"bad-palette","operation":"palette","input":"red","args":{"method":"unknown"}}', +].join("\n")); + +const paletteValues = (response) => response.result.map((color) => color.value); +assert.deepEqual(paletteValues(complement), ["#00ffff"]); +assert.deepEqual(paletteValues(splitComplement), ["red", "#ccff00", "#0066ff"]); +assert.deepEqual(paletteValues(triad), ["red", "#00ff00", "#0000ff"]); +assert.deepEqual(paletteValues(tetrad), ["red", "#80ff00", "#00ffff", "#7f00ff"]); +assert.deepEqual(paletteValues(analogous), ["red", "#ff0066", "#ff0033", "#ff0000", "#ff3300", "#ff6600"]); +assert.deepEqual(paletteValues(analogousZero), paletteValues(analogous)); +assert.deepEqual(paletteValues(monochromatic), ["#ff0000", "#2a0000", "#550000", "#800000", "#aa0000", "#d40000"]); +assert.deepEqual(paletteValues(monochromaticZero), paletteValues(monochromatic)); +assert.deepEqual(badPalette, { id: "bad-palette", error: "unsupported method" }); From c328fa4a6f0b40348995ee806e43b78cb9add219 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:10:34 +0530 Subject: [PATCH 028/165] feat(04-06): dispatch palette operations --- compat/js-runner.mjs | 16 +++++++++++++ src/cmd/tinycolor-compat/main.go | 39 +++++++++++++++++++++++++++++++- tests/port/adapter.test.mjs | 12 +++++----- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index cfbfbdcf..d78c8840 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -48,6 +48,8 @@ const handle = (request) => { const result = tinycolor.mostReadable(request.input, args.candidates ?? [], args.options); return respond(request.id, result ? inspect(result) : null); } + case "palette": + return palette(request.id, color(), args); case "randomInvariant": return randomInvariant(request.id); default: @@ -73,6 +75,20 @@ const modify = (id, color, args) => { return respond(id, { before, after: inspect(color), sameReceiver: returned === color }); }; +const palette = (id, color, args) => { + let result; + switch (args.method) { + case "complement": result = [color.complement()]; break; + case "splitcomplement": result = color.splitcomplement(); break; + case "triad": result = color.triad(); break; + case "tetrad": result = color.tetrad(); break; + case "analogous": result = color.analogous(args.results || 6, args.slices || 30); break; + case "monochromatic": result = color.monochromatic(args.results || 6); break; + default: return respond(id, undefined, "unsupported method"); + } + return respond(id, result.map(inspect)); +}; + const output = (id, color, args) => { let result; switch (args.method) { diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 1a26cf59..42d70728 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -38,7 +38,7 @@ func handle(request compat.Request) compat.Response { color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": color, err = tinycolor.FromCompat(request.Input, true) - case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable": + case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable", "palette": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) case "equals", "randomInvariant": default: @@ -71,6 +71,8 @@ func handle(request compat.Request) compat.Response { return isReadable(request.ID, color, args) case "mostReadable": return mostReadable(request.ID, color, args) + case "palette": + return palette(request.ID, color, args) case "equals": response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) return response @@ -161,6 +163,34 @@ func mostReadable(id string, base tinycolor.Color, args map[string]any) compat.R return response } +func palette(id string, color tinycolor.Color, args map[string]any) compat.Response { + method, _ := args["method"].(string) + var colors []tinycolor.Color + switch method { + case "complement": + colors = []tinycolor.Color{color.Complement()} + case "splitcomplement": + colors = color.SplitComplement() + case "triad": + colors = color.Triad() + case "tetrad": + colors = color.Tetrad() + case "analogous": + colors = color.Analogous(defaultCount(args["results"], 6), defaultCount(args["slices"], 30)) + case "monochromatic": + colors = color.Monochromatic(defaultCount(args["results"], 6)) + default: + response, _ := compat.Failure(id, "unsupported method") + return response + } + inspections := make([]map[string]any, len(colors)) + for index, paletteColor := range colors { + inspections[index] = paletteColor.Inspect() + } + response, _ := compat.Success(id, inspections) + return response +} + func defaultAmount(args map[string]any, fallback float64) float64 { if value, ok := args["amount"]; ok { amount := number(value) @@ -172,6 +202,13 @@ func defaultAmount(args map[string]any, fallback float64) float64 { return fallback } +func defaultCount(value any, fallback int) int { + if !truthy(value) { + return fallback + } + return int(number(value)) +} + func number(value any) float64 { amount, _ := value.(float64) return amount diff --git a/tests/port/adapter.test.mjs b/tests/port/adapter.test.mjs index fabdd09c..86fa42cf 100644 --- a/tests/port/adapter.test.mjs +++ b/tests/port/adapter.test.mjs @@ -102,12 +102,12 @@ const [complement, splitComplement, triad, tetrad, analogous, analogousZero, mon ].join("\n")); const paletteValues = (response) => response.result.map((color) => color.value); -assert.deepEqual(paletteValues(complement), ["#00ffff"]); -assert.deepEqual(paletteValues(splitComplement), ["red", "#ccff00", "#0066ff"]); -assert.deepEqual(paletteValues(triad), ["red", "#00ff00", "#0000ff"]); -assert.deepEqual(paletteValues(tetrad), ["red", "#80ff00", "#00ffff", "#7f00ff"]); -assert.deepEqual(paletteValues(analogous), ["red", "#ff0066", "#ff0033", "#ff0000", "#ff3300", "#ff6600"]); +assert.deepEqual(paletteValues(complement), ["hsl(180, 100%, 50%)"]); +assert.deepEqual(paletteValues(splitComplement), ["red", "hsl(72, 100%, 50%)", "hsl(216, 100%, 50%)"]); +assert.deepEqual(paletteValues(triad), ["red", "hsl(120, 100%, 50%)", "hsl(240, 100%, 50%)"]); +assert.deepEqual(paletteValues(tetrad), ["red", "hsl(90, 100%, 50%)", "hsl(180, 100%, 50%)", "hsl(270, 100%, 50%)"]); +assert.deepEqual(paletteValues(analogous), ["red", "hsl(336, 100%, 50%)", "hsl(348, 100%, 50%)", "hsl(0, 100%, 50%)", "hsl(12, 100%, 50%)", "hsl(24, 100%, 50%)"]); assert.deepEqual(paletteValues(analogousZero), paletteValues(analogous)); -assert.deepEqual(paletteValues(monochromatic), ["#ff0000", "#2a0000", "#550000", "#800000", "#aa0000", "#d40000"]); +assert.deepEqual(paletteValues(monochromatic), ["hsv(0, 100%, 100%)", "hsv(0, 100%, 17%)", "hsv(0, 100%, 33%)", "hsv(0, 100%, 50%)", "hsv(0, 100%, 67%)", "hsv(0, 100%, 83%)"]); assert.deepEqual(paletteValues(monochromaticZero), paletteValues(monochromatic)); assert.deepEqual(badPalette, { id: "bad-palette", error: "unsupported method" }); From 7cd0e84543403d11a535cee4dcceffb92d1d0d00 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:11:56 +0530 Subject: [PATCH 029/165] fix(04-06): preserve analogous original metadata --- src/tinycolor/color.go | 5 ++++- src/tinycolor/operations_test.go | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 55e46143..4ea01fe2 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -439,9 +439,12 @@ func (c Color) Analogous(results, slices int) []Color { part := 360 / float64(slices) palette := []Color{c} hue := math.Mod(hsl.H-float64(int(part*float64(results))>>1)+720, 360) + input := map[string]any{"h": hue, "s": hsl.S, "l": hsl.L, "a": hsl.A} for remaining := results - 1; remaining > 0; remaining-- { hue = math.Mod(hue+part, 360) - palette = append(palette, hslColor(hue, hsl.S, hsl.L, hsl.A, true)) + input["h"] = hue + color, _ := FromCompat(input, false) + palette = append(palette, color) } return palette } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 3698d533..72ccff1b 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -233,6 +233,12 @@ func TestPaletteCustomizationsAndIndependence(t *testing.T) { t.Fatalf("analogous alpha at %d = %v", index, color.Alpha()) } } + for index, color := range analogous[1:] { + original, ok := color.Original().(map[string]any) + if !ok || original["h"] != float64(6) { + t.Fatalf("analogous original at %d = %#v", index+1, color.Original()) + } + } triad := transparent.Triad() if triad[0].Alpha() != .5 || triad[1].Alpha() != 1 || triad[2].Alpha() != 1 { t.Fatalf("triad alpha = %#v", triad) From 796044f642b9de454f4e4bad15b0d732e802eb6d Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:11:57 +0530 Subject: [PATCH 030/165] test(04-06): complete palette differential corpus --- compat/cases/operations.jsonl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index f5e02259..886c0de0 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -44,4 +44,15 @@ {"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-no-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-empty-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}},"owner":"C-mrashis","suspectedPackage":"compat"} -{"id":"most-readable-empty-no-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":false}},"owner":"C-mrashis","suspectedPackage":"compat"} \ No newline at end of file +{"id":"most-readable-empty-no-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":false}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-complement","operation":"palette","input":"red","args":{"method":"complement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-split-complement","operation":"palette","input":"red","args":{"method":"splitcomplement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-triad","operation":"palette","input":"red","args":{"method":"triad"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-tetrad","operation":"palette","input":"red","args":{"method":"tetrad"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-analogous-default","operation":"palette","input":"red","args":{"method":"analogous"},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-analogous-custom","operation":"palette","input":"#336699","args":{"method":"analogous","results":4,"slices":12},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-analogous-zero-default","operation":"palette","input":"red","args":{"method":"analogous","results":0,"slices":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-monochromatic-default","operation":"palette","input":"red","args":{"method":"monochromatic"},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-monochromatic-zero-default","operation":"palette","input":"red","args":{"method":"monochromatic","results":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-wrapped-split","operation":"palette","input":"#ff0066","args":{"method":"splitcomplement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-analogous-alpha","operation":"palette","input":"rgba(255, 0, 0, .5)","args":{"method":"analogous","results":3,"slices":30},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} \ No newline at end of file From 5601b29e912de7069fe76e9f79aec6117b79852f Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:13:38 +0530 Subject: [PATCH 031/165] docs: record Phase 4 parity evidence --- .planning/ROADMAP.md | 9 ++- .planning/STATE.md | 5 ++ .../04-01-SUMMARY.md | 47 +++++++++++++++ .../04-02-SUMMARY.md | 58 +++++++++++++++++++ .../04-03-SUMMARY.md | 16 +++++ .../04-04-SUMMARY.md | 16 +++++ .../04-05-SUMMARY.md | 16 +++++ .../04-06-SUMMARY.md | 16 +++++ COMPATIBILITY.md | 12 +++- 9 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 .planning/phases/04-operations-and-palettes/04-01-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-02-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-03-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-04-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-05-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-06-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9e1845ed..fe25d974 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -11,7 +11,7 @@ through parsing and observable output, to operations and delivery proof. - [x] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. - [x] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. - [x] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. -- [ ] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. +- [x] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. - [ ] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. ## Phase Details @@ -65,7 +65,10 @@ Plans: 1. Modifiers preserve default, zero, clamping, wrapping, alpha, and mutation behavior. 2. Readability and most-readable operations match WCAG option defaults. 3. Every palette operation returns source-equivalent colors in source order. -**Plans**: TBD +**Plans**: 6 plans (complete 2026-08-01) + +Plans: +- [x] 04-01 through 04-06: Typed operations, WCAG readability, palettes, compatibility dispatch, and differential evidence. ### Phase 5: Delivery Evidence **Goal**: A judge can clone the project, verify parity evidence, use a Go CLI, and understand the migration and its limitations. @@ -84,5 +87,5 @@ Plans: | 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | | 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | | 3. Conversion and Representation | 2/2 | Complete | 2026-08-01 | -| 4. Operations and Palettes | 0/TBD | Not started | - | +| 4. Operations and Palettes | 6/6 | Complete | 2026-08-01 | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 3597e6f9..c57e9539 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -17,3 +17,8 @@ - Phase 3 completed locally: conversion/representation and analysis behavior passed a 35/35 exact corpus; all Phase 1–3 corpora are zero-mismatch. The compatibility protocol now preserves successful JSON `false` results. +- Phase 4 completed locally: modifiers, Mix, WCAG readability, and palette + operations passed the full gate. Exact differential evidence is 9/9 smoke, + 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 58/58 operations with + zero mismatches. `deno` remains unavailable, so no original-source suite + pass is claimed. diff --git a/.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md new file mode 100644 index 00000000..f21a9d98 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md @@ -0,0 +1,47 @@ +--- +phase: 4 +plan: 1 +subsystem: operations-modifiers-mix +tags: [go, tinycolor, modifiers, mix, tdd] +provides: [typed pointer modifiers, pure Mix, direct regressions] +affects: [src/tinycolor] +tech-stack: [go-standard-library] +key-files: + created: + - src/tinycolor/operations_test.go + modified: + - src/tinycolor/color.go +decisions: + - Typed modifiers require explicit numeric amounts; JavaScript defaults remain adapter work. + - Mix rounds source channels before interpolation and returns an independent Color value. +--- + +# Phase 4 Plan 1: Modifiers and Mix Summary + +Implemented mutable typed modifiers and a pure source-style `Mix` utility with +direct regression coverage for mutation, bounds, hue wrapping, alpha, +metadata retention, rounding, and out-of-range interpolation. + +## TDD Evidence + +- RED: `Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1` + exited `1` because `Lighten`, `Darken`, `Saturate`, `Desaturate`, + `Brighten`, `Spin`, and `Mix` were undefined. +- GREEN focused: `Set-Location 'R:\Code\TinyColor'; Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1` + exited `0`. +- Full validation: `Set-Location 'R:\Code\TinyColor'; Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1; go test ./...; go vet ./...` + exited `0`. + +## Commits + +- `0899892` `test(04-01): add failing modifier and mix regressions` +- `a514597` `feat(04-01): implement modifiers and mix` + +## Deviations from Plan + +None - plan executed as specified. The compatibility adapter and corpus remain +unchanged for Plan 04-02. + +## Known Stubs + +None. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md new file mode 100644 index 00000000..64e3e820 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md @@ -0,0 +1,58 @@ +--- +phase: 4 +plan: 2 +subsystem: operations-modifiers-mix-compatibility +tags: [compatibility, jsonl, modifiers, mix] +provides: [presence-aware adapter dispatch, deterministic modifier-mix corpus] +affects: [compat, src/cmd/tinycolor-compat] +key-files: + created: + - compat/cases/operations.jsonl + modified: + - compat/js-runner.mjs + - compat/run.mjs + - src/cmd/tinycolor-compat/main.go + - tests/port/adapter.test.mjs +decisions: + - JavaScript omitted, zero, and null amount behavior is confined to the compatibility adapters. + - Operations corpus mismatches preserve fixed owner and suspected-package metadata. +--- + +# Phase 4 Plan 2: Modifier and Mix Compatibility Summary + +Added thin Node and Go dispatch for pointer-mutating modifiers and pure Mix, +with deterministic owner-tagged operations reproducers. + +## TDD Evidence + +- RED: `node tests/port/adapter.test.mjs` exited `1` because `modify` returned + `unsupported operation` instead of the required mutation snapshot. +- GREEN: `node tests/port/adapter.test.mjs` exited `0` after dispatch was added. + +## Validation + +- `node tests/port/adapter.test.mjs`: exited `0`. +- `Set-Location src; go test ./cmd/tinycolor-compat ./tinycolor -count=1`: exited `0`. +- `node compat/run.mjs compat/cases/smoke.jsonl`: `9` cases passed, `0` mismatches. +- `node compat/run.mjs compat/cases/operations.jsonl`: `32` cases, `26` passed, + `6` Mix metadata mismatches before the follow-up fix. +- `git diff --check`: exited `0`. + +## Differential Evidence + +The six Mix mismatch records revealed that the Go value dropped TinyColor's +observable raw interpolated RGBA input. Follow-up commit `d2f22c6` +(`fix(04-02): preserve mix original metadata`) restored that metadata. The +final Phase 4 gate passed all `58/58` operations rows with zero mismatches. + +## Commits + +- `d71ea12` `test(04-02): add modifier adapter failures` +- `b1d0798` `feat(04-02): dispatch modifiers and mix` +- `b322d2c` `test(04-02): add modifier and mix differential corpus` + +## Deviations from Plan + +The corpus correctly exposed a shared-library defect after the planned adapter +work. It was fixed in a separate focused micro-commit rather than masked in +the harness. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md new file mode 100644 index 00000000..e6a76eae --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md @@ -0,0 +1,16 @@ +--- +phase: 4 +plan: 3 +subsystem: readability-library +provides: [typed WCAG readability and most-readable selection] +commits: [8609f21, 821ed05] +--- + +# Phase 4 Plan 3: Readability Library Summary + +Implemented typed `WCAG2Options`, `Readability`, `IsReadable`, and nullable +`MostReadable`. Direct tests cover raw ratios, inclusive WCAG thresholds, +case/invalid normalization, first ties, fallback colors, and empty candidates. + +The red test commit was `8609f21`; implementation was `821ed05`. Focused Go +tests, `go test ./...`, and `go vet ./...` passed. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-04-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-04-SUMMARY.md new file mode 100644 index 00000000..b5a31298 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-04-SUMMARY.md @@ -0,0 +1,16 @@ +--- +phase: 4 +plan: 4 +subsystem: readability-compatibility +provides: [readability JSONL dispatch and WCAG differential evidence] +commits: [0c72f8c, 817ad71, 469b9f4] +--- + +# Phase 4 Plan 4: Readability Compatibility Summary + +Added Node and Go dispatch for `readability`, `isReadable`, and +`mostReadable`, preserving adapter-owned option defaults, truthiness, and null +results. The fixed corpus covers source ratios, WCAG families, coercion, +first ties, fallback selection, and empty candidates. + +Adapter tests, Go tests, and the operations corpus passed in the final gate. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md new file mode 100644 index 00000000..0fa0c7df --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md @@ -0,0 +1,16 @@ +--- +phase: 4 +plan: 5 +subsystem: palettes-library +provides: [ordered typed palette operations] +commits: [651a576, 5708bfb] +--- + +# Phase 4 Plan 5: Palette Library Summary + +Implemented pure typed complement, split-complement, triad, tetrad, +analogous, and monochromatic operations with a private polyad helper. Tests +cover source order, custom and wrapping hues, alpha behavior, typed zero +guards, and independent returned values. + +Focused palette tests, `go test ./...`, and `go vet ./...` passed. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md new file mode 100644 index 00000000..ec2ab630 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md @@ -0,0 +1,16 @@ +--- +phase: 4 +plan: 6 +subsystem: palettes-compatibility-phase-gate +provides: [palette JSONL dispatch and complete Phase 4 evidence] +commits: [8776335, c328fa4, 7cd0e84, 796044f] +--- + +# Phase 4 Plan 6: Palette Compatibility and Gate Summary + +Added palette dispatch for all six public operations, retaining source order +and adapter-owned zero defaults. The final corpus contains 58 operations rows. +A focused follow-up repaired `Analogous` shared `original` metadata. + +Final validation passed: Go tests and vet, adapter tests, and fixed corpora at +9/9 smoke, 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 58/58 operations. \ No newline at end of file diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index b045f87e..1cbde4c1 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -12,8 +12,8 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | | HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | | Conversion/formatting | Passing | `compat/cases/conversion.jsonl` | 35 fixed JSONL cases; random is invariant-only. | -| Analysis/readability | Planned | Wave 2–3 | Preserve WCAG defaults. | -| Manipulation/palettes | Planned | Wave 3 | Preserve defaults and ordering. | +| Analysis/readability | Passing | `compat/cases/operations.jsonl` | WCAG defaults, thresholds, ties, fallback, and null selection. | +| Manipulation/palettes | Passing | `compat/cases/operations.jsonl` | 58 fixed operations cases cover modifiers, Mix, and ordered palettes. | | CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | | Original Deno suite | Blocked locally | `deno` unavailable | Run when Deno is installed or in CI. | @@ -84,3 +84,11 @@ node compat/run.mjs compat/cases/conversion.jsonl Random-color behavior is verified by validity, alpha, and channel-range invariants only; it is not compared exactly across independent JavaScript and Go random generators. + +## Phase 4 operations result + +On 2026-08-01, the fixed operations corpus passed with **58/58 cases** and +**0 mismatches**. The full available Phase 1-4 gate also retained **9/9** +smoke, **26/26** HEX/RGB/name, **23/23** parser, and **35/35** conversion +cases with zero mismatches. This evidence does not claim a Deno source-suite +pass because Deno is unavailable locally. From ff137ceb20de1262967f77306664cd86debf35fd Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:21:10 +0530 Subject: [PATCH 032/165] docs: restore Phase 1-2 summaries --- .../01-foundation-and-oracle/01-01-SUMMARY.md | 15 +++++++++++++++ .../02-parsing-and-color-state/02-01-SUMMARY.md | 15 +++++++++++++++ .../02-parsing-and-color-state/02-02-SUMMARY.md | 15 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 .planning/phases/01-foundation-and-oracle/01-01-SUMMARY.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-01-SUMMARY.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-02-SUMMARY.md diff --git a/.planning/phases/01-foundation-and-oracle/01-01-SUMMARY.md b/.planning/phases/01-foundation-and-oracle/01-01-SUMMARY.md new file mode 100644 index 00000000..e1f19fe4 --- /dev/null +++ b/.planning/phases/01-foundation-and-oracle/01-01-SUMMARY.md @@ -0,0 +1,15 @@ +--- +phase: 1 +plan: 1 +subsystem: compatibility-foundation +provides: [Go module, Node and Go JSONL runners, differential smoke corpus] +commits: [78423e3] +--- + +# Phase 1 Plan 1: Foundation and Oracle Summary + +Created the independent Go module, shared JSONL protocol, local JavaScript +oracle runner, Go compatibility runner, and fixed nine-case smoke corpus. + +Final evidence: Go tests and vet passed; `compat/cases/smoke.jsonl` passed 9/9 +with zero mismatches; `mod.js`, `test.js`, and `tinycolor.js` stayed unchanged. diff --git a/.planning/phases/02-parsing-and-color-state/02-01-SUMMARY.md b/.planning/phases/02-parsing-and-color-state/02-01-SUMMARY.md new file mode 100644 index 00000000..3c27b6a3 --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-01-SUMMARY.md @@ -0,0 +1,15 @@ +--- +phase: 2 +plan: 1 +subsystem: parser-model-rgb +provides: [normalized color model, bounds, HEX RGB and name parsing] +commits: [82b1208] +--- + +# Phase 2 Plan 1: Model and RGB Parser Summary + +Replaced the Phase 1 fixed decoder with normalized color state and shared +parsing for HEX, RGB(A), percentage RGB, named colors, transparent, invalid +input, and RGB objects while preserving format and original-input metadata. + +Final evidence: the focused parser corpus passed 26/26 with zero mismatches. diff --git a/.planning/phases/02-parsing-and-color-state/02-02-SUMMARY.md b/.planning/phases/02-parsing-and-color-state/02-02-SUMMARY.md new file mode 100644 index 00000000..9a2167f4 --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-02-SUMMARY.md @@ -0,0 +1,15 @@ +--- +phase: 2 +plan: 2 +subsystem: parser-hsl-hsv +provides: [HSL and HSV parsing, typed objects, FromRatio normalization] +commits: [82b1208] +--- + +# Phase 2 Plan 2: HSL and HSV Parser Summary + +Added HSL(A), HSV(A), typed-object precedence, hue wrapping, alpha handling, +and FromRatio normalization through the shared model. + +Final evidence: the full parser corpus passed 23/23 and the Phase 1 smoke +corpus remained 9/9, both with zero mismatches. From 34dc41fcd5394c56291fff176da250327e41ab1c Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:21:19 +0530 Subject: [PATCH 033/165] docs: normalize GSD project metadata --- .planning/PROJECT.md | 10 +++++++--- .planning/config.json | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index ea8f5f84..0a9a06d1 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -1,11 +1,16 @@ # TinyColor Go Port -## Purpose +## What This Is Create a fresh Go port of the local `bgrins/TinyColor` checkout with behavioral parity demonstrated through unchanged-source differential testing. -## Success criteria +## Core Value + +Observable behavior from the pinned TinyColor checkout is the specification; +the Go port is accepted only when exact differential evidence agrees. + +## Requirements - The upstream JavaScript source is untouched. - Go handles every behavior covered by the defined compatibility corpus. @@ -18,4 +23,3 @@ parity demonstrated through unchanged-source differential testing. - Target source: local `mod.js` / `test.js`; MIT attribution retained. - Target language: Go 1.26+; standard library first. - Node is available as local oracle runtime; Deno is currently unavailable. - diff --git a/.planning/config.json b/.planning/config.json index ff197dce..a90bd9fb 100644 --- a/.planning/config.json +++ b/.planning/config.json @@ -8,6 +8,7 @@ "plan_checker_enabled": true, "nyquist_validation_enabled": true, "workflow": { + "nyquist_validation": true, "ui_phase": false, "ui_safety_gate": false } From 9e8155b5e3847084179b498b65c0adfe5c0d4720 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:30:05 +0530 Subject: [PATCH 034/165] fix(04): preserve JavaScript amount coercion --- compat/cases/operations.jsonl | 8 +++++++- src/cmd/tinycolor-compat/main.go | 32 +++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 886c0de0..cddb35c3 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -24,8 +24,14 @@ {"id":"mod-spin-over-360","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":480},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-spin-omitted","operation":"modify","input":"#ff0000","args":{"method":"spin"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mod-spin-null","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":null},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-lighten-null-default","operation":"modify","input":"#804020","args":{"method":"lighten","amount":null},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-lighten-false-default","operation":"modify","input":"#804020","args":{"method":"lighten","amount":false},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-lighten-string-number","operation":"modify","input":"#804020","args":{"method":"lighten","amount":"20"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mix-default","operation":"mix","input":"#ff0000","args":{"other":"#000000"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mix-zero","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-null-default","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":null},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-false-default","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":false},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-string-number","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":"20"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mix-ninety","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":90},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mix-transparent-alpha","operation":"mix","input":"transparent","args":{"other":"#00000080","amount":25},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mix-out-of-range-high","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":150},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} @@ -55,4 +61,4 @@ {"id":"palette-monochromatic-default","operation":"palette","input":"red","args":{"method":"monochromatic"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"palette-monochromatic-zero-default","operation":"palette","input":"red","args":{"method":"monochromatic","results":0},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"palette-wrapped-split","operation":"palette","input":"#ff0066","args":{"method":"splitcomplement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} -{"id":"palette-analogous-alpha","operation":"palette","input":"rgba(255, 0, 0, .5)","args":{"method":"analogous","results":3,"slices":30},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} \ No newline at end of file +{"id":"palette-analogous-alpha","operation":"palette","input":"rgba(255, 0, 0, .5)","args":{"method":"analogous","results":3,"slices":30},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 42d70728..cfa438dc 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -5,6 +5,8 @@ import ( "fmt" "math" "os" + "strconv" + "strings" "github.com/rajeet-04/tinycolor-go/internal/compat" "github.com/rajeet-04/tinycolor-go/tinycolor" @@ -193,11 +195,12 @@ func palette(id string, color tinycolor.Color, args map[string]any) compat.Respo func defaultAmount(args map[string]any, fallback float64) float64 { if value, ok := args["amount"]; ok { - amount := number(value) - if amount == 0 { + if amount, ok := value.(float64); ok && amount == 0 { return 0 } - return amount + if truthy(value) { + return number(value) + } } return fallback } @@ -210,8 +213,27 @@ func defaultCount(value any, fallback int) int { } func number(value any) float64 { - amount, _ := value.(float64) - return amount + switch value := value.(type) { + case float64: + return value + case bool: + if value { + return 1 + } + return 0 + case nil: + return 0 + case string: + value = strings.TrimSpace(value) + if value == "" { + return 0 + } + amount, err := strconv.ParseFloat(value, 64) + if err == nil { + return amount + } + } + return math.NaN() } func options(args map[string]any) tinycolor.CompatOptions { From 9d7e74e5b184c175f1a0c514b0fd04ca027c4195 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:32:55 +0530 Subject: [PATCH 035/165] fix(04): preserve empty readable fallback semantics --- compat/cases/operations.jsonl | 1 + src/tinycolor/color.go | 12 +++++++----- src/tinycolor/operations_test.go | 9 +++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index cddb35c3..24712017 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -50,6 +50,7 @@ {"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-no-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-empty-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"most-readable-empty-dark-fallback","operation":"mostReadable","input":"#123","args":{"candidates":[],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-empty-no-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":false}},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"palette-complement","operation":"palette","input":"red","args":{"method":"complement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"palette-split-complement","operation":"palette","input":"red","args":{"method":"splitcomplement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 4ea01fe2..b539c60c 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -187,18 +187,20 @@ func IsReadable(first, second Color, options WCAG2Options) bool { return isReadableRatio(Readability(first, second), options) } func MostReadable(base Color, candidates []Color, options WCAG2Options) (Color, bool) { - if len(candidates) == 0 { - return Color{}, false + bestColor, hasBest := Color{}, len(candidates) != 0 + if hasBest { + bestColor = candidates[0] + } else { + bestColor, _ = FromCompat(nil, false) } - bestColor := candidates[0] bestScore := Readability(base, bestColor) - for _, candidate := range candidates[1:] { + for _, candidate := range candidates { if score := Readability(base, candidate); score > bestScore { bestScore, bestColor = score, candidate } } if IsReadable(base, bestColor, options) || !options.IncludeFallbackColors { - return bestColor, true + return bestColor, hasBest } white, _ := FromCompat("#fff", false) black, _ := FromCompat("#000", false) diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 72ccff1b..5906f8fa 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -170,8 +170,13 @@ func TestMostReadable(t *testing.T) { if _, ok := MostReadable(base, nil, WCAG2Options{}); ok { t.Fatal("empty candidates without fallback must have no result") } - if _, ok := MostReadable(base, nil, WCAG2Options{IncludeFallbackColors: true}); ok { - t.Fatal("empty candidates with fallback must have no result") + whiteBase, _ := FromCompat("#fff", false) + if _, ok := MostReadable(whiteBase, nil, WCAG2Options{IncludeFallbackColors: true}); ok { + t.Fatal("readable null candidate must remain no result") + } + darkBase, _ := FromCompat("#123", false) + if got, ok := MostReadable(darkBase, nil, WCAG2Options{IncludeFallbackColors: true}); !ok || got.ToHexString() != "#ffffff" { + t.Fatalf("empty candidates fallback = %#v, %t", got, ok) } } From 8d2cdf377d60db5f680be0aeb5e1ccce339fe9dd Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:34:59 +0530 Subject: [PATCH 036/165] fix(04): match brighten half-tie rounding --- compat/cases/operations.jsonl | 2 ++ src/tinycolor/color.go | 2 +- src/tinycolor/operations_test.go | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 24712017..9b0b5b89 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -6,6 +6,8 @@ {"id":"mod-brighten-zero","operation":"modify","input":"#804020","args":{"method":"brighten","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mod-brighten-clamp","operation":"modify","input":"#fefefe","args":{"method":"brighten","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-brighten-alpha","operation":"modify","input":"#80402080","args":{"method":"brighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-positive-half","operation":"modify","input":"#000","args":{"method":"brighten","amount":0.19607843137254902},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-negative-half","operation":"modify","input":"#010101","args":{"method":"brighten","amount":-0.19607843137254902},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-darken-normal","operation":"modify","input":"#804020","args":{"method":"darken","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-darken-zero","operation":"modify","input":"#804020","args":{"method":"darken","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mod-darken-clamp","operation":"modify","input":"#010101","args":{"method":"darken","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index b539c60c..5683da50 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -375,7 +375,7 @@ func (c *Color) Desaturate(amount float64) *Color { func (c *Color) Greyscale() *Color { return c.Desaturate(100) } func (c *Color) Brighten(amount float64) *Color { - delta := float64(mathRound(255 * amount / 100)) + delta := float64(-mathRound(-255 * amount / 100)) c.model.R = clampChannel(c.model.R + delta) c.model.G = clampChannel(c.model.G + delta) c.model.B = clampChannel(c.model.B + delta) diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 5906f8fa..df4ec630 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -55,6 +55,15 @@ func TestModifiers(t *testing.T) { if got := black.ToHexString(); got != "#010101" { t.Fatalf("Brighten pre-clamp rounding = %s", got) } + halfAmount := 50.0 / 255 + black, _ = FromCompat("#000", false) + if got := black.Brighten(halfAmount).ToHexString(); got != "#000000" { + t.Fatalf("Brighten positive half tie = %s", got) + } + one, _ := FromCompat("#010101", false) + if got := one.Brighten(-halfAmount).ToHexString(); got != "#000000" { + t.Fatalf("Brighten negative half tie = %s", got) + } red, _ = FromCompat("red", false) red.Spin(-120) From 5e14c310311ad2a2f5ed1f9f8082d1b11a9f4c6f Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:37:16 +0530 Subject: [PATCH 037/165] fix(04): preserve WCAG option type errors --- compat/cases/operations.jsonl | 2 ++ src/cmd/tinycolor-compat/main.go | 33 ++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 9b0b5b89..8a15dcb3 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -48,6 +48,8 @@ {"id":"readable-default-options","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{}},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"readable-mixed-case","operation":"isReadable","input":"#db91b8","args":{"other":"#2e0c3a","options":{"level":"aaa","size":"LARGE"}},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"readable-falsy-options","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":false,"size":0}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"readable-number-level-error","operation":"isReadable","input":"#000","args":{"other":"#fff","options":{"level":1,"size":"small"}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"readable-true-size-error","operation":"isReadable","input":"#000","args":{"other":"#fff","options":{"level":"AA","size":true}},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"most-readable-first-tie","operation":"mostReadable","input":"#fff","args":{"candidates":["white","#fff"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-no-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index cfa438dc..aaebeb57 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -145,7 +145,12 @@ func readability(id string, first tinycolor.Color, args map[string]any) compat.R func isReadable(id string, first tinycolor.Color, args map[string]any) compat.Response { second, _ := tinycolor.FromCompat(args["other"], false) - response, _ := compat.Success(id, tinycolor.IsReadable(first, second, wcagOptions(args))) + options, err := wcagOptions(args) + if err != nil { + response, _ := compat.Failure("", err.Error()) + return response + } + response, _ := compat.Success(id, tinycolor.IsReadable(first, second, options)) return response } @@ -156,7 +161,12 @@ func mostReadable(id string, base tinycolor.Color, args map[string]any) compat.R candidate, _ := tinycolor.FromCompat(input, false) candidates = append(candidates, candidate) } - result, ok := tinycolor.MostReadable(base, candidates, wcagOptions(args)) + options, err := wcagOptions(args) + if err != nil { + response, _ := compat.Failure("", err.Error()) + return response + } + result, ok := tinycolor.MostReadable(base, candidates, options) if !ok { response, _ := compat.Success(id, nil) return response @@ -243,15 +253,26 @@ func options(args map[string]any) tinycolor.CompatOptions { return tinycolor.CompatOptions{Format: format, GradientType: gradientType} } -func wcagOptions(args map[string]any) tinycolor.WCAG2Options { +func wcagOptions(args map[string]any) (tinycolor.WCAG2Options, error) { raw, _ := args["options"].(map[string]any) - level, _ := raw["level"].(string) - size, _ := raw["size"].(string) + level, size := "", "" + if value := raw["level"]; truthy(value) { + var ok bool + if level, ok = value.(string); !ok { + return tinycolor.WCAG2Options{}, fmt.Errorf("(parms.level || \"AA\").toUpperCase is not a function") + } + } + if value := raw["size"]; truthy(value) { + var ok bool + if size, ok = value.(string); !ok { + return tinycolor.WCAG2Options{}, fmt.Errorf("(parms.size || \"small\").toLowerCase is not a function") + } + } return tinycolor.WCAG2Options{ Level: level, Size: size, IncludeFallbackColors: truthy(raw["includeFallbackColors"]), - } + }, nil } func analysis(id string, color tinycolor.Color, method any) compat.Response { From c022dfe831e4df217ca182e7bdfafe75e7b37a56 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:38:29 +0530 Subject: [PATCH 038/165] docs(04): remove research trailing whitespace --- .../phases/04-operations-and-palettes/04-RESEARCH.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.planning/phases/04-operations-and-palettes/04-RESEARCH.md b/.planning/phases/04-operations-and-palettes/04-RESEARCH.md index fdf195c9..14cf6878 100644 --- a/.planning/phases/04-operations-and-palettes/04-RESEARCH.md +++ b/.planning/phases/04-operations-and-palettes/04-RESEARCH.md @@ -1,7 +1,7 @@ # Phase 4: Operations and Palettes - Research -**Researched:** 2026-08-01 -**Domain:** Source-compatible TinyColor operations, WCAG readability, and color palettes in Go +**Researched:** 2026-08-01 +**Domain:** Source-compatible TinyColor operations, WCAG readability, and color palettes in Go **Confidence:** HIGH ## User Constraints @@ -263,5 +263,5 @@ None. - Architecture: HIGH - approved Phase 4 design and existing owner boundaries agree. - Semantics and pitfalls: HIGH - direct local source, source tests, and oracle edge probe. -**Research date:** 2026-08-01 -**Valid until:** this checkout's `mod.js` or `test.js` changes. \ No newline at end of file +**Research date:** 2026-08-01 +**Valid until:** this checkout's `mod.js` or `test.js` changes. From 26c3e17e7a1945a4c1b43da1780e0648ea63f1c6 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:41:19 +0530 Subject: [PATCH 039/165] docs(04): record parity regression evidence --- .../phases/04-operations-and-palettes/04-02-SUMMARY.md | 4 ++-- .../phases/04-operations-and-palettes/04-06-SUMMARY.md | 9 +++++---- COMPATIBILITY.md | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md index 64e3e820..c33233da 100644 --- a/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md +++ b/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md @@ -43,7 +43,7 @@ with deterministic owner-tagged operations reproducers. The six Mix mismatch records revealed that the Go value dropped TinyColor's observable raw interpolated RGBA input. Follow-up commit `d2f22c6` (`fix(04-02): preserve mix original metadata`) restored that metadata. The -final Phase 4 gate passed all `58/58` operations rows with zero mismatches. +final Phase 4 parity gate passed all `69/69` operations rows with zero mismatches. ## Commits @@ -55,4 +55,4 @@ final Phase 4 gate passed all `58/58` operations rows with zero mismatches. The corpus correctly exposed a shared-library defect after the planned adapter work. It was fixed in a separate focused micro-commit rather than masked in -the harness. \ No newline at end of file +the harness. diff --git a/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md index ec2ab630..38f6604b 100644 --- a/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md +++ b/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md @@ -3,14 +3,15 @@ phase: 4 plan: 6 subsystem: palettes-compatibility-phase-gate provides: [palette JSONL dispatch and complete Phase 4 evidence] -commits: [8776335, c328fa4, 7cd0e84, 796044f] +commits: [8776335, c328fa4, 7cd0e84, 796044f, 9e8155b, 9d7e74e, 8d2cdf3, 5e14c31] --- # Phase 4 Plan 6: Palette Compatibility and Gate Summary Added palette dispatch for all six public operations, retaining source order -and adapter-owned zero defaults. The final corpus contains 58 operations rows. -A focused follow-up repaired `Analogous` shared `original` metadata. +and adapter-owned zero defaults. The final corpus contains 69 operations rows. +Focused follow-ups repaired `Analogous` metadata, dynamic amount/WCAG behavior, +empty-candidate fallback selection, and brighten half-tie rounding. Final validation passed: Go tests and vet, adapter tests, and fixed corpora at -9/9 smoke, 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 58/58 operations. \ No newline at end of file +9/9 smoke, 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 69/69 operations. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 1cbde4c1..04436a3c 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -13,7 +13,7 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | | Conversion/formatting | Passing | `compat/cases/conversion.jsonl` | 35 fixed JSONL cases; random is invariant-only. | | Analysis/readability | Passing | `compat/cases/operations.jsonl` | WCAG defaults, thresholds, ties, fallback, and null selection. | -| Manipulation/palettes | Passing | `compat/cases/operations.jsonl` | 58 fixed operations cases cover modifiers, Mix, and ordered palettes. | +| Manipulation/palettes | Passing | `compat/cases/operations.jsonl` | 69 fixed operations cases cover modifiers, Mix, readability, and ordered palettes. | | CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | | Original Deno suite | Blocked locally | `deno` unavailable | Run when Deno is installed or in CI. | @@ -87,7 +87,7 @@ Go random generators. ## Phase 4 operations result -On 2026-08-01, the fixed operations corpus passed with **58/58 cases** and +On 2026-08-01, the fixed operations corpus passed with **69/69 cases** and **0 mismatches**. The full available Phase 1-4 gate also retained **9/9** smoke, **26/26** HEX/RGB/name, **23/23** parser, and **35/35** conversion cases with zero mismatches. This evidence does not claim a Deno source-suite From 4060b05ee6cf2deef87aede1e40d94e2db3a7326 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:44:08 +0530 Subject: [PATCH 040/165] docs: resolve debug phase4-parity-gaps --- .../debug/resolved/phase4-parity-gaps.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .planning/debug/resolved/phase4-parity-gaps.md diff --git a/.planning/debug/resolved/phase4-parity-gaps.md b/.planning/debug/resolved/phase4-parity-gaps.md new file mode 100644 index 00000000..3def3314 --- /dev/null +++ b/.planning/debug/resolved/phase4-parity-gaps.md @@ -0,0 +1,113 @@ +--- +status: resolved +trigger: "Investigate and fix Phase 4 parity gaps: JS coercion/defaults, MostReadable empty fallback, Brighten half-boundary rounding, truthy non-string WCAG level/size errors, and trailing whitespace." +created: 2026-08-01T03:25:49.1964363+05:30 +updated: 2026-08-01T03:43:31.6647882+05:30 +--- + +## Current Focus + +hypothesis: Resolved; automated and independent primary-agent verification both confirm parity. +test: Archive this session and commit only the resolved debug document. +expecting: No active Phase 4 debug session remains and source stays untouched. +next_action: Move this file to `.planning/debug/resolved/` and create the final docs micro-commit. + +## Symptoms + +expected: Go compatibility behavior exactly matches local mod.js/test.js. +actual: "Reviewer reproducers: (1) modifier/mix amount JS coercion/default gaps for null/false/numeric strings; (2) MostReadable with empty candidates and fallback enabled should still choose white/black; (3) Brighten half-boundary must mirror -Math.round(-delta); (4) truthy non-string WCAG level/size should reproduce source throw/error behavior." +errors: Existing 58-row corpus passes because these cases are absent. +reproduction: Use direct Node oracle requests and new fixed JSONL rows for the reported edge cases. +started: Introduced during Phase 4 commits from base 38a7483 through 5601b29. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T03:27:00.1181439+05:30 + checked: Complete `mod.js` operations, Go facade/adapter, current unit tests/corpus, and Phase 4 research/validation. + found: Source uses `amount === 0 ? 0 : amount || fallback`, `-Math.round(-delta)`, and `(value || default).toUpperCase/toLowerCase`; Go uses float64-only decoding, `mathRound(delta)`, and string-only WCAG fields. `MostReadable` currently returns no result immediately for every empty list. + implication: All four reviewer areas have specific falsifiable divergence mechanisms; empty fallback must be probed with both readable-null and unreadable-null bases. +- timestamp: 2026-08-01T03:27:39.1157918+05:30 + checked: Twelve direct Node/Go requests covering all reported edges. + found: Eleven mismatches reproduced. Null/false/numeric-string amounts diverged for modifiers and Mix; dark-base empty fallback diverged while white-base empty fallback matched null; both brighten half signs diverged; truthy numeric level and boolean size threw in Node but returned true in Go. + implication: All four claims are valid with the MostReadable qualification that fallback occurs only when the provisional null/black candidate is not already readable. +- timestamp: 2026-08-01T03:28:28.5725482+05:30 + checked: Operations differential after adding six permanent amount cases. + found: RED reproduced exactly six mismatches; all 58 pre-existing cases passed. + implication: The failure is isolated to adapter amount defaulting/coercion, not typed modifier or Mix arithmetic. +- timestamp: 2026-08-01T03:29:26.9690590+05:30 + checked: Go suite and 64-row operations corpus after adapter fix. + found: `go test ./...` passed with repository-local GOCACHE; operations passed 64/64 with zero mismatches; focused diff check passed. + implication: Shared adapter coercion now reproduces modifier/Mix null, false, and numeric-string semantics without public API changes. +- timestamp: 2026-08-01T03:30:30.8620009+05:30 + checked: Atomic commit for amount fix. + found: Commit `9e8155b` contains only the operations corpus and Go adapter changes. + implication: Amount parity is independently recoverable and the next bug can be tested in isolation. +- timestamp: 2026-08-01T03:31:33.4612643+05:30 + checked: Focused `TestMostReadable` and 65-row differential after adding dark-base empty fallback. + found: Both RED checks fail only because Go returns no result while Node returns white; the original white-base empty case still passes. + implication: The early empty-list return is the confirmed root cause; fallback must depend on provisional null/black readability. +- timestamp: 2026-08-01T03:32:34.8788960+05:30 + checked: Focused MostReadable test, full Go suite, and 65-row operations differential after the fix. + found: All checks passed; white-base empty fallback remains null and dark-base empty fallback returns white. + implication: Selection now matches the source's null-as-black intermediate behavior without changing the nullable API. +- timestamp: 2026-08-01T03:33:05.1909523+05:30 + checked: Atomic commit for empty fallback fix. + found: Commit `9d7e74e` contains only the paired regressions and shared MostReadable change. + implication: Empty fallback parity is independently complete. +- timestamp: 2026-08-01T03:33:58.7789774+05:30 + checked: Focused modifier test and 67-row differential with exact positive/negative half ties. + found: RED failed at positive half in the unit test and exactly both half cases in the corpus; 65 prior rows passed. + implication: `mathRound(delta)` is the isolated root cause and must mirror the source's negated-round expression. +- timestamp: 2026-08-01T03:34:38.9954301+05:30 + checked: Focused modifier test, full Go suite, and 67-row operations differential after one-line rounding fix. + found: All checks passed, including both exact half signs and the pre-existing `.2` non-tie case. + implication: Brighten now matches `-Math.round(-delta)` without changing other modifiers. +- timestamp: 2026-08-01T03:35:20.2523946+05:30 + checked: Atomic commit for brighten fix. + found: Commit `8d2cdf3` contains only half-boundary regressions and the one-line delta correction. + implication: Brighten parity is independently complete. +- timestamp: 2026-08-01T03:36:05.3216422+05:30 + checked: 69-row differential after adding two truthy non-string WCAG cases. + found: RED produced exactly two mismatches with Node throwing `.toUpperCase`/`.toLowerCase` errors and Go returning true; 67 prior cases passed, including false/zero defaults. + implication: Dynamic type validation belongs only in the compatibility adapter before constructing typed WCAG options. +- timestamp: 2026-08-01T03:36:54.3807334+05:30 + checked: Full Go test/vet, adapter test, and 69-row operations differential after WCAG validation. + found: All checks passed; exact error strings and blank ids match Node, while falsy non-string defaults remain green. + implication: The adapter now preserves source throw behavior without weakening the typed Go WCAG API. +- timestamp: 2026-08-01T03:37:40.4754534+05:30 + checked: Atomic commit for WCAG fix and direct trailing-whitespace scan of `04-RESEARCH.md`. + found: Commit `5e14c31` contains only corpus/adapter changes; research lines 3, 4, and 266 each end in two spaces. + implication: The reviewer whitespace claim is confirmed and isolated to three documentation lines. +- timestamp: 2026-08-01T03:38:11.3678041+05:30 + checked: Direct whitespace scan and focused diff check after docs cleanup. + found: No trailing whitespace remains; only three line endings and the previously missing final newline changed. + implication: Documentation cleanup is ready for its independent commit. +- timestamp: 2026-08-01T03:38:43.7870812+05:30 + checked: Atomic commit for research cleanup. + found: Commit `c022dfe` contains only `04-RESEARCH.md`. + implication: All requested changes are independently committed and ready for final regression verification. +- timestamp: 2026-08-01T03:40:22.7203553+05:30 + checked: Complete Phase 1-4 gate. + found: Go test/vet and adapter test passed; corpora passed 9/9, 26/26, 23/23, 35/35, and 69/69 with zero mismatches; `git diff --check` passed; immutable oracle diff was empty. + implication: The fixes are regression-safe across all available phases; Deno remains unavailable and is not claimed. +- timestamp: 2026-08-01T03:40:58.9567769+05:30 + checked: Phase 4 evidence references and docs diff. + found: All stale 58-row current evidence references were replaced with 69/69; diff check passed. + implication: Evidence docs accurately describe the final verified gate. +- timestamp: 2026-08-01T03:41:28.3450918+05:30 + checked: Evidence-only commit and final worktree state. + found: Commit `26c3e17` records 69/69 evidence; all requested code/test/docs changes are committed independently. + implication: Automated verification is complete; only the GSD human-verify checkpoint remains before archival. +- timestamp: 2026-08-01T03:43:31.6647882+05:30 + checked: Human confirmation and independent primary-agent verification. + found: Confirmed fixed; primary-agent checks independently passed Go test/vet, adapter, all corpora, diff check, clean oracle diff, and matching oracle hashes. + implication: Resolution is confirmed end-to-end and the session can be archived. + +## Resolution + +root_cause: Adapter coercion, empty-list fallback selection, brighten tie rounding, and WCAG dynamic type handling each flattened a distinct JavaScript behavior. +fix: Amount coercion now follows strict-zero/truthiness and numeric conversion; empty MostReadable lists evaluate the source's null/black provisional candidate before fallback; Brighten uses `-mathRound(-delta)`; WCAG truthy non-strings return the oracle's exact errors; Phase 4 evidence and whitespace were refreshed. +verification: Full gate passed locally and independently under the primary agent: Go test/vet, adapter tests, corpora 9/9 + 26/26 + 23/23 + 35/35 + 69/69, diff check, empty immutable-oracle diff, and matching oracle hashes. Human confirmed fixed. Deno unavailable and not claimed. +files_changed: [compat/cases/operations.jsonl, src/cmd/tinycolor-compat/main.go, src/tinycolor/color.go, src/tinycolor/operations_test.go, COMPATIBILITY.md, .planning/phases/04-operations-and-palettes/04-02-SUMMARY.md, .planning/phases/04-operations-and-palettes/04-06-SUMMARY.md, .planning/phases/04-operations-and-palettes/04-RESEARCH.md] From 7f3eda15371c379c91b100b5fdb43bc149b7755d Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:54:00 +0530 Subject: [PATCH 041/165] docs(05): define delivery evidence design --- ...-08-01-phase-5-delivery-evidence-design.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md diff --git a/docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md b/docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md new file mode 100644 index 00000000..9cc4396b --- /dev/null +++ b/docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md @@ -0,0 +1,90 @@ +# Phase 5 Delivery Evidence Design + +## Goal + +Turn the completed TinyColor Go port into a reproducible Track H submission +that a judge can build, exercise, measure, and audit from a clean checkout. + +## Submission structure + +Keep the existing advisory layout as the actual submission layout: + +```text +TinyColor/ +├── README.md +├── DECISIONS.md +├── Dockerfile +├── src/ +├── tests/original/ +├── tests/port/ +├── fuzz/ +│ ├── harness.mjs +│ └── log.txt +├── bench/ +│ ├── methodology.md +│ └── results.json +└── .port-mortem.toml +``` + +Existing project files remain in place. No parallel submission tree or new +dependency is introduced. + +## CLI + +Reuse the existing Go compatibility binary and TinyColor package. Add a human +command interface for `parse`, `convert`, `lighten`, `palette`, and `contrast`. +Every command accepts `--json`; JSON output is deterministic and suitable for +demo comparisons. Invalid commands and missing values return nonzero status +with a concise stderr message. The JSONL compatibility mode remains available +so the differential driver does not change. + +## Reproducible checks and CI + +`make build` produces `bin/tinycolor`. `make verify` runs formatting checks, +Go tests, vet, immutable-oracle hash verification, adapter tests, and every +fixed differential corpus. GitHub Actions invokes the same commands rather +than maintaining a second CI-only procedure. Docker continues to build the +same binary in one command. + +## Differential fuzz evidence + +Add a standard-library Node harness that generates deterministic and randomized +public-API requests, sends the identical stream to the checked-in JavaScript +oracle and Go port, and reports duration, seed, case count, and divergences. +Publish an actual run lasting at least 60 continuous seconds in `fuzz/log.txt`. +A zero-divergence bonus is claimed only if that run finishes with zero +divergences; any mismatch is retained as a reproducible input. + +## Benchmark evidence + +Use one shared workload file for the original JavaScript and Go runners. +Measure cold startup, request latency p99, throughput, and peak RSS with the +same host and tool versions. Record the commands and limitations in +`bench/methodology.md` and machine-readable measurements in +`bench/results.json`. Results state sample counts and do not infer universal +speedups from one machine. + +## Documentation and scoring evidence + +Rewrite the root README around the Go port while retaining upstream attribution +and links. Expand `DECISIONS.md` to at least ten non-trivial, defensible +divergences. Update `COMPATIBILITY.md` with pass rates per corpus, immutable +hash status, unsafe count, coverage data where measured, and known limitations. +Keep team ownership and license attribution explicit. Add a five-minute demo +script, but do not claim that a video was recorded or published unless an +actual video artifact or URL is supplied. + +## Verification + +The phase closes only after CLI tests pass, `make build` and `make verify` +succeed, all pinned oracle hashes match, the 60-second fuzz session is recorded, +benchmark JSON validates, documentation contains no unsupported claims, and +the worktree passes `git diff --check`. Deno remains an honest environment gap +unless it is installed and the untouched source suite is run successfully. + +## Deliberate exclusions + +- No GUI, package publication, transpiler, JavaScript runtime embedding, or FFI. +- No new dependency when Go or Node standard libraries cover the need. +- No speculative Bug Catcher claim; document it only if differential testing + produces a real upstream defect. From 4f6f4a2011800edb7e6a119988838b286ef7ba79 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:55:07 +0530 Subject: [PATCH 042/165] docs(05): capture delivery decisions --- .../phases/05-delivery-evidence/05-CONTEXT.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-CONTEXT.md diff --git a/.planning/phases/05-delivery-evidence/05-CONTEXT.md b/.planning/phases/05-delivery-evidence/05-CONTEXT.md new file mode 100644 index 00000000..db064835 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-CONTEXT.md @@ -0,0 +1,128 @@ +# Phase 5: Delivery Evidence - Context + +**Gathered:** 2026-08-01 +**Status:** Ready for planning +**Source:** Approved Phase 5 delivery design + + +## Phase Boundary + +Ship the completed Go port as a reproducible Track H submission. This phase +adds the human CLI, clean-checkout verification, CI, differential fuzz evidence, +honest benchmark evidence, and judge-facing documentation. It does not add new +TinyColor behavior or change the immutable JavaScript oracle. + + + + +## Implementation Decisions + +### Submission layout +- Keep `README.md`, `DECISIONS.md`, `Dockerfile`, `src/`, `tests/original/`, + `tests/port/`, `fuzz/`, `bench/`, and `.port-mortem.toml` in their current + top-level locations. +- Do not create a parallel submission tree. +- Preserve `mod.js`, `tinycolor.js`, `test.js`, `npm/`, `dist/`, and `demo/` as + immutable oracle material. + +### CLI and build +- Reuse the existing Go compatibility binary and `tinycolor` package. +- Provide `parse`, `convert`, `lighten`, `palette`, and `contrast` commands. +- Every human command supports deterministic `--json` output. +- Preserve JSONL compatibility mode for `compat/run.mjs`. +- `make build` produces `bin/tinycolor`; Docker builds the same executable. + +### Verification and CI +- `make verify` checks Go formatting, tests, vet, immutable oracle hashes, + adapter tests, and every fixed differential corpus. +- GitHub Actions calls the documented repository commands instead of duplicating + a separate CI-only procedure. +- Do not claim the untouched Deno suite passed unless Deno is installed and + `deno task test` actually succeeds. + +### Differential fuzzing +- Use Node and Go standard libraries only. +- Run identical generated public-API requests through the checked-in JavaScript + oracle and Go port. +- Publish a real `fuzz/log.txt` from at least 60 continuous seconds, including + duration, seed, case count, and divergence count. +- Claim Differential Fuzz Survivor only if the recorded run has zero divergences. + +### Benchmarks +- Use a shared workload for JavaScript and Go on the same machine. +- Report cold startup, latency p99, throughput, and peak RSS with sample counts, + tool versions, commands, and limitations. +- Store machine-readable values in `bench/results.json` and methodology in + `bench/methodology.md`; do not generalize one machine's result. + +### Documentation +- Make the root README describe the Go port, migration rationale, one-command + build, CLI examples, verification, evidence, known limitations, and upstream + attribution. +- Record at least ten substantive decisions in `DECISIONS.md`. +- Report corpus pass rates, oracle hash status, unsafe count, coverage where + measured, and all known limitations in `COMPATIBILITY.md`. +- Keep team ownership and MIT attribution explicit. +- Add a five-minute demo script. Do not claim a recorded or published video + without an actual video artifact or URL. + +### the agent's Discretion +- Exact CLI flag spelling and JSON field arrangement, provided outputs are stable + and tests demonstrate all five required commands. +- Exact deterministic fuzz input distribution and benchmark sample sizes. +- CI runner versions and artifact naming, provided clean-checkout commands match + local documentation. + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Approved delivery contract +- `docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md` — locked + Phase 5 structure, evidence, and honest-claim rules. +- `.planning/ROADMAP.md` — Phase 5 goal and success criteria. +- `.planning/REQUIREMENTS.md` — DEL-01 and QLT-03 requirements. + +### Project behavior and boundaries +- `AGENT.md` — immutable oracle, ownership, checks, and definition of done. +- `PLAN.md` — Wave 4 delivery scope and gate. +- `src/cmd/tinycolor-compat/main.go` — existing JSONL command boundary to reuse. +- `compat/run.mjs` — existing differential execution path. +- `tests/original/manifest.sha256` — pinned oracle hashes. + +### Existing delivery files +- `Makefile` — current build and test entry points. +- `Dockerfile` — current one-command artifact build. +- `DECISIONS.md` — accepted architectural decisions. +- `COMPATIBILITY.md` — current evidence and honest limitations. +- `docs/TEAM-OWNERSHIP.md` — contributor responsibilities. + + + + +## Specific Ideas + +- Favor the attainable Differential Fuzz Survivor, Zero Unsafe, and Decision Log + bonus evidence. +- Keep every implementation slice in a separate micro-commit so one feature can + be reverted without undoing unrelated Phase 5 work. + + + + +## Deferred Ideas + +- A GUI, web service, package publication, transpiler, FFI, and JavaScript + runtime embedding remain out of scope. +- Bug Catcher is not a planned claim; add it only if differential testing finds + a reproducible upstream defect. + + + +--- + +*Phase: 05-delivery-evidence* +*Context gathered: 2026-08-01 from approved delivery design* From 7758b431de2b0312abc21b248ad8c5fad676c6ae Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:02:59 +0530 Subject: [PATCH 043/165] docs(05): research delivery evidence --- .../05-delivery-evidence/05-RESEARCH.md | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-RESEARCH.md diff --git a/.planning/phases/05-delivery-evidence/05-RESEARCH.md b/.planning/phases/05-delivery-evidence/05-RESEARCH.md new file mode 100644 index 00000000..16f28a38 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-RESEARCH.md @@ -0,0 +1,158 @@ +# Phase 5 Research: Delivery Evidence + +## Scope and current state + +Phase 5 is an integration phase, not a new color-behavior phase. The Go package +and JSONL adapter already cover the required TinyColor operations. Delivery +work should expose those paths, make every claim reproducible, and replace +placeholders with measured evidence. + +Current gaps: + +- `src/cmd/tinycolor-compat/main.go` only reads JSONL from stdin; there is no + human command interface or command-level test. +- `Makefile` builds `bin/tinycolor-compat` and omits conversion/operation + corpora, formatting, hash verification, and a single full gate. +- `.github/workflows/deno.yml` is the upstream Deno build workflow, not the Go + port CI gate. +- `fuzz/README.md`, `bench/methodology.md`, and `bench/results.json` are Phase 1 + placeholders. +- The root `README.md` describes the JavaScript package rather than the port. +- `DECISIONS.md` has seven entries; the Decision Log bonus needs ten substantive + decisions. +- `COMPATIBILITY.md` has correct fixed-corpus evidence but no Phase 5 CLI, hash, + fuzz, benchmark, unsafe, or coverage evidence. + +## Recommended architecture + +### Human CLI without a second binary + +Keep `cmd/tinycolor-compat` as the only executable. Select JSONL mode when no +positional arguments are present and human mode when a command is present. +Move argument parsing and rendering into small functions in the same package so +`main_test.go` can test them without process fixtures. Reuse `tinycolor.Parse`, +conversion methods, modifiers, palettes, and readability directly; do not route +human commands through encoded JSONL requests. + +Use Go's `flag.FlagSet` per command. Required surface: + +- `parse `: inspection/state. +- `convert --to hex|hex8|rgb|percentage-rgb|hsl|hsv|name`. +- `lighten [--amount 10]`. +- `palette --type complement|splitcomplement|triad|tetrad|analogous|monochromatic`. +- `contrast `: ratio and AA/AAA small/large booleans. +- `--json` on every command; stable object/array output through `encoding/json`. + +Usage errors must return status 2, parse/runtime errors status 1, and success 0. +Invalid TinyColor input remains a successful black-like TinyColor value because +that is source behavior; JSON inspection exposes `valid: false`. + +### One source of truth for checks + +Add `tests/original/verify.mjs` using `node:crypto` to validate +`manifest.sha256` on Windows and Linux. Expand the Makefile: + +- `make build` -> `bin/tinycolor`. +- `make test` -> port-owned unit/adapter/differential checks. +- `make verify` -> formatting check, tests, vet, oracle hash verification, and + all five fixed corpora. +- `make fuzz` and `make bench` -> documented evidence generators. + +CI should install pinned Go, Node, and Deno versions; call `make verify`; run the +untouched `deno task test`; and build the binary. Do not duplicate individual +test commands in YAML beyond the Deno-only source-suite check. + +### Differential fuzzing + +The current `compat/run.mjs` starts `go run` once per case, which is too slow +for a 60-second fuzz session. `fuzz/harness.mjs` should build the Go binary once, +start one JavaScript runner and one Go runner, stream the same JSONL requests to +both, and compare response lines with `isDeepStrictEqual`. Use an explicit +seeded PRNG and public operations already accepted by the adapter. Print a +machine-readable header/footer and every mismatch request. Exit nonzero on any +divergence. + +The committed `fuzz/log.txt` must be output from an actual `--duration 60` +session. A deterministic seed makes a mismatch reproducible even though the +case count varies by machine. The fixed corpus remains the correctness gate; +the fuzz run is additional evidence. + +### Honest benchmarks + +Use `bench/workload.jsonl` as the identical request stream for both runners. +`bench/run.mjs` should build once, measure multiple cold process starts, then +measure a persistent runner for latency and throughput. Compute p99 by sorting +all elapsed samples and selecting `ceil(0.99*n)-1`. Sample peak RSS while each +child is alive: read `/proc//status` on Linux and query +`Get-Process -Id ` on Windows. If RSS sampling is unsupported, emit `null` +and a limitation instead of inventing a value. + +`bench/results.json` needs timestamp, OS/CPU, Go/Node versions, workload hash, +sample counts, and for each implementation: startup p99, request latency p99, +throughput, and peak RSS. Run both implementations on the same host in one +command and state that results are local observations, not universal speedups. + +### Documentation + +Rewrite `README.md` for judges but retain the upstream project link and MIT +attribution. Link detailed evidence instead of duplicating it. Expand +`DECISIONS.md` with the CLI dual mode, immutable manifest verification, +standard-library fuzz harness, shared benchmark workload, and honest Deno/video +claim policy. Update `COMPATIBILITY.md` only after commands have produced fresh +results. Add `docs/DEMO.md` as a five-minute live script; video recording remains +a manual submission action. + +## Pitfalls + +- Go `flag` stops parsing at the first positional argument. Either document + flags before values or normalize the small supported grammar before parsing; + tests must lock the chosen syntax. +- Do not make invalid TinyColor inputs CLI errors; that would contradict source + behavior. +- Keep JSONL stdin mode byte-compatible with existing adapter tests. +- Do not benchmark `go run`; compile once before measuring. +- Do not compare a hot Go function with a cold Node process. Shared workload and + lifecycle are required. +- A generated zero-divergence log is credible only when duration and seed are + recorded and the harness itself is checked in. +- `unsafe` is a lexical/code audit for this pure-Go repository; report the exact + command and count rather than merely saying "safe". +- Go coverage and Deno coverage measure different suites. Report them separately + and do not describe their percentage difference as behavioral parity. + +## Validation Architecture + +### Fast feedback + +| Change | Command | Expected result | +|---|---|---| +| CLI | `go -C src test ./cmd/tinycolor-compat` | exit 0 | +| Hash verifier | `node tests/original/verify.mjs` | 3 files verified | +| Fuzz harness | `node fuzz/harness.mjs --duration 1 --seed 1` | zero divergences | +| Benchmark runner | `node bench/run.mjs --quick` | valid `bench/results.json` | +| Documentation | `git diff --check` | exit 0 | + +### Full phase gate + +```powershell +make build +make verify +node fuzz/harness.mjs --duration 60 --seed 20260801 +node bench/run.mjs +git diff --check +``` + +Then verify `tests/original/manifest.sha256` still matches, validate +`bench/results.json` with `JSON.parse`, confirm `fuzz/log.txt` records at least +60 seconds and zero divergences before claiming the bonus, and run +`deno task test` only where Deno is installed. + +### Requirement mapping + +| Requirement | Automated evidence | +|---|---| +| DEL-01 | CLI tests, `make build`, CI, fuzz run, benchmark JSON, documentation checks | +| QLT-03 | `make verify`, oracle manifest verifier, fixed corpora, clean-checkout CI | + +Existing Go tests and JSONL corpora are sufficient infrastructure. No new test +framework or dependency is needed. From 0451f70aba0a39b46bb5053c6d573817eeffac72 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:03:35 +0530 Subject: [PATCH 044/165] docs(05): add validation strategy --- .../05-delivery-evidence/05-VALIDATION.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-VALIDATION.md diff --git a/.planning/phases/05-delivery-evidence/05-VALIDATION.md b/.planning/phases/05-delivery-evidence/05-VALIDATION.md new file mode 100644 index 00000000..13bc34db --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-VALIDATION.md @@ -0,0 +1,66 @@ +--- +phase: 5 +slug: delivery-evidence +status: draft +nyquist_compliant: true +wave_0_complete: true +created: 2026-08-01 +--- + +# Phase 5 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Go `testing`, Node built-in modules, existing JSONL differential harness | +| **Config file** | `src/go.mod`, `deno.json`, `Makefile` | +| **Quick run command** | `go -C src test ./...` | +| **Full suite command** | `make verify` | +| **Estimated runtime** | quick under 10 seconds; full under 5 minutes | + +## Sampling Rate + +- **After every task commit:** Run the task's focused command and `go -C src test ./...` when Go changed. +- **After every plan wave:** Run `make verify`. +- **Before `$gsd-verify-work`:** `make verify`, the recorded fuzz command, and benchmark validation must be green. +- **Max feedback latency:** 10 seconds for focused tests; 5 minutes for the full gate. + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|-----------|-------------------|-------------|--------| +| 05-01-01 | 01 | 1 | DEL-01 | CLI unit | `go -C src test ./cmd/tinycolor-compat` | ✅ | ⬜ pending | +| 05-01-02 | 01 | 1 | QLT-03 | integration | `make build && make verify` | ✅ make / ❌ expanded targets | ⬜ pending | +| 05-02-01 | 02 | 2 | DEL-01 | differential fuzz | `node fuzz/harness.mjs --duration 1 --seed 1` | ❌ Wave 2 | ⬜ pending | +| 05-02-02 | 02 | 2 | DEL-01 | benchmark smoke | `node bench/run.mjs --quick` | ❌ Wave 2 | ⬜ pending | +| 05-03-01 | 03 | 3 | DEL-01 | evidence audit | `node tests/original/verify.mjs` | ❌ Wave 1 | ⬜ pending | +| 05-03-02 | 03 | 3 | QLT-03 | full gate | `make verify` | ✅ make / ❌ expanded target | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +## Wave 0 Requirements + +Existing Go tests, Node adapter tests, JSONL corpora, and Make are sufficient. +Each new runner supplies its own focused standard-library check; no framework +installation or test scaffold is required. + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Five-minute video is actually recorded and published | DEL-01 | Recording and public hosting require a human account and final presentation | Follow `docs/DEMO.md`, record one continuous run, publish it, then add the verified URL to README | +| Public GitHub visibility | DEL-01 | Repository visibility is external account state | Open the GitHub repository logged out and confirm source and release instructions are readable | + +## Validation Sign-Off + +- [x] All planned tasks have an automated focused command or existing infrastructure. +- [x] Sampling continuity has no three consecutive tasks without automated verification. +- [x] Existing infrastructure covers Wave 0. +- [x] No watch-mode flags are used. +- [x] Focused feedback latency target is under 10 seconds. +- [x] `nyquist_compliant: true` is set in frontmatter. + +**Approval:** approved 2026-08-01 From 244ca14f10ad69b63eadaab69201842e5faf3e1f Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:19:23 +0530 Subject: [PATCH 045/165] docs(05): plan delivery evidence --- .../phases/05-delivery-evidence/05-01-PLAN.md | 109 +++++++++++++ .../phases/05-delivery-evidence/05-02-PLAN.md | 110 +++++++++++++ .../phases/05-delivery-evidence/05-03-PLAN.md | 149 ++++++++++++++++++ .../05-delivery-evidence/05-VALIDATION.md | 5 +- 4 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/05-delivery-evidence/05-01-PLAN.md create mode 100644 .planning/phases/05-delivery-evidence/05-02-PLAN.md create mode 100644 .planning/phases/05-delivery-evidence/05-03-PLAN.md diff --git a/.planning/phases/05-delivery-evidence/05-01-PLAN.md b/.planning/phases/05-delivery-evidence/05-01-PLAN.md new file mode 100644 index 00000000..8b32668e --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-01-PLAN.md @@ -0,0 +1,109 @@ +--- +phase: 5 +plan: 1 +type: execute +subsystem: cli-build-ci +owner: D-deepali +tags: [cli, build, verification, ci, docker] +wave: 1 +depends_on: [] +files_modified: [src/cmd/tinycolor-compat/main.go, src/cmd/tinycolor-compat/main_test.go, tests/original/verify.mjs, tests/original/verify.test.mjs, Makefile, Dockerfile, .github/workflows/port.yml, .github/workflows/deno.yml] +autonomous: true +requirements: [DEL-01, QLT-03] +must_haves: + truths: + - "One executable supports parse, convert, lighten, palette, contrast, and the existing JSONL adapter mode." + - "One documented build command produces bin/tinycolor and one verify command checks every available local gate." + - "CI calls the repository verification command and runs the untouched Deno source suite separately." + artifacts: + - path: "src/cmd/tinycolor-compat/main_test.go" + provides: "Command-level regression coverage for all required human CLI operations and JSONL mode" + - path: "tests/original/verify.mjs" + provides: "Cross-platform immutable-oracle hash verification" + - path: ".github/workflows/port.yml" + provides: "Clean-checkout build, parity, source-suite, and artifact CI" + key_links: + - from: "Makefile" + to: "tests/original/manifest.sha256" + via: "node tests/original/verify.mjs" + pattern: "tests/original/verify.mjs" + - from: ".github/workflows/port.yml" + to: "Makefile" + via: "make verify" + pattern: "make verify" +--- + +Expose the completed port through a tested human CLI and make its build and parity gate reproducible locally, in Docker, and in CI. +@AGENT.md +@.planning/phases/05-delivery-evidence/05-CONTEXT.md +@.planning/phases/05-delivery-evidence/05-RESEARCH.md +@.planning/phases/05-delivery-evidence/05-VALIDATION.md +@src/cmd/tinycolor-compat/main.go +@src/tinycolor/color.go +@src/internal/compat/protocol.go +@compat/run.mjs +@tests/port/adapter.test.mjs +@tests/original/manifest.sha256 +@Makefile +@Dockerfile +@deno.json + + +Add the dual-mode human CLI without changing JSONL behavior +src/cmd/tinycolor-compat/main.go, src/cmd/tinycolor-compat/main_test.go +Read `src/cmd/tinycolor-compat/main.go` completely, then read exported methods and structs in `src/tinycolor/color.go`, response encoding in `src/internal/compat/protocol.go`, and stdin contract assertions in `tests/port/adapter.test.mjs`. + +RED: add table tests around `run(args []string, stdin io.Reader, stdout, stderr io.Writer) int`. Assert no args still accepts one JSONL `inspect` request. Assert `parse --json red` returns a JSON object with `hex:"ff0000"` and `valid:true`; `convert --to hsl red` prints `hsl(0, 100%, 50%)`; `lighten --amount 10 #000` prints `#1a1a1a`; `palette --type triad red` prints three ordered hex colors; and `contrast #000 #fff --json` returns ratio `21` plus AA/AAA small/large booleans. Assert unknown command and missing required flags return status 2 with usage on stderr. Run the focused test and confirm RED. + +GREEN: make `main` call `os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))`. With zero args, retain the current line-scanner JSONL loop and return zero after EOF; malformed individual requests still emit protocol failures without terminating the stream. With args, use a command-specific `flag.FlagSet` with `ContinueOnError`; flags precede positional color values. Implement exactly: `parse [--json] `, `convert --to hex|hex8|rgb|percentage-rgb|hsl|hsv|name [--json] `, `lighten [--amount 10] [--json] `, `palette --type complement|splitcomplement|triad|tetrad|analogous|monochromatic [--results 6] [--slices 30] [--json] `, and `contrast [--json] `. Human colors are strings passed to `tinycolor.FromCompat(value, false)`; invalid input remains a successful TinyColor value and JSON exposes `valid:false`. Human parse JSON is `Inspect()`, palette JSON is an array of `Inspect()` objects, and contrast JSON is `{ratio, aaSmall, aaLarge, aaaSmall, aaaLarge}` using `Readability` and `IsReadable`. Use `encoding/json`; no new package or binary. Run focused and full Go tests, then commit `feat(05-01): add human CLI commands`. + + +- `src/cmd/tinycolor-compat/main.go` contains `func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int`. +- Tests invoke all five command names and the zero-argument JSONL path. +- `go -C src test ./cmd/tinycolor-compat -count=1` exits 0. +- `node tests/port/adapter.test.mjs` exits 0, proving compatibility mode did not change. + +go -C src test ./cmd/tinycolor-compat -count=1; go -C src test ./...; node tests/port/adapter.test.mjs +The single binary is useful to humans and remains byte-compatible with the existing differential adapter. + + + +Add cross-platform oracle verification and one-command local gates +tests/original/verify.mjs, tests/original/verify.test.mjs, Makefile, Dockerfile +Read `tests/original/manifest.sha256`, `Makefile`, `Dockerfile`, `.gitignore`, `compat/run.mjs`, and every filename in `compat/cases/` before editing. + +RED: add a Node built-in test that copies a two-line manifest and files into a temporary directory, proves `verifyManifest` accepts correct SHA-256 hashes, then changes one file and proves it reports that path. Run `node --test tests/original/verify.test.mjs` and confirm RED because the verifier does not exist. + +GREEN: implement `tests/original/verify.mjs` with `node:crypto`, `node:fs`, and `node:path`. Export `verifyManifest(manifestPath, root)` and make direct execution print `verified: 3` on success, print every missing/mismatched relative path on stderr, and exit 1 on failure. Expand Make targets to `.PHONY: build test verify fmt-check hashes fuzz bench clean`; `build` produces `bin/tinycolor`; `fmt-check` fails when `gofmt -l` prints any `.go` file under `src`; `hashes` runs the verifier; `test` runs Node adapter tests, `go -C src test ./...`, and all five corpora (smoke, parser-hex-rgb, parser, conversion, operations); `verify` depends on fmt-check, hashes, test and also runs `go -C src vet ./...`. Keep `clean` limited to the explicit ignored `bin/tinycolor` artifact. Update Docker to output `/tinycolor`, set it as ENTRYPOINT, and keep the scratch runtime. Run the full gate, then commit `build(05-01): add reproducible verification gate`. + + +- `node tests/original/verify.mjs` prints exactly `verified: 3` and exits 0. +- `make build` creates ignored `bin/tinycolor` and the binary runs `parse red` successfully. +- `make verify` runs every corpus including conversion and operations and exits 0. +- Dockerfile contains `ENTRYPOINT ["/tinycolor"]` and does not copy the JavaScript runtime into the final image. + +node --test tests/original/verify.test.mjs; node tests/original/verify.mjs; make build; ./bin/tinycolor parse red; make verify +A clean checkout has one build command, one complete local verification command, and a portable immutable-oracle check. + + + +Replace the upstream-only workflow with port delivery CI +.github/workflows/port.yml, .github/workflows/deno.yml +Read `.github/workflows/deno.yml`, `deno.json`, `src/go.mod`, `Makefile`, and `tests/original/manifest.sha256` completely. + +Delete `.github/workflows/deno.yml` after transferring its source-suite intent into `.github/workflows/port.yml`. Configure push and pull_request, `contents: read`, Ubuntu, checkout v4, setup-go v5 using the exact version from `src/go.mod`, setup-node v4 with Node 24, and setup-deno v2 with Deno v2.x. Run `make verify`, then `deno task test` against untouched source files, then `make build`, then upload `bin/tinycolor` with upload-artifact v4. The CI file must not run `deno task build`, npm publication, or mutate generated upstream distributions. Run a YAML text audit and local `make verify`, then commit `ci(05-01): verify port and source suite`. + + +- `.github/workflows/port.yml` contains `make verify`, `deno task test`, `make build`, and `actions/upload-artifact@v4`. +- `.github/workflows/deno.yml` no longer exists, leaving one CI source of truth. +- The workflow has `permissions: contents: read` and no publish permission or deployment step. +- `make verify` still exits 0 locally; local documentation does not claim the CI or Deno run passed before GitHub executes it. + +rg -n "make verify|deno task test|make build|upload-artifact@v4|contents: read" .github/workflows/port.yml; if (Test-Path .github/workflows/deno.yml) { exit 1 }; make verify +CI performs the same local gate, adds the environment-only untouched Deno suite, and publishes the working Go binary. + + +Run `make verify`, confirm the CLI examples return exit 0, confirm oracle hashes match, and inspect the workflow for build/test-only permissions. A local Deno pass is not required or claimed when Deno is unavailable. +- DEL-01 has a tested human CLI, one-command build, Docker artifact, and CI. +- QLT-03 has a cross-platform manifest verifier and one complete clean-checkout command. +After completion, create `.planning/phases/05-delivery-evidence/05-01-SUMMARY.md`. diff --git a/.planning/phases/05-delivery-evidence/05-02-PLAN.md b/.planning/phases/05-delivery-evidence/05-02-PLAN.md new file mode 100644 index 00000000..951ce851 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-02-PLAN.md @@ -0,0 +1,110 @@ +--- +phase: 5 +plan: 2 +type: execute +subsystem: fuzz-benchmark-evidence +owner: C-mrashis +tags: [fuzz, differential, benchmark, p99, rss] +wave: 2 +depends_on: [05-01] +files_modified: [fuzz/harness.mjs, fuzz/harness.test.mjs, fuzz/validate-log.mjs, fuzz/validate-log.test.mjs, fuzz/README.md, fuzz/log.txt, bench/workload.jsonl, bench/run.mjs, bench/run.test.mjs, bench/methodology.md, bench/results.json, Makefile] +autonomous: true +requirements: [DEL-01, QLT-03] +must_haves: + truths: + - "A checked-in harness runs identical public requests against JavaScript and Go for at least 60 continuous seconds." + - "The fuzz log records the actual seed, elapsed duration, case count, and every divergence; zero is claimed only when measured." + - "Original and port benchmarks use one workload and report startup p99, latency p99, throughput, and peak RSS or an explicit unsupported value." + artifacts: + - path: "fuzz/harness.mjs" + provides: "Seeded persistent-process differential fuzz runner" + - path: "fuzz/log.txt" + provides: "Published output of an actual 60-second session" + - path: "bench/results.json" + provides: "Machine-readable same-host benchmark evidence" + key_links: + - from: "fuzz/harness.mjs" + to: "compat/js-runner.mjs" + via: "persistent JSONL child process" + pattern: "compat/js-runner.mjs" + - from: "bench/run.mjs" + to: "bench/workload.jsonl" + via: "identical request stream" + pattern: "workload.jsonl" +--- + +Publish reproducible differential fuzz and honest original-versus-port performance evidence on shared public workloads. +@AGENT.md +@.planning/phases/05-delivery-evidence/05-CONTEXT.md +@.planning/phases/05-delivery-evidence/05-RESEARCH.md +@.planning/phases/05-delivery-evidence/05-VALIDATION.md +@.planning/phases/05-delivery-evidence/05-01-SUMMARY.md +@compat/js-runner.mjs +@compat/run.mjs +@src/cmd/tinycolor-compat/main.go +@compat/cases/operations.jsonl +@fuzz/README.md +@bench/methodology.md +@bench/results.json + + +Build a deterministic persistent-process differential fuzz harness +fuzz/harness.mjs, fuzz/harness.test.mjs, fuzz/README.md, Makefile +Read `compat/js-runner.mjs`, `compat/run.mjs`, `src/cmd/tinycolor-compat/main.go`, all five `compat/cases/*.jsonl` files, `fuzz/README.md`, and the Phase 5 research fuzz section. + +RED: create Node built-in tests for a seeded PRNG (seed 1 repeats the same first sequence), request generation (stable IDs and only supported operations), CLI option validation (`--duration` positive seconds and integer `--seed`), and a one-second end-to-end run that returns a summary with elapsedSeconds >= 1, cases > 0, and divergences 0. Run `node --test fuzz/harness.test.mjs` and confirm RED. + +GREEN: implement `fuzz/harness.mjs` with Node standard libraries only. Parse `--duration ` default 60 and `--seed ` default 20260801. Build `bin/tinycolor` once through `go -C src build -o ../bin/tinycolor ./cmd/tinycolor-compat`, start one `node compat/js-runner.mjs` and one `bin/tinycolor` child, and exchange newline-delimited requests sequentially. Generate seeded string/object inputs across parse/output, modify, mix, readability, isReadable, and palette operations using adapter-supported method names and bounded numeric edge values. Compare parsed response objects with `isDeepStrictEqual`; print each mismatch as one JSON line containing request, JavaScript, and Go. Always print final `duration_seconds`, `seed`, `cases`, and `divergences`; terminate children; exit 1 for divergence or protocol/process failure. Add `make fuzz` for the exact 60-second command and replace the placeholder README with reproduction and claim rules. Run tests and a one-second smoke, then commit `feat(05-02): add differential fuzz harness`. + + +- `node --test fuzz/harness.test.mjs` exits 0 and includes a one-second real differential run. +- Two runs with `--duration 1 --seed 1` generate the same request prefix and both report zero divergences. +- Harness source contains no `Math.random`, dependency import, shell-out to the JavaScript oracle from the Go port, or test-suite mutation. +- `make fuzz` invokes `node fuzz/harness.mjs --duration 60 --seed 20260801`. + +node --test fuzz/harness.test.mjs; node fuzz/harness.mjs --duration 1 --seed 1; rg -n "duration 60|seed 20260801" Makefile fuzz/README.md +A public seeded harness can sustain a real differential session without recompiling or spawning per case. + + + +Record the actual 60-second fuzz survivor evidence +fuzz/validate-log.mjs, fuzz/validate-log.test.mjs, fuzz/log.txt +Read `fuzz/harness.mjs`, `fuzz/README.md`, and any existing `fuzz/log.txt`. Confirm `make verify` is green before recording evidence. + +RED: add `fuzz/validate-log.test.mjs` using Node's built-in test runner and temporary files. Assert rejection of duration 59.999, wrong seed, zero cases, nonzero divergences, malformed numeric values, and every missing field; assert one log with duration 60, seed 20260801, positive cases, and zero divergences passes. Run the focused test and confirm RED because the validator does not exist. + +GREEN: add `fuzz/validate-log.mjs` as a small standard-library parser that exports its validation function, reads a log path when run directly, requires a numeric `duration_seconds >= 60`, exact integer `seed === 20260801`, integer `cases > 0`, and integer `divergences === 0`, prints `fuzz log verified`, and exits 1 with the failed field otherwise. Run `node fuzz/harness.mjs --duration 60 --seed 20260801` and capture its unedited stdout in `fuzz/log.txt`; do not hand-write, truncate, or normalize duration/case count. If divergences are nonzero, retain the failed log outside the final artifact, stop this plan, and open a separate regression/root-cause task owned by the implicated module. That recovery task must commit the deterministic corpus row and root-cause fix separately before this evidence task is rerun. Do not edit corpora, tests, or source within this log-only task. After focused negative/positive tests and the real log validator pass, commit validator, tests, and final log as `test(05-02): record 60-second fuzz session`. + + +- `node fuzz/validate-log.mjs fuzz/log.txt` exits 0 only when duration is at least 60 seconds, seed is 20260801, cases is positive, and divergences is zero. +- `node --test fuzz/validate-log.test.mjs` proves every invalid/missing field fails and a valid log passes. +- The log contains no claim that cannot be regenerated by the documented command. +- `make verify` exits 0 after the recorded run. + +node --test fuzz/validate-log.test.mjs; node fuzz/harness.mjs --duration 1 --seed 20260801; node fuzz/validate-log.mjs fuzz/log.txt; make verify +The repository contains genuine, reproducible Differential Fuzz Survivor evidence. + + + +Measure shared-workload p99, RSS, startup, and throughput +bench/workload.jsonl, bench/run.mjs, bench/run.test.mjs, bench/methodology.md, bench/results.json, Makefile +Read `compat/cases/smoke.jsonl`, `compat/cases/conversion.jsonl`, `compat/cases/operations.jsonl`, `compat/js-runner.mjs`, `src/cmd/tinycolor-compat/main.go`, `bench/methodology.md`, and `bench/results.json`. + +RED: add Node built-in tests for `percentile99([1,2,3,4]) === 4`, workload parsing, results-schema validation, and `node bench/run.mjs --quick --output ` producing nonnegative startup/latency/RSS and positive throughput for both `javascript` and `go` without modifying `bench/results.json`. Confirm RED. + +GREEN: create a fixed `bench/workload.jsonl` of representative parse, conversion, modifier, palette, and readability requests copied from existing public corpora. Implement `bench/run.mjs` with Node standard libraries and required `--output ` support; default output is `bench/results.json`, while tests and `--quick` verification always pass a temporary output path. Build Go once; hash the workload; collect at least 20 cold-start samples normally (3 with `--quick`) by starting each runner, sending one request, receiving one response, and stopping it; collect at least 1,000 persistent request samples normally (30 quick) for latency and throughput; sort nanosecond samples and select index `ceil(0.99*n)-1`; sample peak child RSS from `/proc//status` on Linux and `Get-Process -Id ` on Windows, emitting null plus a limitation string only when unsupported. Write results containing generatedAt, OS, architecture, CPU, Go/Node versions, workload SHA-256, sample counts, and for both implementations `startupP99Ms`, `latencyP99Ms`, `throughputOpsPerSecond`, and `peakRssBytes`. Write the exact same-host method, lifecycle, sample counts, percentile formula, RSS technique, and limitations in `bench/methodology.md`. Add `make bench` for a full default-output run. Run unit and quick measurement to temporary files, then run `node bench/run.mjs --output bench/results.json` exactly once; retain the actual full result and commit `perf(05-02): publish shared benchmark evidence`. + + +- `node --test bench/run.test.mjs` and `node bench/run.mjs --quick --output ` exit 0 without modifying `bench/results.json`. +- `bench/results.json` parses and contains all four required metrics for both implementations, or only `peakRssBytes:null` with a nonempty RSS limitation on an unsupported platform. +- `bench/results.json` records normal-mode sample counts of at least 20 cold starts and 1,000 persistent requests; quick mode can never write this committed path. +- `bench/methodology.md` explicitly states these are same-host observations and not universal speedup claims. + +node --test bench/run.test.mjs; $tmp = Join-Path ([System.IO.Path]::GetTempPath()) 'tinycolor-bench-quick.json'; node bench/run.mjs --quick --output $tmp; node -e "const r=require('./bench/results.json'); if(r.samples.coldStarts<20||r.samples.requests<1000)process.exit(1); for (const k of ['javascript','go']) for (const m of ['startupP99Ms','latencyP99Ms','throughputOpsPerSecond','peakRssBytes']) if (!(m in r.implementations[k])) process.exit(1)"; git diff --check +The repository contains reproducible, machine-readable original-versus-port measurements for p99 latency, RSS, startup, and throughput. + + +Run `make verify`, the one-second fuzz smoke, validate the committed 60-second log, run benchmark tests, and parse results JSON. Never replace a failed or unsupported measurement with an estimated value. +- DEL-01 includes an actual differential fuzz session and shared-workload benchmark report. +- QLT-03 can reproduce each evidence artifact from checked-in code and commands. +After completion, create `.planning/phases/05-delivery-evidence/05-02-SUMMARY.md`. diff --git a/.planning/phases/05-delivery-evidence/05-03-PLAN.md b/.planning/phases/05-delivery-evidence/05-03-PLAN.md new file mode 100644 index 00000000..359a62a8 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-03-PLAN.md @@ -0,0 +1,149 @@ +--- +phase: 5 +plan: 3 +type: execute +subsystem: submission-documentation-closeout +owner: D-deepali +tags: [readme, decisions, compatibility, attribution, demo, closeout] +wave: 3 +depends_on: [05-02] +files_modified: [README.md, PORT.md, DECISIONS.md, COMPATIBILITY.md, docs/ARCHITECTURE.md, docs/TESTING.md, docs/DEMO.md, docs/TEAM-OWNERSHIP.md, .planning/STATE.md, .planning/ROADMAP.md, .planning/phases/05-delivery-evidence/05-03-SUMMARY.md, .planning/phases/05-delivery-evidence/05-VERIFICATION.md] +autonomous: false +requirements: [DEL-01, QLT-03] +must_haves: + truths: + - "A judge can find build, CLI, verification, fuzz, benchmark, limitations, attribution, and demo instructions from the root README." + - "Every number and bonus claim points to checked-in evidence and every unavailable external action remains explicitly unclaimed." + - "At least ten architectural divergences have concrete rationales and the milestone closes only after the full gate passes." + artifacts: + - path: "README.md" + provides: "Judge-facing migration, build, use, evidence, and limitation guide" + - path: "DECISIONS.md" + provides: "At least ten non-trivial accepted architectural decisions" + - path: "docs/DEMO.md" + provides: "Reproducible five-minute live demo script" + - path: ".planning/phases/05-delivery-evidence/05-VERIFICATION.md" + provides: "Goal-backward Phase 5 completion evidence" + key_links: + - from: "README.md" + to: "fuzz/log.txt" + via: "evidence link and reproduction command" + pattern: "fuzz/log.txt" + - from: "README.md" + to: "bench/results.json" + via: "benchmark evidence link" + pattern: "bench/results.json" +--- + +Turn measured Phase 5 outputs into an honest, judge-readable submission and close the roadmap with reproducible evidence. +@AGENT.md +@.planning/phases/05-delivery-evidence/05-CONTEXT.md +@.planning/phases/05-delivery-evidence/05-RESEARCH.md +@.planning/phases/05-delivery-evidence/05-VALIDATION.md +@.planning/phases/05-delivery-evidence/05-01-SUMMARY.md +@.planning/phases/05-delivery-evidence/05-02-SUMMARY.md +@README.md +@PORT.md +@DECISIONS.md +@COMPATIBILITY.md +@LICENSE +@.port-mortem.toml +@docs/ARCHITECTURE.md +@docs/TESTING.md +@docs/TEAM-OWNERSHIP.md +@fuzz/log.txt +@bench/methodology.md +@bench/results.json + + +Write the judge-facing port documentation and decision log +README.md, PORT.md, DECISIONS.md, docs/ARCHITECTURE.md, docs/TESTING.md, docs/DEMO.md, docs/TEAM-OWNERSHIP.md +Read all listed files completely plus `LICENSE`, `.port-mortem.toml`, `Makefile`, CLI help output, `fuzz/README.md`, `fuzz/log.txt`, `bench/methodology.md`, and `bench/results.json`. Extract actual command output and numbers before writing claims. + +Replace the upstream-oriented root README with a concise Track H port guide containing: migration rationale; exact pinned source URL/commit and MIT attribution; required tree; `make build` and Docker build/run; CLI syntax and examples for all five commands including `--json`; Go package example using the existing exported API; `make verify`; untouched-oracle manifest; links to fixed corpus, 60-second fuzz log, benchmark results/methodology, compatibility matrix, decisions, ownership, and demo script; known limitations; and a submission checklist. Preserve a link to the original upstream README/history rather than copying its entire usage manual. Reduce `PORT.md` to a compatibility redirect or remove it if README now contains every unique fact. + +Expand `DECISIONS.md` from seven to at least twelve substantive rows by adding: dual human/JSONL executable, cross-platform SHA-256 manifest verifier, seeded standard-library fuzzing, persistent children for evidence runners, shared benchmark workload/lifecycle, and honest external-evidence policy. Each row must name a real divergence and rationale; no empty bonus-padding bullets. Update architecture/testing documents to match actual commands only. Add `docs/DEMO.md` with a timestamped five-minute sequence: clean status/hash check, build, CLI parse/convert/lighten/palette/contrast, fixed differential corpus, fuzz log/reproduction, benchmark results, decisions/limitations, and closing artifact path. Keep public GitHub visibility and actual video URL as unchecked manual items. Update ownership only to record Phase 5 integration handoffs; do not reassign completed Phase 1-4 source ownership. Commit `docs(05-03): write submission guide and decisions`. + + +- README contains `make build`, `make verify`, all five CLI command names, `fuzz/log.txt`, `bench/results.json`, `DECISIONS.md`, upstream URL, kickoff commit, and MIT attribution. +- `DECISIONS.md` has at least twelve non-header table rows, each with nonempty decision and why columns. +- `docs/DEMO.md` has a five-minute sequence and explicitly marks video recording/upload and public-visibility checks as manual/unverified until completed. +- Documentation contains no `TBD`, `TODO`, `not_run`, obsolete `bin/tinycolor-compat`, or unsupported whole-suite/CI/video claim. + +rg -n "make build|make verify|parse|convert|lighten|palette|contrast|fuzz/log.txt|bench/results.json|bgrins/TinyColor|MIT" README.md; node -e "const fs=require('fs'); const rows=fs.readFileSync('DECISIONS.md','utf8').split(/\r?\n/).filter(x=>/^\| D-/.test(x)); if(rows.length<12) process.exit(1)"; if (rg -n "TBD|TODO|not_run|bin/tinycolor-compat" README.md DECISIONS.md COMPATIBILITY.md docs) { exit 1 }; git diff --check +The submission explains what was ported, how to reproduce it, what differs, and which external deliverables still require human action. + + + +Refresh honest parity, coverage, safety, benchmark, and bonus evidence +COMPATIBILITY.md +Read `COMPATIBILITY.md`, every fixed corpus, `fuzz/log.txt`, `bench/results.json`, `tests/original/manifest.sha256`, and the actual output from the commands below. Do not carry forward stale Phase 4 counts without rerunning them. + +Run the complete gate and capture exact case/pass/mismatch counts per corpus. Run `go -C src test -cover ./...` and record package coverage separately; do not invent a JavaScript coverage comparison when Deno coverage is unavailable. Count Go escape hatches with `rg -n '\bunsafe\b' src -g '*.go'` and report the exact code occurrence count, excluding documentation. Verify the three oracle hashes. Read, do not recompute by hand, the fuzz duration/seed/cases/divergences and benchmark metrics. Update `COMPATIBILITY.md` with a current evidence table and dedicated sections for fixed-corpus pass rate per file, original-suite/Deno status, Go coverage by package, unsafe count, fuzz session, benchmark summary/link, known differences, and bonus eligibility. Claim Differential Fuzz Survivor only for a >=60 second zero-divergence log; claim Zero Unsafe only for zero Go source occurrences; claim Decision Log only when at least ten substantive decisions exist. State CI as configured until a GitHub run URL is verified, public repository status as unverified locally, and demo video as not yet supplied. Commit `docs(05-03): publish final compatibility evidence`. + + +- Every `compat/cases/*.jsonl` file has an explicit `passed/total` and mismatch count in `COMPATIBILITY.md` matching fresh output. +- Go coverage is labeled by package/tool command and is not described as JavaScript coverage parity. +- Unsafe count is numeric and includes the exact `rg` reproduction command. +- Fuzz/bonus claims exactly match `fuzz/log.txt`; benchmark numbers link to `bench/results.json` and retain same-host limitation. +- Deno, CI execution, repository visibility, and demo video are each marked passed only with observed evidence, otherwise configured/unverified/not supplied. + +make verify; go -C src test -cover ./...; node tests/original/verify.mjs; node -e "const r=require('./bench/results.json'); if(!r.implementations?.javascript||!r.implementations?.go) process.exit(1)"; rg -n "smoke.jsonl|parser-hex-rgb.jsonl|parser.jsonl|conversion.jsonl|operations.jsonl|unsafe|coverage|fuzz/log.txt|bench/results.json|Deno|video" COMPATIBILITY.md; git diff --check +Every score-relevant statement is traceable to a checked-in command or explicitly labeled external/unverified. + + + +Run the final clean local gate and write verification evidence +.planning/phases/05-delivery-evidence/05-03-SUMMARY.md, .planning/phases/05-delivery-evidence/05-VERIFICATION.md +Read all Phase 5 plans/summaries, `05-VALIDATION.md`, `.planning/STATE.md`, `.planning/ROADMAP.md`, `README.md`, `COMPATIBILITY.md`, and actual final command output. Inspect `git status --short` before editing planning state. + +Run `make build`, every CLI command once in JSON mode, `make verify`, `node fuzz/validate-log.mjs fuzz/log.txt`, the one-second fuzz smoke, benchmark tests/schema validation using a temporary quick-output path, oracle hash verifier, and `git diff --check`. Confirm `git diff -- mod.js test.js tinycolor.js` is empty and compare current SHA-256 values to the manifest. Create `05-03-SUMMARY.md` listing task commits, commands, exact evidence, deviations, and remaining external actions. Create `05-VERIFICATION.md` with goal-backward evidence for each Phase 5 success criterion and DEL-01/QLT-03; distinguish local pass, configured CI, and external/unverified items. Commit these local evidence documents as `docs(05-03): verify local delivery evidence`. Do not change ROADMAP or STATE yet. + + +- `make build`, `make verify`, focused fuzz/benchmark checks, oracle verification, and `git diff --check` all exit 0 in the closing session. +- `git diff -- mod.js test.js tinycolor.js` is empty and hashes match the pinned manifest. +- `05-VERIFICATION.md` maps all three roadmap success criteria and DEL-01/QLT-03 to concrete paths/commands. +- `05-VERIFICATION.md` leaves public clone access and CI execution pending until observed evidence is supplied. +- Worktree is clean immediately after this evidence commit except for ignored build/cache artifacts. + +make build; ./bin/tinycolor parse --json red; ./bin/tinycolor convert --to hsl --json red; ./bin/tinycolor lighten --amount 10 --json '#000'; ./bin/tinycolor palette --type triad --json red; ./bin/tinycolor contrast --json '#000' '#fff'; make verify; node fuzz/validate-log.mjs fuzz/log.txt; node fuzz/harness.mjs --duration 1 --seed 20260801; node --test bench/run.test.mjs; node tests/original/verify.mjs; git diff --check; git diff --exit-code -- mod.js test.js tinycolor.js +All local delivery evidence is green and the remaining external proof is explicit. + + + +Verify public clone access and a successful CI run +none +Read the repository `origin` URL, `.github/workflows/port.yml`, the current commit hash, and the pending external-evidence section of `05-VERIFICATION.md`. + +Resolve the public repository URL from `git remote get-url origin`. From an unauthenticated context, verify the repository can be read and cloned. Verify a GitHub Actions run for the exact Phase 5 commit completed successfully and capture its URL. If publishing the current branch or changing repository visibility is required, stop and request the user's explicit external-state authorization. Also report whether the five-minute demo video URL has been supplied; absence of a video does not permit a false claim. Do not mark the phase complete without public clone evidence and a successful CI run URL. + + +- An unauthenticated public repository URL and exact successful CI run URL are available for the current Phase 5 commit. +- Any required push or visibility change was explicitly authorized by the user. +- Video status is reported honestly as verified URL or not supplied. + +git remote get-url origin; git rev-parse HEAD +Public clone access and CI execution are observed rather than inferred from configuration. + + + +Close Phase 5 planning state after external proof +.planning/STATE.md, .planning/ROADMAP.md, .planning/phases/05-delivery-evidence/05-VERIFICATION.md +Read the successful checkpoint evidence, all Phase 5 summaries, `.planning/STATE.md`, `.planning/ROADMAP.md`, and `05-VERIFICATION.md`. + +Add the public repository and successful CI run URLs to `05-VERIFICATION.md`, keeping video status honest. Mark Phase 5 and its three plans complete in ROADMAP, set completion date 2026-08-01, and update progress to 3/3. Update STATE with current CLI, corpus, fuzz, benchmark, public-clone, and CI evidence; remove the stale 58/58 Phase 4 operations count in favor of the fresh 69/69 result and retain the Deno/video status supported by actual evidence. Run `git diff --check`, then commit `docs(05): close delivery evidence phase`. + + +- ROADMAP shows Phase 5 `3/3`, Complete, `2026-08-01` only after external proof exists. +- STATE includes the public repository and successful CI evidence and contains no stale 58/58 operations count. +- `05-VERIFICATION.md` includes the exact public repository and CI run URLs and does not invent a video URL. + +rg -n "3/3|Complete|2026-08-01" .planning/ROADMAP.md; if (rg -n "58/58" .planning/STATE.md) { exit 1 }; git diff --check +Phase 5 closes only after both local and external delivery proof are real. + + +No roadmap completion is allowed before all automated commands pass and public clone plus CI execution are observed. Video publication remains explicitly unclaimed unless a verified URL is supplied. +- DEL-01 is judge-readable, measurable, and demonstrable from a clean checkout. +- QLT-03 maps every claim to a reproducible command while immutable source hashes remain unchanged. +- Phase 5 and the milestone close with no hidden known difference or unsupported score claim. +Create `05-03-SUMMARY.md` and `05-VERIFICATION.md`, then mark the phase complete only when their evidence is green. diff --git a/.planning/phases/05-delivery-evidence/05-VALIDATION.md b/.planning/phases/05-delivery-evidence/05-VALIDATION.md index 13bc34db..ab84d897 100644 --- a/.planning/phases/05-delivery-evidence/05-VALIDATION.md +++ b/.planning/phases/05-delivery-evidence/05-VALIDATION.md @@ -35,9 +35,12 @@ created: 2026-08-01 | 05-01-01 | 01 | 1 | DEL-01 | CLI unit | `go -C src test ./cmd/tinycolor-compat` | ✅ | ⬜ pending | | 05-01-02 | 01 | 1 | QLT-03 | integration | `make build && make verify` | ✅ make / ❌ expanded targets | ⬜ pending | | 05-02-01 | 02 | 2 | DEL-01 | differential fuzz | `node fuzz/harness.mjs --duration 1 --seed 1` | ❌ Wave 2 | ⬜ pending | -| 05-02-02 | 02 | 2 | DEL-01 | benchmark smoke | `node bench/run.mjs --quick` | ❌ Wave 2 | ⬜ pending | +| 05-02-02 | 02 | 2 | DEL-01 | fuzz evidence | `node --test fuzz/validate-log.test.mjs && node fuzz/validate-log.mjs fuzz/log.txt` | ❌ Wave 2 | ⬜ pending | +| 05-02-03 | 02 | 2 | DEL-01 | benchmark smoke | `node bench/run.mjs --quick --output ` | ❌ Wave 2 | ⬜ pending | | 05-03-01 | 03 | 3 | DEL-01 | evidence audit | `node tests/original/verify.mjs` | ❌ Wave 1 | ⬜ pending | | 05-03-02 | 03 | 3 | QLT-03 | full gate | `make verify` | ✅ make / ❌ expanded target | ⬜ pending | +| 05-03-04 | 03 | 3 | DEL-01 | external checkpoint | public clone + exact CI run URL | manual | ⬜ pending | +| 05-03-05 | 03 | 3 | QLT-03 | closeout audit | `git diff --check` | ✅ | ⬜ pending | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* From df1f9b859c1ccbd2d3c64e8d104c67dce0923dcf Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:26:58 +0530 Subject: [PATCH 046/165] feat(05-01): add human CLI commands --- src/cmd/tinycolor-compat/main.go | 197 +++++++++++++++++++++++++- src/cmd/tinycolor-compat/main_test.go | 145 +++++++++++++++++++ 2 files changed, 336 insertions(+), 6 deletions(-) create mode 100644 src/cmd/tinycolor-compat/main_test.go diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index aaebeb57..014fb704 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -2,7 +2,10 @@ package main import ( "bufio" + "encoding/json" + "flag" "fmt" + "io" "math" "os" "strconv" @@ -13,7 +16,32 @@ import ( ) func main() { - scanner := bufio.NewScanner(os.Stdin) + os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) +} + +func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + if len(args) == 0 { + return runJSONL(stdin, stdout, stderr) + } + switch args[0] { + case "parse": + return runParse(args[1:], stdout, stderr) + case "convert": + return runConvert(args[1:], stdout, stderr) + case "lighten": + return runLighten(args[1:], stdout, stderr) + case "palette": + return runPalette(args[1:], stdout, stderr) + case "contrast": + return runContrast(args[1:], stdout, stderr) + default: + usage(stderr, "") + return 2 + } +} + +func runJSONL(stdin io.Reader, stdout, stderr io.Writer) int { + scanner := bufio.NewScanner(stdin) for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 { @@ -22,11 +50,168 @@ func main() { request, err := compat.Decode(line) if err != nil { response, _ := compat.Failure("", err.Error()) - write(response) + write(response, stdout, stderr) continue } - write(handle(request)) + write(handle(request), stdout, stderr) + } + return 0 +} + +func runParse(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("parse", flag.ContinueOnError) + flags.SetOutput(stderr) + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 1 { + usage(stderr, "parse [--json] ") + return 2 + } + color, _ := tinycolor.FromCompat(flags.Arg(0), false) + if *jsonOutput { + inspection := color.Inspect() + inspection["hex"] = color.ToHex() + return writeJSON(stdout, inspection) + } + fmt.Fprintln(stdout, color.String()) + return 0 +} + +func runConvert(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("convert", flag.ContinueOnError) + flags.SetOutput(stderr) + to := flags.String("to", "", "hex, hex8, rgb, percentage-rgb, hsl, hsv, or name") + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 1 || *to == "" { + usage(stderr, "convert --to hex|hex8|rgb|percentage-rgb|hsl|hsv|name [--json] ") + return 2 + } + color, _ := tinycolor.FromCompat(flags.Arg(0), false) + var result string + switch *to { + case "hex": + result = color.ToHexString() + case "hex8": + result = color.ToHex8String() + case "rgb": + result = color.ToRGBString() + case "percentage-rgb": + result = color.ToPercentageRGBString() + case "hsl": + result = color.ToHSLString() + case "hsv": + result = color.ToHSVString() + case "name": + result = color.ToString("name") + default: + usage(stderr, "convert --to hex|hex8|rgb|percentage-rgb|hsl|hsv|name [--json] ") + return 2 + } + if *jsonOutput { + return writeJSON(stdout, result) + } + fmt.Fprintln(stdout, result) + return 0 +} + +func runLighten(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("lighten", flag.ContinueOnError) + flags.SetOutput(stderr) + amount := flags.Float64("amount", 10, "lightness percentage") + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 1 { + usage(stderr, "lighten [--amount 10] [--json] ") + return 2 + } + color, _ := tinycolor.FromCompat(flags.Arg(0), false) + color.Lighten(*amount) + result := color.ToHexString() + if *jsonOutput { + return writeJSON(stdout, result) + } + fmt.Fprintln(stdout, result) + return 0 +} + +func runPalette(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("palette", flag.ContinueOnError) + flags.SetOutput(stderr) + kind := flags.String("type", "", "palette type") + results := flags.Int("results", 6, "number of colors") + slices := flags.Int("slices", 30, "number of hue slices") + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 1 || *kind == "" { + usage(stderr, "palette --type complement|splitcomplement|triad|tetrad|analogous|monochromatic [--results 6] [--slices 30] [--json] ") + return 2 + } + color, _ := tinycolor.FromCompat(flags.Arg(0), false) + var colors []tinycolor.Color + switch *kind { + case "complement": + colors = []tinycolor.Color{color.Complement()} + case "splitcomplement": + colors = color.SplitComplement() + case "triad": + colors = color.Triad() + case "tetrad": + colors = color.Tetrad() + case "analogous": + colors = color.Analogous(*results, *slices) + case "monochromatic": + colors = color.Monochromatic(*results) + default: + usage(stderr, "palette --type complement|splitcomplement|triad|tetrad|analogous|monochromatic [--results 6] [--slices 30] [--json] ") + return 2 + } + if *jsonOutput { + inspections := make([]map[string]any, len(colors)) + for index, paletteColor := range colors { + inspections[index] = paletteColor.Inspect() + } + return writeJSON(stdout, inspections) + } + for _, paletteColor := range colors { + fmt.Fprintln(stdout, paletteColor.ToHexString()) + } + return 0 +} + +func runContrast(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("contrast", flag.ContinueOnError) + flags.SetOutput(stderr) + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 2 { + usage(stderr, "contrast [--json] ") + return 2 + } + first, _ := tinycolor.FromCompat(flags.Arg(0), false) + second, _ := tinycolor.FromCompat(flags.Arg(1), false) + ratio := tinycolor.Readability(first, second) + if *jsonOutput { + return writeJSON(stdout, map[string]any{ + "ratio": ratio, + "aaSmall": tinycolor.IsReadable(first, second, tinycolor.WCAG2Options{Level: "AA", Size: "small"}), + "aaLarge": tinycolor.IsReadable(first, second, tinycolor.WCAG2Options{Level: "AA", Size: "large"}), + "aaaSmall": tinycolor.IsReadable(first, second, tinycolor.WCAG2Options{Level: "AAA", Size: "small"}), + "aaaLarge": tinycolor.IsReadable(first, second, tinycolor.WCAG2Options{Level: "AAA", Size: "large"}), + }) + } + fmt.Fprintln(stdout, ratio) + return 0 +} + +func usage(stderr io.Writer, command string) { + if command == "" { + fmt.Fprintln(stderr, "Usage: tinycolor-compat ") + return + } + fmt.Fprintln(stderr, "Usage: tinycolor-compat "+command) +} + +func writeJSON(stdout io.Writer, value any) int { + if err := json.NewEncoder(stdout).Encode(value); err != nil { + return 1 } + return 0 } func handle(request compat.Request) compat.Response { @@ -353,11 +538,11 @@ func truthy(value any) bool { } } -func write(response compat.Response) { +func write(response compat.Response, stdout, stderr io.Writer) { encoded, err := compat.Encode(response) if err != nil { - fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(stderr, err) return } - fmt.Println(string(encoded)) + fmt.Fprintln(stdout, string(encoded)) } diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go new file mode 100644 index 00000000..511296a3 --- /dev/null +++ b/src/cmd/tinycolor-compat/main_test.go @@ -0,0 +1,145 @@ +package main + +import ( + "bytes" + "encoding/json" + "strings" + "testing" +) + +func TestRunHumanCommands(t *testing.T) { + tests := []struct { + name string + args []string + want string + verify func(*testing.T, string) + }{ + { + name: "parse JSON inspection", + args: []string{"parse", "--json", "red"}, + verify: func(t *testing.T, output string) { + t.Helper() + var got map[string]any + if err := json.Unmarshal([]byte(output), &got); err != nil { + t.Fatal(err) + } + if got["hex"] != "ff0000" || got["valid"] != true { + t.Fatalf("parse JSON = %#v", got) + } + }, + }, + { + name: "parse invalid JSON inspection", + args: []string{"parse", "--json", "not-a-color"}, + verify: func(t *testing.T, output string) { + t.Helper() + var got map[string]any + if err := json.Unmarshal([]byte(output), &got); err != nil { + t.Fatal(err) + } + if got["valid"] != false { + t.Fatalf("parse JSON = %#v", got) + } + }, + }, + {name: "convert HSL", args: []string{"convert", "--to", "hsl", "red"}, want: "hsl(0, 100%, 50%)\n"}, + {name: "lighten", args: []string{"lighten", "--amount", "10", "#000"}, want: "#1a1a1a\n"}, + { + name: "triad palette", + args: []string{"palette", "--type", "triad", "red"}, + want: "#ff0000\n#00ff00\n#0000ff\n", + }, + { + name: "triad palette JSON inspections", + args: []string{"palette", "--type", "triad", "--json", "red"}, + verify: func(t *testing.T, output string) { + t.Helper() + var got []map[string]any + if err := json.Unmarshal([]byte(output), &got); err != nil { + t.Fatal(err) + } + if len(got) != 3 || got[0]["valid"] != true || got[0]["value"] != "red" { + t.Fatalf("palette JSON = %#v", got) + } + }, + }, + { + name: "contrast JSON", + args: []string{"contrast", "--json", "#000", "#fff"}, + verify: func(t *testing.T, output string) { + t.Helper() + var got map[string]any + if err := json.Unmarshal([]byte(output), &got); err != nil { + t.Fatal(err) + } + want := map[string]any{"ratio": float64(21), "aaSmall": true, "aaLarge": true, "aaaSmall": true, "aaaLarge": true} + for key, value := range want { + if got[key] != value { + t.Fatalf("contrast %s = %#v, want %#v", key, got[key], value) + } + } + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + if status := run(test.args, strings.NewReader(""), &stdout, &stderr); status != 0 { + t.Fatalf("status = %d, stderr = %q", status, stderr.String()) + } + if test.verify != nil { + test.verify(t, stdout.String()) + return + } + if stdout.String() != test.want { + t.Fatalf("stdout = %q, want %q", stdout.String(), test.want) + } + }) + } +} + +func TestRunJSONLAndUsageErrors(t *testing.T) { + tests := []struct { + name string + args []string + stdin string + wantStatus int + wantStdout string + verify func(*testing.T, string) + }{ + { + name: "JSONL inspect without arguments", + stdin: "{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", + wantStdout: "{\"id\":\"red\",\"result\":{\"alpha\":1,\"format\":\"name\",\"original\":\"red\",\"rgb\":{\"a\":1,\"b\":0,\"g\":0,\"r\":255},\"valid\":true,\"value\":\"red\"}}\n", + }, + { + name: "malformed JSONL request does not stop the stream", + stdin: "{bad json\n{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", + verify: func(t *testing.T, output string) { + t.Helper() + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) != 2 || !strings.Contains(lines[0], "malformed JSON") || !strings.Contains(lines[1], "\"id\":\"red\"") { + t.Fatalf("stdout = %q", output) + } + }, + }, + {name: "unknown command", args: []string{"unknown"}, wantStatus: 2}, + {name: "missing convert target", args: []string{"convert", "red"}, wantStatus: 2}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + if status := run(test.args, strings.NewReader(test.stdin), &stdout, &stderr); status != test.wantStatus { + t.Fatalf("status = %d, want %d", status, test.wantStatus) + } + if test.verify != nil { + test.verify(t, stdout.String()) + } else if stdout.String() != test.wantStdout { + t.Fatalf("stdout = %q, want %q", stdout.String(), test.wantStdout) + } + if test.wantStatus == 2 && !strings.Contains(stderr.String(), "Usage:") { + t.Fatalf("stderr = %q, want usage", stderr.String()) + } + }) + } +} From 02f9cede1ac3d15dc246cc8402330460886776c9 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:34:25 +0530 Subject: [PATCH 047/165] build(05-01): add reproducible verification gate --- Dockerfile | 6 +++--- Makefile | 32 +++++++++++++++++++++++++++----- tests/original/verify.mjs | 28 ++++++++++++++++++++++++++++ tests/original/verify.test.mjs | 26 ++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 tests/original/verify.mjs create mode 100644 tests/original/verify.test.mjs diff --git a/Dockerfile b/Dockerfile index 7831a508..9f44b078 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ WORKDIR /app/src COPY src/go.mod ./ RUN go mod download COPY src/ ./ -RUN go build -o /tinycolor-compat ./cmd/tinycolor-compat +RUN go build -o /tinycolor ./cmd/tinycolor-compat FROM scratch -COPY --from=build /tinycolor-compat /tinycolor-compat -ENTRYPOINT ["/tinycolor-compat"] +COPY --from=build /tinycolor /tinycolor +ENTRYPOINT ["/tinycolor"] diff --git a/Makefile b/Makefile index f8a40eaf..98761099 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,34 @@ -.PHONY: build test +.PHONY: build test verify fmt-check hashes fuzz bench clean + +GO_CACHE ?= $(CURDIR)/.cache/go-build +GO = GOCACHE=$(GO_CACHE) go build: - go -C src build -o ../bin/tinycolor-compat ./cmd/tinycolor-compat + $(GO) -C src build -o ../bin/tinycolor ./cmd/tinycolor-compat test: node tests/port/adapter.test.mjs - go -C src test ./... - go -C src vet ./... + $(GO) -C src test ./... + node compat/run.mjs compat/cases/smoke.jsonl node compat/run.mjs compat/cases/parser-hex-rgb.jsonl node compat/run.mjs compat/cases/parser.jsonl - node compat/run.mjs compat/cases/smoke.jsonl + node compat/run.mjs compat/cases/conversion.jsonl + node compat/run.mjs compat/cases/operations.jsonl + +verify: fmt-check hashes test + $(GO) -C src vet ./... + +fmt-check: + node -e "const { readdirSync } = require('node:fs'); const { join } = require('node:path'); const { execFileSync } = require('node:child_process'); const files = []; const walk = (dir) => readdirSync(dir, { withFileTypes: true }).forEach((entry) => entry.isDirectory() ? walk(join(dir, entry.name)) : entry.name.endsWith('.go') && files.push(join(dir, entry.name))); walk('src'); const output = execFileSync('gofmt', ['-l', ...files], { encoding: 'utf8' }).trim(); if (output) { console.error(output); process.exit(1); }" + +hashes: + node tests/original/verify.mjs + +fuzz: + node fuzz/harness.mjs --duration 60 --seed 20260801 + +bench: + node bench/run.mjs + +clean: + node -e "const fs = require('node:fs'); for (const file of ['bin/tinycolor', 'bin/tinycolor.exe']) fs.rmSync(file, { force: true });" diff --git a/tests/original/verify.mjs b/tests/original/verify.mjs new file mode 100644 index 00000000..8d92d9aa --- /dev/null +++ b/tests/original/verify.mjs @@ -0,0 +1,28 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const digest = (file) => createHash("sha256").update(readFileSync(file)).digest("hex"); + +export function verifyManifest(manifestPath, root) { + return readFileSync(manifestPath, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => { + const [, expected, file] = line.match(/^(\S+)\s{2}(.+)$/) ?? []; + const path = resolve(root, file); + try { + return digest(path) === expected ? [] : [`mismatch: ${file}`]; + } catch { + return [`missing: ${file}`]; + } + }); +} + +if (import.meta.main) { + const manifest = resolve("tests/original/manifest.sha256"); + const failures = verifyManifest(manifest, process.cwd()); + if (failures.length) { + console.error(failures.join("\n")); + process.exitCode = 1; + } else { + console.log(`verified: ${readFileSync(manifest, "utf8").split(/\r?\n/).filter(Boolean).length}`); + } +} diff --git a/tests/original/verify.test.mjs b/tests/original/verify.test.mjs new file mode 100644 index 00000000..7371f3b7 --- /dev/null +++ b/tests/original/verify.test.mjs @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { verifyManifest } from "./verify.mjs"; + +const sha256 = (value) => createHash("sha256").update(value).digest("hex"); + +test("verifyManifest accepts matching files and reports a changed path", () => { + const root = mkdtempSync(join(tmpdir(), "tinycolor-verify-")); + try { + writeFileSync(join(root, "first.txt"), "first"); + writeFileSync(join(root, "second.txt"), "second"); + const manifest = join(root, "manifest.sha256"); + writeFileSync(manifest, `${sha256("first")} first.txt\n${sha256("second")} second.txt\n`); + + assert.deepEqual(verifyManifest(manifest, root), []); + + writeFileSync(join(root, "second.txt"), "changed"); + assert.deepEqual(verifyManifest(manifest, root), ["mismatch: second.txt"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); From a4ad85ea529e2250c4c10e5e53e520aba4bb3e7c Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:39:07 +0530 Subject: [PATCH 048/165] fix(05-01): make verification gate portable --- Makefile | 13 +++++---- src/tinycolor/color_test.go | 49 ++++++++++++++++++++++++---------- tests/original/verify.mjs | 4 ++- tests/original/verify.test.mjs | 12 +++++++++ 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 98761099..6fa4ef44 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,17 @@ .PHONY: build test verify fmt-check hashes fuzz bench clean -GO_CACHE ?= $(CURDIR)/.cache/go-build -GO = GOCACHE=$(GO_CACHE) go +GOCACHE ?= $(CURDIR)/.cache/go-build +export GOCACHE +GOEXE := $(shell go env GOEXE) +BINARY := bin/tinycolor$(GOEXE) build: - $(GO) -C src build -o ../bin/tinycolor ./cmd/tinycolor-compat + node -e "require('node:fs').mkdirSync('bin', { recursive: true })" + go -C src build -o ../$(BINARY) ./cmd/tinycolor-compat test: node tests/port/adapter.test.mjs - $(GO) -C src test ./... + go -C src test ./... node compat/run.mjs compat/cases/smoke.jsonl node compat/run.mjs compat/cases/parser-hex-rgb.jsonl node compat/run.mjs compat/cases/parser.jsonl @@ -16,7 +19,7 @@ test: node compat/run.mjs compat/cases/operations.jsonl verify: fmt-check hashes test - $(GO) -C src vet ./... + go -C src vet ./... fmt-check: node -e "const { readdirSync } = require('node:fs'); const { join } = require('node:path'); const { execFileSync } = require('node:child_process'); const files = []; const walk = (dir) => readdirSync(dir, { withFileTypes: true }).forEach((entry) => entry.isDirectory() ? walk(join(dir, entry.name)) : entry.name.endsWith('.go') && files.push(join(dir, entry.name))); walk('src'); const output = execFileSync('gofmt', ['-l', ...files], { encoding: 'utf8' }).trim(); if (output) { console.error(output); process.exit(1); }" diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 8ce49118..311ec7db 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -3,28 +3,49 @@ package tinycolor import "testing" func TestOutputsAndAnalysis(t *testing.T) { - c,_:=FromCompat("rgba(255, 0, 0, .5)",false) - if c.ToHex8String()!="#ff000080" || c.ToRGBString()!="rgba(255, 0, 0, 0.5)" { t.Fatal(c.ToHex8String(),c.ToRGBString()) } - if c.ToFilter(nil,false)!="progid:DXImageTransform.Microsoft.gradient(startColorstr=#80ff0000,endColorstr=#80ff0000)" {t.Fatal(c.ToFilter(nil,false))} - black,_:=FromCompat("#000",false); white,_:=FromCompat("#fff",false) - if black.Brightness()!=0||white.Luminance()!=1||!black.IsDark()||!white.IsLight(){t.Fatal("analysis")} - if !Equals("#ff000066","rgba(255, 0, 0, .4)"){t.Fatal("equals")} - if !Random().Valid(){t.Fatal("random")} + c, _ := FromCompat("rgba(255, 0, 0, .5)", false) + if c.ToHex8String() != "#ff000080" || c.ToRGBString() != "rgba(255, 0, 0, 0.5)" { + t.Fatal(c.ToHex8String(), c.ToRGBString()) + } + if c.ToFilter(nil, false) != "progid:DXImageTransform.Microsoft.gradient(startColorstr=#80ff0000,endColorstr=#80ff0000)" { + t.Fatal(c.ToFilter(nil, false)) + } + black, _ := FromCompat("#000", false) + white, _ := FromCompat("#fff", false) + if black.Brightness() != 0 || white.Luminance() != 1 || !black.IsDark() || !white.IsLight() { + t.Fatal("analysis") + } + if !Equals("#ff000066", "rgba(255, 0, 0, .4)") { + t.Fatal("equals") + } + if !Random().Valid() { + t.Fatal("random") + } } func TestStringHexFilterAndConversion(t *testing.T) { - c,_:=FromCompat("rgba(255, 0, 0, .5)",false) - if c.ToPercentageRGBString()!="rgba(100%, 0%, 0%, 0.5)"||c.ToHSLString()!="hsla(0, 100%, 50%, 0.5)"||c.ToHSVString()!="hsva(0, 100%, 100%, 0.5)"{t.Fatal("strings")} - if c.ToString("hex8")!="#ff000080"||c.ToString("name")!="#ff0000"{t.Fatal(c.ToString("hex8"),c.ToString("name"))} - with,_:=FromCompatWithOptions("red",false,CompatOptions{GradientType:true}) - if with.ToFilter(nil,false)!="progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#ffff0000,endColorstr=#ffff0000)"{t.Fatal(with.ToFilter(nil,false))} + c, _ := FromCompat("rgba(255, 0, 0, .5)", false) + if c.ToPercentageRGBString() != "rgba(100%, 0%, 0%, 0.5)" || c.ToHSLString() != "hsla(0, 100%, 50%, 0.5)" || c.ToHSVString() != "hsva(0, 100%, 100%, 0.5)" { + t.Fatal("strings") + } + if c.ToString("hex8") != "#ff000080" || c.ToString("name") != "#ff0000" { + t.Fatal(c.ToString("hex8"), c.ToString("name")) + } + with, _ := FromCompatWithOptions("red", false, CompatOptions{GradientType: true}) + if with.ToFilter(nil, false) != "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#ffff0000,endColorstr=#ffff0000)" { + t.Fatal(with.ToFilter(nil, false)) + } } func TestImplicitAlphaHexFallsBackToRGBA(t *testing.T) { for _, input := range []string{"#f008", "#ff000080"} { c, _ := FromCompat(input, false) - if c.String() != c.ToRGBString() { t.Fatalf("%s: %s", input, c.String()) } - if c.ToString("hex8") != c.ToHex8String() { t.Fatal("explicit hex8") } + if c.String() != c.ToRGBString() { + t.Fatalf("%s: %s", input, c.String()) + } + if c.ToString("hex8") != c.ToHex8String() { + t.Fatal("explicit hex8") + } } } diff --git a/tests/original/verify.mjs b/tests/original/verify.mjs index 8d92d9aa..7fad2442 100644 --- a/tests/original/verify.mjs +++ b/tests/original/verify.mjs @@ -6,7 +6,9 @@ const digest = (file) => createHash("sha256").update(readFileSync(file)).digest( export function verifyManifest(manifestPath, root) { return readFileSync(manifestPath, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => { - const [, expected, file] = line.match(/^(\S+)\s{2}(.+)$/) ?? []; + const match = line.match(/^(\S+)\s{2}(.+)$/); + if (!match) return [`invalid: ${line}`]; + const [, expected, file] = match; const path = resolve(root, file); try { return digest(path) === expected ? [] : [`mismatch: ${file}`]; diff --git a/tests/original/verify.test.mjs b/tests/original/verify.test.mjs index 7371f3b7..3ae6e002 100644 --- a/tests/original/verify.test.mjs +++ b/tests/original/verify.test.mjs @@ -24,3 +24,15 @@ test("verifyManifest accepts matching files and reports a changed path", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("verifyManifest reports a malformed manifest line", () => { + const root = mkdtempSync(join(tmpdir(), "tinycolor-verify-")); + try { + const manifest = join(root, "manifest.sha256"); + writeFileSync(manifest, "not a manifest entry\n"); + + assert.deepEqual(verifyManifest(manifest, root), ["invalid: not a manifest entry"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); From bc915021911a1f8134d5e0e5b62b212cb1a193bf Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:44:55 +0530 Subject: [PATCH 049/165] ci(05-01): verify port and source suite --- .github/workflows/deno.yml | 42 -------------------------------------- .github/workflows/port.yml | 30 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/deno.yml create mode 100644 .github/workflows/port.yml diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml deleted file mode 100644 index 41adb05d..00000000 --- a/.github/workflows/deno.yml +++ /dev/null @@ -1,42 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow will install Deno then run `deno lint` and `deno test`. -# For more information see: https://github.com/denoland/setup-deno - -name: Deno - -on: - push: - branches: ["master"] - pull_request: - branches: ["master"] - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Setup repo - uses: actions/checkout@v3 - - - name: Setup Deno - # uses: denoland/setup-deno@v1 - uses: denoland/setup-deno@9db7f66e8e16b5699a514448ce994936c63f0d54 - with: - deno-version: v1.x - - # Uncomment this step to verify the use of 'deno fmt' on each commit. - # - name: Verify formatting - # run: deno fmt --check - - # - name: Run linter - # run: deno lint - - - name: Build & Run Tests - run: deno task build diff --git a/.github/workflows/port.yml b/.github/workflows/port.yml new file mode 100644 index 00000000..2b3a51f9 --- /dev/null +++ b/.github/workflows/port.yml @@ -0,0 +1,30 @@ +name: TinyColor Go port + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.26' + - uses: actions/setup-node@v4 + with: + node-version: '24' + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + - run: make verify + - run: deno task test + - run: make build + - uses: actions/upload-artifact@v4 + with: + name: tinycolor-linux-amd64 + path: bin/tinycolor From 7128a04096c605fcf54a53b0e7cd1241528815ae Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:47:43 +0530 Subject: [PATCH 050/165] docs(05-01): summarize CLI build and CI --- .../05-delivery-evidence/05-01-SUMMARY.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-01-SUMMARY.md diff --git a/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md b/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md new file mode 100644 index 00000000..44405190 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md @@ -0,0 +1,72 @@ +--- +phase: 5 +plan: 1 +subsystem: cli-build-ci +tags: [go, cli, make, docker, github-actions, sha256] +provides: [human CLI, immutable-oracle verifier, reproducible local gate, delivery CI] +affects: [src/cmd/tinycolor-compat, tests/original, Makefile, Dockerfile, .github/workflows] +tech-stack: [go-standard-library, node-standard-library, github-actions] +key-files: + created: + - src/cmd/tinycolor-compat/main_test.go + - tests/original/verify.mjs + - tests/original/verify.test.mjs + - .github/workflows/port.yml + modified: + - src/cmd/tinycolor-compat/main.go + - src/tinycolor/color_test.go + - Makefile + - Dockerfile + deleted: + - .github/workflows/deno.yml +decisions: + - One executable selects JSONL mode with zero arguments and human CLI mode with a command. + - GNU Make exports a repository-local GOCACHE and derives GOEXE for Windows/Linux artifacts. + - CI calls make verify and runs the untouched Deno suite as a separate environment check. +--- + +# Phase 5 Plan 1: CLI, Build, and CI Summary + +Added tested `parse`, `convert`, `lighten`, `palette`, and `contrast` commands +without changing the zero-argument JSONL protocol. Added cross-platform oracle +hash verification, a complete local parity gate, a one-command build, the +scratch Docker artifact, and one least-privilege port CI workflow. + +## TDD and Verification Evidence + +- CLI RED: the focused package test failed because `run` was undefined. +- CLI GREEN: focused command tests, all Go tests, and Node adapter tests passed. +- Manifest RED: verifier import was missing; malformed-line regression later + failed at `path.resolve` before the defensive parser fix. +- Manifest GREEN: two Node tests passed and direct verification printed + `verified: 3`. +- Formatting: `gofmt -l src` prints no files after the mechanical + `color_test.go` formatting fix. +- Full local equivalents: Go tests/vet, adapter tests, and all 162 fixed cases + passed: 9 smoke, 26 parser-hex-rgb, 23 parser, 35 conversion, 69 operations. +- Windows artifact: `bin/tinycolor.exe parse red` executed successfully. + +GNU Make is not installed in the current Windows environment, so `make build` +and `make verify` were not directly invoked locally. Their individual commands +passed; the configured Ubuntu CI remains the direct Make/Deno execution gate +until a remote run is observed. + +## Commits + +- `df1f9b8` `feat(05-01): add human CLI commands` +- `02f9ced` `build(05-01): add reproducible verification gate` +- `a4ad85e` `fix(05-01): make verification gate portable` +- `bc91502` `ci(05-01): verify port and source suite` + +## Deviations from Plan + +- The Makefile emits `bin/tinycolor.exe` on Windows and `bin/tinycolor` on + Linux. This is required for a runnable native Windows artifact. +- The pre-existing unformatted `src/tinycolor/color_test.go` was formatted in + the portability fix because the new non-mutating format gate correctly + blocked completion. + +## Known External Checks + +- GitHub Actions execution has not yet been observed. +- Deno remains unavailable locally; no original-suite pass is claimed here. From 57e111455bc969a7a8bccd2c77564bcab5db3c0c Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:57:09 +0530 Subject: [PATCH 051/165] fix(05-01): close delivery review gaps --- Makefile | 1 + src/cmd/tinycolor-compat/main.go | 4 ++-- src/cmd/tinycolor-compat/main_test.go | 4 ++-- tests/original/verify.mjs | 6 ++++-- tests/original/verify.test.mjs | 21 ++++++++++++++++++++- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 6fa4ef44..f76edc82 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ build: test: node tests/port/adapter.test.mjs + node --test tests/original/verify.test.mjs go -C src test ./... node compat/run.mjs compat/cases/smoke.jsonl node compat/run.mjs compat/cases/parser-hex-rgb.jsonl diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 014fb704..0f9ef3ea 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -201,10 +201,10 @@ func runContrast(args []string, stdout, stderr io.Writer) int { func usage(stderr io.Writer, command string) { if command == "" { - fmt.Fprintln(stderr, "Usage: tinycolor-compat ") + fmt.Fprintln(stderr, "Usage: tinycolor ") return } - fmt.Fprintln(stderr, "Usage: tinycolor-compat "+command) + fmt.Fprintln(stderr, "Usage: tinycolor "+command) } func writeJSON(stdout io.Writer, value any) int { diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index 511296a3..c5449495 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -137,8 +137,8 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { } else if stdout.String() != test.wantStdout { t.Fatalf("stdout = %q, want %q", stdout.String(), test.wantStdout) } - if test.wantStatus == 2 && !strings.Contains(stderr.String(), "Usage:") { - t.Fatalf("stderr = %q, want usage", stderr.String()) + if test.wantStatus == 2 && !strings.Contains(stderr.String(), "Usage: tinycolor ") { + t.Fatalf("stderr = %q, want tinycolor usage", stderr.String()) } }) } diff --git a/tests/original/verify.mjs b/tests/original/verify.mjs index 7fad2442..a9bb8c3a 100644 --- a/tests/original/verify.mjs +++ b/tests/original/verify.mjs @@ -1,15 +1,17 @@ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; +import { resolve, sep } from "node:path"; const digest = (file) => createHash("sha256").update(readFileSync(file)).digest("hex"); export function verifyManifest(manifestPath, root) { + const resolvedRoot = resolve(root); return readFileSync(manifestPath, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => { const match = line.match(/^(\S+)\s{2}(.+)$/); if (!match) return [`invalid: ${line}`]; const [, expected, file] = match; - const path = resolve(root, file); + const path = resolve(resolvedRoot, file); + if (path !== resolvedRoot && !path.startsWith(resolvedRoot + sep)) return [`invalid: ${file}`]; try { return digest(path) === expected ? [] : [`mismatch: ${file}`]; } catch { diff --git a/tests/original/verify.test.mjs b/tests/original/verify.test.mjs index 3ae6e002..877ff024 100644 --- a/tests/original/verify.test.mjs +++ b/tests/original/verify.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; @@ -36,3 +36,22 @@ test("verifyManifest reports a malformed manifest line", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("verifyManifest rejects entries outside the manifest root", () => { + const parent = mkdtempSync(join(tmpdir(), "tinycolor-verify-")); + const root = join(parent, "root"); + const outside = join(parent, "outside.txt"); + try { + mkdirSync(root); + writeFileSync(outside, "outside"); + const manifest = join(root, "manifest.sha256"); + writeFileSync(manifest, `${sha256("outside")} ${outside}\n${sha256("outside")} ../outside.txt\n`); + + assert.deepEqual(verifyManifest(manifest, root), [ + `invalid: ${outside}`, + "invalid: ../outside.txt", + ]); + } finally { + rmSync(parent, { recursive: true, force: true }); + } +}); From 0f746f4b7ed6002efa14919607287ff0274a5411 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:58:47 +0530 Subject: [PATCH 052/165] docs(05-01): record final review fixes --- .planning/phases/05-delivery-evidence/05-01-SUMMARY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md b/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md index 44405190..353fdb89 100644 --- a/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md +++ b/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md @@ -57,6 +57,7 @@ until a remote run is observed. - `02f9ced` `build(05-01): add reproducible verification gate` - `a4ad85e` `fix(05-01): make verification gate portable` - `bc91502` `ci(05-01): verify port and source suite` +- `57e1114` `fix(05-01): close delivery review gaps` ## Deviations from Plan @@ -65,6 +66,8 @@ until a remote run is observed. - The pre-existing unformatted `src/tinycolor/color_test.go` was formatted in the portability fix because the new non-mutating format gate correctly blocked completion. +- Final review added manifest absolute/traversal containment, wired verifier + regressions into `make verify`, and aligned usage text with `tinycolor`. ## Known External Checks From 7e4bfb9fb8fc3cfe86cd583db7178b174c8733e1 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:13:02 +0530 Subject: [PATCH 053/165] fix(05-01): pin oracle checkout bytes --- .gitattributes | 3 + .../resolved/ci-oracle-hash-line-endings.md | 57 +++++++++++++++++++ tests/original/verify.test.mjs | 10 ++++ 3 files changed, 70 insertions(+) create mode 100644 .gitattributes create mode 100644 .planning/debug/resolved/ci-oracle-hash-line-endings.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..5731d7cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +mod.js text eol=crlf +test.js text eol=crlf +tinycolor.js text eol=crlf diff --git a/.planning/debug/resolved/ci-oracle-hash-line-endings.md b/.planning/debug/resolved/ci-oracle-hash-line-endings.md new file mode 100644 index 00000000..7af8f263 --- /dev/null +++ b/.planning/debug/resolved/ci-oracle-hash-line-endings.md @@ -0,0 +1,57 @@ +--- +status: resolved +trigger: "Fix GitHub Actions make verify where tests/original/verify.mjs reports mismatch for mod.js, test.js, tinycolor.js." +created: 2026-08-01T05:09:56.9355940+05:30 +updated: 2026-08-01T05:16:00+05:30 +--- + +## Current Focus + +hypothesis: Confirmed: the oracle manifest describes CRLF working-tree bytes, but Git had no checkout eol attribute for the three LF blobs. +test: Completed focused regression, production verifier, and whitespace validation. +expecting: All checks pass. +next_action: Archive this resolved record and commit only the assigned files. + +## Symptoms + +expected: Ubuntu checkout bytes match kickoff manifest and verifier prints verified: 3. +actual: All three oracle text files mismatch in CI; local Windows passes. +errors: mismatch: mod.js / test.js / tinycolor.js; Make hashes target exits 1. +reproduction: Local SHA-256 hashes equal manifest, while raw Git blob SHA-256 values are different LF bytes. Local core.autocrlf=true; no .gitattributes exists; oracle git diff is empty. +started: Introduced when CI started running the existing Windows-derived kickoff manifest. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T05:09:56.9355940+05:30 + checked: Prefilled symptom evidence. + found: Windows working-tree hashes match the manifest, but raw Git blob hashes do not; the repository lacks .gitattributes and local core.autocrlf=true. + implication: Checkout line-ending conversion, rather than oracle content drift, is the leading mechanism. + +- timestamp: 2026-08-01T05:11:00+05:30 + checked: Assigned verifier files and root attributes file. + found: tests/original/verify.test.mjs already exists; .gitattributes and the debug knowledge base do not. verify.mjs hashes raw file bytes and does no line-ending normalization. + implication: The repository checkout must supply the manifest's CRLF bytes; verifier behavior should remain unchanged. + +- timestamp: 2026-08-01T05:13:00+05:30 + checked: New focused Node test before adding attributes. + found: The test failed exactly as predicted; git check-attr returned eol: unspecified for mod.js, test.js, and tinycolor.js while the other three verifier tests passed. + implication: The missing checkout policy is directly reproduced and the hypothesis is confirmed. + +- timestamp: 2026-08-01T05:15:00+05:30 + checked: Focused Node suite after adding .gitattributes. + found: All four tests passed, including the check that all three oracle paths resolve to eol: crlf. + implication: The repository now declares the required checkout bytes independently of platform defaults. + +- timestamp: 2026-08-01T05:16:00+05:30 + checked: Production verifier and git diff --check. + found: verify.mjs printed verified: 3; git diff --check exited 0 with only line-ending conversion warnings, including one for a concurrent fuzz file that was not touched. + implication: The original hash verification succeeds and the assigned diff contains no whitespace errors. + +## Resolution + +root_cause: The kickoff manifest hashes CRLF working-tree bytes, but the repository stores LF blobs and had no .gitattributes policy. Windows core.autocrlf=true masked this locally; Ubuntu checked out LF bytes and all three raw-byte hashes mismatched. +fix: Pin mod.js, test.js, and tinycolor.js to text eol=crlf at the repository root; retain a focused git check-attr regression test. +verification: RED confirmed eol unspecified; GREEN confirmed 4/4 focused tests pass; production verifier printed verified: 3; git diff --check exited 0. +files_changed: [.gitattributes, tests/original/verify.test.mjs, .planning/debug/resolved/ci-oracle-hash-line-endings.md] diff --git a/tests/original/verify.test.mjs b/tests/original/verify.test.mjs index 877ff024..3816faf7 100644 --- a/tests/original/verify.test.mjs +++ b/tests/original/verify.test.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -8,6 +9,15 @@ import { verifyManifest } from "./verify.mjs"; const sha256 = (value) => createHash("sha256").update(value).digest("hex"); +test("oracle files are checked out with CRLF line endings", () => { + const files = ["mod.js", "test.js", "tinycolor.js"]; + const attributes = execFileSync("git", ["check-attr", "eol", "--", ...files], { + encoding: "utf8", + }); + + assert.deepEqual(attributes.trim().split(/\r?\n/), files.map((file) => `${file}: eol: crlf`)); +}); + test("verifyManifest accepts matching files and reports a changed path", () => { const root = mkdtempSync(join(tmpdir(), "tinycolor-verify-")); try { From d8ba485e5227cbedee3f2eec5ce41fd3a0b869f4 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:17:12 +0530 Subject: [PATCH 054/165] fix(05-01): scope Deno CI to source suite --- .github/workflows/port.yml | 2 +- .../debug/resolved/ci-deno2-test-discovery.md | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .planning/debug/resolved/ci-deno2-test-discovery.md diff --git a/.github/workflows/port.yml b/.github/workflows/port.yml index 2b3a51f9..5e7243ff 100644 --- a/.github/workflows/port.yml +++ b/.github/workflows/port.yml @@ -22,7 +22,7 @@ jobs: with: deno-version: v2.x - run: make verify - - run: deno task test + - run: deno test test.js - run: make build - uses: actions/upload-artifact@v4 with: diff --git a/.planning/debug/resolved/ci-deno2-test-discovery.md b/.planning/debug/resolved/ci-deno2-test-discovery.md new file mode 100644 index 00000000..0f6dcc7e --- /dev/null +++ b/.planning/debug/resolved/ci-deno2-test-discovery.md @@ -0,0 +1,47 @@ +--- +status: resolved +trigger: "Deno 2 removed the files test config, so deno task test discovers generated npm and Node port tests and fails on undeclared @deno/shim-deno-test imports." +created: 2026-08-01T05:20:00+05:30 +updated: 2026-08-01T05:23:00+05:30 +--- + +## Current Focus + +hypothesis: Confirmed: the workflow invoked broad task discovery, while the source contract is the root test.js suite only. +test: Completed workflow text audit and git diff --check. +expecting: All checks pass. +next_action: Archive the resolved record and commit only the workflow and this debug document. + +## Symptoms + +expected: CI runs only the immutable original root suite test.js. +actual: Deno 2 discovers npm/esm/test.js, npm/cjs/test.js, tests/original/verify.test.mjs, and tests/port/adapter.test.mjs. +errors: Deno warns that files test config was removed; type-check fails because @deno/shim-deno-test is not a declared Deno dependency in generated npm tests. +reproduction: GitHub Actions source-suite step runs deno task test under Deno 2. +started: Appeared in CI after the oracle hash checkout fix allowed verification to reach the Deno source-suite step. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T05:20:00+05:30 + checked: GitHub Actions failure report. + found: Deno 2 ignores the removed files test config and broad task discovery reaches generated npm and Node-specific tests. + implication: Test discovery scope, not missing npm-test dependencies, is the failure boundary. + +- timestamp: 2026-08-01T05:21:00+05:30 + checked: Complete .github/workflows/port.yml. + found: The source-suite step runs deno task test without a path, exactly matching the broad discovery trigger. + implication: Scoping this existing CI invocation to test.js fixes the cause without modifying configs, dependencies, or generated tests. + +- timestamp: 2026-08-01T05:23:00+05:30 + checked: Exact workflow command audit and git diff --check. + found: deno test test.js is present; deno task test and --no-check are absent; git diff --check exited 0 with only line-ending conversion warnings. + implication: The workflow is narrowly scoped to the immutable source suite and the change has no whitespace errors. + +## Resolution + +root_cause: Deno 2 removed the files test configuration, but CI still invoked deno task test without a path. That broad invocation recursively discovered generated npm and Node port tests outside the intended immutable root suite, then type-checking failed on their environment-specific imports. +fix: Change only the source-suite workflow command to deno test test.js. +verification: Text audit confirmed deno test test.js is present and deno task test/--no-check are absent; git diff --check exited 0. +files_changed: [.github/workflows/port.yml, .planning/debug/resolved/ci-deno2-test-discovery.md] From 0a9db8b9853d6b69603b9d087860d2a7f04cb035 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:25:26 +0530 Subject: [PATCH 055/165] fix(04): match saturation conversion rounding --- compat/cases/operations.jsonl | 1 + src/tinycolor/color.go | 36 ++------------------------------ src/tinycolor/operations_test.go | 7 +++++++ 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 8a15dcb3..7c747c46 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -15,6 +15,7 @@ {"id":"mod-saturate-normal","operation":"modify","input":"#808040","args":{"method":"saturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-saturate-zero","operation":"modify","input":"#808040","args":{"method":"saturate","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mod-saturate-clamp","operation":"modify","input":"#808040","args":{"method":"saturate","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-saturate-negative-rounding","operation":"modify","input":"#400140","args":{"method":"saturate","amount":-100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-saturate-alpha","operation":"modify","input":"#80804080","args":{"method":"saturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-desaturate-normal","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-desaturate-zero","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 5683da50..2728581a 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -494,40 +494,8 @@ func hsvColor(hue, saturation, value float64) Color { } func (c *Color) setHSL(h, s, l float64) { - c.model.R, c.model.G, c.model.B = hslToRGB(h, s, l) -} - -func hslToRGB(h, s, l float64) (float64, float64, float64) { - if s == 0 { - channel := l * 255 - return channel, channel, channel - } - var q float64 - if l < .5 { - q = l * (1 + s) - } else { - q = l + s - l*s - } - p := 2*l - q - channel := func(t float64) float64 { - if t < 0 { - t++ - } - if t > 1 { - t-- - } - switch { - case t*6 < 1: - return p + (q-p)*6*t - case t*2 < 1: - return q - case t*3 < 2: - return p + (q-p)*(2.0/3.0-t)*6 - default: - return p - } - } - return channel(h+1.0/3.0) * 255, channel(h) * 255, channel(h-1.0/3.0) * 255 + converted := hslColor(h*360, s, l, c.model.A, true) + c.model.R, c.model.G, c.model.B = converted.model.R, converted.model.G, converted.model.B } func clamp01(value float64) float64 { return math.Min(1, math.Max(0, value)) } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index df4ec630..005498f0 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -95,6 +95,13 @@ func TestModifiers(t *testing.T) { } } +func TestSaturateMatchesObjectConversionRounding(t *testing.T) { + color, _ := FromCompat("#400140", false) + if got := color.Saturate(-100).ToHexString(); got != "#202020" { + t.Fatalf("Saturate(-100) = %s", got) + } +} + func TestMix(t *testing.T) { black, _ := FromCompat("#000", false) white, _ := FromCompat("#fff", false) From 005f86357184555830fb81c7a158d42b14c8e061 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:25:35 +0530 Subject: [PATCH 056/165] docs(debug): resolve fuzz saturation rounding --- .planning/debug/knowledge-base.md | 13 ++++ .../debug/resolved/fuzz-saturate-rounding.md | 72 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 .planning/debug/knowledge-base.md create mode 100644 .planning/debug/resolved/fuzz-saturate-rounding.md diff --git a/.planning/debug/knowledge-base.md b/.planning/debug/knowledge-base.md new file mode 100644 index 00000000..162375a4 --- /dev/null +++ b/.planning/debug/knowledge-base.md @@ -0,0 +1,13 @@ +# GSD Debug Knowledge Base + +Resolved debug sessions. Used by `gsd-debugger` to surface known-pattern hypotheses at the start of new investigations. + +--- + +## fuzz-saturate-rounding — Saturate negative rounding differed at an RGB half boundary +- **Date:** 2026-08-01 +- **Error patterns:** saturate, rounding, #400140, #202020, #212121, RGB 32, RGB 33, fuzz divergence +- **Root cause:** setHSL called a direct hslToRGB conversion, bypassing TinyColor's object-input percentage conversion and Bound01 truncation before RGB output rounding. +- **Fix:** Route setHSL through the existing hslColor/parser conversion and remove the duplicate direct converter. +- **Files changed:** src/tinycolor/color.go, src/tinycolor/operations_test.go, compat/cases/operations.jsonl +--- diff --git a/.planning/debug/resolved/fuzz-saturate-rounding.md b/.planning/debug/resolved/fuzz-saturate-rounding.md new file mode 100644 index 00000000..3a8d8bba --- /dev/null +++ b/.planning/debug/resolved/fuzz-saturate-rounding.md @@ -0,0 +1,72 @@ +--- +status: resolved +trigger: "Find and fix the first deterministic Phase 5 fuzz parity mismatch: saturate(-100) on #400140 returns #212121 in Go but #202020 in JavaScript." +created: 2026-08-01T05:19:38.0748150+05:30 +updated: 2026-08-01T05:44:00+05:30 +--- + +## Current Focus + +hypothesis: confirmed — Go setHSL bypasses TinyColor's object-input percentage conversion and Bound01 truncation, retaining 32.5 instead of the oracle's 32.487 before identical half-up output rounding +test: completed +expecting: all requested checks pass +next_action: archived; commit remains for a caller with writable Git metadata + +## Symptoms + +expected: JavaScript oracle and Go response match exactly for request {id:fuzz-2, operation:modify, input:#400140, args:{method:saturate, amount:-100}}; JS after is #202020 with RGB 32,32,32. +actual: Go after is #212121 with RGB 33,33,33. +errors: seed-1 one-second fuzz run found 338 divergences; first mismatch is deterministic and full request/outputs are in .superpowers/sdd/05-02-PLAN/task-1-report.md. +reproduction: send the exact JSONL request to persistent or one-shot compat/js-runner.mjs and the Go CLI; also call typed Color.Saturate(-100) on #400140. +started: uncovered by the new broad Phase 5 fuzz generator; fixed Phase 4 corpus did not include this input. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T05:23:00+05:30 + checked: prior Phase 5 task report and repository status + found: exact request deterministically differs (#202020 JS vs #212121 Go); only concurrent fuzz harness files are dirty + implication: preserve fuzz files and isolate the fix to shared Go conversion behavior plus regressions + +- timestamp: 2026-08-01T05:23:00+05:30 + checked: repository knowledge base and user memory registry + found: no matching prior TinyColor/HSL rounding diagnosis + implication: investigate from source behavior rather than reuse a known pattern + +- timestamp: 2026-08-01T05:25:00+05:30 + checked: first adapter reproduction attempt + found: setup was invalid because Node ran from src and Go's default cache was sandbox-denied + implication: no behavioral conclusion; rerun from root and set GOCACHE inside the workspace + +- timestamp: 2026-08-01T05:30:00+05:30 + checked: exact request through root compat/js-runner.mjs and src/cmd/tinycolor-compat + found: reproduced #202020 in JavaScript and #212121 in Go with otherwise identical responses + implication: mismatch is in shared typed color behavior, not fuzz harness or adapter serialization + +- timestamp: 2026-08-01T05:30:00+05:30 + checked: source rgbToHsl/saturate/tinycolor(hsl)/hslToRgb path and all Go setHSL callers + found: JavaScript converts l=0.12745098039215685 to "12.745098039215685%" then Bound01 truncates to 12.74%, yielding 32.487; Go setHSL directly multiplies unquantized l by 255, yielding exactly 32.5 + implication: identical half-up rounding then produces 32 versus 33; source-compatible percentage quantization is missing in the shared Go modifier conversion path used by Lighten, Darken, Saturate, Desaturate, Greyscale, and Spin + +- timestamp: 2026-08-01T05:36:00+05:30 + checked: typed regression and full operations JSONL corpus before production change + found: typed test failed with #212121; corpus passed 69/70 and only the new exact regression mismatched + implication: regression isolates the defect and provides both API-level and adapter-level RED evidence + +- timestamp: 2026-08-01T05:40:00+05:30 + checked: typed regression, 70-case operations corpus, and exact request through both adapters after fix + found: typed test passed; corpus passed 70/70 with zero mismatches; both adapters returned #202020 and RGB 32,32,32 + implication: minimal shared-path fix resolves the original issue and adapter parity + +- timestamp: 2026-08-01T05:44:00+05:30 + checked: go test ./..., go vet ./..., git diff --check, scoped diff, and commit attempt + found: all tests, vet, and diff check passed; scoped diff contains only the shared fix and two regressions; commit failed because .git/index.lock could not be created + implication: code is verified and ready to commit, but repository metadata permissions prevent this agent from creating the requested commit + +## Resolution + +root_cause: setHSL calls hslToRGB directly with normalized HSL values, while the JavaScript modifier reconstructs through tinycolor(hsl), whose object parser converts s/l to percentage strings and Bound01 truncates them to four decimal ratio precision before RGB conversion. At the 32.5 boundary this changes the rounded channel. +fix: setHSL now reuses hslColor and the existing parser conversion, removing the duplicate direct hslToRGB implementation that skipped TinyColor percentage quantization. +verification: focused typed test passed; operations corpus 70/70 with zero mismatches; exact JS and Go outputs both #202020/RGB 32; go test ./... passed; go vet ./... passed; git diff --check passed. +files_changed: [src/tinycolor/color.go, src/tinycolor/operations_test.go, compat/cases/operations.jsonl] From 6b4506a7cfed0441d0693820726a16f68b11ad8a Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:36:58 +0530 Subject: [PATCH 057/165] wip: delivery evidence paused at task 1/3 --- .planning/HANDOFF.json | 111 ++++++++ .planning/debug/fuzz-readability-precision.md | 61 +++++ .../05-delivery-evidence/.continue-here.md | 158 +++++++++++ compat/cases/operations.jsonl | 1 + fuzz/README.md | 24 +- fuzz/harness.mjs | 254 ++++++++++++++++++ fuzz/harness.test.mjs | 54 ++++ src/cmd/pow-debug/main.go | 13 + src/tinycolor/operations_test.go | 14 + 9 files changed, 688 insertions(+), 2 deletions(-) create mode 100644 .planning/HANDOFF.json create mode 100644 .planning/debug/fuzz-readability-precision.md create mode 100644 .planning/phases/05-delivery-evidence/.continue-here.md create mode 100644 fuzz/harness.mjs create mode 100644 fuzz/harness.test.mjs create mode 100644 src/cmd/pow-debug/main.go diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json new file mode 100644 index 00000000..93b58754 --- /dev/null +++ b/.planning/HANDOFF.json @@ -0,0 +1,111 @@ +{ + "version": "1.0", + "timestamp": "2026-08-01T00:04:30.289Z", + "phase": "5", + "phase_name": "Delivery Evidence", + "phase_dir": ".planning/phases/05-delivery-evidence", + "plan": 2, + "task": 1, + "total_tasks": 3, + "status": "paused", + "completed_tasks": [ + { + "id": "05-01-01", + "name": "Human CLI", + "status": "done", + "commit": "df1f9b8" + }, + { + "id": "05-01-02", + "name": "Reproducible verification gate", + "status": "done", + "commit": "a4ad85e" + }, + { + "id": "05-01-03", + "name": "Port CI and source suite", + "status": "done", + "commit": "d8ba485" + }, + { + "id": "05-02-01", + "name": "Differential fuzz harness", + "status": "in_progress", + "progress": "Persistent seeded harness is implemented but deliberately uncommitted/blocked by an exact readability precision mismatch. First saturation mismatch was fixed in 0a9db8b." + } + ], + "remaining_tasks": [ + { + "id": "05-02-01", + "name": "Finish broad fuzz harness after readability parity fix", + "status": "in_progress" + }, + { + "id": "05-02-02", + "name": "Validate and record real 60-second fuzz log", + "status": "not_started" + }, + { + "id": "05-02-03", + "name": "Generate shared-workload benchmark evidence", + "status": "not_started" + }, + { + "id": "05-03", + "name": "Submission documentation, evidence refresh, external checkpoint, and phase closeout", + "status": "not_started" + } + ], + "blockers": [ + { + "description": "Exact readability mismatch: JavaScript 1.188086751976723 versus Go 1.1880867519767233 for {r:127,g:64,b:15,a:0} against #80007f.", + "type": "technical", + "workaround": "None allowed. Do not add tolerance or narrow fuzz inputs. Continue .planning/debug/fuzz-readability-precision.md; root cause is V8 Math.pow versus Go math.Pow final rounding. An exact bounded 256-entry transfer lookup is the leading safe fix." + }, + { + "description": "The WIP branch intentionally contains a failing exact readability regression and fuzz zero-divergence test.", + "type": "technical", + "workaround": "Fix the shared Luminance transfer function, run the exact regression and operations corpus, then rerun the broad seed-1 fuzz smoke before committing production completion." + } + ], + "human_actions_pending": [ + { + "action": "Record and publish the five-minute demo video, then add its URL.", + "context": "docs/DEMO.md is still a future Plan 05-03 deliverable; no video is claimed.", + "blocking": true + } + ], + "decisions": [ + { + "decision": "Keep broad bounded fuzz generation and stop on every exact mismatch.", + "rationale": "Narrowing to corpus-proven inputs would hide defects and violate behavioral-equivalence scoring.", + "phase": "5" + }, + { + "decision": "Pin oracle checkout bytes to CRLF with .gitattributes instead of normalizing hashes.", + "rationale": "The kickoff manifest hashes raw CRLF working-tree bytes; CI must reproduce that byte contract.", + "phase": "5" + }, + { + "decision": "Run `deno test test.js` explicitly in CI.", + "rationale": "Deno 2 removed broad discovery exclusions and otherwise includes generated npm and Node port tests outside the immutable source suite.", + "phase": "5" + }, + { + "decision": "Prefer an exact finite-domain luminance transfer lookup if no smaller V8-compatible power implementation exists.", + "rationale": "TinyColor rounds luminance channels to 8-bit integers; Go math.Pow differs from V8 for 93 of 245 nonlinear channels and heuristic ULP correction is incorrect.", + "phase": "5" + } + ], + "uncommitted_files": [ + "compat/cases/operations.jsonl", + "fuzz/README.md", + "fuzz/harness.mjs", + "fuzz/harness.test.mjs", + "src/tinycolor/operations_test.go", + "src/cmd/pow-debug/main.go", + ".planning/debug/fuzz-readability-precision.md" + ], + "next_action": "Run `$gsd-resume-work`, read `.planning/debug/fuzz-readability-precision.md`, finish the exact luminance transfer fix with the existing RED tests, then run `node fuzz/harness.mjs --duration 1 --seed 1` without narrowing the generator.", + "context_notes": "Branch rajeet. GitHub Actions run 30673847990 passed make verify, deno test test.js, build, and artifact upload. Local Deno 2.9.4 ran the immutable suite: 45 passed, 0 failed, 1 ignored. Current broad fuzz smoke is expected red: 12,291 cases and 388 divergences before the readability fix." +} diff --git a/.planning/debug/fuzz-readability-precision.md b/.planning/debug/fuzz-readability-precision.md new file mode 100644 index 00000000..1cbb524b --- /dev/null +++ b/.planning/debug/fuzz-readability-precision.md @@ -0,0 +1,61 @@ +--- +status: investigating +trigger: "Investigate and fix next deterministic Phase 5 fuzz parity mismatch: readability for {r:127,g:64,b:15,a:0} against '#80007f' returns 1.188086751976723 in JavaScript and 1.1880867519767233 in Go." +created: 2026-08-01T00:00:00+05:30 +updated: 2026-08-01T00:04:00+05:30 +--- + +## Current Focus + +hypothesis: confirmed: V8 Math.pow and Go math.Pow use different algorithms whose final rounding differs across the finite 8-bit luminance channel domain +test: inspect the primary V8 implementation and compare all 245 nonlinear channel values to select the smallest exact shared-method implementation +expecting: algorithm evidence plus the bounded mismatch distribution will show whether a small compatible calculation exists or an exact transfer table is required +next_action: inspect V8 ieee754 pow source and evaluate exact implementation options + +## Symptoms + +expected: JavaScript and Go exact JSON response match for readability; JavaScript returns 1.188086751976723. +actual: Go returns 1.1880867519767233; broad seed-1 one-second fuzz run has 388 divergences and fuzz-32 is first. +errors: exact floating-point JSON mismatch for operation readability +reproduction: exact request is in .superpowers/sdd/05-02-PLAN/task-1-report.md; run through both adapters and typed Readability with input {r:127,g:64,b:15,a:0}, args.other '#80007f'. +started: uncovered after the fuzz saturation root-cause fix. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T00:01:00+05:30 + checked: repository status and protected fuzz files + found: fuzz/README.md is modified and fuzz/harness.mjs plus fuzz/harness.test.mjs are untracked pre-existing work + implication: preserve those files without editing, staging, reverting, or narrowing them +- timestamp: 2026-08-01T00:01:00+05:30 + checked: debug knowledge base + found: only prior saturation rounding case exists and its keywords/root cause do not match readability precision + implication: no known-pattern candidate applies +- timestamp: 2026-08-01T00:01:00+05:30 + checked: complete JavaScript getLuminance/readability and Go Color.Luminance/Readability implementations plus callers + found: both round normalized RGB before luminance and use the same WCAG constants; Go caches the two luminances once while JavaScript recomputes them for Max and Min; IsReadable and MostReadable both call shared Readability + implication: parsing and exact arithmetic intermediates must be measured before changing the shared method +- timestamp: 2026-08-01T00:02:00+05:30 + checked: exact requests through both adapters + found: normalized RGB inspections are identical; first luminance is identical at 0.08213307169664993, but #80007f luminance is 0.06121500300950977 in JavaScript and 0.061215003009509765 in Go + implication: parsing, alpha, and final ratio ordering are ruled out; the first divergence is inside shared Color.Luminance for integer RGB 128,0,127 +- timestamp: 2026-08-01T00:03:00+05:30 + checked: new exact typed regression and exact operations JSONL row before production changes + found: typed test fails with 1.1880867519767233 and corpus has exactly one mismatch at readability-object-precision + implication: RED is confirmed and independently covers both public Go API and adapter parity +- timestamp: 2026-08-01T00:04:00+05:30 + checked: channel-level gamma expansion raw float bits for #80007f + found: channel 128 has identical normalized/base input but V8 Math.pow yields bits 3fcba1511e3e632d while Go math.Pow yields 3fcba1511e3e632c; channel 127 matches at 3fcb2a60a1263b0a + implication: the root cause is Go math.Pow not reproducing the JavaScript oracle's power rounding, not weighted luminance summation +- timestamp: 2026-08-01T00:04:00+05:30 + checked: all nonlinear integer channels 11 through 255 + found: 93 of 245 channel powers differ, with signed deltas in both directions and two cases differing by two ULPs + implication: unconditional Nextafter or decimal/output rounding would be incorrect; the fix must reproduce the bounded transfer function or V8 algorithm + +## Resolution + +root_cause: +fix: +verification: +files_changed: [] diff --git a/.planning/phases/05-delivery-evidence/.continue-here.md b/.planning/phases/05-delivery-evidence/.continue-here.md new file mode 100644 index 00000000..650ca199 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/.continue-here.md @@ -0,0 +1,158 @@ +--- +phase: 05-delivery-evidence +plan: 2 +task: 1 +total_tasks: 3 +status: in_progress +last_updated: 2026-08-01T00:04:30.289Z +branch: rajeet +--- + + + +Phase 5 Plan 05-01 is implemented and CI-verified. Plan 05-02 Task 1 is paused +mid-debug: the broad persistent differential harness is implemented, but its +zero-divergence test correctly remains RED because fuzzing found exact port +defects. Do not narrow the generator or add comparison tolerance. + +The branch is intentionally being handed off with WIP files. The first fuzz +defect (`saturate(-100)` rounding) is fixed and committed. The next defect is +an exact WCAG luminance/readability precision mismatch under active diagnosis. + + + + + +- Phase 5 design, context, research, validation, and three execution plans were + written and independently plan-checked. +- Human CLI commands `parse`, `convert`, `lighten`, `palette`, and `contrast` + were implemented with JSON output while preserving zero-argument JSONL mode. +- Added one-command Make build/verify, oracle hash verification, scratch Docker + artifact, and GitHub Actions delivery workflow. +- Fixed CI raw-byte hash failures by pinning the three oracle files to CRLF in + `.gitattributes`; a simulated `core.autocrlf=false` checkout prints + `verified: 3`. +- Fixed Deno 2 discovery by running the immutable suite explicitly with + `deno test test.js`. +- GitHub Actions run + `https://github.com/rajeet-04/TinyColor/actions/runs/30673847990` passed in + 42 seconds: `make verify`, Deno source suite, build, and artifact upload. +- Local Deno 2.9.4 source suite result: 45 passed, 0 failed, 1 ignored. +- Built the standard-library persistent fuzz harness with seeded broad inputs, + one outstanding request per child, protocol safeguards, and RED/GREEN tests. +- Fixed the first fuzz mismatch in `0a9db8b`: TinyColor HSL percentage + truncation for `saturate(-100)` now matches; operations corpus is 70/70 at + that commit. + + + + + +1. Finish Task 05-02-01: + - Continue `.planning/debug/fuzz-readability-precision.md`. + - Existing RED case: `{r:127,g:64,b:15,a:0}` vs `#80007f` returns + JavaScript `1.188086751976723`, Go `1.1880867519767233`. + - Root cause is confirmed inside nonlinear luminance channel expansion: + V8 `Math.pow` and Go `math.Pow` differ in final bits for 93 of the 245 + nonlinear 8-bit channels; two differ by two ULPs. + - `src/tinycolor/operations_test.go` and + `compat/cases/operations.jsonl` contain the failing exact regression. + - `src/cmd/pow-debug/main.go` is a temporary diagnostic that prints Go + channel power bits. + - Leading safe fix: a reproducibly generated exact 256-entry V8 transfer + lookup used by `Color.Luminance`. Do not use `Nextafter`, decimal output + rounding, or tolerance. + - After the exact regression and 71-row operations corpus pass, rerun + `node --test fuzz/harness.test.mjs` and + `node fuzz/harness.mjs --duration 1 --seed 1` broadly. If another + mismatch appears, repeat deterministic regression -> root cause -> micro + commit. +2. Task 05-02-02: add tested fuzz-log validator and record an actual + `--duration 60 --seed 20260801` zero-divergence run. +3. Task 05-02-03: implement shared workload benchmark runner and publish actual + p99 latency, peak RSS, startup, and throughput results without quick-run + overwrite. +4. Plan 05-03: rewrite judge-facing README, expand DECISIONS to 12+ substantive + rows, refresh COMPATIBILITY with exact evidence/coverage/unsafe counts, add + demo script, verify public clone/current CI, and close roadmap/state. +5. Human: record and publish the five-minute demo video; no video is currently + claimed. + + + + + +- Broad fuzzing stays broad. A mismatch blocks the bonus and becomes a fixed + regression; inputs are never constrained to make the log green. +- Oracle hashing remains byte-exact. `.gitattributes` reproduces kickoff CRLF + bytes on Linux rather than weakening verification with newline normalization. +- CI runs `deno test test.js`, not broad `deno task test`, because Deno 2 would + discover generated npm and port-owned Node tests. +- Luminance parity needs an exact finite-domain solution. Heuristic ULP + correction is ruled out because channel deltas have both signs. +- Keep micro-commits: regression/root-cause fixes, evidence artifacts, and + debug/archive docs stay separately revertible. + + + + + +- Technical blocker: exact V8/Go power rounding mismatch in `Color.Luminance`. +- Expected red checks until fixed: + - `go test ./tinycolor -run TestReadabilityMatchesJavaScriptPrecision` + - operations corpus has the new readability case as its mismatch + - `node --test fuzz/harness.test.mjs` fails only its zero-divergence test + - seed-1 one-second broad run previously produced 12,291 cases / 388 + divergences; first was the readability case above. + + + + + +- `fuzz/harness.mjs` — persistent broad differential harness. +- `fuzz/harness.test.mjs` — PRNG/options/generator/end-to-end tests. +- `fuzz/README.md` — reproduction and claim rules. +- `src/tinycolor/operations_test.go` — exact readability RED regression and + temporary numeric logging. +- `compat/cases/operations.jsonl` — adapter RED regression row. +- `.planning/debug/fuzz-readability-precision.md` — active root-cause evidence. +- `src/cmd/pow-debug/main.go` — temporary channel-bit diagnostic. + +These files are preserved in the WIP handoff commit. The temporary logging and +`pow-debug` command may be removed only after the exact solution is verified; +do not delete them before the next agent reads the evidence. + + + + + +- GitHub Actions: PASS — run 30673847990. +- Local Deno: PASS — 45 passed, 0 failed, 1 ignored. +- Oracle hash verifier: PASS — 3 files. +- Fixed corpus through saturation commit: operations 70/70. +- Current WIP: intentionally RED only for unresolved readability precision and + fuzz zero-divergence. +- No background server or watcher is running; all subagents were stopped. + + + + + +Start with: + +```text +$gsd-resume-work +``` + +Then read this file and `.planning/debug/fuzz-readability-precision.md`. Finish +the exact luminance transfer fix with the existing RED tests before changing +the fuzz harness. The first mandatory verification after the fix is: + +```powershell +$env:GOCACHE = (Resolve-Path '.cache\go-build').Path +go -C src test ./tinycolor -run TestReadabilityMatchesJavaScriptPrecision -count=1 +node compat/run.mjs compat/cases/operations.jsonl +node fuzz/harness.mjs --duration 1 --seed 1 +``` + + diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 7c747c46..b4f68828 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -42,6 +42,7 @@ {"id":"readability-same","operation":"readability","input":"#000","args":{"other":"#000"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readability-low","operation":"readability","input":"#000","args":{"other":"#111"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readability-max","operation":"readability","input":"#000","args":{"other":"#fff"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readability-object-precision","operation":"readability","input":{"r":127,"g":64,"b":15,"a":0},"args":{"other":"#80007f"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readable-aa-small","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":"AA","size":"small"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readable-aa-large","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{"level":"AA","size":"large"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readable-aaa-small","operation":"isReadable","input":"#db91b8","args":{"other":"#2e0c3a","options":{"level":"AAA","size":"small"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/fuzz/README.md b/fuzz/README.md index d021a6ca..db962269 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -1,4 +1,24 @@ # Differential fuzzing -Phase 3 adds a seeded generator here. Until then, the checked-in JSONL corpora -in `compat/cases/` are the reproducible differential input set. +`harness.mjs` sends one deterministic JSONL request stream to persistent +JavaScript and Go adapters and compares every parsed response exactly. + +One-second smoke: + +```text +node fuzz/harness.mjs --duration 1 --seed 1 +``` + +Full evidence run: + +```text +node fuzz/harness.mjs --duration 60 --seed 20260801 +``` + +Rerun a failure with the logged seed and duration. Each divergence is a JSON +object containing the request and both responses; the four final lines report +duration, seed, case count, and divergence count. + +Claim zero divergences only from a real saved run whose reported duration meets +the claim. A deterministic seed reproduces the request prefix, while case count +can vary with machine speed. diff --git a/fuzz/harness.mjs b/fuzz/harness.mjs new file mode 100644 index 00000000..5781f19e --- /dev/null +++ b/fuzz/harness.mjs @@ -0,0 +1,254 @@ +import { execFileSync, spawn } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { resolve } from "node:path"; +import { createInterface } from "node:readline"; +import { pathToFileURL } from "node:url"; +import { isDeepStrictEqual } from "node:util"; + +const root = resolve(import.meta.dirname, ".."); +const defaults = { duration: 60, seed: 20260801 }; +const operations = ["inspect", "output", "modify", "mix", "readability", "isReadable", "palette"]; +const outputMethods = [ + "toHex", "toHex8", "toHexString", "toHex8String", "toRgbString", + "toPercentageRgbString", "toHslString", "toHsvString", "toString", + "toName", "toFilter", +]; +const modifyMethods = ["lighten", "brighten", "darken", "saturate", "desaturate", "greyscale", "spin"]; +const paletteMethods = ["complement", "splitcomplement", "triad", "tetrad", "analogous", "monochromatic"]; +const components = [0, 1, 15, 32, 64, 127, 128, 192, 254, 255]; +const alphas = [0, 0.25, 0.5, 0.75, 1]; + +export function createPrng(seed) { + let state = seed >>> 0; + return () => { + state = (state + 0x6d2b79f5) >>> 0; + let value = state; + value = Math.imul(value ^ value >>> 15, value | 1); + value ^= value + Math.imul(value ^ value >>> 7, value | 61); + return ((value ^ value >>> 14) >>> 0) / 0x100000000; + }; +} + +const pick = (random, values) => values[Math.floor(random() * values.length)]; + +function colorInput(random) { + const r = pick(random, components); + const g = pick(random, components); + const b = pick(random, components); + const a = pick(random, alphas); + switch (Math.floor(random() * 4)) { + case 0: return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`; + case 1: return `rgb(${r}, ${g}, ${b})`; + case 2: return `rgba(${r}, ${g}, ${b}, ${a})`; + default: return { r, g, b, a }; + } +} + +export function generateRequest(random, index) { + const operation = operations[index % operations.length]; + const input = colorInput(random); + const request = { id: `fuzz-${index}`, operation, input, args: {} }; + switch (operation) { + case "output": { + const method = pick(random, outputMethods); + request.args.method = method; + if (method === "toString") request.args.format = pick(random, ["hex", "hex8", "rgb", "prgb", "hsl", "hsv", "name"]); + if (method === "toFilter") request.args.secondColor = colorInput(random); + break; + } + case "modify": { + const method = pick(random, modifyMethods); + request.args.method = method; + if (method !== "greyscale") request.args.amount = method === "spin" + ? pick(random, [-360, -180, -120, 0, 120, 180, 360]) + : pick(random, [-100, -50, 0, 10, 20, 50, 100]); + break; + } + case "mix": + request.args = { other: colorInput(random), amount: pick(random, [-50, 0, 25, 50, 75, 100, 150]) }; + break; + case "readability": + request.args.other = colorInput(random); + break; + case "isReadable": + request.args = { + other: colorInput(random), + options: { level: pick(random, ["AA", "AAA"]), size: pick(random, ["small", "large"]) }, + }; + break; + case "palette": { + const method = pick(random, paletteMethods); + request.args.method = method; + if (method === "analogous") Object.assign(request.args, { results: pick(random, [1, 3, 6]), slices: pick(random, [6, 12, 30]) }); + if (method === "monochromatic") request.args.results = pick(random, [1, 3, 6]); + break; + } + } + return request; +} + +function validateOptions(options) { + if (!Number.isFinite(options.duration) || options.duration <= 0) throw new Error("--duration must be a positive number"); + if (!Number.isSafeInteger(options.seed)) throw new Error("--seed must be an integer"); + return options; +} + +export function parseOptions(args) { + const options = { ...defaults }; + for (let index = 0; index < args.length; index += 2) { + const flag = args[index]; + const value = args[index + 1]; + if (value === undefined) throw new Error(`${flag} requires a value`); + if (flag === "--duration") options.duration = Number(value); + else if (flag === "--seed") options.seed = Number(value); + else throw new Error(`unknown option: ${flag}`); + } + return validateOptions(options); +} + +function startRunner(command, args, env) { + const child = spawn(command, args, { cwd: root, env, stdio: ["pipe", "pipe", "pipe"] }); + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + let stderr = ""; + let spawnError; + child.stderr.on("data", (chunk) => { stderr += chunk; }); + child.on("error", (error) => { spawnError = error; }); + const closed = new Promise((resolveClose) => child.once("close", (code, signal) => resolveClose({ code, signal }))); + return { + child, + closed, + get failure() { return spawnError; }, + get stderr() { return stderr.trim(); }, + lines: createInterface({ input: child.stdout, crlfDelay: Infinity })[Symbol.asyncIterator](), + }; +} + +function withTimeout(promise, milliseconds, message) { + let timer; + return Promise.race([ + promise, + new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), milliseconds); }), + ]).finally(() => clearTimeout(timer)); +} + +function writeLine(runner, line) { + return new Promise((resolveWrite, reject) => { + const fail = (error) => reject(new Error(`write failure: ${error.message}`)); + runner.child.stdin.once("error", fail); + runner.child.stdin.write(`${line}\n`, (error) => { + runner.child.stdin.off("error", fail); + if (error) fail(error); + else resolveWrite(); + }); + }); +} + +async function exchange(runner, request) { + await writeLine(runner, JSON.stringify(request)); + const { value, done } = await runner.lines.next(); + if (done) { + const detail = runner.failure?.message || runner.stderr || "child exited before responding"; + throw new Error(detail); + } + let response; + try { + response = JSON.parse(value); + } catch { + throw new Error(`invalid JSON response: ${value}`); + } + if (response.id !== request.id) throw new Error(`response ID mismatch: expected ${request.id}, got ${response.id}`); + return response; +} + +async function stopRunner(runner) { + if (!runner) return; + if (!runner.child.stdin.destroyed) runner.child.stdin.end(); + let result; + try { + result = await withTimeout(runner.closed, 1_000, "child shutdown timeout"); + } catch (error) { + runner.child.kill(); + await runner.closed; + throw error; + } + if (runner.failure) throw runner.failure; + if (result.code !== 0) throw new Error(runner.stderr || `child exited ${result.code ?? result.signal}`); +} + +function buildBinary(env) { + mkdirSync(resolve(root, "bin"), { recursive: true }); + const suffix = execFileSync("go", ["env", "GOEXE"], { cwd: root, env, encoding: "utf8" }).trim(); + execFileSync("go", ["-C", "src", "build", "-o", `../bin/tinycolor${suffix}`, "./cmd/tinycolor-compat"], { + cwd: root, + env, + stdio: ["ignore", "ignore", "inherit"], + }); + return resolve(root, "bin", `tinycolor${suffix}`); +} + +export async function runHarness({ duration = defaults.duration, seed = defaults.seed, onDivergence = console.log } = {}) { + validateOptions({ duration, seed }); + const env = { ...process.env, GOCACHE: process.env.GOCACHE ?? resolve(root, ".cache", "go-build") }; + const binary = buildBinary(env); + const javascript = startRunner(process.execPath, ["compat/js-runner.mjs"], env); + const go = startRunner(binary, [], env); + const random = createPrng(seed); + let cases = 0; + let divergences = 0; + let start; + let failure; + + try { + start = process.hrtime.bigint(); + const durationNanoseconds = BigInt(Math.ceil(duration * 1e9)); + do { + const request = generateRequest(random, cases); + const [javascriptResponse, goResponse] = await withTimeout( + Promise.all([exchange(javascript, request), exchange(go, request)]), + 5_000, + `timeout waiting for ${request.id}`, + ); + cases++; + if (!isDeepStrictEqual(javascriptResponse, goResponse)) { + divergences++; + onDivergence(JSON.stringify({ request, javascript: javascriptResponse, go: goResponse })); + } + } while (process.hrtime.bigint() - start < durationNanoseconds); + } catch (error) { + failure = error; + } + + const stopped = await Promise.allSettled([stopRunner(javascript), stopRunner(go)]); + if (!failure) failure = stopped.find(({ status }) => status === "rejected")?.reason; + const elapsedSeconds = start ? Number(process.hrtime.bigint() - start) / 1e9 : 0; + const summary = { elapsedSeconds, seed, cases, divergences }; + if (failure) { + failure.summary = summary; + throw failure; + } + return summary; +} + +function printSummary(summary) { + console.log(`duration_seconds: ${summary.elapsedSeconds.toFixed(3)}`); + console.log(`seed: ${summary.seed}`); + console.log(`cases: ${summary.cases}`); + console.log(`divergences: ${summary.divergences}`); +} + +async function main() { + let options = defaults; + try { + options = parseOptions(process.argv.slice(2)); + const summary = await runHarness({ ...options, onDivergence: console.log }); + printSummary(summary); + if (summary.divergences) process.exitCode = 1; + } catch (error) { + console.error(error.message); + printSummary(error.summary ?? { elapsedSeconds: 0, seed: options.seed, cases: 0, divergences: 0 }); + process.exitCode = 1; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) await main(); diff --git a/fuzz/harness.test.mjs b/fuzz/harness.test.mjs new file mode 100644 index 00000000..0537c46a --- /dev/null +++ b/fuzz/harness.test.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createPrng, generateRequest, parseOptions, runHarness } from "./harness.mjs"; + +const methods = { + output: new Set([ + "toHex", "toHex8", "toHexString", "toHex8String", "toRgbString", + "toPercentageRgbString", "toHslString", "toHsvString", "toString", + "toName", "toFilter", + ]), + modify: new Set(["lighten", "brighten", "darken", "saturate", "desaturate", "greyscale", "spin"]), + palette: new Set(["complement", "splitcomplement", "triad", "tetrad", "analogous", "monochromatic"]), +}; +const operations = new Set(["inspect", "output", "modify", "mix", "readability", "isReadable", "palette"]); + +test("seed 1 repeats the same PRNG sequence", () => { + const first = createPrng(1); + const second = createPrng(1); + assert.deepEqual(Array.from({ length: 8 }, first), Array.from({ length: 8 }, second)); +}); + +test("request generation is deterministic and adapter-supported", () => { + const first = createPrng(20260801); + const second = createPrng(20260801); + const requests = Array.from({ length: 70 }, (_, index) => generateRequest(first, index)); + + assert.deepEqual(requests, Array.from({ length: 70 }, (_, index) => generateRequest(second, index))); + for (const [index, request] of requests.entries()) { + assert.equal(request.id, `fuzz-${index}`); + assert.ok(operations.has(request.operation), request.operation); + if (methods[request.operation]) assert.ok(methods[request.operation].has(request.args.method), request.args.method); + } + assert.deepEqual(new Set(requests.map(({ operation }) => operation)), operations); +}); + +test("option parsing validates duration and seed", () => { + assert.deepEqual(parseOptions([]), { duration: 60, seed: 20260801 }); + assert.deepEqual(parseOptions(["--duration", "0.25", "--seed", "1"]), { duration: 0.25, seed: 1 }); + for (const args of [ + ["--duration", "0"], + ["--duration", "-1"], + ["--duration", "nope"], + ["--seed", "1.5"], + ["--seed", "nope"], + ]) assert.throws(() => parseOptions(args)); +}); + +test("one-second differential run uses persistent adapters", { timeout: 30_000 }, async () => { + const summary = await runHarness({ duration: 1, seed: 1, onDivergence() {} }); + assert.ok(summary.elapsedSeconds >= 1, summary.elapsedSeconds); + assert.ok(summary.cases > 0, summary.cases); + assert.equal(summary.divergences, 0); +}); diff --git a/src/cmd/pow-debug/main.go b/src/cmd/pow-debug/main.go new file mode 100644 index 00000000..6909c8f4 --- /dev/null +++ b/src/cmd/pow-debug/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "math" +) + +func main() { + for channel := 11; channel < 256; channel++ { + normalized := float64(channel) / 255 + fmt.Printf("%d %x\n", channel, math.Float64bits(math.Pow((normalized+.055)/1.055, 2.4))) + } +} diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 005498f0..a4a40f31 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -144,6 +144,20 @@ func TestReadability(t *testing.T) { } } +func TestReadabilityMatchesJavaScriptPrecision(t *testing.T) { + first, _ := FromCompat(map[string]any{"r": 127.0, "g": 64.0, "b": 15.0, "a": 0.0}, false) + second, _ := FromCompat("#80007f", false) + for _, channel := range []float64{128, 127} { + normalized := channel / 255 + base := (normalized + .055) / 1.055 + linear := math.Pow(base, 2.4) + t.Logf("channel %.0f normalized %.17g pow %.17g/%x exp-log %.17g/%x exp2-log2 %.17g/%x", channel, normalized, linear, math.Float64bits(linear), math.Exp(2.4*math.Log(base)), math.Float64bits(math.Exp(2.4*math.Log(base))), math.Exp2(2.4*math.Log2(base)), math.Float64bits(math.Exp2(2.4*math.Log2(base)))) + } + if got := Readability(first, second); got != 1.188086751976723 { + t.Fatalf("Readability precision = %.17g", got) + } +} + func TestIsReadable(t *testing.T) { for _, test := range []struct { name string From 0e54f1ab7178b74311b6594b42735a2b8dbaf4ae Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:46:53 +0530 Subject: [PATCH 058/165] fix(05-02): match exact fuzz parity --- .planning/debug/fuzz-readability-precision.md | 20 ++++----- DECISIONS.md | 3 +- src/cmd/pow-debug/main.go | 13 ------ src/tinycolor/color.go | 29 +++++++----- src/tinycolor/luminance.go | 45 +++++++++++++++++++ src/tinycolor/operations_test.go | 14 +++--- 6 files changed, 82 insertions(+), 42 deletions(-) delete mode 100644 src/cmd/pow-debug/main.go create mode 100644 src/tinycolor/luminance.go diff --git a/.planning/debug/fuzz-readability-precision.md b/.planning/debug/fuzz-readability-precision.md index 1cbb524b..3383d91c 100644 --- a/.planning/debug/fuzz-readability-precision.md +++ b/.planning/debug/fuzz-readability-precision.md @@ -1,16 +1,16 @@ --- -status: investigating +status: resolved trigger: "Investigate and fix next deterministic Phase 5 fuzz parity mismatch: readability for {r:127,g:64,b:15,a:0} against '#80007f' returns 1.188086751976723 in JavaScript and 1.1880867519767233 in Go." created: 2026-08-01T00:00:00+05:30 -updated: 2026-08-01T00:04:00+05:30 +updated: 2026-08-01T06:00:00+05:30 --- ## Current Focus -hypothesis: confirmed: V8 Math.pow and Go math.Pow use different algorithms whose final rounding differs across the finite 8-bit luminance channel domain -test: inspect the primary V8 implementation and compare all 245 nonlinear channel values to select the smallest exact shared-method implementation -expecting: algorithm evidence plus the bounded mismatch distribution will show whether a small compatible calculation exists or an exact transfer table is required -next_action: inspect V8 ieee754 pow source and evaluate exact implementation options +hypothesis: resolved: exact V8-derived results are required for the finite 8-bit luminance domain +test: exact readability regression, 71-case operations corpus, and broad seeded fuzz harness +expecting: exact parity with no tolerance or output rounding +next_action: record and validate the required 60-second seed-20260801 fuzz log ## Symptoms @@ -55,7 +55,7 @@ started: uncovered after the fuzz saturation root-cause fix. ## Resolution -root_cause: -fix: -verification: -files_changed: [] +root_cause: V8 Math.pow and Go math.Pow produce different final float64 bits for 93 of TinyColor's 245 nonlinear 8-bit channel values. +fix: Color.Luminance now uses a finite V8-derived 256-entry transfer table. The follow-on palette divergences were fixed at their shared constructor boundary by applying TinyColor's sub-one channel rounding and bound01 normalization. +verification: The exact regression passed; all 71 operations cases matched; the full local verify equivalent passed; seed 1 ran for 1.008 seconds across 14,778 cases with zero divergences. +files_changed: [src/tinycolor/luminance.go, src/tinycolor/color.go, src/tinycolor/operations_test.go, src/cmd/pow-debug/main.go] diff --git a/DECISIONS.md b/DECISIONS.md index acb5813d..96b472d3 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -8,4 +8,5 @@ | D-004 | Use JSON Lines adapters over stdin/stdout. | Node and Go can compare dynamic inputs and operations without modifying source tests. | Accepted | | D-005 | Use Go standard library first. | The port's algorithms are small and self-contained; dependencies add compatibility and supply-chain surface. | Accepted | | D-006 | Preserve source quirks at the adapter boundary; expose an idiomatic Go API separately. | Compatibility and Go usability are both required, but they should not leak dynamic JavaScript semantics into every caller. | Accepted | -| D-007 | Deno's source suite is a required later check, not a current pass. | `deno` is not installed in the checked environment as of 2026-08-01. | Accepted | +| D-007 | Run the immutable Deno source suite explicitly as `deno test test.js`. | Deno 2 broad discovery includes generated npm and Node port tests outside the pinned source suite. | Accepted | +| D-008 | Use a finite V8-derived luminance transfer table. | TinyColor rounds RGB to 8-bit channels before luminance, while Go `math.Pow` differs from V8 for 93 nonlinear values; the table preserves exact parity without tolerance. | Accepted | diff --git a/src/cmd/pow-debug/main.go b/src/cmd/pow-debug/main.go deleted file mode 100644 index 6909c8f4..00000000 --- a/src/cmd/pow-debug/main.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "fmt" - "math" -) - -func main() { - for channel := 11; channel < 256; channel++ { - normalized := float64(channel) / 255 - fmt.Printf("%d %x\n", channel, math.Float64bits(math.Pow((normalized+.055)/1.055, 2.4))) - } -} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 2728581a..4c7a8e5a 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -33,10 +33,22 @@ type WCAG2Options struct { } func FromCompat(input any, fromRatio bool) (Color, error) { + var model color.Model if fromRatio { - return Color{model: parser.ParseFromRatio(input)}, nil + model = parser.ParseFromRatio(input) + } else { + model = parser.Parse(input) + } + if model.R < 1 { + model.R = math.Floor(model.R + .5) + } + if model.G < 1 { + model.G = math.Floor(model.G + .5) } - return Color{model: parser.Parse(input)}, nil + if model.B < 1 { + model.B = math.Floor(model.B + .5) + } + return Color{model: model}, nil } func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) { c, e := FromCompat(input, fromRatio) @@ -170,14 +182,7 @@ func (c Color) ToFilter(second *Color, gradient bool) string { func (c Color) Brightness() float64 { x := c.ToRGB(); return float64(x.R*299+x.G*587+x.B*114) / 1000 } func (c Color) Luminance() float64 { x := c.ToRGB() - f := func(v int) float64 { - z := float64(v) / 255 - if z <= .03928 { - return z / 12.92 - } - return math.Pow((z+.055)/1.055, 2.4) - } - return .2126*f(x.R) + .7152*f(x.G) + .0722*f(x.B) + return .2126*luminanceChannel(x.R) + .7152*luminanceChannel(x.G) + .0722*luminanceChannel(x.B) } func Readability(first, second Color) float64 { firstLuminance, secondLuminance := first.Luminance(), second.Luminance() @@ -303,7 +308,7 @@ func roundedAlpha(alpha float64) string { } func rgbToHSL(r, g, b float64) (float64, float64, float64) { - r, g, b = r/255, g/255, b/255 + r, g, b = color.Bound01(r, 255), color.Bound01(g, 255), color.Bound01(b, 255) max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) l := (max + min) / 2 if max == min { @@ -327,7 +332,7 @@ func rgbToHSL(r, g, b float64) (float64, float64, float64) { } func rgbToHSV(r, g, b float64) (float64, float64, float64) { - r, g, b = r/255, g/255, b/255 + r, g, b = color.Bound01(r, 255), color.Bound01(g, 255), color.Bound01(b, 255) max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) d := max - min if max == 0 { diff --git a/src/tinycolor/luminance.go b/src/tinycolor/luminance.go new file mode 100644 index 00000000..5dfa5707 --- /dev/null +++ b/src/tinycolor/luminance.go @@ -0,0 +1,45 @@ +package tinycolor + +import "math" + +// TinyColor rounds channels to 8-bit values before applying Math.pow. These +// V8-derived results preserve exact JavaScript behavior where Go math.Pow +// rounds differently. +var luminanceBits = [...]uint64{ + 0x0000000000000000, 0x3f33e45677c176f7, 0x3f43e45677c176f7, 0x3f4dd681b3a23272, 0x3f53e45677c176f7, 0x3f58dd6c15b1d4b4, 0x3f5dd681b3a23272, 0x3f6167cba8c94818, + 0x3f63e45677c176f7, 0x3f6660e146b9a5d5, 0x3f68dd6c15b1d4b4, 0x3f6b6a31b5259c99, 0x3f6e1e31d70c99dd, 0x3f707c38bf8583a9, 0x3f71fcc2beed6421, 0x3f7390ffaf95e279, + 0x3f753936cc7bc928, 0x3f76f5addb50c915, 0x3f78c6a94031b561, 0x3f7aac6c0fb97351, 0x3f7ca7381f9f602b, 0x3f7eb74e160978d0, 0x3f806e76bbda92b8, 0x3f818c2a5a8a8044, + 0x3f82b4e09b3f0ae3, 0x3f83e8b7b3bde965, 0x3f8527cd60af8b85, 0x3f86723eea8d3709, 0x3f87c8292a3db6b3, 0x3f8929a88d67b521, 0x3f8a96d91a8016bd, 0x3f8c0fd67499fab6, + 0x3f8d94bbdefd740e, 0x3f8f25a44089883f, 0x3f9061551372c694, 0x3f9135f3e4c2cce2, 0x3f9210bb8642b172, 0x3f92f1b8c1ae46bd, 0x3f93d8f839b79c0b, 0x3f94c6866b3e9fa4, + 0x3f95ba6fae794313, 0x3f96b4c0380d2def, 0x3f97b5841a1bf3ac, 0x3f98bcc74542addb, 0x3f99ca95898dc8b5, 0x3f9adefa9761c020, 0x3f9bfa0200597bd9, 0x3f9d1bb7381aec1f, + 0x3f9e442595227bca, 0x3f9f73585185e1b5, 0x3fa054ad45d76878, 0x3fa0f31ba386ff26, 0x3fa194fcb663747b, 0x3fa23a55e62a662a, 0x3fa2e32c8e148d11, 0x3fa38f85fd21eacf, + 0x3fa43f67766310ff, 0x3fa4f2d6313fa8d0, 0x3fa5a9d759ba5ed0, 0x3fa6647010b254ee, 0x3fa722a56c2239ee, 0x3fa7e47c775d2427, 0x3fa8a9fa33494b07, 0x3fa973239698b9cc, + 0x3faa3ffd8e001389, 0x3fab108cfc6b7fbc, 0x3fabe4d6bb31d522, 0x3facbcdf9a4616f2, 0x3fad98ac60675833, 0x3fae7841cb4f16df, 0x3faf5ba48fde2048, 0x3fb0216cad240765, + 0x3fb096f2671eb815, 0x3fb10e65c38a5192, 0x3fb187c90bf8bce2, 0x3fb2031e85f5d6da, 0x3fb28068731a1952, 0x3fb2ffa9111cb94b, 0x3fb380e299e53f92, 0x3fb40417439ca10f, + 0x3fb4894940bddbfb, 0x3fb5107ac0261e59, 0x3fb599aded247aac, 0x3fb624e4ef892ed4, 0x3fb6b221ebb4817e, 0x3fb7416702a539d1, 0x3fb7d2b65206b527, 0x3fb86611f43e9e6a, + 0x3fb8fb7c007a4a70, 0x3fb992f68abbbc89, 0x3fba2c83a3e6566d, 0x3fbac82559cb3644, 0x3fbb65ddb7354604, 0x3fbc05aec3f4fe5e, 0x3fbca79a84ebe030, 0x3fbd4ba2fc17a6a5, + 0x3fbdf1ca289d34b8, 0x3fbe9a1206d34003, 0x3fbf447c904cbb4e, 0x3fbff10bbbe302c2, 0x3fc04fe0bedfe5f1, 0x3fc0a84fe3b36d8f, 0x3fc101d443dfc06f, 0x3fc15c6ed58eefdf, + 0x3fc1b8208da5fef0, 0x3fc214ea5fc9514a, 0x3fc272cd3e610123, 0x3fc2d1ca1a9d1cfb, 0x3fc331e1e479cdf5, 0x3fc393158ac3674e, 0x3fc3f565fb1a5fd5, 0x3fc458d421f735df, + 0x3fc4bd60eaae3e73, 0x3fc5230d3f736034, 0x3fc589da095dbaa1, 0x3fc5f1c8306b3a3c, 0x3fc65ad89b841a2b, 0x3fc6c50c307e53bf, 0x3fc73063d420fc80, 0x3fc79ce06a279303, + 0x3fc80a82d5453b5d, 0x3fc8794bf727eb3f, 0x3fc8e93cb07b8679, 0x3fc95a55e0ecec0b, 0x3fc9cc98672cf47e, 0x3fca400520f3619c, 0x3fcab49ceb01c003, 0x3fcb2a60a1263b0a, + 0x3fcba1511e3e632d, 0x3fcc196f3c39e76f, 0x3fcc92bbd41d41fe, 0x3fcd0d37be045851, 0x3fcd88e3d1250f68, 0x3fce05c0e3d1d3e0, 0x3fce83cfcb7c16f0, 0x3fcf03115cb6bfd3, + 0x3fcf83866b38924d, 0x3fd00297e4ef4553, 0x3fd044072557177a, 0x3fd086115f6beb3a, 0x3fd0c8b6fb5c735e, 0x3fd10bf860ef039a, 0x3fd14fd5f782a5a6, 0x3fd1945026102997, + 0x3fd1d967532b31b1, 0x3fd21f1be50339e7, 0x3fd2656e41649ae3, 0x3fd2ac5ecdb988f8, 0x3fd2f3edef0b0ed8, 0x3fd33c1c0a020438, 0x3fd384e982e800b1, 0x3fd3ce56bda84a81, + 0x3fd418641dd0c1bc, 0x3fd463120692c7af, 0x3fd4ae60dac4229d, 0x3fd4fa50fcdfde15, 0x3fd546e2cf0727a9, 0x3fd59416b3022858, 0x3fd5e1ed0a40daab, 0x3fd6306635dbdd7b, + 0x3fd67f82969543a2, 0x3fd6cf428cd96079, 0x3fd71fa678bf915d, 0x3fd770aeba0b042a, 0x3fd7c25bb02b7ac5, 0x3fd814adba3e0bd9, 0x3fd867a5370de0b1, 0x3fd8bb428514f067, + 0x3fd90f86027cb84e, 0x3fd964700d1ef1b1, 0x3fd9ba0102864521, 0x3fda10393feefafd, 0x3fda67192247a9be, 0x3fdabea10631e195, 0x3fdb16d14802d5ca, 0x3fdb6faa43c403bb, + 0x3fdbc92c5533d785, 0x3fdc2357d7c64e5d, 0x3fdc7e2d26a596de, 0x3fdcd9ac9cb2aef2, 0x3fdd35d69485ffc5, 0x3fdd92ab686ff782, 0x3fddf02b7279a10d, 0x3fde4e570c6539c5, + 0x3fdead2e8faec526, 0x3fdf0cb2558c9ea4, 0x3fdf6ce2b6f00983, 0x3fdfcdc00c85bec2, 0x3fe017a5575b3cb2, 0x3fe048c17ad3c04b, 0x3fe07a349c9d9837, 0x3fe0abfee888c050, + 0x3fe0de208a4444c8, 0x3fe11099ad5e83eb, 0x3fe1436a7d456eef, 0x3fe176932546ca12, 0x3fe1aa13d0906bda, 0x3fe1ddecaa307b85, 0x3fe2121ddd15aece, 0x3fe246a7940f86d1, + 0x3fe27b89f9ce8c4b, 0x3fe2b0c538e48b07, 0x3fe2e6597bc4cca0, 0x3fe31c46ecc4528d, 0x3fe3528db61a0f73, 0x3fe3892e01df1fcc, 0x3fe3c027fa0f01eb, 0x3fe3f77bc887cd3b, + 0x3fe42f29970a68f8, 0x3fe467318f3ac22d, 0x3fe49f93daa00113, 0x3fe4d850a2a4bde1, 0x3fe51168109734e5, 0x3fe54ada4da97a1b, 0x3fe584a782f1ac23, 0x3fe5becfd96a2698, + 0x3fe5f95379f1b3ed, 0x3fe634328d4bbe97, 0x3fe66f6d3c2081cf, 0x3fe6ab03aefd39aa, 0x3fe6e6f60e5452b1, 0x3fe72344827d98f6, 0x3fe75fef33b6669b, 0x3fe79cf64a21d1e2, + 0x3fe7da59edc8dab0, 0x3fe8181a469a9787, 0x3fe856377c6c6224, 0x3fe894b1b6fa0377, 0x3fe8d3891de5df49, 0x3fe912bdd8b91f45, 0x3fe952500ee3dda5, 0x3fe9923fe7bd4f67, + 0x3fe9d28d8a83edfc, 0x3fea13391e5da09f, 0x3fea5442ca57e52e, 0x3fea95aab567f88f, 0x3fead771066afec2, 0x3feb1995e4262a69, 0x3feb5c197546e3f8, 0x3feb9efbe062f086, + 0x3febe23d4bf8981b, 0x3fec25ddde6ecbbb, 0x3fec69ddbe154af1, 0x3fecae3d1124c90b, 0x3fecf2fbfdbf11f1, 0x3fed381aa9ef2e82, 0x3fed7d993ba988d4, 0x3fedc377d8cc0fd5, + 0x3fee09b6a71e5aa6, 0x3fee5055cc51cbb4, 0x3fee97556e01b351, 0x3feedeb5b1b37216, 0x3fef2676bcd69ade, 0x3fef6e98b4c51466, 0x3fefb71bbec33ab2, 0x3ff0000000000000, +} + +func luminanceChannel(channel int) float64 { + return math.Float64frombits(luminanceBits[channel]) +} diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index a4a40f31..24c20323 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -147,12 +147,6 @@ func TestReadability(t *testing.T) { func TestReadabilityMatchesJavaScriptPrecision(t *testing.T) { first, _ := FromCompat(map[string]any{"r": 127.0, "g": 64.0, "b": 15.0, "a": 0.0}, false) second, _ := FromCompat("#80007f", false) - for _, channel := range []float64{128, 127} { - normalized := channel / 255 - base := (normalized + .055) / 1.055 - linear := math.Pow(base, 2.4) - t.Logf("channel %.0f normalized %.17g pow %.17g/%x exp-log %.17g/%x exp2-log2 %.17g/%x", channel, normalized, linear, math.Float64bits(linear), math.Exp(2.4*math.Log(base)), math.Float64bits(math.Exp(2.4*math.Log(base))), math.Exp2(2.4*math.Log2(base)), math.Float64bits(math.Exp2(2.4*math.Log2(base)))) - } if got := Readability(first, second); got != 1.188086751976723 { t.Fatalf("Readability precision = %.17g", got) } @@ -284,6 +278,14 @@ func TestPaletteCustomizationsAndIndependence(t *testing.T) { } } +func TestPaletteConversionUsesTinyColorChannelBounds(t *testing.T) { + color, _ := FromCompat("rgb(127, 0, 255)", false) + converted := color.Tetrad()[1] + if got := converted.ToHSLString(); got != "hsl(0, 100%, 50%)" { + t.Fatalf("tetrad wrapped hue = %s", got) + } +} + func paletteHex(colors []Color) string { values := make([]string, len(colors)) for index, color := range colors { From ad69b3e651aaa18fb9223dc73ed9104955ad4ebd Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:50:19 +0530 Subject: [PATCH 059/165] test(05-02): record 60-second fuzz session --- Makefile | 2 ++ fuzz/README.md | 6 ++++++ fuzz/log.txt | 4 ++++ fuzz/validate-log.mjs | 38 ++++++++++++++++++++++++++++++++++++ fuzz/validate-log.test.mjs | 40 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+) create mode 100644 fuzz/log.txt create mode 100644 fuzz/validate-log.mjs create mode 100644 fuzz/validate-log.test.mjs diff --git a/Makefile b/Makefile index f76edc82..2bc4395f 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ build: test: node tests/port/adapter.test.mjs node --test tests/original/verify.test.mjs + node --test fuzz/harness.test.mjs fuzz/validate-log.test.mjs + node fuzz/validate-log.mjs fuzz/log.txt go -C src test ./... node compat/run.mjs compat/cases/smoke.jsonl node compat/run.mjs compat/cases/parser-hex-rgb.jsonl diff --git a/fuzz/README.md b/fuzz/README.md index db962269..45dd690a 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -15,6 +15,12 @@ Full evidence run: node fuzz/harness.mjs --duration 60 --seed 20260801 ``` +Validate the checked-in evidence: + +```text +node fuzz/validate-log.mjs fuzz/log.txt +``` + Rerun a failure with the logged seed and duration. Each divergence is a JSON object containing the request and both responses; the four final lines report duration, seed, case count, and divergence count. diff --git a/fuzz/log.txt b/fuzz/log.txt new file mode 100644 index 00000000..d0f6498e --- /dev/null +++ b/fuzz/log.txt @@ -0,0 +1,4 @@ +duration_seconds: 60.012 +seed: 20260801 +cases: 1091630 +divergences: 0 diff --git a/fuzz/validate-log.mjs b/fuzz/validate-log.mjs new file mode 100644 index 00000000..7afe12d6 --- /dev/null +++ b/fuzz/validate-log.mjs @@ -0,0 +1,38 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +export function validateLog(text) { + const read = (name) => { + const matches = [...text.matchAll(new RegExp(`^${name}: (.+)$`, "gm"))]; + if (matches.length !== 1) throw new Error(`${name} must appear exactly once`); + const value = Number(matches[0][1]); + if (!Number.isFinite(value)) throw new Error(`${name} must be numeric`); + return value; + }; + const result = { + duration_seconds: read("duration_seconds"), + seed: read("seed"), + cases: read("cases"), + divergences: read("divergences"), + }; + if (result.duration_seconds < 60) throw new Error("duration_seconds must be at least 60"); + if (!Number.isSafeInteger(result.seed) || result.seed !== 20260801) throw new Error("seed must be 20260801"); + if (!Number.isSafeInteger(result.cases) || result.cases <= 0) throw new Error("cases must be a positive integer"); + if (!Number.isSafeInteger(result.divergences) || result.divergences !== 0) throw new Error("divergences must be zero"); + return result; +} + +function main() { + try { + const path = process.argv[2]; + if (!path) throw new Error("usage: node fuzz/validate-log.mjs "); + validateLog(readFileSync(path, "utf8")); + console.log("fuzz log verified"); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main(); diff --git a/fuzz/validate-log.test.mjs b/fuzz/validate-log.test.mjs new file mode 100644 index 00000000..6e9afa35 --- /dev/null +++ b/fuzz/validate-log.test.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { validateLog } from "./validate-log.mjs"; + +const valid = `duration_seconds: 60.001 +seed: 20260801 +cases: 1 +divergences: 0 +`; + +test("accepts a complete zero-divergence 60-second log", () => { + assert.deepEqual(validateLog(valid), { + duration_seconds: 60.001, + seed: 20260801, + cases: 1, + divergences: 0, + }); +}); + +test("rejects invalid evidence fields", () => { + for (const [name, value] of [ + ["duration_seconds", "59.999"], + ["duration_seconds", "nope"], + ["seed", "1"], + ["seed", "1.5"], + ["cases", "0"], + ["cases", "1.5"], + ["divergences", "1"], + ["divergences", "nope"], + ]) { + assert.throws(() => validateLog(valid.replace(new RegExp(`^${name}: .+$`, "m"), `${name}: ${value}`)), name); + } +}); + +test("rejects every missing evidence field", () => { + for (const name of ["duration_seconds", "seed", "cases", "divergences"]) { + assert.throws(() => validateLog(valid.replace(new RegExp(`^${name}: .+\\n`, "m"), "")), name); + } +}); From 145c1b57ace6f86595c631d30387f5f7d2e93a37 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:52:09 +0530 Subject: [PATCH 060/165] wip: delivery evidence paused at task 3/3 --- .planning/HANDOFF.json | 98 ++++------- .planning/STATE.md | 52 +++--- .../05-delivery-evidence/.continue-here.md | 154 +++--------------- 3 files changed, 92 insertions(+), 212 deletions(-) diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json index 93b58754..2babbf40 100644 --- a/.planning/HANDOFF.json +++ b/.planning/HANDOFF.json @@ -1,111 +1,85 @@ { "version": "1.0", - "timestamp": "2026-08-01T00:04:30.289Z", + "timestamp": "2026-08-01T00:20:50.023Z", "phase": "5", "phase_name": "Delivery Evidence", "phase_dir": ".planning/phases/05-delivery-evidence", "plan": 2, - "task": 1, + "task": 3, "total_tasks": 3, "status": "paused", "completed_tasks": [ { - "id": "05-01-01", - "name": "Human CLI", - "status": "done", - "commit": "df1f9b8" - }, - { - "id": "05-01-02", - "name": "Reproducible verification gate", + "id": "05-02-01", + "name": "Deterministic persistent-process differential fuzz harness", "status": "done", - "commit": "a4ad85e" + "commit": "6b4506a, 0e54f1a" }, { - "id": "05-01-03", - "name": "Port CI and source suite", + "id": "05-02-02", + "name": "Validated 60-second fuzz survivor evidence", "status": "done", - "commit": "d8ba485" - }, - { - "id": "05-02-01", - "name": "Differential fuzz harness", - "status": "in_progress", - "progress": "Persistent seeded harness is implemented but deliberately uncommitted/blocked by an exact readability precision mismatch. First saturation mismatch was fixed in 0a9db8b." + "commit": "ad69b3e" } ], "remaining_tasks": [ { - "id": "05-02-01", - "name": "Finish broad fuzz harness after readability parity fix", - "status": "in_progress" + "id": "05-02-03", + "name": "Measure shared-workload p99, RSS, startup, and throughput", + "status": "not_started" }, { - "id": "05-02-02", - "name": "Validate and record real 60-second fuzz log", + "id": "05-03-01", + "name": "Write judge-facing port documentation and decision log", "status": "not_started" }, { - "id": "05-02-03", - "name": "Generate shared-workload benchmark evidence", + "id": "05-03-02", + "name": "Refresh honest parity, coverage, safety, benchmark, and bonus evidence", "status": "not_started" }, { - "id": "05-03", - "name": "Submission documentation, evidence refresh, external checkpoint, and phase closeout", + "id": "05-03-03", + "name": "Run final clean local gate and write verification evidence", "status": "not_started" - } - ], - "blockers": [ + }, { - "description": "Exact readability mismatch: JavaScript 1.188086751976723 versus Go 1.1880867519767233 for {r:127,g:64,b:15,a:0} against #80007f.", - "type": "technical", - "workaround": "None allowed. Do not add tolerance or narrow fuzz inputs. Continue .planning/debug/fuzz-readability-precision.md; root cause is V8 Math.pow versus Go math.Pow final rounding. An exact bounded 256-entry transfer lookup is the leading safe fix." + "id": "05-03-04", + "name": "Verify public clone access and successful CI", + "status": "not_started" }, { - "description": "The WIP branch intentionally contains a failing exact readability regression and fuzz zero-divergence test.", - "type": "technical", - "workaround": "Fix the shared Luminance transfer function, run the exact regression and operations corpus, then rerun the broad seed-1 fuzz smoke before committing production completion." + "id": "05-03-05", + "name": "Close Phase 5 planning state after external proof", + "status": "not_started" } ], + "blockers": [], "human_actions_pending": [ { - "action": "Record and publish the five-minute demo video, then add its URL.", - "context": "docs/DEMO.md is still a future Plan 05-03 deliverable; no video is claimed.", + "action": "Record and publish the five-minute demo video, then add its URL during Plan 05-03.", + "context": "The submission requires a live demonstration; no video is currently claimed.", "blocking": true } ], "decisions": [ { - "decision": "Keep broad bounded fuzz generation and stop on every exact mismatch.", - "rationale": "Narrowing to corpus-proven inputs would hide defects and violate behavioral-equivalence scoring.", + "decision": "Use a finite V8-derived 256-entry luminance transfer table.", + "rationale": "Go math.Pow differs from V8 for 93 nonlinear 8-bit values, and exact parity forbids tolerance.", "phase": "5" }, { - "decision": "Pin oracle checkout bytes to CRLF with .gitattributes instead of normalizing hashes.", - "rationale": "The kickoff manifest hashes raw CRLF working-tree bytes; CI must reproduce that byte contract.", + "decision": "Apply TinyColor's sub-one channel rounding at the FromCompat constructor boundary.", + "rationale": "One shared boundary fixes palette conversions without per-operation exceptions.", "phase": "5" }, { - "decision": "Run `deno test test.js` explicitly in CI.", - "rationale": "Deno 2 removed broad discovery exclusions and otherwise includes generated npm and Node port tests outside the immutable source suite.", - "phase": "5" - }, - { - "decision": "Prefer an exact finite-domain luminance transfer lookup if no smaller V8-compatible power implementation exists.", - "rationale": "TinyColor rounds luminance channels to 8-bit integers; Go math.Pow differs from V8 for 93 of 245 nonlinear channels and heuristic ULP correction is incorrect.", + "decision": "Keep the fuzz generator broad and compare parsed responses exactly.", + "rationale": "The broad seed exposed both arithmetic and constructor parity defects that a narrowed corpus would hide.", "phase": "5" } ], - "uncommitted_files": [ - "compat/cases/operations.jsonl", - "fuzz/README.md", - "fuzz/harness.mjs", - "fuzz/harness.test.mjs", - "src/tinycolor/operations_test.go", - "src/cmd/pow-debug/main.go", - ".planning/debug/fuzz-readability-precision.md" - ], - "next_action": "Run `$gsd-resume-work`, read `.planning/debug/fuzz-readability-precision.md`, finish the exact luminance transfer fix with the existing RED tests, then run `node fuzz/harness.mjs --duration 1 --seed 1` without narrowing the generator.", - "context_notes": "Branch rajeet. GitHub Actions run 30673847990 passed make verify, deno test test.js, build, and artifact upload. Local Deno 2.9.4 ran the immutable suite: 45 passed, 0 failed, 1 ignored. Current broad fuzz smoke is expected red: 12,291 cases and 388 divergences before the readability fix." + "uncommitted_files": [], + "next_action": "Run `$gsd-resume-work`, then execute task 05-02-03 from `.planning/phases/05-delivery-evidence/05-02-PLAN.md`: build the shared benchmark workload and runner with tests first, write real results once, and create 05-02-SUMMARY.md only after benchmark verification passes.", + "context_notes": "Branch rajeet. Exact fuzz parity is resolved. The real checked-in log reports 60.012 seconds, seed 20260801, 1,091,630 cases, and zero divergences. Local verification passed all Go and Node gates, all five corpora (9/9, 26/26, 23/23, 35/35, 71/71), vet, fuzz tests, log validation, and the immutable Deno suite (45 passed, 0 failed, 1 ignored). Local Windows has no make, so Makefile commands were run directly; GitHub Actions must confirm `make verify` after push. No background process is running." } diff --git a/.planning/STATE.md b/.planning/STATE.md index c57e9539..64398ab1 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,23 +2,35 @@ **Updated:** 2026-08-01 -- Planning initialized from the local `bgrins/TinyColor` checkout. -- JavaScript source and tests are the immutable oracle. -- Node 24.18.0 and Go 1.26.1 are available locally. -- `deno` is not installed; no original-source test pass is claimed. -- Phase 1 completed in commit `78423e3`: Go/Node JSONL runners, protocol tests, - and a fixed corpus passed 9/9 with zero mismatches. This is not whole-library - parity. -- Phase 2 completed locally: the normalized parser covers HEX, RGB(A), HSL(A), - HSV(A), names, objects, invalid input, and FromRatio. Differential evidence: - 26/26 HEX/RGB/name cases, 23/23 complete parser cases, and 9/9 smoke cases. -- The Go module lives in `src/`; `tests/original/manifest.sha256` pins the - unmodified root JavaScript oracle. -- Phase 3 completed locally: conversion/representation and analysis behavior - passed a 35/35 exact corpus; all Phase 1–3 corpora are zero-mismatch. The - compatibility protocol now preserves successful JSON `false` results. -- Phase 4 completed locally: modifiers, Mix, WCAG readability, and palette - operations passed the full gate. Exact differential evidence is 9/9 smoke, - 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 58/58 operations with - zero mismatches. `deno` remains unavailable, so no original-source suite - pass is claimed. +## Current Position + +- Phase 5 of 5: Delivery Evidence +- Plan 05-02, Task 3 of 3: shared-workload benchmark not started +- Plans 1–4 are complete; Phase 5 Plan 05-01 and fuzz Tasks 05-02-01/02 are complete. + +## Verified Evidence + +- Immutable JavaScript oracle hashes: 3/3 verified. +- Exact corpora: smoke 9/9, HEX/RGB 26/26, parser 23/23, conversion 35/35, operations 71/71. +- Go tests and vet pass; Node adapter, verifier, harness, and log-validator tests pass. +- Immutable Deno source suite: 45 passed, 0 failed, 1 ignored. +- Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. + +## Recent Decisions + +- Use the pinned V8-derived 8-bit luminance transfer table for exact `Math.pow` parity. +- Apply TinyColor's sub-one RGB rounding once at the `FromCompat` constructor boundary. +- Keep fuzz inputs broad and comparisons exact; never replace divergences with tolerance. + +## Remaining + +- Implement honest same-host startup p99, latency p99, throughput, and peak RSS benchmarks. +- Complete Plan 05-03 judge-facing documentation and final evidence refresh. +- Verify the pushed GitHub Actions run and public clone workflow. +- Human action: record and publish the five-minute demo video. + +## Session Continuity + +Last session: 2026-08-01T00:20:50.023Z +Stopped at: Plan 05-02 Task 3, ready to implement benchmarks with TDD. +Resume file: `.planning/phases/05-delivery-evidence/.continue-here.md` diff --git a/.planning/phases/05-delivery-evidence/.continue-here.md b/.planning/phases/05-delivery-evidence/.continue-here.md index 650ca199..5bcc38c8 100644 --- a/.planning/phases/05-delivery-evidence/.continue-here.md +++ b/.planning/phases/05-delivery-evidence/.continue-here.md @@ -1,158 +1,52 @@ --- phase: 05-delivery-evidence -plan: 2 -task: 1 +task: 3 total_tasks: 3 status: in_progress -last_updated: 2026-08-01T00:04:30.289Z -branch: rajeet +last_updated: 2026-08-01T00:20:50.023Z --- - -Phase 5 Plan 05-01 is implemented and CI-verified. Plan 05-02 Task 1 is paused -mid-debug: the broad persistent differential harness is implemented, but its -zero-divergence test correctly remains RED because fuzzing found exact port -defects. Do not narrow the generator or add comparison tolerance. - -The branch is intentionally being handed off with WIP files. The first fuzz -defect (`saturate(-100)` rounding) is fixed and committed. The next defect is -an exact WCAG luminance/readability precision mismatch under active diagnosis. - +Plan 05-02 is at Task 3/3. Differential fuzzing is finished and verified; the next task is the shared original-versus-port benchmark. Branch `rajeet` contains the exact parity fix in `0e54f1a` and the validated evidence in `ad69b3e`. -- Phase 5 design, context, research, validation, and three execution plans were - written and independently plan-checked. -- Human CLI commands `parse`, `convert`, `lighten`, `palette`, and `contrast` - were implemented with JSON output while preserving zero-argument JSONL mode. -- Added one-command Make build/verify, oracle hash verification, scratch Docker - artifact, and GitHub Actions delivery workflow. -- Fixed CI raw-byte hash failures by pinning the three oracle files to CRLF in - `.gitattributes`; a simulated `core.autocrlf=false` checkout prints - `verified: 3`. -- Fixed Deno 2 discovery by running the immutable suite explicitly with - `deno test test.js`. -- GitHub Actions run - `https://github.com/rajeet-04/TinyColor/actions/runs/30673847990` passed in - 42 seconds: `make verify`, Deno source suite, build, and artifact upload. -- Local Deno 2.9.4 source suite result: 45 passed, 0 failed, 1 ignored. -- Built the standard-library persistent fuzz harness with seeded broad inputs, - one outstanding request per child, protocol safeguards, and RED/GREEN tests. -- Fixed the first fuzz mismatch in `0a9db8b`: TinyColor HSL percentage - truncation for `saturate(-100)` now matches; operations corpus is 70/70 at - that commit. - +- Task 05-02-01: Persistent seeded differential harness — done. +- Exact readability mismatch fixed with a V8-derived 256-entry transfer table. +- Follow-on palette mismatches fixed at the shared TinyColor constructor boundary. +- One-second seed-1 smoke: 14,778 cases, zero divergences. +- Task 05-02-02: Real 60-second evidence — done. +- `fuzz/log.txt`: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. +- `make verify` now runs harness tests and validates the checked-in fuzz log. +- Local direct verification passed all Node/Go gates and corpora; Deno passed 45, failed 0, ignored 1. -1. Finish Task 05-02-01: - - Continue `.planning/debug/fuzz-readability-precision.md`. - - Existing RED case: `{r:127,g:64,b:15,a:0}` vs `#80007f` returns - JavaScript `1.188086751976723`, Go `1.1880867519767233`. - - Root cause is confirmed inside nonlinear luminance channel expansion: - V8 `Math.pow` and Go `math.Pow` differ in final bits for 93 of the 245 - nonlinear 8-bit channels; two differ by two ULPs. - - `src/tinycolor/operations_test.go` and - `compat/cases/operations.jsonl` contain the failing exact regression. - - `src/cmd/pow-debug/main.go` is a temporary diagnostic that prints Go - channel power bits. - - Leading safe fix: a reproducibly generated exact 256-entry V8 transfer - lookup used by `Color.Luminance`. Do not use `Nextafter`, decimal output - rounding, or tolerance. - - After the exact regression and 71-row operations corpus pass, rerun - `node --test fuzz/harness.test.mjs` and - `node fuzz/harness.mjs --duration 1 --seed 1` broadly. If another - mismatch appears, repeat deterministic regression -> root cause -> micro - commit. -2. Task 05-02-02: add tested fuzz-log validator and record an actual - `--duration 60 --seed 20260801` zero-divergence run. -3. Task 05-02-03: implement shared workload benchmark runner and publish actual - p99 latency, peak RSS, startup, and throughput results without quick-run - overwrite. -4. Plan 05-03: rewrite judge-facing README, expand DECISIONS to 12+ substantive - rows, refresh COMPATIBILITY with exact evidence/coverage/unsafe counts, add - demo script, verify public clone/current CI, and close roadmap/state. -5. Human: record and publish the five-minute demo video; no video is currently - claimed. - +- Task 05-02-03: Implement and run the benchmark specified in `05-02-PLAN.md`. +- After Task 3 passes, create `05-02-SUMMARY.md`. +- Execute Plan 05-03 documentation, honest evidence refresh, final external CI/public-clone proof, and closeout. +- Human: record and publish the required five-minute demo video. -- Broad fuzzing stays broad. A mismatch blocks the bonus and becomes a fixed - regression; inputs are never constrained to make the log green. -- Oracle hashing remains byte-exact. `.gitattributes` reproduces kickoff CRLF - bytes on Linux rather than weakening verification with newline normalization. -- CI runs `deno test test.js`, not broad `deno task test`, because Deno 2 would - discover generated npm and port-owned Node tests. -- Luminance parity needs an exact finite-domain solution. Heuristic ULP - correction is ruled out because channel deltas have both signs. -- Keep micro-commits: regression/root-cause fixes, evidence artifacts, and - debug/archive docs stay separately revertible. - +- Use exact V8-derived luminance bits because heuristic ULP correction is wrong for the finite channel domain. +- Preserve TinyColor's constructor channel rounding in `FromCompat`, where every operation routes through it. +- Keep fuzz comparisons exact and the generator broad; do not add tolerance or remove failing input families. -- Technical blocker: exact V8/Go power rounding mismatch in `Color.Luminance`. -- Expected red checks until fixed: - - `go test ./tinycolor -run TestReadabilityMatchesJavaScriptPrecision` - - operations corpus has the new readability case as its mismatch - - `node --test fuzz/harness.test.mjs` fails only its zero-divergence test - - seed-1 one-second broad run previously produced 12,291 cases / 388 - divergences; first was the readability case above. - +- No technical blocker for benchmarking. +- The demo video remains a human submission action for Plan 05-03. - - -- `fuzz/harness.mjs` — persistent broad differential harness. -- `fuzz/harness.test.mjs` — PRNG/options/generator/end-to-end tests. -- `fuzz/README.md` — reproduction and claim rules. -- `src/tinycolor/operations_test.go` — exact readability RED regression and - temporary numeric logging. -- `compat/cases/operations.jsonl` — adapter RED regression row. -- `.planning/debug/fuzz-readability-precision.md` — active root-cause evidence. -- `src/cmd/pow-debug/main.go` — temporary channel-bit diagnostic. - -These files are preserved in the WIP handoff commit. The temporary logging and -`pow-debug` command may be removed only after the exact solution is verified; -do not delete them before the next agent reads the evidence. - - - - - -- GitHub Actions: PASS — run 30673847990. -- Local Deno: PASS — 45 passed, 0 failed, 1 ignored. -- Oracle hash verifier: PASS — 3 files. -- Fixed corpus through saturation commit: operations 70/70. -- Current WIP: intentionally RED only for unresolved readability precision and - fuzz zero-divergence. -- No background server or watcher is running; all subagents were stopped. - - + +The previous Actions failure was caused by a deliberately committed RED regression. It is now green locally. Windows has no `make`, so all Makefile verification commands were executed directly; confirm the pushed Ubuntu Actions run before treating the external CI checkpoint as complete. No server or watcher is running. + - -Start with: - -```text -$gsd-resume-work -``` - -Then read this file and `.planning/debug/fuzz-readability-precision.md`. Finish -the exact luminance transfer fix with the existing RED tests before changing -the fuzz harness. The first mandatory verification after the fix is: - -```powershell -$env:GOCACHE = (Resolve-Path '.cache\go-build').Path -go -C src test ./tinycolor -run TestReadabilityMatchesJavaScriptPrecision -count=1 -node compat/run.mjs compat/cases/operations.jsonl -node fuzz/harness.mjs --duration 1 --seed 1 -``` - +Start with: run `$gsd-resume-work`, read Task 05-02-03 in `.planning/phases/05-delivery-evidence/05-02-PLAN.md`, then write the benchmark tests before implementing `bench/run.mjs`. Use temporary output for quick tests and write `bench/results.json` only once with the full measured run. From 2532926628eb5e959f1f92e06c2d05b3bdd7cb53 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:53:56 +0530 Subject: [PATCH 061/165] docs(05): record green handoff CI --- .planning/HANDOFF.json | 2 +- .planning/STATE.md | 1 + .planning/phases/05-delivery-evidence/.continue-here.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json index 2babbf40..551ed4d3 100644 --- a/.planning/HANDOFF.json +++ b/.planning/HANDOFF.json @@ -81,5 +81,5 @@ ], "uncommitted_files": [], "next_action": "Run `$gsd-resume-work`, then execute task 05-02-03 from `.planning/phases/05-delivery-evidence/05-02-PLAN.md`: build the shared benchmark workload and runner with tests first, write real results once, and create 05-02-SUMMARY.md only after benchmark verification passes.", - "context_notes": "Branch rajeet. Exact fuzz parity is resolved. The real checked-in log reports 60.012 seconds, seed 20260801, 1,091,630 cases, and zero divergences. Local verification passed all Go and Node gates, all five corpora (9/9, 26/26, 23/23, 35/35, 71/71), vet, fuzz tests, log validation, and the immutable Deno suite (45 passed, 0 failed, 1 ignored). Local Windows has no make, so Makefile commands were run directly; GitHub Actions must confirm `make verify` after push. No background process is running." + "context_notes": "Branch rajeet. Exact fuzz parity is resolved. The real checked-in log reports 60.012 seconds, seed 20260801, 1,091,630 cases, and zero divergences. Local verification passed all Go and Node gates, all five corpora (9/9, 26/26, 23/23, 35/35, 71/71), vet, fuzz tests, log validation, and the immutable Deno suite (45 passed, 0 failed, 1 ignored). GitHub Actions run 30675374085 passed make verify, Deno, build, and artifact upload in 43 seconds. No background process is running." } diff --git a/.planning/STATE.md b/.planning/STATE.md index 64398ab1..ee8e67d8 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -15,6 +15,7 @@ - Go tests and vet pass; Node adapter, verifier, harness, and log-validator tests pass. - Immutable Deno source suite: 45 passed, 0 failed, 1 ignored. - Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. +- GitHub Actions run 30675374085 passed verification, Deno, build, and artifact upload. ## Recent Decisions diff --git a/.planning/phases/05-delivery-evidence/.continue-here.md b/.planning/phases/05-delivery-evidence/.continue-here.md index 5bcc38c8..282aa63c 100644 --- a/.planning/phases/05-delivery-evidence/.continue-here.md +++ b/.planning/phases/05-delivery-evidence/.continue-here.md @@ -44,7 +44,7 @@ Plan 05-02 is at Task 3/3. Differential fuzzing is finished and verified; the ne -The previous Actions failure was caused by a deliberately committed RED regression. It is now green locally. Windows has no `make`, so all Makefile verification commands were executed directly; confirm the pushed Ubuntu Actions run before treating the external CI checkpoint as complete. No server or watcher is running. +The previous Actions failure was caused by a deliberately committed RED regression. GitHub Actions run `30675374085` passed `make verify`, Deno, build, and artifact upload in 43 seconds. Plan 05-03 still requires a final public-clone and CI proof after benchmarks and documentation. No server or watcher is running. From 5fe0f6546c41bd2d5dab43c12b10e7e63aedc661 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 06:02:39 +0530 Subject: [PATCH 062/165] chore: classify upstream JavaScript fixtures --- .gitattributes | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitattributes b/.gitattributes index 5731d7cd..f2b91f67 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,11 @@ mod.js text eol=crlf test.js text eol=crlf tinycolor.js text eol=crlf + +# Keep upstream oracle and published JavaScript copies in the repository for +# differential testing, but exclude them from the implementation language chart. +mod.js linguist-vendored +test.js linguist-vendored +tinycolor.js linguist-vendored +npm/** linguist-vendored +dist/** linguist-generated From 03683ecfff7a2934c73e048c087f9e4c9ef05f3d Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 11:25:53 +0530 Subject: [PATCH 063/165] perf(05-02): publish shared benchmark evidence --- .gitattributes | 1 + Makefile | 2 +- bench/methodology.md | 25 +++++- bench/results.json | 27 ++++++- bench/run.mjs | 179 +++++++++++++++++++++++++++++++++++++++++++ bench/run.test.mjs | 34 ++++++++ bench/workload.jsonl | 9 +++ 7 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 bench/run.mjs create mode 100644 bench/run.test.mjs create mode 100644 bench/workload.jsonl diff --git a/.gitattributes b/.gitattributes index f2b91f67..b82e657e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ mod.js text eol=crlf test.js text eol=crlf tinycolor.js text eol=crlf +bench/*.jsonl text eol=lf # Keep upstream oracle and published JavaScript copies in the repository for # differential testing, but exclude them from the implementation language chart. diff --git a/Makefile b/Makefile index 2bc4395f..d00bbc6c 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ fuzz: node fuzz/harness.mjs --duration 60 --seed 20260801 bench: - node bench/run.mjs + node bench/run.mjs --output bench/results.json clean: node -e "const fs = require('node:fs'); for (const file of ['bin/tinycolor', 'bin/tinycolor.exe']) fs.rmSync(file, { force: true });" diff --git a/bench/methodology.md b/bench/methodology.md index 29dd6d56..2bb44686 100644 --- a/bench/methodology.md +++ b/bench/methodology.md @@ -1,5 +1,24 @@ # Benchmark methodology -Measure the compiled compatibility CLI on a quiet host with the same corpus for -each run. Record median and p99 latency, RSS, startup time, and throughput. -Do not compare results across different hosts or Go versions. +Run `node bench/run.mjs --output bench/results.json` from the repository root. +It builds the Go compatibility CLI once, then sends the same fixed requests in +`bench/workload.jsonl` to the original JavaScript adapter and the Go adapter on +the same host. The workload covers parsing, conversion, mutation, mixing, +readability, and palettes. + +Normal mode takes 20 cold starts and 1,000 persistent requests per +implementation. Cold-start time is process launch through receipt of the first +JSONL response. Persistent latency is one sequential JSONL request/response; +throughput is all persistent requests divided by their total elapsed time. p99 +is the sorted sample at `ceil(0.99 * n) - 1`. + +Peak RSS is sampled after each cold-start response and ten times after the +persistent workload from `/proc//status` on Linux and `Get-Process ... +WorkingSet64` on Windows. Sampling is outside latency and throughput timing. +Platforms without either mechanism record `null` and an `rssLimitation` instead +of estimating a value. + +These are same-host observations, not universal speedup claims. Use +`node bench/run.mjs --quick --output ` only as a smoke check; +quick mode uses 3 cold starts and 30 requests and refuses to overwrite the +committed evidence file. diff --git a/bench/results.json b/bench/results.json index 42e6257d..a4113981 100644 --- a/bench/results.json +++ b/bench/results.json @@ -1,4 +1,27 @@ { - "status": "not_run", - "reason": "Benchmarking is scheduled for the delivery phase." + "generatedAt": "2026-08-01T05:53:35.885Z", + "os": "win32", + "architecture": "x64", + "cpu": "AMD Ryzen 9 8940HX with Radeon Graphics ", + "nodeVersion": "v24.18.0", + "goVersion": "go version go1.26.1 windows/amd64", + "workloadSha256": "3d91bf1f988e4a69173c8413d08ee6ba5b0d4179b9905778e4093da778206f8f", + "samples": { + "coldStarts": 20, + "requests": 1000 + }, + "implementations": { + "javascript": { + "startupP99Ms": 41.4131, + "latencyP99Ms": 0.2795, + "throughputOpsPerSecond": 7834.77401769562, + "peakRssBytes": 42868736 + }, + "go": { + "startupP99Ms": 11.0109, + "latencyP99Ms": 0.1808, + "throughputOpsPerSecond": 16756.790270337297, + "peakRssBytes": 12668928 + } + } } diff --git a/bench/run.mjs b/bench/run.mjs new file mode 100644 index 00000000..bcf8194b --- /dev/null +++ b/bench/run.mjs @@ -0,0 +1,179 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { execFileSync, spawn } from "node:child_process"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { cpus } from "node:os"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import readline from "node:readline"; + +const root = resolve(import.meta.dirname, ".."); +const committedOutput = resolve(root, "bench/results.json"); +const runnerEnv = { ...process.env, GOCACHE: process.env.GOCACHE ?? resolve(root, ".cache", "go-build") }; + +export const percentile99 = (values) => { + assert.ok(values.length > 0, "percentile requires at least one sample"); + const sorted = [...values].sort((a, b) => a - b); + return sorted[Math.ceil(sorted.length * 0.99) - 1]; +}; + +export const parseWorkload = (text) => text.split(/\r?\n/).filter(Boolean).map((line, index) => { + const request = JSON.parse(line); + if (!request.id || !request.operation) throw new Error(`workload line ${index + 1} requires id and operation`); + return request; +}); + +export const validateResults = (results) => { + assert.equal(typeof results.generatedAt, "string"); + assert.ok(results.samples.coldStarts >= 20); + assert.ok(results.samples.requests >= 1000); + for (const name of ["javascript", "go"]) { + const metrics = results.implementations?.[name]; + assert.ok(metrics, `missing ${name} metrics`); + for (const key of ["startupP99Ms", "latencyP99Ms", "throughputOpsPerSecond"]) { + assert.ok(Number.isFinite(metrics[key]) && metrics[key] >= 0, `${name}.${key} must be nonnegative`); + } + assert.ok(metrics.peakRssBytes === null || (Number.isFinite(metrics.peakRssBytes) && metrics.peakRssBytes >= 0)); + } + return true; +}; + +const nsToMs = (nanoseconds) => Number(nanoseconds) / 1e6; +const version = (command, args) => execFileSync(command, args, { cwd: root, encoding: "utf8" }).trim(); +const commands = () => ({ + javascript: [process.execPath, ["compat/js-runner.mjs"]], + go: [resolve(root, "bin", process.platform === "win32" ? "tinycolor.exe" : "tinycolor"), []], +}); + +class Runner { + constructor(command, args) { + this.child = spawn(command, args, { cwd: root, env: runnerEnv, stdio: ["pipe", "pipe", "pipe"] }); + this.pending = []; + this.stderr = ""; + readline.createInterface({ input: this.child.stdout, crlfDelay: Infinity }).on("line", (line) => { + this.pending.shift()?.resolve(JSON.parse(line)); + }); + this.child.stderr.on("data", (chunk) => { this.stderr += chunk; }); + this.child.on("exit", (code) => { + const error = new Error(`runner exited with ${code}: ${this.stderr}`); + for (const pending of this.pending.splice(0)) pending.reject(error); + }); + } + + request(value) { + return new Promise((resolveRequest, reject) => { + if (this.child.exitCode !== null) return reject(new Error(`runner exited: ${this.stderr}`)); + this.pending.push({ resolve: resolveRequest, reject }); + this.child.stdin.write(`${JSON.stringify(value)}\n`, (error) => { + if (error) reject(error); + }); + }); + } + + close() { + if (this.child.exitCode === null) this.child.kill(); + } +} + +const readRss = (pid) => { + try { + if (process.platform === "linux") { + const match = /^VmRSS:\s+(\d+)\s+kB$/m.exec(readFileSync(`/proc/${pid}/status`, "utf8")); + return match ? Number(match[1]) * 1024 : null; + } + if (process.platform === "win32") { + const value = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).WorkingSet64`], { encoding: "utf8" }).trim(); + return Number(value); + } + } catch { + // The child can exit between the response and RSS sample. + } + return null; +}; + +const measure = async (command, args, workload, coldStarts, requests) => { + const startup = []; + let peakRss = 0; + let rssSupported = false; + for (let index = 0; index < coldStarts; index++) { + const started = process.hrtime.bigint(); + const runner = new Runner(command, args); + await runner.request(workload[index % workload.length]); + startup.push(nsToMs(process.hrtime.bigint() - started)); + const rss = readRss(runner.child.pid); + if (rss !== null) { + rssSupported = true; + peakRss = Math.max(peakRss, rss); + } + runner.close(); + } + + const runner = new Runner(command, args); + const latencies = []; + const throughputStarted = process.hrtime.bigint(); + for (let index = 0; index < requests; index++) { + const started = process.hrtime.bigint(); + await runner.request(workload[index % workload.length]); + latencies.push(nsToMs(process.hrtime.bigint() - started)); + } + const totalSeconds = Number(process.hrtime.bigint() - throughputStarted) / 1e9; + for (let index = 0; index < 10; index++) { + const rss = readRss(runner.child.pid); + if (rss !== null) { + rssSupported = true; + peakRss = Math.max(peakRss, rss); + } + await new Promise((resolveTick) => setImmediate(resolveTick)); + } + runner.close(); + + return { + startupP99Ms: percentile99(startup), + latencyP99Ms: percentile99(latencies), + throughputOpsPerSecond: requests / totalSeconds, + peakRssBytes: rssSupported ? peakRss : null, + ...(rssSupported ? {} : { rssLimitation: `RSS sampling is unsupported on ${process.platform}` }), + }; +}; + +const main = async () => { + const args = process.argv.slice(2); + const quick = args.includes("--quick"); + const outputIndex = args.indexOf("--output"); + if (outputIndex !== -1 && !args[outputIndex + 1]) throw new Error("usage: node bench/run.mjs [--quick] [--output ]"); + const output = outputIndex === -1 ? committedOutput : resolve(root, args[outputIndex + 1]); + if (quick && output === committedOutput) throw new Error("--quick must not write bench/results.json"); + + const coldStarts = quick ? 3 : 20; + const requests = quick ? 30 : 1000; + const workloadText = readFileSync(resolve(root, "bench/workload.jsonl"), "utf8").replace(/\r\n/g, "\n"); + const workload = parseWorkload(workloadText); + execFileSync("go", ["-C", "src", "build", "-o", `../bin/${process.platform === "win32" ? "tinycolor.exe" : "tinycolor"}`, "./cmd/tinycolor-compat"], { cwd: root, env: runnerEnv, stdio: "inherit" }); + + const implementations = {}; + for (const [name, [command, commandArgs]] of Object.entries(commands())) { + implementations[name] = await measure(command, commandArgs, workload, coldStarts, requests); + } + const results = { + generatedAt: new Date().toISOString(), + os: process.platform, + architecture: process.arch, + cpu: cpus()[0]?.model ?? "unknown", + nodeVersion: process.version, + goVersion: version("go", ["version"]), + workloadSha256: createHash("sha256").update(workloadText).digest("hex"), + samples: { coldStarts, requests }, + implementations, + }; + if (!quick) validateResults(results); + mkdirSync(dirname(output), { recursive: true }); + writeFileSync(output, `${JSON.stringify(results, null, 2)}\n`); + console.log(`wrote ${output}`); +}; + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/bench/run.test.mjs b/bench/run.test.mjs new file mode 100644 index 00000000..64e84cf0 --- /dev/null +++ b/bench/run.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { parseWorkload, percentile99, validateResults } from "./run.mjs"; + +test("percentile99 selects the nearest-rank p99 sample", () => { + assert.equal(percentile99([1, 2, 3, 4]), 4); +}); + +test("workload parser accepts a representative public request", () => { + assert.deepEqual(parseWorkload('{"id":"x","operation":"inspect","input":"red"}\n'), [ + { id: "x", operation: "inspect", input: "red" }, + ]); +}); + +test("results schema rejects a missing implementation", () => { + assert.throws(() => validateResults({ samples: { coldStarts: 20, requests: 1000 } })); +}); + +test("quick benchmark writes complete measurements to a temporary output", () => { + const output = join(mkdtempSync(join(tmpdir(), "tinycolor-bench-")), "results.json"); + execFileSync(process.execPath, ["bench/run.mjs", "--quick", "--output", output], { stdio: "inherit" }); + const results = JSON.parse(readFileSync(output, "utf8")); + for (const name of ["javascript", "go"]) { + const metrics = results.implementations[name]; + assert.ok(metrics.startupP99Ms >= 0); + assert.ok(metrics.latencyP99Ms >= 0); + assert.ok(metrics.throughputOpsPerSecond > 0); + assert.ok(metrics.peakRssBytes === null || metrics.peakRssBytes >= 0); + } +}); diff --git a/bench/workload.jsonl b/bench/workload.jsonl new file mode 100644 index 00000000..2555599a --- /dev/null +++ b/bench/workload.jsonl @@ -0,0 +1,9 @@ +{"id":"bench-parse-name","operation":"inspect","input":"rebeccapurple"} +{"id":"bench-parse-rgba","operation":"inspect","input":"rgba(12, 34, 56, 0.75)"} +{"id":"bench-convert-hsl","operation":"output","input":"#336699","args":{"method":"toHslString"}} +{"id":"bench-convert-hex8","operation":"output","input":"hsla(210, 50%, 40%, 0.5)","args":{"method":"toHex8String"}} +{"id":"bench-modify","operation":"modify","input":"#80402080","args":{"method":"lighten","amount":20}} +{"id":"bench-mix","operation":"mix","input":"rgba(255,0,0,0.25)","args":{"other":"#0000ff80","amount":35}} +{"id":"bench-readability","operation":"readability","input":"#123456","args":{"other":"#fefefe"}} +{"id":"bench-readable","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":"AA","size":"small"}}} +{"id":"bench-palette","operation":"palette","input":"#336699","args":{"method":"analogous","results":6,"slices":30}} From 9e7e42272832f762c10bd179794ec2b58d84ff11 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 11:28:52 +0530 Subject: [PATCH 064/165] docs(05-02): summarize fuzz and benchmark evidence --- .../05-delivery-evidence/05-02-SUMMARY.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-02-SUMMARY.md diff --git a/.planning/phases/05-delivery-evidence/05-02-SUMMARY.md b/.planning/phases/05-delivery-evidence/05-02-SUMMARY.md new file mode 100644 index 00000000..a5da7be9 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-02-SUMMARY.md @@ -0,0 +1,38 @@ +--- +phase: 5 +plan: 2 +subsystem: fuzz-benchmark-evidence +tags: [fuzz, differential, benchmark, p99, rss] +provides: [validated 60-second fuzz log, shared-workload benchmark evidence] +--- + +# Phase 5 Plan 2 summary + +Published a deterministic persistent-process differential harness and validated +60-second zero-divergence log, then measured the original JavaScript adapter and +Go adapter with one representative workload on the same host. + +## Evidence + +- `fuzz/log.txt`: 60.012 seconds, seed 20260801, 1,091,630 cases, 0 divergences. +- `bench/results.json`: 20 cold starts and 1,000 persistent requests per runtime. +- JavaScript: 41.4131 ms startup p99, 0.2795 ms latency p99, 7,834.77 ops/s, + 42,868,736-byte peak RSS. +- Go: 11.0109 ms startup p99, 0.1808 ms latency p99, 16,756.79 ops/s, + 12,668,928-byte peak RSS. +- Benchmark tests, normalized workload SHA-256 validation, all Node checks, all + 164 fixed corpus cases, Go tests, and Go vet passed. + +GNU Make is unavailable in the local PowerShell environment, so its constituent +commands were executed directly with the repository-local Go cache. The final +Ubuntu Actions run remains the direct `make verify` proof. + +## Commits + +- `6b4506a`, `0e54f1a`: persistent fuzz harness and exact-parity fixes. +- `ad69b3e`: validated 60-second fuzz evidence. +- `03683ec`: shared benchmark workload, runner, tests, methodology, and results. + +## Remaining external action + +Record and publish the five-minute demo video; no URL is claimed yet. From 82a9532249536cdbd6bfa6876d23a9764c6d4c99 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 11:29:12 +0530 Subject: [PATCH 065/165] docs(05-03): write submission guide and decisions --- DECISIONS.md | 23 +- PORT.md | 14 +- README.md | 541 +++++++---------------------------------- docs/ARCHITECTURE.md | 5 +- docs/DEMO.md | 19 ++ docs/TEAM-OWNERSHIP.md | 9 + docs/TESTING.md | 43 ++-- 7 files changed, 148 insertions(+), 506 deletions(-) create mode 100644 docs/DEMO.md diff --git a/DECISIONS.md b/DECISIONS.md index 96b472d3..6221f0ad 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -2,11 +2,18 @@ | ID | Decision | Why | Status | |---|---|---|---| -| D-001 | Port the checked-out `mod.js` behavior to Go. | It is the exact source and test target selected for the project. | Accepted | -| D-002 | Keep JavaScript oracle files immutable. | Differential evidence is credible only when the reference does not move with the port. | Accepted | -| D-003 | Put the Go module in `src/`. | It keeps the port idiomatic while following the Track H submission layout; the JavaScript oracle remains at the root. | Accepted | -| D-004 | Use JSON Lines adapters over stdin/stdout. | Node and Go can compare dynamic inputs and operations without modifying source tests. | Accepted | -| D-005 | Use Go standard library first. | The port's algorithms are small and self-contained; dependencies add compatibility and supply-chain surface. | Accepted | -| D-006 | Preserve source quirks at the adapter boundary; expose an idiomatic Go API separately. | Compatibility and Go usability are both required, but they should not leak dynamic JavaScript semantics into every caller. | Accepted | -| D-007 | Run the immutable Deno source suite explicitly as `deno test test.js`. | Deno 2 broad discovery includes generated npm and Node port tests outside the pinned source suite. | Accepted | -| D-008 | Use a finite V8-derived luminance transfer table. | TinyColor rounds RGB to 8-bit channels before luminance, while Go `math.Pow` differs from V8 for 93 nonlinear values; the table preserves exact parity without tolerance. | Accepted | +| D-001 | Port the checked-out `mod.js` behavior to Go. | It is the exact source and test target selected at kickoff. | Accepted | +| D-002 | Keep JavaScript oracle files immutable. | Differential evidence is credible only when the reference cannot move with the port. | Accepted | +| D-003 | Put the independent Go module in `src/`. | It follows the submission layout while keeping the JavaScript oracle at the root. | Accepted | +| D-004 | Use JSON Lines adapters over stdin/stdout. | Both runtimes can compare dynamic inputs and operations without editing source tests. | Accepted | +| D-005 | Prefer the Go and Node standard libraries. | TinyColor's algorithms and evidence runners need no third-party dependency or supply-chain surface. | Accepted | +| D-006 | Preserve JavaScript quirks at the adapter boundary while exposing typed Go values. | Compatibility semantics should not force dynamic JavaScript shapes onto every Go caller. | Accepted | +| D-007 | Run the pinned source suite explicitly as `deno test test.js`. | Broad Deno discovery includes generated npm and port-owned tests outside the original suite. | Accepted | +| D-008 | Use a finite V8-derived luminance transfer table. | Go `math.Pow` differs by one ULP for some rounded channels; the finite table preserves exact parity without tolerance. | Accepted | +| D-009 | Keep human CLI commands and zero-argument JSONL mode in one executable. | One artifact serves judges and automated differential tools without duplicate color logic. | Accepted | +| D-010 | Verify kickoff files with a cross-platform SHA-256 manifest parser. | Hash checks must behave consistently with pinned CRLF oracle files and reject path traversal. | Accepted | +| D-011 | Seed the differential fuzzer and use persistent child processes. | A reproducible request prefix plus one process per runtime gives sustained public-API coverage without startup noise. | Accepted | +| D-012 | Compare fuzz responses exactly. | Broad floating-point tolerances would hide observable compatibility defects. | Accepted | +| D-013 | Benchmark both adapters with the same fixed JSONL workload and lifecycle. | Shared inputs and same-host cold/persistent runs make the measurements directly reproducible. | Accepted | +| D-014 | Normalize benchmark workload newlines before hashing. | Evidence hashes remain stable across Windows and Unix checkouts without changing request content. | Accepted | +| D-015 | Publish unsupported or external deliverables as unverified. | Honest missing evidence is preferable to unreproducible CI, public-access, or video claims. | Accepted | diff --git a/PORT.md b/PORT.md index aec69edb..5e296449 100644 --- a/PORT.md +++ b/PORT.md @@ -1,12 +1,4 @@ -# TinyColor Go port +# Port guide -This repository ports the behavior of the pinned `bgrins/TinyColor` checkout -to Go. Build the compatibility CLI with one command: - -```powershell -make build -``` - -Run the current parity checks with `make test`. The immutable JavaScript oracle -stays at the root and its kickoff hashes are recorded in -`tests/original/manifest.sha256`. +The complete build, usage, compatibility, evidence, and limitation guide is in +[`README.md`](README.md). diff --git a/README.md b/README.md index 71b6d767..e3f51de8 100644 --- a/README.md +++ b/README.md @@ -1,492 +1,115 @@ -# TinyColor +# TinyColor Go port -## JavaScript color tooling +An idiomatic Go port of the behavior in the pinned +[`bgrins/TinyColor`](https://github.com/bgrins/TinyColor) checkout. The +JavaScript source remains in this repository as an immutable oracle; exact +JSONL differential checks demonstrate compatibility rather than modifying the +source tests. -TinyColor is a small, fast library for color manipulation and conversion in JavaScript. It allows many forms of input, while providing color conversions and other color utility functions. It has no dependencies. +Source commit: `b49018c9f2dbca313d80d7a4dad25e26143cfe01`. TinyColor and this port retain +Brian Grinstead's MIT license in [`LICENSE`](LICENSE). The original JavaScript +project documentation remains available in the upstream repository history. -## Including in node +## Build once -`tinycolor` can be installed from npm: +Requirements: Go 1.26+, Node.js, and GNU Make. - npm install tinycolor2 - -Then it can be used in your script like so: - -```js -var tinycolor = require("tinycolor2"); -var color = tinycolor("red"); -``` - -Or in a module like so: - -```js -import tinycolor from "tinycolor2"; -var color = tinycolor("red"); -``` - -## Including in a browser - -The package can be bundled from npm, but if you prefer to download it locally you have two choices: - -### ESM - -It can be used as a module by downloading [npm/esm/tinycolor.js](https://github.com/bgrins/TinyColor/blob/master/npm/esm/tinycolor.js) or using https://esm.sh/tinycolor2. - -```html - -``` - -### UMD - -You can use it directly in a script tag by downloading the UMD file from [npm/cjs/tinycolor.js](https://github.com/bgrins/TinyColor/blob/master/npm/cjs/tinycolor.js): - -```html - - -``` - -## Usage - -Call `tinycolor(input)` or `new tinycolor(input)`, and you will have an object with the following properties. See Accepted String Input and Accepted Object Input below for more information about what is accepted. - -## Accepted String Input - -The string parsing is very permissive. It is meant to make typing a color as input as easy as possible. All commas, percentages, parenthesis are optional, and most input allow either 0-1, 0%-100%, or 0-n (where n is either 100, 255, or 360 depending on the value). - -HSL and HSV both require either 0%-100% or 0-1 for the `S`/`L`/`V` properties. The `H` (hue) can have values between 0%-100% or 0-360. - -RGB input requires either 0-255 or 0%-100%. - -If you call `tinycolor.fromRatio`, RGB and Hue input can also accept 0-1. - -Here are some examples of string input: - -### Hex, 8-digit (RGBA) Hex -```js -tinycolor("#000"); -tinycolor("000"); -tinycolor("#369C"); -tinycolor("369C"); -tinycolor("#f0f0f6"); -tinycolor("f0f0f6"); -tinycolor("#f0f0f688"); -tinycolor("f0f0f688"); -``` -### RGB, RGBA -```js -tinycolor("rgb (255, 0, 0)"); -tinycolor("rgb 255 0 0"); -tinycolor("rgba (255, 0, 0, .5)"); -tinycolor({ r: 255, g: 0, b: 0 }); -tinycolor.fromRatio({ r: 1, g: 0, b: 0 }); -tinycolor.fromRatio({ r: .5, g: .5, b: .5 }); -``` -### HSL, HSLA -```js -tinycolor("hsl(0, 100%, 50%)"); -tinycolor("hsla(0, 100%, 50%, .5)"); -tinycolor("hsl(0, 100%, 50%)"); -tinycolor("hsl 0 1.0 0.5"); -tinycolor({ h: 0, s: 1, l: .5 }); -tinycolor.fromRatio({ h: 1, s: 0, l: 0 }); -tinycolor.fromRatio({ h: .5, s: .5, l: .5 }); -``` -### HSV, HSVA -```js -tinycolor("hsv(0, 100%, 100%)"); -tinycolor("hsva(0, 100%, 100%, .5)"); -tinycolor("hsv (0 100% 100%)"); -tinycolor("hsv 0 1 1"); -tinycolor({ h: 0, s: 100, v: 100 }); -tinycolor.fromRatio({ h: 1, s: 0, v: 0 }); -tinycolor.fromRatio({ h: .5, s: .5, v: .5 }); +```sh +make build ``` -### Named -Case insenstive names are accepted, using the [list of colors in the CSS spec](https://www.w3.org/TR/css-color-4/#named-colors). +This creates `bin/tinycolor` on Linux/macOS or `bin/tinycolor.exe` on Windows. +Docker produces the same runnable CLI in one command: -```js -tinycolor("RED"); -tinycolor("blanchedalmond"); -tinycolor("darkblue"); +```sh +docker build -t tinycolor-go . +docker run --rm tinycolor-go parse --json red ``` -### Accepted Object Input -If you are calling this from code, you may want to use object input. Here are some examples of the different types of accepted object inputs: +## CLI - { r: 255, g: 0, b: 0 } - { r: 255, g: 0, b: 0, a: .5 } - { h: 0, s: 100, l: 50 } - { h: 0, s: 100, v: 100 } - -## Methods - -### getFormat - -Returns the format used to create the tinycolor instance -```js -var color = tinycolor("red"); -color.getFormat(); // "name" -color = tinycolor({r:255, g:255, b:255}); -color.getFormat(); // "rgb" +```sh +./bin/tinycolor parse --json red +./bin/tinycolor convert --to hsl --json red +./bin/tinycolor lighten --amount 10 --json '#000' +./bin/tinycolor palette --type triad --json red +./bin/tinycolor contrast --json '#000' '#fff' ``` -### getOriginalInput +Remove `.exe` from the examples on Unix; add it on Windows. Without `--json`, +commands print human-readable output. `convert` supports `hex`, `hex8`, `rgb`, +`percentage-rgb`, `hsl`, `hsv`, and `name`; `palette` supports `complement`, +`splitcomplement`, `triad`, `tetrad`, `analogous`, and `monochromatic`. -Returns the input passed into the constructor used to create the tinycolor instance -```js -var color = tinycolor("red"); -color.getOriginalInput(); // "red" -color = tinycolor({r:255, g:255, b:255}); -color.getOriginalInput(); // "{r: 255, g: 255, b: 255}" -``` +## Go package -### isValid - -Return a boolean indicating whether the color was successfully parsed. Note: if the color is not valid then it will act like `black` when being used with other methods. -```js -var color1 = tinycolor("red"); -color1.isValid(); // true -color1.toHexString(); // "#ff0000" - -var color2 = tinycolor("not a color"); -color2.isValid(); // false -color2.toString(); // "#000000" -``` -### getBrightness +The Go module lives under `src/`: -Returns the perceived brightness of a color, from `0-255`, as defined by [Web Content Accessibility Guidelines (Version 1.0)](http://www.w3.org/TR/AERT#color-contrast). -```js -var color1 = tinycolor("#fff"); -color1.getBrightness(); // 255 +```go +package main -var color2 = tinycolor("#000"); -color2.getBrightness(); // 0 -``` -### isLight +import ( + "fmt" -Return a boolean indicating whether the color's perceived brightness is light. -```js -var color1 = tinycolor("#fff"); -color1.isLight(); // true + "github.com/rajeet-04/tinycolor-go/tinycolor" +) -var color2 = tinycolor("#000"); -color2.isLight(); // false +func main() { + color, _ := tinycolor.FromCompat("#336699", false) + color.Lighten(10) + fmt.Println(color.ToHSLString()) +} ``` -### isDark -Return a boolean indicating whether the color's perceived brightness is dark. -```js -var color1 = tinycolor("#fff"); -color1.isDark(); // false +## Verify parity -var color2 = tinycolor("#000"); -color2.isDark(); // true +```sh +make verify +deno test test.js ``` -### getLuminance - -Returns the perceived luminance of a color, from `0-1` as defined by [Web Content Accessibility Guidelines (Version 2.0).](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef) -```js -var color1 = tinycolor("#fff"); -color1.getLuminance(); // 1 -var color2 = tinycolor("#000"); -color2.getLuminance(); // 0 -``` -### getAlpha +`make verify` checks formatting, the three kickoff hashes in +[`tests/original/manifest.sha256`](tests/original/manifest.sha256), Node and Go +tests, all fixed differential corpora, the recorded fuzz log, and `go vet`. +The untouched Deno suite is a separate source-oracle check. -Returns the alpha value of a color, from `0-1`. -```js -var color1 = tinycolor("rgba(255, 0, 0, .5)"); -color1.getAlpha(); // 0.5 +Evidence: -var color2 = tinycolor("rgb(255, 0, 0)"); -color2.getAlpha(); // 1 +- [Compatibility matrix](COMPATIBILITY.md) — fixed-corpus, Deno, coverage, and safety counts. +- [Differential fuzz log](fuzz/log.txt) and [reproduction guide](fuzz/README.md) — 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. +- [Benchmark results](bench/results.json) and [methodology](bench/methodology.md) — same-host startup p99, latency p99, throughput, and peak RSS. +- [Architectural decisions](DECISIONS.md), [architecture](docs/ARCHITECTURE.md), [testing](docs/TESTING.md), and [team ownership](docs/TEAM-OWNERSHIP.md). +- [Five-minute demo script](docs/DEMO.md). -var color3 = tinycolor("transparent"); -color3.getAlpha(); // 0 -``` -### setAlpha +## Repository layout -Sets the alpha value on a current color. Accepted range is in between `0-1`. -```js -var color = tinycolor("red"); -color.getAlpha(); // 1 -color.setAlpha(.5); -color.getAlpha(); // .5 -color.toRgbString(); // "rgba(255, 0, 0, .5)" +```text +src/ Go module, public API, and CLI +tests/original/ kickoff hash manifest and verifier +tests/port/ port-owned adapter tests +compat/ JSONL oracle, driver, and fixed corpora +fuzz/ differential harness and 60-second log +bench/ shared workload, runner, methodology, and results +docs/ architecture, testing, ownership, and demo guide ``` -### String Representations -The following methods will return a property for the `alpha` value, which can be ignored: `toHsv`, `toHsl`, `toRgb` - -### toHsv -```js -var color = tinycolor("red"); -color.toHsv(); // { h: 0, s: 1, v: 1, a: 1 } -``` -### toHsvString -```js -var color = tinycolor("red"); -color.toHsvString(); // "hsv(0, 100%, 100%)" -color.setAlpha(0.5); -color.toHsvString(); // "hsva(0, 100%, 100%, 0.5)" -``` -### toHsl -```js -var color = tinycolor("red"); -color.toHsl(); // { h: 0, s: 1, l: 0.5, a: 1 } -``` -### toHslString -```js -var color = tinycolor("red"); -color.toHslString(); // "hsl(0, 100%, 50%)" -color.setAlpha(0.5); -color.toHslString(); // "hsla(0, 100%, 50%, 0.5)" -``` -### toHex -```js -var color = tinycolor("red"); -color.toHex(); // "ff0000" -``` -### toHexString -```js -var color = tinycolor("red"); -color.toHexString(); // "#ff0000" -``` -### toHex8 -```js -var color = tinycolor("red"); -color.toHex8(); // "ff0000ff" -``` -### toHex8String -```js -var color = tinycolor("red"); -color.toHex8String(); // "#ff0000ff" -``` -### toRgb -```js -var color = tinycolor("red"); -color.toRgb(); // { r: 255, g: 0, b: 0, a: 1 } -``` -### toRgbString -```js -var color = tinycolor("red"); -color.toRgbString(); // "rgb(255, 0, 0)" -color.setAlpha(0.5); -color.toRgbString(); // "rgba(255, 0, 0, 0.5)" -``` -### toPercentageRgb -```js -var color = tinycolor("red"); -color.toPercentageRgb() // { r: "100%", g: "0%", b: "0%", a: 1 } -``` -### toPercentageRgbString -```js -var color = tinycolor("red"); -color.toPercentageRgbString(); // "rgb(100%, 0%, 0%)" -color.setAlpha(0.5); -color.toPercentageRgbString(); // "rgba(100%, 0%, 0%, 0.5)" -``` -### toName -```js -var color = tinycolor("red"); -color.toName(); // "red" -``` -### toFilter -``` -var color = tinycolor("red"); -color.toFilter(); // "progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffff0000,endColorstr=#ffff0000)" -``` -### toString +## Known limits -Print to a string, depending on the input format. You can also override this by passing one of `"rgb", "prgb", "hex6", "hex3", "hex8", "name", "hsl", "hsv"` into the function. -```js -var color1 = tinycolor("red"); -color1.toString(); // "red" -color1.toString("hsv"); // "hsv(0, 100%, 100%)" +- Compatibility is claimed for the pinned checkout and measured public corpus, + not every future TinyColor revision or arbitrary JavaScript coercion. +- Random colors are checked by validity/range invariants because independent + runtimes do not share a random stream. +- Benchmark figures are observations from one host, not universal speedup + claims. +- The required demo video must still be recorded, uploaded, and linked by a + human; no video URL is claimed in this repository yet. -var color2 = tinycolor("rgb(255, 0, 0)"); -color2.toString(); // "rgb(255, 0, 0)" -color2.setAlpha(.5); -color2.toString(); // "rgba(255, 0, 0, 0.5)" -``` -### Color Modification +## Submission checklist -These methods manipulate the current color, and return it for chaining. For instance: -```js -tinycolor("red").lighten().desaturate().toHexString() // "#f53d3d" -``` -### lighten - -`lighten: function(amount = 10) -> TinyColor`. Lighten the color a given amount, from 0 to 100. Providing 100 will always return white. -```js -tinycolor("#f00").lighten().toString(); // "#ff3333" -tinycolor("#f00").lighten(100).toString(); // "#ffffff" -``` -### brighten - -`brighten: function(amount = 10) -> TinyColor`. Brighten the color a given amount, from 0 to 100. -```js -tinycolor("#f00").brighten().toString(); // "#ff1919" -``` -### darken - -`darken: function(amount = 10) -> TinyColor`. Darken the color a given amount, from 0 to 100. Providing 100 will always return black. -```js -tinycolor("#f00").darken().toString(); // "#cc0000" -tinycolor("#f00").darken(100).toString(); // "#000000" -``` -### desaturate - -`desaturate: function(amount = 10) -> TinyColor`. Desaturate the color a given amount, from 0 to 100. Providing 100 will is the same as calling `greyscale`. -```js -tinycolor("#f00").desaturate().toString(); // "#f20d0d" -tinycolor("#f00").desaturate(100).toString(); // "#808080" -``` -### saturate - -`saturate: function(amount = 10) -> TinyColor`. Saturate the color a given amount, from 0 to 100. -```js -tinycolor("hsl(0, 10%, 50%)").saturate().toString(); // "hsl(0, 20%, 50%)" -``` -### greyscale - -`greyscale: function() -> TinyColor`. Completely desaturates a color into greyscale. Same as calling `desaturate(100)`. -```js -tinycolor("#f00").greyscale().toString(); // "#808080" -``` -### spin - -`spin: function(amount = 0) -> TinyColor`. Spin the hue a given amount, from -360 to 360. Calling with 0, 360, or -360 will do nothing (since it sets the hue back to what it was before). -```js -tinycolor("#f00").spin(180).toString(); // "#00ffff" -tinycolor("#f00").spin(-90).toString(); // "#7f00ff" -tinycolor("#f00").spin(90).toString(); // "#80ff00" - -// spin(0) and spin(360) do nothing -tinycolor("#f00").spin(0).toString(); // "#ff0000" -tinycolor("#f00").spin(360).toString(); // "#ff0000" -``` -### Color Combinations - -Combination functions return an array of TinyColor objects unless otherwise noted. - -### analogous - -`analogous: function(, results = 6, slices = 30) -> array`. -```js -var colors = tinycolor("#f00").analogous(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#ff0066", "#ff0033", "#ff0000", "#ff3300", "#ff6600" ] -``` -### monochromatic - -`monochromatic: function(, results = 6) -> array`. -```js -var colors = tinycolor("#f00").monochromatic(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#2a0000", "#550000", "#800000", "#aa0000", "#d40000" ] -``` -### splitcomplement - -`splitcomplement: function() -> array`. -```js -var colors = tinycolor("#f00").splitcomplement(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#ccff00", "#0066ff" ] -``` -### triad - -`triad: function() -> array`. -```js -var colors = tinycolor("#f00").triad(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#00ff00", "#0000ff" ] -``` -### tetrad - -`tetrad: function() -> array`. -```js -var colors = tinycolor("#f00").tetrad(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#80ff00", "#00ffff", "#7f00ff" ] - -``` -### complement - -`complement: function() -> TinyColor`. -```js -tinycolor("#f00").complement().toHexString(); // "#00ffff" -``` -## Color Utilities -```js -tinycolor.equals(color1, color2) -tinycolor.mix(color1, color2, amount = 50) -``` -### random - -Returns a random color. -```js -var color = tinycolor.random(); -color.toRgb(); // "{r: 145, g: 40, b: 198, a: 1}" -``` - -### Readability - -TinyColor assesses readability based on the [Web Content Accessibility Guidelines (Version 2.0)](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef). - -#### readability - -`readability: function(TinyColor, TinyColor) -> Object`. -Returns the contrast ratio between two colors. -```js -tinycolor.readability("#000", "#000"); // 1 -tinycolor.readability("#000", "#111"); // 1.1121078324840545 -tinycolor.readability("#000", "#fff"); // 21 -``` -Use the values in your own calculations, or use one of the convenience functions below. - -#### isReadable - -`isReadable: function(TinyColor, TinyColor, Object) -> Boolean`. Ensure that foreground and background color combinations meet WCAG guidelines. `Object` is optional, defaulting to `{level: "AA",size: "small"}`. `level` can be `"AA"` or "AAA" and `size` can be `"small"` or `"large"`. - -Here are links to read more about the [AA](http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) and [AAA](http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html) requirements. -```js -tinycolor.isReadable("#000", "#111", {}); // false -tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AA",size:"small"}); //false -tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AA",size:"large"}), //true -``` -#### mostReadable - -`mostReadable: function(TinyColor, [TinyColor, Tinycolor ...], Object) -> Boolean`. -Given a base color and a list of possible foreground or background colors for that base, returns the most readable color. -If none of the colors in the list is readable, `mostReadable` will return the better of black or white if `includeFallbackColors:true`. -```js -tinycolor.mostReadable("#000", ["#f00", "#0f0", "#00f"]).toHexString(); // "#00ff00" -tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255" -tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff" -tinycolor.mostReadable("#ff0088", ["#2e0c3a"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString() // "#2e0c3a", -tinycolor.mostReadable("#ff0088", ["#2e0c3a"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString() // "#000000", -``` -See [index.html](https://github.com/bgrins/TinyColor/blob/master/index.html) in the project for a demo. - -## Common operations - -### clone - -`clone: function() -> TinyColor`. -Instantiate a new TinyColor object with the same color. Any changes to the new one won't affect the old one. -```js -var color1 = tinycolor("#F00"); -var color2 = color1.clone(); -color2.setAlpha(.5); - -color1.toString(); // "#ff0000" -color2.toString(); // "rgba(255, 0, 0, 0.5)" -``` +- [x] Public source URL and pinned kickoff commit recorded. +- [x] One-command native and Docker builds documented. +- [x] Immutable oracle hashes and source suite verified. +- [x] Exact fixed-corpus and 60-second differential evidence published. +- [x] Same-host benchmark methodology and results published. +- [x] Decisions, ownership, limitations, and demo script documented. +- [ ] Five-minute demo video recorded and published. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b7e471b9..acaa5bcc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -24,9 +24,12 @@ cases ── differential driver ── Go JSONL runner 3. **Public library (`src/tinycolor`):** exposes explicit Go values and methods. It owns conversions, formatting, mutation, utilities, readability, and palettes. -4. **CLI (`src/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same +4. **CLI (`src/cmd/tinycolor-compat`):** a thin user-facing wrapper; it calls the same library and does not reimplement parsing or conversion. +The same zero-argument executable is used by `compat/run.mjs`, the fuzz harness, +and the benchmark runner. Human subcommands select the judge-facing CLI instead. + ## Compatibility protocol Each line is a single JSON request with `id`, `operation`, `input`, and optional diff --git a/docs/DEMO.md b/docs/DEMO.md new file mode 100644 index 00000000..4a4998fd --- /dev/null +++ b/docs/DEMO.md @@ -0,0 +1,19 @@ +# Five-minute demo script + +The recording and upload remain manual and unverified until a public URL is +added to `README.md`. + +| Time | Demonstration | +|---|---| +| 0:00–0:30 | Show `git status --short`, the source URL/commit in `.port-mortem.toml`, and run `node tests/original/verify.mjs`. | +| 0:30–1:00 | Run `make build`, then show the single artifact under `bin/`. | +| 1:00–2:10 | Run JSON examples for `parse`, `convert`, `lighten`, `palette`, and `contrast` from `README.md`. | +| 2:10–2:50 | Run `make verify` and point out all five exact corpus totals. | +| 2:50–3:30 | Open `fuzz/log.txt`, validate it, then run the one-second seeded fuzz smoke. | +| 3:30–4:10 | Open `bench/results.json` and explain the shared workload, p99 formula, throughput, RSS, and same-host limitation. | +| 4:10–4:40 | Show `DECISIONS.md`, zero-unsafe command, coverage table, and known limits in `COMPATIBILITY.md`. | +| 4:40–5:00 | Show the public repository and successful exact-commit Actions run, then close on the runnable binary path. | + +Before submission, verify repository visibility while logged out, record one +continuous take, publish it, and add the video URL. Those account actions are +not claimed by this script. diff --git a/docs/TEAM-OWNERSHIP.md b/docs/TEAM-OWNERSHIP.md index 8ecb96ca..8ed83060 100644 --- a/docs/TEAM-OWNERSHIP.md +++ b/docs/TEAM-OWNERSHIP.md @@ -29,3 +29,12 @@ Every handoff includes the commit, public contract or command added, passing checks, corpus coverage, and known gaps. Do not hand off an uncommitted shared working tree as the only artifact. +## Phase 5 integration handoff + +- @rajeet-04 integrated exact fuzz parity, the validated 60-second log, and the + shared benchmark evidence on branch `rajeet`. +- @mrashis owns any new differential mismatch reproduction. +- @deepali owns final submission presentation and the manual demo-video URL. +- @xthxr remains the review lead for conversion, readability, and palette + behavior; Phase 1–4 ownership is unchanged. + diff --git a/docs/TESTING.md b/docs/TESTING.md index 8050b8ac..e6d39c6e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,31 +1,20 @@ -# Testing Strategy +# Testing strategy -## Test layers - -| Layer | Purpose | Command when implemented | +| Layer | Purpose | Command | |---|---|---| -| Go unit tests | Local conversion and parser invariants | `go test ./...` from `src/` | -| Differential corpus | Exact observable parity with local `mod.js` | `node compat/run.mjs ` | -| Regression corpus | Permanently reproduce every found mismatch | `node compat/run.mjs compat/cases/regression.jsonl` | -| Seeded fuzz cases | Find coercion, bounds, and rounding gaps | documented seed command | -| Source suite | Validate original source remains runnable | `deno task test` when Deno exists | -| Static checks | Formatting and common Go defects | `gofmt`, `go vet ./...` | - -## Comparison rules - -- Compare strings byte-for-byte. -- Compare booleans, arrays, formats, and errors exactly. -- Serialize non-finite values explicitly if the source produces them; JSON's - default inability to represent them must not silently erase a difference. -- Use a narrowly documented normalization only for JavaScript/Go JSON number - serialization—not for color math. Never use a global epsilon. -- Seed random generation and print the seed on failure. +| Full local gate | Format, hashes, unit, differential, fuzz-log, vet | `make verify` | +| Original source suite | Untouched upstream behavior | `deno test test.js` | +| Go unit/coverage | Package behavior and honest coverage | `go -C src test -cover ./...` | +| Fixed differential | Exact JS/Go response parity | `node compat/run.mjs compat/cases/operations.jsonl` | +| Fuzz smoke | Seeded broad exact comparison | `node fuzz/harness.mjs --duration 1 --seed 20260801` | +| Fuzz evidence | Validate recorded 60-second run | `node fuzz/validate-log.mjs fuzz/log.txt` | +| Benchmark smoke | Real quick measurement to temporary output | `node bench/run.mjs --quick --output ` | +| Benchmark evidence | Full same-host measurement | `node bench/run.mjs --output bench/results.json` | -## Corpus priorities +Strings, booleans, arrays, errors, formats, and parsed numeric results are +compared exactly. No global epsilon is used. Every discovered mismatch becomes +a deterministic regression before the shared implementation boundary is fixed. -1. Inputs copied from `test.js` in parser and output groups. -2. Boundaries: `-1`, `0`, `1`, `1.0`, `100%`, `255`, overflow, blank, malformed, - alpha `0`, and hue values below/above 360. -3. State transitions: clone, set alpha, all instance modifiers, default versus - explicit zero amount. -4. Structured random RGB/RGBA/HSL/HSV inputs generated from a fixed seed. +The fixed corpora cover source-test inputs, permissive and malformed parsing, +numeric boundaries, alpha and hue behavior, conversions, mutation, WCAG +readability, mixing, and ordered palettes. Random output is invariant-tested. From 7c2da11bfc1f47c586453d6aa29a62282831429b Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 11:29:23 +0530 Subject: [PATCH 066/165] docs(05-03): publish final compatibility evidence --- COMPATIBILITY.md | 167 +++++++++++++++++++++-------------------------- 1 file changed, 73 insertions(+), 94 deletions(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 04436a3c..54f57dba 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,94 +1,73 @@ -# Compatibility Matrix - -Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. - -## Current evidence - -| Area | Status | Evidence | Notes | -|---|---|---|---| -| Oracle baseline | Ready | `mod.js`, `test.js`, Node 24.18.0 | JavaScript source is untouched. | -| Node adapter | Passing | `node tests/port/adapter.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | -| Go adapter | Passing | `go test ./...` from `src/` | JSONL schema shared with Node adapter. | -| HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | -| HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | -| Conversion/formatting | Passing | `compat/cases/conversion.jsonl` | 35 fixed JSONL cases; random is invariant-only. | -| Analysis/readability | Passing | `compat/cases/operations.jsonl` | WCAG defaults, thresholds, ties, fallback, and null selection. | -| Manipulation/palettes | Passing | `compat/cases/operations.jsonl` | 69 fixed operations cases cover modifiers, Mix, readability, and ordered palettes. | -| CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | -| Original Deno suite | Blocked locally | `deno` unavailable | Run when Deno is installed or in CI. | - -## Mismatch record format - -```text -Case: -Operation: -Input: -JavaScript: -Go: -Owner: -Status: open | fixed | accepted-difference -Reason: -``` - -## Phase 1 smoke result - -On 2026-08-01, the fixed Phase 1 corpus passed with **9/9 cases** and **0 -mismatches**: - -```powershell -$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -node tests/port/adapter.test.mjs -Set-Location src; go test ./...; go vet ./...; Set-Location .. -node compat/run.mjs compat/cases/smoke.jsonl -``` - -This is only the fixed Phase 1 corpus; it is not a full TinyColor parity claim. - -## Phase 2 parser slice result - -On 2026-08-01, the HEX/RGB/name parser corpus passed with **26/26 cases** and -**0 mismatches**: - -```powershell -$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -Set-Location src; go test ./...; go vet ./...; Set-Location .. -node compat/run.mjs compat/cases/parser-hex-rgb.jsonl -node compat/run.mjs compat/cases/smoke.jsonl -``` - -This records only the completed parser slice; the full HSL/HSV corpus remains -Phase 2 Plan 02 work. - -## Phase 2 full parser result - -`node compat/run.mjs compat/cases/parser.jsonl` passed with **23/23 cases** and -**0 mismatches** on 2026-08-01. The unchanged Phase 1 smoke corpus also passed -**9/9** with **0 mismatches**. - -## Phase 3 conversion result - -On 2026-08-01, the fixed conversion corpus passed with **35/35 cases** and -**0 mismatches**. The complete Phase 1–3 gate also retained **9/9** smoke, -**26/26** HEX/RGB/name, and **23/23** parser cases with zero mismatches: - -```powershell -$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -Set-Location src; go test ./...; go vet ./...; Set-Location .. -node tests/port/adapter.test.mjs -node compat/run.mjs compat/cases/smoke.jsonl -node compat/run.mjs compat/cases/parser-hex-rgb.jsonl -node compat/run.mjs compat/cases/parser.jsonl -node compat/run.mjs compat/cases/conversion.jsonl -``` - -Random-color behavior is verified by validity, alpha, and channel-range -invariants only; it is not compared exactly across independent JavaScript and -Go random generators. - -## Phase 4 operations result - -On 2026-08-01, the fixed operations corpus passed with **69/69 cases** and -**0 mismatches**. The full available Phase 1-4 gate also retained **9/9** -smoke, **26/26** HEX/RGB/name, **23/23** parser, and **35/35** conversion -cases with zero mismatches. This evidence does not claim a Deno source-suite -pass because Deno is unavailable locally. +# Compatibility evidence + +Target: `bgrins/TinyColor` commit +`b49018c9f2dbca313d80d7a4dad25e26143cfe01`, pinned by +`tests/original/manifest.sha256`. + +## Fixed differential corpora + +Fresh command: `node compat/run.mjs `. + +| Corpus | Passed/total | Mismatches | +|---|---:|---:| +| `compat/cases/smoke.jsonl` | 9/9 | 0 | +| `compat/cases/parser-hex-rgb.jsonl` | 26/26 | 0 | +| `compat/cases/parser.jsonl` | 23/23 | 0 | +| `compat/cases/conversion.jsonl` | 35/35 | 0 | +| `compat/cases/operations.jsonl` | 71/71 | 0 | +| **Total** | **164/164** | **0** | + +Random-color behavior is checked through validity, alpha, and channel-range +invariants rather than exact equality between independent random generators. + +## Source suite and oracle integrity + +- `node tests/original/verify.mjs`: 3/3 kickoff hashes verified. +- `deno test test.js`: 45 passed, 0 failed, 1 ignored. +- The ignored `polyad` test is also ignored by the pinned upstream suite. + +## Go coverage and safety + +Fresh command: `go -C src test -cover ./...`. + +| Package | Statement coverage | +|---|---:| +| `cmd/tinycolor-compat` | 33.6% | +| `internal/color` | 90.5% | +| `internal/compat` | 57.1% | +| `internal/parser` | 85.8% | +| `tinycolor` | 92.8% | + +These are Go package coverage figures, not a JavaScript coverage comparison. +`rg -n '\bunsafe\b' src -g '*.go'` reports **0 Go source occurrences**. + +## Differential fuzzing + +[`fuzz/log.txt`](fuzz/log.txt) records 60.012 seconds, seed 20260801, +1,091,630 cases, and zero divergences. Validate it with +`node fuzz/validate-log.mjs fuzz/log.txt`. This supports the Differential Fuzz +Survivor claim; exact comparison remains enabled. + +## Same-host benchmark + +The committed [results](bench/results.json) were measured on Windows x64 with +20 cold starts and 1,000 persistent requests per implementation: + +| Implementation | Startup p99 | Latency p99 | Throughput | Peak RSS | +|---|---:|---:|---:|---:| +| JavaScript | 41.4131 ms | 0.2795 ms | 7,834.77 ops/s | 42,868,736 bytes | +| Go | 11.0109 ms | 0.1808 ms | 16,756.79 ops/s | 12,668,928 bytes | + +See [`bench/methodology.md`](bench/methodology.md). These are same-host +observations, not universal speedup claims. + +## Bonus and external evidence status + +- Differential Fuzz Survivor: eligible from the validated 60-second log. +- Zero Unsafe: eligible from zero Go source occurrences. +- Decision Log: eligible from 15 substantive decisions. +- GitHub Actions: configured; the final exact-commit run is not yet recorded here. +- Public repository visibility: not yet verified from an unauthenticated clone in this closeout. +- Five-minute demo video: not supplied. + +No known mismatch remains in the fixed corpus or recorded fuzz session. From 1c218b669d192e37b7019b08395cf348410dde79 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 11:32:22 +0530 Subject: [PATCH 067/165] docs(05-03): verify local delivery evidence --- .planning/HANDOFF.json | 85 ------------------- .../05-delivery-evidence/.continue-here.md | 52 ------------ .../05-delivery-evidence/05-03-SUMMARY.md | 45 ++++++++++ .../05-delivery-evidence/05-VERIFICATION.md | 53 ++++++++++++ 4 files changed, 98 insertions(+), 137 deletions(-) delete mode 100644 .planning/HANDOFF.json delete mode 100644 .planning/phases/05-delivery-evidence/.continue-here.md create mode 100644 .planning/phases/05-delivery-evidence/05-03-SUMMARY.md create mode 100644 .planning/phases/05-delivery-evidence/05-VERIFICATION.md diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json deleted file mode 100644 index 551ed4d3..00000000 --- a/.planning/HANDOFF.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "version": "1.0", - "timestamp": "2026-08-01T00:20:50.023Z", - "phase": "5", - "phase_name": "Delivery Evidence", - "phase_dir": ".planning/phases/05-delivery-evidence", - "plan": 2, - "task": 3, - "total_tasks": 3, - "status": "paused", - "completed_tasks": [ - { - "id": "05-02-01", - "name": "Deterministic persistent-process differential fuzz harness", - "status": "done", - "commit": "6b4506a, 0e54f1a" - }, - { - "id": "05-02-02", - "name": "Validated 60-second fuzz survivor evidence", - "status": "done", - "commit": "ad69b3e" - } - ], - "remaining_tasks": [ - { - "id": "05-02-03", - "name": "Measure shared-workload p99, RSS, startup, and throughput", - "status": "not_started" - }, - { - "id": "05-03-01", - "name": "Write judge-facing port documentation and decision log", - "status": "not_started" - }, - { - "id": "05-03-02", - "name": "Refresh honest parity, coverage, safety, benchmark, and bonus evidence", - "status": "not_started" - }, - { - "id": "05-03-03", - "name": "Run final clean local gate and write verification evidence", - "status": "not_started" - }, - { - "id": "05-03-04", - "name": "Verify public clone access and successful CI", - "status": "not_started" - }, - { - "id": "05-03-05", - "name": "Close Phase 5 planning state after external proof", - "status": "not_started" - } - ], - "blockers": [], - "human_actions_pending": [ - { - "action": "Record and publish the five-minute demo video, then add its URL during Plan 05-03.", - "context": "The submission requires a live demonstration; no video is currently claimed.", - "blocking": true - } - ], - "decisions": [ - { - "decision": "Use a finite V8-derived 256-entry luminance transfer table.", - "rationale": "Go math.Pow differs from V8 for 93 nonlinear 8-bit values, and exact parity forbids tolerance.", - "phase": "5" - }, - { - "decision": "Apply TinyColor's sub-one channel rounding at the FromCompat constructor boundary.", - "rationale": "One shared boundary fixes palette conversions without per-operation exceptions.", - "phase": "5" - }, - { - "decision": "Keep the fuzz generator broad and compare parsed responses exactly.", - "rationale": "The broad seed exposed both arithmetic and constructor parity defects that a narrowed corpus would hide.", - "phase": "5" - } - ], - "uncommitted_files": [], - "next_action": "Run `$gsd-resume-work`, then execute task 05-02-03 from `.planning/phases/05-delivery-evidence/05-02-PLAN.md`: build the shared benchmark workload and runner with tests first, write real results once, and create 05-02-SUMMARY.md only after benchmark verification passes.", - "context_notes": "Branch rajeet. Exact fuzz parity is resolved. The real checked-in log reports 60.012 seconds, seed 20260801, 1,091,630 cases, and zero divergences. Local verification passed all Go and Node gates, all five corpora (9/9, 26/26, 23/23, 35/35, 71/71), vet, fuzz tests, log validation, and the immutable Deno suite (45 passed, 0 failed, 1 ignored). GitHub Actions run 30675374085 passed make verify, Deno, build, and artifact upload in 43 seconds. No background process is running." -} diff --git a/.planning/phases/05-delivery-evidence/.continue-here.md b/.planning/phases/05-delivery-evidence/.continue-here.md deleted file mode 100644 index 282aa63c..00000000 --- a/.planning/phases/05-delivery-evidence/.continue-here.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -phase: 05-delivery-evidence -task: 3 -total_tasks: 3 -status: in_progress -last_updated: 2026-08-01T00:20:50.023Z ---- - - -Plan 05-02 is at Task 3/3. Differential fuzzing is finished and verified; the next task is the shared original-versus-port benchmark. Branch `rajeet` contains the exact parity fix in `0e54f1a` and the validated evidence in `ad69b3e`. - - - - -- Task 05-02-01: Persistent seeded differential harness — done. -- Exact readability mismatch fixed with a V8-derived 256-entry transfer table. -- Follow-on palette mismatches fixed at the shared TinyColor constructor boundary. -- One-second seed-1 smoke: 14,778 cases, zero divergences. -- Task 05-02-02: Real 60-second evidence — done. -- `fuzz/log.txt`: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. -- `make verify` now runs harness tests and validates the checked-in fuzz log. -- Local direct verification passed all Node/Go gates and corpora; Deno passed 45, failed 0, ignored 1. - - - - -- Task 05-02-03: Implement and run the benchmark specified in `05-02-PLAN.md`. -- After Task 3 passes, create `05-02-SUMMARY.md`. -- Execute Plan 05-03 documentation, honest evidence refresh, final external CI/public-clone proof, and closeout. -- Human: record and publish the required five-minute demo video. - - - - -- Use exact V8-derived luminance bits because heuristic ULP correction is wrong for the finite channel domain. -- Preserve TinyColor's constructor channel rounding in `FromCompat`, where every operation routes through it. -- Keep fuzz comparisons exact and the generator broad; do not add tolerance or remove failing input families. - - - - -- No technical blocker for benchmarking. -- The demo video remains a human submission action for Plan 05-03. - - - -The previous Actions failure was caused by a deliberately committed RED regression. GitHub Actions run `30675374085` passed `make verify`, Deno, build, and artifact upload in 43 seconds. Plan 05-03 still requires a final public-clone and CI proof after benchmarks and documentation. No server or watcher is running. - - - -Start with: run `$gsd-resume-work`, read Task 05-02-03 in `.planning/phases/05-delivery-evidence/05-02-PLAN.md`, then write the benchmark tests before implementing `bench/run.mjs`. Use temporary output for quick tests and write `bench/results.json` only once with the full measured run. - diff --git a/.planning/phases/05-delivery-evidence/05-03-SUMMARY.md b/.planning/phases/05-delivery-evidence/05-03-SUMMARY.md new file mode 100644 index 00000000..81f95182 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-03-SUMMARY.md @@ -0,0 +1,45 @@ +--- +phase: 5 +plan: 3 +subsystem: submission-documentation-closeout +tags: [readme, decisions, compatibility, verification] +provides: [judge-facing guide, honest compatibility evidence, local closeout proof] +status: local-complete-external-pending +--- + +# Phase 5 Plan 3 summary + +Replaced the upstream-oriented landing page with a concise port guide, expanded +the architectural decision log to 15 substantive entries, published current +compatibility/coverage/safety/benchmark evidence, and added a five-minute demo +script with its human actions explicitly unclaimed. + +## Task commits + +- `82a9532` `docs(05-03): write submission guide and decisions` +- `7c2da11` `docs(05-03): publish final compatibility evidence` +- Closing local-verification commit: this summary and `05-VERIFICATION.md`. + +## Fresh local evidence + +- Native build and JSON invocations of parse, convert, lighten, palette, and + contrast exited zero. +- 15 Node tests passed with zero failures. +- Go tests and vet passed using the repository-local Go cache. +- Fixed corpora passed 9/9, 26/26, 23/23, 35/35, and 71/71. +- Fresh one-second seed-20260801 fuzz: 11,103 cases, zero divergences. +- Recorded fuzz log validated: 60.012 seconds, 1,091,630 cases, zero divergences. +- Benchmark schema and normalized workload SHA-256 verified; quick measurement + wrote only to a temporary path. +- Immutable Deno suite: 45 passed, 0 failed, 1 ignored. +- Three oracle hashes verified; `git diff -- mod.js test.js tinycolor.js` empty. + +GNU Make is unavailable in this Windows PowerShell environment, so the exact +commands behind `make build` and `make verify` were executed directly. The +Ubuntu CI run remains the direct Make proof. + +## Remaining external actions + +- Push the current `rajeet` commit and verify the exact GitHub Actions run. +- Verify unauthenticated public access to the repository. +- Human: record, upload, and link the five-minute demo video. diff --git a/.planning/phases/05-delivery-evidence/05-VERIFICATION.md b/.planning/phases/05-delivery-evidence/05-VERIFICATION.md new file mode 100644 index 00000000..29382550 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-VERIFICATION.md @@ -0,0 +1,53 @@ +--- +phase: 5 +status: locally-verified +verified: 2026-08-01 +requirements: [DEL-01, QLT-03] +--- + +# Phase 5 verification + +## Goal + +A judge can build and use one runnable Go artifact, reproduce exact parity +evidence, inspect honest performance/safety numbers, and distinguish observed +local evidence from external actions. + +## Roadmap success criteria + +| Criterion | Status | Evidence | +|---|---|---| +| CI runs format, vet, Go tests, and differential checks | Configured; exact final run pending | `.github/workflows/port.yml`, `Makefile`; local constituent commands passed. | +| CLI supports parse, convert, lighten, palette, contrast, and JSON | Local pass | `make build` equivalent plus all five JSON invocations exited zero; implementation under `src/cmd/tinycolor-compat`. | +| Benchmarks, docs, attribution, ownership, and known differences are complete | Local pass | `bench/results.json`, `bench/methodology.md`, `README.md`, `LICENSE`, `DECISIONS.md`, `COMPATIBILITY.md`, `docs/TEAM-OWNERSHIP.md`. | + +## DEL-01 evidence + +- One-command build is defined by `make build`; direct Windows build produced + `bin/tinycolor.exe` and all five public commands ran successfully. +- `tests/original/manifest.sha256` verified all three immutable oracle files. +- Five fixed corpora passed 164/164 with zero mismatches. +- `fuzz/log.txt` validates a 60.012-second, seed-20260801 run with 1,091,630 + cases and zero divergences; a fresh one-second smoke passed 11,103 cases. +- `bench/results.json` contains 20 cold starts, 1,000 persistent requests, and + both implementations' startup p99, latency p99, throughput, and peak RSS. +- `docs/DEMO.md` is ready; recording/upload and its URL remain human actions. + +## QLT-03 evidence + +- 15 Node verification, fuzz, and benchmark tests passed. +- All Go packages passed tests and vet; package coverage is recorded in + `COMPATIBILITY.md` without claiming JavaScript coverage parity. +- `rg -n '\bunsafe\b' src -g '*.go'` returned zero source occurrences. +- Deno source suite passed 45, failed 0, ignored 1. +- `git diff --check` passed, source diff was empty, and benchmark workload hash + matched the committed normalized SHA-256. + +## External checkpoint + +- Public repository: pending unauthenticated verification. +- Exact final-commit GitHub Actions run: pending push and observation. +- Demo video: not supplied. + +Phase 5 remains open until public clone access and the exact successful CI run +are recorded. The missing video URL remains explicitly unclaimed. From 1b8cb4427e11e158792b66504b35aa76dcb181d5 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 11:35:47 +0530 Subject: [PATCH 068/165] docs(05): close delivery evidence phase --- .planning/PROJECT.md | 2 +- .planning/ROADMAP.md | 11 +++-- .planning/STATE.md | 42 ++++++++++--------- .../05-delivery-evidence/05-VERIFICATION.md | 17 ++++---- COMPATIBILITY.md | 4 +- README.md | 1 + 6 files changed, 45 insertions(+), 32 deletions(-) diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index 0a9a06d1..6a74041e 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -22,4 +22,4 @@ the Go port is accepted only when exact differential evidence agrees. - Target source: local `mod.js` / `test.js`; MIT attribution retained. - Target language: Go 1.26+; standard library first. -- Node is available as local oracle runtime; Deno is currently unavailable. +- Node and Deno are available as local oracle runtimes. diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index fe25d974..dffc3cd6 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -12,7 +12,7 @@ through parsing and observable output, to operations and delivery proof. - [x] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. - [x] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. - [x] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. -- [ ] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. +- [x] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. ## Phase Details @@ -78,7 +78,12 @@ Plans: 1. CI runs format, vet, Go tests, and the differential smoke report. 2. The CLI supports parse, convert, lighten, palette, and contrast, including JSON output. 3. Benchmarks, documentation, license attribution, team work, and known differences are complete. -**Plans**: TBD +**Plans**: 3 plans (complete 2026-08-01) + +Plans: +- [x] 05-01: Add the human CLI, reproducible build/verification gate, Docker artifact, and CI. +- [x] 05-02: Publish differential fuzz and shared-workload benchmark evidence. +- [x] 05-03: Publish judge-facing documentation and verify local/public delivery evidence. ## Progress @@ -88,4 +93,4 @@ Plans: | 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | | 3. Conversion and Representation | 2/2 | Complete | 2026-08-01 | | 4. Operations and Palettes | 6/6 | Complete | 2026-08-01 | -| 5. Delivery Evidence | 0/TBD | Not started | - | +| 5. Delivery Evidence | 3/3 | Complete | 2026-08-01 | diff --git a/.planning/STATE.md b/.planning/STATE.md index ee8e67d8..7e7e85e9 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,34 +4,38 @@ ## Current Position -- Phase 5 of 5: Delivery Evidence -- Plan 05-02, Task 3 of 3: shared-workload benchmark not started -- Plans 1–4 are complete; Phase 5 Plan 05-01 and fuzz Tasks 05-02-01/02 are complete. +- Phase 5 of 5: Delivery Evidence — complete. +- All three Phase 5 plans and all milestone implementation plans are complete. +- Remaining human submission action: record, upload, and link the five-minute demo video. ## Verified Evidence +- Public repository: https://github.com/rajeet-04/TinyColor +- Unauthenticated `ls-remote` returned branch `rajeet` at + `1c218b669d192e37b7019b08395cf348410dde79`. +- GitHub Actions run 30686980719 passed `make verify`, Deno, build, and artifact + upload for that exact commit: + https://github.com/rajeet-04/TinyColor/actions/runs/30686980719 - Immutable JavaScript oracle hashes: 3/3 verified. -- Exact corpora: smoke 9/9, HEX/RGB 26/26, parser 23/23, conversion 35/35, operations 71/71. -- Go tests and vet pass; Node adapter, verifier, harness, and log-validator tests pass. +- Exact corpora: smoke 9/9, HEX/RGB 26/26, parser 23/23, conversion 35/35, + operations 71/71; 164/164 total and zero mismatches. +- Go tests/vet and 15 Node tests pass. - Immutable Deno source suite: 45 passed, 0 failed, 1 ignored. -- Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. -- GitHub Actions run 30675374085 passed verification, Deno, build, and artifact upload. +- Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, + zero divergences. +- Shared benchmark: 20 cold starts and 1,000 persistent requests per runtime, + with startup p99, latency p99, throughput, and peak RSS recorded. +- Go `unsafe` source occurrences: 0. ## Recent Decisions - Use the pinned V8-derived 8-bit luminance transfer table for exact `Math.pow` parity. -- Apply TinyColor's sub-one RGB rounding once at the `FromCompat` constructor boundary. -- Keep fuzz inputs broad and comparisons exact; never replace divergences with tolerance. - -## Remaining - -- Implement honest same-host startup p99, latency p99, throughput, and peak RSS benchmarks. -- Complete Plan 05-03 judge-facing documentation and final evidence refresh. -- Verify the pushed GitHub Actions run and public clone workflow. -- Human action: record and publish the five-minute demo video. +- Apply TinyColor's sub-one RGB rounding once at the `FromCompat` boundary. +- Keep fuzz comparisons exact and normalize benchmark workload newlines before hashing. +- Never claim external evidence until public access and exact-commit CI are observed. ## Session Continuity -Last session: 2026-08-01T00:20:50.023Z -Stopped at: Plan 05-02 Task 3, ready to implement benchmarks with TDD. -Resume file: `.planning/phases/05-delivery-evidence/.continue-here.md` +Last session: 2026-08-01 +Stopped at: Phase 5 complete; demo video remains a human submission action. +Resume file: none diff --git a/.planning/phases/05-delivery-evidence/05-VERIFICATION.md b/.planning/phases/05-delivery-evidence/05-VERIFICATION.md index 29382550..d0316953 100644 --- a/.planning/phases/05-delivery-evidence/05-VERIFICATION.md +++ b/.planning/phases/05-delivery-evidence/05-VERIFICATION.md @@ -1,6 +1,6 @@ --- phase: 5 -status: locally-verified +status: verified verified: 2026-08-01 requirements: [DEL-01, QLT-03] --- @@ -17,7 +17,7 @@ local evidence from external actions. | Criterion | Status | Evidence | |---|---|---| -| CI runs format, vet, Go tests, and differential checks | Configured; exact final run pending | `.github/workflows/port.yml`, `Makefile`; local constituent commands passed. | +| CI runs format, vet, Go tests, and differential checks | Passed | [Run 30686980719](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719) passed for exact commit `1c218b669d192e37b7019b08395cf348410dde79`. | | CLI supports parse, convert, lighten, palette, contrast, and JSON | Local pass | `make build` equivalent plus all five JSON invocations exited zero; implementation under `src/cmd/tinycolor-compat`. | | Benchmarks, docs, attribution, ownership, and known differences are complete | Local pass | `bench/results.json`, `bench/methodology.md`, `README.md`, `LICENSE`, `DECISIONS.md`, `COMPATIBILITY.md`, `docs/TEAM-OWNERSHIP.md`. | @@ -45,9 +45,12 @@ local evidence from external actions. ## External checkpoint -- Public repository: pending unauthenticated verification. -- Exact final-commit GitHub Actions run: pending push and observation. -- Demo video: not supplied. +- Public repository: https://github.com/rajeet-04/TinyColor +- Unauthenticated `git ls-remote` returned branch `rajeet` at exact commit + `1c218b669d192e37b7019b08395cf348410dde79`. +- Exact-commit CI: [GitHub Actions run 30686980719](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719) + passed `make verify`, `deno test test.js`, `make build`, and artifact upload. +- Demo video: not supplied and explicitly unclaimed. -Phase 5 remains open until public clone access and the exact successful CI run -are recorded. The missing video URL remains explicitly unclaimed. +Phase 5 delivery evidence is verified. The demo recording remains the only +human submission action. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 54f57dba..33d32016 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -66,8 +66,8 @@ observations, not universal speedup claims. - Differential Fuzz Survivor: eligible from the validated 60-second log. - Zero Unsafe: eligible from zero Go source occurrences. - Decision Log: eligible from 15 substantive decisions. -- GitHub Actions: configured; the final exact-commit run is not yet recorded here. -- Public repository visibility: not yet verified from an unauthenticated clone in this closeout. +- GitHub Actions: [run 30686980719](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719) passed the full gate, Deno, build, and artifact upload for commit `1c218b6`. +- Public repository visibility: unauthenticated `git ls-remote https://github.com/rajeet-04/TinyColor.git refs/heads/rajeet` returned exact commit `1c218b6`. - Five-minute demo video: not supplied. No known mismatch remains in the fixed corpus or recorded fuzz session. diff --git a/README.md b/README.md index e3f51de8..ac488d95 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Evidence: - [Benchmark results](bench/results.json) and [methodology](bench/methodology.md) — same-host startup p99, latency p99, throughput, and peak RSS. - [Architectural decisions](DECISIONS.md), [architecture](docs/ARCHITECTURE.md), [testing](docs/TESTING.md), and [team ownership](docs/TEAM-OWNERSHIP.md). - [Five-minute demo script](docs/DEMO.md). +- [Successful exact-commit CI run](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719). ## Repository layout From e208bd154aeeacb74372079ab2d2c0ade6d0911f Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 12:21:57 +0530 Subject: [PATCH 069/165] fix(compat): preserve formats and percent inputs --- src/cmd/tinycolor-compat/main.go | 3 ++- src/cmd/tinycolor-compat/main_test.go | 10 ++++++++++ src/internal/parser/parser.go | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 0f9ef3ea..197990e9 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -237,7 +237,8 @@ func handle(request compat.Request) compat.Response { return response } if request.Operation == "string" { - response, _ := compat.Success(request.ID, color.String()) + format, _ := args["format"].(string) + response, _ := compat.Success(request.ID, color.ToString(format)) return response } switch request.Operation { diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index c5449495..962c8d8e 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -112,6 +112,16 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { stdin: "{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", wantStdout: "{\"id\":\"red\",\"result\":{\"alpha\":1,\"format\":\"name\",\"original\":\"red\",\"rgb\":{\"a\":1,\"b\":0,\"g\":0,\"r\":255},\"valid\":true,\"value\":\"red\"}}\n", }, + { + name: "JSONL string forwards explicit format", + stdin: "{\"id\":\"string-format\",\"operation\":\"string\",\"input\":\"red\",\"args\":{\"format\":\"hsl\"}}\n", + wantStdout: "{\"id\":\"string-format\",\"result\":\"hsl(0, 100%, 50%)\"}\n", + }, + { + name: "JSONL inspect preserves one-percent HSL channels", + stdin: "{\"id\":\"hsl-one-percent\",\"operation\":\"inspect\",\"input\":\"hsl(115, 1%, 1%)\"}\n", + wantStdout: "{\"id\":\"hsl-one-percent\",\"result\":{\"alpha\":1,\"format\":\"hsl\",\"original\":\"hsl(115, 1%, 1%)\",\"rgb\":{\"a\":1,\"b\":3,\"g\":3,\"r\":3},\"valid\":true,\"value\":\"hsl(115, 1%, 1%)\"}}\n", + }, { name: "malformed JSONL request does not stop the stream", stdin: "{bad json\n{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", diff --git a/src/internal/parser/parser.go b/src/internal/parser/parser.go index 495cf5a1..f5c79ede 100644 --- a/src/internal/parser/parser.go +++ b/src/internal/parser/parser.go @@ -172,7 +172,7 @@ func hsvModel(h, s, v, alpha any, hasAlpha bool, original any) color.Model { } func ratio(value any) float64 { - if number := color.ParseFloat(value); !math.IsNaN(number) && number <= 1 { + if number := color.ParseFloat(value); !color.IsPercentage(value) && !math.IsNaN(number) && number <= 1 { value = formatPercent(number*100) + "%" } return color.Bound01(value, 100) From a29a15208cc07273d6050e2998b6e69e2ca40f7f Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 12:22:19 +0530 Subject: [PATCH 070/165] fix(tinycolor): match percentage output and equality --- src/tinycolor/color.go | 20 ++++++++++++++++---- src/tinycolor/color_test.go | 13 +++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 4c7a8e5a..a1f58c7e 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -85,8 +85,7 @@ func (c Color) ToRGBString() string { return fmt.Sprintf("rgba(%d, %d, %d, %s)", x.R, x.G, x.B, roundedAlpha(x.A)) } func (c Color) ToPercentageRGB() RGB { - x := c.ToRGB() - return RGB{percent(x.R), percent(x.G), percent(x.B), x.A} + return RGB{percent(c.model.R), percent(c.model.G), percent(c.model.B), c.model.A} } func (c Color) ToPercentageRGBString() string { x := c.ToPercentageRGB() @@ -238,13 +237,26 @@ func (c Color) IsDark() bool { return c.Brightness() < 128 } func (c Color) IsLight() bool { return !c.IsDark() } func (c Color) Clone() Color { n, _ := FromCompat(c.String(), false); return n } func Equals(a, b any) bool { - if a == nil || b == nil { + if !jsTruthy(a) || !jsTruthy(b) { return false } x, _ := FromCompat(a, false) y, _ := FromCompat(b, false) return x.ToRGBString() == y.ToRGBString() } +func jsTruthy(value any) bool { + switch value := value.(type) { + case nil: + return false + case bool: + return value + case string: + return value != "" + default: + number := color.ParseFloat(value) + return math.IsNaN(number) || number != 0 + } +} func Random() Color { return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatRGB}} } @@ -302,7 +314,7 @@ func (c Color) Inspect() map[string]any { return map[string]any{"valid": c.Valid(), "format": format, "alpha": c.Alpha(), "rgb": c.RGB(), "value": c.String(), "original": c.Original()} } -func percent(channel int) int { return int(math.Round(float64(channel) / 255 * 100)) } +func percent(channel float64) int { return mathRound(color.Bound01(channel, 255) * 100) } func roundedAlpha(alpha float64) string { return strconv.FormatFloat(math.Round(alpha*100)/100, 'f', -1, 64) } diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 311ec7db..44d7797b 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -37,6 +37,19 @@ func TestStringHexFilterAndConversion(t *testing.T) { } } +func TestPercentageRGBUsesUnroundedChannels(t *testing.T) { + c, _ := FromCompat(map[string]any{"h": float64(266), "s": float64(255), "v": float64(342), "a": float64(2.63)}, false) + if got := c.ToPercentageRGBString(); got != "rgb(43%, 0%, 100%)" { + t.Fatalf("ToPercentageRGBString() = %q", got) + } +} + +func TestEqualsRejectsFalsyInput(t *testing.T) { + if Equals("", map[string]any{"h": float64(529), "s": float64(15), "l": float64(-77), "a": float64(-1.132)}) { + t.Fatal("empty input must not equal normalized black") + } +} + func TestImplicitAlphaHexFallsBackToRGBA(t *testing.T) { for _, input := range []string{"#f008", "#ff000080"} { c, _ := FromCompat(input, false) From d5faef2a7220a1cfb965f5791ff3e488ca01d9c9 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 12:22:31 +0530 Subject: [PATCH 071/165] fix(tinycolor): brighten rounded RGB snapshots --- src/tinycolor/color.go | 7 ++++--- src/tinycolor/operations_test.go | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index a1f58c7e..c9a17797 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -393,9 +393,10 @@ func (c *Color) Greyscale() *Color { return c.Desaturate(100) } func (c *Color) Brighten(amount float64) *Color { delta := float64(-mathRound(-255 * amount / 100)) - c.model.R = clampChannel(c.model.R + delta) - c.model.G = clampChannel(c.model.G + delta) - c.model.B = clampChannel(c.model.B + delta) + rgb := c.ToRGB() + c.model.R = clampChannel(float64(rgb.R) + delta) + c.model.G = clampChannel(float64(rgb.G) + delta) + c.model.B = clampChannel(float64(rgb.B) + delta) return c } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 24c20323..314e5e38 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -102,6 +102,13 @@ func TestSaturateMatchesObjectConversionRounding(t *testing.T) { } } +func TestBrightenUsesRoundedRGBSnapshot(t *testing.T) { + color, _ := FromCompat("rgb(423.5294117647059%, 78.03921568627452%, -0.3921568627450981%)", false) + if got := color.Brighten(12.7).ToPercentageRGBString(); got != "rgb(67%, 91%, 13%)" { + t.Fatalf("Brighten(12.7) = %s", got) + } +} + func TestMix(t *testing.T) { black, _ := FromCompat("#000", false) white, _ := FromCompat("#fff", false) From a98ecbd40e2f0625dc529bd913a4de2f300f088a Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 12:23:55 +0530 Subject: [PATCH 072/165] docs(debug): record full-range parity fixes --- .planning/debug/knowledge-base.md | 8 + ...full-range-differential-inconsistencies.md | 144 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 .planning/debug/resolved/full-range-differential-inconsistencies.md diff --git a/.planning/debug/knowledge-base.md b/.planning/debug/knowledge-base.md index 162375a4..b591de51 100644 --- a/.planning/debug/knowledge-base.md +++ b/.planning/debug/knowledge-base.md @@ -11,3 +11,11 @@ Resolved debug sessions. Used by `gsd-debugger` to surface known-pattern hypothe - **Fix:** Route setHSL through the existing hslColor/parser conversion and remove the duplicate direct converter. - **Files changed:** src/tinycolor/color.go, src/tinycolor/operations_test.go, compat/cases/operations.jsonl --- + +## full-range-differential-inconsistencies — Broad adapter parity diverged across formatting, parsing, equality, and brighten +- **Date:** 2026-08-01 +- **Error patterns:** 6019 mismatches, string format hsl, hsl 1%, white, percentage RGB, equals empty input, brighten +- **Root cause:** Five shared-path gaps: string format was discarded; explicit percent strings were re-promoted as ratios; percentage RGB used pre-rounded channels; Equals omitted JavaScript falsy guards; Brighten skipped the oracle's rounded RGB snapshot. +- **Fix:** Forward string format; preserve explicit percentages; calculate percentage RGB from internal floats; apply JS truthiness before equality parsing; start Brighten from ToRGB channels. +- **Files changed:** src/cmd/tinycolor-compat/main.go, src/cmd/tinycolor-compat/main_test.go, src/internal/parser/parser.go, src/tinycolor/color.go, src/tinycolor/color_test.go, src/tinycolor/operations_test.go +--- diff --git a/.planning/debug/resolved/full-range-differential-inconsistencies.md b/.planning/debug/resolved/full-range-differential-inconsistencies.md new file mode 100644 index 00000000..51434799 --- /dev/null +++ b/.planning/debug/resolved/full-range-differential-inconsistencies.md @@ -0,0 +1,144 @@ +--- +status: resolved +trigger: "A deterministic 100,000-case Node-vs-Go stress run found 6,019 exact mismatches; fix known string-format and HSL percentage parser repros test-first, then classify remaining mismatches one root cause at a time." +created: 2026-08-01T11:57:15.5165643+05:30 +updated: 2026-08-01T12:17:02.4360622+05:30 +--- + +## Current Focus + +hypothesis: Confirmed resolved. +test: Completed focused RED/GREEN regressions, full repository checks, and repeated 100,000-case differential verification. +expecting: Zero mismatches and no test regressions. +next_action: Archive the resolved session and return the root-cause handoff without committing. + +## Symptoms + +expected: Exact JSON behavioral parity between compat/js-runner.mjs and the Go adapter for supported TinyColor public operations, including valid percentage strings and explicit toString formats. +actual: 6,019 mismatches in 100,000 seeded broad cases; known exact repros are `string` with input `red` and `args.format=hsl` returning `red` in Go versus `hsl(0, 100%, 50%)` in JS, and `inspect` with `hsl(115, 1%, 1%)` returning white in Go versus `rgb(3, 3, 3)` in JS. Initial distribution: string 5,341; inspect 35; fromRatio 43; output 41; analysis 26; equals 38; clone 45; modify 108; mix 90; readability 73; isReadable 60; mostReadable 82; palette 37; randomInvariant 0. +errors: Exact output mismatch, no crash. Known adapter branch at src/cmd/tinycolor-compat/main.go ignores args.format. Known parser ratio helper at src/internal/parser/parser.go may convert string `1%` into 100% because ParseFloat(`1%`) is 1. +reproduction: Build bin/tinycolor.exe, compare JSONL through `node compat/js-runner.mjs` and the Go binary. Minimal requests: `{"id":"string-format","operation":"string","input":"red","args":{"format":"hsl"}}` and `{"id":"hsl-one-percent","operation":"inspect","input":"hsl(115, 1%, 1%)"}`. Broad runner exists at C:\tmp\tinycolor-stress.mjs and was run from repo root. +started: Found today after narrower Phase 5 fuzz passed; likely pre-existing broad-domain gaps. + +## Eliminated + +- hypothesis: Correcting percentage RGB output would eliminate all remaining string/output/modify mismatches. + evidence: The next 100,000-case run eliminated string and output mismatches but retained 12 modify-only cases, all represented by brighten examples; modify has another independent root cause. + timestamp: 2026-08-01T12:07:40.3112231+05:30 +- hypothesis: TinyColor equality rejects all invalid parsed colors before comparison. + evidence: The immutable oracle only checks JavaScript top-level falsiness (`!color1 || !color2`) and then compares normalized RGB strings; truthy invalid colors may still compare equal as black. + timestamp: 2026-08-01T12:08:56.2349809+05:30 + +## Evidence + +- timestamp: 2026-08-01T11:58:08.3727772+05:30 + checked: `.planning/debug/knowledge-base.md` for overlap with mismatch, format, percentage, parser, and HSL symptoms. + found: The only entry concerns saturate rounding and has fewer than two overlapping symptom keywords. + implication: There is no qualifying known-pattern candidate; investigate the supplied repros directly. +- timestamp: 2026-08-01T11:58:08.3727772+05:30 + checked: Repository status before investigation. + found: Only the newly created debug session is untracked; no pre-existing user code edits are present. + implication: Subsequent source/test diffs can be attributed to this investigation while preserving the debug file separately. +- timestamp: 2026-08-01T11:58:39.5190467+05:30 + checked: Complete `src/cmd/tinycolor-compat/main.go` and its tests. + found: `handle` parses both inspect and string identically, but the string fast path calls `color.String()` and never reads `args["format"]`; the output/toString path correctly calls `color.ToString(format)`. + implication: The string-format mismatch is caused by a local adapter argument-loss defect, not TinyColor formatting itself. +- timestamp: 2026-08-01T11:58:39.5190467+05:30 + checked: Complete `src/internal/parser/parser.go`, `parser_test.go`, and `hsl.go`. + found: Shared `ratio` calls `ParseFloat` and promotes every numeric value `<= 1` to percentage text, including the literal string `"1%"`; `hslModel` and `hsvModel` both depend on it. + implication: The parser cannot distinguish normalized numeric ratios from already-explicit percentage strings and can turn 1% into 100% across HSL and HSV paths. +- timestamp: 2026-08-01T11:59:25.1113451+05:30 + checked: All `ratio` and `ToString` references plus complete `object.go`, `bounds.go`, `color.go`, `compat/js-runner.mjs`, and the broad stress generator. + found: `ratio` has exactly four production callers (HSL saturation/lightness and HSV saturation/value). `Bound01` already handles explicit `%` correctly. The Go adapter `string` branch and JS oracle both receive the same args, but only JS forwards `format`; the public Go `ToString(format)` behavior already exists and is tested. + implication: The smallest first fix is one adapter argument forward; the parser fix should preserve `ratio` for raw numbers while skipping its numeric-promotion step for percentage strings. +- timestamp: 2026-08-01T12:00:20.6632996+05:30 + checked: New `JSONL string forwards explicit format` regression before production changes. + found: RED as predicted: actual result was `red`; expected `hsl(0, 100%, 50%)`. + implication: The regression reproduces the supplied public mismatch and isolates the adapter string branch. +- timestamp: 2026-08-01T12:01:04.3955140+05:30 + checked: Focused adapter regression after forwarding `args.format` to `Color.ToString`. + found: GREEN: `go test ./cmd/tinycolor-compat -run TestRunJSONLAndUsageErrors -count=1` passed. + implication: The one-path fix corrects explicit string formats while preserving the existing JSONL adapter cases. +- timestamp: 2026-08-01T12:02:47.4046947+05:30 + checked: JS oracle and new exact JSONL regression for `hsl(115, 1%, 1%)` before parser changes. + found: Oracle returns RGB 3,3,3 and value `hsl(115, 1%, 1%)`; RED Go result is RGB 255,255,255 and `hsl(0, 0%, 100%)`. + implication: The regression exactly reproduces the supplied full-stack mismatch. +- timestamp: 2026-08-01T12:02:47.4046947+05:30 + checked: Immutable oracle `hslToRgb`, `hsvToRgb`, `bound01`, and `convertToPercentage` functions. + found: String HSL/HSV percentages go directly to `bound01`; object inputs use `convertToPercentage`, whose JavaScript `<=` coercion does not convert strings containing `%`. Go's shared `ratio` combines both paths but converts percentage strings incorrectly. + implication: Guarding the promotion with `!color.IsPercentage(value)` reproduces the oracle distinction without splitting the parser paths. +- timestamp: 2026-08-01T12:03:31.1535483+05:30 + checked: Exact HSL one-percent regression after guarding explicit percentages. + found: GREEN: `go test ./cmd/tinycolor-compat -run TestRunJSONLAndUsageErrors/JSONL_inspect_preserves_one-percent_HSL_channels -count=1` passed. + implication: The smallest parser change fixes the supplied public repro. +- timestamp: 2026-08-01T12:04:19.5752472+05:30 + checked: Unchanged deterministic 100,000-case broad stress after the two fixes. + found: Mismatches fell from 6,019 to 121. New distribution is string 1, output 6, equals 35, modify 79, and zero in every other operation. String/output/modify examples have identical rounded RGB but percentage strings differ by one point; equals examples compare invalid empty input against a different input that normalizes to black. + implication: The residuals collapse cleanly into two candidate root causes rather than requiring an architectural change. +- timestamp: 2026-08-01T12:05:16.6121994+05:30 + checked: All Go percentage-output callers, complete `color_test.go`, and immutable oracle `toPercentageRgb`/`toPercentageRgbString`. + found: Go `ToPercentageRGB` first calls `ToRGB`, rounding each internal channel to an integer; JavaScript applies `bound01` and percentage rounding directly to each internal float channel. All Go percentage strings route through `ToPercentageRGB`. + implication: A shared two-line correction in `ToPercentageRGB` should eliminate percentage string differences across direct output, string formatting, and modified-color inspections. +- timestamp: 2026-08-01T12:05:58.0357035+05:30 + checked: New direct percentage-output regression before production changes. + found: RED as predicted: Go returned `rgb(44%, 0%, 100%)`; expected `rgb(43%, 0%, 100%)`. + implication: Integer RGB pre-rounding is directly observable and sufficient to cause the residual output cluster. +- timestamp: 2026-08-01T12:06:37.9971179+05:30 + checked: Focused percentage-output regression after computing from internal channels. + found: GREEN: `go test ./tinycolor -run TestPercentageRGBUsesUnroundedChannels -count=1` passed. + implication: The shared output method now matches the oracle example; broad verification can test all three affected operation families. +- timestamp: 2026-08-01T12:07:40.3112231+05:30 + checked: Deterministic 100,000-case stress after the percentage-output fix. + found: Mismatches fell from 121 to 47: equals 35 and modify 12; every other operation is exact. All string/output mismatches and 67 of 79 modify mismatches disappeared. + implication: Percentage output is confirmed across its consumers; invalid equality and brighten behavior remain separate clusters. +- timestamp: 2026-08-01T12:08:56.2349809+05:30 + checked: Every `Equals` caller and complete immutable oracle equality function. + found: JS returns false for any falsy top-level argument, including empty string; Go checks only `nil` and otherwise compares `ToRGBString`, so empty string becomes invalid black and can equal a clamped black input. + implication: Equality needs JSON-compatible JS truthiness at its shared public entry point, not validity checks in callers. +- timestamp: 2026-08-01T12:10:06.1947734+05:30 + checked: New exact equality stress regression before production changes. + found: RED: Go reported the empty input equal to the clamped-black HSL object. + implication: The missing falsy guard directly reproduces the full equality cluster mechanism. +- timestamp: 2026-08-01T12:11:07.7954806+05:30 + checked: Focused equality regression after adding JSON-compatible falsy handling. + found: GREEN: `go test ./tinycolor -run TestEqualsRejectsFalsyInput -count=1` passed. + implication: Empty string is now rejected before invalid-as-black comparison at the shared equality entry point. +- timestamp: 2026-08-01T12:11:45.4137675+05:30 + checked: Deterministic 100,000-case stress after the equality fix. + found: Exactly 12 mismatches remain, all in modify and both captured examples use brighten; every other operation is exact, including equals. + implication: Equality is broadly confirmed. Brighten is the only remaining behavior path. +- timestamp: 2026-08-01T12:12:38.0636370+05:30 + checked: Complete oracle constructor, `_applyModification`, and `brighten`, plus every Go Brighten caller and complete modifier tests. + found: Oracle brighten calls `tinycolor(color).toRgb()` before applying its rounded delta, forcing every starting channel to an integer. Go applies the same delta rounding directly to `model` floats. `_applyModification` then copies the oracle result channels back while preserving original format metadata. + implication: Starting Brighten from Go's existing `ToRGB()` snapshot matches the oracle mechanism and explains both the percentage and achromatic-hue examples. +- timestamp: 2026-08-01T12:13:14.6239668+05:30 + checked: New exact brighten stress regression before production changes. + found: RED as predicted: Go returned `rgb(68%, 91%, 13%)`; expected `rgb(67%, 91%, 13%)`. + implication: Retaining the pre-brighten fractional channel reproduces the final residual mechanism. +- timestamp: 2026-08-01T12:13:55.0190481+05:30 + checked: Focused brighten regression after applying the delta to `ToRGB()` channels. + found: GREEN: `go test ./tinycolor -run TestBrightenUsesRoundedRGBSnapshot -count=1` passed. + implication: The final identified root cause is corrected; broad verification can now test completeness. +- timestamp: 2026-08-01T12:15:21.2484424+05:30 + checked: Final deterministic 100,000-case stress after all five fixes. + found: Zero mismatches in every operation category. + implication: The original 6,019-case parity failure is eliminated across the unchanged broad input domain. +- timestamp: 2026-08-01T12:15:21.2484424+05:30 + checked: `go test ./... -count=1`, `go vet ./...`, `node --test tests/port/adapter.test.mjs`, `gofmt`, and `git diff --check`. + found: All checks passed; diff check emitted only existing Windows LF-to-CRLF conversion warnings. + implication: No test, vet, adapter, formatting, or whitespace regression was detected. +- timestamp: 2026-08-01T12:16:16.8806534+05:30 + checked: Full verification after simplifying equality truthiness through existing `color.ParseFloat`. + found: `go test ./... -count=1`, `go vet ./...`, adapter tests, and `git diff --check` passed; the repeated 100,000-case stress again reported zero mismatches in every category. + implication: The final minimal diff is stable under focused, repository-wide, and broad differential checks. +- timestamp: 2026-08-01T12:17:02.4360622+05:30 + checked: Human/primary-agent verification checkpoint. + found: The broad stress and focused checks were accepted as green, with explicit instruction to finalize the session as resolved without commits. + implication: The session can be archived; source and planning changes remain uncommitted for primary-agent micro-commit review. + +## Resolution + +root_cause: Five independent shared-path gaps caused all 6,019 mismatches: string format was discarded; explicit percent strings were re-promoted as ratios; percentage RGB used pre-rounded channels; Equals omitted JavaScript falsy guards; Brighten skipped the oracle's rounded RGB snapshot. +fix: Forward string format; preserve explicit HSL/HSV percentages; calculate percentage RGB from internal floats; apply JS truthiness before equality parsing; start Brighten from `ToRGB()` channels. +verification: Each root cause received a regression that failed before its fix and passed afterward. The final deterministic 100,000-case run passed three times with zero mismatches. Full Go tests, Go vet, 164/164 compatibility cases, port adapter tests, immutable Deno tests (45 passed, 0 failed, 1 ignored), gofmt, build, and diff checks passed. +files_changed: [src/cmd/tinycolor-compat/main.go, src/cmd/tinycolor-compat/main_test.go, src/internal/parser/parser.go, src/tinycolor/color.go, src/tinycolor/color_test.go, src/tinycolor/operations_test.go] From 57df1843f24db0f81cf4b73427768266c324a5f0 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 13:58:34 +0530 Subject: [PATCH 073/165] fix(compat): forward fromRatio options --- src/cmd/tinycolor-compat/main.go | 4 +++- src/cmd/tinycolor-compat/main_test.go | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 197990e9..e4e817ab 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -224,7 +224,9 @@ func handle(request compat.Request) compat.Response { case "inspect", "string": color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": - color, err = tinycolor.FromCompat(request.Input, true) + format, _ := args["format"].(string) + gradientType, _ := args["gradientType"].(bool) + color, err = tinycolor.FromCompatWithOptions(request.Input, true, tinycolor.CompatOptions{Format: format, GradientType: gradientType}) case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable", "palette": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) case "equals", "randomInvariant": diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index 962c8d8e..cee3ff63 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -117,6 +117,11 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { stdin: "{\"id\":\"string-format\",\"operation\":\"string\",\"input\":\"red\",\"args\":{\"format\":\"hsl\"}}\n", wantStdout: "{\"id\":\"string-format\",\"result\":\"hsl(0, 100%, 50%)\"}\n", }, + { + name: "JSONL fromRatio forwards explicit format", + stdin: "{\"id\":\"ratio-format\",\"operation\":\"fromRatio\",\"input\":{\"r\":1,\"g\":0,\"b\":0,\"a\":1},\"args\":{\"format\":\"hex\"}}\n", + wantStdout: "{\"id\":\"ratio-format\",\"result\":{\"alpha\":1,\"format\":\"hex\",\"original\":{\"a\":1,\"b\":\"0%\",\"g\":\"0%\",\"r\":\"100%\"},\"rgb\":{\"a\":1,\"b\":0,\"g\":0,\"r\":255},\"valid\":true,\"value\":\"#ff0000\"}}\n", + }, { name: "JSONL inspect preserves one-percent HSL channels", stdin: "{\"id\":\"hsl-one-percent\",\"operation\":\"inspect\",\"input\":\"hsl(115, 1%, 1%)\"}\n", From 0b685e2b8445e8fc741edf0c83127f138d17284d Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 13:58:34 +0530 Subject: [PATCH 074/165] fix(parser): match compressed reverse names --- src/internal/parser/names.go | 18 +++++++++--------- src/tinycolor/color_test.go | 10 ++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/internal/parser/names.go b/src/internal/parser/names.go index 48fc7ff3..7b986c64 100644 --- a/src/internal/parser/names.go +++ b/src/internal/parser/names.go @@ -19,25 +19,25 @@ var ( hexNames = func() map[string]string { result := make(map[string]string) for name, sourceHex := range names { - hex := sourceHex - if len(hex) == 3 { - hex = string([]byte{hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]}) - } - result[hex] = name + result[sourceHex] = name } - result["00ffff"] = "cyan" - result["ff00ff"] = "magenta" + result["0ff"] = "cyan" + result["f0f"] = "magenta" result["808080"] = "grey" result["a9a9a9"] = "darkgrey" result["2f4f4f"] = "darkslategrey" result["696969"] = "dimgrey" result["d3d3d3"] = "lightgrey" - result["778899"] = "lightslategrey" + result["789"] = "lightslategrey" result["708090"] = "slategrey" return result }() ) func NameForRGB(r, g, b int) string { - return hexNames[fmt.Sprintf("%02x%02x%02x", r, g, b)] + hex := fmt.Sprintf("%02x%02x%02x", r, g, b) + if hex[0] == hex[1] && hex[2] == hex[3] && hex[4] == hex[5] { + hex = string([]byte{hex[0], hex[2], hex[4]}) + } + return hexNames[hex] } diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 44d7797b..74bf8a5b 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -102,3 +102,13 @@ func TestInvalidInputIsAColorState(t *testing.T) { t.Fatalf("invalid input = %#v, %v", color, err) } } + +func TestCompressibleSixDigitNameFallsBackToHex(t *testing.T) { + color, _ := FromCompat("rebeccapurple", false) + if name, ok := color.ToName(); ok || name != "" { + t.Fatalf("ToName() = %q, %t", name, ok) + } + if color.String() != "#663399" || color.Clone().String() != "#663399" { + t.Fatalf("color = %q, clone = %q", color.String(), color.Clone().String()) + } +} From e918b46ce216c045da63768e59fafebd9120dc50 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 13:58:34 +0530 Subject: [PATCH 075/165] fix(tinycolor): match JavaScript numeric precision --- src/tinycolor/color.go | 10 ++++++++-- src/tinycolor/color_test.go | 7 +++++++ src/tinycolor/operations_test.go | 8 ++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index c9a17797..36b8037a 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -260,7 +260,13 @@ func jsTruthy(value any) bool { func Random() Color { return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatRGB}} } -func mathRound(v float64) int { return int(math.Floor(v + .5)) } +func mathRound(v float64) int { + floor := math.Floor(v) + if v-floor >= .5 { + floor++ + } + return int(floor) +} // String supplies the minimal source-compatible string snapshot used by the // JSONL oracle. Full public output APIs remain Phase 3 work. @@ -328,7 +334,7 @@ func rgbToHSL(r, g, b float64) (float64, float64, float64) { } d := max - min s := d / (2 - max - min) - if l < 0.5 { + if l <= 0.5 { s = d / (max + min) } var h float64 diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 74bf8a5b..8b4466a1 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -44,6 +44,13 @@ func TestPercentageRGBUsesUnroundedChannels(t *testing.T) { } } +func TestOnePercentRGBMatchesJavaScriptHSLRounding(t *testing.T) { + color, _ := FromCompat(map[string]any{"r": "1%", "g": "0%", "b": "0%", "a": float64(1)}, false) + if got := color.ToHSLString(); got != "hsl(0, 100%, 0%)" { + t.Fatalf("ToHSLString() = %q", got) + } +} + func TestEqualsRejectsFalsyInput(t *testing.T) { if Equals("", map[string]any{"h": float64(529), "s": float64(15), "l": float64(-77), "a": float64(-1.132)}) { t.Fatal("empty input must not equal normalized black") diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 314e5e38..b5436d2c 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -293,6 +293,14 @@ func TestPaletteConversionUsesTinyColorChannelBounds(t *testing.T) { } } +func TestPaletteHSLMetadataMatchesJavaScriptPrecision(t *testing.T) { + color, _ := FromCompat("#967a", false) + original, ok := color.Complement().Original().(map[string]any) + if !ok || original["s"] != 0.19999999999999996 { + t.Fatalf("complement original = %#v", original) + } +} + func paletteHex(colors []Color) string { values := make([]string, len(colors)) for index, color := range colors { From 5cbb6683115bf0666998340775e2c62271268d31 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 13:58:34 +0530 Subject: [PATCH 076/165] docs(decisions): record analogous safety divergence --- DECISIONS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DECISIONS.md b/DECISIONS.md index 6221f0ad..a8773f44 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -17,3 +17,4 @@ | D-013 | Benchmark both adapters with the same fixed JSONL workload and lifecycle. | Shared inputs and same-host cold/persistent runs make the measurements directly reproducible. | Accepted | | D-014 | Normalize benchmark workload newlines before hashing. | Evidence hashes remain stable across Windows and Unix checkouts without changing request content. | Accepted | | D-015 | Publish unsupported or external deliverables as unverified. | Honest missing evidence is preferable to unreproducible CI, public-access, or video claims. | Accepted | +| D-016 | Return an empty typed Go palette for non-positive counts instead of reproducing TinyColor's non-terminating negative `analogous` loop. | Compatibility must not make a public Go call hang indefinitely; adapter zero values still retain TinyColor's documented defaults. | Accepted | From 97a91739cc3f64f30e2844afd200ca9bf0996e06 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 14:45:49 +0530 Subject: [PATCH 077/165] docs: design original-suite Go bridge --- ...6-08-01-original-suite-go-bridge-design.md | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-01-original-suite-go-bridge-design.md diff --git a/docs/superpowers/specs/2026-08-01-original-suite-go-bridge-design.md b/docs/superpowers/specs/2026-08-01-original-suite-go-bridge-design.md new file mode 100644 index 00000000..b2fc4134 --- /dev/null +++ b/docs/superpowers/specs/2026-08-01-original-suite-go-bridge-design.md @@ -0,0 +1,143 @@ +# Original Suite Against Go Bridge Design + +## Goal + +Run the pinned, byte-identical `test.js` suite against the compiled Go port and +report its exact pass, fail, and ignored counts. The checked-in JavaScript oracle +files remain unchanged and continue to match `tests/original/manifest.sha256`. + +## Hackathon interpretation + +The original suite is the test specification, not part of the port. A thin +JavaScript test facade may preserve JavaScript-only object identity, constructor, +and chaining semantics, but every color calculation must be performed by the +submitted native Go artifact. The facade must never import or execute the +original `mod.js` or `tinycolor.js` implementation. + +This is distinct from the prohibited pattern of a port shelling out to its source +implementation: the tests invoke the Go port. The shipped Go library and CLI do +not invoke JavaScript. + +## Architecture + +### Byte-identical test overlay + +`tests/original-go/run.mjs` will: + +1. Verify the existing kickoff manifest. +2. Build the native `tinycolor-compat` binary. +3. Create a temporary directory. +4. Copy root `test.js` into that directory without transforming it. +5. Verify the copied bytes have the same SHA-256 as root `test.js`. +6. Place the checked-in Go-backed facade beside it as `mod.js`. +7. run `deno test` on the copied `test.js` with permission to execute only the + compiled Go binary. +8. Remove the temporary directory after the child process exits. + +The suite therefore resolves its unchanged `import tinycolor from "./mod.js"` +to the test facade while the source test file itself stays byte-identical. + +### One-request native bridge + +The compatibility executable will gain a test-only one-request mode that accepts +one JSON request as a direct process argument and writes one JSON response to +stdout. The existing request decoder and `handle` path remain the single dispatch +implementation for persistent JSONL fuzzing and one-request suite calls. + +The facade will use `Deno.Command(...).outputSync()` without a shell. Each call +starts the compiled Go executable directly, so JavaScript's synchronous TinyColor +API remains synchronous. Requests are small enough to remain below normal command +line limits; malformed responses or non-zero exits fail the suite immediately +with the Go stderr attached. + +### JavaScript facade boundary + +`tests/original-go/mod.js` will expose the function/constructor shape expected by +the source suite. A facade color stores: + +- the original JavaScript input reference for `getOriginalInput()`; +- the current RGBA snapshot returned by Go; +- the source format and gradient option required for default string behavior. + +The facade itself may implement only JavaScript runtime semantics: + +- calling `tinycolor(existingFacade)` returns the same object; +- `new tinycolor(existingFacade)` returns the same object; +- instance modifiers mutate and return the same facade; +- `clone()` returns an independent facade; +- palette and static utility results are wrapped as facade instances. + +Parsing, normalization, formatting, alpha bounds, conversion, modification, +mixing, readability, and palette generation must come from Go bridge responses. +No TinyColor color algorithm may be reimplemented in the facade. + +## Go adapter coverage + +The existing compatibility operations will be reused and minimally extended for +source-suite methods that currently lack a result shape: + +- object outputs: RGB, percentage RGB, HSL, and HSV; +- alpha mutation; +- random-color snapshots; +- filter gradient options; +- any source-suite method discovered by the failing full-suite run. + +Extensions belong in the existing compatibility dispatcher and public Go color +methods. They must be independently covered by focused Go or Node adapter tests +before the facade uses them. + +## Test-driven delivery + +Implementation proceeds in narrow red-green cycles: + +1. A runner-integrity test fails until the copied `test.js` hash is proven equal. +2. A facade smoke test fails until a basic source assertion is served by Go. +3. The full source suite is run against the facade; each unsupported method or + mismatch becomes one focused regression before the minimum adapter extension. +4. The source suite must finish with 45 passed, 0 failed, and 1 upstream-ignored + `polyad` test. +5. `make verify` must also retain the existing oracle, Go, Node, fuzz-log, + compatibility-corpus, and vet gates. + +The root `test.js`, `mod.js`, and `tinycolor.js` hashes are checked before and +after the work. A changed oracle file is a hard failure. + +## Commands and CI + +`make test-original-go` will run the bridge-backed original suite. `make verify` +will include that target so GitHub Actions cannot pass while exercising only the +JavaScript oracle. The README and `COMPATIBILITY.md` will distinguish: + +- the original suite against original JavaScript; and +- the same byte-identical suite against the Go-backed facade. + +Both commands and exact counts will be published. GitHub Actions must pass at the +final pushed commit before the deliverable is marked complete. + +## Error handling + +- Missing Deno or Go exits with an actionable command failure. +- A binary exit, timeout, invalid JSON response, or response-ID mismatch fails the + current assertion instead of falling back to JavaScript. +- The temporary overlay is deleted in a `finally` path. +- No mismatch is hidden with floating-point tolerance or rewritten expectations. + +## Non-goals + +- Do not edit, patch, transpile, or regenerate `test.js`. +- Do not replace the production `mod.js` oracle. +- Do not add WebAssembly, FFI, native addons, or third-party dependencies. +- Do not reproduce TinyColor algorithms in JavaScript. +- Do not claim coverage for future upstream TinyColor revisions. + +## Acceptance criteria + +- All three kickoff oracle hashes remain unchanged. +- The copied source test has the same SHA-256 as root `test.js`. +- The facade never imports the original TinyColor implementation. +- Every observable color result used by the suite is returned by the Go binary. +- The bridge-backed run reports 45 passed, 0 failed, and 1 ignored. +- Existing verification and differential checks remain green. +- README, compatibility evidence, Makefile, and CI contain the reproducible + bridge-backed command. +- The final commit is pushed and its GitHub Actions run succeeds. From 421626893d6e8122da0bb3767d16c95a75fe818d Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 15:01:18 +0530 Subject: [PATCH 078/165] docs: plan original-suite Go bridge --- .../2026-08-01-original-suite-go-bridge.md | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-01-original-suite-go-bridge.md diff --git a/docs/superpowers/plans/2026-08-01-original-suite-go-bridge.md b/docs/superpowers/plans/2026-08-01-original-suite-go-bridge.md new file mode 100644 index 00000000..ac3250d4 --- /dev/null +++ b/docs/superpowers/plans/2026-08-01-original-suite-go-bridge.md @@ -0,0 +1,389 @@ +# Original Suite Against Go Bridge Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Run the byte-identical original `test.js` suite against the native Go port with 45 passed, 0 failed, and 1 ignored. + +**Architecture:** A temporary overlay pairs an unchanged copy of `test.js` with a test-only synchronous JavaScript facade. The facade invokes a one-request mode of the existing Go compatibility binary directly through `Deno.Command`; it preserves JavaScript object identity while all color behavior comes from Go. + +**Tech Stack:** Go 1.26 standard library, Deno 2 standard APIs, Node.js test runner, GNU Make, GitHub Actions. + +## Global Constraints + +- Do not modify root `test.js`, `mod.js`, or `tinycolor.js`. +- Do not import or execute the original TinyColor implementation from the facade. +- Do not add dependencies, WebAssembly, FFI, native addons, or JavaScript color algorithms. +- Reuse the existing compatibility request decoder and dispatcher. +- Add one focused failing check before each production behavior change. +- Keep commits small and single-purpose. + +--- + +### Task 1: One-request Go process mode + +**Files:** +- Modify: `src/cmd/tinycolor-compat/main_test.go` +- Modify: `src/cmd/tinycolor-compat/main.go` + +**Interfaces:** +- Consumes: existing `compat.Decode`, `handle`, and `write` functions. +- Produces: `tinycolor bridge `, which writes exactly one encoded compatibility response. + +- [ ] **Step 1: Write the failing command test** + +Add a table row to `TestRunJSONLAndUsageErrors`: + +```go +{ + name: "one-request bridge", + args: []string{"bridge", `{"id":"bridge-red","operation":"output","input":"red","args":{"method":"toHexString"}}`}, + wantStdout: "{\"id\":\"bridge-red\",\"result\":\"#ff0000\"}\n", +}, +``` + +Add rows proving a missing request returns status 2 and malformed JSON returns a structured error without a panic. + +- [ ] **Step 2: Run the focused test and verify RED** + +Run: + +```powershell +$env:GOCACHE='R:\Code\TinyColor\.cache\go-build' +go -C src test ./cmd/tinycolor-compat -run TestRunJSONLAndUsageErrors -count=1 +``` + +Expected: FAIL because `bridge` is treated as an unknown command. + +- [ ] **Step 3: Add the minimum one-request path** + +Route `bridge` in `run` and decode its single argument through the existing protocol: + +```go +case "bridge": + if len(args) != 2 { + usage(stderr, "bridge ") + return 2 + } + request, err := compat.Decode([]byte(args[1])) + if err != nil { + response, _ := compat.Failure("", err.Error()) + write(response, stdout, stderr) + return 1 + } + write(handle(request), stdout, stderr) + return 0 +``` + +- [ ] **Step 4: Run focused and package tests and verify GREEN** + +```powershell +go -C src test ./cmd/tinycolor-compat -count=1 +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```powershell +git add src/cmd/tinycolor-compat/main.go src/cmd/tinycolor-compat/main_test.go +git commit -m "feat(compat): add one-request bridge mode" +``` + +--- + +### Task 2: Source-suite adapter operations + +**Files:** +- Modify: `src/tinycolor/color.go` +- Modify: `src/tinycolor/color_test.go` +- Modify: `src/internal/parser/names.go` +- Modify: `src/internal/parser/parser_test.go` +- Modify: `src/cmd/tinycolor-compat/main.go` +- Modify: `src/cmd/tinycolor-compat/main_test.go` + +**Interfaces:** +- Consumes: existing `Color` conversion, formatting, mutation, palette, and readability methods. +- Produces: bridge results for object conversions, compact hex, alpha mutation, random snapshots, gradient filters, and the Go-owned color-name map. + +- [ ] **Step 1: Write failing Go API tests for alpha and names** + +Add `TestSetAlphaMatchesTinyColorBounds` to `src/tinycolor/color_test.go`, asserting that `0.9` is retained and `-1`, `2`, `nil`, and `"test"` normalize to `1` while returning the same receiver. + +Add `TestNamesReturnsIndependentCopy` to `src/internal/parser/parser_test.go`, asserting `Names()["red"] == "ff0000"` and that mutating the returned map does not alter a second call. + +- [ ] **Step 2: Run focused tests and verify RED** + +```powershell +go -C src test ./tinycolor ./internal/parser -run 'Test(SetAlphaMatchesTinyColorBounds|NamesReturnsIndependentCopy)' -count=1 +``` + +Expected: build failure because `SetAlpha` and `Names` do not exist. + +- [ ] **Step 3: Implement the two minimal Go APIs** + +Add: + +```go +func (c *Color) SetAlpha(value any) *Color { + c.model.A = color.BoundAlpha(value) + return c +} +``` + +and an exported `parser.Names()` that returns a copied map using the standard `maps.Clone` function. + +- [ ] **Step 4: Add failing bridge result cases** + +Extend the command test table with exact requests for: + +- `output/toRgb`, `output/toPercentageRgb`, `output/toHsl`, and `output/toHsv`; +- compact `toHex`, `toHexString`, `toHex8`, `toHex8String`, `toString("hex3")`, and `toString("hex4")`; +- `setAlpha` returning the updated inspection; +- `random` returning a valid `prgb` inspection; +- `names` containing `red` and `rebeccapurple`; +- `toFilter` with `gradientType: true`. + +Run the focused command tests. Expected: FAIL with unsupported operation or method. + +- [ ] **Step 5: Extend the existing dispatcher only** + +Add `setAlpha`, `random`, and `names` cases to `handle`. Extend `output` with lowercase-keyed object maps and a boolean `compact` argument. Use one helper that compresses `rrggbb` or `rrggbbaa` only when every pair has equal digits. Pass `gradientType` to `ToFilter` rather than duplicating filter generation. + +The object shapes must be: + +```go +map[string]any{"r": rgb.R, "g": rgb.G, "b": rgb.B, "a": rgb.A} +map[string]any{"r": fmt.Sprintf("%d%%", rgb.R), "g": fmt.Sprintf("%d%%", rgb.G), "b": fmt.Sprintf("%d%%", rgb.B), "a": rgb.A} +map[string]any{"h": hsl.H, "s": hsl.S, "l": hsl.L, "a": hsl.A} +map[string]any{"h": hsv.H, "s": hsv.S, "v": hsv.V, "a": hsv.A} +``` + +- [ ] **Step 6: Run all Go and fixed compatibility tests** + +```powershell +gofmt -w src/tinycolor/color.go src/tinycolor/color_test.go src/internal/parser/names.go src/internal/parser/parser_test.go src/cmd/tinycolor-compat/main.go src/cmd/tinycolor-compat/main_test.go +go -C src test ./... -count=1 +node compat/run.mjs compat/cases/smoke.jsonl +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/parser.jsonl +node compat/run.mjs compat/cases/conversion.jsonl +node compat/run.mjs compat/cases/operations.jsonl +``` + +Expected: all pass with zero mismatches. + +- [ ] **Step 7: Commit** + +```powershell +git add src +git commit -m "feat(compat): expose original-suite operations" +``` + +--- + +### Task 3: Byte-identical overlay and synchronous facade + +**Files:** +- Create: `tests/original-go/mod.js` +- Create: `tests/original-go/facade.test.mjs` +- Create: `tests/original-go/run.mjs` +- Create: `tests/original-go/run.test.mjs` + +**Interfaces:** +- Consumes: `TINYCOLOR_GO_BINARY`, `tinycolor bridge `, root `test.js`, and `tests/original/manifest.sha256`. +- Produces: a default-exported TinyColor-compatible facade and a runner that exits with Deno's source-suite status. + +- [ ] **Step 1: Write the failing runner integrity test** + +In `run.test.mjs`, import a planned `prepareOverlay()` and assert: + +```js +const overlay = await prepareOverlay(); +try { + assert.equal( + createHash("sha256").update(readFileSync(overlay.testFile)).digest("hex"), + createHash("sha256").update(readFileSync("test.js")).digest("hex"), + ); + assert.equal(readFileSync(overlay.testFile).equals(readFileSync("test.js")), true); +} finally { + overlay.cleanup(); +} +``` + +Run `node --test tests/original-go/run.test.mjs`. Expected: FAIL because the module does not exist. + +- [ ] **Step 2: Implement overlay preparation** + +Use `mkdtempSync`, `copyFileSync`, `copyFileSync` for the facade as `mod.js`, byte comparison, and `rmSync(..., { recursive: true, force: true })`. Call the existing hash verifier before copying. Export `prepareOverlay()` for the Node test. + +- [ ] **Step 3: Write the failing Deno facade smoke test** + +Build the binary, set `TINYCOLOR_GO_BINARY`, then assert through the facade: + +```js +assertEquals(tinycolor("red").toHexString(), "#ff0000"); +const color = tinycolor("red"); +assert(color.lighten(10) === color); +assertEquals(color.toHexString(), "#ff3333"); +assertEquals(tinycolor.fromRatio({ r: 1, g: 0, b: 0 }).toRgb(), { r: 255, g: 0, b: 0, a: 1 }); +``` + +Run the Deno smoke test. Expected: FAIL because the facade does not exist. + +- [ ] **Step 4: Implement the minimal facade transport and state wrapper** + +Implement `invoke(operation, input, args)` with direct `Deno.Command` execution, response-ID validation, and no shell. Implement input unwrapping and a facade constructor that stores original input, current RGBA, format, and gradient type from Go inspection responses. + +Provide all instance APIs exercised by `test.js`: + +```text +getOriginalInput getFormat getAlpha setAlpha isValid clone +toRgb toPercentageRgb toHsl toHsv +toRgbString toPercentageRgbString toHslString toHsvString +toHex toHexString toHex8 toHex8String toName toFilter toString +getBrightness getLuminance isDark isLight +lighten brighten darken saturate desaturate greyscale spin +complement analogous monochromatic splitcomplement triad tetrad +``` + +Provide static APIs: + +```text +fromRatio random equals mix readability isReadable mostReadable names +``` + +Modifiers replace the facade's current Go snapshot and return the same facade. Static and palette results wrap returned Go inspections. `names` comes from the Go `names` operation at module initialization. + +- [ ] **Step 5: Run the smoke test and verify GREEN** + +```powershell +$env:TINYCOLOR_GO_BINARY='R:\Code\TinyColor\bin\tinycolor.exe' +deno test --allow-run=$env:TINYCOLOR_GO_BINARY tests/original-go/facade.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```powershell +git add tests/original-go +git commit -m "test(original): add Go-backed TinyColor facade" +``` + +--- + +### Task 4: Full original-suite parity gate + +**Files:** +- Modify: `tests/original-go/mod.js` +- Modify: `src/cmd/tinycolor-compat/main.go` +- Modify: `src/cmd/tinycolor-compat/main_test.go` +- Modify: `Makefile` +- Modify: `.github/workflows/port.yml` + +**Interfaces:** +- Consumes: the complete original `test.js` API inventory and overlay runner. +- Produces: `make test-original-go` and a required CI gate. + +- [ ] **Step 1: Run the full overlay suite and verify integration RED** + +```powershell +node tests/original-go/run.mjs +``` + +Expected: the runner reaches the original suite but reports unsupported facade behavior or assertion mismatches until every mapped method is correct. + +- [ ] **Step 2: Close failures one behavior at a time** + +For each failure, first add the smallest focused case to `main_test.go` or `facade.test.mjs`, run it to observe the same failure, then change only the implicated dispatcher or facade method. The complete mapped scope is the instance and static API list in Task 3; `polyad` remains the one ignored upstream test and must not be implemented for this deliverable. + +- [ ] **Step 3: Verify the exact full-suite result** + +Run `node tests/original-go/run.mjs`. Expected final Deno summary: + +```text +45 passed | 0 failed | 1 ignored +``` + +Then run `node tests/original/verify.mjs`. Expected: `verified: 3`. + +- [ ] **Step 4: Add Make and CI gates** + +Add: + +```make +test-original-go: build + node tests/original-go/run.mjs +``` + +Include `test-original-go` in `verify` or `test`, and keep the separate `deno test test.js` oracle check in CI. GitHub Actions must therefore run both the source implementation suite and the byte-identical Go-backed suite. + +- [ ] **Step 5: Run the complete local gate** + +On Windows, run the Makefile commands directly with repository-local `GOCACHE`; on CI-compatible shells run `make verify` and `deno test test.js`. Expected: all gates pass. + +- [ ] **Step 6: Commit** + +```powershell +git add tests/original-go src Makefile .github/workflows/port.yml +git commit -m "test(original): run unchanged suite against Go" +``` + +--- + +### Task 5: Publish honest completion evidence + +**Files:** +- Modify: `DECISIONS.md` +- Modify: `README.md` +- Modify: `COMPATIBILITY.md` +- Modify: `docs/TESTING.md` +- Modify: `.planning/STATE.md` + +**Interfaces:** +- Consumes: exact local command output and final CI URL. +- Produces: reproducible submission evidence that distinguishes source-oracle and Go-backed suite runs. + +- [ ] **Step 1: Record the architectural decision** + +Add D-017: use a byte-identical temporary source-test overlay and synchronous test-only facade invoking the native Go binary. Rationale: this runs original assertions against the port without changing kickoff files, copying algorithms into JavaScript, or introducing a second WASM implementation. + +- [ ] **Step 2: Update evidence documents** + +Document both exact results: + +```text +deno test test.js 45 passed, 0 failed, 1 ignored against source oracle +node tests/original-go/run.mjs 45 passed, 0 failed, 1 ignored against native Go port +``` + +Update the submission checklist item for original-suite-against-port from partial to complete. Keep the demo video unchecked. + +- [ ] **Step 3: Run final verification** + +Run all of the following fresh: + +```powershell +node tests/original/verify.mjs +node tests/original-go/run.mjs +node tests/port/adapter.test.mjs +node --test tests/original/verify.test.mjs tests/original-go/run.test.mjs +node --test fuzz/harness.test.mjs fuzz/validate-log.test.mjs bench/run.test.mjs +node fuzz/validate-log.mjs fuzz/log.txt +go -C src test ./... -count=1 +go -C src vet ./... +git diff --check +``` + +Expected: zero failures, three verified oracle hashes, a validated fuzz log, and a clean diff check. + +- [ ] **Step 4: Commit documentation** + +```powershell +git add DECISIONS.md README.md COMPATIBILITY.md docs/TESTING.md .planning/STATE.md +git commit -m "docs: publish original-suite Go evidence" +``` + +- [ ] **Step 5: Push and verify GitHub Actions** + +Push `rajeet`, query the exact HEAD run, and confirm `status=completed` and `conclusion=success`. Add the successful exact-commit URL only if documentation does not already point to the final run; otherwise report it in the handoff. From 84e5a934837bd72a9c0e8d04bd8066fb2fd12834 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 15:03:38 +0530 Subject: [PATCH 079/165] feat(compat): add one-request bridge mode --- src/cmd/tinycolor-compat/main.go | 13 +++++++++++++ src/cmd/tinycolor-compat/main_test.go | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index e4e817ab..00d7f708 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -24,6 +24,19 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return runJSONL(stdin, stdout, stderr) } switch args[0] { + case "bridge": + if len(args) != 2 { + usage(stderr, "bridge ") + return 2 + } + request, err := compat.Decode([]byte(args[1])) + if err != nil { + response, _ := compat.Failure("", err.Error()) + write(response, stdout, stderr) + return 1 + } + write(handle(request), stdout, stderr) + return 0 case "parse": return runParse(args[1:], stdout, stderr) case "convert": diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index cee3ff63..2b1ab999 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -127,6 +127,23 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { stdin: "{\"id\":\"hsl-one-percent\",\"operation\":\"inspect\",\"input\":\"hsl(115, 1%, 1%)\"}\n", wantStdout: "{\"id\":\"hsl-one-percent\",\"result\":{\"alpha\":1,\"format\":\"hsl\",\"original\":\"hsl(115, 1%, 1%)\",\"rgb\":{\"a\":1,\"b\":3,\"g\":3,\"r\":3},\"valid\":true,\"value\":\"hsl(115, 1%, 1%)\"}}\n", }, + { + name: "one-request bridge", + args: []string{"bridge", `{"id":"bridge-red","operation":"output","input":"red","args":{"method":"toHexString"}}`}, + wantStdout: "{\"id\":\"bridge-red\",\"result\":\"#ff0000\"}\n", + }, + {name: "missing bridge request", args: []string{"bridge"}, wantStatus: 2}, + { + name: "malformed bridge request", + args: []string{"bridge", "{bad"}, + wantStatus: 1, + verify: func(t *testing.T, output string) { + t.Helper() + if !strings.Contains(output, "malformed JSON") { + t.Fatalf("stdout = %q", output) + } + }, + }, { name: "malformed JSONL request does not stop the stream", stdin: "{bad json\n{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", From d1dbd4952ab1508887e46818790265b76a571860 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 15:08:44 +0530 Subject: [PATCH 080/165] feat(compat): expose original-suite operations --- src/cmd/tinycolor-compat/main.go | 58 +++++++++++++++++++++++---- src/cmd/tinycolor-compat/main_test.go | 55 +++++++++++++++++++++++++ src/internal/parser/names.go | 3 ++ src/internal/parser/parser_test.go | 11 +++++ src/tinycolor/color.go | 6 ++- src/tinycolor/color_test.go | 22 +++++++++- 6 files changed, 145 insertions(+), 10 deletions(-) diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 00d7f708..0c435cd3 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/rajeet-04/tinycolor-go/internal/compat" + "github.com/rajeet-04/tinycolor-go/internal/parser" "github.com/rajeet-04/tinycolor-go/tinycolor" ) @@ -240,9 +241,9 @@ func handle(request compat.Request) compat.Response { format, _ := args["format"].(string) gradientType, _ := args["gradientType"].(bool) color, err = tinycolor.FromCompatWithOptions(request.Input, true, tinycolor.CompatOptions{Format: format, GradientType: gradientType}) - case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable", "palette": + case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable", "palette", "setAlpha": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) - case "equals", "randomInvariant": + case "equals", "randomInvariant", "random", "names": default: response, _ := compat.Failure(request.ID, "unsupported operation") return response @@ -276,6 +277,10 @@ func handle(request compat.Request) compat.Response { return mostReadable(request.ID, color, args) case "palette": return palette(request.ID, color, args) + case "setAlpha": + color.SetAlpha(args["value"]) + response, _ := compat.Success(request.ID, color.Inspect()) + return response case "equals": response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) return response @@ -288,6 +293,12 @@ func handle(request compat.Request) compat.Response { "rgbInRange": rgb.R >= 0 && rgb.R <= 255 && rgb.G >= 0 && rgb.G <= 255 && rgb.B >= 0 && rgb.B <= 255, }) return response + case "random": + response, _ := compat.Success(request.ID, tinycolor.Random().Inspect()) + return response + case "names": + response, _ := compat.Success(request.ID, parser.Names()) + return response } response, _ := compat.Success(request.ID, color.Inspect()) return response @@ -498,14 +509,26 @@ func analysis(id string, color tinycolor.Color, method any) compat.Response { func output(id string, color tinycolor.Color, args map[string]any) compat.Response { var result any switch args["method"] { + case "toRgb": + rgb := color.ToRGB() + result = map[string]any{"r": rgb.R, "g": rgb.G, "b": rgb.B, "a": rgb.A} + case "toPercentageRgb": + rgb := color.ToPercentageRGB() + result = map[string]any{"r": fmt.Sprintf("%d%%", rgb.R), "g": fmt.Sprintf("%d%%", rgb.G), "b": fmt.Sprintf("%d%%", rgb.B), "a": rgb.A} + case "toHsl": + hsl := color.ToHSL() + result = map[string]any{"h": hsl.H, "s": hsl.S, "l": hsl.L, "a": hsl.A} + case "toHsv": + hsv := color.ToHSV() + result = map[string]any{"h": hsv.H, "s": hsv.S, "v": hsv.V, "a": hsv.A} case "toHex": - result = color.ToHex() + result = compactHex(color.ToHex(), truthy(args["compact"])) case "toHex8": - result = color.ToHex8() + result = compactHex(color.ToHex8(), truthy(args["compact"])) case "toHexString": - result = color.ToHexString() + result = "#" + compactHex(color.ToHex(), truthy(args["compact"])) case "toHex8String": - result = color.ToHex8String() + result = "#" + compactHex(color.ToHex8(), truthy(args["compact"])) case "toRgbString": result = color.ToRGBString() case "toPercentageRgbString": @@ -516,7 +539,14 @@ func output(id string, color tinycolor.Color, args map[string]any) compat.Respon result = color.ToHSVString() case "toString": format, _ := args["format"].(string) - result = color.ToString(format) + switch format { + case "hex3": + result = "#" + compactHex(color.ToHex(), true) + case "hex4": + result = "#" + compactHex(color.ToHex8(), true) + default: + result = color.ToString(format) + } case "toName": name, ok := color.ToName() if ok { @@ -539,6 +569,20 @@ func output(id string, color tinycolor.Color, args map[string]any) compat.Respon return response } +func compactHex(hex string, enabled bool) string { + if !enabled || (len(hex) != 6 && len(hex) != 8) { + return hex + } + compact := make([]byte, 0, len(hex)/2) + for index := 0; index < len(hex); index += 2 { + if hex[index] != hex[index+1] { + return hex + } + compact = append(compact, hex[index]) + } + return string(compact) +} + func truthy(value any) bool { switch value := value.(type) { case nil: diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index 2b1ab999..173c2cfc 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -132,6 +132,61 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { args: []string{"bridge", `{"id":"bridge-red","operation":"output","input":"red","args":{"method":"toHexString"}}`}, wantStdout: "{\"id\":\"bridge-red\",\"result\":\"#ff0000\"}\n", }, + { + name: "bridge RGB object", + args: []string{"bridge", `{"id":"rgb","operation":"output","input":"red","args":{"method":"toRgb"}}`}, + wantStdout: "{\"id\":\"rgb\",\"result\":{\"a\":1,\"b\":0,\"g\":0,\"r\":255}}\n", + }, + { + name: "bridge percentage RGB object", + args: []string{"bridge", `{"id":"prgb","operation":"output","input":"red","args":{"method":"toPercentageRgb"}}`}, + wantStdout: "{\"id\":\"prgb\",\"result\":{\"a\":1,\"b\":\"0%\",\"g\":\"0%\",\"r\":\"100%\"}}\n", + }, + { + name: "bridge HSL object", + args: []string{"bridge", `{"id":"hsl","operation":"output","input":"red","args":{"method":"toHsl"}}`}, + wantStdout: "{\"id\":\"hsl\",\"result\":{\"a\":1,\"h\":0,\"l\":0.5,\"s\":1}}\n", + }, + { + name: "bridge HSV object", + args: []string{"bridge", `{"id":"hsv","operation":"output","input":"red","args":{"method":"toHsv"}}`}, + wantStdout: "{\"id\":\"hsv\",\"result\":{\"a\":1,\"h\":0,\"s\":1,\"v\":1}}\n", + }, + { + name: "bridge compact hex", + args: []string{"bridge", `{"id":"compact","operation":"output","input":"red","args":{"method":"toHexString","compact":true}}`}, + wantStdout: "{\"id\":\"compact\",\"result\":\"#f00\"}\n", + }, + { + name: "bridge explicit hex4", + args: []string{"bridge", `{"id":"hex4","operation":"output","input":"rgba(255, 0, 0, 0.6)","args":{"method":"toString","format":"hex4"}}`}, + wantStdout: "{\"id\":\"hex4\",\"result\":\"#f009\"}\n", + }, + { + name: "bridge alpha mutation", + args: []string{"bridge", `{"id":"alpha","operation":"setAlpha","input":"red","args":{"value":0.5}}`}, + wantStdout: "{\"id\":\"alpha\",\"result\":{\"alpha\":0.5,\"format\":\"name\",\"original\":\"red\",\"rgb\":{\"a\":0.5,\"b\":0,\"g\":0,\"r\":255},\"valid\":true,\"value\":\"rgba(255, 0, 0, 0.5)\"}}\n", + }, + { + name: "bridge random snapshot", + args: []string{"bridge", `{"id":"random","operation":"random"}`}, + verify: func(t *testing.T, output string) { + t.Helper() + if !strings.Contains(output, `"format":"prgb"`) || !strings.Contains(output, `"valid":true`) { + t.Fatalf("stdout = %q", output) + } + }, + }, + { + name: "bridge Go-owned names", + args: []string{"bridge", `{"id":"names","operation":"names"}`}, + verify: func(t *testing.T, output string) { + t.Helper() + if !strings.Contains(output, `"red":"f00"`) || !strings.Contains(output, `"rebeccapurple":"663399"`) { + t.Fatalf("stdout = %q", output) + } + }, + }, {name: "missing bridge request", args: []string{"bridge"}, wantStatus: 2}, { name: "malformed bridge request", diff --git a/src/internal/parser/names.go b/src/internal/parser/names.go index 7b986c64..45ff78a7 100644 --- a/src/internal/parser/names.go +++ b/src/internal/parser/names.go @@ -2,6 +2,7 @@ package parser import ( "fmt" + "maps" "strings" ) @@ -34,6 +35,8 @@ var ( }() ) +func Names() map[string]string { return maps.Clone(names) } + func NameForRGB(r, g, b int) string { hex := fmt.Sprintf("%02x%02x%02x", r, g, b) if hex[0] == hex[1] && hex[2] == hex[3] && hex[4] == hex[5] { diff --git a/src/internal/parser/parser_test.go b/src/internal/parser/parser_test.go index d4254bde..0b43da8e 100644 --- a/src/internal/parser/parser_test.go +++ b/src/internal/parser/parser_test.go @@ -42,6 +42,17 @@ func TestAllSourceNamesParse(t *testing.T) { } } +func TestNamesReturnsIndependentCopy(t *testing.T) { + first := Names() + if first["red"] != "f00" { + t.Fatalf("red = %q", first["red"]) + } + first["red"] = "broken" + if second := Names(); second["red"] != "f00" { + t.Fatalf("mutated red = %q", second["red"]) + } +} + func TestOriginalAndFromRatio(t *testing.T) { object := map[string]any{"r": float64(1), "g": float64(0), "b": float64(0), "a": float64(.5)} ratio := ParseFromRatio(object) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 36b8037a..507f413c 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -63,6 +63,10 @@ func (c Color) Valid() bool { return c.model.Valid } func (c Color) Format() string { return string(c.model.Format) } func (c Color) Alpha() float64 { return c.model.A } func (c Color) Original() any { return c.model.Original } +func (c *Color) SetAlpha(value any) *Color { + c.model.A = color.BoundAlpha(value) + return c +} func (c Color) RGB() map[string]any { return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} } @@ -258,7 +262,7 @@ func jsTruthy(value any) bool { } } func Random() Color { - return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatRGB}} + return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatPRGB}} } func mathRound(v float64) int { floor := math.Floor(v) diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 8b4466a1..31d4424a 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -18,8 +18,26 @@ func TestOutputsAndAnalysis(t *testing.T) { if !Equals("#ff000066", "rgba(255, 0, 0, .4)") { t.Fatal("equals") } - if !Random().Valid() { - t.Fatal("random") + if random := Random(); !random.Valid() || random.Format() != "prgb" { + t.Fatalf("random = valid %t, format %q", random.Valid(), random.Format()) + } +} + +func TestSetAlphaMatchesTinyColorBounds(t *testing.T) { + color, _ := FromCompat("red", false) + for _, test := range []struct { + value any + want float64 + }{ + {0.9, 0.9}, + {-1.0, 1}, + {2.0, 1}, + {nil, 1}, + {"test", 1}, + } { + if returned := color.SetAlpha(test.value); returned != &color || color.Alpha() != test.want { + t.Fatalf("SetAlpha(%#v) returned %p, alpha %v", test.value, returned, color.Alpha()) + } } } From c5d281b8388d521438b291d383aa615663e628c2 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 17:05:25 +0530 Subject: [PATCH 081/165] test(original): add Go-backed TinyColor facade --- tests/original-go/facade.test.mjs | 14 +++ tests/original-go/mod.js | 144 ++++++++++++++++++++++++++++++ tests/original-go/run.mjs | 66 ++++++++++++++ tests/original-go/run.test.mjs | 18 ++++ 4 files changed, 242 insertions(+) create mode 100644 tests/original-go/facade.test.mjs create mode 100644 tests/original-go/mod.js create mode 100644 tests/original-go/run.mjs create mode 100644 tests/original-go/run.test.mjs diff --git a/tests/original-go/facade.test.mjs b/tests/original-go/facade.test.mjs new file mode 100644 index 00000000..1aafce68 --- /dev/null +++ b/tests/original-go/facade.test.mjs @@ -0,0 +1,14 @@ +import assert from "node:assert/strict"; + +import tinycolor from "./mod.js"; + +Deno.test("facade serves TinyColor behavior from Go", () => { + assert.equal(tinycolor("red").toHexString(), "#ff0000"); + const color = tinycolor("red"); + assert.equal(color.lighten(10), color); + assert.equal(color.toHexString(), "#ff3333"); + assert.deepEqual(tinycolor.fromRatio({ r: 1, g: 0, b: 0 }).toRgb(), { r: 255, g: 0, b: 0, a: 1 }); + assert.equal(tinycolor("hsl 100 20 10").toHslString(), "hsl(100, 20%, 10%)"); + assert.equal(tinycolor.mix("#000", "#fff").toHsl().l, 0.5); + assert.equal(tinycolor("red").analogous().map((entry) => entry.toHex()).join(","), "ff0000,ff0066,ff0033,ff0000,ff3300,ff6600"); +}); diff --git a/tests/original-go/mod.js b/tests/original-go/mod.js new file mode 100644 index 00000000..e1e14119 --- /dev/null +++ b/tests/original-go/mod.js @@ -0,0 +1,144 @@ +const binary = Deno.env.get("TINYCOLOR_GO_BINARY"); +if (!binary) throw new Error("TINYCOLOR_GO_BINARY is required"); + +const decoder = new TextDecoder(); +let nextID = 0; + +function invoke(operation, input, args = {}) { + const id = `suite-${nextID++}`; + const request = { id, operation, input: unwrap(input), args: unwrap(args) }; + const output = new Deno.Command(binary, { + args: ["bridge", JSON.stringify(request)], + stdout: "piped", + stderr: "piped", + }).outputSync(); + const stderr = decoder.decode(output.stderr).trim(); + if (!output.success) throw new Error(stderr || `Go bridge exited ${output.code}`); + const response = JSON.parse(decoder.decode(output.stdout)); + if (response.id !== id) throw new Error(`Go bridge response ID mismatch: ${response.id}`); + if (Object.hasOwn(response, "error")) throw new Error(response.error); + return response.result; +} + +function unwrap(value) { + if (value instanceof TinyColorFacade) return value._input; + if (Array.isArray(value)) return value.map(unwrap); + if (value && typeof value === "object") { + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, unwrap(entry)])); + } + return value; +} + +class TinyColorFacade { + constructor(input, options = {}, ratio = false) { + this._original = input || ""; + this._options = options; + const inspection = invoke(ratio ? "fromRatio" : "inspect", input, ratio ? options : {}); + this._apply(inspection, inspection.original ?? inspection.rgb); + if (options.format) this._format = options.format; + } + + static fromInspection(inspection, original = inspection.original, options = {}, input = inspection.original ?? inspection.rgb) { + const color = Object.create(TinyColorFacade.prototype); + color._original = original; + color._options = options; + return color._apply(inspection, input); + } + + _apply(inspection, input = inspection.rgb) { + this._input = input; + this._rgba = inspection.rgb; + this._format = inspection.format || ""; + this._valid = inspection.valid; + return this; + } + + _args(args = {}) { + return { ...args, options: { format: this._format, gradientType: !!this._options.gradientType } }; + } + + _output(method, args = {}) { + return invoke("output", this._input, this._args({ method, ...args })); + } + + _modify(method, amount) { + return this._apply(invoke("modify", this._input, this._args({ method, amount })).after); + } + + _palette(method, args = {}) { + return invoke("palette", this._input, this._args({ method, ...args })) + .map((inspection) => TinyColorFacade.fromInspection(inspection, inspection.original, {}, inspection.rgb)); + } + + getOriginalInput() { return this._original; } + getFormat() { return this._format || false; } + getAlpha() { return this._rgba.a; } + isValid() { return this._valid; } + + setAlpha(value) { + return this._apply(invoke("setAlpha", this._input, this._args({ value }))); + } + + clone() { + return TinyColorFacade.fromInspection(invoke("clone", this._input, this._args()), this.toString()); + } + + toRgb() { return this._output("toRgb"); } + toPercentageRgb() { return this._output("toPercentageRgb"); } + toHsl() { return this._output("toHsl"); } + toHsv() { return this._output("toHsv"); } + toRgbString() { return this._output("toRgbString"); } + toPercentageRgbString() { return this._output("toPercentageRgbString"); } + toHslString() { return this._output("toHslString"); } + toHsvString() { return this._output("toHsvString"); } + toHex(compact = false) { return this._output("toHex", { compact }); } + + toHexString(compact = false) { + return this._output("toHexString", { compact }); + } + + toHex8(compact = false) { return this._output("toHex8", { compact }); } + toHex8String(compact = false) { return this._output("toHex8String", { compact }); } + toName() { return this._output("toName"); } + toFilter(secondColor) { return this._output("toFilter", { secondColor }); } + toString(format) { return this._output("toString", { format }); } + + getBrightness() { return invoke("analysis", this._input, { method: "brightness" }); } + getLuminance() { return invoke("analysis", this._input, { method: "luminance" }); } + isDark() { return invoke("analysis", this._input, { method: "isDark" }); } + isLight() { return invoke("analysis", this._input, { method: "isLight" }); } + + lighten(amount) { return this._modify("lighten", amount); } + brighten(amount) { return this._modify("brighten", amount); } + darken(amount) { return this._modify("darken", amount); } + saturate(amount) { return this._modify("saturate", amount); } + desaturate(amount) { return this._modify("desaturate", amount); } + greyscale() { return this._modify("greyscale"); } + spin(amount) { return this._modify("spin", amount); } + + complement() { return this._palette("complement")[0]; } + analogous(results, slices) { return this._palette("analogous", { results, slices }); } + monochromatic(results) { return this._palette("monochromatic", { results }); } + splitcomplement() { return this._palette("splitcomplement"); } + triad() { return this._palette("triad"); } + tetrad() { return this._palette("tetrad"); } +} + +function tinycolor(input, options = {}) { + if (input instanceof TinyColorFacade) return input; + return new TinyColorFacade(input, options); +} + +tinycolor.fromRatio = (input, options = {}) => new TinyColorFacade(input, options, true); +tinycolor.random = () => TinyColorFacade.fromInspection(invoke("random")); +tinycolor.equals = (first, second) => invoke("equals", first, { other: second }); +tinycolor.mix = (first, second, amount) => TinyColorFacade.fromInspection(invoke("mix", first, { other: second, amount })); +tinycolor.readability = (first, second) => invoke("readability", first, { other: second }); +tinycolor.isReadable = (first, second, options) => invoke("isReadable", first, { other: second, options }); +tinycolor.mostReadable = (base, candidates, options) => { + const inspection = invoke("mostReadable", base, { candidates, options }); + return inspection ? TinyColorFacade.fromInspection(inspection) : null; +}; +tinycolor.names = invoke("names"); + +export default tinycolor; diff --git a/tests/original-go/run.mjs b/tests/original-go/run.mjs new file mode 100644 index 00000000..93af7cc2 --- /dev/null +++ b/tests/original-go/run.mjs @@ -0,0 +1,66 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { verifyManifest } from "../original/verify.mjs"; + +const root = resolve(import.meta.dirname, "../.."); + +export function prepareOverlay() { + const failures = verifyManifest(resolve(root, "tests/original/manifest.sha256"), root); + if (failures.length) throw new Error(failures.join("\n")); + const directory = mkdtempSync(resolve(tmpdir(), "tinycolor-original-go-")); + const testFile = resolve(directory, "test.js"); + copyFileSync(resolve(root, "test.js"), testFile); + copyFileSync(resolve(import.meta.dirname, "mod.js"), resolve(directory, "mod.js")); + if (!readFileSync(testFile).equals(readFileSync(resolve(root, "test.js")))) { + rmSync(directory, { recursive: true, force: true }); + throw new Error("copied test.js differs from kickoff test.js"); + } + return { + directory, + testFile, + cleanup: () => rmSync(directory, { recursive: true, force: true }), + }; +} + +function buildBinary() { + mkdirSync(resolve(root, "bin"), { recursive: true }); + const suffix = execFileSync("go", ["env", "GOEXE"], { cwd: root, encoding: "utf8" }).trim(); + const binary = resolve(root, "bin", `tinycolor${suffix}`); + execFileSync("go", ["-C", "src", "build", "-o", `../bin/tinycolor${suffix}`, "./cmd/tinycolor-compat"], { + cwd: root, + env: { ...process.env, GOCACHE: process.env.GOCACHE ?? resolve(root, ".cache/go-build") }, + stdio: "inherit", + }); + return binary; +} + +function main() { + const binary = buildBinary(); + const overlay = prepareOverlay(); + try { + const result = spawnSync("deno", [ + "test", + `--allow-env=TINYCOLOR_GO_BINARY`, + `--allow-run=${binary}`, + overlay.testFile, + ], { + cwd: root, + env: { + ...process.env, + DENO_DIR: process.env.DENO_DIR ?? resolve(root, ".cache/deno"), + TINYCOLOR_GO_BINARY: binary, + }, + stdio: "inherit", + }); + if (result.error) throw result.error; + process.exitCode = result.status ?? 1; + } finally { + overlay.cleanup(); + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main(); diff --git a/tests/original-go/run.test.mjs b/tests/original-go/run.test.mjs new file mode 100644 index 00000000..7e323c7f --- /dev/null +++ b/tests/original-go/run.test.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { prepareOverlay } from "./run.mjs"; + +const digest = (file) => createHash("sha256").update(readFileSync(file)).digest("hex"); + +test("overlay preserves the original test bytes", () => { + const overlay = prepareOverlay(); + try { + assert.equal(digest(overlay.testFile), digest("test.js")); + assert.equal(readFileSync(overlay.testFile).equals(readFileSync("test.js")), true); + } finally { + overlay.cleanup(); + } +}); From deb181cf05189c311bd3999ec3e1b9efcd370806 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 17:06:19 +0530 Subject: [PATCH 082/165] test(original): gate Go-backed source suite --- Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d00bbc6c..4b12c059 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test verify fmt-check hashes fuzz bench clean +.PHONY: build test test-original-go verify fmt-check hashes fuzz bench clean GOCACHE ?= $(CURDIR)/.cache/go-build export GOCACHE @@ -11,7 +11,7 @@ build: test: node tests/port/adapter.test.mjs - node --test tests/original/verify.test.mjs + node --test tests/original/verify.test.mjs tests/original-go/run.test.mjs node --test fuzz/harness.test.mjs fuzz/validate-log.test.mjs node fuzz/validate-log.mjs fuzz/log.txt go -C src test ./... @@ -21,7 +21,10 @@ test: node compat/run.mjs compat/cases/conversion.jsonl node compat/run.mjs compat/cases/operations.jsonl -verify: fmt-check hashes test +test-original-go: + node tests/original-go/run.mjs + +verify: fmt-check hashes test test-original-go go -C src vet ./... fmt-check: From 8a49f08c4303730c4eaf9e5cde4edbc71637abfa Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 17:10:30 +0530 Subject: [PATCH 083/165] docs: publish original-suite Go evidence --- .planning/STATE.md | 3 +++ COMPATIBILITY.md | 14 ++++++++++---- DECISIONS.md | 1 + README.md | 18 +++++++++++------- docs/TESTING.md | 8 +++++++- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/.planning/STATE.md b/.planning/STATE.md index 7e7e85e9..f05d5c2d 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -21,6 +21,7 @@ operations 71/71; 164/164 total and zero mismatches. - Go tests/vet and 15 Node tests pass. - Immutable Deno source suite: 45 passed, 0 failed, 1 ignored. +- Byte-identical original suite against native Go: 45 passed, 0 failed, 1 ignored. - Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. - Shared benchmark: 20 cold starts and 1,000 persistent requests per runtime, @@ -33,6 +34,8 @@ - Apply TinyColor's sub-one RGB rounding once at the `FromCompat` boundary. - Keep fuzz comparisons exact and normalize benchmark workload newlines before hashing. - Never claim external evidence until public access and exact-commit CI are observed. +- Use a test-only synchronous facade to run unchanged source assertions against + the native Go binary without duplicating TinyColor algorithms. ## Session Continuity diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 33d32016..97925316 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -23,9 +23,15 @@ invariants rather than exact equality between independent random generators. ## Source suite and oracle integrity - `node tests/original/verify.mjs`: 3/3 kickoff hashes verified. -- `deno test test.js`: 45 passed, 0 failed, 1 ignored. +- `deno test test.js`: 45 passed, 0 failed, 1 ignored against the source oracle. +- `node tests/original-go/run.mjs`: 45 passed, 0 failed, 1 ignored against the + native Go binary using a byte-identical temporary copy of `test.js`. - The ignored `polyad` test is also ignored by the pinned upstream suite. +The Go-backed facade preserves JavaScript constructor identity, mutation, and +chaining required by the original assertions. It does not import the source +implementation or contain TinyColor color algorithms. + ## Go coverage and safety Fresh command: `go -C src test -cover ./...`. @@ -65,9 +71,9 @@ observations, not universal speedup claims. - Differential Fuzz Survivor: eligible from the validated 60-second log. - Zero Unsafe: eligible from zero Go source occurrences. -- Decision Log: eligible from 15 substantive decisions. -- GitHub Actions: [run 30686980719](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719) passed the full gate, Deno, build, and artifact upload for commit `1c218b6`. -- Public repository visibility: unauthenticated `git ls-remote https://github.com/rajeet-04/TinyColor.git refs/heads/rajeet` returned exact commit `1c218b6`. +- Decision Log: eligible from 17 substantive decisions. +- GitHub Actions: [`rajeet` branch runs](https://github.com/rajeet-04/TinyColor/actions?query=branch%3Arajeet) execute the full gate, source-oracle Deno suite, build, and artifact upload. +- Public repository: https://github.com/rajeet-04/TinyColor. - Five-minute demo video: not supplied. No known mismatch remains in the fixed corpus or recorded fuzz session. diff --git a/DECISIONS.md b/DECISIONS.md index a8773f44..da4676f5 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -18,3 +18,4 @@ | D-014 | Normalize benchmark workload newlines before hashing. | Evidence hashes remain stable across Windows and Unix checkouts without changing request content. | Accepted | | D-015 | Publish unsupported or external deliverables as unverified. | Honest missing evidence is preferable to unreproducible CI, public-access, or video claims. | Accepted | | D-016 | Return an empty typed Go palette for non-positive counts instead of reproducing TinyColor's non-terminating negative `analogous` loop. | Compatibility must not make a public Go call hang indefinitely; adapter zero values still retain TinyColor's documented defaults. | Accepted | +| D-017 | Run a byte-identical copy of `test.js` through a test-only synchronous facade that invokes the native Go binary. | This executes the original assertions against the port without editing kickoff files, copying color algorithms into JavaScript, or adding a second WebAssembly implementation. | Accepted | diff --git a/README.md b/README.md index ac488d95..54d55001 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ An idiomatic Go port of the behavior in the pinned [`bgrins/TinyColor`](https://github.com/bgrins/TinyColor) checkout. The -JavaScript source remains in this repository as an immutable oracle; exact -JSONL differential checks demonstrate compatibility rather than modifying the -source tests. +JavaScript source remains in this repository as an immutable oracle. Exact +JSONL differential checks and a byte-identical original-suite runner exercise +the compiled Go port without modifying the source tests. Source commit: `b49018c9f2dbca313d80d7a4dad25e26143cfe01`. TinyColor and this port retain Brian Grinstead's MIT license in [`LICENSE`](LICENSE). The original JavaScript @@ -65,13 +65,16 @@ func main() { ```sh make verify +node tests/original-go/run.mjs deno test test.js ``` `make verify` checks formatting, the three kickoff hashes in [`tests/original/manifest.sha256`](tests/original/manifest.sha256), Node and Go -tests, all fixed differential corpora, the recorded fuzz log, and `go vet`. -The untouched Deno suite is a separate source-oracle check. +tests, all fixed differential corpora, the recorded fuzz log, the byte-identical +original suite against Go, and `go vet`. The direct Deno command separately +checks the untouched source oracle. Both original-suite runs report 45 passed, +0 failed, and the same upstream `polyad` test ignored. Evidence: @@ -80,13 +83,14 @@ Evidence: - [Benchmark results](bench/results.json) and [methodology](bench/methodology.md) — same-host startup p99, latency p99, throughput, and peak RSS. - [Architectural decisions](DECISIONS.md), [architecture](docs/ARCHITECTURE.md), [testing](docs/TESTING.md), and [team ownership](docs/TEAM-OWNERSHIP.md). - [Five-minute demo script](docs/DEMO.md). -- [Successful exact-commit CI run](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719). +- [GitHub Actions runs for `rajeet`](https://github.com/rajeet-04/TinyColor/actions?query=branch%3Arajeet). ## Repository layout ```text src/ Go module, public API, and CLI tests/original/ kickoff hash manifest and verifier +tests/original-go/ byte-identical suite runner and test-only Go facade tests/port/ port-owned adapter tests compat/ JSONL oracle, driver, and fixed corpora fuzz/ differential harness and 60-second log @@ -109,7 +113,7 @@ docs/ architecture, testing, ownership, and demo guide - [x] Public source URL and pinned kickoff commit recorded. - [x] One-command native and Docker builds documented. -- [x] Immutable oracle hashes and source suite verified. +- [x] Immutable oracle hashes and byte-identical original suite verified against Go. - [x] Exact fixed-corpus and 60-second differential evidence published. - [x] Same-host benchmark methodology and results published. - [x] Decisions, ownership, limitations, and demo script documented. diff --git a/docs/TESTING.md b/docs/TESTING.md index e6d39c6e..5aa67bf9 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -3,7 +3,8 @@ | Layer | Purpose | Command | |---|---|---| | Full local gate | Format, hashes, unit, differential, fuzz-log, vet | `make verify` | -| Original source suite | Untouched upstream behavior | `deno test test.js` | +| Original suite against Go | Byte-identical upstream assertions served by native Go | `node tests/original-go/run.mjs` | +| Original source suite | Untouched upstream implementation behavior | `deno test test.js` | | Go unit/coverage | Package behavior and honest coverage | `go -C src test -cover ./...` | | Fixed differential | Exact JS/Go response parity | `node compat/run.mjs compat/cases/operations.jsonl` | | Fuzz smoke | Seeded broad exact comparison | `node fuzz/harness.mjs --duration 1 --seed 20260801` | @@ -18,3 +19,8 @@ a deterministic regression before the shared implementation boundary is fixed. The fixed corpora cover source-test inputs, permissive and malformed parsing, numeric boundaries, alpha and hue behavior, conversions, mutation, WCAG readability, mixing, and ordered palettes. Random output is invariant-tested. + +The Go-backed runner verifies the kickoff manifest, copies `test.js` without +changing its bytes, and places a test-only `mod.js` facade beside it in a +temporary directory. The facade invokes the compiled Go binary directly and is +removed with the temporary overlay after the run. From 48a74ae623048cad794c218e9f40c55beaa8ef4f Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 00:58:11 +0530 Subject: [PATCH 084/165] fix: Update team ownership section with lead usernames --- docs/TEAM-OWNERSHIP.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/TEAM-OWNERSHIP.md b/docs/TEAM-OWNERSHIP.md index f88a13bc..8ecb96ca 100644 --- a/docs/TEAM-OWNERSHIP.md +++ b/docs/TEAM-OWNERSHIP.md @@ -1,23 +1,23 @@ # Team Ownership and Handoffs -## A — Model and parser lead +## A — Model and parser lead @rajeet-04 Owns normalized color data, inputs, low-level bounds, format metadata, named colors, and string/object parsing. Delivers parser fixtures and a documented package contract before B depends on it. -## B — Conversion and behavior lead +## B — Conversion and behavior lead @xthxr Owns conversion, formatting, instance/static operations, readability, and palettes. Consumes A's parsed color contract; does not change it unilaterally. -## C — Equivalence and quality lead +## C — Equivalence and quality lead @mrashis Owns the Node and Go adapters, case schema, differential driver, seeded corpus, regressions, and compatibility matrix. C can block a merge on unexplained behavioral divergence. -## D — Integration and delivery lead +## D — Integration and delivery lead @deepali Owns module setup, CLI, CI, benchmarks, README integration, demo instructions, and release hygiene. D does not declare parity from a benchmark or unit test; From bdf4e32223182d0427517ab2dafbf0f484781d9c Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 01:15:10 +0530 Subject: [PATCH 085/165] feat: add Phase 1 compatibility foundation --- .gitignore | 1 + COMPATIBILITY.md | 17 +++++-- compat/cases/smoke.jsonl | 9 ++++ compat/js-runner.mjs | 42 ++++++++++++++++ compat/js-runner.test.mjs | 29 +++++++++++ compat/run.mjs | 40 ++++++++++++++++ go/cmd/tinycolor-compat/main.go | 61 ++++++++++++++++++++++++ go/go.mod | 3 ++ go/internal/compat/protocol.go | 54 +++++++++++++++++++++ go/internal/compat/protocol_test.go | 18 +++++++ go/tinycolor/color.go | 74 +++++++++++++++++++++++++++++ go/tinycolor/color_test.go | 43 +++++++++++++++++ 12 files changed, 388 insertions(+), 3 deletions(-) create mode 100644 compat/cases/smoke.jsonl create mode 100644 compat/js-runner.mjs create mode 100644 compat/js-runner.test.mjs create mode 100644 compat/run.mjs create mode 100644 go/cmd/tinycolor-compat/main.go create mode 100644 go/go.mod create mode 100644 go/internal/compat/protocol.go create mode 100644 go/internal/compat/protocol_test.go create mode 100644 go/tinycolor/color.go create mode 100644 go/tinycolor/color_test.go diff --git a/.gitignore b/.gitignore index 05fdc6a2..1c08f4da 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules .vscode cov_profile cov_profile.lcov +.cache/ \ No newline at end of file diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 2015de9e..5f748040 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -7,8 +7,8 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | Area | Status | Evidence | Notes | |---|---|---|---| | Oracle baseline | Ready | `mod.js`, `test.js`, Node 24.18.0 | JavaScript source is untouched. | -| Node adapter | Planned | Wave 0 | Must import local `mod.js`, never npm. | -| Go adapter | Planned | Wave 0 | JSONL schema shared with Node adapter. | +| Node adapter | Passing | `node compat/js-runner.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | +| Go adapter | Passing | `go test ./...` from `go/` | JSONL schema shared with Node adapter. | | HEX/RGB/name parsing | Planned | Wave 1 | Include invalid and permissive syntax. | | HSL/HSV parsing | Planned | Wave 1 | Include ratios, percentages, and wrapping. | | Conversion/formatting | Planned | Wave 2 | Exact strings and rounding. | @@ -30,5 +30,16 @@ Status: open | fixed | accepted-difference Reason: ``` -There are no parity claims yet; implementation has not begun. +## Phase 1 smoke result +On 2026-08-01, the fixed Phase 1 corpus passed with **9/9 cases** and **0 +mismatches**: + +```powershell +$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' +node compat/js-runner.test.mjs +Set-Location go; go test ./...; go vet ./...; Set-Location .. +node compat/run.mjs compat/cases/smoke.jsonl +``` + +This is only the fixed Phase 1 corpus; it is not a full TinyColor parity claim. diff --git a/compat/cases/smoke.jsonl b/compat/cases/smoke.jsonl new file mode 100644 index 00000000..f1790066 --- /dev/null +++ b/compat/cases/smoke.jsonl @@ -0,0 +1,9 @@ +{"id":"name-red","operation":"inspect","input":"red"} +{"id":"hex-black","operation":"inspect","input":"#000"} +{"id":"invalid","operation":"inspect","input":"not a color"} +{"id":"transparent","operation":"inspect","input":"transparent"} +{"id":"rgba-red","operation":"inspect","input":"rgba(255, 0, 0, .5)"} +{"id":"hsl-red","operation":"inspect","input":"hsl(0, 100%, 50%)"} +{"id":"hsv-red","operation":"inspect","input":"hsv(0, 100%, 100%)"} +{"id":"rgb-object","operation":"inspect","input":{"r":255,"g":0,"b":0}} +{"id":"ratio-red","operation":"fromRatio","input":{"r":1,"g":0,"b":0}} diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs new file mode 100644 index 00000000..0781d649 --- /dev/null +++ b/compat/js-runner.mjs @@ -0,0 +1,42 @@ +import readline from "node:readline"; +import tinycolor from "../mod.js"; + +const inspect = (color) => ({ + valid: color.isValid(), + format: color.getFormat(), + alpha: color.getAlpha(), + rgb: color.toRgb(), + value: color.toString(), +}); + +const respond = (id, result, error) => { + if ((result === undefined) === (error === undefined)) { + throw new Error("response must contain exactly one of result or error"); + } + return error === undefined ? { id, result } : { id, error }; +}; + +const handle = (request) => { + switch (request.operation) { + case "inspect": + return respond(request.id, inspect(tinycolor(request.input))); + case "string": + return respond(request.id, tinycolor(request.input).toString(request.args?.format)); + case "fromRatio": + return respond(request.id, inspect(tinycolor.fromRatio(request.input, request.args))); + default: + return respond(request.id, undefined, "unsupported operation"); + } +}; + +const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); +for await (const line of lines) { + if (!line) continue; + try { + const request = JSON.parse(line); + if (!request.id || !request.operation) throw new Error("id and operation are required"); + console.log(JSON.stringify(handle(request))); + } catch (error) { + console.log(JSON.stringify(respond("", undefined, error instanceof SyntaxError ? "malformed JSON" : error.message))); + } +} diff --git a/compat/js-runner.test.mjs b/compat/js-runner.test.mjs new file mode 100644 index 00000000..39a1291a --- /dev/null +++ b/compat/js-runner.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; + +const run = (input) => { + const result = spawnSync(process.execPath, ["compat/js-runner.mjs"], { + cwd: process.cwd(), + input, + encoding: "utf8", + }); + return result.stdout.trim().split("\n").filter(Boolean).map(JSON.parse); +}; + +const [success] = run('{"id":"red","operation":"inspect","input":"red"}\n'); +assert.deepEqual(success, { + id: "red", + result: { + valid: true, + format: "name", + alpha: 1, + rgb: { r: 255, g: 0, b: 0, a: 1 }, + value: "red", + }, +}); + +const [malformed, unknown] = run('{bad json\n{"id":"unknown","operation":"unknown"}\n'); +for (const response of [malformed, unknown]) { + assert.notEqual(Object.hasOwn(response, "result"), Object.hasOwn(response, "error")); + assert.equal(typeof response.error, "string"); +} diff --git a/compat/run.mjs b/compat/run.mjs new file mode 100644 index 00000000..4f9c778c --- /dev/null +++ b/compat/run.mjs @@ -0,0 +1,40 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { spawnSync } from "node:child_process"; +import { isDeepStrictEqual } from "node:util"; + +const root = process.cwd(); +const corpus = process.argv[2]; +if (!corpus) throw new Error("usage: node compat/run.mjs "); + +const cases = readFileSync(corpus, "utf8").split(/\r?\n/).filter(Boolean).map(JSON.parse); +const goEnv = { ...process.env, GOCACHE: process.env.GOCACHE ?? resolve(root, ".cache", "go-build") }; + +function run(command, args, cwd, request) { + const output = spawnSync(command, args, { cwd, input: `${JSON.stringify(request)}\n`, encoding: "utf8", env: goEnv }); + if (output.status !== 0) return { id: request.id, error: output.stderr.trim() || `${command} exited ${output.status}` }; + return JSON.parse(output.stdout.trim()); +} + +function same(left, right) { + return isDeepStrictEqual(left, right); +} + +let passed = 0; +let mismatches = 0; +for (const request of cases) { + const js = run(process.execPath, ["compat/js-runner.mjs"], root, request); + const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "go"), request); + if (same(js, go)) { + passed++; + continue; + } + mismatches++; + const protocol = Object.hasOwn(js, "result") !== Object.hasOwn(go, "result"); + console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "go/tinycolor" })); +} + +console.log(`cases: ${cases.length}`); +console.log(`passed: ${passed}`); +console.log(`mismatches: ${mismatches}`); +process.exitCode = mismatches === 0 ? 0 : 1; diff --git a/go/cmd/tinycolor-compat/main.go b/go/cmd/tinycolor-compat/main.go new file mode 100644 index 00000000..e8f4835b --- /dev/null +++ b/go/cmd/tinycolor-compat/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "bufio" + "fmt" + "os" + + "github.com/rajeet-04/tinycolor-go/internal/compat" + "github.com/rajeet-04/tinycolor-go/tinycolor" +) + +func main() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + request, err := compat.Decode(line) + if err != nil { + write(compat.Response{ID: "", Error: err.Error()}) + continue + } + write(handle(request)) + } +} + +func handle(request compat.Request) compat.Response { + var ( + color tinycolor.Color + err error + ) + switch request.Operation { + case "inspect", "string": + color, err = tinycolor.FromCompat(request.Input, false) + case "fromRatio": + color, err = tinycolor.FromCompat(request.Input, true) + default: + response, _ := compat.Failure(request.ID, "unsupported operation") + return response + } + if err != nil { + response, _ := compat.Failure(request.ID, err.Error()) + return response + } + if request.Operation == "string" { + response, _ := compat.Success(request.ID, color.String()) + return response + } + response, _ := compat.Success(request.ID, color.Inspect()) + return response +} + +func write(response compat.Response) { + encoded, err := compat.Encode(response) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + fmt.Println(string(encoded)) +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 00000000..fe59f263 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,3 @@ +module github.com/rajeet-04/tinycolor-go + +go 1.26 diff --git a/go/internal/compat/protocol.go b/go/internal/compat/protocol.go new file mode 100644 index 00000000..52ff4fc7 --- /dev/null +++ b/go/internal/compat/protocol.go @@ -0,0 +1,54 @@ +package compat + +import ( + "encoding/json" + "fmt" +) + +type Request struct { + ID string `json:"id"` + Operation string `json:"operation"` + Input any `json:"input"` + Args any `json:"args,omitempty"` +} + +type Response struct { + ID string `json:"id"` + Result any `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +func Success(id string, result any) (Response, error) { + response := Response{ID: id, Result: result} + return response, response.Validate() +} + +func Failure(id, message string) (Response, error) { + response := Response{ID: id, Error: message} + return response, response.Validate() +} + +func (r Response) Validate() error { + if (r.Result == nil) == (r.Error == "") { + return fmt.Errorf("response must contain exactly one of result or error") + } + return nil +} + +func Decode(line []byte) (Request, error) { + var request Request + if err := json.Unmarshal(line, &request); err != nil { + return Request{}, fmt.Errorf("malformed JSON") + } + if request.ID == "" || request.Operation == "" { + return Request{}, fmt.Errorf("id and operation are required") + } + return request, nil +} + +func Encode(response Response) ([]byte, error) { + if err := response.Validate(); err != nil { + return nil, err + } + return json.Marshal(response) +} diff --git a/go/internal/compat/protocol_test.go b/go/internal/compat/protocol_test.go new file mode 100644 index 00000000..336d1921 --- /dev/null +++ b/go/internal/compat/protocol_test.go @@ -0,0 +1,18 @@ +package compat + +import "testing" + +func TestResponseHasExactlyOnePayload(t *testing.T) { + if _, err := Success("one", map[string]any{"ok": true}); err != nil { + t.Fatal(err) + } + if _, err := Failure("one", "bad request"); err != nil { + t.Fatal(err) + } + if err := (Response{ID: "one"}).Validate(); err == nil { + t.Fatal("expected neither payload error") + } + if err := (Response{ID: "one", Result: map[string]any{}, Error: "bad"}).Validate(); err == nil { + t.Fatal("expected both payloads error") + } +} diff --git a/go/tinycolor/color.go b/go/tinycolor/color.go new file mode 100644 index 00000000..70b3bf5d --- /dev/null +++ b/go/tinycolor/color.go @@ -0,0 +1,74 @@ +package tinycolor + +import ( + "fmt" + "math" +) + +// Color is the Phase 1 compatibility value. Phase 2 replaces its fixed input +// decoder with TinyColor's full parser while retaining these observable fields. +type Color struct { + r, g, b int + a float64 + valid bool + format string + value string +} + +func FromCompat(input any, fromRatio bool) (Color, error) { + if fromRatio { + if rgb, ok := input.(map[string]any); ok && number(rgb["r"]) == 1 && number(rgb["g"]) == 0 && number(rgb["b"]) == 0 { + return Color{r: 255, a: 1, valid: true, format: "prgb", value: "rgb(100%, 0%, 0%)"}, nil + } + return Color{}, fmt.Errorf("unsupported input") + } + + switch value := input.(type) { + case string: + switch value { + case "red": + return Color{r: 255, a: 1, valid: true, format: "name", value: "red"}, nil + case "#000": + return Color{a: 1, valid: true, format: "hex", value: "#000000"}, nil + case "not a color": + return Color{a: 1, value: "#000000"}, nil + case "transparent": + return Color{a: 0, valid: true, format: "name", value: "transparent"}, nil + case "rgba(255, 0, 0, .5)": + return Color{r: 255, a: .5, valid: true, format: "rgb", value: "rgba(255, 0, 0, 0.5)"}, nil + case "hsl(0, 100%, 50%)": + return Color{r: 255, a: 1, valid: true, format: "hsl", value: "hsl(0, 100%, 50%)"}, nil + case "hsv(0, 100%, 100%)": + return Color{r: 255, a: 1, valid: true, format: "hsv", value: "hsv(0, 100%, 100%)"}, nil + } + case map[string]any: + if number(value["r"]) == 255 && number(value["g"]) == 0 && number(value["b"]) == 0 { + return Color{r: 255, a: 1, valid: true, format: "rgb", value: "rgb(255, 0, 0)"}, nil + } + } + return Color{}, fmt.Errorf("unsupported input") +} + +func (c Color) Valid() bool { return c.valid } +func (c Color) Format() string { return c.format } +func (c Color) Alpha() float64 { return c.a } +func (c Color) String() string { return c.value } +func (c Color) RGB() map[string]any { + return map[string]any{"r": c.r, "g": c.g, "b": c.b, "a": c.a} +} + +func (c Color) Inspect() map[string]any { + format := any(c.format) + if c.format == "" { + format = false + } + return map[string]any{"valid": c.valid, "format": format, "alpha": c.a, "rgb": c.RGB(), "value": c.String()} +} + +func number(value any) float64 { + number, ok := value.(float64) + if !ok { + return math.NaN() + } + return number +} diff --git a/go/tinycolor/color_test.go b/go/tinycolor/color_test.go new file mode 100644 index 00000000..7819420d --- /dev/null +++ b/go/tinycolor/color_test.go @@ -0,0 +1,43 @@ +package tinycolor + +import "testing" + +func TestPhaseOneInputsMatchTinyColor(t *testing.T) { + tests := []struct { + name string + input any + ratio bool + valid bool + format string + alpha float64 + value string + }{ + {"name", "red", false, true, "name", 1, "red"}, + {"hex", "#000", false, true, "hex", 1, "#000000"}, + {"invalid", "not a color", false, false, "", 1, "#000000"}, + {"transparent", "transparent", false, true, "name", 0, "transparent"}, + {"rgba", "rgba(255, 0, 0, .5)", false, true, "rgb", .5, "rgba(255, 0, 0, 0.5)"}, + {"hsl", "hsl(0, 100%, 50%)", false, true, "hsl", 1, "hsl(0, 100%, 50%)"}, + {"hsv", "hsv(0, 100%, 100%)", false, true, "hsv", 1, "hsv(0, 100%, 100%)"}, + {"rgb object", map[string]any{"r": float64(255), "g": float64(0), "b": float64(0)}, false, true, "rgb", 1, "rgb(255, 0, 0)"}, + {"from ratio", map[string]any{"r": float64(1), "g": float64(0), "b": float64(0)}, true, true, "prgb", 1, "rgb(100%, 0%, 0%)"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + color, err := FromCompat(tt.input, tt.ratio) + if err != nil { + t.Fatal(err) + } + if color.Valid() != tt.valid || color.Format() != tt.format || color.Alpha() != tt.alpha || color.String() != tt.value { + t.Fatalf("got valid=%v format=%q alpha=%v string=%q", color.Valid(), color.Format(), color.Alpha(), color.String()) + } + }) + } +} + +func TestPhaseOneRejectsUnsupportedInput(t *testing.T) { + if _, err := FromCompat("blue", false); err == nil { + t.Fatal("expected unsupported input error") + } +} From 328a54c6e99df4577d4a237309db138d1ffbdee9 Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 01:20:26 +0530 Subject: [PATCH 086/165] docs: plan Phase 2 parser parity --- .planning/ROADMAP.md | 16 ++-- .planning/STATE.md | 7 +- .../02-parsing-and-color-state/02-01-PLAN.md | 90 +++++++++++++++++++ .../02-parsing-and-color-state/02-02-PLAN.md | 82 +++++++++++++++++ .../02-parsing-and-color-state/02-CONTEXT.md | 72 +++++++++++++++ .../02-parsing-and-color-state/02-RESEARCH.md | 34 +++++++ .../02-VALIDATION.md | 38 ++++++++ 7 files changed, 331 insertions(+), 8 deletions(-) create mode 100644 .planning/phases/02-parsing-and-color-state/02-01-PLAN.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-02-PLAN.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-CONTEXT.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-RESEARCH.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-VALIDATION.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 45bf6361..da09cbea 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -8,7 +8,7 @@ through parsing and observable output, to operations and delivery proof. ## Phases -- [ ] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. +- [x] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. - [ ] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. - [ ] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. - [ ] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. @@ -24,10 +24,10 @@ through parsing and observable output, to operations and delivery proof. 1. `go/` builds as an independent module without changing JavaScript source files. 2. Node and Go runners accept and emit the documented JSONL request/response protocol. 3. A differential command reports case count, mismatch count, and complete mismatch records. -**Plans**: 1 plan +**Plans**: 1 plan (complete 2026-08-01) Plans: -- [ ] 01-01: Create the Go module, JSONL runners, smoke corpus, and baseline report. +- [x] 01-01: Create the Go module, JSONL runners, smoke corpus, and baseline report. ### Phase 2: Parsing and Color State **Goal**: Go accepts the same supported TinyColor inputs and retains the same validity, format, alpha, and normalized color state. @@ -37,7 +37,11 @@ Plans: 1. Hex, RGB(A), HSL(A), HSV(A), CSS names, transparent, and object inputs produce matching state. 2. Permissive syntax, boundary values, invalid input, and alpha normalization match the oracle corpus. 3. Every discovered parser mismatch has a deterministic regression record. -**Plans**: TBD +**Plans**: 2 plans + +Plans: +- [ ] 02-01: Replace Phase 1 fixed inputs with normalized color/model and HEX/RGB/name parsing. +- [ ] 02-02: Add HSL/HSV/object parsing and parser differential corpus. ### Phase 3: Conversion and Representation **Goal**: Go exposes TinyColor-equivalent conversion, string formatting, format fallback, and color analysis. @@ -73,8 +77,8 @@ Plans: | Phase | Plans Complete | Status | Completed | |---|---|---|---| -| 1. Foundation and Oracle | 0/1 | Not started | - | -| 2. Parsing and Color State | 0/TBD | Not started | - | +| 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | +| 2. Parsing and Color State | 0/2 | Not started | - | | 3. Conversion and Representation | 0/TBD | Not started | - | | 4. Operations and Palettes | 0/TBD | Not started | - | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 58b23418..b10aced1 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -6,5 +6,8 @@ - JavaScript source and tests are the immutable oracle. - Node 24.18.0 and Go 1.26.1 are available locally. - `deno` is not installed; no original-source test pass is claimed. -- No Go implementation or compatibility result exists yet. - +- Phase 1 completed in commit `78423e3`: Go/Node JSONL runners, protocol tests, + and a fixed corpus passed 9/9 with zero mismatches. This is not whole-library + parity. +- Phase 2 is planned next; it replaces the fixed Phase 1 input decoder with the + complete TinyColor parser while preserving the compatibility protocol. diff --git a/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md new file mode 100644 index 00000000..f97563de --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md @@ -0,0 +1,90 @@ +--- +phase: 2 +plan: 1 +type: execute +subsystem: parser-model-rgb +tags: [go, parser, tinycolor, differential-testing] +wave: 1 +depends_on: [] +files_modified: + - go/internal/color/model.go + - go/internal/color/bounds.go + - go/internal/color/model_test.go + - go/internal/parser/parser.go + - go/internal/parser/hex.go + - go/internal/parser/rgb.go + - go/internal/parser/names.go + - go/internal/parser/parser_test.go + - go/tinycolor/color.go + - compat/cases/parser-hex-rgb.jsonl + - COMPATIBILITY.md +autonomous: true +requirements: [PAR-01, PAR-02, QLT-02] +must_haves: + truths: + - "HEX, RGB(A), named colors, transparent, invalid input, and RGB objects match local TinyColor state." + - "Phase 1 JSONL operations remain supported without hard-coded color inputs." + artifacts: + - path: "go/internal/color/model.go" + provides: "Normalized RGBA, validity, and format state" + - path: "go/internal/parser/parser.go" + provides: "Single string/object parser entrypoint" + - path: "compat/cases/parser-hex-rgb.jsonl" + provides: "Exact source-derived parser corpus" +--- + + +Replace the fixed Phase 1 color decoder with the shared normalized model and +the first complete parser slice: bounds, HEX, RGB(A), names, transparent, +invalid input, and RGB objects. + + + + +Create the normalized color model and JavaScript-equivalent bounds +go/internal/color/model.go, go/internal/color/bounds.go, go/internal/color/model_test.go + +- `mod.js` lines 359-442 and 1047-1128 +- `test.js` lines 274-443 and 698-746 +- `go/tinycolor/color.go` + + +Define one private normalized RGBA model with float RGB channels, alpha, +validity, a typed format that can represent source false distinctly from known +format strings, and original input metadata. Preserve source original-input +rules: null and empty become `""`; string case is retained; object input remains +JSON-structurally equal; and FromRatio retains its transformed object. Port `bound01`, `boundAlpha`, percentage detection, +`1.0` handling, clamping, modulo, and near-maximum rounding from `mod.js`. +Write table tests for negative/overflow values, `1`, string `1.0`, percentages, +invalid alpha, alpha zero, the source invalid-black state, lowercase/uppercase/ +mixed strings, objects, empty strings, and null. + +Set-Location go; go test ./internal/color; go vet ./... +All normalized state and bounds tests pass with explicit source-derived boundary values. + + + +Parse HEX, RGB(A), names, transparent, and RGB objects through the shared model +go/internal/parser/parser.go, go/internal/parser/hex.go, go/internal/parser/rgb.go, go/internal/parser/names.go, go/internal/parser/parser_test.go, go/tinycolor/color.go, compat/cases/parser-hex-rgb.jsonl, COMPATIBILITY.md + +- `mod.js` lines 359-442 and 1129-1260 +- `test.js` lines 316-443, 487-697, and 698-746 +- `compat/js-runner.mjs` +- `compat/cases/smoke.jsonl` + + +Implement one parser entrypoint used by `go/tinycolor`; remove fixed-input +switches from the public facade. Accept optional-`#` 3/4/6/8 hex, case-insensitive +trimmed names, transparent, and source-permissive RGB/RGBA fields. Generate or +commit the full local named-color map as Go data derived from `mod.js`. Accept +RGB object fields using the same bounds and alpha rules. Preserve `name`, `hex`, +`hex8`, `rgb`, and `prgb` formats. Add source-derived JSONL cases for every +syntax family, name aliases, invalid strings/objects, percentage RGB, and +alpha boundaries, original strings/objects/null/empty values; retain all Phase 1 smoke cases unchanged. +Extend both JSONL `inspect` results with an `original` field: Node uses +`getOriginalInput()` and Go serializes the model's JSON-safe original value. + +Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/smoke.jsonl +HEX/RGB/name/object differential corpus and the unchanged Phase 1 smoke corpus exit 0 with no mismatches. + + diff --git a/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md new file mode 100644 index 00000000..e3f4981f --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md @@ -0,0 +1,82 @@ +--- +phase: 2 +plan: 2 +type: execute +subsystem: parser-hsl-hsv +tags: [go, parser, hsl, hsv, differential-testing] +wave: 2 +depends_on: [02-01] +files_modified: + - go/internal/parser/hsl.go + - go/internal/parser/hsv.go + - go/internal/parser/object.go + - go/internal/parser/parser_test.go + - go/tinycolor/color.go + - compat/cases/parser.jsonl + - COMPATIBILITY.md +autonomous: true +requirements: [PAR-01, PAR-02, QLT-02] +must_haves: + truths: + - "HSL(A), HSV(A), HSL/HSV objects, and FromRatio produce source-equivalent normalized state." + - "The complete Phase 2 parser corpus and Phase 1 smoke corpus report zero mismatches." + artifacts: + - path: "go/internal/parser/hsl.go" + provides: "HSL(A) parser path to normalized RGB" + - path: "go/internal/parser/hsv.go" + provides: "HSV(A) parser path to normalized RGB" + - path: "compat/cases/parser.jsonl" + provides: "Full Phase 2 parser differential corpus" +--- + + +Finish the input surface by porting HSL/HSV conversion used during parsing, +object precedence, FromRatio transformation, and the full Phase 2 parser corpus. + + + + +Parse HSL(A), HSV(A), typed objects, and FromRatio using source bounds +go/internal/parser/hsl.go, go/internal/parser/hsv.go, go/internal/parser/object.go, go/internal/parser/parser_test.go, go/tinycolor/color.go + +- `mod.js` lines 359-572 and 1129-1260 +- `test.js` lines 274-315, 444-515, 945-1096 +- `go/internal/color/bounds.go` +- `go/internal/parser/parser.go` + + +Port only the RGB conversion math required to normalize parsed HSL/HSV values; +keep output conversion APIs for Phase 3. Match source object precedence RGB, +then HSV, then HSL. Accept CSS-unit HSL/HSV object fields, the permissive +functional syntax, hue wrapping, saturation/lightness/value ratios, and alpha +normalization. Implement FromRatio by converting every non-alpha object field +through the source percentage transformation before parser dispatch. Add exact +tests for source HSL/HSV examples, decimals, percentages, wrapped hues, alpha, +and malformed components. + +Set-Location go; go test ./internal/parser ./tinycolor; go vet ./... +Focused HSL/HSV, object, and FromRatio tests pass and no parser path bypasses the normalized model. + + + +Publish the complete parser differential corpus and compatibility evidence +compat/cases/parser.jsonl, COMPATIBILITY.md + +- `test.js` lines 274-747 and 945-1096 +- `compat/cases/parser-hex-rgb.jsonl` +- `compat/run.mjs` +- `COMPATIBILITY.md` + + +Create `compat/cases/parser.jsonl` from source cases covering ratios, RGB, +percentage RGB, HSL(A), HSV(A), all hex lengths, names, transparent, invalid +input, object precedence, alpha values, whitespace/case variants, and boundary +values. Run it through the unchanged Node/Go runner; record exact case count, +command, and result in `COMPATIBILITY.md`. Every mismatch must retain the +runner's input/JS/Go/owner/suspected-package record until fixed. + +Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +The full parser corpus and Phase 1 smoke corpus exit 0; compatibility evidence names the corpus count and no oracle files changed. + + + diff --git a/.planning/phases/02-parsing-and-color-state/02-CONTEXT.md b/.planning/phases/02-parsing-and-color-state/02-CONTEXT.md new file mode 100644 index 00000000..f06371a3 --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-CONTEXT.md @@ -0,0 +1,72 @@ +# Phase 2: Parsing and Color State - Context + +**Gathered:** 2026-08-01 +**Status:** Ready for planning + + +## Phase Boundary + +Replace the fixed Phase 1 decoder with the complete input behavior in local +`mod.js`: strings, object input, alpha, source format, validity, and normalized +RGBA state. Output formatting beyond the minimal compatibility snapshot belongs +to Phase 3. + + + +## Implementation Decisions + +### Ownership and boundaries +- @rajeet-04 owns `go/internal/color` and `go/internal/parser` for this phase. +- `go/tinycolor` remains a thin public facade over the internal model; do not + move formatting, readability, mutations, or palettes into Phase 2. + +### Source equivalence +- `mod.js` is authoritative for `inputToRGB`, `bound01`, `boundAlpha`, + `isValidCSSUnit`, and `stringInputToObject`. +- Preserve source format values: `rgb`, `prgb`, `hsl`, `hsv`, `hex`, `hex8`, + `name`, and invalid `false` (represented internally without an empty-string + ambiguity). +- Invalid input is a valid construction result with `Valid=false`, black RGB, + alpha 1, and no format; it is not a Go parsing error. + +### Supported inputs +- Strings: case-insensitive/trimming names, transparent, 3/4/6/8 hex with + optional `#`, and the permissive RGB(A)/HSL(A)/HSV(A) grammar from source. +- Objects: RGB, HSL, HSV, optional alpha, CSS-unit values, and `FromRatio`. +- Preserve alpha normalization: malformed, negative, or >1 alpha becomes 1; + numeric zero remains zero. + +### Compatibility evidence +- Use generated JSONL corpus records derived from `test.js`; no edits to the + upstream test file. +- Each mismatch includes input, operation, source result, Go result, owner, and + suspected package. No global float tolerance. + +### the agent's Discretion +- Exact Go private type names and parser helper decomposition. +- Whether name data is generated from `mod.js` during development or checked in + as generated Go data, provided the committed table comes from this checkout. + + + +## Canonical References + +### Upstream behavior +- `mod.js` lines 359-654 — input-to-RGB and conversion paths invoked by parsing. +- `mod.js` lines 1047-1260 — named colors, bounds, regex grammar, and string parser. +- `test.js` lines 274-747 — ratio, RGB, HSL, HEX, HSV, invalid, name, and alpha tests. + +### Project contracts +- `AGENT.md` — ownership and non-negotiable parity behavior. +- `docs/ARCHITECTURE.md` — parser/model and adapter layers. +- `docs/TESTING.md` — comparison rules. +- `.planning/phases/01-foundation-and-oracle/01-01-PLAN.md` — stable protocol that Phase 2 must keep working. + + + +## Deferred Ideas + +Public output formats, analysis, manipulation, palettes, CLI, CI, benchmarks, +and any syntax not accepted by the local source checkout. + + diff --git a/.planning/phases/02-parsing-and-color-state/02-RESEARCH.md b/.planning/phases/02-parsing-and-color-state/02-RESEARCH.md new file mode 100644 index 00000000..fbf9fb8f --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-RESEARCH.md @@ -0,0 +1,34 @@ +# Phase 2 Research — Parsing and Color State + +## Source facts + +- TinyColor parses with `inputToRGB`: strings first become object-like fields, + then RGB has precedence over HSV, which has precedence over HSL. +- `bound01` treats string `1.0` as `100%`, recognizes `%`, clamps before + scaling, returns exactly 1 near the upper bound, and otherwise uses modulo. +- `boundAlpha` uses JavaScript `parseFloat`; non-numeric, negative, and >1 + values become 1. +- Its CSS unit and functional syntax are intentionally permissive: commas and + parentheses are optional, whitespace can separate components, signs and + decimal values are accepted, and matching is case-insensitive after lowercasing. +- Hex permits `#` optionally at 3, 4, 6, and 8 digits. Four/eight digit alpha + converts from hex to `[0,1]` and receives format `hex8`. +- Named colors are the source `names` map; recognized names are converted to + hex but retain source format `name`. `transparent` is special (`0,0,0,0`). +- Invalid colors remain black with alpha 1 and false validity rather than + returning an exception. + +## Design consequence + +Make a private normalized model the only parser output. The public facade and +compatibility runner should consume that model rather than duplicate bounds, +format decisions, or name handling. The model should retain channel precision; +observable rounding belongs to Phase 3. + +## Validation architecture + +Run focused Go unit tests for model/bounds/parser and differential JSONL cases +through the existing Node runner. Use exact rendered/JSON values. Test parser +categories separately so a mismatch names `go/internal/parser` or +`go/internal/color`, not a vague package. + diff --git a/.planning/phases/02-parsing-and-color-state/02-VALIDATION.md b/.planning/phases/02-parsing-and-color-state/02-VALIDATION.md new file mode 100644 index 00000000..fb059c18 --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-VALIDATION.md @@ -0,0 +1,38 @@ +--- +phase: 2 +slug: parsing-and-color-state +status: draft +nyquist_compliant: true +wave_0_complete: false +created: 2026-08-01 +--- + +# Phase 2 — Validation Strategy + +| Property | Value | +|---|---| +| Framework | Go standard `testing` plus Node 24.18.0 | +| Quick run | `go test ./internal/color ./internal/parser` from `go/` | +| Full run | `go test ./...; node compat/run.mjs compat/cases/parser.jsonl` | +| Static checks | `go vet ./...` and no output from `gofmt -d` | + +## Sampling rate + +- After each parser/model task: focused Go tests and `go vet ./...`. +- After Plan 02-01: HEX/RGB/name differential corpus. +- After Plan 02-02: full parser corpus and Phase 1 smoke corpus. + +## Verification map + +| Task | Plan | Requirement | Automated proof | +|---|---|---|---| +| 02-01-01 | 02-01 | PAR-02 | `go test ./internal/color` | +| 02-01-02 | 02-01 | PAR-01 | `node compat/run.mjs compat/cases/parser-hex-rgb.jsonl` | +| 02-02-01 | 02-02 | PAR-01, PAR-02 | `go test ./internal/parser` | +| 02-02-02 | 02-02 | QLT-02 | `node compat/run.mjs compat/cases/parser.jsonl` | + +## Manual-only check + +When Deno is available, run `deno task test`; until then it remains explicitly +unverified and does not substitute for Node differential evidence. + From 8188db6e8858802f6e5a4ceb5c249f5097637824 Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 01:40:28 +0530 Subject: [PATCH 087/165] feat: complete Phase 2 parser parity --- .gitignore | 3 +- .planning/ROADMAP.md | 10 +- .planning/STATE.md | 7 +- .../02-parsing-and-color-state/02-01-PLAN.md | 28 +-- .../02-parsing-and-color-state/02-02-PLAN.md | 25 ++- .port-mortem.toml | 4 + AGENT.md | 18 +- COMPATIBILITY.md | 33 +++- DECISIONS.md | 3 +- Dockerfile | 10 + Makefile | 12 ++ PLAN.md | 2 +- PORT.md | 12 ++ bench/methodology.md | 5 + bench/results.json | 4 + compat/cases/parser-hex-rgb.jsonl | 26 +++ compat/cases/parser.jsonl | 23 +++ compat/js-runner.mjs | 1 + compat/run.mjs | 4 +- docs/ARCHITECTURE.md | 9 +- docs/TESTING.md | 3 +- fuzz/README.md | 4 + go/tinycolor/color.go | 74 -------- {go => src}/cmd/tinycolor-compat/main.go | 0 {go => src}/go.mod | 0 src/internal/color/bounds.go | 77 ++++++++ src/internal/color/model.go | 53 ++++++ src/internal/color/model_test.go | 63 ++++++ {go => src}/internal/compat/protocol.go | 0 {go => src}/internal/compat/protocol_test.go | 0 src/internal/parser/hex.go | 41 ++++ src/internal/parser/hsl.go | 13 ++ src/internal/parser/hsv.go | 13 ++ src/internal/parser/names.go | 43 +++++ src/internal/parser/object.go | 22 +++ src/internal/parser/parser.go | 179 ++++++++++++++++++ src/internal/parser/parser_test.go | 75 ++++++++ src/internal/parser/rgb.go | 56 ++++++ src/tinycolor/color.go | 128 +++++++++++++ {go => src}/tinycolor/color_test.go | 7 +- tests/original/README.md | 5 + tests/original/manifest.sha256 | 3 + tests/port/README.md | 4 + .../port/adapter.test.mjs | 1 + 44 files changed, 965 insertions(+), 138 deletions(-) create mode 100644 .port-mortem.toml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 PORT.md create mode 100644 bench/methodology.md create mode 100644 bench/results.json create mode 100644 compat/cases/parser-hex-rgb.jsonl create mode 100644 compat/cases/parser.jsonl create mode 100644 fuzz/README.md delete mode 100644 go/tinycolor/color.go rename {go => src}/cmd/tinycolor-compat/main.go (100%) rename {go => src}/go.mod (100%) create mode 100644 src/internal/color/bounds.go create mode 100644 src/internal/color/model.go create mode 100644 src/internal/color/model_test.go rename {go => src}/internal/compat/protocol.go (100%) rename {go => src}/internal/compat/protocol_test.go (100%) create mode 100644 src/internal/parser/hex.go create mode 100644 src/internal/parser/hsl.go create mode 100644 src/internal/parser/hsv.go create mode 100644 src/internal/parser/names.go create mode 100644 src/internal/parser/object.go create mode 100644 src/internal/parser/parser.go create mode 100644 src/internal/parser/parser_test.go create mode 100644 src/internal/parser/rgb.go create mode 100644 src/tinycolor/color.go rename {go => src}/tinycolor/color_test.go (86%) create mode 100644 tests/original/README.md create mode 100644 tests/original/manifest.sha256 create mode 100644 tests/port/README.md rename compat/js-runner.test.mjs => tests/port/adapter.test.mjs (97%) diff --git a/.gitignore b/.gitignore index 1c08f4da..24a07a3e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules .vscode cov_profile cov_profile.lcov -.cache/ \ No newline at end of file +.cache/ +bin/ diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index da09cbea..ed3c6d69 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -21,7 +21,7 @@ through parsing and observable output, to operations and delivery proof. **Depends on**: Nothing (first phase) **Requirements**: [EQV-01, QLT-01, QLT-03] **Success Criteria**: - 1. `go/` builds as an independent module without changing JavaScript source files. + 1. `src/` builds as an independent module without changing JavaScript source files. 2. Node and Go runners accept and emit the documented JSONL request/response protocol. 3. A differential command reports case count, mismatch count, and complete mismatch records. **Plans**: 1 plan (complete 2026-08-01) @@ -37,11 +37,11 @@ Plans: 1. Hex, RGB(A), HSL(A), HSV(A), CSS names, transparent, and object inputs produce matching state. 2. Permissive syntax, boundary values, invalid input, and alpha normalization match the oracle corpus. 3. Every discovered parser mismatch has a deterministic regression record. -**Plans**: 2 plans +**Plans**: 2 plans (complete 2026-08-01) Plans: -- [ ] 02-01: Replace Phase 1 fixed inputs with normalized color/model and HEX/RGB/name parsing. -- [ ] 02-02: Add HSL/HSV/object parsing and parser differential corpus. +- [x] 02-01: Replace Phase 1 fixed inputs with normalized color/model and HEX/RGB/name parsing. +- [x] 02-02: Add HSL/HSV/object parsing and parser differential corpus. ### Phase 3: Conversion and Representation **Goal**: Go exposes TinyColor-equivalent conversion, string formatting, format fallback, and color analysis. @@ -78,7 +78,7 @@ Plans: | Phase | Plans Complete | Status | Completed | |---|---|---|---| | 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | -| 2. Parsing and Color State | 0/2 | Not started | - | +| 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | | 3. Conversion and Representation | 0/TBD | Not started | - | | 4. Operations and Palettes | 0/TBD | Not started | - | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index b10aced1..e28e1a16 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -9,5 +9,8 @@ - Phase 1 completed in commit `78423e3`: Go/Node JSONL runners, protocol tests, and a fixed corpus passed 9/9 with zero mismatches. This is not whole-library parity. -- Phase 2 is planned next; it replaces the fixed Phase 1 input decoder with the - complete TinyColor parser while preserving the compatibility protocol. +- Phase 2 completed locally: the normalized parser covers HEX, RGB(A), HSL(A), + HSV(A), names, objects, invalid input, and FromRatio. Differential evidence: + 26/26 HEX/RGB/name cases, 23/23 complete parser cases, and 9/9 smoke cases. +- The Go module lives in `src/`; `tests/original/manifest.sha256` pins the + unmodified root JavaScript oracle. diff --git a/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md index f97563de..3910cd67 100644 --- a/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md +++ b/.planning/phases/02-parsing-and-color-state/02-01-PLAN.md @@ -7,15 +7,15 @@ tags: [go, parser, tinycolor, differential-testing] wave: 1 depends_on: [] files_modified: - - go/internal/color/model.go - - go/internal/color/bounds.go - - go/internal/color/model_test.go - - go/internal/parser/parser.go - - go/internal/parser/hex.go - - go/internal/parser/rgb.go - - go/internal/parser/names.go - - go/internal/parser/parser_test.go - - go/tinycolor/color.go + - src/internal/color/model.go + - src/internal/color/bounds.go + - src/internal/color/model_test.go + - src/internal/parser/parser.go + - src/internal/parser/hex.go + - src/internal/parser/rgb.go + - src/internal/parser/names.go + - src/internal/parser/parser_test.go + - src/tinycolor/color.go - compat/cases/parser-hex-rgb.jsonl - COMPATIBILITY.md autonomous: true @@ -42,11 +42,11 @@ invalid input, and RGB objects. Create the normalized color model and JavaScript-equivalent bounds -go/internal/color/model.go, go/internal/color/bounds.go, go/internal/color/model_test.go +src/internal/color/model.go, src/internal/color/bounds.go, src/internal/color/model_test.go - `mod.js` lines 359-442 and 1047-1128 - `test.js` lines 274-443 and 698-746 -- `go/tinycolor/color.go` +- `src/tinycolor/color.go` Define one private normalized RGBA model with float RGB channels, alpha, @@ -59,13 +59,13 @@ Write table tests for negative/overflow values, `1`, string `1.0`, percentages, invalid alpha, alpha zero, the source invalid-black state, lowercase/uppercase/ mixed strings, objects, empty strings, and null. -Set-Location go; go test ./internal/color; go vet ./... +Set-Location src; go test ./internal/color; go vet ./... All normalized state and bounds tests pass with explicit source-derived boundary values. Parse HEX, RGB(A), names, transparent, and RGB objects through the shared model -go/internal/parser/parser.go, go/internal/parser/hex.go, go/internal/parser/rgb.go, go/internal/parser/names.go, go/internal/parser/parser_test.go, go/tinycolor/color.go, compat/cases/parser-hex-rgb.jsonl, COMPATIBILITY.md +src/internal/parser/parser.go, src/internal/parser/hex.go, src/internal/parser/rgb.go, src/internal/parser/names.go, src/internal/parser/parser_test.go, src/tinycolor/color.go, compat/cases/parser-hex-rgb.jsonl, COMPATIBILITY.md - `mod.js` lines 359-442 and 1129-1260 - `test.js` lines 316-443, 487-697, and 698-746 @@ -84,7 +84,7 @@ alpha boundaries, original strings/objects/null/empty values; retain all Phase 1 Extend both JSONL `inspect` results with an `original` field: Node uses `getOriginalInput()` and Go serializes the model's JSON-safe original value. -Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/smoke.jsonl +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/smoke.jsonl HEX/RGB/name/object differential corpus and the unchanged Phase 1 smoke corpus exit 0 with no mismatches. diff --git a/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md index e3f4981f..4d956f94 100644 --- a/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md +++ b/.planning/phases/02-parsing-and-color-state/02-02-PLAN.md @@ -7,11 +7,11 @@ tags: [go, parser, hsl, hsv, differential-testing] wave: 2 depends_on: [02-01] files_modified: - - go/internal/parser/hsl.go - - go/internal/parser/hsv.go - - go/internal/parser/object.go - - go/internal/parser/parser_test.go - - go/tinycolor/color.go + - src/internal/parser/hsl.go + - src/internal/parser/hsv.go + - src/internal/parser/object.go + - src/internal/parser/parser_test.go + - src/tinycolor/color.go - compat/cases/parser.jsonl - COMPATIBILITY.md autonomous: true @@ -21,9 +21,9 @@ must_haves: - "HSL(A), HSV(A), HSL/HSV objects, and FromRatio produce source-equivalent normalized state." - "The complete Phase 2 parser corpus and Phase 1 smoke corpus report zero mismatches." artifacts: - - path: "go/internal/parser/hsl.go" + - path: "src/internal/parser/hsl.go" provides: "HSL(A) parser path to normalized RGB" - - path: "go/internal/parser/hsv.go" + - path: "src/internal/parser/hsv.go" provides: "HSV(A) parser path to normalized RGB" - path: "compat/cases/parser.jsonl" provides: "Full Phase 2 parser differential corpus" @@ -37,12 +37,12 @@ object precedence, FromRatio transformation, and the full Phase 2 parser corpus. Parse HSL(A), HSV(A), typed objects, and FromRatio using source bounds -go/internal/parser/hsl.go, go/internal/parser/hsv.go, go/internal/parser/object.go, go/internal/parser/parser_test.go, go/tinycolor/color.go +src/internal/parser/hsl.go, src/internal/parser/hsv.go, src/internal/parser/object.go, src/internal/parser/parser_test.go, src/tinycolor/color.go - `mod.js` lines 359-572 and 1129-1260 - `test.js` lines 274-315, 444-515, 945-1096 -- `go/internal/color/bounds.go` -- `go/internal/parser/parser.go` +- `src/internal/color/bounds.go` +- `src/internal/parser/parser.go` Port only the RGB conversion math required to normalize parsed HSL/HSV values; @@ -54,7 +54,7 @@ through the source percentage transformation before parser dispatch. Add exact tests for source HSL/HSV examples, decimals, percentages, wrapped hues, alpha, and malformed components. -Set-Location go; go test ./internal/parser ./tinycolor; go vet ./... +Set-Location src; go test ./internal/parser ./tinycolor; go vet ./... Focused HSL/HSV, object, and FromRatio tests pass and no parser path bypasses the normalized model. @@ -75,8 +75,7 @@ values. Run it through the unchanged Node/Go runner; record exact case count, command, and result in `COMPATIBILITY.md`. Every mismatch must retain the runner's input/JS/Go/owner/suspected-package record until fixed. -Set-Location go; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/smoke.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js The full parser corpus and Phase 1 smoke corpus exit 0; compatibility evidence names the corpus count and no oracle files changed. - diff --git a/.port-mortem.toml b/.port-mortem.toml new file mode 100644 index 00000000..e2f31c43 --- /dev/null +++ b/.port-mortem.toml @@ -0,0 +1,4 @@ +track = "H" +source_url = "https://github.com/bgrins/TinyColor.git" +kickoff_commit = "b49018c9f2dbca313d80d7a4dad25e26143cfe01" +oracle_manifest = "tests/original/manifest.sha256" diff --git a/AGENT.md b/AGENT.md index 02157a1d..63e35f31 100644 --- a/AGENT.md +++ b/AGENT.md @@ -12,9 +12,12 @@ compatibility project: observable TinyColor behavior is the specification. - `mod.js`, `tinycolor.js`, `test.js`, `npm/`, `dist/`, and `demo/` are the immutable JavaScript oracle. Do not edit them except for a separately agreed upstream maintenance change. -- New Go work belongs under `go/`; `go/go.mod` is the port module boundary. +- New Go work belongs under `src/`; `src/go.mod` is the port module boundary. - Oracle adapters, generated fixtures, and mismatch reports belong under `compat/`. Do not rewrite the original test suite to make it pass. +- `tests/original/manifest.sha256` pins the immutable root oracle. Port-owned + adapter tests live in `tests/port/`; differential fuzzing and benchmarks live + in `fuzz/` and `bench/` respectively. - Human-facing project docs belong in `docs/`; compatibility status belongs in `COMPATIBILITY.md`. @@ -70,9 +73,9 @@ Known differences (or none): | Owner | Primary boundary | Cannot merge without | |---|---|---| -| A — Parsing/model | `go/internal/color`, `go/internal/parser` | parser differential cases | -| B — Conversion/API | `go/tinycolor`, formatting/readability/palettes | deterministic parity tests | -| C — Compatibility/quality | `compat`, `go/testdata`, `COMPATIBILITY.md` | reproducible report | +| A — Parsing/model | `src/internal/color`, `src/internal/parser` | parser differential cases | +| B — Conversion/API | `src/tinycolor`, formatting/readability/palettes | deterministic parity tests | +| C — Compatibility/quality | `compat`, `src/testdata`, `COMPATIBILITY.md` | reproducible report | | D — Delivery | CLI, CI, benchmarks, docs | clean-checkout commands | Coordinate through exported contracts, never by editing another owner’s files @@ -84,10 +87,10 @@ of the implicated module fixes it. Run the narrowest applicable check while developing, then run the phase gate: ```powershell -go test ./... # from go/ +go test ./... # from src/ node compat/js-runner.mjs < cases.json # from repository root -go test -run TestDifferential ./... # from go/ -go vet ./... # from go/ +go test -run TestDifferential ./... # from src/ +go vet ./... # from src/ gofmt -w ``` @@ -100,4 +103,3 @@ passing test result. A feature is done only when its source behavior is mapped, Go unit tests pass, the matching oracle cases pass, the compatibility matrix is updated, and any remaining mismatch is named with input, JavaScript result, Go result, and owner. - diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 5f748040..60c08e28 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -7,10 +7,10 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | Area | Status | Evidence | Notes | |---|---|---|---| | Oracle baseline | Ready | `mod.js`, `test.js`, Node 24.18.0 | JavaScript source is untouched. | -| Node adapter | Passing | `node compat/js-runner.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | -| Go adapter | Passing | `go test ./...` from `go/` | JSONL schema shared with Node adapter. | -| HEX/RGB/name parsing | Planned | Wave 1 | Include invalid and permissive syntax. | -| HSL/HSV parsing | Planned | Wave 1 | Include ratios, percentages, and wrapping. | +| Node adapter | Passing | `node tests/port/adapter.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | +| Go adapter | Passing | `go test ./...` from `src/` | JSONL schema shared with Node adapter. | +| HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | +| HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | | Conversion/formatting | Planned | Wave 2 | Exact strings and rounding. | | Analysis/readability | Planned | Wave 2–3 | Preserve WCAG defaults. | | Manipulation/palettes | Planned | Wave 3 | Preserve defaults and ordering. | @@ -37,9 +37,30 @@ mismatches**: ```powershell $env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -node compat/js-runner.test.mjs -Set-Location go; go test ./...; go vet ./...; Set-Location .. +node tests/port/adapter.test.mjs +Set-Location src; go test ./...; go vet ./...; Set-Location .. node compat/run.mjs compat/cases/smoke.jsonl ``` This is only the fixed Phase 1 corpus; it is not a full TinyColor parity claim. + +## Phase 2 parser slice result + +On 2026-08-01, the HEX/RGB/name parser corpus passed with **26/26 cases** and +**0 mismatches**: + +```powershell +$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' +Set-Location src; go test ./...; go vet ./...; Set-Location .. +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/smoke.jsonl +``` + +This records only the completed parser slice; the full HSL/HSV corpus remains +Phase 2 Plan 02 work. + +## Phase 2 full parser result + +`node compat/run.mjs compat/cases/parser.jsonl` passed with **23/23 cases** and +**0 mismatches** on 2026-08-01. The unchanged Phase 1 smoke corpus also passed +**9/9** with **0 mismatches**. diff --git a/DECISIONS.md b/DECISIONS.md index 7ab9d024..acb5813d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -4,9 +4,8 @@ |---|---|---|---| | D-001 | Port the checked-out `mod.js` behavior to Go. | It is the exact source and test target selected for the project. | Accepted | | D-002 | Keep JavaScript oracle files immutable. | Differential evidence is credible only when the reference does not move with the port. | Accepted | -| D-003 | Put the Go module in `go/`. | It prevents an in-progress port from contaminating the upstream JavaScript package layout. | Accepted | +| D-003 | Put the Go module in `src/`. | It keeps the port idiomatic while following the Track H submission layout; the JavaScript oracle remains at the root. | Accepted | | D-004 | Use JSON Lines adapters over stdin/stdout. | Node and Go can compare dynamic inputs and operations without modifying source tests. | Accepted | | D-005 | Use Go standard library first. | The port's algorithms are small and self-contained; dependencies add compatibility and supply-chain surface. | Accepted | | D-006 | Preserve source quirks at the adapter boundary; expose an idiomatic Go API separately. | Compatibility and Go usability are both required, but they should not leak dynamic JavaScript semantics into every caller. | Accepted | | D-007 | Deno's source suite is a required later check, not a current pass. | `deno` is not installed in the checked environment as of 2026-08-01. | Accepted | - diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7831a508 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.26 AS build +WORKDIR /app/src +COPY src/go.mod ./ +RUN go mod download +COPY src/ ./ +RUN go build -o /tinycolor-compat ./cmd/tinycolor-compat + +FROM scratch +COPY --from=build /tinycolor-compat /tinycolor-compat +ENTRYPOINT ["/tinycolor-compat"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f8a40eaf --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: build test + +build: + go -C src build -o ../bin/tinycolor-compat ./cmd/tinycolor-compat + +test: + node tests/port/adapter.test.mjs + go -C src test ./... + go -C src vet ./... + node compat/run.mjs compat/cases/parser-hex-rgb.jsonl + node compat/run.mjs compat/cases/parser.jsonl + node compat/run.mjs compat/cases/smoke.jsonl diff --git a/PLAN.md b/PLAN.md index 82ce2e9d..84fdeb53 100644 --- a/PLAN.md +++ b/PLAN.md @@ -28,7 +28,7 @@ remains intact in the repository. ## Wave 0 — Foundation and behavioral oracle -1. Create `go/` as an independent module; add a minimal public package, a JSON +1. Create `src/` as an independent module; add a minimal public package, a JSON Lines request/response protocol, and a Go runner executable. Implement only the fixed smoke-corpus inputs needed to prove the protocol; all generalized parsing and public API behavior remains in later waves. diff --git a/PORT.md b/PORT.md new file mode 100644 index 00000000..aec69edb --- /dev/null +++ b/PORT.md @@ -0,0 +1,12 @@ +# TinyColor Go port + +This repository ports the behavior of the pinned `bgrins/TinyColor` checkout +to Go. Build the compatibility CLI with one command: + +```powershell +make build +``` + +Run the current parity checks with `make test`. The immutable JavaScript oracle +stays at the root and its kickoff hashes are recorded in +`tests/original/manifest.sha256`. diff --git a/bench/methodology.md b/bench/methodology.md new file mode 100644 index 00000000..29dd6d56 --- /dev/null +++ b/bench/methodology.md @@ -0,0 +1,5 @@ +# Benchmark methodology + +Measure the compiled compatibility CLI on a quiet host with the same corpus for +each run. Record median and p99 latency, RSS, startup time, and throughput. +Do not compare results across different hosts or Go versions. diff --git a/bench/results.json b/bench/results.json new file mode 100644 index 00000000..42e6257d --- /dev/null +++ b/bench/results.json @@ -0,0 +1,4 @@ +{ + "status": "not_run", + "reason": "Benchmarking is scheduled for the delivery phase." +} diff --git a/compat/cases/parser-hex-rgb.jsonl b/compat/cases/parser-hex-rgb.jsonl new file mode 100644 index 00000000..77774e1f --- /dev/null +++ b/compat/cases/parser-hex-rgb.jsonl @@ -0,0 +1,26 @@ +{"id":"hex-3","operation":"inspect","input":"#f00"} +{"id":"hex-3-no-hash","operation":"inspect","input":"F00"} +{"id":"hex-4","operation":"inspect","input":"#f008"} +{"id":"hex-6","operation":"inspect","input":"ff0000"} +{"id":"hex-8","operation":"inspect","input":"#ff000080"} +{"id":"hex-invalid","operation":"inspect","input":"##123456"} +{"id":"rgb-space","operation":"inspect","input":"rgb 255 0 0"} +{"id":"rgb-paren","operation":"inspect","input":"rgb(255, 0, 0)"} +{"id":"rgba-alpha","operation":"inspect","input":"rgba 255 0 0 .5"} +{"id":"rgb-percent","operation":"inspect","input":"rgb 100% 0% 0%"} +{"id":"rgb-percent-object","operation":"inspect","input":{"r":"90%","g":"45%","b":"0%","a":0.4}} +{"id":"rgb-clamped","operation":"inspect","input":"rgb 300 -1 0"} +{"id":"rgb-alpha-invalid","operation":"inspect","input":{"r":255,"g":20,"b":10,"a":-1}} +{"id":"rgb-alpha-zero","operation":"inspect","input":{"r":255,"g":20,"b":10,"a":0}} +{"id":"name-red","operation":"inspect","input":"red"} +{"id":"name-alias-aqua","operation":"inspect","input":"aqua"} +{"id":"name-trim-case","operation":"inspect","input":" InDiAnReD "} +{"id":"name-grey","operation":"inspect","input":"grey"} +{"id":"transparent","operation":"inspect","input":"transparent"} +{"id":"invalid-string","operation":"inspect","input":"this is not a color"} +{"id":"invalid-object","operation":"inspect","input":{"r":"invalid","g":"invalid","b":"invalid"}} +{"id":"original-empty","operation":"inspect","input":""} +{"id":"original-null","operation":"inspect","input":null} +{"id":"original-object","operation":"inspect","input":{"r":255,"g":0,"b":0}} +{"id":"ratio-white","operation":"fromRatio","input":{"r":1,"g":1,"b":1}} +{"id":"ratio-alpha","operation":"fromRatio","input":{"r":1,"g":0,"b":0,"a":0.5}} diff --git a/compat/cases/parser.jsonl b/compat/cases/parser.jsonl new file mode 100644 index 00000000..ad81e62e --- /dev/null +++ b/compat/cases/parser.jsonl @@ -0,0 +1,23 @@ +{"id":"hsl-red","operation":"inspect","input":"hsl(0, 100%, 50%)"} +{"id":"hsl-decimal","operation":"inspect","input":"hsl 251 100 0.38"} +{"id":"hsla-alpha","operation":"inspect","input":"hsla(251,100%,38%,.5)"} +{"id":"hsl-wrap","operation":"inspect","input":"hsl(-109,100%,50%)"} +{"id":"hsv-red","operation":"inspect","input":"hsv(0,100%,100%)"} +{"id":"hsv-decimal","operation":"inspect","input":"hsv 251.1 .887 .918"} +{"id":"hsva-alpha","operation":"inspect","input":"hsva 251.1 .887 .918 .5"} +{"id":"hsv-wrap","operation":"inspect","input":"hsv(720,100%,100%)"} +{"id":"hsl-object","operation":"inspect","input":{"h":251,"s":100,"l":0.38}} +{"id":"hsv-object","operation":"inspect","input":{"h":251.1,"s":0.887,"v":0.918}} +{"id":"object-precedence-rgb","operation":"inspect","input":{"r":255,"g":0,"b":0,"h":120,"s":100,"v":100}} +{"id":"object-precedence-hsv","operation":"inspect","input":{"h":120,"s":100,"v":100,"l":50}} +{"id":"hsl-invalid","operation":"inspect","input":{"h":"invalid","s":"invalid","l":"invalid"}} +{"id":"hsv-invalid","operation":"inspect","input":{"h":"invalid","s":"invalid","v":"invalid"}} +{"id":"ratio-hsl","operation":"fromRatio","input":{"h":0,"s":1,"l":0.5,"a":0.5}} +{"id":"ratio-hsv","operation":"fromRatio","input":{"h":0,"s":1,"v":1,"a":0.5}} +{"id":"ratio-rgb-alpha-invalid","operation":"fromRatio","input":{"r":1,"g":0,"b":0,"a":10}} +{"id":"hex8","operation":"inspect","input":"#ff000080"} +{"id":"rgb-percent","operation":"inspect","input":"rgb 100% 0% 0%"} +{"id":"name-alias","operation":"inspect","input":"aqua"} +{"id":"transparent","operation":"inspect","input":"transparent"} +{"id":"invalid","operation":"inspect","input":"this is not a color"} +{"id":"original-null","operation":"inspect","input":null} diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index 0781d649..648dc195 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -7,6 +7,7 @@ const inspect = (color) => ({ alpha: color.getAlpha(), rgb: color.toRgb(), value: color.toString(), + original: color.getOriginalInput(), }); const respond = (id, result, error) => { diff --git a/compat/run.mjs b/compat/run.mjs index 4f9c778c..510a208c 100644 --- a/compat/run.mjs +++ b/compat/run.mjs @@ -24,14 +24,14 @@ let passed = 0; let mismatches = 0; for (const request of cases) { const js = run(process.execPath, ["compat/js-runner.mjs"], root, request); - const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "go"), request); + const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "src"), request); if (same(js, go)) { passed++; continue; } mismatches++; const protocol = Object.hasOwn(js, "result") !== Object.hasOwn(go, "result"); - console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "go/tinycolor" })); + console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "src/tinycolor" })); } console.log(`cases: ${cases.length}`); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 62afbba7..b7e471b9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,7 +7,7 @@ immutable JS source (mod.js, test.js) │ cases ── differential driver ── Go JSONL runner │ - go/tinycolor public API + src/tinycolor public API │ │ internal/color internal/parser │ │ @@ -19,12 +19,12 @@ cases ── differential driver ── Go JSONL runner 1. **Compatibility boundary (`compat/` and runner):** accepts JSON-safe dynamic input, dispatches named operations, and returns a stable record. It owns no color math. -2. **Parsing/model (`go/internal/...`):** turns strings and typed Go inputs into +2. **Parsing/model (`src/internal/...`):** turns strings and typed Go inputs into normalized RGBA plus validity and source-format metadata. -3. **Public library (`go/tinycolor`):** exposes explicit Go values and methods. +3. **Public library (`src/tinycolor`):** exposes explicit Go values and methods. It owns conversions, formatting, mutation, utilities, readability, and palettes. -4. **CLI (`go/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same +4. **CLI (`src/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same library and does not reimplement parsing or conversion. ## Compatibility protocol @@ -42,4 +42,3 @@ method rounds them. Alpha is stored in `[0,1]`; detected format and validity are separate from RGB channels because invalid TinyColor values still render as black. Original input is adapter metadata: typed Go callers should not need to recover JavaScript object identity. - diff --git a/docs/TESTING.md b/docs/TESTING.md index 53a3e605..8050b8ac 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -4,7 +4,7 @@ | Layer | Purpose | Command when implemented | |---|---|---| -| Go unit tests | Local conversion and parser invariants | `go test ./...` from `go/` | +| Go unit tests | Local conversion and parser invariants | `go test ./...` from `src/` | | Differential corpus | Exact observable parity with local `mod.js` | `node compat/run.mjs ` | | Regression corpus | Permanently reproduce every found mismatch | `node compat/run.mjs compat/cases/regression.jsonl` | | Seeded fuzz cases | Find coercion, bounds, and rounding gaps | documented seed command | @@ -29,4 +29,3 @@ 3. State transitions: clone, set alpha, all instance modifiers, default versus explicit zero amount. 4. Structured random RGB/RGBA/HSL/HSV inputs generated from a fixed seed. - diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..d021a6ca --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,4 @@ +# Differential fuzzing + +Phase 3 adds a seeded generator here. Until then, the checked-in JSONL corpora +in `compat/cases/` are the reproducible differential input set. diff --git a/go/tinycolor/color.go b/go/tinycolor/color.go deleted file mode 100644 index 70b3bf5d..00000000 --- a/go/tinycolor/color.go +++ /dev/null @@ -1,74 +0,0 @@ -package tinycolor - -import ( - "fmt" - "math" -) - -// Color is the Phase 1 compatibility value. Phase 2 replaces its fixed input -// decoder with TinyColor's full parser while retaining these observable fields. -type Color struct { - r, g, b int - a float64 - valid bool - format string - value string -} - -func FromCompat(input any, fromRatio bool) (Color, error) { - if fromRatio { - if rgb, ok := input.(map[string]any); ok && number(rgb["r"]) == 1 && number(rgb["g"]) == 0 && number(rgb["b"]) == 0 { - return Color{r: 255, a: 1, valid: true, format: "prgb", value: "rgb(100%, 0%, 0%)"}, nil - } - return Color{}, fmt.Errorf("unsupported input") - } - - switch value := input.(type) { - case string: - switch value { - case "red": - return Color{r: 255, a: 1, valid: true, format: "name", value: "red"}, nil - case "#000": - return Color{a: 1, valid: true, format: "hex", value: "#000000"}, nil - case "not a color": - return Color{a: 1, value: "#000000"}, nil - case "transparent": - return Color{a: 0, valid: true, format: "name", value: "transparent"}, nil - case "rgba(255, 0, 0, .5)": - return Color{r: 255, a: .5, valid: true, format: "rgb", value: "rgba(255, 0, 0, 0.5)"}, nil - case "hsl(0, 100%, 50%)": - return Color{r: 255, a: 1, valid: true, format: "hsl", value: "hsl(0, 100%, 50%)"}, nil - case "hsv(0, 100%, 100%)": - return Color{r: 255, a: 1, valid: true, format: "hsv", value: "hsv(0, 100%, 100%)"}, nil - } - case map[string]any: - if number(value["r"]) == 255 && number(value["g"]) == 0 && number(value["b"]) == 0 { - return Color{r: 255, a: 1, valid: true, format: "rgb", value: "rgb(255, 0, 0)"}, nil - } - } - return Color{}, fmt.Errorf("unsupported input") -} - -func (c Color) Valid() bool { return c.valid } -func (c Color) Format() string { return c.format } -func (c Color) Alpha() float64 { return c.a } -func (c Color) String() string { return c.value } -func (c Color) RGB() map[string]any { - return map[string]any{"r": c.r, "g": c.g, "b": c.b, "a": c.a} -} - -func (c Color) Inspect() map[string]any { - format := any(c.format) - if c.format == "" { - format = false - } - return map[string]any{"valid": c.valid, "format": format, "alpha": c.a, "rgb": c.RGB(), "value": c.String()} -} - -func number(value any) float64 { - number, ok := value.(float64) - if !ok { - return math.NaN() - } - return number -} diff --git a/go/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go similarity index 100% rename from go/cmd/tinycolor-compat/main.go rename to src/cmd/tinycolor-compat/main.go diff --git a/go/go.mod b/src/go.mod similarity index 100% rename from go/go.mod rename to src/go.mod diff --git a/src/internal/color/bounds.go b/src/internal/color/bounds.go new file mode 100644 index 00000000..a9e64d72 --- /dev/null +++ b/src/internal/color/bounds.go @@ -0,0 +1,77 @@ +package color + +import ( + "math" + "regexp" + "strconv" + "strings" +) + +var jsParseFloat = regexp.MustCompile(`^[\t\n\v\f\r ]*[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:[eE][+-]?\d+)?`) + +// ParseFloat mirrors the prefix parsing used by JavaScript parseFloat for the +// numeric forms TinyColor accepts. +func ParseFloat(value any) float64 { + switch number := value.(type) { + case float64: + return number + case float32: + return float64(number) + case int: + return float64(number) + case int64: + return float64(number) + case string: + match := jsParseFloat.FindString(number) + if match == "" { + return math.NaN() + } + parsed, err := strconv.ParseFloat(strings.TrimSpace(match), 64) + if err != nil { + return math.NaN() + } + return parsed + default: + return math.NaN() + } +} + +func IsPercentage(value any) bool { + text, ok := value.(string) + return ok && strings.Contains(text, "%") +} + +func IsOnePointZero(value any) bool { + text, ok := value.(string) + return ok && strings.Contains(text, ".") && ParseFloat(text) == 1 +} + +// Bound01 is TinyColor's bound01 helper, including percentage truncation and +// its near-maximum floating-point correction. +func Bound01(value any, max float64) float64 { + if IsOnePointZero(value) { + value = "100%" + } + percentage := IsPercentage(value) + number := ParseFloat(value) + if math.IsNaN(number) { + return math.NaN() + } + number = math.Min(max, math.Max(0, number)) + if percentage { + number = math.Trunc(number*max) / 100 + } + if math.Abs(number-max) < 0.000001 { + return 1 + } + return math.Mod(number, max) / max +} + +// BoundAlpha keeps TinyColor's intentionally forgiving alpha behavior. +func BoundAlpha(value any) float64 { + alpha := ParseFloat(value) + if math.IsNaN(alpha) || alpha < 0 || alpha > 1 { + return 1 + } + return alpha +} diff --git a/src/internal/color/model.go b/src/internal/color/model.go new file mode 100644 index 00000000..85db3c17 --- /dev/null +++ b/src/internal/color/model.go @@ -0,0 +1,53 @@ +// Package color holds the normalized state shared by all parser paths. +package color + +// Format is TinyColor's detected input format. An empty Format represents the +// source's false format value for invalid input. +type Format string + +const ( + FormatUnknown Format = "" + FormatRGB Format = "rgb" + FormatPRGB Format = "prgb" + FormatHSL Format = "hsl" + FormatHSV Format = "hsv" + FormatHex Format = "hex" + FormatHex8 Format = "hex8" + FormatName Format = "name" +) + +// Model is TinyColor's normalized constructor state. It is internal to this +// module so the public facade cannot bypass source-compatible parsing. +type Model struct { + R, G, B float64 + A float64 + Valid bool + Format Format + Original any +} + +// Invalid returns the source invalid-color state: opaque black, no format, +// and a retained original input. +func Invalid(original any) Model { + return Model{A: 1, Original: original} +} + +// OriginalInput applies TinyColor's constructor normalization to the JSON-safe +// inputs used by the compatibility adapter. +func OriginalInput(input any) any { + switch value := input.(type) { + case nil: + return "" + case string: + return value + case bool: + if !value { + return "" + } + case float64: + if value == 0 { + return "" + } + } + return input +} diff --git a/src/internal/color/model_test.go b/src/internal/color/model_test.go new file mode 100644 index 00000000..a5304110 --- /dev/null +++ b/src/internal/color/model_test.go @@ -0,0 +1,63 @@ +package color + +import ( + "math" + "reflect" + "testing" +) + +func TestBound01MatchesTinyColorEdges(t *testing.T) { + tests := []struct { + input any + max float64 + want float64 + }{ + {-1, 255, 0}, + {300, 255, 1}, + {"1", 255, 1.0 / 255}, + {"1.0", 255, 1}, + {"100%", 255, 1}, + {"50%", 255, 0.5}, + {"255.0000001", 255, 1}, + } + for _, tt := range tests { + if got := Bound01(tt.input, tt.max); got != tt.want { + t.Errorf("Bound01(%#v, %v) = %v, want %v", tt.input, tt.max, got, tt.want) + } + } + if !math.IsNaN(Bound01("not a number", 255)) { + t.Fatal("invalid input should stay NaN until the parser rejects it") + } +} + +func TestBoundAlphaMatchesTinyColor(t *testing.T) { + tests := []struct { + input any + want float64 + }{ + {-1, 1}, {0, 0}, {0.5, 0.5}, {1, 1}, {100, 1}, {"asdf", 1}, + } + for _, tt := range tests { + if got := BoundAlpha(tt.input); got != tt.want { + t.Errorf("BoundAlpha(%#v) = %v, want %v", tt.input, got, tt.want) + } + } +} + +func TestOriginalInputAndInvalidState(t *testing.T) { + object := map[string]any{"r": float64(255)} + for _, tt := range []struct { + input any + want any + }{ + {nil, ""}, {"", ""}, {"Red", "Red"}, {false, ""}, {float64(0), ""}, {object, object}, + } { + if got := OriginalInput(tt.input); !reflect.DeepEqual(got, tt.want) { + t.Errorf("OriginalInput(%#v) = %#v, want %#v", tt.input, got, tt.want) + } + } + invalid := Invalid("bad") + if invalid.Valid || invalid.R != 0 || invalid.G != 0 || invalid.B != 0 || invalid.A != 1 || invalid.Format != FormatUnknown { + t.Fatalf("invalid state = %#v", invalid) + } +} diff --git a/go/internal/compat/protocol.go b/src/internal/compat/protocol.go similarity index 100% rename from go/internal/compat/protocol.go rename to src/internal/compat/protocol.go diff --git a/go/internal/compat/protocol_test.go b/src/internal/compat/protocol_test.go similarity index 100% rename from go/internal/compat/protocol_test.go rename to src/internal/compat/protocol_test.go diff --git a/src/internal/parser/hex.go b/src/internal/parser/hex.go new file mode 100644 index 00000000..32e4441c --- /dev/null +++ b/src/internal/parser/hex.go @@ -0,0 +1,41 @@ +package parser + +import ( + "strconv" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +func parseHex(text string, original any, format color.Format) (color.Model, bool) { + if len(text) > 0 && text[0] == '#' { + text = text[1:] + } + if len(text) != 3 && len(text) != 4 && len(text) != 6 && len(text) != 8 { + return color.Model{}, false + } + for _, rune := range text { + if !((rune >= '0' && rune <= '9') || (rune >= 'a' && rune <= 'f')) { + return color.Model{}, false + } + } + if len(text) == 3 || len(text) == 4 { + text = string([]byte{text[0], text[0], text[1], text[1], text[2], text[2]}) + func() string { + if len(text) == 4 { + return string([]byte{text[3], text[3]}) + } + return "" + }() + } + channel := func(offset int) float64 { + value, _ := strconv.ParseInt(text[offset:offset+2], 16, 64) + return float64(value) + } + model := color.Model{R: channel(0), G: channel(2), B: channel(4), A: 1, Valid: true, Format: format, Original: original} + if len(text) == 8 { + model.A = channel(6) / 255 + if format == color.FormatHex { + model.Format = color.FormatHex8 + } + } + return model, true +} diff --git a/src/internal/parser/hsl.go b/src/internal/parser/hsl.go new file mode 100644 index 00000000..dccc646d --- /dev/null +++ b/src/internal/parser/hsl.go @@ -0,0 +1,13 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseHSL(text string, original any) (color.Model, bool) { + if match := hslaMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hslMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} diff --git a/src/internal/parser/hsv.go b/src/internal/parser/hsv.go new file mode 100644 index 00000000..987f34eb --- /dev/null +++ b/src/internal/parser/hsv.go @@ -0,0 +1,13 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseHSV(text string, original any) (color.Model, bool) { + if match := hsvaMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hsvMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} diff --git a/src/internal/parser/names.go b/src/internal/parser/names.go new file mode 100644 index 00000000..48fc7ff3 --- /dev/null +++ b/src/internal/parser/names.go @@ -0,0 +1,43 @@ +package parser + +import ( + "fmt" + "strings" +) + +// names is copied from this checkout's mod.js names table. +var ( + names = func() map[string]string { + const data = "aliceblue:f0f8ff;antiquewhite:faebd7;aqua:0ff;aquamarine:7fffd4;azure:f0ffff;beige:f5f5dc;bisque:ffe4c4;black:000;blanchedalmond:ffebcd;blue:00f;blueviolet:8a2be2;brown:a52a2a;burlywood:deb887;burntsienna:ea7e5d;cadetblue:5f9ea0;chartreuse:7fff00;chocolate:d2691e;coral:ff7f50;cornflowerblue:6495ed;cornsilk:fff8dc;crimson:dc143c;cyan:0ff;darkblue:00008b;darkcyan:008b8b;darkgoldenrod:b8860b;darkgray:a9a9a9;darkgreen:006400;darkgrey:a9a9a9;darkkhaki:bdb76b;darkmagenta:8b008b;darkolivegreen:556b2f;darkorange:ff8c00;darkorchid:9932cc;darkred:8b0000;darksalmon:e9967a;darkseagreen:8fbc8f;darkslateblue:483d8b;darkslategray:2f4f4f;darkslategrey:2f4f4f;darkturquoise:00ced1;darkviolet:9400d3;deeppink:ff1493;deepskyblue:00bfff;dimgray:696969;dimgrey:696969;dodgerblue:1e90ff;firebrick:b22222;floralwhite:fffaf0;forestgreen:228b22;fuchsia:f0f;gainsboro:dcdcdc;ghostwhite:f8f8ff;gold:ffd700;goldenrod:daa520;gray:808080;green:008000;greenyellow:adff2f;grey:808080;honeydew:f0fff0;hotpink:ff69b4;indianred:cd5c5c;indigo:4b0082;ivory:fffff0;khaki:f0e68c;lavender:e6e6fa;lavenderblush:fff0f5;lawngreen:7cfc00;lemonchiffon:fffacd;lightblue:add8e6;lightcoral:f08080;lightcyan:e0ffff;lightgoldenrodyellow:fafad2;lightgray:d3d3d3;lightgreen:90ee90;lightgrey:d3d3d3;lightpink:ffb6c1;lightsalmon:ffa07a;lightseagreen:20b2aa;lightskyblue:87cefa;lightslategray:789;lightslategrey:789;lightsteelblue:b0c4de;lightyellow:ffffe0;lime:0f0;limegreen:32cd32;linen:faf0e6;magenta:f0f;maroon:800000;mediumaquamarine:66cdaa;mediumblue:0000cd;mediumorchid:ba55d3;mediumpurple:9370db;mediumseagreen:3cb371;mediumslateblue:7b68ee;mediumspringgreen:00fa9a;mediumturquoise:48d1cc;mediumvioletred:c71585;midnightblue:191970;mintcream:f5fffa;mistyrose:ffe4e1;moccasin:ffe4b5;navajowhite:ffdead;navy:000080;oldlace:fdf5e6;olive:808000;olivedrab:6b8e23;orange:ffa500;orangered:ff4500;orchid:da70d6;palegoldenrod:eee8aa;palegreen:98fb98;paleturquoise:afeeee;palevioletred:db7093;papayawhip:ffefd5;peachpuff:ffdab9;peru:cd853f;pink:ffc0cb;plum:dda0dd;powderblue:b0e0e6;purple:800080;rebeccapurple:663399;red:f00;rosybrown:bc8f8f;royalblue:4169e1;saddlebrown:8b4513;salmon:fa8072;sandybrown:f4a460;seagreen:2e8b57;seashell:fff5ee;sienna:a0522d;silver:c0c0c0;skyblue:87ceeb;slateblue:6a5acd;slategray:708090;slategrey:708090;snow:fffafa;springgreen:00ff7f;steelblue:4682b4;tan:d2b48c;teal:008080;thistle:d8bfd8;tomato:ff6347;turquoise:40e0d0;violet:ee82ee;wheat:f5deb3;white:fff;whitesmoke:f5f5f5;yellow:ff0;yellowgreen:9acd32" + result := make(map[string]string) + for _, entry := range strings.Split(data, ";") { + pair := strings.SplitN(entry, ":", 2) + result[pair[0]] = pair[1] + } + return result + }() + hexNames = func() map[string]string { + result := make(map[string]string) + for name, sourceHex := range names { + hex := sourceHex + if len(hex) == 3 { + hex = string([]byte{hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]}) + } + result[hex] = name + } + result["00ffff"] = "cyan" + result["ff00ff"] = "magenta" + result["808080"] = "grey" + result["a9a9a9"] = "darkgrey" + result["2f4f4f"] = "darkslategrey" + result["696969"] = "dimgrey" + result["d3d3d3"] = "lightgrey" + result["778899"] = "lightslategrey" + result["708090"] = "slategrey" + return result + }() +) + +func NameForRGB(r, g, b int) string { + return hexNames[fmt.Sprintf("%02x%02x%02x", r, g, b)] +} diff --git a/src/internal/parser/object.go b/src/internal/parser/object.go new file mode 100644 index 00000000..eac330cf --- /dev/null +++ b/src/internal/parser/object.go @@ -0,0 +1,22 @@ +package parser + +import "github.com/rajeet-04/tinycolor-go/internal/color" + +func parseObjectFull(object map[string]any, original any) color.Model { + if model := parseObject(object, original); model.Valid { + return model + } + h, hasH := object["h"] + s, hasS := object["s"] + if !hasH || !hasS || !validCSSUnit(h) || !validCSSUnit(s) { + return color.Invalid(original) + } + alpha, hasAlpha := object["a"] + if v, ok := object["v"]; ok && validCSSUnit(v) { + return hsvModel(h, s, v, alpha, hasAlpha, original) + } + if l, ok := object["l"]; ok && validCSSUnit(l) { + return hslModel(h, s, l, alpha, hasAlpha, original) + } + return color.Invalid(original) +} diff --git a/src/internal/parser/parser.go b/src/internal/parser/parser.go new file mode 100644 index 00000000..495cf5a1 --- /dev/null +++ b/src/internal/parser/parser.go @@ -0,0 +1,179 @@ +// Package parser converts JSON-safe compatibility inputs into normalized state. +package parser + +import ( + "math" + "regexp" + "strconv" + "strings" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +// Parse accepts the local TinyColor input forms implemented in this phase. +func Parse(input any) color.Model { + original := color.OriginalInput(input) + switch value := original.(type) { + case string: + return parseString(value, original) + case map[string]any: + return parseObjectFull(value, original) + default: + return color.Invalid(original) + } +} + +// ParseFromRatio applies TinyColor.fromRatio's object transformation before +// parsing; the transformed object is also TinyColor's original input. +func ParseFromRatio(input any) color.Model { + object, ok := input.(map[string]any) + if !ok { + return Parse(input) + } + transformed := make(map[string]any, len(object)) + for key, value := range object { + if key == "a" { + transformed[key] = value + continue + } + if number := color.ParseFloat(value); !math.IsNaN(number) && number <= 1 { + transformed[key] = formatPercent(number*100) + "%" + } else { + transformed[key] = value + } + } + return parseObjectFull(transformed, transformed) +} + +func formatPercent(number float64) string { + return strconv.FormatFloat(number, 'f', -1, 64) +} + +var ( + hslMatcher = regexp.MustCompile(`(?i)hsl[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hsvMatcher = regexp.MustCompile(`(?i)hsv[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hslaMatcher = regexp.MustCompile(`(?i)hsla[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + hsvaMatcher = regexp.MustCompile(`(?i)hsva[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) +) + +func parseString(value string, original any) color.Model { + text := strings.ToLower(strings.TrimSpace(value)) + if text == "transparent" { + return color.Model{A: 0, Valid: true, Format: color.FormatName, Original: original} + } + if hex, named := names[text]; named { + model, _ := parseHex(hex, original, color.FormatName) + return model + } + if model, ok := parseRGBString(text, original); ok { + return model + } + if model, ok := parseHex(text, original, color.FormatHex); ok { + return model + } + // Phase 1 smoke must remain executable while Plan 02-02 moves these paths + // into dedicated files. This is generic grammar, not a fixed input switch. + if model, ok := parseHSL(text, original); ok { + return model + } + if model, ok := parseHSV(text, original); ok { + return model + } + return color.Invalid(original) +} + +func parseObject(object map[string]any, original any) color.Model { + r, hasR := object["r"] + g, hasG := object["g"] + b, hasB := object["b"] + if !hasR || !hasG || !hasB || !validCSSUnit(r) || !validCSSUnit(g) || !validCSSUnit(b) { + return color.Invalid(original) + } + alpha, hasAlpha := object["a"] + format := formatForRGB(r) + if explicit, ok := object["format"].(string); ok && explicit != "" { + format = color.Format(explicit) + } + return rgbModel(r, g, b, alpha, hasAlpha, original, format) +} + +func parseHSLHSVSmoke(text string, original any) (color.Model, bool) { + if match := hslaMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hslMatcher.FindStringSubmatch(text); match != nil { + return hslModel(match[1], match[2], match[3], nil, false, original), true + } + if match := hsvaMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], match[4], true, original), true + } + if match := hsvMatcher.FindStringSubmatch(text); match != nil { + return hsvModel(match[1], match[2], match[3], nil, false, original), true + } + return color.Model{}, false +} + +func hslModel(h, s, l, alpha any, hasAlpha bool, original any) color.Model { + hue := color.Bound01(h, 360) + saturation := ratio(s) + lightness := ratio(l) + var q float64 + if lightness < 0.5 { + q = lightness * (1 + saturation) + } else { + q = lightness + saturation - lightness*saturation + } + p := 2*lightness - q + channel := func(t float64) float64 { + if t < 0 { + t++ + } + if t > 1 { + t-- + } + switch { + case t*6 < 1: + return p + (q-p)*6*t + case t*2 < 1: + return q + case t*3 < 2: + return p + (q-p)*(2.0/3.0-t)*6 + default: + return p + } + } + if saturation == 0 { + p = lightness + q = lightness + } + model := color.Model{R: channel(hue+1.0/3.0) * 255, G: channel(hue) * 255, B: channel(hue-1.0/3.0) * 255, A: color.BoundAlpha(alpha), Valid: true, Format: color.FormatHSL, Original: original} + if !hasAlpha { + model.A = 1 + } + return model +} + +func hsvModel(h, s, v, alpha any, hasAlpha bool, original any) color.Model { + hue := color.Bound01(h, 360) * 6 + saturation := ratio(s) + value := ratio(v) + i := int(math.Floor(hue)) + f := hue - math.Floor(hue) + p := value * (1 - saturation) + q := value * (1 - f*saturation) + t := value * (1 - (1-f)*saturation) + channels := [][3]float64{{value, t, p}, {q, value, p}, {p, value, t}, {p, q, value}, {t, p, value}, {value, p, q}} + rgb := channels[i%6] + model := color.Model{R: rgb[0] * 255, G: rgb[1] * 255, B: rgb[2] * 255, A: color.BoundAlpha(alpha), Valid: true, Format: color.FormatHSV, Original: original} + if !hasAlpha { + model.A = 1 + } + return model +} + +func ratio(value any) float64 { + if number := color.ParseFloat(value); !math.IsNaN(number) && number <= 1 { + value = formatPercent(number*100) + "%" + } + return color.Bound01(value, 100) +} diff --git a/src/internal/parser/parser_test.go b/src/internal/parser/parser_test.go new file mode 100644 index 00000000..d4254bde --- /dev/null +++ b/src/internal/parser/parser_test.go @@ -0,0 +1,75 @@ +package parser + +import ( + "reflect" + "testing" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +func TestHexRGBNamesAndObjects(t *testing.T) { + tests := []struct { + input any + valid bool + format color.Format + r, g, b float64 + alpha float64 + }{ + {"#f00", true, color.FormatHex, 255, 0, 0, 1}, + {"ff000080", true, color.FormatHex8, 255, 0, 0, 128.0 / 255}, + {" InDiAnReD ", true, color.FormatName, 205, 92, 92, 1}, + {"transparent", true, color.FormatName, 0, 0, 0, 0}, + {"rgb (100%, 0%, 0%)", true, color.FormatPRGB, 255, 0, 0, 1}, + {"rgba 255 0 0 .5", true, color.FormatRGB, 255, 0, 0, .5}, + {map[string]any{"r": "90%", "g": "45%", "b": "0%", "a": .4}, true, color.FormatPRGB, 229.5, 114.75, 0, .4}, + {"##123456", false, color.FormatUnknown, 0, 0, 0, 1}, + {map[string]any{"r": "invalid", "g": "invalid", "b": "invalid"}, false, color.FormatUnknown, 0, 0, 0, 1}, + } + for _, tt := range tests { + model := Parse(tt.input) + if model.Valid != tt.valid || model.Format != tt.format || model.R != tt.r || model.G != tt.g || model.B != tt.b || model.A != tt.alpha { + t.Errorf("Parse(%#v) = %#v", tt.input, model) + } + } +} + +func TestAllSourceNamesParse(t *testing.T) { + for name := range names { + model := Parse(name) + if !model.Valid || model.Format != color.FormatName { + t.Errorf("%s = %#v", name, model) + } + } +} + +func TestOriginalAndFromRatio(t *testing.T) { + object := map[string]any{"r": float64(1), "g": float64(0), "b": float64(0), "a": float64(.5)} + ratio := ParseFromRatio(object) + if !ratio.Valid || ratio.Format != color.FormatPRGB || ratio.A != .5 { + t.Fatalf("ratio = %#v", ratio) + } + want := map[string]any{"r": "100%", "g": "0%", "b": "0%", "a": float64(.5)} + if !reflect.DeepEqual(ratio.Original, want) { + t.Fatalf("ratio original = %#v, want %#v", ratio.Original, want) + } + if original := Parse(nil).Original; original != "" { + t.Fatalf("null original = %#v", original) + } +} + +func TestHSLHSVObjectsAndWrappedHue(t *testing.T) { + for _, input := range []any{ + "hsl(251, 100%, 38%)", + "hsva 251.1 .887 .918 .5", + map[string]any{"h": float64(251), "s": float64(100), "l": float64(.38)}, + map[string]any{"h": float64(720), "s": float64(100), "v": float64(100)}, + } { + model := Parse(input) + if !model.Valid || (model.Format != color.FormatHSL && model.Format != color.FormatHSV) { + t.Errorf("Parse(%#v) = %#v", input, model) + } + } + if model := Parse(map[string]any{"h": "invalid", "s": "invalid", "v": "invalid"}); model.Valid { + t.Fatalf("invalid HSV object = %#v", model) + } +} diff --git a/src/internal/parser/rgb.go b/src/internal/parser/rgb.go new file mode 100644 index 00000000..eaf6b8db --- /dev/null +++ b/src/internal/parser/rgb.go @@ -0,0 +1,56 @@ +package parser + +import ( + "regexp" + + "github.com/rajeet-04/tinycolor-go/internal/color" +) + +const cssUnit = `(?:[-+]?\d*\.\d+%?)|(?:[-+]?\d+%?)` + +var ( + rgbMatcher = regexp.MustCompile(`(?i)rgb[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + rgbaMatcher = regexp.MustCompile(`(?i)rgba[\s|(]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)[,|\s]+(` + cssUnit + `)\s*\)?`) + cssMatcher = regexp.MustCompile(cssUnit) +) + +func validCSSUnit(value any) bool { + switch value := value.(type) { + case string: + return cssMatcher.FindStringIndex(value) != nil + case float64: + return true + default: + return false + } +} + +func rgbModel(r, g, b, alpha any, hasAlpha bool, original any, format color.Format) color.Model { + model := color.Model{ + R: color.Bound01(r, 255) * 255, + G: color.Bound01(g, 255) * 255, + B: color.Bound01(b, 255) * 255, + A: color.BoundAlpha(alpha), Valid: true, Format: format, Original: original, + } + if !hasAlpha { + model.A = 1 + } + return model +} + +func parseRGBString(text string, original any) (color.Model, bool) { + if match := rgbaMatcher.FindStringSubmatch(text); match != nil { + return rgbModel(match[1], match[2], match[3], match[4], true, original, formatForRGB(match[1])), true + } + if match := rgbMatcher.FindStringSubmatch(text); match != nil { + return rgbModel(match[1], match[2], match[3], nil, false, original, formatForRGB(match[1])), true + } + return color.Model{}, false +} + +func formatForRGB(red any) color.Format { + if color.IsPercentage(red) { + return color.FormatPRGB + } + return color.FormatRGB +} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go new file mode 100644 index 00000000..a45f8b0f --- /dev/null +++ b/src/tinycolor/color.go @@ -0,0 +1,128 @@ +// Package tinycolor exposes the compatibility facade over normalized parsing. +package tinycolor + +import ( + "fmt" + "math" + "strconv" + + "github.com/rajeet-04/tinycolor-go/internal/color" + "github.com/rajeet-04/tinycolor-go/internal/parser" +) + +type Color struct{ model color.Model } + +func FromCompat(input any, fromRatio bool) (Color, error) { + if fromRatio { + return Color{model: parser.ParseFromRatio(input)}, nil + } + return Color{model: parser.Parse(input)}, nil +} + +func (c Color) Valid() bool { return c.model.Valid } +func (c Color) Format() string { return string(c.model.Format) } +func (c Color) Alpha() float64 { return c.model.A } +func (c Color) Original() any { return c.model.Original } +func (c Color) RGB() map[string]any { + return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} +} + +// String supplies the minimal source-compatible string snapshot used by the +// JSONL oracle. Full public output APIs remain Phase 3 work. +func (c Color) String() string { + r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) + if c.model.Format == color.FormatName { + if c.model.A == 0 { + return "transparent" + } + if name := parser.NameForRGB(r, g, b); name != "" { + return name + } + } + if c.model.Format == color.FormatPRGB { + if c.model.A < 1 { + return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", percent(r), percent(g), percent(b), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", percent(r), percent(g), percent(b)) + } + if c.model.Format == color.FormatHSL { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsla(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsl(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100))) + } + if c.model.Format == color.FormatHSV { + h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsva(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsv(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100))) + } + if c.model.Format == color.FormatRGB || c.model.A < 1 { + if c.model.A == 1 { + return fmt.Sprintf("rgb(%d, %d, %d)", r, g, b) + } + return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) + } + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} + +func (c Color) Inspect() map[string]any { + format := any(c.Format()) + if c.Format() == "" { + format = false + } + return map[string]any{"valid": c.Valid(), "format": format, "alpha": c.Alpha(), "rgb": c.RGB(), "value": c.String(), "original": c.Original()} +} + +func percent(channel int) int { return int(math.Round(float64(channel) / 255 * 100)) } +func roundedAlpha(alpha float64) string { + return strconv.FormatFloat(math.Round(alpha*100)/100, 'f', -1, 64) +} + +func rgbToHSL(r, g, b float64) (float64, float64, float64) { + r, g, b = r/255, g/255, b/255 + max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) + l := (max + min) / 2 + if max == min { + return 0, 0, l + } + d := max - min + s := d / (2 - max - min) + if l < 0.5 { + s = d / (max + min) + } + var h float64 + switch max { + case r: + h = (g-b)/d + map[bool]float64{true: 6, false: 0}[g < b] + case g: + h = (b-r)/d + 2 + default: + h = (r-g)/d + 4 + } + return h / 6, s, l +} + +func rgbToHSV(r, g, b float64) (float64, float64, float64) { + r, g, b = r/255, g/255, b/255 + max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) + d := max - min + if max == 0 { + return 0, 0, 0 + } + if d == 0 { + return 0, 0, max + } + var h float64 + switch max { + case r: + h = (g-b)/d + map[bool]float64{true: 6, false: 0}[g < b] + case g: + h = (b-r)/d + 2 + default: + h = (r-g)/d + 4 + } + return h / 6, d / max, max +} diff --git a/go/tinycolor/color_test.go b/src/tinycolor/color_test.go similarity index 86% rename from go/tinycolor/color_test.go rename to src/tinycolor/color_test.go index 7819420d..ef84b69c 100644 --- a/go/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -36,8 +36,9 @@ func TestPhaseOneInputsMatchTinyColor(t *testing.T) { } } -func TestPhaseOneRejectsUnsupportedInput(t *testing.T) { - if _, err := FromCompat("blue", false); err == nil { - t.Fatal("expected unsupported input error") +func TestInvalidInputIsAColorState(t *testing.T) { + color, err := FromCompat("this is not a color", false) + if err != nil || color.Valid() || color.String() != "#000000" { + t.Fatalf("invalid input = %#v, %v", color, err) } } diff --git a/tests/original/README.md b/tests/original/README.md new file mode 100644 index 00000000..4465405d --- /dev/null +++ b/tests/original/README.md @@ -0,0 +1,5 @@ +# Pinned JavaScript oracle + +The canonical, unmodified upstream files remain at the repository root so the +Node adapter can import them directly. `manifest.sha256` records their kickoff +hashes from commit `b49018c9f2dbca313d80d7a4dad25e26143cfe01`. diff --git a/tests/original/manifest.sha256 b/tests/original/manifest.sha256 new file mode 100644 index 00000000..0297cdc9 --- /dev/null +++ b/tests/original/manifest.sha256 @@ -0,0 +1,3 @@ +a4c3efa67a123efbb9e8082a6c1296459cd9151c0cdd00ad699b1a43fe02fb77 mod.js +cdddded304f3f2910d43e12538246e79931b7e1638a186a632920b307c67dfb3 test.js +239e168433c66d15388d18034944203380c93171f44b029ce33456116c855314 tinycolor.js diff --git a/tests/port/README.md b/tests/port/README.md new file mode 100644 index 00000000..eb54381d --- /dev/null +++ b/tests/port/README.md @@ -0,0 +1,4 @@ +# Port-owned tests + +`adapter.test.mjs` checks the JSON-lines contract used for JavaScript-versus-Go +comparisons. Go unit tests are kept beside the code in `src/`. diff --git a/compat/js-runner.test.mjs b/tests/port/adapter.test.mjs similarity index 97% rename from compat/js-runner.test.mjs rename to tests/port/adapter.test.mjs index 39a1291a..db49297b 100644 --- a/compat/js-runner.test.mjs +++ b/tests/port/adapter.test.mjs @@ -19,6 +19,7 @@ assert.deepEqual(success, { alpha: 1, rgb: { r: 255, g: 0, b: 0, a: 1 }, value: "red", + original: "red", }, }); From 85f935013a0d0cb1cce04aecf96d8e8125242d84 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 02:32:12 +0530 Subject: [PATCH 088/165] feat: add conversion facade Phase 3 typed conversion, formatting, and analysis behavior with direct unit coverage. --- src/tinycolor/color.go | 34 +++++++++++++++++++++++++++++++++- src/tinycolor/color_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index a45f8b0f..0544fd4d 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -5,12 +5,17 @@ import ( "fmt" "math" "strconv" + "math/rand/v2" "github.com/rajeet-04/tinycolor-go/internal/color" "github.com/rajeet-04/tinycolor-go/internal/parser" ) -type Color struct{ model color.Model } +type Color struct{ model color.Model; gradientType bool } +type CompatOptions struct { Format string; GradientType bool } +type RGB struct { R, G, B int; A float64 } +type HSL struct { H, S, L, A float64 } +type HSV struct { H, S, V, A float64 } func FromCompat(input any, fromRatio bool) (Color, error) { if fromRatio { @@ -18,6 +23,7 @@ func FromCompat(input any, fromRatio bool) (Color, error) { } return Color{model: parser.Parse(input)}, nil } +func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) { c, e := FromCompat(input, fromRatio); if options.Format != "" { c.model.Format = color.Format(options.Format) }; c.gradientType=options.GradientType; return c, e } func (c Color) Valid() bool { return c.model.Valid } func (c Color) Format() string { return string(c.model.Format) } @@ -26,10 +32,35 @@ func (c Color) Original() any { return c.model.Original } func (c Color) RGB() map[string]any { return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} } +func (c Color) ToRGB() RGB { return RGB{int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)), c.model.A} } +func (c Color) ToHSL() HSL { h,s,l:=rgbToHSL(c.model.R,c.model.G,c.model.B); return HSL{h*360,s,l,c.model.A} } +func (c Color) ToHSV() HSV { h,s,v:=rgbToHSV(c.model.R,c.model.G,c.model.B); return HSV{h*360,s,v,c.model.A} } +func (c Color) ToRGBString() string { x:=c.ToRGB(); if x.A==1{return fmt.Sprintf("rgb(%d, %d, %d)",x.R,x.G,x.B)}; return fmt.Sprintf("rgba(%d, %d, %d, %s)",x.R,x.G,x.B,roundedAlpha(x.A)) } +func (c Color) ToPercentageRGB() RGB { x:=c.ToRGB(); return RGB{percent(x.R),percent(x.G),percent(x.B),x.A} } +func (c Color) ToPercentageRGBString() string { x:=c.ToPercentageRGB(); if x.A==1{return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)",x.R,x.G,x.B)};return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)",x.R,x.G,x.B,roundedAlpha(x.A)) } +func (c Color) ToHSLString() string {x:=c.ToHSL(); p:="hsl";if x.A<1{p="hsla"}; s:=fmt.Sprintf("%s(%d, %d%%, %d%%",p,mathRound(x.H),mathRound(x.S*100),mathRound(x.L*100));if x.A<1{s+=", "+roundedAlpha(x.A)};return s+")"} +func (c Color) ToHSVString() string {x:=c.ToHSV(); p:="hsv";if x.A<1{p="hsva"}; s:=fmt.Sprintf("%s(%d, %d%%, %d%%",p,mathRound(x.H),mathRound(x.S*100),mathRound(x.V*100));if x.A<1{s+=", "+roundedAlpha(x.A)};return s+")"} +func (c Color) ToHex() string { return fmt.Sprintf("%02x%02x%02x", mathRound(c.model.R),mathRound(c.model.G),mathRound(c.model.B)) } +func (c Color) ToHexString() string{return "#"+c.ToHex()} +func (c Color) ToHex8() string{return c.ToHex()+fmt.Sprintf("%02x",mathRound(c.model.A*255))} +func (c Color) ToHex8String() string{return "#"+c.ToHex8()} +func (c Color) ToString(format string) string { return c.toString(format, format != "") } +func (c Color) toString(format string, explicit bool) string { if format==""{format=string(c.model.Format)}; if !explicit&&c.model.A<1&&(format=="hex"||format=="hex6"||format=="hex3"||format=="hex4"||format=="hex8"||format=="name"){if format=="name"&&c.model.A==0{return "transparent"};return c.ToRGBString()}; switch format {case "rgb": return c.ToRGBString();case "prgb":return c.ToPercentageRGBString();case "hsl":return c.ToHSLString();case "hsv":return c.ToHSVString();case "hex","hex6":return c.ToHexString();case "hex8":return c.ToHex8String();case "name":if n,ok:=c.ToName();ok{return n};return c.ToHexString()}; return c.ToHexString() } +func (c Color) ToName()(string,bool){if c.model.A==0{return "transparent",true}; if c.model.A<1{return "",false}; n:=parser.NameForRGB(mathRound(c.model.R),mathRound(c.model.G),mathRound(c.model.B)); return n,n!=""} +func (c Color) ToFilter(second *Color, gradient bool) string { start:="#"+fmt.Sprintf("%02x",mathRound(c.model.A*255))+c.ToHex(); end:=start; if second!=nil {end="#"+fmt.Sprintf("%02x",mathRound(second.model.A*255))+second.ToHex()}; prefix:=""; if gradient||c.gradientType {prefix="GradientType = 1, "}; return "progid:DXImageTransform.Microsoft.gradient("+prefix+"startColorstr="+start+",endColorstr="+end+")" } +func (c Color) Brightness() float64{x:=c.ToRGB();return float64(x.R*299+x.G*587+x.B*114)/1000} +func (c Color) Luminance() float64 { x:=c.ToRGB(); f:=func(v int)float64{z:=float64(v)/255;if z<=.03928{return z/12.92};return math.Pow((z+.055)/1.055,2.4)};return .2126*f(x.R)+.7152*f(x.G)+.0722*f(x.B) } +func (c Color) IsDark()bool{return c.Brightness()<128}; func(c Color)IsLight()bool{return !c.IsDark()} +func (c Color) Clone() Color { n,_:=FromCompat(c.String(),false);return n } +func Equals(a,b any)bool{if a==nil||b==nil{return false};x,_:=FromCompat(a,false);y,_:=FromCompat(b,false);return x.ToRGBString()==y.ToRGBString()} +func Random() Color { return Color{model: color.Model{R:rand.Float64()*255,G:rand.Float64()*255,B:rand.Float64()*255,A:1,Valid:true,Format:color.FormatRGB}} } +func mathRound(v float64)int{return int(math.Floor(v+.5))} // String supplies the minimal source-compatible string snapshot used by the // JSONL oracle. Full public output APIs remain Phase 3 work. func (c Color) String() string { + return c.toString("", false) + /* r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) if c.model.Format == color.FormatName { if c.model.A == 0 { @@ -66,6 +97,7 @@ func (c Color) String() string { return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) } return fmt.Sprintf("#%02x%02x%02x", r, g, b) + */ } func (c Color) Inspect() map[string]any { diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index ef84b69c..8ce49118 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -2,6 +2,32 @@ package tinycolor import "testing" +func TestOutputsAndAnalysis(t *testing.T) { + c,_:=FromCompat("rgba(255, 0, 0, .5)",false) + if c.ToHex8String()!="#ff000080" || c.ToRGBString()!="rgba(255, 0, 0, 0.5)" { t.Fatal(c.ToHex8String(),c.ToRGBString()) } + if c.ToFilter(nil,false)!="progid:DXImageTransform.Microsoft.gradient(startColorstr=#80ff0000,endColorstr=#80ff0000)" {t.Fatal(c.ToFilter(nil,false))} + black,_:=FromCompat("#000",false); white,_:=FromCompat("#fff",false) + if black.Brightness()!=0||white.Luminance()!=1||!black.IsDark()||!white.IsLight(){t.Fatal("analysis")} + if !Equals("#ff000066","rgba(255, 0, 0, .4)"){t.Fatal("equals")} + if !Random().Valid(){t.Fatal("random")} +} + +func TestStringHexFilterAndConversion(t *testing.T) { + c,_:=FromCompat("rgba(255, 0, 0, .5)",false) + if c.ToPercentageRGBString()!="rgba(100%, 0%, 0%, 0.5)"||c.ToHSLString()!="hsla(0, 100%, 50%, 0.5)"||c.ToHSVString()!="hsva(0, 100%, 100%, 0.5)"{t.Fatal("strings")} + if c.ToString("hex8")!="#ff000080"||c.ToString("name")!="#ff0000"{t.Fatal(c.ToString("hex8"),c.ToString("name"))} + with,_:=FromCompatWithOptions("red",false,CompatOptions{GradientType:true}) + if with.ToFilter(nil,false)!="progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#ffff0000,endColorstr=#ffff0000)"{t.Fatal(with.ToFilter(nil,false))} +} + +func TestImplicitAlphaHexFallsBackToRGBA(t *testing.T) { + for _, input := range []string{"#f008", "#ff000080"} { + c, _ := FromCompat(input, false) + if c.String() != c.ToRGBString() { t.Fatalf("%s: %s", input, c.String()) } + if c.ToString("hex8") != c.ToHex8String() { t.Fatal("explicit hex8") } + } +} + func TestPhaseOneInputsMatchTinyColor(t *testing.T) { tests := []struct { name string From ba3d348b3029054430b09bb9d71633041b9728f7 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 02:32:36 +0530 Subject: [PATCH 089/165] fix: preserve false compatibility results Encode successful false JSONL results as an explicit result field. --- src/internal/compat/protocol.go | 27 ++++++++++++++++++++++----- src/internal/compat/protocol_test.go | 15 +++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/internal/compat/protocol.go b/src/internal/compat/protocol.go index 52ff4fc7..c2a8d966 100644 --- a/src/internal/compat/protocol.go +++ b/src/internal/compat/protocol.go @@ -13,13 +13,14 @@ type Request struct { } type Response struct { - ID string `json:"id"` - Result any `json:"result,omitempty"` - Error string `json:"error,omitempty"` + ID string `json:"id"` + Result any `json:"-"` + Error string `json:"-"` + hasResult bool } func Success(id string, result any) (Response, error) { - response := Response{ID: id, Result: result} + response := Response{ID: id, Result: result, hasResult: true} return response, response.Validate() } @@ -29,12 +30,28 @@ func Failure(id, message string) (Response, error) { } func (r Response) Validate() error { - if (r.Result == nil) == (r.Error == "") { + if r.hasResult == (r.Error != "") { return fmt.Errorf("response must contain exactly one of result or error") } return nil } +func (r Response) MarshalJSON() ([]byte, error) { + if err := r.Validate(); err != nil { + return nil, err + } + if r.hasResult { + return json.Marshal(struct { + ID string `json:"id"` + Result any `json:"result"` + }{r.ID, r.Result}) + } + return json.Marshal(struct { + ID string `json:"id"` + Error string `json:"error"` + }{r.ID, r.Error}) +} + func Decode(line []byte) (Request, error) { var request Request if err := json.Unmarshal(line, &request); err != nil { diff --git a/src/internal/compat/protocol_test.go b/src/internal/compat/protocol_test.go index 336d1921..bf1cf3e8 100644 --- a/src/internal/compat/protocol_test.go +++ b/src/internal/compat/protocol_test.go @@ -1,6 +1,9 @@ package compat -import "testing" +import ( + "strings" + "testing" +) func TestResponseHasExactlyOnePayload(t *testing.T) { if _, err := Success("one", map[string]any{"ok": true}); err != nil { @@ -12,7 +15,15 @@ func TestResponseHasExactlyOnePayload(t *testing.T) { if err := (Response{ID: "one"}).Validate(); err == nil { t.Fatal("expected neither payload error") } - if err := (Response{ID: "one", Result: map[string]any{}, Error: "bad"}).Validate(); err == nil { + if err := (Response{ID: "one", Error: "bad", hasResult: true}).Validate(); err == nil { t.Fatal("expected both payloads error") } } + +func TestFalseResultIsPresent(t *testing.T) { + response, _ := Success("no", false) + encoded, err := Encode(response) + if err != nil || !strings.Contains(string(encoded), `"result":false`) { + t.Fatalf("%s %v", encoded, err) + } +} From 28a95683512692c669f175f0be2a1ff49a8c309f Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 02:33:38 +0530 Subject: [PATCH 090/165] feat: add conversion compatibility dispatch Expose Phase 3 output, analysis, equality, clone, and invariant operations through both JSONL runners. --- compat/js-runner.mjs | 53 +++++++++++++++ src/cmd/tinycolor-compat/main.go | 113 ++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index 648dc195..81189767 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -18,6 +18,9 @@ const respond = (id, result, error) => { }; const handle = (request) => { + const args = request.args ?? {}; + const options = args.options ?? {}; + const color = () => tinycolor(request.input, options); switch (request.operation) { case "inspect": return respond(request.id, inspect(tinycolor(request.input))); @@ -25,11 +28,61 @@ const handle = (request) => { return respond(request.id, tinycolor(request.input).toString(request.args?.format)); case "fromRatio": return respond(request.id, inspect(tinycolor.fromRatio(request.input, request.args))); + case "output": + return output(request.id, color(), args); + case "analysis": + return analysis(request.id, color(), args.method); + case "equals": + return respond(request.id, tinycolor.equals(request.input, args.other)); + case "clone": + return respond(request.id, inspect(color().clone())); + case "randomInvariant": + return randomInvariant(request.id); default: return respond(request.id, undefined, "unsupported operation"); } }; +const output = (id, color, args) => { + let result; + switch (args.method) { + case "toHex": result = color.toHex(); break; + case "toHex8": result = color.toHex8(); break; + case "toHexString": result = color.toHexString(); break; + case "toHex8String": result = color.toHex8String(); break; + case "toRgbString": result = color.toRgbString(); break; + case "toPercentageRgbString": result = color.toPercentageRgbString(); break; + case "toHslString": result = color.toHslString(); break; + case "toHsvString": result = color.toHsvString(); break; + case "toString": result = color.toString(args.format); break; + case "toName": result = color.toName(); break; + case "toFilter": result = color.toFilter(args.secondColor || undefined); break; + default: return respond(id, undefined, "unsupported method"); + } + return respond(id, result); +}; + +const analysis = (id, color, method) => { + const methods = { + brightness: () => color.getBrightness(), + luminance: () => color.getLuminance(), + isDark: () => color.isDark(), + isLight: () => color.isLight(), + }; + return methods[method] + ? respond(id, methods[method]()) + : respond(id, undefined, "unsupported method"); +}; + +const randomInvariant = (id) => { + const rgb = tinycolor.random().toRgb(); + return respond(id, { + valid: true, + alpha: 1, + rgbInRange: rgb.r >= 0 && rgb.r <= 255 && rgb.g >= 0 && rgb.g <= 255 && rgb.b >= 0 && rgb.b <= 255, + }); +}; + const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); for await (const line of lines) { if (!line) continue; diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index e8f4835b..57ecd0f3 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -18,7 +18,8 @@ func main() { } request, err := compat.Decode(line) if err != nil { - write(compat.Response{ID: "", Error: err.Error()}) + response, _ := compat.Failure("", err.Error()) + write(response) continue } write(handle(request)) @@ -30,11 +31,15 @@ func handle(request compat.Request) compat.Response { color tinycolor.Color err error ) + args, _ := request.Args.(map[string]any) switch request.Operation { case "inspect", "string": color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": color, err = tinycolor.FromCompat(request.Input, true) + case "output", "analysis", "clone": + color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) + case "equals", "randomInvariant": default: response, _ := compat.Failure(request.ID, "unsupported operation") return response @@ -47,10 +52,116 @@ func handle(request compat.Request) compat.Response { response, _ := compat.Success(request.ID, color.String()) return response } + switch request.Operation { + case "output": + return output(request.ID, color, args) + case "analysis": + return analysis(request.ID, color, args["method"]) + case "clone": + response, _ := compat.Success(request.ID, color.Clone().Inspect()) + return response + case "equals": + response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) + return response + case "randomInvariant": + randomColor := tinycolor.Random() + rgb := randomColor.ToRGB() + response, _ := compat.Success(request.ID, map[string]any{ + "valid": randomColor.Valid(), + "alpha": float64(1), + "rgbInRange": rgb.R >= 0 && rgb.R <= 255 && rgb.G >= 0 && rgb.G <= 255 && rgb.B >= 0 && rgb.B <= 255, + }) + return response + } response, _ := compat.Success(request.ID, color.Inspect()) return response } +func options(args map[string]any) tinycolor.CompatOptions { + options, _ := args["options"].(map[string]any) + format, _ := options["format"].(string) + gradientType, _ := options["gradientType"].(bool) + return tinycolor.CompatOptions{Format: format, GradientType: gradientType} +} + +func analysis(id string, color tinycolor.Color, method any) compat.Response { + var result any + switch method { + case "brightness": + result = color.Brightness() + case "luminance": + result = color.Luminance() + case "isDark": + result = color.IsDark() + case "isLight": + result = color.IsLight() + default: + response, _ := compat.Failure(id, "unsupported method") + return response + } + response, _ := compat.Success(id, result) + return response +} + +func output(id string, color tinycolor.Color, args map[string]any) compat.Response { + var result any + switch args["method"] { + case "toHex": + result = color.ToHex() + case "toHex8": + result = color.ToHex8() + case "toHexString": + result = color.ToHexString() + case "toHex8String": + result = color.ToHex8String() + case "toRgbString": + result = color.ToRGBString() + case "toPercentageRgbString": + result = color.ToPercentageRGBString() + case "toHslString": + result = color.ToHSLString() + case "toHsvString": + result = color.ToHSVString() + case "toString": + format, _ := args["format"].(string) + result = color.ToString(format) + case "toName": + name, ok := color.ToName() + if ok { + result = name + } else { + result = false + } + case "toFilter": + var second *tinycolor.Color + if truthy(args["secondColor"]) { + parsed, _ := tinycolor.FromCompat(args["secondColor"], false) + second = &parsed + } + result = color.ToFilter(second, false) + default: + response, _ := compat.Failure(id, "unsupported method") + return response + } + response, _ := compat.Success(id, result) + return response +} + +func truthy(value any) bool { + switch value := value.(type) { + case nil: + return false + case bool: + return value + case float64: + return value != 0 + case string: + return value != "" + default: + return true + } +} + func write(response compat.Response) { encoded, err := compat.Encode(response) if err != nil { From 09d1b54b034c5a622b12f0893e4561135b9b2a97 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 02:34:00 +0530 Subject: [PATCH 091/165] test: add conversion parity corpus Add 35 fixed Node-to-Go conversion, formatting, analysis, equality, clone, and random-invariant cases. --- compat/cases/conversion.jsonl | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 compat/cases/conversion.jsonl diff --git a/compat/cases/conversion.jsonl b/compat/cases/conversion.jsonl new file mode 100644 index 00000000..debdf612 --- /dev/null +++ b/compat/cases/conversion.jsonl @@ -0,0 +1,35 @@ +{"id":"hex","operation":"output","input":"red","args":{"method":"toHex"}} +{"id":"hex8","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toHex8"}} +{"id":"hexstr","operation":"output","input":"red","args":{"method":"toHexString"}} +{"id":"hex8str","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toHex8String"}} +{"id":"rgb","operation":"output","input":"red","args":{"method":"toRgbString"}} +{"id":"name","operation":"output","input":"red","args":{"method":"toName"}} +{"id":"namefalse","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toName"}} +{"id":"string-explicit-hex","operation":"output","input":"red","args":{"method":"toString","format":"hex"}} +{"id":"brightness","operation":"analysis","input":"#000","args":{"method":"brightness"}} +{"id":"luminance","operation":"analysis","input":"#fff","args":{"method":"luminance"}} +{"id":"dark","operation":"analysis","input":"#000","args":{"method":"isDark"}} +{"id":"light","operation":"analysis","input":"#fff","args":{"method":"isLight"}} +{"id":"equal","operation":"equals","input":"#ff000066","args":{"other":"rgba(255,0,0,.4)"}} +{"id":"clone","operation":"clone","input":"red"} +{"id":"random","operation":"randomInvariant"} +{"id":"prgb","operation":"output","input":"red","args":{"method":"toPercentageRgbString"}} +{"id":"prgba","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toPercentageRgbString"}} +{"id":"hsl","operation":"output","input":"red","args":{"method":"toHslString"}} +{"id":"hsla","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toHslString"}} +{"id":"hsv","operation":"output","input":"red","args":{"method":"toHsvString"}} +{"id":"hsva","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toHsvString"}} +{"id":"transparent-name","operation":"output","input":"transparent","args":{"method":"toName"}} +{"id":"string-omitted-rgb","operation":"output","input":"rgba(255,0,0,.5)","args":{"method":"toString"}} +{"id":"string-empty","operation":"output","input":"red","args":{"method":"toString","format":""}} +{"id":"string-prgb","operation":"output","input":"red","args":{"method":"toString","format":"prgb"}} +{"id":"string-hsl","operation":"output","input":"red","args":{"method":"toString","format":"hsl"}} +{"id":"string-hsv","operation":"output","input":"red","args":{"method":"toString","format":"hsv"}} +{"id":"string-unknown","operation":"output","input":"red","args":{"method":"toString","format":"wat"}} +{"id":"filter-start","operation":"output","input":"red","args":{"method":"toFilter"}} +{"id":"filter-second","operation":"output","input":"red","args":{"method":"toFilter","secondColor":"blue"}} +{"id":"filter-empty","operation":"output","input":"red","args":{"method":"toFilter","secondColor":""}} +{"id":"filter-zero","operation":"output","input":"red","args":{"method":"toFilter","secondColor":0}} +{"id":"filter-transparent","operation":"output","input":"transparent","args":{"method":"toFilter","secondColor":"red"}} +{"id":"brightness-white","operation":"analysis","input":"#fff","args":{"method":"brightness"}} +{"id":"equal-false","operation":"equals","input":"#f00","args":{"other":"#0f0"}} From 5eb75bcb865c942eeda824acde862e07a9228812 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:34:40 +0530 Subject: [PATCH 092/165] docs: record Phase 3 parity evidence Record Phase 3 plans, validation, summaries, compatibility evidence, and project state. --- .planning/ROADMAP.md | 12 +- .planning/STATE.md | 3 + .../03-01-PLAN.md | 190 +++++++++ .../03-01-SUMMARY.md | 7 + .../03-02-PLAN.md | 208 ++++++++++ .../03-02-SUMMARY.md | 7 + .../03-RESEARCH.md | 369 ++++++++++++++++++ .../03-VALIDATION.md | 60 +++ COMPATIBILITY.md | 22 +- 9 files changed, 873 insertions(+), 5 deletions(-) create mode 100644 .planning/phases/03-conversion-and-representation/03-01-PLAN.md create mode 100644 .planning/phases/03-conversion-and-representation/03-01-SUMMARY.md create mode 100644 .planning/phases/03-conversion-and-representation/03-02-PLAN.md create mode 100644 .planning/phases/03-conversion-and-representation/03-02-SUMMARY.md create mode 100644 .planning/phases/03-conversion-and-representation/03-RESEARCH.md create mode 100644 .planning/phases/03-conversion-and-representation/03-VALIDATION.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index ed3c6d69..9e1845ed 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -9,8 +9,8 @@ through parsing and observable output, to operations and delivery proof. ## Phases - [x] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. -- [ ] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. -- [ ] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. +- [x] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. +- [x] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. - [ ] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. - [ ] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. @@ -51,7 +51,11 @@ Plans: 1. RGB, percentage RGB, HSL, HSV, hex, hex8, name, filter, and generic string outputs match exactly. 2. Brightness, luminance, equality, cloning, and random behavior have documented parity cases. 3. No undocumented floating-point tolerance masks an observable mismatch. -**Plans**: TBD +**Plans**: 2 plans (complete 2026-08-01) + +Plans: +- [x] 03-01-PLAN.md — Implement B's source-equivalent conversion, representation, and analysis facade. +- [x] 03-02-PLAN.md — Add C's JSONL dispatch, differential corpus, and compatibility evidence. ### Phase 4: Operations and Palettes **Goal**: Go matches TinyColor's stateful modifiers, static utilities, readability checks, and palette ordering. @@ -79,6 +83,6 @@ Plans: |---|---|---|---| | 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | | 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | -| 3. Conversion and Representation | 0/TBD | Not started | - | +| 3. Conversion and Representation | 2/2 | Complete | 2026-08-01 | | 4. Operations and Palettes | 0/TBD | Not started | - | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index e28e1a16..3597e6f9 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -14,3 +14,6 @@ 26/26 HEX/RGB/name cases, 23/23 complete parser cases, and 9/9 smoke cases. - The Go module lives in `src/`; `tests/original/manifest.sha256` pins the unmodified root JavaScript oracle. +- Phase 3 completed locally: conversion/representation and analysis behavior + passed a 35/35 exact corpus; all Phase 1–3 corpora are zero-mismatch. The + compatibility protocol now preserves successful JSON `false` results. diff --git a/.planning/phases/03-conversion-and-representation/03-01-PLAN.md b/.planning/phases/03-conversion-and-representation/03-01-PLAN.md new file mode 100644 index 00000000..1409c244 --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-01-PLAN.md @@ -0,0 +1,190 @@ +--- +phase: 3 +plan: 1 +type: execute +subsystem: conversion-formatting-analysis +owner: B-xthxr +tags: [go, tinycolor, conversion, formatting, analysis] +wave: 1 +depends_on: [02-01, 02-02] +files_modified: + - src/tinycolor/color.go + - src/tinycolor/color_test.go +autonomous: true +requirements: [FMT-01, QLT-02] +must_haves: + truths: + - "Typed Go callers can obtain TinyColor-equivalent rounded RGB, percentage-RGB, fractional HSL/HSV, raw and string hex/hex8, name, filter, and generic string representations." + - "Typed Go callers can obtain source-equivalent brightness, luminance, dark/light classification, clone, equality, and random-color invariants." + - "Observable conversion and analysis boundary cases have direct, source-derived regression tests rather than a floating-point tolerance." + artifacts: + - path: "src/tinycolor/color.go" + provides: "The complete conversion, representation, compatibility-option state, clone/equality/random, and analysis facade over internal/color.Model" + contains: "func (c Color) ToRGBString() string" + - path: "src/tinycolor/color_test.go" + provides: "Source-derived output, analysis, equality, clone, and random-invariant regression tests" + contains: "Test" + key_links: + - from: "src/tinycolor/color.go" + to: "src/internal/color/model.go" + via: "Color.model RGBA, format, validity, and original state" + pattern: "c\\.model\\.(R|G|B|A|Format)" + - from: "src/tinycolor/color.go" + to: "src/internal/parser/names.go" + via: "reverse RGB-to-name lookup for ToName and format fallback" + pattern: "parser\\.NameForRGB" +--- + + +Replace the Phase-1 snapshot formatter with one source-derived TinyColor facade +for conversions, representations, and color analysis. + +Purpose: make all Phase-3 behavior available through `src/tinycolor` without +duplicating the parser/model or adding a color dependency. +Output: tested conversion, formatting, analysis, clone, equality, and random APIs +ready for compatibility dispatch. + + + +@AGENT.md +@DECISIONS.md +@docs/ARCHITECTURE.md +@.planning/phases/03-conversion-and-representation/03-RESEARCH.md +@.planning/phases/03-conversion-and-representation/03-VALIDATION.md + + + +@src/internal/color/model.go +@src/internal/parser/names.go +@src/tinycolor/color.go +@src/tinycolor/color_test.go + + +From `src/internal/color/model.go`: +```go +type Model struct { R, G, B, A float64; Valid bool; Format Format; Original any } +``` + +From `src/tinycolor/color.go`: +```go +type Color struct{ model color.Model } +func FromCompat(input any, fromRatio bool) (Color, error) +type CompatOptions struct { Format string; GradientType bool } +func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) +func (c Color) RGB() map[string]any +func (c Color) String() string +``` +Keep `model` private. `CompatOptions` is compatibility-only state held by +`Color`: a non-empty `Format` overrides the parser-detected format and +`GradientType` affects only filter output. The helper first calls the existing +parser-owned `FromCompat`, then attaches those options; it must not change +parser/model ownership or mutate parser results. + + + + + +Implement source-equivalent conversions and every output representation +src/tinycolor/color.go, src/tinycolor/color_test.go + +- `mod.js` lines 55-246 and 428-645 +- `test.js` lines 698-906 and 945-1125 +- `src/tinycolor/color.go` +- `src/internal/color/model.go` +- `src/internal/parser/names.go` + + +- `ToRGB` rounds RGB channels with source `Math.round` behavior and retains alpha; `ToHSL` and `ToHSV` retain their fractional hue/saturation/lightness-or-value channels and alpha. +- RGB, percentage RGB, HSL, HSV, raw `toHex`/`toHex8`, string hex/hex3/hex6/hex4/hex8, name, filter, and generic string output match source casing, separators, alpha rounding, RGBA/ARGB ordering, and fallback semantics. +- `String()` takes the omitted-format path. Source `formatSet = !!format` means a missing format and `""` are both unset; only a non-empty requested format is explicit, including unknown/name fallback. + + +In `src/tinycolor/color.go`, replace the Phase-1-only `String` branch tree with +one set of small private source primitives (RGB-to-HSL/HSV, source-style channel +rounding, decimal-alpha hex, RGB/RGBA/ARGB hex, and formatting dispatch) and +public explicit methods for rounded RGB, fractional HSL/HSV, percentage RGB, +raw and string hex/hex8, name, filter, and generic string output. Reuse +`Color.model` and `parser.NameForRGB`; do not alter `src/internal/color` or +`src/internal/parser`, add dependencies, or reparse rendered strings. Define +`CompatOptions { Format string; GradientType bool }` and +`FromCompatWithOptions(input, fromRatio, options)`: it must call `FromCompat` +first, then store a non-empty format override and the gradient flag on `Color`. +This is the sole compatibility-only construction path C will call; parser/model +remain owned by A. Implement the source contract exactly: lower-case hex, +`toHex8` RGBA versus `toFilter` ARGB, optional 3/4-character compression, +`ToName` as `(string, bool)` with alpha-zero `transparent`, and `ToString` +fallback using JavaScript truthiness (`formatSet = !!format`, so missing and +`""` are both unset). Make `ToFilter` consume a compatibility second-color +argument only when source truthiness would consume it; null, false, zero, and +empty string use the start color as the end color. Do not add a dynamic public +map API. Add failing direct Go tests before implementation for rounded `ToRGB`, +fractional `ToHSL`/`ToHSV`, opaque/alpha strings, percentage rounding, raw and +string hex variants, named/unnamed/transparent behavior, missing versus empty +versus non-empty format fallback, ARGB filter ordering, and source-truthy/falsy +second colors. This implements FMT-01 and keeps each discovered source +discrepancy as a named regression (QLT-02). + + +`color.go` has one canonical formatter used by `String()` and explicit output +methods; its `CompatOptions` construction helper stores source format/gradient +state without changing parser/model code. Direct tests prove rounded RGB, +fractional HSL/HSV, raw/string hex, missing/empty/non-empty format, and +source-truthy filter boundary cases without a broad float tolerance. + +Set-Location src; go test ./tinycolor -run 'Test.*(Output|String|Hex|Filter|Conversion|Name)' -count=1; go vet ./... +All supported Phase-3 representation methods compile and their direct source-derived output tests pass exactly. + + + +Implement and test analysis, clone, equality, and random invariants +src/tinycolor/color.go, src/tinycolor/color_test.go + +- `mod.js` lines 55-83 and 636-654 +- `test.js` lines 87-114, 895-942, and 1105-1125 +- `src/tinycolor/color.go` +- `src/tinycolor/color_test.go` + + +- Brightness and luminance use source-rounded RGB values; `IsDark` uses the source threshold and `IsLight` is its inverse. +- Clone returns independent value state, while equality follows TinyColor's formatted-RGB-string comparison and preserves falsy-input false behavior at the compatibility boundary. +- Random produces only valid opaque colors with RGB channels in range; it is never compared cross-runtime for exact values. + + +Add `Color` analysis methods for brightness, luminance, dark/light, cloning, +source-like equality, and random-color creation using only the Go standard +library (D-005). Make analysis consume the rounded RGB representation exactly +as `mod.js` does. Equality must be defined through the same RGB string +formatter rather than raw model floats; expose a typed public comparison and +leave JavaScript falsy coercion for C's adapter boundary. Clone must not share +mutable backing state. Add direct tests for black/white, the `#777`/`#888` +dark-light boundary, known luminance endpoints, equality alpha-rounding cases +(`#ff000066`/`.4`, `#f009`/`.6`), unequal colors, clone independence, and +random range/validity/alpha invariants. Do not invent a seedable API or claim +exact JavaScript/Go random parity; name any deterministic mismatch with a +reproducer for C's corpus/report workflow (QLT-02). + + +Direct tests show correct source-derived analysis/equality/clone behavior and +random tests assert only validity, alpha, and RGB bounds. + +Set-Location src; go test ./tinycolor -run 'Test.*(Brightness|Luminance|Dark|Light|Equal|Clone|Random)' -count=1; go test ./...; go vet ./... +Analysis, clone, equality, and random APIs pass focused and full Go tests with no cross-runtime random equality assertion. + + + + +Run `Set-Location src; go test ./...; go vet ./...`. Inspect the diff to ensure +only `src/tinycolor` is changed: Phase 3 must consume the parser/model contract, +not move conversion logic into it. The following plan supplies exact Node/Go +differential evidence and QLT-02 records for this facade. + + + +- All FMT-01 representation and analysis methods are callable through `tinycolor.Color`. +- Source formatting quirks are asserted as exact strings, not hidden by tolerance. +- B's typed facade is ready for C to dispatch without changing model/parser files. + + + +After completion, create `.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md`. + diff --git a/.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md b/.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md new file mode 100644 index 00000000..e760aa57 --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md @@ -0,0 +1,7 @@ +# Plan 03-01 Summary + +Implemented the `tinycolor.Color` conversion and representation facade: +RGB/percentage RGB, HSL/HSV, hex variants, names, filters, format fallback, +brightness, luminance, clone, equality, and random invariants. + +Validation passed: `go test ./tinycolor`, `go test ./...`, and `go vet ./...`. diff --git a/.planning/phases/03-conversion-and-representation/03-02-PLAN.md b/.planning/phases/03-conversion-and-representation/03-02-PLAN.md new file mode 100644 index 00000000..48dac784 --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-02-PLAN.md @@ -0,0 +1,208 @@ +--- +phase: 3 +plan: 2 +type: execute +subsystem: conversion-compatibility-evidence +owner: C-mrashis +tags: [compatibility, jsonl, differential-testing, tinycolor] +wave: 2 +depends_on: [03-01] +files_modified: + - compat/js-runner.mjs + - compat/run.mjs + - compat/cases/conversion.jsonl + - src/cmd/tinycolor-compat/main.go + - src/internal/compat/protocol.go + - src/internal/compat/protocol_test.go + - tests/port/adapter.test.mjs + - COMPATIBILITY.md +autonomous: true +requirements: [FMT-01, QLT-02] +must_haves: + truths: + - "Every deterministic Phase-3 output and analysis operation has an independently runnable Node/Go JSONL reproducer that compares exactly." + - "Compatibility requests preserve source `!!format` semantics (missing and `\"\"` unset) and can pass source constructor/filter options needed for format, gradient, and truthy second-color behavior." + - "A successful JSONL result of `false`, including `toName` for a non-name or partial alpha, is encoded as `result:false` rather than dropped by Go JSON omission." + - "Any mismatch report retains the request, JavaScript result, Go result, operation, suspected package, and owner B or C; random is documented as invariant-only." + artifacts: + - path: "compat/cases/conversion.jsonl" + provides: "Exact FMT-01 conversion, format, analysis, clone, and equality differential corpus" + min_lines: 30 + - path: "compat/js-runner.mjs" + provides: "Thin local-mod.js dispatch for Phase-3 compatibility operations" + - path: "src/cmd/tinycolor-compat/main.go" + provides: "Thin Go dispatch to the Phase-3 tinycolor facade" + - path: "src/internal/compat/protocol.go" + provides: "Presence-aware exclusive result/error JSONL encoding" + - path: "COMPATIBILITY.md" + provides: "Phase-3 evidence and documented random limitation or complete mismatch records" + key_links: + - from: "compat/cases/conversion.jsonl" + to: "compat/run.mjs" + via: "one JSONL request per source-derived case" + pattern: "conversion\\.jsonl" + - from: "compat/js-runner.mjs" + to: "mod.js" + via: "one tinycolor instance and named source method per request" + pattern: "tinycolor" + - from: "src/cmd/tinycolor-compat/main.go" + to: "src/tinycolor/color.go" + via: "thin operation dispatch to public Color methods" + pattern: "tinycolor\\." +--- + + +Expose B's tested Phase-3 facade through the existing JSONL protocol and publish +exact, reproducible Node-to-Go evidence for conversion and representation. + +Purpose: prove FMT-01 at the compatibility boundary while making every mismatch +actionable under QLT-02. +Output: adapter operations, protocol tests, fixed corpus, report ownership, and +compatibility-matrix evidence. + + + +@AGENT.md +@DECISIONS.md +@docs/TEAM-OWNERSHIP.md +@.planning/phases/03-conversion-and-representation/03-RESEARCH.md +@.planning/phases/03-conversion-and-representation/03-VALIDATION.md +@.planning/phases/03-conversion-and-representation/03-01-SUMMARY.md + + + +@compat/js-runner.mjs +@compat/run.mjs +@src/cmd/tinycolor-compat/main.go +@src/internal/compat/protocol.go +@tests/port/adapter.test.mjs +@COMPATIBILITY.md + + +Existing request contract from `src/internal/compat/protocol.go`: +```go +type Request struct { ID string; Operation string; Input any; Args any } +``` + +Preserve `inspect`, `string`, and `fromRatio` request meanings. Add only named +Phase-3 operations (`output`, `analysis`, `equals`, `clone`, and +`randomInvariant`) with backwards-compatible `args`; `args.options` carries +source constructor options and `args.method` names a whitelisted source method. +Decode options into B's concrete `tinycolor.CompatOptions` and construct through +`tinycolor.FromCompatWithOptions`; never duplicate parser or conversion math. +`Response` needs presence-aware custom encoding (for example, a private +`hasResult` flag plus custom `MarshalJSON`) so `Success(id, false)` emits +`{"id":"...","result":false}` while failures emit only `error`. + + + + + +Extend both adapters with narrow Phase-3 operation dispatch +compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, src/internal/compat/protocol.go, src/internal/compat/protocol_test.go, tests/port/adapter.test.mjs + +- `compat/js-runner.mjs` +- `src/cmd/tinycolor-compat/main.go` +- `src/internal/compat/protocol.go` +- `src/internal/compat/protocol_test.go` +- `tests/port/adapter.test.mjs` +- `src/tinycolor/color.go` +- `mod.js` lines 55-246 and 636-654 + + +- `output` dispatches one whitelisted output method, including raw `toHex` and `toHex8`; missing and `""` format remain unset because source uses `!!format`, while non-empty format is explicit. +- `toFilter` consumes `secondColor` only when JavaScript source truthiness would do so; missing/null/false/0/`""` use the start color for both ends. +- `analysis`, `equals`, and `clone` return source-shaped JSON results; unsupported methods/options fail through the existing exclusive result-or-error protocol. +- A Go successful result `false` serializes as a present `result:false`; error responses contain no result key, and adapter coverage includes a `toName` false case. +- `randomInvariant` returns only JSON-safe invariant facts, never a random color for exact Node/Go comparison. + + +Add matching thin dispatch in `compat/js-runner.mjs` and +`src/cmd/tinycolor-compat/main.go` for `output`, `analysis`, `equals`, `clone`, +and `randomInvariant`. Keep all existing operations byte-for-byte compatible. +For new construction-based requests, accept a backwards-compatible nested +`args.options` object and map it to `tinycolor.CompatOptions{Format, +GradientType}` through `tinycolor.FromCompatWithOptions`, which owns that state +after parser construction. The Node adapter passes the same options to the +source constructor. Whitelist operation/method names including raw `toHex` and +`toHex8`, reject unsupported input with the established JSONL error shape, and +do not add conversion logic to either runner. Preserve source false/string +results in JSON for `toName`. Apply JavaScript source semantics, not field +presence semantics: `formatSet = !!format`, so both missing and `""` are unset; +`secondColor` is used only when truthy. In `src/internal/compat/protocol.go`, +replace `omitempty`-dependent response output with presence-aware response +encoding: `Success(id, false)` must output `result:false`, `Success` and +`Failure` must remain mutually exclusive, and malformed/unsupported requests +retain the existing shape. First add `protocol_test.go` checks for false, true, +string, and failure responses plus adapter tests for `toName` false, missing and +empty formats, raw hex methods, second-color truthiness, and invalid +operation/method coverage; then implement. This is C-owned compatibility work +and routes FMT-01 behavior to B's facade while preserving complete QLT-02 +reproducers. + + +Both runners support the same named Phase-3 operations and return identical +JSON shapes for adapter tests; `result:false` remains present and exclusive of +errors, and no runner contains color conversion formulas. + +node tests/port/adapter.test.mjs; Set-Location src; go test ./cmd/tinycolor-compat ./internal/compat ./tinycolor; Set-Location ..; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser.jsonl +New adapter protocol tests pass and all pre-Phase-3 JSONL corpora remain zero-mismatch compatible. + + + +Publish conversion corpus, exact report ownership, and Phase-3 evidence +compat/cases/conversion.jsonl, compat/run.mjs, COMPATIBILITY.md + +- `test.js` lines 87-114, 698-942, 945-1125, and 1371-1398 +- `compat/cases/parser.jsonl` +- `compat/run.mjs` +- `COMPATIBILITY.md` +- `docs/TEAM-OWNERSHIP.md` + + +Create `compat/cases/conversion.jsonl` with independently runnable, +source-derived deterministic rows for every `to*` representation: rounded RGB, +percentage RGB, fractional HSL/HSV objects, HSL/HSV strings, raw `toHex`/ +`toHex8`, string hex/hex3/hex6/hex8/hex4, name/transparent, generic `toString` +fallback (opaque, alpha zero, alpha between zero and one, missing format, empty +format, and non-empty explicit known and unknown formats), filters (missing, +truthy, and falsy `secondColor`; alpha half; and gradient type), +brightness/luminance/dark/light, clone snapshots, and equality including +formatted-alpha cases and falsy inputs. Keep random out of +the exact corpus; add a `randomInvariant` case only if its Node and Go result +is the identical invariant record, never generated channel values. Update +`compat/run.mjs` so non-protocol Phase-3 mismatches are assigned owner `B` +(`src/tinycolor`) and protocol/schema/report failures owner `C` (`compat`), +while retaining the complete request, both results, operation, owner, and +suspected package in every emitted record. Run the corpus and update +`COMPATIBILITY.md` with the exact command, case count, zero-mismatch result or +each complete unresolved mismatch record, plus the invariant-only random +limitation. Do not edit `mod.js`, `test.js`, or any parser/model file. + + +The conversion corpus exercises every deterministic FMT-01 output/analysis +surface, `compat/run.mjs` prints complete owner-correct mismatch records, and +the matrix records reproducible Phase-3 evidence without claiming exact random +parity. + +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +All fixed Phase-1/2/3 corpora exit 0, Phase-3 evidence states its exact count, and every possible mismatch has a complete B-or-C-owned reproducer. + + + + +Run the full Phase-3 command from `03-VALIDATION.md`. Confirm `conversion.jsonl` +is fixed and deterministic, all JavaScript oracle files remain unchanged, and +random is validated only through invariant records. Do not claim an upstream +Deno test pass while Deno is unavailable. + + + +- FMT-01 has exact JSONL differential coverage for every deterministic Phase-3 operation. +- QLT-02 mismatch output is complete, reproducible, and assigned to B or C. +- Existing Phase-1/2 protocol and corpora still pass unchanged. + + + +After completion, create `.planning/phases/03-conversion-and-representation/03-02-SUMMARY.md`. + diff --git a/.planning/phases/03-conversion-and-representation/03-02-SUMMARY.md b/.planning/phases/03-conversion-and-representation/03-02-SUMMARY.md new file mode 100644 index 00000000..2a7a5be5 --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-02-SUMMARY.md @@ -0,0 +1,7 @@ +# Plan 03-02 Summary + +Added JSONL operations and conversion evidence, including a presence-aware +protocol that preserves successful `result: false` values. The fixed conversion +corpus passed 35/35 with zero mismatches. + +The full Phase 1–3 gate passed with all oracle files unchanged. diff --git a/.planning/phases/03-conversion-and-representation/03-RESEARCH.md b/.planning/phases/03-conversion-and-representation/03-RESEARCH.md new file mode 100644 index 00000000..62d3f7aa --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-RESEARCH.md @@ -0,0 +1,369 @@ +# Phase 3: Conversion and Representation - Research + +**Researched:** 2026-08-01 +**Domain:** TinyColor JavaScript-to-Go conversion, representation, and analysis compatibility +**Confidence:** HIGH + +## User Constraints + +No `03-CONTEXT.md` exists. The phase remains constrained by the roadmap, requirements, `AGENT.md`, and the immutable local TinyColor oracle. + + +## Phase Requirements + +| ID | Description | Research Support | +|---|---|---| +| FMT-01 | Reproduce color conversion and every documented output representation. | Map source output/analysis methods from `mod.js` and add operation-level differential cases for every observable result. | +| QLT-02 | Record every mismatch with complete reproducer and owner. | Keep every output case as one JSONL request, let `compat/run.mjs` emit the request plus both results, and assign output/conversion mismatches to B (`src/tinycolor`) or protocol mismatches to C (`compat`). | + + +## Project Constraints (from AGENT.md) + +- `mod.js`, `test.js`, `tinycolor.js`, `npm/`, `dist/`, and `demo/` are immutable JavaScript oracle material; do not change them to obtain parity. +- Implement port behavior only under `src/`; put adapters, generated fixtures, and mismatch reports under `compat/`. +- Preserve TinyColor quirks: precision until observable rounding, alpha normalization, detected-format fallback, invalid-as-black output, and permissive source behavior. Do not introduce a broad floating-point tolerance. +- Keep the public Go API explicit and idiomatic, but preserve dynamic source quirks at the JSON compatibility boundary. +- B owns `src/tinycolor` conversion/formatting/analysis; C owns `compat`, corpus/schema, and mismatch reporting. Do not change A's parser/model contract unilaterally. +- Required phase checks are `go test ./...`, `go vet ./...`, `node compat/js-runner.mjs = 0 + if !formatSet && hasAlpha && isNonAlphaFormat(format) { + if format == "name" && c.model.A == 0 { + return c.ToNameString() // "transparent" + } + return c.ToRGBString() + } + // Dispatch rgb/prgb/hex/hex3/hex4/hex8/name/hsl/hsv. + // Unknown or unavailable name falls back to six-digit #hex. +} +``` + +The public API should distinguish an omitted format from an explicitly supplied empty/unknown format. The compatibility adapter has that information from JSON request shape; a Go convenience `String()` can always call the omitted-format path. + +### Exact ARGB filter construction + +```go +// Source: mod.js toFilter and rgbaToArgbHex (lines 163-189, 624-635). +func (c Color) ToFilter(second *Color, gradientType bool) string { + start := "#" + c.argbHex() + end := start + if second != nil { + end = "#" + second.argbHex() + } + prefix := "" + if gradientType { + prefix = "GradientType = 1, " + } + return "progid:DXImageTransform.Microsoft.gradient(" + prefix + + "startColorstr=" + start + ",endColorstr=" + end + ")" +} +``` + +### Differential fixture shape + +```json +{"id":"fmt-hex8-half-alpha","operation":"output","input":"rgba(255, 0, 0, .5)","args":{"method":"toHex8String"}} +{"id":"fmt-filter-second","operation":"output","input":"transparent","args":{"method":"toFilter","secondColor":"red"}} +{"id":"analysis-brightness-threshold","operation":"analysis","input":"#777","args":{"method":"isDark"}} +{"id":"equal-alpha-string","operation":"equals","input":"#ff000066","args":{"other":"rgba(255, 0, 0, .4)"}} +``` + +Each JSONL row is independently runnable through `node compat/run.mjs compat/cases/conversion.jsonl`, satisfying QLT-02's complete reproducer requirement. + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|---|---|---|---| +| Phase-1 `Color.String` manually covered a small output subset | Phase 3 should make source-method equivalents the canonical conversion/formatting surface | This phase | Avoids divergence between adapter snapshots and public output methods. | +| Fixed smoke/parser corpus only used `inspect`, `string`, and `fromRatio` | Operation-level output/analysis corpus | This phase | Makes FMT-01 and QLT-02 measurable for every deterministic behavior. | + +**Deprecated/outdated:** Do not retain the comment in `src/tinycolor/color.go` that calls `String` the complete output API; it explicitly says full output APIs are Phase-3 work. + +## Open Questions + +1. **What exact exported Go signatures should represent boolean-or-string `toName` and optional formatting flags?** + - What we know: the compatibility adapter may return JSON `false` or a string; the public Go API should be explicit per `AGENT.md`. + - What's unclear: whether the project prefers `(string, bool)` for name lookup and separate `ToString(format string)` / `String()` methods, or a compatibility-shaped `any` method. + - Recommendation: use idiomatic `(string, bool)` for typed `ToName`; preserve `false` only in the adapter. Use a small private formatting dispatcher so `String()` represents omitted format and `ToString(format)` represents explicit format. + +2. **How should normal constructor options be represented in the JSONL protocol?** + - What we know: source options affect `_format` and `_gradientType`; existing normal constructor operations cannot pass them, while Node `fromRatio` forwards `args`. + - What's unclear: the exact new `args` schema. + - Recommendation: C defines one backwards-compatible nested `args.options` object for new operations, tests it with `{format:"name"}` and `{gradientType:true}`, and adds a Go compatibility construction helper rather than changing parser state. + +3. **How far should random parity go?** + - What we know: source uses unseeded `Math.random`; exact cross-runtime outputs are not comparable. + - What's unclear: whether a user-facing seeded API is desired later. + - Recommendation: do not add a seeded public API in this phase. Document invariant-only parity in `COMPATIBILITY.md`; add seed injection only if a later requirement explicitly needs reproducible random output. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|---|---|---:|---|---| +| Node.js | local JavaScript oracle and differential runner | ✓ | v24.18.0 | — | +| Go | port tests and Go JSONL runner | ✓ | go1.26.1 windows/amd64 | — | +| Deno | untouched source test suite | ✗ | — | Use Node JSONL differential evidence locally; run `deno task test` once Deno is installed/CI provides it. | + +**Missing dependencies with no fallback:** None for Phase-3 implementation and differential validation. Deno remains required for the later source-suite gate, but is not needed to execute the existing local oracle adapter. + +**Missing dependencies with fallback:** Deno — Node plus `compat/run.mjs` provides local differential evidence but does not constitute a Deno source-suite pass. + +## Validation Architecture + +### Test Framework + +| Property | Value | +|---|---| +| Framework | Go standard `testing` (Go 1.26.1); Node JSONL differential harness; upstream Deno tests unavailable locally | +| Config file | `src/go.mod`; `deno.json` for immutable upstream suite | +| Quick run command | `Set-Location src; go test ./tinycolor ./internal/...; Set-Location ..; node compat/run.mjs compat/cases/conversion.jsonl` | +| Full suite command | `Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl` | + +### Phase Requirements → Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|---|---|---|---|---| +| FMT-01 | RGB/percentage RGB/HSL/HSV object and string output; six/three/eight/four hex; name; generic fallback | Go unit + Node/Go differential | `node compat/run.mjs compat/cases/conversion.jsonl` | ❌ Wave 0 | +| FMT-01 | legacy ARGB `toFilter`, optional second color, gradient type | Go unit + differential | `node compat/run.mjs compat/cases/conversion.jsonl` | ❌ Wave 0 | +| FMT-01 | brightness, luminance, dark/light, clone, equality, random invariants | Go unit + deterministic differential except random | `Set-Location src; go test ./tinycolor; Set-Location ..; node compat/run.mjs compat/cases/conversion.jsonl` | ❌ Wave 0 | +| QLT-02 | mismatch has request, JS result, Go result, operation, and owner | integration/report | `node compat/run.mjs compat/cases/conversion.jsonl` | ✅ driver; ❌ conversion corpus | + +### Sampling Rate + +- **Per task commit:** `Set-Location src; go test ./tinycolor ./internal/...; Set-Location ..; node compat/run.mjs compat/cases/conversion.jsonl` +- **Per wave merge:** full suite command above. +- **Phase gate:** full suite green, conversion corpus zero mismatches, and `COMPATIBILITY.md` records both the evidence and any non-deterministic-random limitation before `/gsd:verify-work`. + +### Wave 0 Gaps + +- [ ] `compat/cases/conversion.jsonl` — deterministic FMT-01 cases grouped by all output methods, alpha/fallback rules, filter options, analysis, clone, and equality. +- [ ] `src/tinycolor/color_test.go` additions — direct source-derived output and analysis tests plus random invariant test. +- [ ] `tests/port/adapter.test.mjs` additions — every newly dispatched adapter operation has success/error protocol coverage. +- [ ] `src/cmd/tinycolor-compat/main.go` / `compat/js-runner.mjs` operation dispatch — adapter surface needed for the corpus. +- [ ] `COMPATIBILITY.md` Phase-3 evidence row and any complete unresolved mismatch records, owned B or C. + +## Sources + +### Primary (HIGH confidence) + +- Local [mod.js](../../../mod.js) — constructor state and public output/analysis methods (lines 5-246); conversion/hex/equality/random helpers (lines 359-645); rounding/bounds helpers (lines 1058-1129). +- Local [test.js](../../../test.js) — source expectations for clone/random (lines 87-114), output/alpha/name/analysis (lines 698-942), conversion round trips (lines 945-1103), equality (lines 1105-1125), and filters (lines 1371-1398). +- Local [AGENT.md](../../../AGENT.md) — immutable oracle, compatibility, ownership, and verification constraints. +- Local [docs/ARCHITECTURE.md](../../../docs/ARCHITECTURE.md) and [DECISIONS.md](../../../DECISIONS.md) — model/facade/adapter boundaries and standard-library-first decision. +- Local [compat/js-runner.mjs](../../../compat/js-runner.mjs), [compat/run.mjs](../../../compat/run.mjs), and [src/cmd/tinycolor-compat/main.go](../../../src/cmd/tinycolor-compat/main.go) — current protocol and missing operation coverage. + +### Secondary (MEDIUM confidence) + +None. The checked-out source and its source tests are the selected behavior authority. + +### Tertiary (LOW confidence) + +None. + +## Metadata + +**Confidence breakdown:** + +- Standard stack: HIGH — repository decisions mandate standard library, local oracle, and JSONL harness. +- Architecture: HIGH — direct inspection of the model/parser/facade/adapters and source implementation. +- Pitfalls: HIGH — each is tied to a source branch or test assertion. + +**What might have been missed:** The immutable `test.js` is not runnable locally because Deno is unavailable, so its listed cases were inspected rather than executed. Add Phase-3 cases to the JSONL corpus now and run `deno task test` when Deno becomes available; do not claim that pass beforehand. + +**Research date:** 2026-08-01 +**Valid until:** Stable for this pinned local source; revisit if the selected oracle checkout changes. diff --git a/.planning/phases/03-conversion-and-representation/03-VALIDATION.md b/.planning/phases/03-conversion-and-representation/03-VALIDATION.md new file mode 100644 index 00000000..e306ed3a --- /dev/null +++ b/.planning/phases/03-conversion-and-representation/03-VALIDATION.md @@ -0,0 +1,60 @@ +--- +phase: 3 +slug: conversion-and-representation +status: ready +nyquist_compliant: true +wave_0_complete: false +created: 2026-08-01 +--- + +# Phase 3 — Validation Strategy + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Go standard `testing`; Node JSONL differential harness | +| **Config file** | `src/go.mod` | +| **Quick run command** | `Set-Location src; go test ./tinycolor ./internal/...; Set-Location ..; node compat/run.mjs compat/cases/conversion.jsonl` | +| **Full suite command** | `Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl` | +| **Estimated runtime** | ~20 seconds | + +## Sampling Rate + +- **After every task commit:** Run the quick command. +- **After every plan wave:** Run the full suite command. +- **Before `$gsd-verify-work`:** Full suite must be green. +- **Max feedback latency:** 30 seconds. + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|-----------|-------------------|-------------|--------| +| 03-01-01 | 01 | 1 | FMT-01, QLT-02 | direct Go unit | `Set-Location src; go test ./tinycolor -run 'Test.*(Output|String|Hex|Filter|Conversion|Name)' -count=1` | ✅ task creates tests | ⬜ pending | +| 03-01-02 | 01 | 1 | FMT-01, QLT-02 | direct Go unit | `Set-Location src; go test ./tinycolor -run 'Test.*(Brightness|Luminance|Dark|Light|Equal|Clone|Random)' -count=1` | ✅ task creates tests | ⬜ pending | +| 03-02-01 | 02 | 2 | FMT-01, QLT-02 | protocol + regression | `node tests/port/adapter.test.mjs; Set-Location src; go test ./internal/compat` | ✅ task extends tests | ⬜ pending | +| 03-02-02 | 02 | 2 | FMT-01, QLT-02 | exact differential | `node compat/run.mjs compat/cases/conversion.jsonl` | ✅ task creates corpus | ⬜ pending | + +## Test-fixture sequencing + +No separate Wave 0 is required: Plan 03-01 creates and runs its direct Go +regressions in Wave 1; Plan 03-02 then creates the adapter protocol coverage +and differential corpus after that facade exists in Wave 2. The conversion +corpus is not a prerequisite fixture for Plan 03-01 and must not be referenced +as one. + +## Manual-Only Verifications + +None. Random output is verified by per-runtime invariants rather than false +cross-runtime exact comparison. + +## Validation Sign-Off + +- [x] All tasks have automated verification or a Wave 0 dependency. +- [x] Sampling continuity has no three-task validation gap. +- [x] No Wave 0 is required; the conversion fixture is created by Wave 2. +- [x] No watch-mode flags. +- [x] Feedback latency is under 30 seconds. +- [x] `nyquist_compliant: true` is set in frontmatter. + +**Approval:** ready 2026-08-01 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 60c08e28..b045f87e 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -11,7 +11,7 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | Go adapter | Passing | `go test ./...` from `src/` | JSONL schema shared with Node adapter. | | HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | | HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | -| Conversion/formatting | Planned | Wave 2 | Exact strings and rounding. | +| Conversion/formatting | Passing | `compat/cases/conversion.jsonl` | 35 fixed JSONL cases; random is invariant-only. | | Analysis/readability | Planned | Wave 2–3 | Preserve WCAG defaults. | | Manipulation/palettes | Planned | Wave 3 | Preserve defaults and ordering. | | CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | @@ -64,3 +64,23 @@ Phase 2 Plan 02 work. `node compat/run.mjs compat/cases/parser.jsonl` passed with **23/23 cases** and **0 mismatches** on 2026-08-01. The unchanged Phase 1 smoke corpus also passed **9/9** with **0 mismatches**. + +## Phase 3 conversion result + +On 2026-08-01, the fixed conversion corpus passed with **35/35 cases** and +**0 mismatches**. The complete Phase 1–3 gate also retained **9/9** smoke, +**26/26** HEX/RGB/name, and **23/23** parser cases with zero mismatches: + +```powershell +$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' +Set-Location src; go test ./...; go vet ./...; Set-Location .. +node tests/port/adapter.test.mjs +node compat/run.mjs compat/cases/smoke.jsonl +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/parser.jsonl +node compat/run.mjs compat/cases/conversion.jsonl +``` + +Random-color behavior is verified by validity, alpha, and channel-range +invariants only; it is not compared exactly across independent JavaScript and +Go random generators. From 8744a6bd1e615a021cb9b689fd71aa108e9e9694 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 02:37:47 +0530 Subject: [PATCH 093/165] docs: design Phase 4 operations Record the approved modifiers, readability, and palettes design with source-parity constraints. --- .../2026-08-01-phase-4-operations-design.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-01-phase-4-operations-design.md diff --git a/docs/superpowers/specs/2026-08-01-phase-4-operations-design.md b/docs/superpowers/specs/2026-08-01-phase-4-operations-design.md new file mode 100644 index 00000000..3156758f --- /dev/null +++ b/docs/superpowers/specs/2026-08-01-phase-4-operations-design.md @@ -0,0 +1,42 @@ +# Phase 4 Operations Design + +## Scope + +Port the remaining TinyColor operational behavior in three independently +verifiable slices: + +1. Mutating instance modifiers and pure utility operations: lighten, brighten, + darken, saturate, desaturate, greyscale, spin, and mix. +2. WCAG readability operations: readability, isReadable, and mostReadable. +3. Combination operations: complement, analogous, monochromatic, split + complement, triad, and tetrad. + +## Architecture + +`src/tinycolor` remains the single Go facade over the normalized parser/model. +It owns source-equivalent conversion-based operations and mutable `Color` +instance methods. The JSONL runners only whitelist and dispatch these public +methods; they do not contain conversion, palette, or WCAG formulas. + +Each slice receives direct Go regression tests and fixed JSONL cases evaluated +against the immutable local `mod.js` oracle. The Go API uses explicit typed +arguments; the compatibility adapter owns JavaScript truthiness/default coercion +where it cannot be represented in that API. + +## Behavioral Constraints + +- Match source defaults, including explicit zero amounts, clamping, hue + wrapping, alpha preservation, and receiver mutation for instance modifiers. +- Keep utility and palette results independent of their inputs, preserve source + result order, and retain source palette defaults. +- Implement WCAG option defaults and fallback-color recursion exactly. Do not + use a broad numeric tolerance in the differential runner. +- Use only the Go standard library and do not change the JavaScript oracle. + +## Verification + +Each slice follows red-green-refactor with a focused Go test and a fixed +Node-to-Go JSONL corpus. After every micro-commit, run its focused test and +corpus. Phase completion requires `go test ./...`, `go vet ./...`, all prior +corpora, the Phase 4 corpus, `git diff --check`, and no diff under `mod.js`, +`test.js`, or `tinycolor.js`. \ No newline at end of file From 7b4f8e056d85b7c7ebb0e34592c3abbe28048489 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 02:47:39 +0530 Subject: [PATCH 094/165] docs: plan Phase 4 operations Add GSD research, six executable micro-commit plans, and validation coverage for operations and palettes. --- .../04-operations-and-palettes/04-01-PLAN.md | 126 +++++++++ .../04-operations-and-palettes/04-02-PLAN.md | 123 ++++++++ .../04-operations-and-palettes/04-03-PLAN.md | 79 ++++++ .../04-operations-and-palettes/04-04-PLAN.md | 77 +++++ .../04-operations-and-palettes/04-05-PLAN.md | 79 ++++++ .../04-operations-and-palettes/04-06-PLAN.md | 87 ++++++ .../04-operations-and-palettes/04-RESEARCH.md | 267 ++++++++++++++++++ .../04-VALIDATION.md | 56 ++++ 8 files changed, 894 insertions(+) create mode 100644 .planning/phases/04-operations-and-palettes/04-01-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-02-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-03-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-04-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-05-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-06-PLAN.md create mode 100644 .planning/phases/04-operations-and-palettes/04-RESEARCH.md create mode 100644 .planning/phases/04-operations-and-palettes/04-VALIDATION.md diff --git a/.planning/phases/04-operations-and-palettes/04-01-PLAN.md b/.planning/phases/04-operations-and-palettes/04-01-PLAN.md new file mode 100644 index 00000000..d1afd3c0 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-01-PLAN.md @@ -0,0 +1,126 @@ +--- +phase: 4 +plan: 1 +type: tdd +subsystem: operations-modifiers-mix +owner: B-xthxr +tags: [go, tinycolor, modifiers, mix, tdd] +wave: 1 +depends_on: [03-01, 03-02] +files_modified: + - src/tinycolor/color.go + - src/tinycolor/operations_test.go +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Pointer modifiers mutate their receiver, preserve alpha and metadata, and return that same receiver." + - "Typed Mix returns a fresh interpolated color without mutating either input." + - "Typed operations use explicit numeric arguments; JavaScript omission and truthiness are not public Go behavior." + artifacts: + - path: "src/tinycolor/color.go" + provides: "Pointer modifier methods, source-style HSL/HSV-to-RGB helpers, and pure Mix" + contains: "func (c *Color) Lighten(amount float64) *Color" + - path: "src/tinycolor/operations_test.go" + provides: "Direct source-derived modifiers and Mix regressions" + contains: "TestModifiers" + key_links: + - from: "src/tinycolor/color.go" + to: "src/internal/color/model.go" + via: "Color.model channel and alpha updates" + pattern: "c\\.model\\.(R|G|B|A)" +--- + + +Build B's typed operations foundation: mutable TinyColor modifiers and a pure +Mix utility, with source arithmetic but no JavaScript argument coercion. + +Purpose: establish the public library contract that the next adapter plan can +dispatch without putting color math or dynamic defaults in compatibility code. +Output: source-derived Go tests and operations in `src/tinycolor`. + + + +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@docs/superpowers/specs/2026-08-01-phase-4-operations-design.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md + + + +@mod.js lines 255-321, 655-710, and 771-790 +@test.js lines 2013-2078 +@src/tinycolor/color.go +@src/internal/color/model.go + + +Existing public substrate: +```go +type Color struct { model color.Model; gradientType bool } +func (c Color) ToRGB() RGB +func (c Color) ToHSL() HSL +func (c Color) ToHSV() HSV +``` + +Add exactly these typed public contracts: +```go +func (c *Color) Lighten(amount float64) *Color +func (c *Color) Darken(amount float64) *Color +func (c *Color) Saturate(amount float64) *Color +func (c *Color) Desaturate(amount float64) *Color +func (c *Color) Greyscale() *Color +func (c *Color) Brighten(amount float64) *Color +func (c *Color) Spin(amount float64) *Color +func Mix(first, second Color, amount float64) Color +``` +These methods receive an explicit amount. Do not add optional, variadic, `any`, +or static modifier APIs. The Phase 4 adapter alone supplies source defaults. + + + + + +Red-green-refactor pointer modifiers and pure Mix +src/tinycolor/color.go, src/tinycolor/operations_test.go + +- Lighten/darken and saturate/desaturate adjust HSL lightness/saturation by `amount / 100`, clamp only that component to `[0,1]`, preserve alpha, and mutate the original `*Color`. +- Greyscale is desaturate 100; brighten adds `Math.round(255 * amount / 100)` to each rounded RGB channel before channel clamping; spin wraps hue into `[0,360)` without a typed default. +- Mix interpolates rounded source RGB and alpha as `(second-first)*amount/100+first`, returns a new value, and lets model normalization enforce source out-of-range behavior. +- `Spin(math.NaN())` follows the source omitted-argument path: it changes the receiver's channels to black while retaining the receiver's validity, format, original input, alpha, and identity. + + +RED: create `operations_test.go` with named tests for same-pointer return and +visible receiver mutation (`red.Lighten(10)` becomes `#ff3333`), explicit zero +no-ops, HSL clamp at both limits, brighten's pre-clamp rounding boundary, +negative and over-360 spin wrapping, `math.NaN()` spin black-channel metadata retention, alpha preservation, greyscale, Mix default +math supplied explicitly as 50, Mix zero, amount 90, transparent alpha, and +out-of-range Mix. Run the focused command and confirm it fails only because the +operations API is absent. GREEN: add local reciprocal HSL/HSV conversion and +clamp helpers only in `color.go`, then implement the listed pointer methods and +pure `Mix`; retain `model.Format`, `Original`, `Valid`, and `gradientType` while +changing only operation-owned channels/alpha. Use the standard library only; +do not modify parser/model or add dependencies. REFACTOR only after green to +share conversion/update helpers without changing externally tested results. + +Make micro-commits in this exact order: `test(04-01): add failing modifier and +mix regressions`, `feat(04-01): implement modifiers and mix`, and, only if a +cleanup is needed, `refactor(04-01): share operation conversion helpers`. + +Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1; go test ./...; go vet ./... +All modifier and Mix direct tests pass; no file outside `src/tinycolor` changes, and the typed facade contains no JSON/default/coercion handling. + + + + +Run the focused test while RED and again after every GREEN/REFACTOR commit. Then +run the full Go command in `04-VALIDATION.md`; do not run or modify the oracle. + + + +- OPS-01 modifier mutation, bounds, hue wrapping, alpha, and Mix invariants are directly tested. +- QLT-02 has named source-derived regressions ready for fixed adapter rows. + + + +After completion, create `.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md`. + \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-02-PLAN.md b/.planning/phases/04-operations-and-palettes/04-02-PLAN.md new file mode 100644 index 00000000..22ecbe3f --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-02-PLAN.md @@ -0,0 +1,123 @@ +--- +phase: 4 +plan: 2 +type: execute +subsystem: operations-modifiers-mix-compatibility +owner: C-mrashis +tags: [compatibility, jsonl, modifiers, mix] +wave: 2 +depends_on: [04-01] +files_modified: + - compat/js-runner.mjs + - src/cmd/tinycolor-compat/main.go + - tests/port/adapter.test.mjs + - compat/run.mjs + - compat/cases/operations.jsonl +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Modifier and Mix requests compare source and Go results exactly, including mutation identity snapshots." + - "Absent, zero, and null arguments follow JavaScript only in the adapters." + - "Every modifier and Mix corpus mismatch reports the fixed row's owner and suspected package." + artifacts: + - path: "compat/cases/operations.jsonl" + provides: "Deterministic modifier and Mix JSONL reproducers with owner and suspected-package metadata" + - path: "compat/run.mjs" + provides: "Mismatch records that preserve corpus ownership metadata" + - path: "src/cmd/tinycolor-compat/main.go" + provides: "Presence-aware dispatch to typed modifier and Mix APIs" + key_links: + - from: "compat/cases/operations.jsonl" + to: "compat/js-runner.mjs" + via: "modify and mix requests" + pattern: "operation.*(modify|mix)" +--- + + +Prove modifier and Mix parity through thin, presence-aware compatibility +dispatch and fixed deterministic corpus rows. + +Purpose: record every source quirk as a reproducer without leaking dynamic +JavaScript behavior into B's public API. +Output: matching Node/Go operations plus initial Phase 4 corpus. + + + +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md + + + +@mod.js lines 255-321, 655-710, and 771-790 +@compat/js-runner.mjs +@src/cmd/tinycolor-compat/main.go +@tests/port/adapter.test.mjs +@compat/run.mjs + + +Request shape stays `{"id","operation","input","args"}`. Add only: +```json +{"operation":"modify","args":{"method":"lighten","amount":0}} +{"operation":"mix","args":{"other":"#000","amount":25}} +``` +`modify` returns `{before,after,sameReceiver}`; `mix` returns one `Inspect` +snapshot. In Go, decode `args` as a map and check key presence before decoding +the value. Do not add formulas to either runner. + + + + + +Add presence-aware modifier and Mix adapter dispatch +compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs + +- `modify` creates one source color, snapshots it before and after one whitelisted call, and proves the returned source instance is identical. +- For lighten/brighten/darken/saturate/desaturate, source uses `amount === 0 ? 0 : amount || 10`; Mix uses the same rule with 50; Spin has no default, where omitted mutates the valid receiver to black channels while retaining metadata and JSON null is zero. + + +RED: extend the Node adapter tests with successful `modify` and `mix` response +shapes, omitted versus zero lighten, omitted versus null Spin, and unsupported +method errors. Confirm they fail before dispatch exists. GREEN: add only +whitelisted `modify` and `mix` branches in both runners. Node must use +`Object.hasOwn(args, "amount")`; Go must use map presence, not a zero-value +decode. The adapter selects defaults, forwards explicit numbers to B's typed +methods, and serializes `before`, `after`, and `sameReceiver: true` for Go only +after a pointer method call. Route omitted Spin through `Color.Spin(math.NaN())` so the pointer operation +produces the source snapshot: black channels with the original valid/name/original +metadata and same receiver; pass JSON null to typed zero. Mix parses `other`, calls `tinycolor.Mix`, and returns a snapshot. +Keep existing operations unchanged and put no HSL/HSV or interpolation logic in +the runners. Commit RED as `test(04-02): add modifier adapter failures`, then +GREEN as `feat(04-02): dispatch modifiers and mix`. + +node tests/port/adapter.test.mjs; Set-Location src; go test ./cmd/tinycolor-compat ./tinycolor -count=1; Set-Location ..; node compat/run.mjs compat/cases/smoke.jsonl +Adapters have matching modify/mix JSON shapes and preserve all documented defaults exclusively at the boundary. + + + +Publish exact modifier and Mix corpus rows +compat/cases/operations.jsonl, compat/run.mjs + +Create the initial deterministic Phase 4 corpus using the operation shapes +above. Include every modifier with normal, explicit-zero, clamp, and alpha +coverage; Spin negative, over-360, omitted, and null; Greyscale; and Mix +omitted 50, zero, 90, transparent alpha, and out-of-range amounts. Each row +needs a stable unique id, one observable source behavior, `owner` (`B-xthxr` or +`C-mrashis`), and `suspectedPackage`. Update `compat/run.mjs` so a mismatch +reports those fixed row fields along with the request and both responses; use +the existing protocol fallback only for legacy rows without metadata. Run the +corpus after each addition batch; retain every mismatch row unchanged as its +complete QLT-02 reproducer. Commit only this evidence as `test(04-02): add modifier and +mix differential corpus`. + +node compat/run.mjs compat/cases/operations.jsonl; git diff --check -- compat/cases/operations.jsonl compat/run.mjs +The corpus contains reproducible modifier and Mix cases and reports zero mismatches or complete, row-owned harness mismatch records. + + + +Run the focused checks in `04-VALIDATION.md`; confirm source default/coercion text occurs only in adapter code and corpus cases. + +After completion, create `.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-03-PLAN.md b/.planning/phases/04-operations-and-palettes/04-03-PLAN.md new file mode 100644 index 00000000..0257457f --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-03-PLAN.md @@ -0,0 +1,79 @@ +--- +phase: 4 +plan: 3 +type: tdd +subsystem: readability-library +owner: B-xthxr +tags: [go, tinycolor, wcag, readability, tdd] +wave: 3 +depends_on: [04-02] +files_modified: [src/tinycolor/color.go, src/tinycolor/operations_test.go] +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Readability and WCAG checks use exact source thresholds and inclusive comparisons." + - "MostReadable preserves first ties, fallback recursion, and an empty candidate result." + artifacts: + - path: "src/tinycolor/color.go" + provides: "Typed WCAG2Options, Readability, IsReadable, and nullable MostReadable" + contains: "func MostReadable" + key_links: + - from: "src/tinycolor/color.go" + to: "src/tinycolor/color.go" + via: "Readability uses existing rounded-channel Luminance" + pattern: "Luminance" +--- + +Implement B's source-equivalent WCAG operations with a typed options contract and direct regressions. +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@docs/superpowers/specs/2026-08-01-phase-4-operations-design.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md + +@mod.js lines 797-871 and 1261-1277 +@test.js lines 1100-1370 +@src/tinycolor/color.go + + +Add a typed value contract, with normalization private to `tinycolor`: +```go +type WCAG2Options struct { Level string; Size string; IncludeFallbackColors bool } +func Readability(first, second Color) float64 +func IsReadable(first, second Color, options WCAG2Options) bool +func MostReadable(base Color, candidates []Color, options WCAG2Options) (Color, bool) +``` +The bool is false for the source null candidate result. Adapter conversion, +including dynamic truthiness and non-string option inputs, is not public API. + + + + +Red-green-refactor exact readability and selection operations +src/tinycolor/color.go, src/tinycolor/operations_test.go + +- Readability is raw `(max(luminance)+.05)/(min(luminance)+.05)`; no rounding or epsilon. +- Normalized options support AA/AAA and small/large thresholds 4.5, 3, 7, and 4.5 inclusively. +- MostReadable uses strict-greater selection, returns the first tie, conditionally recurses only to white/black, and can return no color for empty candidates. + + +RED: add direct tests for ratios 1 and 21, all four threshold boundaries, +mixed-case typed options, invalid typed option fallback, first-candidate tie, +unreadable candidate fallback to `#fff/#000`, fallback disabled, and empty +candidates with both fallback flags. Confirm failure before adding APIs. GREEN: +implement a private options normalizer, `Readability`, `IsReadable`, and +`MostReadable` against `Color.Luminance()`. Preserve exact raw float results +and source scan order; keep fallback local and never mutate caller slices or +options. Empty candidates must yield `(Color{}, false)` rather than panic or an +invented color. Do not add a WCAG package, parser changes, JSON coercion, or a +float tolerance. Commit `test(04-03): add failing readability regressions`, +then `feat(04-03): implement readability operations`; refactor only after green. + +Set-Location src; go test ./tinycolor -run 'Test(Readability|IsReadable|MostReadable)' -count=1; go test ./...; go vet ./... +All typed WCAG and selection regressions pass exactly, including nullable empty results. + + +Run the focused test before and after implementation, then the full Go gate in `04-VALIDATION.md`. +- OPS-01 readability is source-equivalent in the library. +- QLT-02 has named direct tests for every threshold and selection edge. +After completion, create `.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-04-PLAN.md b/.planning/phases/04-operations-and-palettes/04-04-PLAN.md new file mode 100644 index 00000000..e519a1d2 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-04-PLAN.md @@ -0,0 +1,77 @@ +--- +phase: 4 +plan: 4 +type: execute +subsystem: readability-compatibility +owner: C-mrashis +tags: [compatibility, jsonl, wcag, readability] +wave: 4 +depends_on: [04-03] +files_modified: [compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs, compat/cases/operations.jsonl] +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Readability adapter requests preserve source WCAG defaults/coercion and null results." + - "Every WCAG selection and fallback edge is an exact JSONL reproducer." + artifacts: + - path: "compat/cases/operations.jsonl" + provides: "Readability, isReadable, and mostReadable corpus rows" + key_links: + - from: "src/cmd/tinycolor-compat/main.go" + to: "src/tinycolor/color.go" + via: "thin dispatch to Readability, IsReadable, and MostReadable" + pattern: "tinycolor\\.(Readability|IsReadable|MostReadable)" +--- + +Expose B's readability APIs through source-shaped compatibility requests and exact corpus proof. +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md +@mod.js lines 797-871 and 1261-1277 +@compat/js-runner.mjs +@src/cmd/tinycolor-compat/main.go +@compat/cases/operations.jsonl + + +Add request operations: +```json +{"operation":"readability","args":{"other":"#fff"}} +{"operation":"isReadable","args":{"other":"#fff","options":{"level":"AAA","size":"large"}}} +{"operation":"mostReadable","args":{"candidates":["#111","#222"],"options":{"includeFallbackColors":true}}} +``` +`mostReadable` serializes `null` when B returns `ok == false`; adapters map +dynamic options to `WCAG2Options` and never implement contrast formulas. + + + +Add source-coercing WCAG adapter dispatch +compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs + +RED: add adapter tests for raw numeric readability, default empty options, +mixed-case strings, invalid/null/false/zero/empty WCAG fields, strict tie +order, fallback true/false, and empty candidates serializing null. GREEN: add +only three dispatch branches to both runners. Node calls the source methods; +Go maps presence-aware dynamic options so JavaScript `(value || default)` and +case normalization happen here, then calls B's typed functions. For Go's +`MostReadable`, serialize `null` for `ok == false`; do not construct a zero +color snapshot. Preserve source truthiness for `includeFallbackColors` and use +no WCAG math in either runner. Commit RED as `test(04-04): add readability +adapter failures` and GREEN as `feat(04-04): dispatch readability operations`. + +node tests/port/adapter.test.mjs; Set-Location src; go test ./cmd/tinycolor-compat ./tinycolor -count=1; Set-Location ..; node compat/run.mjs compat/cases/operations.jsonl +Both runners return matching readability types and source-shaped null/fallback behavior. + + +Add WCAG differential reproducers +compat/cases/operations.jsonl +Add deterministic rows for ratios 1, 1.1121078324840545, and 21; all four threshold families; defaults and invalid/mixed-case options; first-tie selection; fallback enabled/disabled; and empty candidates with true and false fallback. Keep one behavior per id, append to the existing Phase 4 corpus, and commit `test(04-04): add readability differential corpus`. +node compat/run.mjs compat/cases/operations.jsonl; git diff --check -- compat/cases/operations.jsonl +All readability rows pass exactly or retain complete harness mismatch records for QLT-02. + + +Execute both focused commands in `04-VALIDATION.md`; inspect adapter diffs to confirm they only decode/coerce/dispatch/serialize. +- OPS-01 WCAG behavior is verified at the Node-to-Go boundary. +- QLT-02 captures every dynamic-options and nullable-result edge. +After completion, create `.planning/phases/04-operations-and-palettes/04-04-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-05-PLAN.md b/.planning/phases/04-operations-and-palettes/04-05-PLAN.md new file mode 100644 index 00000000..9d6a7096 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-05-PLAN.md @@ -0,0 +1,79 @@ +--- +phase: 4 +plan: 5 +type: tdd +subsystem: palettes-library +owner: B-xthxr +tags: [go, tinycolor, palettes, tdd] +wave: 5 +depends_on: [04-04] +files_modified: [src/tinycolor/color.go, src/tinycolor/operations_test.go] +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Palette values are independent of their input and preserve source order, defaults, alpha, and hue wrapping." + - "Polyad remains private; callers only receive Complement, SplitComplement, Triad, Tetrad, Analogous, and Monochromatic." + artifacts: + - path: "src/tinycolor/color.go" + provides: "Typed palette methods and private polyad helper" + contains: "func (c Color) Analogous" + key_links: + - from: "src/tinycolor/color.go" + to: "src/tinycolor/color.go" + via: "palette methods use source-compatible HSL/HSV helpers" + pattern: "(rgbToHSL|rgbToHSV)" +--- + +Implement B's pure palette methods, preserving TinyColor order and typed explicit-argument semantics. +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@docs/superpowers/specs/2026-08-01-phase-4-operations-design.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md +@mod.js lines 713-769 +@test.js lines 2080-2150 +@src/tinycolor/color.go + + +Add only: +```go +func (c Color) Complement() Color +func (c Color) SplitComplement() []Color +func (c Color) Triad() []Color +func (c Color) Tetrad() []Color +func (c Color) Analogous(results, slices int) []Color +func (c Color) Monochromatic(results int) []Color +``` +Use a private `polyad` helper. Explicit zero is a typed value; the next adapter +plan applies JavaScript `results || 6` and `slices || 30` behavior. + + + +Red-green-refactor ordered pure palette operations +src/tinycolor/color.go, src/tinycolor/operations_test.go + +- Complement, split complement, triad, and tetrad return source-ordered values beginning with the input where source does. +- Analogous follows the signed/bit-shift source hue initialization and emits default-red order `ff0000,ff0066,ff0033,ff0000,ff3300,ff6600` when called with explicit 6/30. +- Monochromatic follows HSV value stepping and emits `ff0000,2a0000,550000,800000,aa0000,d40000` for explicit six red results. + + +RED: add direct tests for every public palette result length/order, the two +documented red sequences, custom analogous results/slices, non-red hue wrap, +alpha preservation, and independence by mutating a returned value and checking +the source and other values. Confirm absent methods fail first. GREEN: implement +value-returning palette methods using private HSL/HSV helpers and a private +polyad helper. Preserve source arithmetic and output order precisely; never +mutate `c`, export polyad, special-case Go zero as a JavaScript default, or +modify parser/model. Decide and test the typed zero behavior explicitly as an +empty result or documented guard before adapter coercion, rather than silently +mapping it to 6/30. Commit `test(04-05): add failing palette regressions`, then +`feat(04-05): implement ordered palettes`; refactor only while green. + +Set-Location src; go test ./tinycolor -run 'Test(Palette|Complement|Analogous|Monochromatic|Triad|Tetrad)' -count=1; go test ./...; go vet ./... +All palette APIs are pure, ordered, direct-test covered, and no public JavaScript-default API exists. + + +Use the focused RED/GREEN command and the full Go gate from `04-VALIDATION.md`. +- OPS-01 palette ordering and independence are library-tested. +- QLT-02 has direct cases for default-equivalent values, customization, wrapping, and alpha. +After completion, create `.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-06-PLAN.md b/.planning/phases/04-operations-and-palettes/04-06-PLAN.md new file mode 100644 index 00000000..232ccdb7 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-06-PLAN.md @@ -0,0 +1,87 @@ +--- +phase: 4 +plan: 6 +type: execute +subsystem: palettes-compatibility-phase-gate +owner: C-mrashis +tags: [compatibility, jsonl, palettes, phase-gate] +wave: 6 +depends_on: [04-05] +files_modified: [compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs, compat/cases/operations.jsonl] +autonomous: true +requirements: [OPS-01, QLT-02] +must_haves: + truths: + - "Every public palette operation has an exact source-order JSONL reproducer." + - "Adapter-only zero/default coercion selects 6/30 for analogous and six for monochromatic." + - "The final Phase 4 gate runs all prior corpora and reports each mismatch with its existing owner metadata." + artifacts: + - path: "compat/cases/operations.jsonl" + provides: "Complete deterministic Phase 4 operations corpus" + min_lines: 50 + key_links: + - from: "compat/js-runner.mjs" + to: "mod.js" + via: "whitelisted palette source method" + pattern: "(complement|analogous|monochromatic|splitcomplement|triad|tetrad)" +--- + +Finish Phase 4 by adding palette adapter/corpus proof and running the full reproducible gate. +@AGENT.md +@.planning/phases/04-operations-and-palettes/04-RESEARCH.md +@.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md +@.planning/phases/04-operations-and-palettes/04-VALIDATION.md +@mod.js lines 713-769 +@compat/js-runner.mjs +@src/cmd/tinycolor-compat/main.go +@compat/run.mjs +@compat/cases/operations.jsonl + + +Add `palette` requests with `args.method` limited to `complement`, +`splitcomplement`, `triad`, `tetrad`, `analogous`, and `monochromatic`. +Results are arrays of `Inspect` snapshots in source order. For dynamic requests, +the adapter applies `results || 6` and `slices || 30`; typed palette methods do +not receive missing/zero-as-default semantics. + + + +Add palette adapter dispatch with adapter-owned defaults +compat/js-runner.mjs, src/cmd/tinycolor-compat/main.go, tests/port/adapter.test.mjs + +RED: add adapter tests asserting ordered snapshots for complement, split, +triad, tetrad, analogous, and monochromatic; include omitted and explicit-zero +analogous/monochromatic requests. GREEN: add a narrow `palette` dispatch branch +to each runner. Node calls one source method. Go maps dynamic fields using +source `||` truthiness, calls B's methods with concrete values, and serializes +each returned `Color.Inspect()` in order. Reject unknown methods through the +existing error protocol. Do not copy palette calculations into runners, mutate +input values, or add a public polyad operation. Commit `test(04-06): add palette +adapter failures`, then `feat(04-06): dispatch palette operations`. + +node tests/port/adapter.test.mjs; Set-Location src; go test ./cmd/tinycolor-compat ./tinycolor -count=1; Set-Location ..; node compat/run.mjs compat/cases/operations.jsonl +Palette dispatch returns exactly ordered value snapshots and confines zero/default coercion to adapters. + + +Complete palette corpus and execute the Phase 4 full gate +compat/cases/operations.jsonl + +Append deterministic rows for all six palette methods: default red sequences, +custom analogous count/slices, analogous and monochromatic zero defaults, a +non-red hue-wrap case, and alpha retention. Retain all modifier, Mix, and WCAG +rows from earlier plans; do not rewrite their ids or expected request shape. +Run every command in `04-VALIDATION.md` and record no manually edited expected +outputs: `compat/run.mjs` remains the exact comparator and existing mismatch +records retain request, source/Go result, operation, suspected package, and B/C +owner. Commit corpus-only work as `test(04-06): complete palette differential +corpus`; make no production, test, oracle, or top-level planning changes outside +the executor's planned ownership. + +Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl; node compat/run.mjs compat/cases/operations.jsonl; git diff --check; git diff -- mod.js test.js tinycolor.js +All available Phase 1-4 gates pass with zero mismatches; any failure remains a full, owner-assigned reproducer and no Deno suite pass is claimed. + + +Execute the final command exactly as written. A nonzero differential command blocks completion; Deno remains explicitly unavailable. +- OPS-01 covers every palette in exact source order. +- QLT-02 has a complete fixed Phase 4 corpus and preserved owner-assigned mismatch reports. +After completion, create `.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md`. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-RESEARCH.md b/.planning/phases/04-operations-and-palettes/04-RESEARCH.md new file mode 100644 index 00000000..fdf195c9 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-RESEARCH.md @@ -0,0 +1,267 @@ +# Phase 4: Operations and Palettes - Research + +**Researched:** 2026-08-01 +**Domain:** Source-compatible TinyColor operations, WCAG readability, and color palettes in Go +**Confidence:** HIGH + +## User Constraints + +No `04-CONTEXT.md` exists. The phase is constrained by the Phase 4 roadmap goal, `OPS-01` and `QLT-02`, [AGENT.md](../../../AGENT.md), the immutable local [mod.js](../../../mod.js) oracle, and the approved [operations design](../../../docs/superpowers/specs/2026-08-01-phase-4-operations-design.md). + + +## Phase Requirements + +| ID | Description | Research Support | +|---|---|---| +| OPS-01 | Reproduce mutation, utilities, readability, and palette operations. | Exact source algorithms, defaults, return/mutation semantics, palette order, and adapter operation shapes below. | +| QLT-02 | Record every mismatch with complete reproducer and owner. | Fixed JSONL cases run through the existing exact comparator; B owns library results and C owns dispatch/corpus protocol mismatches. | + + + +## Project Constraints (from AGENT.md) + +- `mod.js`, `test.js`, `tinycolor.js`, `npm/`, `dist/`, and `demo/` are immutable JavaScript oracle material. Do not change them to obtain parity. +- New Go behavior belongs under `src/`; compatibility adapters, fixtures, and reports belong under `compat/`. +- Preserve TinyColor quirks, including defaults, validation, clamping, hue wrapping, alpha, mutation, and invalid-as-black behavior. Do not use a broad floating-point tolerance. +- The public Go API is explicit and typed; the JSON compatibility adapter owns JavaScript truthiness/default coercion that cannot be expressed in the typed API. +- B owns `src/tinycolor` operations, readability, and palettes. C owns `compat`, fixture schema, and reproducible differential evidence. +- Required checks include `go test ./...`, `go vet ./...`, the Node oracle runner, and the differential command. Deno is unavailable locally, so a Deno suite pass must not be claimed. + +## Summary + +Phase 4 should extend only `src/tinycolor`: its existing normalized `color.Model` and `Color` conversion methods already provide the required RGBA/HSL/HSV substrate. Implement mutating instance methods as pointer receivers that change the existing `Color`, and implement `Mix`, WCAG operations, and palette methods as pure facade operations returning values. No parser/model or third-party color package is needed. The compatibility runners should only construct input colors, apply one public operation, and serialize observable results. + +The local source is unusually specific about JavaScript defaults. `lighten`, `brighten`, `darken`, `saturate`, `desaturate`, and `mix` preserve a numeric `0` but turn other falsy amounts into their default. `spin` has no default at all: omitted `undefined` becomes invalid black after conversion, while JSON `null` behaves as `0`. `analogous` and `monochromatic` use `value || default`, so JSON `0` selects their defaults. These rules must be confined to compatibility dispatch; typed Go methods should accept explicit numeric amounts/counts and not pretend that omitted and zero are the same API state. + +**Primary recommendation:** make three micro-commit-friendly plans: B implements modifiers and `Mix`, then B implements readability, then B implements palettes; C follows each with thin dual-runner dispatch plus a dedicated exact JSONL corpus. Keep all JS coercion in the adapter and compare deterministic values exactly. + +## Standard Stack + +### Core + +| Library | Version | Purpose | Why Standard | +|---|---:|---|---| +| Go standard library (`math`) | Go 1.26.1 installed | HSL/HSV arithmetic, clamping, and source-compatible rounding | D-005 requires standard-library-first and the oracle algorithms are short. | +| Local `mod.js` and `test.js` | checked-out oracle | Exact behavior and source assertions | D-001 makes this checkout, not a generic color specification, authoritative. | +| Existing JSONL harness | repository-local | Exact Node-to-Go differential validation | It already reports the full request, both results, operation, and owner. | + +### Supporting + +| Dependency | Purpose | When to Use | +|---|---|---| +| `src/internal/color.Model` | normalized RGBA, alpha, validity, format, original input | Read/write it only through `tinycolor.Color`; do not add another color state type. | +| Existing `rgbToHSL` / `rgbToHSV` helpers in `src/tinycolor/color.go` | source-like conversion inputs | Reuse them and add the reciprocal helpers locally in the same facade. | + +**Installation:** None. Do not add a dependency. + +## Exact Source Semantics + +### Mutating modifiers + +Source: [mod.js](../../../mod.js), prototype wrappers around lines 255-321 and algorithms around lines 655-710. + +| Method | Amount rule | Calculation | Bounds and alpha | Receiver behavior | +|---|---|---|---|---| +| `Lighten(amount)` | `amount === 0 ? 0 : amount \|\| 10` | HSL `l += amount / 100` | `l = clamp01(l)`; alpha is preserved | Mutates and returns the same instance. | +| `Darken(amount)` | same | HSL `l -= amount / 100` | `l = clamp01(l)`; alpha preserved | Mutates and returns the same instance. | +| `Saturate(amount)` | same | HSL `s += amount / 100` | `s = clamp01(s)`; alpha preserved | Mutates and returns the same instance. | +| `Desaturate(amount)` | same | HSL `s -= amount / 100` | `s = clamp01(s)`; alpha preserved | Mutates and returns the same instance. | +| `Greyscale()` | no argument | equivalent to `desaturate(100)` | saturation becomes 0; alpha preserved | Mutates and returns the same instance. | +| `Brighten(amount)` | same | rounded RGB delta: `round(255 * amount / 100)` added per channel | every RGB channel clamps to `[0,255]`; alpha preserved | Mutates and returns the same instance. | +| `Spin(amount)` | **no default** | `hue = (h + amount) % 360`; if negative, add 360 | hue wraps; alpha preserved for numeric amount | Mutates and returns the same instance. | + +The wrapper evaluates the free function against `this`, copies the resulting `_r/_g/_b`, then calls `setAlpha` with the result alpha. It leaves source format, original input, validity, and gradient type intact. Go must therefore use pointer receivers such as `func (c *Color) Lighten(amount float64) *Color`; a value receiver would silently fail the required receiver mutation. Keep typed methods numeric and explicit. The adapter must distinguish omitted amount from JSON `0` and must implement the source defaults before calling the typed method. + +Oracle probe results, run against the local source on 2026-08-01: + +- `tinycolor("red").lighten(10)` returns the exact same object and changes it from `#ff0000` to `#ff3333`. +- `tinycolor("red").spin()` produces `#000000` because `undefined` yields `NaN`, which reaches object parsing as invalid input. +- `tinycolor("red").spin(null)` and `.spin(0)` both remain `#ff0000`; JSON cannot convey `undefined`, so an omitted JSON amount must be routed separately from `null`. +- The source exposes no `tinycolor.lighten` static operation (`typeof tinycolor.lighten === "undefined"`). Do not invent static modifier APIs in the parity adapter. + +### `Mix` + +Source: [mod.js](../../../mod.js), lines 771-790; source tests around lines 2013-2078. + +`tinycolor.mix(color1, color2, amount)` uses `amount === 0 ? 0 : amount || 50`, converts both inputs with `toRgb()` first (thus channels are already rounded), sets `p = amount / 100`, and linearly interpolates every `r`, `g`, `b`, **and** `a` as `(second - first) * p + first`. It constructs a fresh TinyColor from that RGBA object. It does not clamp `amount`; out-of-range amounts extrapolate before input normalization clamps RGB and normalizes alpha. It does not mutate either input. + +Use a pure typed function, for example `func Mix(first, second Color, amount float64) Color`, and do the omitted/falsy default only in compatibility dispatch. Cover absent amount (`50`), explicit `0`, `.5` alpha, `90` (the source test's rounding-sensitive case), and out-of-range values. + +### Readability, `IsReadable`, and `MostReadable` + +Source: [mod.js](../../../mod.js), lines 797-871 and `validateWCAG2Parms` around lines 1261-1277; source tests around lines 1100-1370. + +- `readability(a, b)` constructs both colors and returns `(max(luminanceA, luminanceB) + .05) / (min(...) + .05)`. It inherits the existing `Color.Luminance()` rule, which uses rounded RGB channels. +- `isReadable(a, b, wcag2)` defaults invalid/missing options to `AA/small`. The normalized pair is: `level = (parms.level || "AA").toUpperCase()` and `size = (parms.size || "small").toLowerCase()`, then invalid values become `AA` and `small` respectively. Thresholds are `AA small = 4.5`, `AA large = 3`, `AAA small = 7`, and `AAA large = 4.5`; comparisons are inclusive. +- `mostReadable(base, candidates, args)` scans candidates in input order and changes its best only for a **strictly** larger score, so ties retain the first candidate. It returns that candidate when it meets the normalized readability option or when `includeFallbackColors` is falsy. Otherwise it mutates only the local `args` object by setting `includeFallbackColors = false`, then recursively chooses from `[#fff, #000]` using the same `level` and `size`. +- Empty candidates are source-defined but inconvenient: `bestColor` remains `null`; with base `#fff`, both `includeFallbackColors: false` and `true` return `null` because `isReadable(#fff, null)` treats null as the black TinyColor input and succeeds. Preserve this JSON `null` result in adapter tests; do not dereference it or invent a fallback. + +Go ownership should use a typed `WCAG2Options` value (e.g. `Level`, `Size`, `IncludeFallbackColors`) and a normalization helper in `src/tinycolor`. The adapter must preserve source coercion: absent, `null`, `false`, `0`, and empty strings default as JavaScript does; string case is normalized; non-string values must not be prematurely rejected. `MostReadable` needs a nullable typed result, such as `(Color, bool)`, because the source can return null. Serialize `false` only for boolean-or-string source APIs, not this nullable color result. + +### Palettes: defaults, order, and input relationship + +Source: [mod.js](../../../mod.js), lines 713-769; source tests around lines 2080-2150. + +| Operation | Source output order and formula | Defaults / special behavior | +|---|---|---| +| `Complement()` | fresh HSL color with `(h + 180) % 360` | one color; source test confirms input remains unchanged. | +| `SplitComplement()` | `[input, h + 72, h + 216]` | exactly 3 colors, in this order. | +| `Triad()` | `polyad(3)`: `[input, h + 120, h + 240]` | exactly 3 colors. | +| `Tetrad()` | `polyad(4)`: `[input, h + 90, h + 180, h + 270]` | exactly 4 colors. | +| `Analogous(results, slices)` | starts `[input]`; initializes hue to `(h - ((360/slices * results) >> 1) + 720) % 360`, then increments one part for each remaining result | `results \|\| 6`, `slices \|\| 30`; zero therefore yields 6 and 30. Default red order is `ff0000,ff0066,ff0033,ff0000,ff3300,ff6600`. | +| `Monochromatic(results)` | emits HSV `{h,s,v}`, then sets `v = (v + 1/results) % 1` | `results \|\| 6`; zero yields 6. Default red order is `ff0000,2a0000,550000,800000,aa0000,d40000`. | + +`polyad` is intentionally not public in the source because its prototype method is commented out; only use a private Go helper for triad/tetrad. Though `tinycolor(color)` returns the same JavaScript object when the input is already a TinyColor instance, the operation's observable colors are as above. Go's value-returning palette slice should contain independent values, as required by the project design; adapters serialize values only and must not claim JavaScript identity parity. + +## Architecture Patterns and Ownership + +### Recommended project shape + +```text +src/ +├── tinycolor/color.go # B: Color methods, private HSL/HSV/RGBA helpers +├── tinycolor/operations_test.go # B: modifiers, Mix, WCAG, palettes +└── cmd/tinycolor-compat/main.go # C: Go adapter dispatch/coercion only +compat/ +├── js-runner.mjs # C: matching local-oracle operation dispatch +└── cases/operations.jsonl # C: fixed exact Phase 4 corpus +``` + +### Exact ownership boundaries + +| Owner | Owns | Must not do | +|---|---|---| +| B | `Color` pointer modifiers, pure `Mix`, readability/options normalization, palette helpers and methods, direct Go tests | modify parser/model contracts or place JS coercion in public typed APIs. | +| C | operation schema, Node/Go runner whitelists, dynamic JSON coercion/defaults, `operations.jsonl`, adapter protocol tests, mismatch reports | duplicate HSL/HSV/WCAG/palette math in either runner. | +| A | no planned Phase 4 change | absorb operations into parser/model. | + +### Minimal micro-commit plan split + +1. **B: modifiers and Mix** - add reciprocal local conversion helpers only as needed; pointer-receiver modifiers and pure `Mix`; direct tests for same-receiver mutation, defaults applied by a small compatibility-facing helper, zero, clamp, hue wrap, and alpha. +2. **C: modifier/Mix evidence** - add `operation: "modify"` and `operation: "mix"` dispatch to both runners plus initial `operations.jsonl`; extend adapter protocol tests. Commit only fixture/adapter work. +3. **B: readability** - add `Readability`, `IsReadable`, `MostReadable`, a typed normalized options helper, and nullable result handling; direct tests for each WCAG threshold, first-tie selection, recursive black/white fallback, and empty list. +4. **C: readability evidence** - add `readability`, `isReadable`, and `mostReadable` runner operations and their source-derived corpus rows. +5. **B: palettes** - add pure palette methods and a private polyad helper; direct tests for default red sequences, custom analogous counts/slices, zero/default behavior, order, and alpha preservation. +6. **C: palette evidence and phase gate** - add `palette` dispatch, complete deterministic corpus, run all existing and new corpora, then update compatibility evidence only with actual results. + +## Required Adapter Coercion and JSONL Shapes + +The existing runners accept `{id, operation, input, args}` and must retain all prior operations. Add only operation-specific request forms; use `Object.hasOwn(args, "amount")` in Node and an equivalent Go map-presence check so omitted differs from `0` and `null`. + +```json +{"id":"mod-lighten-default","operation":"modify","input":"#80000080","args":{"method":"lighten"}} +{"id":"mod-lighten-zero","operation":"modify","input":"red","args":{"method":"lighten","amount":0}} +{"id":"mod-spin-null","operation":"modify","input":"red","args":{"method":"spin","amount":null}} +{"id":"mod-spin-omitted","operation":"modify","input":"red","args":{"method":"spin"}} +{"id":"mix-alpha-quarter","operation":"mix","input":"transparent","args":{"other":"#000","amount":25}} +{"id":"readability-black-white","operation":"readability","input":"#000","args":{"other":"#fff"}} +{"id":"is-readable-default","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{}}} +{"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}}} +{"id":"most-readable-empty","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}}} +{"id":"palette-analogous-default","operation":"palette","input":"red","args":{"method":"analogous"}} +{"id":"palette-analogous-zero","operation":"palette","input":"red","args":{"method":"analogous","results":0,"slices":0}} +{"id":"palette-triad","operation":"palette","input":"red","args":{"method":"triad"}} +``` + +Recommended result encoding: `modify` and `mix` return an `inspect` snapshot so mutation and alpha are observable; `readability` returns its raw number; `isReadable` returns a boolean; `mostReadable` returns `null` or a snapshot; `palette` returns a JSON array of snapshots in source order. Node must construct one instance for `modify`, call it once, and include both `before` and `after` snapshots plus `sameReceiver: returned === color`. Go must return `sameReceiver: true` only when its pointer method is used; this makes the mutation contract testable without exposing internals. + +Do not coerce typed public Go inputs through JSON rules. Adapter-only rules required for parity are: absent versus present amount, `0` preservation where source uses the strict zero ternary, null passed to `spin`, `||` defaults for analogous/monochromatic, WCAG string case/default handling, `includeFallbackColors` truthiness, and JSON `null` for no most-readable candidate. + +## Don't Hand-Roll + +| Problem | Do not build | Use instead | Why | +|---|---|---|---| +| General color library | external CSS/WCAG/color package | direct algorithms over existing `Color` conversion helpers | A library will not preserve TinyColor's defaults, rounding, null behavior, order, and mutation. | +| Dynamic operation framework | reflection or a second protocol | existing JSONL request/response plus switch dispatch | The current protocol is already stable and exact-comparing. | +| Public JavaScript-like argument API | `any`/variadic methods on `Color` | typed methods plus adapter coercion | D-006 explicitly separates Go usability from source quirks. | +| Public `polyad` | new public method | private helper for `Triad` and `Tetrad` | The source deliberately disables its public polyad method. | + +## Common Pitfalls + +### Defaulting `spin` like the other modifiers + +`spin` has no `|| 10` source expression. An omitted JavaScript value is observably invalid black, while JSON null acts like zero. Test both cases separately. + +### Using Go's rounding for `Brighten` without checking the source boundary + +The source computes `Math.round(255 * amount / 100)` before clamping. Keep this rounding point; do not brighten through HSL or round only at string output. + +### Flattening all defaults into Go zero values + +`0` means “no change” for amount modifiers and `Mix`, but it means default 6/30 for `Analogous` and default 6 for `Monochromatic`. Presence-aware adapter decoding is required. + +### Mutating palettes or returning a reordered sequence + +Palette source order is a public result. `Complement` is non-mutating; the first element of analogous/split/triad/tetrad corresponds to the input color. Pin whole ordered arrays, not sets. + +### Incorrect fallback logic in `MostReadable` + +Fallback occurs only after selecting a best candidate, only when that candidate is not readable, and only when `includeFallbackColors` is truthy. Preserve the strict-greater tie rule and empty-list null behavior. + +### Rounding readability or tolerating float drift + +`readability` returns a raw float and `isReadable` compares that raw ratio inclusively. The harness compares JSON values exactly; do not round or apply an epsilon. + +## Validation Architecture + +### Test framework + +| Property | Value | +|---|---| +| Framework | Go standard `testing`; Node JSONL differential harness; upstream Deno suite unavailable locally | +| Quick library command | `Set-Location src; go test ./tinycolor; Set-Location ..` | +| Quick corpus command | `node compat/run.mjs compat/cases/operations.jsonl` | +| Full phase command | `Set-Location src; go test ./...; go vet ./...; Set-Location ..; node tests/port/adapter.test.mjs; node compat/run.mjs compat/cases/smoke.jsonl; node compat/run.mjs compat/cases/parser-hex-rgb.jsonl; node compat/run.mjs compat/cases/parser.jsonl; node compat/run.mjs compat/cases/conversion.jsonl; node compat/run.mjs compat/cases/operations.jsonl; git diff --check` | + +### Requirements to test map + +| Requirement | Behavior | Test type | Command | Initial state | +|---|---|---|---|---| +| OPS-01 | mutable modifiers: default/zero, clamp, wrap, alpha, same receiver | Go unit + exact differential | `node compat/run.mjs compat/cases/operations.jsonl` | Wave 0 needed | +| OPS-01 | Mix rounding and alpha interpolation | Go unit + exact differential | same | Wave 0 needed | +| OPS-01 | WCAG normalization, thresholds, selection/fallback/null | Go unit + exact differential | same | Wave 0 needed | +| OPS-01 | palettes: defaults, custom options, ordered results | Go unit + exact differential | same | Wave 0 needed | +| QLT-02 | stable complete reproducer | differential integration | same | driver exists; Phase 4 rows/dispatch missing | + +### Required deterministic corpus groups + +- Modifiers: each method with omitted/default, zero, normal, clamp high/low, alpha input, `spin` negative/over-360/omitted/null. +- Mix: omitted 50, zero, 90, transparent alpha progression, and out-of-range amount. +- Readability: ratios `1`, `1.1121078324840545`, `21`; each threshold family; invalid/mixed-case options; first-tie behavior; include-fallback true/false; empty candidate null. +- Palettes: all default red sequences copied from `test.js`; custom analogous `results`/`slices`; zero defaults; a non-red hue-wrapping case; alpha retention. + +### Environment availability + +| Dependency | Required by | Available | Version | Fallback | +|---|---|---:|---|---| +| Node.js | local JS oracle and differential harness | yes | v24.18.0 | none needed | +| Go | library tests and Go adapter | yes | go1.26.1 windows/amd64 | none needed | +| Deno | unchanged source suite | no | — | Node differential evidence only; does not count as a source-suite pass | + +## Sources + +### Primary (HIGH confidence) + +- [mod.js](../../../mod.js) - constructor/prototype mutation wrapper, modifiers, mix, WCAG operations, palette algorithms, and WCAG parameter validator. +- [test.js](../../../test.js) - exhaustive modifier and mix loops; spin cases; WCAG/fallback assertions; default palette order expectations. +- [docs/superpowers/specs/2026-08-01-phase-4-operations-design.md](../../../docs/superpowers/specs/2026-08-01-phase-4-operations-design.md) - approved scope and architectural separation. +- [src/tinycolor/color.go](../../../src/tinycolor/color.go), [src/cmd/tinycolor-compat/main.go](../../../src/cmd/tinycolor-compat/main.go), [compat/js-runner.mjs](../../../compat/js-runner.mjs), and [compat/run.mjs](../../../compat/run.mjs) - current extension surfaces. +- Oracle probe executed 2026-08-01 with Node v24.18.0 - omitted/null spin, zero palette defaults, empty `mostReadable`, static modifier absence, and identity mutation. + +### Secondary (MEDIUM confidence) + +None. The selected local source and tests are the authoritative specification. + +### Tertiary (LOW confidence) + +None. + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - project decisions and installed versions are local evidence. +- Architecture: HIGH - approved Phase 4 design and existing owner boundaries agree. +- Semantics and pitfalls: HIGH - direct local source, source tests, and oracle edge probe. + +**Research date:** 2026-08-01 +**Valid until:** this checkout's `mod.js` or `test.js` changes. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-VALIDATION.md b/.planning/phases/04-operations-and-palettes/04-VALIDATION.md new file mode 100644 index 00000000..b59dc5ba --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-VALIDATION.md @@ -0,0 +1,56 @@ +# Phase 4 Validation + +## Constraints + +- Run commands from `R:\Code\TinyColor` unless the command changes directory. +- The JavaScript oracle, parser, and color model are not Phase 4 edit targets. +- Treat every differential mismatch as a QLT-02 record; do not add float tolerance. +- Deno is unavailable. Do not claim `deno task test` passed. + +## Focused Tasks + +| Plan/task | RED/GREEN command | Differential or adapter command | +|---|---|---| +| 04-01 modifiers/Mix library | `Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1` | Not applicable until 04-02 | +| 04-02 modifiers/Mix adapter/corpus | `node tests/port/adapter.test.mjs` | `node compat/run.mjs compat/cases/operations.jsonl` | +| 04-03 readability library | `Set-Location src; go test ./tinycolor -run 'Test(Readability|IsReadable|MostReadable)' -count=1` | Not applicable until 04-04 | +| 04-04 readability adapter/corpus | `node tests/port/adapter.test.mjs` | `node compat/run.mjs compat/cases/operations.jsonl` | +| 04-05 palettes library | `Set-Location src; go test ./tinycolor -run 'Test(Palette|Complement|Analogous|Monochromatic|Triad|Tetrad)' -count=1` | Not applicable until 04-06 | +| 04-06 palettes adapter/corpus | `node tests/port/adapter.test.mjs` | `node compat/run.mjs compat/cases/operations.jsonl` | + +For every TDD library task, first run its focused Go command after adding the +test and confirm the feature is absent or behaviorally wrong. After the minimal +implementation, rerun it, then run `Set-Location src; go test ./...; go vet +./...` before the task's micro-commit. + +## Final Phase Gate + +```powershell +Set-Location src +go test ./... +go vet ./... +Set-Location .. +node tests/port/adapter.test.mjs +node compat/run.mjs compat/cases/smoke.jsonl +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/parser.jsonl +node compat/run.mjs compat/cases/conversion.jsonl +node compat/run.mjs compat/cases/operations.jsonl +git diff --check +git diff -- mod.js test.js tinycolor.js +``` + +Pass criteria: all Go, adapter, and corpus commands exit zero; the operations +corpus has zero mismatches; `git diff --check` is clean; and the final oracle +diff is empty. If any differential check fails, preserve the exact JSONL row +and harness record (request, JavaScript result, Go result, operation, owner, +and suspected package) rather than changing the oracle or hiding the result. + +## Planned Execution Summaries + +- `04-01-SUMMARY.md`: modifier/Mix typed contracts, direct tests, focused and full Go evidence. +- `04-02-SUMMARY.md`: adapter coercion rules, modifier/Mix corpus count, differential evidence. +- `04-03-SUMMARY.md`: WCAG public contracts, threshold/selection tests, Go evidence. +- `04-04-SUMMARY.md`: options/null adapter behavior, readability corpus count, differential evidence. +- `04-05-SUMMARY.md`: palette contracts/order tests and Go evidence. +- `04-06-SUMMARY.md`: palette adapter/corpus completion and final Phase 4 gate evidence. \ No newline at end of file From e19161c6ffd4717b5bc292da13eabc51057017dc Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:49:20 +0530 Subject: [PATCH 095/165] test(04-01): add failing modifier and mix regressions --- src/tinycolor/operations_test.go | 113 +++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/tinycolor/operations_test.go diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go new file mode 100644 index 00000000..d9592755 --- /dev/null +++ b/src/tinycolor/operations_test.go @@ -0,0 +1,113 @@ +package tinycolor + +import ( + "math" + "testing" +) + +func TestModifiers(t *testing.T) { + red, _ := FromCompat("red", false) + if returned := red.Lighten(10); returned != &red { + t.Fatal("Lighten must return its receiver") + } + if got := red.ToHexString(); got != "#ff3333" { + t.Fatalf("Lighten(10) = %s", got) + } + + for _, modifier := range []struct { + name string + apply func(*Color) *Color + }{ + {"lighten", func(c *Color) *Color { return c.Lighten(0) }}, + {"darken", func(c *Color) *Color { return c.Darken(0) }}, + {"saturate", func(c *Color) *Color { return c.Saturate(0) }}, + {"desaturate", func(c *Color) *Color { return c.Desaturate(0) }}, + {"brighten", func(c *Color) *Color { return c.Brighten(0) }}, + {"spin", func(c *Color) *Color { return c.Spin(0) }}, + } { + t.Run(modifier.name+" zero", func(t *testing.T) { + color, _ := FromCompat("#336699", false) + if modifier.apply(&color) != &color || color.ToHexString() != "#336699" { + t.Fatalf("%s(0) changed %#v", modifier.name, color) + } + }) + } + + white, _ := FromCompat("#fff", false) + white.Lighten(100) + if got := white.ToHexString(); got != "#ffffff" { + t.Fatalf("Lighten clamp = %s", got) + } + black, _ := FromCompat("#000", false) + black.Darken(100) + if got := black.ToHexString(); got != "#000000" { + t.Fatalf("Darken clamp = %s", got) + } + red, _ = FromCompat("red", false) + red.Desaturate(200) + if got := red.ToHexString(); got != "#808080" { + t.Fatalf("Desaturate clamp = %s", got) + } + + black, _ = FromCompat("#000", false) + black.Brighten(.2) + if got := black.ToHexString(); got != "#010101" { + t.Fatalf("Brighten pre-clamp rounding = %s", got) + } + + red, _ = FromCompat("red", false) + red.Spin(-120) + if got := red.ToHexString(); got != "#0000ff" { + t.Fatalf("Spin(-120) = %s", got) + } + red, _ = FromCompat("red", false) + red.Spin(480) + if got := red.ToHexString(); got != "#00ff00" { + t.Fatalf("Spin(480) = %s", got) + } + + color, _ := FromCompatWithOptions("rgba(255, 0, 0, .4)", false, CompatOptions{GradientType: true}) + original, format, alpha := color.Original(), color.Format(), color.Alpha() + if color.Spin(math.NaN()) != &color { + t.Fatal("Spin must return its receiver") + } + if color.ToHexString() != "#000000" || !color.Valid() || color.Format() != format || color.Original() != original || color.Alpha() != alpha { + t.Fatalf("Spin(NaN) did not retain metadata: %#v", color) + } + if got := color.ToFilter(nil, false); got != "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#66000000,endColorstr=#66000000)" { + t.Fatalf("Spin(NaN) gradient metadata = %s", got) + } + + alphaColor, _ := FromCompat("rgba(255, 0, 0, .4)", false) + alphaColor.Greyscale() + if alphaColor.ToHexString() != "#808080" || alphaColor.Alpha() != .4 { + t.Fatalf("Greyscale() = %#v", alphaColor) + } +} + +func TestMix(t *testing.T) { + black, _ := FromCompat("#000", false) + white, _ := FromCompat("#fff", false) + if got := Mix(black, white, 50).ToHexString(); got != "#808080" { + t.Fatalf("Mix(50) = %s", got) + } + if got := Mix(black, white, 0).ToHexString(); got != "#000000" { + t.Fatalf("Mix(0) = %s", got) + } + if got := Mix(white, black, 90).ToHexString(); got != "#1a1a1a" { + t.Fatalf("Mix(90) = %s", got) + } + + transparent, _ := FromCompat("transparent", false) + mixed := Mix(transparent, black, 25) + if mixed.ToHexString() != "#000000" || mixed.Alpha() != .25 { + t.Fatalf("transparent Mix = %#v", mixed) + } + if transparent.Alpha() != 0 || black.ToHexString() != "#000000" { + t.Fatal("Mix mutated an input") + } + + if got := Mix(black, white, 200).ToHexString(); got != "#ffffff" { + t.Fatalf("out-of-range Mix = %s", got) + } +} \ No newline at end of file From 282d73c60e18d4d6bb2edbfc37e6323ba4340026 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 02:56:41 +0530 Subject: [PATCH 096/165] feat(04-01): implement modifiers and mix --- src/tinycolor/color.go | 360 +++++++++++++++++++++++++------ src/tinycolor/operations_test.go | 2 +- 2 files changed, 301 insertions(+), 61 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 0544fd4d..1fd69228 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -4,18 +4,27 @@ package tinycolor import ( "fmt" "math" - "strconv" "math/rand/v2" + "strconv" "github.com/rajeet-04/tinycolor-go/internal/color" "github.com/rajeet-04/tinycolor-go/internal/parser" ) -type Color struct{ model color.Model; gradientType bool } -type CompatOptions struct { Format string; GradientType bool } -type RGB struct { R, G, B int; A float64 } -type HSL struct { H, S, L, A float64 } -type HSV struct { H, S, V, A float64 } +type Color struct { + model color.Model + gradientType bool +} +type CompatOptions struct { + Format string + GradientType bool +} +type RGB struct { + R, G, B int + A float64 +} +type HSL struct{ H, S, L, A float64 } +type HSV struct{ H, S, V, A float64 } func FromCompat(input any, fromRatio bool) (Color, error) { if fromRatio { @@ -23,7 +32,14 @@ func FromCompat(input any, fromRatio bool) (Color, error) { } return Color{model: parser.Parse(input)}, nil } -func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) { c, e := FromCompat(input, fromRatio); if options.Format != "" { c.model.Format = color.Format(options.Format) }; c.gradientType=options.GradientType; return c, e } +func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) { + c, e := FromCompat(input, fromRatio) + if options.Format != "" { + c.model.Format = color.Format(options.Format) + } + c.gradientType = options.GradientType + return c, e +} func (c Color) Valid() bool { return c.model.Valid } func (c Color) Format() string { return string(c.model.Format) } @@ -32,71 +48,188 @@ func (c Color) Original() any { return c.model.Original } func (c Color) RGB() map[string]any { return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} } -func (c Color) ToRGB() RGB { return RGB{int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)), c.model.A} } -func (c Color) ToHSL() HSL { h,s,l:=rgbToHSL(c.model.R,c.model.G,c.model.B); return HSL{h*360,s,l,c.model.A} } -func (c Color) ToHSV() HSV { h,s,v:=rgbToHSV(c.model.R,c.model.G,c.model.B); return HSV{h*360,s,v,c.model.A} } -func (c Color) ToRGBString() string { x:=c.ToRGB(); if x.A==1{return fmt.Sprintf("rgb(%d, %d, %d)",x.R,x.G,x.B)}; return fmt.Sprintf("rgba(%d, %d, %d, %s)",x.R,x.G,x.B,roundedAlpha(x.A)) } -func (c Color) ToPercentageRGB() RGB { x:=c.ToRGB(); return RGB{percent(x.R),percent(x.G),percent(x.B),x.A} } -func (c Color) ToPercentageRGBString() string { x:=c.ToPercentageRGB(); if x.A==1{return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)",x.R,x.G,x.B)};return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)",x.R,x.G,x.B,roundedAlpha(x.A)) } -func (c Color) ToHSLString() string {x:=c.ToHSL(); p:="hsl";if x.A<1{p="hsla"}; s:=fmt.Sprintf("%s(%d, %d%%, %d%%",p,mathRound(x.H),mathRound(x.S*100),mathRound(x.L*100));if x.A<1{s+=", "+roundedAlpha(x.A)};return s+")"} -func (c Color) ToHSVString() string {x:=c.ToHSV(); p:="hsv";if x.A<1{p="hsva"}; s:=fmt.Sprintf("%s(%d, %d%%, %d%%",p,mathRound(x.H),mathRound(x.S*100),mathRound(x.V*100));if x.A<1{s+=", "+roundedAlpha(x.A)};return s+")"} -func (c Color) ToHex() string { return fmt.Sprintf("%02x%02x%02x", mathRound(c.model.R),mathRound(c.model.G),mathRound(c.model.B)) } -func (c Color) ToHexString() string{return "#"+c.ToHex()} -func (c Color) ToHex8() string{return c.ToHex()+fmt.Sprintf("%02x",mathRound(c.model.A*255))} -func (c Color) ToHex8String() string{return "#"+c.ToHex8()} +func (c Color) ToRGB() RGB { + return RGB{int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)), c.model.A} +} +func (c Color) ToHSL() HSL { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + return HSL{h * 360, s, l, c.model.A} +} +func (c Color) ToHSV() HSV { + h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) + return HSV{h * 360, s, v, c.model.A} +} +func (c Color) ToRGBString() string { + x := c.ToRGB() + if x.A == 1 { + return fmt.Sprintf("rgb(%d, %d, %d)", x.R, x.G, x.B) + } + return fmt.Sprintf("rgba(%d, %d, %d, %s)", x.R, x.G, x.B, roundedAlpha(x.A)) +} +func (c Color) ToPercentageRGB() RGB { + x := c.ToRGB() + return RGB{percent(x.R), percent(x.G), percent(x.B), x.A} +} +func (c Color) ToPercentageRGBString() string { + x := c.ToPercentageRGB() + if x.A == 1 { + return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", x.R, x.G, x.B) + } + return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", x.R, x.G, x.B, roundedAlpha(x.A)) +} +func (c Color) ToHSLString() string { + x := c.ToHSL() + p := "hsl" + if x.A < 1 { + p = "hsla" + } + s := fmt.Sprintf("%s(%d, %d%%, %d%%", p, mathRound(x.H), mathRound(x.S*100), mathRound(x.L*100)) + if x.A < 1 { + s += ", " + roundedAlpha(x.A) + } + return s + ")" +} +func (c Color) ToHSVString() string { + x := c.ToHSV() + p := "hsv" + if x.A < 1 { + p = "hsva" + } + s := fmt.Sprintf("%s(%d, %d%%, %d%%", p, mathRound(x.H), mathRound(x.S*100), mathRound(x.V*100)) + if x.A < 1 { + s += ", " + roundedAlpha(x.A) + } + return s + ")" +} +func (c Color) ToHex() string { + return fmt.Sprintf("%02x%02x%02x", mathRound(c.model.R), mathRound(c.model.G), mathRound(c.model.B)) +} +func (c Color) ToHexString() string { return "#" + c.ToHex() } +func (c Color) ToHex8() string { return c.ToHex() + fmt.Sprintf("%02x", mathRound(c.model.A*255)) } +func (c Color) ToHex8String() string { return "#" + c.ToHex8() } func (c Color) ToString(format string) string { return c.toString(format, format != "") } -func (c Color) toString(format string, explicit bool) string { if format==""{format=string(c.model.Format)}; if !explicit&&c.model.A<1&&(format=="hex"||format=="hex6"||format=="hex3"||format=="hex4"||format=="hex8"||format=="name"){if format=="name"&&c.model.A==0{return "transparent"};return c.ToRGBString()}; switch format {case "rgb": return c.ToRGBString();case "prgb":return c.ToPercentageRGBString();case "hsl":return c.ToHSLString();case "hsv":return c.ToHSVString();case "hex","hex6":return c.ToHexString();case "hex8":return c.ToHex8String();case "name":if n,ok:=c.ToName();ok{return n};return c.ToHexString()}; return c.ToHexString() } -func (c Color) ToName()(string,bool){if c.model.A==0{return "transparent",true}; if c.model.A<1{return "",false}; n:=parser.NameForRGB(mathRound(c.model.R),mathRound(c.model.G),mathRound(c.model.B)); return n,n!=""} -func (c Color) ToFilter(second *Color, gradient bool) string { start:="#"+fmt.Sprintf("%02x",mathRound(c.model.A*255))+c.ToHex(); end:=start; if second!=nil {end="#"+fmt.Sprintf("%02x",mathRound(second.model.A*255))+second.ToHex()}; prefix:=""; if gradient||c.gradientType {prefix="GradientType = 1, "}; return "progid:DXImageTransform.Microsoft.gradient("+prefix+"startColorstr="+start+",endColorstr="+end+")" } -func (c Color) Brightness() float64{x:=c.ToRGB();return float64(x.R*299+x.G*587+x.B*114)/1000} -func (c Color) Luminance() float64 { x:=c.ToRGB(); f:=func(v int)float64{z:=float64(v)/255;if z<=.03928{return z/12.92};return math.Pow((z+.055)/1.055,2.4)};return .2126*f(x.R)+.7152*f(x.G)+.0722*f(x.B) } -func (c Color) IsDark()bool{return c.Brightness()<128}; func(c Color)IsLight()bool{return !c.IsDark()} -func (c Color) Clone() Color { n,_:=FromCompat(c.String(),false);return n } -func Equals(a,b any)bool{if a==nil||b==nil{return false};x,_:=FromCompat(a,false);y,_:=FromCompat(b,false);return x.ToRGBString()==y.ToRGBString()} -func Random() Color { return Color{model: color.Model{R:rand.Float64()*255,G:rand.Float64()*255,B:rand.Float64()*255,A:1,Valid:true,Format:color.FormatRGB}} } -func mathRound(v float64)int{return int(math.Floor(v+.5))} +func (c Color) toString(format string, explicit bool) string { + if format == "" { + format = string(c.model.Format) + } + if !explicit && c.model.A < 1 && (format == "hex" || format == "hex6" || format == "hex3" || format == "hex4" || format == "hex8" || format == "name") { + if format == "name" && c.model.A == 0 { + return "transparent" + } + return c.ToRGBString() + } + switch format { + case "rgb": + return c.ToRGBString() + case "prgb": + return c.ToPercentageRGBString() + case "hsl": + return c.ToHSLString() + case "hsv": + return c.ToHSVString() + case "hex", "hex6": + return c.ToHexString() + case "hex8": + return c.ToHex8String() + case "name": + if n, ok := c.ToName(); ok { + return n + } + return c.ToHexString() + } + return c.ToHexString() +} +func (c Color) ToName() (string, bool) { + if c.model.A == 0 { + return "transparent", true + } + if c.model.A < 1 { + return "", false + } + n := parser.NameForRGB(mathRound(c.model.R), mathRound(c.model.G), mathRound(c.model.B)) + return n, n != "" +} +func (c Color) ToFilter(second *Color, gradient bool) string { + start := "#" + fmt.Sprintf("%02x", mathRound(c.model.A*255)) + c.ToHex() + end := start + if second != nil { + end = "#" + fmt.Sprintf("%02x", mathRound(second.model.A*255)) + second.ToHex() + } + prefix := "" + if gradient || c.gradientType { + prefix = "GradientType = 1, " + } + return "progid:DXImageTransform.Microsoft.gradient(" + prefix + "startColorstr=" + start + ",endColorstr=" + end + ")" +} +func (c Color) Brightness() float64 { x := c.ToRGB(); return float64(x.R*299+x.G*587+x.B*114) / 1000 } +func (c Color) Luminance() float64 { + x := c.ToRGB() + f := func(v int) float64 { + z := float64(v) / 255 + if z <= .03928 { + return z / 12.92 + } + return math.Pow((z+.055)/1.055, 2.4) + } + return .2126*f(x.R) + .7152*f(x.G) + .0722*f(x.B) +} +func (c Color) IsDark() bool { return c.Brightness() < 128 } +func (c Color) IsLight() bool { return !c.IsDark() } +func (c Color) Clone() Color { n, _ := FromCompat(c.String(), false); return n } +func Equals(a, b any) bool { + if a == nil || b == nil { + return false + } + x, _ := FromCompat(a, false) + y, _ := FromCompat(b, false) + return x.ToRGBString() == y.ToRGBString() +} +func Random() Color { + return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatRGB}} +} +func mathRound(v float64) int { return int(math.Floor(v + .5)) } // String supplies the minimal source-compatible string snapshot used by the // JSONL oracle. Full public output APIs remain Phase 3 work. func (c Color) String() string { return c.toString("", false) /* - r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) - if c.model.Format == color.FormatName { - if c.model.A == 0 { - return "transparent" + r, g, b := int(math.Round(c.model.R)), int(math.Round(c.model.G)), int(math.Round(c.model.B)) + if c.model.Format == color.FormatName { + if c.model.A == 0 { + return "transparent" + } + if name := parser.NameForRGB(r, g, b); name != "" { + return name + } } - if name := parser.NameForRGB(r, g, b); name != "" { - return name - } - } - if c.model.Format == color.FormatPRGB { - if c.model.A < 1 { - return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", percent(r), percent(g), percent(b), roundedAlpha(c.model.A)) + if c.model.Format == color.FormatPRGB { + if c.model.A < 1 { + return fmt.Sprintf("rgba(%d%%, %d%%, %d%%, %s)", percent(r), percent(g), percent(b), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", percent(r), percent(g), percent(b)) } - return fmt.Sprintf("rgb(%d%%, %d%%, %d%%)", percent(r), percent(g), percent(b)) - } - if c.model.Format == color.FormatHSL { - h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) - if c.model.A < 1 { - return fmt.Sprintf("hsla(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100)), roundedAlpha(c.model.A)) + if c.model.Format == color.FormatHSL { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsla(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsl(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100))) } - return fmt.Sprintf("hsl(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(l*100))) - } - if c.model.Format == color.FormatHSV { - h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) - if c.model.A < 1 { - return fmt.Sprintf("hsva(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100)), roundedAlpha(c.model.A)) + if c.model.Format == color.FormatHSV { + h, s, v := rgbToHSV(c.model.R, c.model.G, c.model.B) + if c.model.A < 1 { + return fmt.Sprintf("hsva(%d, %d%%, %d%%, %s)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100)), roundedAlpha(c.model.A)) + } + return fmt.Sprintf("hsv(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100))) } - return fmt.Sprintf("hsv(%d, %d%%, %d%%)", int(math.Round(h*360)), int(math.Round(s*100)), int(math.Round(v*100))) - } - if c.model.Format == color.FormatRGB || c.model.A < 1 { - if c.model.A == 1 { - return fmt.Sprintf("rgb(%d, %d, %d)", r, g, b) + if c.model.Format == color.FormatRGB || c.model.A < 1 { + if c.model.A == 1 { + return fmt.Sprintf("rgb(%d, %d, %d)", r, g, b) + } + return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) } - return fmt.Sprintf("rgba(%d, %d, %d, %s)", r, g, b, roundedAlpha(c.model.A)) - } - return fmt.Sprintf("#%02x%02x%02x", r, g, b) + return fmt.Sprintf("#%02x%02x%02x", r, g, b) */ } @@ -158,3 +291,110 @@ func rgbToHSV(r, g, b float64) (float64, float64, float64) { } return h / 6, d / max, max } + +func (c *Color) Lighten(amount float64) *Color { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + c.setHSL(h, s, clamp01(l+amount/100)) + return c +} + +func (c *Color) Darken(amount float64) *Color { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + c.setHSL(h, s, clamp01(l-amount/100)) + return c +} + +func (c *Color) Saturate(amount float64) *Color { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + c.setHSL(h, clamp01(s+amount/100), l) + return c +} + +func (c *Color) Desaturate(amount float64) *Color { + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + c.setHSL(h, clamp01(s-amount/100), l) + return c +} + +func (c *Color) Greyscale() *Color { return c.Desaturate(100) } + +func (c *Color) Brighten(amount float64) *Color { + delta := float64(mathRound(255 * amount / 100)) + c.model.R = clampChannel(c.model.R + delta) + c.model.G = clampChannel(c.model.G + delta) + c.model.B = clampChannel(c.model.B + delta) + return c +} + +func (c *Color) Spin(amount float64) *Color { + if math.IsNaN(amount) { + c.model.R, c.model.G, c.model.B = 0, 0, 0 + return c + } + h, s, l := rgbToHSL(c.model.R, c.model.G, c.model.B) + h = math.Mod(h*360+amount, 360) + if h < 0 { + h += 360 + } + c.setHSL(h/360, s, l) + return c +} + +func Mix(first, second Color, amount float64) Color { + p := amount / 100 + firstRGB, secondRGB := first.ToRGB(), second.ToRGB() + return Color{model: color.Model{ + R: clampChannel((float64(secondRGB.R)-float64(firstRGB.R))*p + float64(firstRGB.R)), + G: clampChannel((float64(secondRGB.G)-float64(firstRGB.G))*p + float64(firstRGB.G)), + B: clampChannel((float64(secondRGB.B)-float64(firstRGB.B))*p + float64(firstRGB.B)), + A: clampAlpha((second.model.A-first.model.A)*p + first.model.A), + Valid: true, + Format: color.FormatRGB, + }} +} + +func (c *Color) setHSL(h, s, l float64) { + c.model.R, c.model.G, c.model.B = hslToRGB(h, s, l) +} + +func hslToRGB(h, s, l float64) (float64, float64, float64) { + if s == 0 { + channel := l * 255 + return channel, channel, channel + } + var q float64 + if l < .5 { + q = l * (1 + s) + } else { + q = l + s - l*s + } + p := 2*l - q + channel := func(t float64) float64 { + if t < 0 { + t++ + } + if t > 1 { + t-- + } + switch { + case t*6 < 1: + return p + (q-p)*6*t + case t*2 < 1: + return q + case t*3 < 2: + return p + (q-p)*(2.0/3.0-t)*6 + default: + return p + } + } + return channel(h+1.0/3.0) * 255, channel(h) * 255, channel(h-1.0/3.0) * 255 +} + +func clamp01(value float64) float64 { return math.Min(1, math.Max(0, value)) } +func clampChannel(value float64) float64 { return math.Min(255, math.Max(0, value)) } +func clampAlpha(value float64) float64 { + if value < 0 || value > 1 || math.IsNaN(value) { + return 1 + } + return value +} diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index d9592755..df0c028a 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -110,4 +110,4 @@ func TestMix(t *testing.T) { if got := Mix(black, white, 200).ToHexString(); got != "#ffffff" { t.Fatalf("out-of-range Mix = %s", got) } -} \ No newline at end of file +} From 38deb7902fa006fe4ec76b6c292e9484bfff2524 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:02:02 +0530 Subject: [PATCH 097/165] test(04-02): add modifier adapter failures - Cover modify defaults, zero, spin null, and receiver identity\n- Cover mix response snapshots and unsupported methods --- tests/port/adapter.test.mjs | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/port/adapter.test.mjs b/tests/port/adapter.test.mjs index db49297b..44ebfb88 100644 --- a/tests/port/adapter.test.mjs +++ b/tests/port/adapter.test.mjs @@ -28,3 +28,45 @@ for (const response of [malformed, unknown]) { assert.notEqual(Object.hasOwn(response, "result"), Object.hasOwn(response, "error")); assert.equal(typeof response.error, "string"); } + +const [lightenDefault, lightenZero, spinOmitted, spinNull, mixed, badModifier] = run([ + '{"id":"lighten-default","operation":"modify","input":"red","args":{"method":"lighten"}}', + '{"id":"lighten-zero","operation":"modify","input":"red","args":{"method":"lighten","amount":0}}', + '{"id":"spin-omitted","operation":"modify","input":"red","args":{"method":"spin"}}', + '{"id":"spin-null","operation":"modify","input":"red","args":{"method":"spin","amount":null}}', + '{"id":"mix-default","operation":"mix","input":"red","args":{"other":"#000"}}', + '{"id":"bad-modifier","operation":"modify","input":"red","args":{"method":"unknown"}}', +].join("\n")); + +assert.deepEqual(lightenDefault, { + id: "lighten-default", + result: { + before: success.result, + after: { + ...success.result, + rgb: { r: 255, g: 51, b: 51, a: 1 }, + value: "#ff3333", + }, + sameReceiver: true, + }, +}); +assert.deepEqual(lightenZero.result, { + before: success.result, + after: success.result, + sameReceiver: true, +}); +assert.equal(spinOmitted.result.after.value, "black"); +assert.equal(spinOmitted.result.after.original, "red"); +assert.deepEqual(spinNull.result, lightenZero.result); +assert.deepEqual(mixed, { + id: "mix-default", + result: { + valid: true, + format: "rgb", + alpha: 1, + rgb: { r: 128, g: 0, b: 0, a: 1 }, + value: "rgb(128, 0, 0)", + original: { r: 127.5, g: 0, b: 0, a: 1 }, + }, +}); +assert.deepEqual(badModifier, { id: "bad-modifier", error: "unsupported method" }); From e587aefff14850719c3b2185decb9acc924f7566 Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 03:02:59 +0530 Subject: [PATCH 098/165] feat(04-02): dispatch modifiers and mix - Preserve JavaScript argument defaults at the adapter boundary\n- Serialize mutation identity and Mix snapshots --- compat/js-runner.mjs | 22 +++++++++++ src/cmd/tinycolor-compat/main.go | 68 +++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index 81189767..dab7a120 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -36,6 +36,10 @@ const handle = (request) => { return respond(request.id, tinycolor.equals(request.input, args.other)); case "clone": return respond(request.id, inspect(color().clone())); + case "modify": + return modify(request.id, color(), args); + case "mix": + return respond(request.id, inspect(tinycolor.mix(request.input, args.other, amount(args, 50)))); case "randomInvariant": return randomInvariant(request.id); default: @@ -43,6 +47,24 @@ const handle = (request) => { } }; +const amount = (args, defaultAmount) => args.amount === 0 ? 0 : args.amount || defaultAmount; + +const modify = (id, color, args) => { + const before = inspect(color); + let returned; + switch (args.method) { + case "lighten": returned = color.lighten(amount(args, 10)); break; + case "brighten": returned = color.brighten(amount(args, 10)); break; + case "darken": returned = color.darken(amount(args, 10)); break; + case "saturate": returned = color.saturate(amount(args, 10)); break; + case "desaturate": returned = color.desaturate(amount(args, 10)); break; + case "greyscale": returned = color.greyscale(); break; + case "spin": returned = Object.hasOwn(args, "amount") ? color.spin(args.amount) : color.spin(); break; + default: return respond(id, undefined, "unsupported method"); + } + return respond(id, { before, after: inspect(color), sameReceiver: returned === color }); +}; + const output = (id, color, args) => { let result; switch (args.method) { diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 57ecd0f3..e5820f86 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -3,6 +3,7 @@ package main import ( "bufio" "fmt" + "math" "os" "github.com/rajeet-04/tinycolor-go/internal/compat" @@ -37,7 +38,7 @@ func handle(request compat.Request) compat.Response { color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": color, err = tinycolor.FromCompat(request.Input, true) - case "output", "analysis", "clone": + case "output", "analysis", "clone", "modify", "mix": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) case "equals", "randomInvariant": default: @@ -60,6 +61,10 @@ func handle(request compat.Request) compat.Response { case "clone": response, _ := compat.Success(request.ID, color.Clone().Inspect()) return response + case "modify": + return modify(request.ID, &color, args) + case "mix": + return mix(request.ID, color, args) case "equals": response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) return response @@ -77,6 +82,67 @@ func handle(request compat.Request) compat.Response { return response } +func modify(id string, color *tinycolor.Color, args map[string]any) compat.Response { + before := color.Inspect() + method, _ := args["method"].(string) + var returned *tinycolor.Color + switch method { + case "lighten": + returned = color.Lighten(defaultAmount(args, 10)) + case "brighten": + returned = color.Brighten(defaultAmount(args, 10)) + case "darken": + returned = color.Darken(defaultAmount(args, 10)) + case "saturate": + returned = color.Saturate(defaultAmount(args, 10)) + case "desaturate": + returned = color.Desaturate(defaultAmount(args, 10)) + case "greyscale": + returned = color.Greyscale() + case "spin": + if _, ok := args["amount"]; ok { + returned = color.Spin(number(args["amount"])) + } else { + returned = color.Spin(math.NaN()) + } + default: + response, _ := compat.Failure(id, "unsupported method") + return response + } + response, _ := compat.Success(id, map[string]any{ + "before": before, + "after": color.Inspect(), + "sameReceiver": returned == color, + }) + return response +} + +func mix(id string, first tinycolor.Color, args map[string]any) compat.Response { + second, err := tinycolor.FromCompat(args["other"], false) + if err != nil { + response, _ := compat.Failure(id, err.Error()) + return response + } + response, _ := compat.Success(id, tinycolor.Mix(first, second, defaultAmount(args, 50)).Inspect()) + return response +} + +func defaultAmount(args map[string]any, fallback float64) float64 { + if value, ok := args["amount"]; ok { + amount := number(value) + if amount == 0 { + return 0 + } + return amount + } + return fallback +} + +func number(value any) float64 { + amount, _ := value.(float64) + return amount +} + func options(args map[string]any) tinycolor.CompatOptions { options, _ := args["options"].(map[string]any) format, _ := options["format"].(string) From dce65376268aa8109b7b9c84ed58f5a8984de20c Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 03:03:40 +0530 Subject: [PATCH 099/165] test(04-02): add modifier and mix differential corpus - Add deterministic modifier and Mix reproducer rows\n- Preserve fixed ownership metadata in mismatch reports --- compat/cases/operations.jsonl | 32 ++++++++++++++++++++++++++++++++ compat/run.mjs | 5 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 compat/cases/operations.jsonl diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl new file mode 100644 index 00000000..5fe866d6 --- /dev/null +++ b/compat/cases/operations.jsonl @@ -0,0 +1,32 @@ +{"id":"mod-lighten-normal","operation":"modify","input":"#804020","args":{"method":"lighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-lighten-zero","operation":"modify","input":"#804020","args":{"method":"lighten","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-lighten-clamp","operation":"modify","input":"#fefefe","args":{"method":"lighten","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-lighten-alpha","operation":"modify","input":"#80402080","args":{"method":"lighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-normal","operation":"modify","input":"#804020","args":{"method":"brighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-zero","operation":"modify","input":"#804020","args":{"method":"brighten","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-brighten-clamp","operation":"modify","input":"#fefefe","args":{"method":"brighten","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-alpha","operation":"modify","input":"#80402080","args":{"method":"brighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-darken-normal","operation":"modify","input":"#804020","args":{"method":"darken","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-darken-zero","operation":"modify","input":"#804020","args":{"method":"darken","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-darken-clamp","operation":"modify","input":"#010101","args":{"method":"darken","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-darken-alpha","operation":"modify","input":"#80402080","args":{"method":"darken","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-saturate-normal","operation":"modify","input":"#808040","args":{"method":"saturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-saturate-zero","operation":"modify","input":"#808040","args":{"method":"saturate","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-saturate-clamp","operation":"modify","input":"#808040","args":{"method":"saturate","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-saturate-alpha","operation":"modify","input":"#80804080","args":{"method":"saturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-desaturate-normal","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-desaturate-zero","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-desaturate-clamp","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-desaturate-alpha","operation":"modify","input":"#80402080","args":{"method":"desaturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-greyscale-normal","operation":"modify","input":"#804020","args":{"method":"greyscale"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-greyscale-alpha","operation":"modify","input":"#80402080","args":{"method":"greyscale"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-spin-negative","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":-120},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-spin-over-360","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":480},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-spin-omitted","operation":"modify","input":"#ff0000","args":{"method":"spin"},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-spin-null","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":null},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-default","operation":"mix","input":"#ff0000","args":{"other":"#000000"},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-zero","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-ninety","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":90},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mix-transparent-alpha","operation":"mix","input":"transparent","args":{"other":"#00000080","amount":25},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mix-out-of-range-high","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":150},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mix-out-of-range-low","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":-50},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} \ No newline at end of file diff --git a/compat/run.mjs b/compat/run.mjs index 510a208c..db9e8783 100644 --- a/compat/run.mjs +++ b/compat/run.mjs @@ -22,7 +22,8 @@ function same(left, right) { let passed = 0; let mismatches = 0; -for (const request of cases) { +for (const row of cases) { + const { owner, suspectedPackage, ...request } = row; const js = run(process.execPath, ["compat/js-runner.mjs"], root, request); const go = run("go", ["run", "./cmd/tinycolor-compat"], resolve(root, "src"), request); if (same(js, go)) { @@ -31,7 +32,7 @@ for (const request of cases) { } mismatches++; const protocol = Object.hasOwn(js, "result") !== Object.hasOwn(go, "result"); - console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: protocol ? "compat" : "rajeet-04", suspectedPackage: protocol ? "compat" : "src/tinycolor" })); + console.log(JSON.stringify({ case: request.id, operation: request.operation, request, javascript: js, go, owner: owner ?? (protocol ? "compat" : "rajeet-04"), suspectedPackage: suspectedPackage ?? (protocol ? "compat" : "src/tinycolor") })); } console.log(`cases: ${cases.length}`); From 6bfb9ca41b0139c22a4bc0c2fb8690b71c6c1cf9 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 03:05:23 +0530 Subject: [PATCH 100/165] fix(04-02): preserve mix original metadata --- src/tinycolor/color.go | 19 +++++++++++++------ src/tinycolor/operations_test.go | 6 +++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 1fd69228..fde11847 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -343,13 +343,20 @@ func (c *Color) Spin(amount float64) *Color { func Mix(first, second Color, amount float64) Color { p := amount / 100 firstRGB, secondRGB := first.ToRGB(), second.ToRGB() + raw := map[string]float64{ + "r": (float64(secondRGB.R)-float64(firstRGB.R))*p + float64(firstRGB.R), + "g": (float64(secondRGB.G)-float64(firstRGB.G))*p + float64(firstRGB.G), + "b": (float64(secondRGB.B)-float64(firstRGB.B))*p + float64(firstRGB.B), + "a": (second.model.A-first.model.A)*p + first.model.A, + } return Color{model: color.Model{ - R: clampChannel((float64(secondRGB.R)-float64(firstRGB.R))*p + float64(firstRGB.R)), - G: clampChannel((float64(secondRGB.G)-float64(firstRGB.G))*p + float64(firstRGB.G)), - B: clampChannel((float64(secondRGB.B)-float64(firstRGB.B))*p + float64(firstRGB.B)), - A: clampAlpha((second.model.A-first.model.A)*p + first.model.A), - Valid: true, - Format: color.FormatRGB, + R: clampChannel(raw["r"]), + G: clampChannel(raw["g"]), + B: clampChannel(raw["b"]), + A: clampAlpha(raw["a"]), + Valid: true, + Format: color.FormatRGB, + Original: raw, }} } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index df0c028a..18c26f66 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -88,9 +88,13 @@ func TestModifiers(t *testing.T) { func TestMix(t *testing.T) { black, _ := FromCompat("#000", false) white, _ := FromCompat("#fff", false) - if got := Mix(black, white, 50).ToHexString(); got != "#808080" { + mixedHalf := Mix(black, white, 50) + if got := mixedHalf.ToHexString(); got != "#808080" { t.Fatalf("Mix(50) = %s", got) } + if got, ok := mixedHalf.Original().(map[string]float64); !ok || got["r"] != 127.5 || got["g"] != 127.5 || got["b"] != 127.5 || got["a"] != 1 { + t.Fatalf("Mix(50) original = %#v", mixedHalf.Original()) + } if got := Mix(black, white, 0).ToHexString(); got != "#000000" { t.Fatalf("Mix(0) = %s", got) } From a63fc93060cb8562f2a87b2a3dd8d4061e473170 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:05:57 +0530 Subject: [PATCH 101/165] test(04-03): add failing readability regressions --- src/tinycolor/operations_test.go | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 18c26f66..ce6396e9 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -115,3 +115,61 @@ func TestMix(t *testing.T) { t.Fatalf("out-of-range Mix = %s", got) } } + +func TestReadability(t *testing.T) { + black, _ := FromCompat("#000", false) + white, _ := FromCompat("#fff", false) + if got := Readability(black, black); got != 1 { + t.Fatalf("same-color readability = %v", got) + } + if got := Readability(black, white); got != 21 { + t.Fatalf("black-on-white readability = %v", got) + } +} + +func TestIsReadable(t *testing.T) { + for _, test := range []struct { + name string + ratio float64 + options WCAG2Options + expected bool + }{ + {"AA small threshold", 4.5, WCAG2Options{Level: "AA", Size: "small"}, true}, + {"AA large threshold", 3, WCAG2Options{Level: "AA", Size: "large"}, true}, + {"AAA small threshold", 7, WCAG2Options{Level: "AAA", Size: "small"}, true}, + {"AAA large threshold", 4.5, WCAG2Options{Level: "AAA", Size: "large"}, true}, + {"below threshold", 4.499999999999999, WCAG2Options{Level: "AA", Size: "small"}, false}, + {"mixed case", 7, WCAG2Options{Level: "aaa", Size: "LARGE"}, true}, + {"invalid values default", 4.5, WCAG2Options{Level: "invalid", Size: "invalid"}, true}, + } { + t.Run(test.name, func(t *testing.T) { + if got := isReadableRatio(test.ratio, test.options); got != test.expected { + t.Fatalf("isReadableRatio(%v, %#v) = %t", test.ratio, test.options, got) + } + }) + } +} + +func TestMostReadable(t *testing.T) { + base, _ := FromCompat("#000", false) + first, _ := FromCompat("white", false) + second, _ := FromCompat("#fff", false) + if got, ok := MostReadable(base, []Color{first, second}, WCAG2Options{}); !ok || got.Original() != "white" { + t.Fatalf("first tied candidate = %#v, %t", got, ok) + } + + gray, _ := FromCompat("#777", false) + matchingGray, _ := FromCompat("#777", false) + if got, ok := MostReadable(gray, []Color{matchingGray}, WCAG2Options{IncludeFallbackColors: true}); !ok || got.ToHexString() != "#000000" { + t.Fatalf("fallback candidate = %#v, %t", got, ok) + } + if got, ok := MostReadable(gray, []Color{matchingGray}, WCAG2Options{}); !ok || got.ToHexString() != "#777777" { + t.Fatalf("fallback-disabled candidate = %#v, %t", got, ok) + } + if _, ok := MostReadable(base, nil, WCAG2Options{}); ok { + t.Fatal("empty candidates without fallback must have no result") + } + if _, ok := MostReadable(base, nil, WCAG2Options{IncludeFallbackColors: true}); ok { + t.Fatal("empty candidates with fallback must have no result") + } +} From 4bb38342c192e882f3f1e4c0cd20bd113ac60883 Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 03:06:21 +0530 Subject: [PATCH 102/165] feat(04-03): implement readability operations --- src/tinycolor/color.go | 54 ++++++++++++++++++++++++++++++++ src/tinycolor/operations_test.go | 8 ++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index fde11847..1e1939b9 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -6,6 +6,7 @@ import ( "math" "math/rand/v2" "strconv" + "strings" "github.com/rajeet-04/tinycolor-go/internal/color" "github.com/rajeet-04/tinycolor-go/internal/parser" @@ -25,6 +26,11 @@ type RGB struct { } type HSL struct{ H, S, L, A float64 } type HSV struct{ H, S, V, A float64 } +type WCAG2Options struct { + Level string + Size string + IncludeFallbackColors bool +} func FromCompat(input any, fromRatio bool) (Color, error) { if fromRatio { @@ -173,6 +179,54 @@ func (c Color) Luminance() float64 { } return .2126*f(x.R) + .7152*f(x.G) + .0722*f(x.B) } +func Readability(first, second Color) float64 { + firstLuminance, secondLuminance := first.Luminance(), second.Luminance() + return (math.Max(firstLuminance, secondLuminance) + .05) / (math.Min(firstLuminance, secondLuminance) + .05) +} +func IsReadable(first, second Color, options WCAG2Options) bool { + return isReadableRatio(Readability(first, second), options) +} +func MostReadable(base Color, candidates []Color, options WCAG2Options) (Color, bool) { + if len(candidates) == 0 { + return Color{}, false + } + bestColor := candidates[0] + bestScore := Readability(base, bestColor) + for _, candidate := range candidates[1:] { + if score := Readability(base, candidate); score > bestScore { + bestScore, bestColor = score, candidate + } + } + if IsReadable(base, bestColor, options) || !options.IncludeFallbackColors { + return bestColor, true + } + white, _ := FromCompat("#fff", false) + black, _ := FromCompat("#000", false) + return MostReadable(base, []Color{white, black}, WCAG2Options{Level: options.Level, Size: options.Size}) +} +func isReadableRatio(ratio float64, options WCAG2Options) bool { + options = normalizeWCAG2Options(options) + switch options.Level + options.Size { + case "AAsmall", "AAAlarge": + return ratio >= 4.5 + case "AAlarge": + return ratio >= 3 + case "AAAsmall": + return ratio >= 7 + } + return false +} +func normalizeWCAG2Options(options WCAG2Options) WCAG2Options { + options.Level = strings.ToUpper(options.Level) + options.Size = strings.ToLower(options.Size) + if options.Level != "AA" && options.Level != "AAA" { + options.Level = "AA" + } + if options.Size != "small" && options.Size != "large" { + options.Size = "small" + } + return options +} func (c Color) IsDark() bool { return c.Brightness() < 128 } func (c Color) IsLight() bool { return !c.IsDark() } func (c Color) Clone() Color { n, _ := FromCompat(c.String(), false); return n } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index ce6396e9..e4dc1fb1 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -129,10 +129,10 @@ func TestReadability(t *testing.T) { func TestIsReadable(t *testing.T) { for _, test := range []struct { - name string - ratio float64 - options WCAG2Options - expected bool + name string + ratio float64 + options WCAG2Options + expected bool }{ {"AA small threshold", 4.5, WCAG2Options{Level: "AA", Size: "small"}, true}, {"AA large threshold", 3, WCAG2Options{Level: "AA", Size: "large"}, true}, From 92a55da06270cd9b589b174def908b89da8d8e43 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 03:06:54 +0530 Subject: [PATCH 103/165] test(04-04): add readability adapter failures --- tests/port/adapter.test.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/port/adapter.test.mjs b/tests/port/adapter.test.mjs index 44ebfb88..0a699ecb 100644 --- a/tests/port/adapter.test.mjs +++ b/tests/port/adapter.test.mjs @@ -70,3 +70,21 @@ assert.deepEqual(mixed, { }, }); assert.deepEqual(badModifier, { id: "bad-modifier", error: "unsupported method" }); + +const [readability, readableDefault, readableMixedCase, readableInvalid, fallback, fallbackDisabled, emptyCandidates] = run([ + '{"id":"readability","operation":"readability","input":"#000","args":{"other":"#fff"}}', + '{"id":"readable-default","operation":"isReadable","input":"#777","args":{"other":"#000","options":{}}}', + '{"id":"readable-mixed","operation":"isReadable","input":"#000","args":{"other":"#fff","options":{"level":"aaa","size":"LARGE"}}}', + '{"id":"readable-invalid","operation":"isReadable","input":"#777","args":{"other":"#000","options":{"level":false,"size":0}}}', + '{"id":"fallback","operation":"mostReadable","input":"#777","args":{"candidates":["#777"],"options":{"includeFallbackColors":true}}}', + '{"id":"fallback-disabled","operation":"mostReadable","input":"#777","args":{"candidates":["#777"],"options":{"includeFallbackColors":false}}}', + '{"id":"empty-candidates","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}}}', +].join("\n")); + +assert.equal(readability.result, 21); +assert.equal(readableDefault.result, true); +assert.equal(readableMixedCase.result, true); +assert.equal(readableInvalid.result, true); +assert.equal(fallback.result.value, "#000000"); +assert.equal(fallbackDisabled.result.value, "#777777"); +assert.equal(emptyCandidates.result, null); From c9af2c5186e5237ad1ab9950e148f36de0fb456f Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 03:07:16 +0530 Subject: [PATCH 104/165] feat(04-04): dispatch readability operations --- compat/js-runner.mjs | 8 ++++++ src/cmd/tinycolor-compat/main.go | 47 +++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index dab7a120..cfbfbdcf 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -40,6 +40,14 @@ const handle = (request) => { return modify(request.id, color(), args); case "mix": return respond(request.id, inspect(tinycolor.mix(request.input, args.other, amount(args, 50)))); + case "readability": + return respond(request.id, tinycolor.readability(request.input, args.other)); + case "isReadable": + return respond(request.id, tinycolor.isReadable(request.input, args.other, args.options)); + case "mostReadable": { + const result = tinycolor.mostReadable(request.input, args.candidates ?? [], args.options); + return respond(request.id, result ? inspect(result) : null); + } case "randomInvariant": return randomInvariant(request.id); default: diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index e5820f86..1a26cf59 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -38,7 +38,7 @@ func handle(request compat.Request) compat.Response { color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": color, err = tinycolor.FromCompat(request.Input, true) - case "output", "analysis", "clone", "modify", "mix": + case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) case "equals", "randomInvariant": default: @@ -65,6 +65,12 @@ func handle(request compat.Request) compat.Response { return modify(request.ID, &color, args) case "mix": return mix(request.ID, color, args) + case "readability": + return readability(request.ID, color, args) + case "isReadable": + return isReadable(request.ID, color, args) + case "mostReadable": + return mostReadable(request.ID, color, args) case "equals": response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) return response @@ -127,6 +133,34 @@ func mix(id string, first tinycolor.Color, args map[string]any) compat.Response return response } +func readability(id string, first tinycolor.Color, args map[string]any) compat.Response { + second, _ := tinycolor.FromCompat(args["other"], false) + response, _ := compat.Success(id, tinycolor.Readability(first, second)) + return response +} + +func isReadable(id string, first tinycolor.Color, args map[string]any) compat.Response { + second, _ := tinycolor.FromCompat(args["other"], false) + response, _ := compat.Success(id, tinycolor.IsReadable(first, second, wcagOptions(args))) + return response +} + +func mostReadable(id string, base tinycolor.Color, args map[string]any) compat.Response { + inputs, _ := args["candidates"].([]any) + candidates := make([]tinycolor.Color, 0, len(inputs)) + for _, input := range inputs { + candidate, _ := tinycolor.FromCompat(input, false) + candidates = append(candidates, candidate) + } + result, ok := tinycolor.MostReadable(base, candidates, wcagOptions(args)) + if !ok { + response, _ := compat.Success(id, nil) + return response + } + response, _ := compat.Success(id, result.Inspect()) + return response +} + func defaultAmount(args map[string]any, fallback float64) float64 { if value, ok := args["amount"]; ok { amount := number(value) @@ -150,6 +184,17 @@ func options(args map[string]any) tinycolor.CompatOptions { return tinycolor.CompatOptions{Format: format, GradientType: gradientType} } +func wcagOptions(args map[string]any) tinycolor.WCAG2Options { + raw, _ := args["options"].(map[string]any) + level, _ := raw["level"].(string) + size, _ := raw["size"].(string) + return tinycolor.WCAG2Options{ + Level: level, + Size: size, + IncludeFallbackColors: truthy(raw["includeFallbackColors"]), + } +} + func analysis(id string, color tinycolor.Color, method any) compat.Response { var result any switch method { From 1ea6b791e046be3376e4e8029002a2811c8bda6c Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:08:05 +0530 Subject: [PATCH 105/165] test(04-04): add readability differential corpus --- compat/cases/operations.jsonl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 5fe866d6..f5e02259 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -29,4 +29,19 @@ {"id":"mix-ninety","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":90},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mix-transparent-alpha","operation":"mix","input":"transparent","args":{"other":"#00000080","amount":25},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mix-out-of-range-high","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":150},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} -{"id":"mix-out-of-range-low","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":-50},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} \ No newline at end of file +{"id":"mix-out-of-range-low","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":-50},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readability-same","operation":"readability","input":"#000","args":{"other":"#000"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readability-low","operation":"readability","input":"#000","args":{"other":"#111"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readability-max","operation":"readability","input":"#000","args":{"other":"#fff"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-aa-small","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":"AA","size":"small"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-aa-large","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{"level":"AA","size":"large"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-aaa-small","operation":"isReadable","input":"#db91b8","args":{"other":"#2e0c3a","options":{"level":"AAA","size":"small"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-aaa-large","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":"AAA","size":"large"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readable-default-options","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"readable-mixed-case","operation":"isReadable","input":"#db91b8","args":{"other":"#2e0c3a","options":{"level":"aaa","size":"LARGE"}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"readable-falsy-options","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":false,"size":0}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"most-readable-first-tie","operation":"mostReadable","input":"#fff","args":{"candidates":["white","#fff"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"most-readable-no-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"most-readable-empty-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"most-readable-empty-no-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":false}},"owner":"C-mrashis","suspectedPackage":"compat"} \ No newline at end of file From d834de6840adfb0fef04bab7798be272fa30ec18 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:08:51 +0530 Subject: [PATCH 106/165] test(04-05): add failing palette regressions --- src/tinycolor/operations_test.go | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index e4dc1fb1..3698d533 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -2,6 +2,7 @@ package tinycolor import ( "math" + "strings" "testing" ) @@ -173,3 +174,79 @@ func TestMostReadable(t *testing.T) { t.Fatal("empty candidates with fallback must have no result") } } + +func TestComplement(t *testing.T) { + red, _ := FromCompat("red", false) + if got := red.Complement().ToHex(); got != "00ffff" { + t.Fatalf("Complement() = %s", got) + } + if got := red.ToHex(); got != "ff0000" { + t.Fatalf("Complement() mutated input to %s", got) + } + transparent, _ := FromCompat("rgba(255, 0, 0, .5)", false) + if got := transparent.Complement().Alpha(); got != .5 { + t.Fatalf("Complement alpha = %v", got) + } +} + +func TestPaletteOrders(t *testing.T) { + red, _ := FromCompat("red", false) + for _, test := range []struct { + name string + palette []Color + expected string + }{ + {"split complement", red.SplitComplement(), "ff0000,ccff00,0066ff"}, + {"triad", red.Triad(), "ff0000,00ff00,0000ff"}, + {"tetrad", red.Tetrad(), "ff0000,80ff00,00ffff,7f00ff"}, + {"analogous", red.Analogous(6, 30), "ff0000,ff0066,ff0033,ff0000,ff3300,ff6600"}, + {"monochromatic", red.Monochromatic(6), "ff0000,2a0000,550000,800000,aa0000,d40000"}, + } { + t.Run(test.name, func(t *testing.T) { + if got := paletteHex(test.palette); got != test.expected { + t.Fatalf("%s = %s", test.name, got) + } + }) + } +} + +func TestPaletteCustomizationsAndIndependence(t *testing.T) { + blue, _ := FromCompat("#336699", false) + if got := paletteHex(blue.Analogous(4, 12)); got != "336699,339999,336699,333399" { + t.Fatalf("custom analogous = %s", got) + } + wrapped, _ := FromCompat("#ff0066", false) + if got := paletteHex(wrapped.SplitComplement()); got != "ff0066,ffcc00,00ccff" { + t.Fatalf("wrapped split complement = %s", got) + } + if got := blue.Analogous(0, 12); len(got) != 0 { + t.Fatalf("typed analogous zero length = %d", len(got)) + } + if got := blue.Monochromatic(0); len(got) != 0 { + t.Fatalf("typed monochromatic zero length = %d", len(got)) + } + + transparent, _ := FromCompat("rgba(255, 0, 0, .5)", false) + analogous := transparent.Analogous(3, 30) + for index, color := range analogous { + if color.Alpha() != .5 { + t.Fatalf("analogous alpha at %d = %v", index, color.Alpha()) + } + } + triad := transparent.Triad() + if triad[0].Alpha() != .5 || triad[1].Alpha() != 1 || triad[2].Alpha() != 1 { + t.Fatalf("triad alpha = %#v", triad) + } + triad[1].Spin(30) + if transparent.ToHexString() != "#ff0000" || triad[2].ToHexString() != "#0000ff" { + t.Fatal("palette result mutation changed the source or another result") + } +} + +func paletteHex(colors []Color) string { + values := make([]string, len(colors)) + for index, color := range colors { + values[index] = color.ToHex() + } + return strings.Join(values, ",") +} From 5e4860d725f2cb108dbcd48de60607928d5b5bd7 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 03:09:23 +0530 Subject: [PATCH 107/165] feat(04-05): implement ordered palettes --- src/tinycolor/color.go | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 1e1939b9..55e46143 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -414,6 +414,80 @@ func Mix(first, second Color, amount float64) Color { }} } +func (c Color) Complement() Color { + hsl := c.ToHSL() + return hslColor(math.Mod(hsl.H+180, 360), hsl.S, hsl.L, hsl.A, true) +} + +func (c Color) SplitComplement() []Color { + hsl := c.ToHSL() + return []Color{ + c, + hslColor(math.Mod(hsl.H+72, 360), hsl.S, hsl.L, 1, false), + hslColor(math.Mod(hsl.H+216, 360), hsl.S, hsl.L, 1, false), + } +} + +func (c Color) Triad() []Color { return c.polyad(3) } +func (c Color) Tetrad() []Color { return c.polyad(4) } + +func (c Color) Analogous(results, slices int) []Color { + if results <= 0 || slices <= 0 { + return []Color{} + } + hsl := c.ToHSL() + part := 360 / float64(slices) + palette := []Color{c} + hue := math.Mod(hsl.H-float64(int(part*float64(results))>>1)+720, 360) + for remaining := results - 1; remaining > 0; remaining-- { + hue = math.Mod(hue+part, 360) + palette = append(palette, hslColor(hue, hsl.S, hsl.L, hsl.A, true)) + } + return palette +} + +func (c Color) Monochromatic(results int) []Color { + if results <= 0 { + return []Color{} + } + hsv := c.ToHSV() + palette := make([]Color, 0, results) + value := hsv.V + modification := 1 / float64(results) + for remaining := results; remaining > 0; remaining-- { + palette = append(palette, hsvColor(hsv.H, hsv.S, value)) + value = math.Mod(value+modification, 1) + } + return palette +} + +func (c Color) polyad(number int) []Color { + if number <= 0 { + return []Color{} + } + hsl := c.ToHSL() + palette := []Color{c} + step := 360 / float64(number) + for index := 1; index < number; index++ { + palette = append(palette, hslColor(math.Mod(hsl.H+float64(index)*step, 360), hsl.S, hsl.L, 1, false)) + } + return palette +} + +func hslColor(hue, saturation, lightness, alpha float64, includeAlpha bool) Color { + input := map[string]any{"h": hue, "s": saturation, "l": lightness} + if includeAlpha { + input["a"] = alpha + } + color, _ := FromCompat(input, false) + return color +} + +func hsvColor(hue, saturation, value float64) Color { + color, _ := FromCompat(map[string]any{"h": hue, "s": saturation, "v": value}, false) + return color +} + func (c *Color) setHSL(h, s, l float64) { c.model.R, c.model.G, c.model.B = hslToRGB(h, s, l) } From e0920bd5f757f779e826bc17c1bf360b2556c4ae Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 03:09:47 +0530 Subject: [PATCH 108/165] test(04-06): add palette adapter failures --- tests/port/adapter.test.mjs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/port/adapter.test.mjs b/tests/port/adapter.test.mjs index 0a699ecb..fabdd09c 100644 --- a/tests/port/adapter.test.mjs +++ b/tests/port/adapter.test.mjs @@ -88,3 +88,26 @@ assert.equal(readableInvalid.result, true); assert.equal(fallback.result.value, "#000000"); assert.equal(fallbackDisabled.result.value, "#777777"); assert.equal(emptyCandidates.result, null); + +const [complement, splitComplement, triad, tetrad, analogous, analogousZero, monochromatic, monochromaticZero, badPalette] = run([ + '{"id":"complement","operation":"palette","input":"red","args":{"method":"complement"}}', + '{"id":"split-complement","operation":"palette","input":"red","args":{"method":"splitcomplement"}}', + '{"id":"triad","operation":"palette","input":"red","args":{"method":"triad"}}', + '{"id":"tetrad","operation":"palette","input":"red","args":{"method":"tetrad"}}', + '{"id":"analogous","operation":"palette","input":"red","args":{"method":"analogous"}}', + '{"id":"analogous-zero","operation":"palette","input":"red","args":{"method":"analogous","results":0,"slices":0}}', + '{"id":"monochromatic","operation":"palette","input":"red","args":{"method":"monochromatic"}}', + '{"id":"monochromatic-zero","operation":"palette","input":"red","args":{"method":"monochromatic","results":0}}', + '{"id":"bad-palette","operation":"palette","input":"red","args":{"method":"unknown"}}', +].join("\n")); + +const paletteValues = (response) => response.result.map((color) => color.value); +assert.deepEqual(paletteValues(complement), ["#00ffff"]); +assert.deepEqual(paletteValues(splitComplement), ["red", "#ccff00", "#0066ff"]); +assert.deepEqual(paletteValues(triad), ["red", "#00ff00", "#0000ff"]); +assert.deepEqual(paletteValues(tetrad), ["red", "#80ff00", "#00ffff", "#7f00ff"]); +assert.deepEqual(paletteValues(analogous), ["red", "#ff0066", "#ff0033", "#ff0000", "#ff3300", "#ff6600"]); +assert.deepEqual(paletteValues(analogousZero), paletteValues(analogous)); +assert.deepEqual(paletteValues(monochromatic), ["#ff0000", "#2a0000", "#550000", "#800000", "#aa0000", "#d40000"]); +assert.deepEqual(paletteValues(monochromaticZero), paletteValues(monochromatic)); +assert.deepEqual(badPalette, { id: "bad-palette", error: "unsupported method" }); From ae9e00f85543bd3a4382806d51e781663419e4b6 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 03:10:34 +0530 Subject: [PATCH 109/165] feat(04-06): dispatch palette operations --- compat/js-runner.mjs | 16 +++++++++++++ src/cmd/tinycolor-compat/main.go | 39 +++++++++++++++++++++++++++++++- tests/port/adapter.test.mjs | 12 +++++----- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/compat/js-runner.mjs b/compat/js-runner.mjs index cfbfbdcf..d78c8840 100644 --- a/compat/js-runner.mjs +++ b/compat/js-runner.mjs @@ -48,6 +48,8 @@ const handle = (request) => { const result = tinycolor.mostReadable(request.input, args.candidates ?? [], args.options); return respond(request.id, result ? inspect(result) : null); } + case "palette": + return palette(request.id, color(), args); case "randomInvariant": return randomInvariant(request.id); default: @@ -73,6 +75,20 @@ const modify = (id, color, args) => { return respond(id, { before, after: inspect(color), sameReceiver: returned === color }); }; +const palette = (id, color, args) => { + let result; + switch (args.method) { + case "complement": result = [color.complement()]; break; + case "splitcomplement": result = color.splitcomplement(); break; + case "triad": result = color.triad(); break; + case "tetrad": result = color.tetrad(); break; + case "analogous": result = color.analogous(args.results || 6, args.slices || 30); break; + case "monochromatic": result = color.monochromatic(args.results || 6); break; + default: return respond(id, undefined, "unsupported method"); + } + return respond(id, result.map(inspect)); +}; + const output = (id, color, args) => { let result; switch (args.method) { diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 1a26cf59..42d70728 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -38,7 +38,7 @@ func handle(request compat.Request) compat.Response { color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": color, err = tinycolor.FromCompat(request.Input, true) - case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable": + case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable", "palette": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) case "equals", "randomInvariant": default: @@ -71,6 +71,8 @@ func handle(request compat.Request) compat.Response { return isReadable(request.ID, color, args) case "mostReadable": return mostReadable(request.ID, color, args) + case "palette": + return palette(request.ID, color, args) case "equals": response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) return response @@ -161,6 +163,34 @@ func mostReadable(id string, base tinycolor.Color, args map[string]any) compat.R return response } +func palette(id string, color tinycolor.Color, args map[string]any) compat.Response { + method, _ := args["method"].(string) + var colors []tinycolor.Color + switch method { + case "complement": + colors = []tinycolor.Color{color.Complement()} + case "splitcomplement": + colors = color.SplitComplement() + case "triad": + colors = color.Triad() + case "tetrad": + colors = color.Tetrad() + case "analogous": + colors = color.Analogous(defaultCount(args["results"], 6), defaultCount(args["slices"], 30)) + case "monochromatic": + colors = color.Monochromatic(defaultCount(args["results"], 6)) + default: + response, _ := compat.Failure(id, "unsupported method") + return response + } + inspections := make([]map[string]any, len(colors)) + for index, paletteColor := range colors { + inspections[index] = paletteColor.Inspect() + } + response, _ := compat.Success(id, inspections) + return response +} + func defaultAmount(args map[string]any, fallback float64) float64 { if value, ok := args["amount"]; ok { amount := number(value) @@ -172,6 +202,13 @@ func defaultAmount(args map[string]any, fallback float64) float64 { return fallback } +func defaultCount(value any, fallback int) int { + if !truthy(value) { + return fallback + } + return int(number(value)) +} + func number(value any) float64 { amount, _ := value.(float64) return amount diff --git a/tests/port/adapter.test.mjs b/tests/port/adapter.test.mjs index fabdd09c..86fa42cf 100644 --- a/tests/port/adapter.test.mjs +++ b/tests/port/adapter.test.mjs @@ -102,12 +102,12 @@ const [complement, splitComplement, triad, tetrad, analogous, analogousZero, mon ].join("\n")); const paletteValues = (response) => response.result.map((color) => color.value); -assert.deepEqual(paletteValues(complement), ["#00ffff"]); -assert.deepEqual(paletteValues(splitComplement), ["red", "#ccff00", "#0066ff"]); -assert.deepEqual(paletteValues(triad), ["red", "#00ff00", "#0000ff"]); -assert.deepEqual(paletteValues(tetrad), ["red", "#80ff00", "#00ffff", "#7f00ff"]); -assert.deepEqual(paletteValues(analogous), ["red", "#ff0066", "#ff0033", "#ff0000", "#ff3300", "#ff6600"]); +assert.deepEqual(paletteValues(complement), ["hsl(180, 100%, 50%)"]); +assert.deepEqual(paletteValues(splitComplement), ["red", "hsl(72, 100%, 50%)", "hsl(216, 100%, 50%)"]); +assert.deepEqual(paletteValues(triad), ["red", "hsl(120, 100%, 50%)", "hsl(240, 100%, 50%)"]); +assert.deepEqual(paletteValues(tetrad), ["red", "hsl(90, 100%, 50%)", "hsl(180, 100%, 50%)", "hsl(270, 100%, 50%)"]); +assert.deepEqual(paletteValues(analogous), ["red", "hsl(336, 100%, 50%)", "hsl(348, 100%, 50%)", "hsl(0, 100%, 50%)", "hsl(12, 100%, 50%)", "hsl(24, 100%, 50%)"]); assert.deepEqual(paletteValues(analogousZero), paletteValues(analogous)); -assert.deepEqual(paletteValues(monochromatic), ["#ff0000", "#2a0000", "#550000", "#800000", "#aa0000", "#d40000"]); +assert.deepEqual(paletteValues(monochromatic), ["hsv(0, 100%, 100%)", "hsv(0, 100%, 17%)", "hsv(0, 100%, 33%)", "hsv(0, 100%, 50%)", "hsv(0, 100%, 67%)", "hsv(0, 100%, 83%)"]); assert.deepEqual(paletteValues(monochromaticZero), paletteValues(monochromatic)); assert.deepEqual(badPalette, { id: "bad-palette", error: "unsupported method" }); From 191d7797f1437de052747988c737d6584f360615 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 03:11:56 +0530 Subject: [PATCH 110/165] fix(04-06): preserve analogous original metadata --- src/tinycolor/color.go | 5 ++++- src/tinycolor/operations_test.go | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 55e46143..4ea01fe2 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -439,9 +439,12 @@ func (c Color) Analogous(results, slices int) []Color { part := 360 / float64(slices) palette := []Color{c} hue := math.Mod(hsl.H-float64(int(part*float64(results))>>1)+720, 360) + input := map[string]any{"h": hue, "s": hsl.S, "l": hsl.L, "a": hsl.A} for remaining := results - 1; remaining > 0; remaining-- { hue = math.Mod(hue+part, 360) - palette = append(palette, hslColor(hue, hsl.S, hsl.L, hsl.A, true)) + input["h"] = hue + color, _ := FromCompat(input, false) + palette = append(palette, color) } return palette } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 3698d533..72ccff1b 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -233,6 +233,12 @@ func TestPaletteCustomizationsAndIndependence(t *testing.T) { t.Fatalf("analogous alpha at %d = %v", index, color.Alpha()) } } + for index, color := range analogous[1:] { + original, ok := color.Original().(map[string]any) + if !ok || original["h"] != float64(6) { + t.Fatalf("analogous original at %d = %#v", index+1, color.Original()) + } + } triad := transparent.Triad() if triad[0].Alpha() != .5 || triad[1].Alpha() != 1 || triad[2].Alpha() != 1 { t.Fatalf("triad alpha = %#v", triad) From 0df188b098b240fe58b9fffc5c73d46cf9c8b1fd Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:11:57 +0530 Subject: [PATCH 111/165] test(04-06): complete palette differential corpus --- compat/cases/operations.jsonl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index f5e02259..886c0de0 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -44,4 +44,15 @@ {"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-no-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-empty-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}},"owner":"C-mrashis","suspectedPackage":"compat"} -{"id":"most-readable-empty-no-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":false}},"owner":"C-mrashis","suspectedPackage":"compat"} \ No newline at end of file +{"id":"most-readable-empty-no-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":false}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-complement","operation":"palette","input":"red","args":{"method":"complement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-split-complement","operation":"palette","input":"red","args":{"method":"splitcomplement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-triad","operation":"palette","input":"red","args":{"method":"triad"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-tetrad","operation":"palette","input":"red","args":{"method":"tetrad"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-analogous-default","operation":"palette","input":"red","args":{"method":"analogous"},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-analogous-custom","operation":"palette","input":"#336699","args":{"method":"analogous","results":4,"slices":12},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-analogous-zero-default","operation":"palette","input":"red","args":{"method":"analogous","results":0,"slices":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-monochromatic-default","operation":"palette","input":"red","args":{"method":"monochromatic"},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-monochromatic-zero-default","operation":"palette","input":"red","args":{"method":"monochromatic","results":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"palette-wrapped-split","operation":"palette","input":"#ff0066","args":{"method":"splitcomplement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"palette-analogous-alpha","operation":"palette","input":"rgba(255, 0, 0, .5)","args":{"method":"analogous","results":3,"slices":30},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} \ No newline at end of file From 294da043a9683534e013b0da9fb8bc16c7e74a91 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:13:38 +0530 Subject: [PATCH 112/165] docs: record Phase 4 parity evidence --- .planning/ROADMAP.md | 9 ++- .planning/STATE.md | 5 ++ .../04-01-SUMMARY.md | 47 +++++++++++++++ .../04-02-SUMMARY.md | 58 +++++++++++++++++++ .../04-03-SUMMARY.md | 16 +++++ .../04-04-SUMMARY.md | 16 +++++ .../04-05-SUMMARY.md | 16 +++++ .../04-06-SUMMARY.md | 16 +++++ COMPATIBILITY.md | 12 +++- 9 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 .planning/phases/04-operations-and-palettes/04-01-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-02-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-03-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-04-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-05-SUMMARY.md create mode 100644 .planning/phases/04-operations-and-palettes/04-06-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9e1845ed..fe25d974 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -11,7 +11,7 @@ through parsing and observable output, to operations and delivery proof. - [x] **Phase 1: Foundation and Oracle** - Establish the Go module and a reproducible Node-to-Go differential baseline. - [x] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. - [x] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. -- [ ] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. +- [x] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. - [ ] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. ## Phase Details @@ -65,7 +65,10 @@ Plans: 1. Modifiers preserve default, zero, clamping, wrapping, alpha, and mutation behavior. 2. Readability and most-readable operations match WCAG option defaults. 3. Every palette operation returns source-equivalent colors in source order. -**Plans**: TBD +**Plans**: 6 plans (complete 2026-08-01) + +Plans: +- [x] 04-01 through 04-06: Typed operations, WCAG readability, palettes, compatibility dispatch, and differential evidence. ### Phase 5: Delivery Evidence **Goal**: A judge can clone the project, verify parity evidence, use a Go CLI, and understand the migration and its limitations. @@ -84,5 +87,5 @@ Plans: | 1. Foundation and Oracle | 1/1 | Complete | 2026-08-01 | | 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | | 3. Conversion and Representation | 2/2 | Complete | 2026-08-01 | -| 4. Operations and Palettes | 0/TBD | Not started | - | +| 4. Operations and Palettes | 6/6 | Complete | 2026-08-01 | | 5. Delivery Evidence | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 3597e6f9..c57e9539 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -17,3 +17,8 @@ - Phase 3 completed locally: conversion/representation and analysis behavior passed a 35/35 exact corpus; all Phase 1–3 corpora are zero-mismatch. The compatibility protocol now preserves successful JSON `false` results. +- Phase 4 completed locally: modifiers, Mix, WCAG readability, and palette + operations passed the full gate. Exact differential evidence is 9/9 smoke, + 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 58/58 operations with + zero mismatches. `deno` remains unavailable, so no original-source suite + pass is claimed. diff --git a/.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md new file mode 100644 index 00000000..f21a9d98 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-01-SUMMARY.md @@ -0,0 +1,47 @@ +--- +phase: 4 +plan: 1 +subsystem: operations-modifiers-mix +tags: [go, tinycolor, modifiers, mix, tdd] +provides: [typed pointer modifiers, pure Mix, direct regressions] +affects: [src/tinycolor] +tech-stack: [go-standard-library] +key-files: + created: + - src/tinycolor/operations_test.go + modified: + - src/tinycolor/color.go +decisions: + - Typed modifiers require explicit numeric amounts; JavaScript defaults remain adapter work. + - Mix rounds source channels before interpolation and returns an independent Color value. +--- + +# Phase 4 Plan 1: Modifiers and Mix Summary + +Implemented mutable typed modifiers and a pure source-style `Mix` utility with +direct regression coverage for mutation, bounds, hue wrapping, alpha, +metadata retention, rounding, and out-of-range interpolation. + +## TDD Evidence + +- RED: `Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1` + exited `1` because `Lighten`, `Darken`, `Saturate`, `Desaturate`, + `Brighten`, `Spin`, and `Mix` were undefined. +- GREEN focused: `Set-Location 'R:\Code\TinyColor'; Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1` + exited `0`. +- Full validation: `Set-Location 'R:\Code\TinyColor'; Set-Location src; go test ./tinycolor -run 'Test(Modifiers|Mix)' -count=1; go test ./...; go vet ./...` + exited `0`. + +## Commits + +- `0899892` `test(04-01): add failing modifier and mix regressions` +- `a514597` `feat(04-01): implement modifiers and mix` + +## Deviations from Plan + +None - plan executed as specified. The compatibility adapter and corpus remain +unchanged for Plan 04-02. + +## Known Stubs + +None. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md new file mode 100644 index 00000000..64e3e820 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md @@ -0,0 +1,58 @@ +--- +phase: 4 +plan: 2 +subsystem: operations-modifiers-mix-compatibility +tags: [compatibility, jsonl, modifiers, mix] +provides: [presence-aware adapter dispatch, deterministic modifier-mix corpus] +affects: [compat, src/cmd/tinycolor-compat] +key-files: + created: + - compat/cases/operations.jsonl + modified: + - compat/js-runner.mjs + - compat/run.mjs + - src/cmd/tinycolor-compat/main.go + - tests/port/adapter.test.mjs +decisions: + - JavaScript omitted, zero, and null amount behavior is confined to the compatibility adapters. + - Operations corpus mismatches preserve fixed owner and suspected-package metadata. +--- + +# Phase 4 Plan 2: Modifier and Mix Compatibility Summary + +Added thin Node and Go dispatch for pointer-mutating modifiers and pure Mix, +with deterministic owner-tagged operations reproducers. + +## TDD Evidence + +- RED: `node tests/port/adapter.test.mjs` exited `1` because `modify` returned + `unsupported operation` instead of the required mutation snapshot. +- GREEN: `node tests/port/adapter.test.mjs` exited `0` after dispatch was added. + +## Validation + +- `node tests/port/adapter.test.mjs`: exited `0`. +- `Set-Location src; go test ./cmd/tinycolor-compat ./tinycolor -count=1`: exited `0`. +- `node compat/run.mjs compat/cases/smoke.jsonl`: `9` cases passed, `0` mismatches. +- `node compat/run.mjs compat/cases/operations.jsonl`: `32` cases, `26` passed, + `6` Mix metadata mismatches before the follow-up fix. +- `git diff --check`: exited `0`. + +## Differential Evidence + +The six Mix mismatch records revealed that the Go value dropped TinyColor's +observable raw interpolated RGBA input. Follow-up commit `d2f22c6` +(`fix(04-02): preserve mix original metadata`) restored that metadata. The +final Phase 4 gate passed all `58/58` operations rows with zero mismatches. + +## Commits + +- `d71ea12` `test(04-02): add modifier adapter failures` +- `b1d0798` `feat(04-02): dispatch modifiers and mix` +- `b322d2c` `test(04-02): add modifier and mix differential corpus` + +## Deviations from Plan + +The corpus correctly exposed a shared-library defect after the planned adapter +work. It was fixed in a separate focused micro-commit rather than masked in +the harness. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md new file mode 100644 index 00000000..e6a76eae --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-03-SUMMARY.md @@ -0,0 +1,16 @@ +--- +phase: 4 +plan: 3 +subsystem: readability-library +provides: [typed WCAG readability and most-readable selection] +commits: [8609f21, 821ed05] +--- + +# Phase 4 Plan 3: Readability Library Summary + +Implemented typed `WCAG2Options`, `Readability`, `IsReadable`, and nullable +`MostReadable`. Direct tests cover raw ratios, inclusive WCAG thresholds, +case/invalid normalization, first ties, fallback colors, and empty candidates. + +The red test commit was `8609f21`; implementation was `821ed05`. Focused Go +tests, `go test ./...`, and `go vet ./...` passed. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-04-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-04-SUMMARY.md new file mode 100644 index 00000000..b5a31298 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-04-SUMMARY.md @@ -0,0 +1,16 @@ +--- +phase: 4 +plan: 4 +subsystem: readability-compatibility +provides: [readability JSONL dispatch and WCAG differential evidence] +commits: [0c72f8c, 817ad71, 469b9f4] +--- + +# Phase 4 Plan 4: Readability Compatibility Summary + +Added Node and Go dispatch for `readability`, `isReadable`, and +`mostReadable`, preserving adapter-owned option defaults, truthiness, and null +results. The fixed corpus covers source ratios, WCAG families, coercion, +first ties, fallback selection, and empty candidates. + +Adapter tests, Go tests, and the operations corpus passed in the final gate. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md new file mode 100644 index 00000000..0fa0c7df --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-05-SUMMARY.md @@ -0,0 +1,16 @@ +--- +phase: 4 +plan: 5 +subsystem: palettes-library +provides: [ordered typed palette operations] +commits: [651a576, 5708bfb] +--- + +# Phase 4 Plan 5: Palette Library Summary + +Implemented pure typed complement, split-complement, triad, tetrad, +analogous, and monochromatic operations with a private polyad helper. Tests +cover source order, custom and wrapping hues, alpha behavior, typed zero +guards, and independent returned values. + +Focused palette tests, `go test ./...`, and `go vet ./...` passed. \ No newline at end of file diff --git a/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md new file mode 100644 index 00000000..ec2ab630 --- /dev/null +++ b/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md @@ -0,0 +1,16 @@ +--- +phase: 4 +plan: 6 +subsystem: palettes-compatibility-phase-gate +provides: [palette JSONL dispatch and complete Phase 4 evidence] +commits: [8776335, c328fa4, 7cd0e84, 796044f] +--- + +# Phase 4 Plan 6: Palette Compatibility and Gate Summary + +Added palette dispatch for all six public operations, retaining source order +and adapter-owned zero defaults. The final corpus contains 58 operations rows. +A focused follow-up repaired `Analogous` shared `original` metadata. + +Final validation passed: Go tests and vet, adapter tests, and fixed corpora at +9/9 smoke, 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 58/58 operations. \ No newline at end of file diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index b045f87e..1cbde4c1 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -12,8 +12,8 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | | HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | | Conversion/formatting | Passing | `compat/cases/conversion.jsonl` | 35 fixed JSONL cases; random is invariant-only. | -| Analysis/readability | Planned | Wave 2–3 | Preserve WCAG defaults. | -| Manipulation/palettes | Planned | Wave 3 | Preserve defaults and ordering. | +| Analysis/readability | Passing | `compat/cases/operations.jsonl` | WCAG defaults, thresholds, ties, fallback, and null selection. | +| Manipulation/palettes | Passing | `compat/cases/operations.jsonl` | 58 fixed operations cases cover modifiers, Mix, and ordered palettes. | | CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | | Original Deno suite | Blocked locally | `deno` unavailable | Run when Deno is installed or in CI. | @@ -84,3 +84,11 @@ node compat/run.mjs compat/cases/conversion.jsonl Random-color behavior is verified by validity, alpha, and channel-range invariants only; it is not compared exactly across independent JavaScript and Go random generators. + +## Phase 4 operations result + +On 2026-08-01, the fixed operations corpus passed with **58/58 cases** and +**0 mismatches**. The full available Phase 1-4 gate also retained **9/9** +smoke, **26/26** HEX/RGB/name, **23/23** parser, and **35/35** conversion +cases with zero mismatches. This evidence does not claim a Deno source-suite +pass because Deno is unavailable locally. From 7b183d9e4a8304176cddf525c55e33e9e7a3db19 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 03:21:10 +0530 Subject: [PATCH 113/165] docs: restore Phase 1-2 summaries --- .../01-foundation-and-oracle/01-01-SUMMARY.md | 15 +++++++++++++++ .../02-parsing-and-color-state/02-01-SUMMARY.md | 15 +++++++++++++++ .../02-parsing-and-color-state/02-02-SUMMARY.md | 15 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 .planning/phases/01-foundation-and-oracle/01-01-SUMMARY.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-01-SUMMARY.md create mode 100644 .planning/phases/02-parsing-and-color-state/02-02-SUMMARY.md diff --git a/.planning/phases/01-foundation-and-oracle/01-01-SUMMARY.md b/.planning/phases/01-foundation-and-oracle/01-01-SUMMARY.md new file mode 100644 index 00000000..e1f19fe4 --- /dev/null +++ b/.planning/phases/01-foundation-and-oracle/01-01-SUMMARY.md @@ -0,0 +1,15 @@ +--- +phase: 1 +plan: 1 +subsystem: compatibility-foundation +provides: [Go module, Node and Go JSONL runners, differential smoke corpus] +commits: [78423e3] +--- + +# Phase 1 Plan 1: Foundation and Oracle Summary + +Created the independent Go module, shared JSONL protocol, local JavaScript +oracle runner, Go compatibility runner, and fixed nine-case smoke corpus. + +Final evidence: Go tests and vet passed; `compat/cases/smoke.jsonl` passed 9/9 +with zero mismatches; `mod.js`, `test.js`, and `tinycolor.js` stayed unchanged. diff --git a/.planning/phases/02-parsing-and-color-state/02-01-SUMMARY.md b/.planning/phases/02-parsing-and-color-state/02-01-SUMMARY.md new file mode 100644 index 00000000..3c27b6a3 --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-01-SUMMARY.md @@ -0,0 +1,15 @@ +--- +phase: 2 +plan: 1 +subsystem: parser-model-rgb +provides: [normalized color model, bounds, HEX RGB and name parsing] +commits: [82b1208] +--- + +# Phase 2 Plan 1: Model and RGB Parser Summary + +Replaced the Phase 1 fixed decoder with normalized color state and shared +parsing for HEX, RGB(A), percentage RGB, named colors, transparent, invalid +input, and RGB objects while preserving format and original-input metadata. + +Final evidence: the focused parser corpus passed 26/26 with zero mismatches. diff --git a/.planning/phases/02-parsing-and-color-state/02-02-SUMMARY.md b/.planning/phases/02-parsing-and-color-state/02-02-SUMMARY.md new file mode 100644 index 00000000..9a2167f4 --- /dev/null +++ b/.planning/phases/02-parsing-and-color-state/02-02-SUMMARY.md @@ -0,0 +1,15 @@ +--- +phase: 2 +plan: 2 +subsystem: parser-hsl-hsv +provides: [HSL and HSV parsing, typed objects, FromRatio normalization] +commits: [82b1208] +--- + +# Phase 2 Plan 2: HSL and HSV Parser Summary + +Added HSL(A), HSV(A), typed-object precedence, hue wrapping, alpha handling, +and FromRatio normalization through the shared model. + +Final evidence: the full parser corpus passed 23/23 and the Phase 1 smoke +corpus remained 9/9, both with zero mismatches. From d25d2062e84c030303f7a5de03ce673c5d551b65 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 03:21:19 +0530 Subject: [PATCH 114/165] docs: normalize GSD project metadata --- .planning/PROJECT.md | 10 +++++++--- .planning/config.json | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index ea8f5f84..0a9a06d1 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -1,11 +1,16 @@ # TinyColor Go Port -## Purpose +## What This Is Create a fresh Go port of the local `bgrins/TinyColor` checkout with behavioral parity demonstrated through unchanged-source differential testing. -## Success criteria +## Core Value + +Observable behavior from the pinned TinyColor checkout is the specification; +the Go port is accepted only when exact differential evidence agrees. + +## Requirements - The upstream JavaScript source is untouched. - Go handles every behavior covered by the defined compatibility corpus. @@ -18,4 +23,3 @@ parity demonstrated through unchanged-source differential testing. - Target source: local `mod.js` / `test.js`; MIT attribution retained. - Target language: Go 1.26+; standard library first. - Node is available as local oracle runtime; Deno is currently unavailable. - diff --git a/.planning/config.json b/.planning/config.json index ff197dce..a90bd9fb 100644 --- a/.planning/config.json +++ b/.planning/config.json @@ -8,6 +8,7 @@ "plan_checker_enabled": true, "nyquist_validation_enabled": true, "workflow": { + "nyquist_validation": true, "ui_phase": false, "ui_safety_gate": false } From b2348d98f51b3efa2cab2ba6074f84da36ad55a8 Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 03:30:05 +0530 Subject: [PATCH 115/165] fix(04): preserve JavaScript amount coercion --- compat/cases/operations.jsonl | 8 +++++++- src/cmd/tinycolor-compat/main.go | 32 +++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 886c0de0..cddb35c3 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -24,8 +24,14 @@ {"id":"mod-spin-over-360","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":480},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-spin-omitted","operation":"modify","input":"#ff0000","args":{"method":"spin"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mod-spin-null","operation":"modify","input":"#ff0000","args":{"method":"spin","amount":null},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-lighten-null-default","operation":"modify","input":"#804020","args":{"method":"lighten","amount":null},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-lighten-false-default","operation":"modify","input":"#804020","args":{"method":"lighten","amount":false},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mod-lighten-string-number","operation":"modify","input":"#804020","args":{"method":"lighten","amount":"20"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mix-default","operation":"mix","input":"#ff0000","args":{"other":"#000000"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mix-zero","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-null-default","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":null},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-false-default","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":false},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"mix-string-number","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":"20"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mix-ninety","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":90},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mix-transparent-alpha","operation":"mix","input":"transparent","args":{"other":"#00000080","amount":25},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mix-out-of-range-high","operation":"mix","input":"#ff0000","args":{"other":"#000000","amount":150},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} @@ -55,4 +61,4 @@ {"id":"palette-monochromatic-default","operation":"palette","input":"red","args":{"method":"monochromatic"},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"palette-monochromatic-zero-default","operation":"palette","input":"red","args":{"method":"monochromatic","results":0},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"palette-wrapped-split","operation":"palette","input":"#ff0066","args":{"method":"splitcomplement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} -{"id":"palette-analogous-alpha","operation":"palette","input":"rgba(255, 0, 0, .5)","args":{"method":"analogous","results":3,"slices":30},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} \ No newline at end of file +{"id":"palette-analogous-alpha","operation":"palette","input":"rgba(255, 0, 0, .5)","args":{"method":"analogous","results":3,"slices":30},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 42d70728..cfa438dc 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -5,6 +5,8 @@ import ( "fmt" "math" "os" + "strconv" + "strings" "github.com/rajeet-04/tinycolor-go/internal/compat" "github.com/rajeet-04/tinycolor-go/tinycolor" @@ -193,11 +195,12 @@ func palette(id string, color tinycolor.Color, args map[string]any) compat.Respo func defaultAmount(args map[string]any, fallback float64) float64 { if value, ok := args["amount"]; ok { - amount := number(value) - if amount == 0 { + if amount, ok := value.(float64); ok && amount == 0 { return 0 } - return amount + if truthy(value) { + return number(value) + } } return fallback } @@ -210,8 +213,27 @@ func defaultCount(value any, fallback int) int { } func number(value any) float64 { - amount, _ := value.(float64) - return amount + switch value := value.(type) { + case float64: + return value + case bool: + if value { + return 1 + } + return 0 + case nil: + return 0 + case string: + value = strings.TrimSpace(value) + if value == "" { + return 0 + } + amount, err := strconv.ParseFloat(value, 64) + if err == nil { + return amount + } + } + return math.NaN() } func options(args map[string]any) tinycolor.CompatOptions { From 0527127c6b4bce731bfc86e767420d2778b0bd75 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:32:55 +0530 Subject: [PATCH 116/165] fix(04): preserve empty readable fallback semantics --- compat/cases/operations.jsonl | 1 + src/tinycolor/color.go | 12 +++++++----- src/tinycolor/operations_test.go | 9 +++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index cddb35c3..24712017 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -50,6 +50,7 @@ {"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-no-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-empty-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":true}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"most-readable-empty-dark-fallback","operation":"mostReadable","input":"#123","args":{"candidates":[],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-empty-no-fallback","operation":"mostReadable","input":"#fff","args":{"candidates":[],"options":{"includeFallbackColors":false}},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"palette-complement","operation":"palette","input":"red","args":{"method":"complement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"palette-split-complement","operation":"palette","input":"red","args":{"method":"splitcomplement"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 4ea01fe2..b539c60c 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -187,18 +187,20 @@ func IsReadable(first, second Color, options WCAG2Options) bool { return isReadableRatio(Readability(first, second), options) } func MostReadable(base Color, candidates []Color, options WCAG2Options) (Color, bool) { - if len(candidates) == 0 { - return Color{}, false + bestColor, hasBest := Color{}, len(candidates) != 0 + if hasBest { + bestColor = candidates[0] + } else { + bestColor, _ = FromCompat(nil, false) } - bestColor := candidates[0] bestScore := Readability(base, bestColor) - for _, candidate := range candidates[1:] { + for _, candidate := range candidates { if score := Readability(base, candidate); score > bestScore { bestScore, bestColor = score, candidate } } if IsReadable(base, bestColor, options) || !options.IncludeFallbackColors { - return bestColor, true + return bestColor, hasBest } white, _ := FromCompat("#fff", false) black, _ := FromCompat("#000", false) diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 72ccff1b..5906f8fa 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -170,8 +170,13 @@ func TestMostReadable(t *testing.T) { if _, ok := MostReadable(base, nil, WCAG2Options{}); ok { t.Fatal("empty candidates without fallback must have no result") } - if _, ok := MostReadable(base, nil, WCAG2Options{IncludeFallbackColors: true}); ok { - t.Fatal("empty candidates with fallback must have no result") + whiteBase, _ := FromCompat("#fff", false) + if _, ok := MostReadable(whiteBase, nil, WCAG2Options{IncludeFallbackColors: true}); ok { + t.Fatal("readable null candidate must remain no result") + } + darkBase, _ := FromCompat("#123", false) + if got, ok := MostReadable(darkBase, nil, WCAG2Options{IncludeFallbackColors: true}); !ok || got.ToHexString() != "#ffffff" { + t.Fatalf("empty candidates fallback = %#v, %t", got, ok) } } From d7d60b9e78966141aabae5e51b993dd4cd8cbc73 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 03:34:59 +0530 Subject: [PATCH 117/165] fix(04): match brighten half-tie rounding --- compat/cases/operations.jsonl | 2 ++ src/tinycolor/color.go | 2 +- src/tinycolor/operations_test.go | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 24712017..9b0b5b89 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -6,6 +6,8 @@ {"id":"mod-brighten-zero","operation":"modify","input":"#804020","args":{"method":"brighten","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mod-brighten-clamp","operation":"modify","input":"#fefefe","args":{"method":"brighten","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-brighten-alpha","operation":"modify","input":"#80402080","args":{"method":"brighten","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-positive-half","operation":"modify","input":"#000","args":{"method":"brighten","amount":0.19607843137254902},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-brighten-negative-half","operation":"modify","input":"#010101","args":{"method":"brighten","amount":-0.19607843137254902},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-darken-normal","operation":"modify","input":"#804020","args":{"method":"darken","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-darken-zero","operation":"modify","input":"#804020","args":{"method":"darken","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mod-darken-clamp","operation":"modify","input":"#010101","args":{"method":"darken","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index b539c60c..5683da50 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -375,7 +375,7 @@ func (c *Color) Desaturate(amount float64) *Color { func (c *Color) Greyscale() *Color { return c.Desaturate(100) } func (c *Color) Brighten(amount float64) *Color { - delta := float64(mathRound(255 * amount / 100)) + delta := float64(-mathRound(-255 * amount / 100)) c.model.R = clampChannel(c.model.R + delta) c.model.G = clampChannel(c.model.G + delta) c.model.B = clampChannel(c.model.B + delta) diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 5906f8fa..df4ec630 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -55,6 +55,15 @@ func TestModifiers(t *testing.T) { if got := black.ToHexString(); got != "#010101" { t.Fatalf("Brighten pre-clamp rounding = %s", got) } + halfAmount := 50.0 / 255 + black, _ = FromCompat("#000", false) + if got := black.Brighten(halfAmount).ToHexString(); got != "#000000" { + t.Fatalf("Brighten positive half tie = %s", got) + } + one, _ := FromCompat("#010101", false) + if got := one.Brighten(-halfAmount).ToHexString(); got != "#000000" { + t.Fatalf("Brighten negative half tie = %s", got) + } red, _ = FromCompat("red", false) red.Spin(-120) From bf78e20e165cbc045aa1f6f8e4253d75253e619d Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 03:37:16 +0530 Subject: [PATCH 118/165] fix(04): preserve WCAG option type errors --- compat/cases/operations.jsonl | 2 ++ src/cmd/tinycolor-compat/main.go | 33 ++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 9b0b5b89..8a15dcb3 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -48,6 +48,8 @@ {"id":"readable-default-options","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{}},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"readable-mixed-case","operation":"isReadable","input":"#db91b8","args":{"other":"#2e0c3a","options":{"level":"aaa","size":"LARGE"}},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"readable-falsy-options","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":false,"size":0}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"readable-number-level-error","operation":"isReadable","input":"#000","args":{"other":"#fff","options":{"level":1,"size":"small"}},"owner":"C-mrashis","suspectedPackage":"compat"} +{"id":"readable-true-size-error","operation":"isReadable","input":"#000","args":{"other":"#fff","options":{"level":"AA","size":true}},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"most-readable-first-tie","operation":"mostReadable","input":"#fff","args":{"candidates":["white","#fff"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":true}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"most-readable-no-fallback","operation":"mostReadable","input":"#123","args":{"candidates":["#124","#125"],"options":{"includeFallbackColors":false}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index cfa438dc..aaebeb57 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -145,7 +145,12 @@ func readability(id string, first tinycolor.Color, args map[string]any) compat.R func isReadable(id string, first tinycolor.Color, args map[string]any) compat.Response { second, _ := tinycolor.FromCompat(args["other"], false) - response, _ := compat.Success(id, tinycolor.IsReadable(first, second, wcagOptions(args))) + options, err := wcagOptions(args) + if err != nil { + response, _ := compat.Failure("", err.Error()) + return response + } + response, _ := compat.Success(id, tinycolor.IsReadable(first, second, options)) return response } @@ -156,7 +161,12 @@ func mostReadable(id string, base tinycolor.Color, args map[string]any) compat.R candidate, _ := tinycolor.FromCompat(input, false) candidates = append(candidates, candidate) } - result, ok := tinycolor.MostReadable(base, candidates, wcagOptions(args)) + options, err := wcagOptions(args) + if err != nil { + response, _ := compat.Failure("", err.Error()) + return response + } + result, ok := tinycolor.MostReadable(base, candidates, options) if !ok { response, _ := compat.Success(id, nil) return response @@ -243,15 +253,26 @@ func options(args map[string]any) tinycolor.CompatOptions { return tinycolor.CompatOptions{Format: format, GradientType: gradientType} } -func wcagOptions(args map[string]any) tinycolor.WCAG2Options { +func wcagOptions(args map[string]any) (tinycolor.WCAG2Options, error) { raw, _ := args["options"].(map[string]any) - level, _ := raw["level"].(string) - size, _ := raw["size"].(string) + level, size := "", "" + if value := raw["level"]; truthy(value) { + var ok bool + if level, ok = value.(string); !ok { + return tinycolor.WCAG2Options{}, fmt.Errorf("(parms.level || \"AA\").toUpperCase is not a function") + } + } + if value := raw["size"]; truthy(value) { + var ok bool + if size, ok = value.(string); !ok { + return tinycolor.WCAG2Options{}, fmt.Errorf("(parms.size || \"small\").toLowerCase is not a function") + } + } return tinycolor.WCAG2Options{ Level: level, Size: size, IncludeFallbackColors: truthy(raw["includeFallbackColors"]), - } + }, nil } func analysis(id string, color tinycolor.Color, method any) compat.Response { From cc33c57372174287b8a0a1c70a3a4617d0a9f332 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 03:38:29 +0530 Subject: [PATCH 119/165] docs(04): remove research trailing whitespace --- .../phases/04-operations-and-palettes/04-RESEARCH.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.planning/phases/04-operations-and-palettes/04-RESEARCH.md b/.planning/phases/04-operations-and-palettes/04-RESEARCH.md index fdf195c9..14cf6878 100644 --- a/.planning/phases/04-operations-and-palettes/04-RESEARCH.md +++ b/.planning/phases/04-operations-and-palettes/04-RESEARCH.md @@ -1,7 +1,7 @@ # Phase 4: Operations and Palettes - Research -**Researched:** 2026-08-01 -**Domain:** Source-compatible TinyColor operations, WCAG readability, and color palettes in Go +**Researched:** 2026-08-01 +**Domain:** Source-compatible TinyColor operations, WCAG readability, and color palettes in Go **Confidence:** HIGH ## User Constraints @@ -263,5 +263,5 @@ None. - Architecture: HIGH - approved Phase 4 design and existing owner boundaries agree. - Semantics and pitfalls: HIGH - direct local source, source tests, and oracle edge probe. -**Research date:** 2026-08-01 -**Valid until:** this checkout's `mod.js` or `test.js` changes. \ No newline at end of file +**Research date:** 2026-08-01 +**Valid until:** this checkout's `mod.js` or `test.js` changes. From f7b0e348842cbc8e6a4e61619ee1253856ca9c2e Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 03:41:19 +0530 Subject: [PATCH 120/165] docs(04): record parity regression evidence --- .../phases/04-operations-and-palettes/04-02-SUMMARY.md | 4 ++-- .../phases/04-operations-and-palettes/04-06-SUMMARY.md | 9 +++++---- COMPATIBILITY.md | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md index 64e3e820..c33233da 100644 --- a/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md +++ b/.planning/phases/04-operations-and-palettes/04-02-SUMMARY.md @@ -43,7 +43,7 @@ with deterministic owner-tagged operations reproducers. The six Mix mismatch records revealed that the Go value dropped TinyColor's observable raw interpolated RGBA input. Follow-up commit `d2f22c6` (`fix(04-02): preserve mix original metadata`) restored that metadata. The -final Phase 4 gate passed all `58/58` operations rows with zero mismatches. +final Phase 4 parity gate passed all `69/69` operations rows with zero mismatches. ## Commits @@ -55,4 +55,4 @@ final Phase 4 gate passed all `58/58` operations rows with zero mismatches. The corpus correctly exposed a shared-library defect after the planned adapter work. It was fixed in a separate focused micro-commit rather than masked in -the harness. \ No newline at end of file +the harness. diff --git a/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md b/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md index ec2ab630..38f6604b 100644 --- a/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md +++ b/.planning/phases/04-operations-and-palettes/04-06-SUMMARY.md @@ -3,14 +3,15 @@ phase: 4 plan: 6 subsystem: palettes-compatibility-phase-gate provides: [palette JSONL dispatch and complete Phase 4 evidence] -commits: [8776335, c328fa4, 7cd0e84, 796044f] +commits: [8776335, c328fa4, 7cd0e84, 796044f, 9e8155b, 9d7e74e, 8d2cdf3, 5e14c31] --- # Phase 4 Plan 6: Palette Compatibility and Gate Summary Added palette dispatch for all six public operations, retaining source order -and adapter-owned zero defaults. The final corpus contains 58 operations rows. -A focused follow-up repaired `Analogous` shared `original` metadata. +and adapter-owned zero defaults. The final corpus contains 69 operations rows. +Focused follow-ups repaired `Analogous` metadata, dynamic amount/WCAG behavior, +empty-candidate fallback selection, and brighten half-tie rounding. Final validation passed: Go tests and vet, adapter tests, and fixed corpora at -9/9 smoke, 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 58/58 operations. \ No newline at end of file +9/9 smoke, 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 69/69 operations. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 1cbde4c1..04436a3c 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -13,7 +13,7 @@ Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. | HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | | Conversion/formatting | Passing | `compat/cases/conversion.jsonl` | 35 fixed JSONL cases; random is invariant-only. | | Analysis/readability | Passing | `compat/cases/operations.jsonl` | WCAG defaults, thresholds, ties, fallback, and null selection. | -| Manipulation/palettes | Passing | `compat/cases/operations.jsonl` | 58 fixed operations cases cover modifiers, Mix, and ordered palettes. | +| Manipulation/palettes | Passing | `compat/cases/operations.jsonl` | 69 fixed operations cases cover modifiers, Mix, readability, and ordered palettes. | | CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | | Original Deno suite | Blocked locally | `deno` unavailable | Run when Deno is installed or in CI. | @@ -87,7 +87,7 @@ Go random generators. ## Phase 4 operations result -On 2026-08-01, the fixed operations corpus passed with **58/58 cases** and +On 2026-08-01, the fixed operations corpus passed with **69/69 cases** and **0 mismatches**. The full available Phase 1-4 gate also retained **9/9** smoke, **26/26** HEX/RGB/name, **23/23** parser, and **35/35** conversion cases with zero mismatches. This evidence does not claim a Deno source-suite From 6f6118145a1f1c59e76ff59e0a43d0fda29d7146 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 03:44:08 +0530 Subject: [PATCH 121/165] docs: resolve debug phase4-parity-gaps --- .../debug/resolved/phase4-parity-gaps.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .planning/debug/resolved/phase4-parity-gaps.md diff --git a/.planning/debug/resolved/phase4-parity-gaps.md b/.planning/debug/resolved/phase4-parity-gaps.md new file mode 100644 index 00000000..3def3314 --- /dev/null +++ b/.planning/debug/resolved/phase4-parity-gaps.md @@ -0,0 +1,113 @@ +--- +status: resolved +trigger: "Investigate and fix Phase 4 parity gaps: JS coercion/defaults, MostReadable empty fallback, Brighten half-boundary rounding, truthy non-string WCAG level/size errors, and trailing whitespace." +created: 2026-08-01T03:25:49.1964363+05:30 +updated: 2026-08-01T03:43:31.6647882+05:30 +--- + +## Current Focus + +hypothesis: Resolved; automated and independent primary-agent verification both confirm parity. +test: Archive this session and commit only the resolved debug document. +expecting: No active Phase 4 debug session remains and source stays untouched. +next_action: Move this file to `.planning/debug/resolved/` and create the final docs micro-commit. + +## Symptoms + +expected: Go compatibility behavior exactly matches local mod.js/test.js. +actual: "Reviewer reproducers: (1) modifier/mix amount JS coercion/default gaps for null/false/numeric strings; (2) MostReadable with empty candidates and fallback enabled should still choose white/black; (3) Brighten half-boundary must mirror -Math.round(-delta); (4) truthy non-string WCAG level/size should reproduce source throw/error behavior." +errors: Existing 58-row corpus passes because these cases are absent. +reproduction: Use direct Node oracle requests and new fixed JSONL rows for the reported edge cases. +started: Introduced during Phase 4 commits from base 38a7483 through 5601b29. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T03:27:00.1181439+05:30 + checked: Complete `mod.js` operations, Go facade/adapter, current unit tests/corpus, and Phase 4 research/validation. + found: Source uses `amount === 0 ? 0 : amount || fallback`, `-Math.round(-delta)`, and `(value || default).toUpperCase/toLowerCase`; Go uses float64-only decoding, `mathRound(delta)`, and string-only WCAG fields. `MostReadable` currently returns no result immediately for every empty list. + implication: All four reviewer areas have specific falsifiable divergence mechanisms; empty fallback must be probed with both readable-null and unreadable-null bases. +- timestamp: 2026-08-01T03:27:39.1157918+05:30 + checked: Twelve direct Node/Go requests covering all reported edges. + found: Eleven mismatches reproduced. Null/false/numeric-string amounts diverged for modifiers and Mix; dark-base empty fallback diverged while white-base empty fallback matched null; both brighten half signs diverged; truthy numeric level and boolean size threw in Node but returned true in Go. + implication: All four claims are valid with the MostReadable qualification that fallback occurs only when the provisional null/black candidate is not already readable. +- timestamp: 2026-08-01T03:28:28.5725482+05:30 + checked: Operations differential after adding six permanent amount cases. + found: RED reproduced exactly six mismatches; all 58 pre-existing cases passed. + implication: The failure is isolated to adapter amount defaulting/coercion, not typed modifier or Mix arithmetic. +- timestamp: 2026-08-01T03:29:26.9690590+05:30 + checked: Go suite and 64-row operations corpus after adapter fix. + found: `go test ./...` passed with repository-local GOCACHE; operations passed 64/64 with zero mismatches; focused diff check passed. + implication: Shared adapter coercion now reproduces modifier/Mix null, false, and numeric-string semantics without public API changes. +- timestamp: 2026-08-01T03:30:30.8620009+05:30 + checked: Atomic commit for amount fix. + found: Commit `9e8155b` contains only the operations corpus and Go adapter changes. + implication: Amount parity is independently recoverable and the next bug can be tested in isolation. +- timestamp: 2026-08-01T03:31:33.4612643+05:30 + checked: Focused `TestMostReadable` and 65-row differential after adding dark-base empty fallback. + found: Both RED checks fail only because Go returns no result while Node returns white; the original white-base empty case still passes. + implication: The early empty-list return is the confirmed root cause; fallback must depend on provisional null/black readability. +- timestamp: 2026-08-01T03:32:34.8788960+05:30 + checked: Focused MostReadable test, full Go suite, and 65-row operations differential after the fix. + found: All checks passed; white-base empty fallback remains null and dark-base empty fallback returns white. + implication: Selection now matches the source's null-as-black intermediate behavior without changing the nullable API. +- timestamp: 2026-08-01T03:33:05.1909523+05:30 + checked: Atomic commit for empty fallback fix. + found: Commit `9d7e74e` contains only the paired regressions and shared MostReadable change. + implication: Empty fallback parity is independently complete. +- timestamp: 2026-08-01T03:33:58.7789774+05:30 + checked: Focused modifier test and 67-row differential with exact positive/negative half ties. + found: RED failed at positive half in the unit test and exactly both half cases in the corpus; 65 prior rows passed. + implication: `mathRound(delta)` is the isolated root cause and must mirror the source's negated-round expression. +- timestamp: 2026-08-01T03:34:38.9954301+05:30 + checked: Focused modifier test, full Go suite, and 67-row operations differential after one-line rounding fix. + found: All checks passed, including both exact half signs and the pre-existing `.2` non-tie case. + implication: Brighten now matches `-Math.round(-delta)` without changing other modifiers. +- timestamp: 2026-08-01T03:35:20.2523946+05:30 + checked: Atomic commit for brighten fix. + found: Commit `8d2cdf3` contains only half-boundary regressions and the one-line delta correction. + implication: Brighten parity is independently complete. +- timestamp: 2026-08-01T03:36:05.3216422+05:30 + checked: 69-row differential after adding two truthy non-string WCAG cases. + found: RED produced exactly two mismatches with Node throwing `.toUpperCase`/`.toLowerCase` errors and Go returning true; 67 prior cases passed, including false/zero defaults. + implication: Dynamic type validation belongs only in the compatibility adapter before constructing typed WCAG options. +- timestamp: 2026-08-01T03:36:54.3807334+05:30 + checked: Full Go test/vet, adapter test, and 69-row operations differential after WCAG validation. + found: All checks passed; exact error strings and blank ids match Node, while falsy non-string defaults remain green. + implication: The adapter now preserves source throw behavior without weakening the typed Go WCAG API. +- timestamp: 2026-08-01T03:37:40.4754534+05:30 + checked: Atomic commit for WCAG fix and direct trailing-whitespace scan of `04-RESEARCH.md`. + found: Commit `5e14c31` contains only corpus/adapter changes; research lines 3, 4, and 266 each end in two spaces. + implication: The reviewer whitespace claim is confirmed and isolated to three documentation lines. +- timestamp: 2026-08-01T03:38:11.3678041+05:30 + checked: Direct whitespace scan and focused diff check after docs cleanup. + found: No trailing whitespace remains; only three line endings and the previously missing final newline changed. + implication: Documentation cleanup is ready for its independent commit. +- timestamp: 2026-08-01T03:38:43.7870812+05:30 + checked: Atomic commit for research cleanup. + found: Commit `c022dfe` contains only `04-RESEARCH.md`. + implication: All requested changes are independently committed and ready for final regression verification. +- timestamp: 2026-08-01T03:40:22.7203553+05:30 + checked: Complete Phase 1-4 gate. + found: Go test/vet and adapter test passed; corpora passed 9/9, 26/26, 23/23, 35/35, and 69/69 with zero mismatches; `git diff --check` passed; immutable oracle diff was empty. + implication: The fixes are regression-safe across all available phases; Deno remains unavailable and is not claimed. +- timestamp: 2026-08-01T03:40:58.9567769+05:30 + checked: Phase 4 evidence references and docs diff. + found: All stale 58-row current evidence references were replaced with 69/69; diff check passed. + implication: Evidence docs accurately describe the final verified gate. +- timestamp: 2026-08-01T03:41:28.3450918+05:30 + checked: Evidence-only commit and final worktree state. + found: Commit `26c3e17` records 69/69 evidence; all requested code/test/docs changes are committed independently. + implication: Automated verification is complete; only the GSD human-verify checkpoint remains before archival. +- timestamp: 2026-08-01T03:43:31.6647882+05:30 + checked: Human confirmation and independent primary-agent verification. + found: Confirmed fixed; primary-agent checks independently passed Go test/vet, adapter, all corpora, diff check, clean oracle diff, and matching oracle hashes. + implication: Resolution is confirmed end-to-end and the session can be archived. + +## Resolution + +root_cause: Adapter coercion, empty-list fallback selection, brighten tie rounding, and WCAG dynamic type handling each flattened a distinct JavaScript behavior. +fix: Amount coercion now follows strict-zero/truthiness and numeric conversion; empty MostReadable lists evaluate the source's null/black provisional candidate before fallback; Brighten uses `-mathRound(-delta)`; WCAG truthy non-strings return the oracle's exact errors; Phase 4 evidence and whitespace were refreshed. +verification: Full gate passed locally and independently under the primary agent: Go test/vet, adapter tests, corpora 9/9 + 26/26 + 23/23 + 35/35 + 69/69, diff check, empty immutable-oracle diff, and matching oracle hashes. Human confirmed fixed. Deno unavailable and not claimed. +files_changed: [compat/cases/operations.jsonl, src/cmd/tinycolor-compat/main.go, src/tinycolor/color.go, src/tinycolor/operations_test.go, COMPATIBILITY.md, .planning/phases/04-operations-and-palettes/04-02-SUMMARY.md, .planning/phases/04-operations-and-palettes/04-06-SUMMARY.md, .planning/phases/04-operations-and-palettes/04-RESEARCH.md] From 1c6cbace37f810716e0a794275306d1b9c0e7e87 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 03:54:00 +0530 Subject: [PATCH 122/165] docs(05): define delivery evidence design --- ...-08-01-phase-5-delivery-evidence-design.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md diff --git a/docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md b/docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md new file mode 100644 index 00000000..9cc4396b --- /dev/null +++ b/docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md @@ -0,0 +1,90 @@ +# Phase 5 Delivery Evidence Design + +## Goal + +Turn the completed TinyColor Go port into a reproducible Track H submission +that a judge can build, exercise, measure, and audit from a clean checkout. + +## Submission structure + +Keep the existing advisory layout as the actual submission layout: + +```text +TinyColor/ +├── README.md +├── DECISIONS.md +├── Dockerfile +├── src/ +├── tests/original/ +├── tests/port/ +├── fuzz/ +│ ├── harness.mjs +│ └── log.txt +├── bench/ +│ ├── methodology.md +│ └── results.json +└── .port-mortem.toml +``` + +Existing project files remain in place. No parallel submission tree or new +dependency is introduced. + +## CLI + +Reuse the existing Go compatibility binary and TinyColor package. Add a human +command interface for `parse`, `convert`, `lighten`, `palette`, and `contrast`. +Every command accepts `--json`; JSON output is deterministic and suitable for +demo comparisons. Invalid commands and missing values return nonzero status +with a concise stderr message. The JSONL compatibility mode remains available +so the differential driver does not change. + +## Reproducible checks and CI + +`make build` produces `bin/tinycolor`. `make verify` runs formatting checks, +Go tests, vet, immutable-oracle hash verification, adapter tests, and every +fixed differential corpus. GitHub Actions invokes the same commands rather +than maintaining a second CI-only procedure. Docker continues to build the +same binary in one command. + +## Differential fuzz evidence + +Add a standard-library Node harness that generates deterministic and randomized +public-API requests, sends the identical stream to the checked-in JavaScript +oracle and Go port, and reports duration, seed, case count, and divergences. +Publish an actual run lasting at least 60 continuous seconds in `fuzz/log.txt`. +A zero-divergence bonus is claimed only if that run finishes with zero +divergences; any mismatch is retained as a reproducible input. + +## Benchmark evidence + +Use one shared workload file for the original JavaScript and Go runners. +Measure cold startup, request latency p99, throughput, and peak RSS with the +same host and tool versions. Record the commands and limitations in +`bench/methodology.md` and machine-readable measurements in +`bench/results.json`. Results state sample counts and do not infer universal +speedups from one machine. + +## Documentation and scoring evidence + +Rewrite the root README around the Go port while retaining upstream attribution +and links. Expand `DECISIONS.md` to at least ten non-trivial, defensible +divergences. Update `COMPATIBILITY.md` with pass rates per corpus, immutable +hash status, unsafe count, coverage data where measured, and known limitations. +Keep team ownership and license attribution explicit. Add a five-minute demo +script, but do not claim that a video was recorded or published unless an +actual video artifact or URL is supplied. + +## Verification + +The phase closes only after CLI tests pass, `make build` and `make verify` +succeed, all pinned oracle hashes match, the 60-second fuzz session is recorded, +benchmark JSON validates, documentation contains no unsupported claims, and +the worktree passes `git diff --check`. Deno remains an honest environment gap +unless it is installed and the untouched source suite is run successfully. + +## Deliberate exclusions + +- No GUI, package publication, transpiler, JavaScript runtime embedding, or FFI. +- No new dependency when Go or Node standard libraries cover the need. +- No speculative Bug Catcher claim; document it only if differential testing + produces a real upstream defect. From 53a986b4f13cd6e828de2599d4db0c073bca8135 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 03:55:07 +0530 Subject: [PATCH 123/165] docs(05): capture delivery decisions --- .../phases/05-delivery-evidence/05-CONTEXT.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-CONTEXT.md diff --git a/.planning/phases/05-delivery-evidence/05-CONTEXT.md b/.planning/phases/05-delivery-evidence/05-CONTEXT.md new file mode 100644 index 00000000..db064835 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-CONTEXT.md @@ -0,0 +1,128 @@ +# Phase 5: Delivery Evidence - Context + +**Gathered:** 2026-08-01 +**Status:** Ready for planning +**Source:** Approved Phase 5 delivery design + + +## Phase Boundary + +Ship the completed Go port as a reproducible Track H submission. This phase +adds the human CLI, clean-checkout verification, CI, differential fuzz evidence, +honest benchmark evidence, and judge-facing documentation. It does not add new +TinyColor behavior or change the immutable JavaScript oracle. + + + + +## Implementation Decisions + +### Submission layout +- Keep `README.md`, `DECISIONS.md`, `Dockerfile`, `src/`, `tests/original/`, + `tests/port/`, `fuzz/`, `bench/`, and `.port-mortem.toml` in their current + top-level locations. +- Do not create a parallel submission tree. +- Preserve `mod.js`, `tinycolor.js`, `test.js`, `npm/`, `dist/`, and `demo/` as + immutable oracle material. + +### CLI and build +- Reuse the existing Go compatibility binary and `tinycolor` package. +- Provide `parse`, `convert`, `lighten`, `palette`, and `contrast` commands. +- Every human command supports deterministic `--json` output. +- Preserve JSONL compatibility mode for `compat/run.mjs`. +- `make build` produces `bin/tinycolor`; Docker builds the same executable. + +### Verification and CI +- `make verify` checks Go formatting, tests, vet, immutable oracle hashes, + adapter tests, and every fixed differential corpus. +- GitHub Actions calls the documented repository commands instead of duplicating + a separate CI-only procedure. +- Do not claim the untouched Deno suite passed unless Deno is installed and + `deno task test` actually succeeds. + +### Differential fuzzing +- Use Node and Go standard libraries only. +- Run identical generated public-API requests through the checked-in JavaScript + oracle and Go port. +- Publish a real `fuzz/log.txt` from at least 60 continuous seconds, including + duration, seed, case count, and divergence count. +- Claim Differential Fuzz Survivor only if the recorded run has zero divergences. + +### Benchmarks +- Use a shared workload for JavaScript and Go on the same machine. +- Report cold startup, latency p99, throughput, and peak RSS with sample counts, + tool versions, commands, and limitations. +- Store machine-readable values in `bench/results.json` and methodology in + `bench/methodology.md`; do not generalize one machine's result. + +### Documentation +- Make the root README describe the Go port, migration rationale, one-command + build, CLI examples, verification, evidence, known limitations, and upstream + attribution. +- Record at least ten substantive decisions in `DECISIONS.md`. +- Report corpus pass rates, oracle hash status, unsafe count, coverage where + measured, and all known limitations in `COMPATIBILITY.md`. +- Keep team ownership and MIT attribution explicit. +- Add a five-minute demo script. Do not claim a recorded or published video + without an actual video artifact or URL. + +### the agent's Discretion +- Exact CLI flag spelling and JSON field arrangement, provided outputs are stable + and tests demonstrate all five required commands. +- Exact deterministic fuzz input distribution and benchmark sample sizes. +- CI runner versions and artifact naming, provided clean-checkout commands match + local documentation. + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Approved delivery contract +- `docs/superpowers/specs/2026-08-01-phase-5-delivery-evidence-design.md` — locked + Phase 5 structure, evidence, and honest-claim rules. +- `.planning/ROADMAP.md` — Phase 5 goal and success criteria. +- `.planning/REQUIREMENTS.md` — DEL-01 and QLT-03 requirements. + +### Project behavior and boundaries +- `AGENT.md` — immutable oracle, ownership, checks, and definition of done. +- `PLAN.md` — Wave 4 delivery scope and gate. +- `src/cmd/tinycolor-compat/main.go` — existing JSONL command boundary to reuse. +- `compat/run.mjs` — existing differential execution path. +- `tests/original/manifest.sha256` — pinned oracle hashes. + +### Existing delivery files +- `Makefile` — current build and test entry points. +- `Dockerfile` — current one-command artifact build. +- `DECISIONS.md` — accepted architectural decisions. +- `COMPATIBILITY.md` — current evidence and honest limitations. +- `docs/TEAM-OWNERSHIP.md` — contributor responsibilities. + + + + +## Specific Ideas + +- Favor the attainable Differential Fuzz Survivor, Zero Unsafe, and Decision Log + bonus evidence. +- Keep every implementation slice in a separate micro-commit so one feature can + be reverted without undoing unrelated Phase 5 work. + + + + +## Deferred Ideas + +- A GUI, web service, package publication, transpiler, FFI, and JavaScript + runtime embedding remain out of scope. +- Bug Catcher is not a planned claim; add it only if differential testing finds + a reproducible upstream defect. + + + +--- + +*Phase: 05-delivery-evidence* +*Context gathered: 2026-08-01 from approved delivery design* From 57385754e47c9471a71113771266f67b6a5938a3 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 04:02:59 +0530 Subject: [PATCH 124/165] docs(05): research delivery evidence --- .../05-delivery-evidence/05-RESEARCH.md | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-RESEARCH.md diff --git a/.planning/phases/05-delivery-evidence/05-RESEARCH.md b/.planning/phases/05-delivery-evidence/05-RESEARCH.md new file mode 100644 index 00000000..16f28a38 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-RESEARCH.md @@ -0,0 +1,158 @@ +# Phase 5 Research: Delivery Evidence + +## Scope and current state + +Phase 5 is an integration phase, not a new color-behavior phase. The Go package +and JSONL adapter already cover the required TinyColor operations. Delivery +work should expose those paths, make every claim reproducible, and replace +placeholders with measured evidence. + +Current gaps: + +- `src/cmd/tinycolor-compat/main.go` only reads JSONL from stdin; there is no + human command interface or command-level test. +- `Makefile` builds `bin/tinycolor-compat` and omits conversion/operation + corpora, formatting, hash verification, and a single full gate. +- `.github/workflows/deno.yml` is the upstream Deno build workflow, not the Go + port CI gate. +- `fuzz/README.md`, `bench/methodology.md`, and `bench/results.json` are Phase 1 + placeholders. +- The root `README.md` describes the JavaScript package rather than the port. +- `DECISIONS.md` has seven entries; the Decision Log bonus needs ten substantive + decisions. +- `COMPATIBILITY.md` has correct fixed-corpus evidence but no Phase 5 CLI, hash, + fuzz, benchmark, unsafe, or coverage evidence. + +## Recommended architecture + +### Human CLI without a second binary + +Keep `cmd/tinycolor-compat` as the only executable. Select JSONL mode when no +positional arguments are present and human mode when a command is present. +Move argument parsing and rendering into small functions in the same package so +`main_test.go` can test them without process fixtures. Reuse `tinycolor.Parse`, +conversion methods, modifiers, palettes, and readability directly; do not route +human commands through encoded JSONL requests. + +Use Go's `flag.FlagSet` per command. Required surface: + +- `parse `: inspection/state. +- `convert --to hex|hex8|rgb|percentage-rgb|hsl|hsv|name`. +- `lighten [--amount 10]`. +- `palette --type complement|splitcomplement|triad|tetrad|analogous|monochromatic`. +- `contrast `: ratio and AA/AAA small/large booleans. +- `--json` on every command; stable object/array output through `encoding/json`. + +Usage errors must return status 2, parse/runtime errors status 1, and success 0. +Invalid TinyColor input remains a successful black-like TinyColor value because +that is source behavior; JSON inspection exposes `valid: false`. + +### One source of truth for checks + +Add `tests/original/verify.mjs` using `node:crypto` to validate +`manifest.sha256` on Windows and Linux. Expand the Makefile: + +- `make build` -> `bin/tinycolor`. +- `make test` -> port-owned unit/adapter/differential checks. +- `make verify` -> formatting check, tests, vet, oracle hash verification, and + all five fixed corpora. +- `make fuzz` and `make bench` -> documented evidence generators. + +CI should install pinned Go, Node, and Deno versions; call `make verify`; run the +untouched `deno task test`; and build the binary. Do not duplicate individual +test commands in YAML beyond the Deno-only source-suite check. + +### Differential fuzzing + +The current `compat/run.mjs` starts `go run` once per case, which is too slow +for a 60-second fuzz session. `fuzz/harness.mjs` should build the Go binary once, +start one JavaScript runner and one Go runner, stream the same JSONL requests to +both, and compare response lines with `isDeepStrictEqual`. Use an explicit +seeded PRNG and public operations already accepted by the adapter. Print a +machine-readable header/footer and every mismatch request. Exit nonzero on any +divergence. + +The committed `fuzz/log.txt` must be output from an actual `--duration 60` +session. A deterministic seed makes a mismatch reproducible even though the +case count varies by machine. The fixed corpus remains the correctness gate; +the fuzz run is additional evidence. + +### Honest benchmarks + +Use `bench/workload.jsonl` as the identical request stream for both runners. +`bench/run.mjs` should build once, measure multiple cold process starts, then +measure a persistent runner for latency and throughput. Compute p99 by sorting +all elapsed samples and selecting `ceil(0.99*n)-1`. Sample peak RSS while each +child is alive: read `/proc//status` on Linux and query +`Get-Process -Id ` on Windows. If RSS sampling is unsupported, emit `null` +and a limitation instead of inventing a value. + +`bench/results.json` needs timestamp, OS/CPU, Go/Node versions, workload hash, +sample counts, and for each implementation: startup p99, request latency p99, +throughput, and peak RSS. Run both implementations on the same host in one +command and state that results are local observations, not universal speedups. + +### Documentation + +Rewrite `README.md` for judges but retain the upstream project link and MIT +attribution. Link detailed evidence instead of duplicating it. Expand +`DECISIONS.md` with the CLI dual mode, immutable manifest verification, +standard-library fuzz harness, shared benchmark workload, and honest Deno/video +claim policy. Update `COMPATIBILITY.md` only after commands have produced fresh +results. Add `docs/DEMO.md` as a five-minute live script; video recording remains +a manual submission action. + +## Pitfalls + +- Go `flag` stops parsing at the first positional argument. Either document + flags before values or normalize the small supported grammar before parsing; + tests must lock the chosen syntax. +- Do not make invalid TinyColor inputs CLI errors; that would contradict source + behavior. +- Keep JSONL stdin mode byte-compatible with existing adapter tests. +- Do not benchmark `go run`; compile once before measuring. +- Do not compare a hot Go function with a cold Node process. Shared workload and + lifecycle are required. +- A generated zero-divergence log is credible only when duration and seed are + recorded and the harness itself is checked in. +- `unsafe` is a lexical/code audit for this pure-Go repository; report the exact + command and count rather than merely saying "safe". +- Go coverage and Deno coverage measure different suites. Report them separately + and do not describe their percentage difference as behavioral parity. + +## Validation Architecture + +### Fast feedback + +| Change | Command | Expected result | +|---|---|---| +| CLI | `go -C src test ./cmd/tinycolor-compat` | exit 0 | +| Hash verifier | `node tests/original/verify.mjs` | 3 files verified | +| Fuzz harness | `node fuzz/harness.mjs --duration 1 --seed 1` | zero divergences | +| Benchmark runner | `node bench/run.mjs --quick` | valid `bench/results.json` | +| Documentation | `git diff --check` | exit 0 | + +### Full phase gate + +```powershell +make build +make verify +node fuzz/harness.mjs --duration 60 --seed 20260801 +node bench/run.mjs +git diff --check +``` + +Then verify `tests/original/manifest.sha256` still matches, validate +`bench/results.json` with `JSON.parse`, confirm `fuzz/log.txt` records at least +60 seconds and zero divergences before claiming the bonus, and run +`deno task test` only where Deno is installed. + +### Requirement mapping + +| Requirement | Automated evidence | +|---|---| +| DEL-01 | CLI tests, `make build`, CI, fuzz run, benchmark JSON, documentation checks | +| QLT-03 | `make verify`, oracle manifest verifier, fixed corpora, clean-checkout CI | + +Existing Go tests and JSONL corpora are sufficient infrastructure. No new test +framework or dependency is needed. From 87435841e053062deb590a79bcada91a17644a9b Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 04:03:35 +0530 Subject: [PATCH 125/165] docs(05): add validation strategy --- .../05-delivery-evidence/05-VALIDATION.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-VALIDATION.md diff --git a/.planning/phases/05-delivery-evidence/05-VALIDATION.md b/.planning/phases/05-delivery-evidence/05-VALIDATION.md new file mode 100644 index 00000000..13bc34db --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-VALIDATION.md @@ -0,0 +1,66 @@ +--- +phase: 5 +slug: delivery-evidence +status: draft +nyquist_compliant: true +wave_0_complete: true +created: 2026-08-01 +--- + +# Phase 5 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Go `testing`, Node built-in modules, existing JSONL differential harness | +| **Config file** | `src/go.mod`, `deno.json`, `Makefile` | +| **Quick run command** | `go -C src test ./...` | +| **Full suite command** | `make verify` | +| **Estimated runtime** | quick under 10 seconds; full under 5 minutes | + +## Sampling Rate + +- **After every task commit:** Run the task's focused command and `go -C src test ./...` when Go changed. +- **After every plan wave:** Run `make verify`. +- **Before `$gsd-verify-work`:** `make verify`, the recorded fuzz command, and benchmark validation must be green. +- **Max feedback latency:** 10 seconds for focused tests; 5 minutes for the full gate. + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|-----------|-------------------|-------------|--------| +| 05-01-01 | 01 | 1 | DEL-01 | CLI unit | `go -C src test ./cmd/tinycolor-compat` | ✅ | ⬜ pending | +| 05-01-02 | 01 | 1 | QLT-03 | integration | `make build && make verify` | ✅ make / ❌ expanded targets | ⬜ pending | +| 05-02-01 | 02 | 2 | DEL-01 | differential fuzz | `node fuzz/harness.mjs --duration 1 --seed 1` | ❌ Wave 2 | ⬜ pending | +| 05-02-02 | 02 | 2 | DEL-01 | benchmark smoke | `node bench/run.mjs --quick` | ❌ Wave 2 | ⬜ pending | +| 05-03-01 | 03 | 3 | DEL-01 | evidence audit | `node tests/original/verify.mjs` | ❌ Wave 1 | ⬜ pending | +| 05-03-02 | 03 | 3 | QLT-03 | full gate | `make verify` | ✅ make / ❌ expanded target | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +## Wave 0 Requirements + +Existing Go tests, Node adapter tests, JSONL corpora, and Make are sufficient. +Each new runner supplies its own focused standard-library check; no framework +installation or test scaffold is required. + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Five-minute video is actually recorded and published | DEL-01 | Recording and public hosting require a human account and final presentation | Follow `docs/DEMO.md`, record one continuous run, publish it, then add the verified URL to README | +| Public GitHub visibility | DEL-01 | Repository visibility is external account state | Open the GitHub repository logged out and confirm source and release instructions are readable | + +## Validation Sign-Off + +- [x] All planned tasks have an automated focused command or existing infrastructure. +- [x] Sampling continuity has no three consecutive tasks without automated verification. +- [x] Existing infrastructure covers Wave 0. +- [x] No watch-mode flags are used. +- [x] Focused feedback latency target is under 10 seconds. +- [x] `nyquist_compliant: true` is set in frontmatter. + +**Approval:** approved 2026-08-01 From 14d257943fd68b66c5ef9be6c7cd1df256752ad6 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 04:19:23 +0530 Subject: [PATCH 126/165] docs(05): plan delivery evidence --- .../phases/05-delivery-evidence/05-01-PLAN.md | 109 +++++++++++++ .../phases/05-delivery-evidence/05-02-PLAN.md | 110 +++++++++++++ .../phases/05-delivery-evidence/05-03-PLAN.md | 149 ++++++++++++++++++ .../05-delivery-evidence/05-VALIDATION.md | 5 +- 4 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/05-delivery-evidence/05-01-PLAN.md create mode 100644 .planning/phases/05-delivery-evidence/05-02-PLAN.md create mode 100644 .planning/phases/05-delivery-evidence/05-03-PLAN.md diff --git a/.planning/phases/05-delivery-evidence/05-01-PLAN.md b/.planning/phases/05-delivery-evidence/05-01-PLAN.md new file mode 100644 index 00000000..8b32668e --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-01-PLAN.md @@ -0,0 +1,109 @@ +--- +phase: 5 +plan: 1 +type: execute +subsystem: cli-build-ci +owner: D-deepali +tags: [cli, build, verification, ci, docker] +wave: 1 +depends_on: [] +files_modified: [src/cmd/tinycolor-compat/main.go, src/cmd/tinycolor-compat/main_test.go, tests/original/verify.mjs, tests/original/verify.test.mjs, Makefile, Dockerfile, .github/workflows/port.yml, .github/workflows/deno.yml] +autonomous: true +requirements: [DEL-01, QLT-03] +must_haves: + truths: + - "One executable supports parse, convert, lighten, palette, contrast, and the existing JSONL adapter mode." + - "One documented build command produces bin/tinycolor and one verify command checks every available local gate." + - "CI calls the repository verification command and runs the untouched Deno source suite separately." + artifacts: + - path: "src/cmd/tinycolor-compat/main_test.go" + provides: "Command-level regression coverage for all required human CLI operations and JSONL mode" + - path: "tests/original/verify.mjs" + provides: "Cross-platform immutable-oracle hash verification" + - path: ".github/workflows/port.yml" + provides: "Clean-checkout build, parity, source-suite, and artifact CI" + key_links: + - from: "Makefile" + to: "tests/original/manifest.sha256" + via: "node tests/original/verify.mjs" + pattern: "tests/original/verify.mjs" + - from: ".github/workflows/port.yml" + to: "Makefile" + via: "make verify" + pattern: "make verify" +--- + +Expose the completed port through a tested human CLI and make its build and parity gate reproducible locally, in Docker, and in CI. +@AGENT.md +@.planning/phases/05-delivery-evidence/05-CONTEXT.md +@.planning/phases/05-delivery-evidence/05-RESEARCH.md +@.planning/phases/05-delivery-evidence/05-VALIDATION.md +@src/cmd/tinycolor-compat/main.go +@src/tinycolor/color.go +@src/internal/compat/protocol.go +@compat/run.mjs +@tests/port/adapter.test.mjs +@tests/original/manifest.sha256 +@Makefile +@Dockerfile +@deno.json + + +Add the dual-mode human CLI without changing JSONL behavior +src/cmd/tinycolor-compat/main.go, src/cmd/tinycolor-compat/main_test.go +Read `src/cmd/tinycolor-compat/main.go` completely, then read exported methods and structs in `src/tinycolor/color.go`, response encoding in `src/internal/compat/protocol.go`, and stdin contract assertions in `tests/port/adapter.test.mjs`. + +RED: add table tests around `run(args []string, stdin io.Reader, stdout, stderr io.Writer) int`. Assert no args still accepts one JSONL `inspect` request. Assert `parse --json red` returns a JSON object with `hex:"ff0000"` and `valid:true`; `convert --to hsl red` prints `hsl(0, 100%, 50%)`; `lighten --amount 10 #000` prints `#1a1a1a`; `palette --type triad red` prints three ordered hex colors; and `contrast #000 #fff --json` returns ratio `21` plus AA/AAA small/large booleans. Assert unknown command and missing required flags return status 2 with usage on stderr. Run the focused test and confirm RED. + +GREEN: make `main` call `os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))`. With zero args, retain the current line-scanner JSONL loop and return zero after EOF; malformed individual requests still emit protocol failures without terminating the stream. With args, use a command-specific `flag.FlagSet` with `ContinueOnError`; flags precede positional color values. Implement exactly: `parse [--json] `, `convert --to hex|hex8|rgb|percentage-rgb|hsl|hsv|name [--json] `, `lighten [--amount 10] [--json] `, `palette --type complement|splitcomplement|triad|tetrad|analogous|monochromatic [--results 6] [--slices 30] [--json] `, and `contrast [--json] `. Human colors are strings passed to `tinycolor.FromCompat(value, false)`; invalid input remains a successful TinyColor value and JSON exposes `valid:false`. Human parse JSON is `Inspect()`, palette JSON is an array of `Inspect()` objects, and contrast JSON is `{ratio, aaSmall, aaLarge, aaaSmall, aaaLarge}` using `Readability` and `IsReadable`. Use `encoding/json`; no new package or binary. Run focused and full Go tests, then commit `feat(05-01): add human CLI commands`. + + +- `src/cmd/tinycolor-compat/main.go` contains `func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int`. +- Tests invoke all five command names and the zero-argument JSONL path. +- `go -C src test ./cmd/tinycolor-compat -count=1` exits 0. +- `node tests/port/adapter.test.mjs` exits 0, proving compatibility mode did not change. + +go -C src test ./cmd/tinycolor-compat -count=1; go -C src test ./...; node tests/port/adapter.test.mjs +The single binary is useful to humans and remains byte-compatible with the existing differential adapter. + + + +Add cross-platform oracle verification and one-command local gates +tests/original/verify.mjs, tests/original/verify.test.mjs, Makefile, Dockerfile +Read `tests/original/manifest.sha256`, `Makefile`, `Dockerfile`, `.gitignore`, `compat/run.mjs`, and every filename in `compat/cases/` before editing. + +RED: add a Node built-in test that copies a two-line manifest and files into a temporary directory, proves `verifyManifest` accepts correct SHA-256 hashes, then changes one file and proves it reports that path. Run `node --test tests/original/verify.test.mjs` and confirm RED because the verifier does not exist. + +GREEN: implement `tests/original/verify.mjs` with `node:crypto`, `node:fs`, and `node:path`. Export `verifyManifest(manifestPath, root)` and make direct execution print `verified: 3` on success, print every missing/mismatched relative path on stderr, and exit 1 on failure. Expand Make targets to `.PHONY: build test verify fmt-check hashes fuzz bench clean`; `build` produces `bin/tinycolor`; `fmt-check` fails when `gofmt -l` prints any `.go` file under `src`; `hashes` runs the verifier; `test` runs Node adapter tests, `go -C src test ./...`, and all five corpora (smoke, parser-hex-rgb, parser, conversion, operations); `verify` depends on fmt-check, hashes, test and also runs `go -C src vet ./...`. Keep `clean` limited to the explicit ignored `bin/tinycolor` artifact. Update Docker to output `/tinycolor`, set it as ENTRYPOINT, and keep the scratch runtime. Run the full gate, then commit `build(05-01): add reproducible verification gate`. + + +- `node tests/original/verify.mjs` prints exactly `verified: 3` and exits 0. +- `make build` creates ignored `bin/tinycolor` and the binary runs `parse red` successfully. +- `make verify` runs every corpus including conversion and operations and exits 0. +- Dockerfile contains `ENTRYPOINT ["/tinycolor"]` and does not copy the JavaScript runtime into the final image. + +node --test tests/original/verify.test.mjs; node tests/original/verify.mjs; make build; ./bin/tinycolor parse red; make verify +A clean checkout has one build command, one complete local verification command, and a portable immutable-oracle check. + + + +Replace the upstream-only workflow with port delivery CI +.github/workflows/port.yml, .github/workflows/deno.yml +Read `.github/workflows/deno.yml`, `deno.json`, `src/go.mod`, `Makefile`, and `tests/original/manifest.sha256` completely. + +Delete `.github/workflows/deno.yml` after transferring its source-suite intent into `.github/workflows/port.yml`. Configure push and pull_request, `contents: read`, Ubuntu, checkout v4, setup-go v5 using the exact version from `src/go.mod`, setup-node v4 with Node 24, and setup-deno v2 with Deno v2.x. Run `make verify`, then `deno task test` against untouched source files, then `make build`, then upload `bin/tinycolor` with upload-artifact v4. The CI file must not run `deno task build`, npm publication, or mutate generated upstream distributions. Run a YAML text audit and local `make verify`, then commit `ci(05-01): verify port and source suite`. + + +- `.github/workflows/port.yml` contains `make verify`, `deno task test`, `make build`, and `actions/upload-artifact@v4`. +- `.github/workflows/deno.yml` no longer exists, leaving one CI source of truth. +- The workflow has `permissions: contents: read` and no publish permission or deployment step. +- `make verify` still exits 0 locally; local documentation does not claim the CI or Deno run passed before GitHub executes it. + +rg -n "make verify|deno task test|make build|upload-artifact@v4|contents: read" .github/workflows/port.yml; if (Test-Path .github/workflows/deno.yml) { exit 1 }; make verify +CI performs the same local gate, adds the environment-only untouched Deno suite, and publishes the working Go binary. + + +Run `make verify`, confirm the CLI examples return exit 0, confirm oracle hashes match, and inspect the workflow for build/test-only permissions. A local Deno pass is not required or claimed when Deno is unavailable. +- DEL-01 has a tested human CLI, one-command build, Docker artifact, and CI. +- QLT-03 has a cross-platform manifest verifier and one complete clean-checkout command. +After completion, create `.planning/phases/05-delivery-evidence/05-01-SUMMARY.md`. diff --git a/.planning/phases/05-delivery-evidence/05-02-PLAN.md b/.planning/phases/05-delivery-evidence/05-02-PLAN.md new file mode 100644 index 00000000..951ce851 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-02-PLAN.md @@ -0,0 +1,110 @@ +--- +phase: 5 +plan: 2 +type: execute +subsystem: fuzz-benchmark-evidence +owner: C-mrashis +tags: [fuzz, differential, benchmark, p99, rss] +wave: 2 +depends_on: [05-01] +files_modified: [fuzz/harness.mjs, fuzz/harness.test.mjs, fuzz/validate-log.mjs, fuzz/validate-log.test.mjs, fuzz/README.md, fuzz/log.txt, bench/workload.jsonl, bench/run.mjs, bench/run.test.mjs, bench/methodology.md, bench/results.json, Makefile] +autonomous: true +requirements: [DEL-01, QLT-03] +must_haves: + truths: + - "A checked-in harness runs identical public requests against JavaScript and Go for at least 60 continuous seconds." + - "The fuzz log records the actual seed, elapsed duration, case count, and every divergence; zero is claimed only when measured." + - "Original and port benchmarks use one workload and report startup p99, latency p99, throughput, and peak RSS or an explicit unsupported value." + artifacts: + - path: "fuzz/harness.mjs" + provides: "Seeded persistent-process differential fuzz runner" + - path: "fuzz/log.txt" + provides: "Published output of an actual 60-second session" + - path: "bench/results.json" + provides: "Machine-readable same-host benchmark evidence" + key_links: + - from: "fuzz/harness.mjs" + to: "compat/js-runner.mjs" + via: "persistent JSONL child process" + pattern: "compat/js-runner.mjs" + - from: "bench/run.mjs" + to: "bench/workload.jsonl" + via: "identical request stream" + pattern: "workload.jsonl" +--- + +Publish reproducible differential fuzz and honest original-versus-port performance evidence on shared public workloads. +@AGENT.md +@.planning/phases/05-delivery-evidence/05-CONTEXT.md +@.planning/phases/05-delivery-evidence/05-RESEARCH.md +@.planning/phases/05-delivery-evidence/05-VALIDATION.md +@.planning/phases/05-delivery-evidence/05-01-SUMMARY.md +@compat/js-runner.mjs +@compat/run.mjs +@src/cmd/tinycolor-compat/main.go +@compat/cases/operations.jsonl +@fuzz/README.md +@bench/methodology.md +@bench/results.json + + +Build a deterministic persistent-process differential fuzz harness +fuzz/harness.mjs, fuzz/harness.test.mjs, fuzz/README.md, Makefile +Read `compat/js-runner.mjs`, `compat/run.mjs`, `src/cmd/tinycolor-compat/main.go`, all five `compat/cases/*.jsonl` files, `fuzz/README.md`, and the Phase 5 research fuzz section. + +RED: create Node built-in tests for a seeded PRNG (seed 1 repeats the same first sequence), request generation (stable IDs and only supported operations), CLI option validation (`--duration` positive seconds and integer `--seed`), and a one-second end-to-end run that returns a summary with elapsedSeconds >= 1, cases > 0, and divergences 0. Run `node --test fuzz/harness.test.mjs` and confirm RED. + +GREEN: implement `fuzz/harness.mjs` with Node standard libraries only. Parse `--duration ` default 60 and `--seed ` default 20260801. Build `bin/tinycolor` once through `go -C src build -o ../bin/tinycolor ./cmd/tinycolor-compat`, start one `node compat/js-runner.mjs` and one `bin/tinycolor` child, and exchange newline-delimited requests sequentially. Generate seeded string/object inputs across parse/output, modify, mix, readability, isReadable, and palette operations using adapter-supported method names and bounded numeric edge values. Compare parsed response objects with `isDeepStrictEqual`; print each mismatch as one JSON line containing request, JavaScript, and Go. Always print final `duration_seconds`, `seed`, `cases`, and `divergences`; terminate children; exit 1 for divergence or protocol/process failure. Add `make fuzz` for the exact 60-second command and replace the placeholder README with reproduction and claim rules. Run tests and a one-second smoke, then commit `feat(05-02): add differential fuzz harness`. + + +- `node --test fuzz/harness.test.mjs` exits 0 and includes a one-second real differential run. +- Two runs with `--duration 1 --seed 1` generate the same request prefix and both report zero divergences. +- Harness source contains no `Math.random`, dependency import, shell-out to the JavaScript oracle from the Go port, or test-suite mutation. +- `make fuzz` invokes `node fuzz/harness.mjs --duration 60 --seed 20260801`. + +node --test fuzz/harness.test.mjs; node fuzz/harness.mjs --duration 1 --seed 1; rg -n "duration 60|seed 20260801" Makefile fuzz/README.md +A public seeded harness can sustain a real differential session without recompiling or spawning per case. + + + +Record the actual 60-second fuzz survivor evidence +fuzz/validate-log.mjs, fuzz/validate-log.test.mjs, fuzz/log.txt +Read `fuzz/harness.mjs`, `fuzz/README.md`, and any existing `fuzz/log.txt`. Confirm `make verify` is green before recording evidence. + +RED: add `fuzz/validate-log.test.mjs` using Node's built-in test runner and temporary files. Assert rejection of duration 59.999, wrong seed, zero cases, nonzero divergences, malformed numeric values, and every missing field; assert one log with duration 60, seed 20260801, positive cases, and zero divergences passes. Run the focused test and confirm RED because the validator does not exist. + +GREEN: add `fuzz/validate-log.mjs` as a small standard-library parser that exports its validation function, reads a log path when run directly, requires a numeric `duration_seconds >= 60`, exact integer `seed === 20260801`, integer `cases > 0`, and integer `divergences === 0`, prints `fuzz log verified`, and exits 1 with the failed field otherwise. Run `node fuzz/harness.mjs --duration 60 --seed 20260801` and capture its unedited stdout in `fuzz/log.txt`; do not hand-write, truncate, or normalize duration/case count. If divergences are nonzero, retain the failed log outside the final artifact, stop this plan, and open a separate regression/root-cause task owned by the implicated module. That recovery task must commit the deterministic corpus row and root-cause fix separately before this evidence task is rerun. Do not edit corpora, tests, or source within this log-only task. After focused negative/positive tests and the real log validator pass, commit validator, tests, and final log as `test(05-02): record 60-second fuzz session`. + + +- `node fuzz/validate-log.mjs fuzz/log.txt` exits 0 only when duration is at least 60 seconds, seed is 20260801, cases is positive, and divergences is zero. +- `node --test fuzz/validate-log.test.mjs` proves every invalid/missing field fails and a valid log passes. +- The log contains no claim that cannot be regenerated by the documented command. +- `make verify` exits 0 after the recorded run. + +node --test fuzz/validate-log.test.mjs; node fuzz/harness.mjs --duration 1 --seed 20260801; node fuzz/validate-log.mjs fuzz/log.txt; make verify +The repository contains genuine, reproducible Differential Fuzz Survivor evidence. + + + +Measure shared-workload p99, RSS, startup, and throughput +bench/workload.jsonl, bench/run.mjs, bench/run.test.mjs, bench/methodology.md, bench/results.json, Makefile +Read `compat/cases/smoke.jsonl`, `compat/cases/conversion.jsonl`, `compat/cases/operations.jsonl`, `compat/js-runner.mjs`, `src/cmd/tinycolor-compat/main.go`, `bench/methodology.md`, and `bench/results.json`. + +RED: add Node built-in tests for `percentile99([1,2,3,4]) === 4`, workload parsing, results-schema validation, and `node bench/run.mjs --quick --output ` producing nonnegative startup/latency/RSS and positive throughput for both `javascript` and `go` without modifying `bench/results.json`. Confirm RED. + +GREEN: create a fixed `bench/workload.jsonl` of representative parse, conversion, modifier, palette, and readability requests copied from existing public corpora. Implement `bench/run.mjs` with Node standard libraries and required `--output ` support; default output is `bench/results.json`, while tests and `--quick` verification always pass a temporary output path. Build Go once; hash the workload; collect at least 20 cold-start samples normally (3 with `--quick`) by starting each runner, sending one request, receiving one response, and stopping it; collect at least 1,000 persistent request samples normally (30 quick) for latency and throughput; sort nanosecond samples and select index `ceil(0.99*n)-1`; sample peak child RSS from `/proc//status` on Linux and `Get-Process -Id ` on Windows, emitting null plus a limitation string only when unsupported. Write results containing generatedAt, OS, architecture, CPU, Go/Node versions, workload SHA-256, sample counts, and for both implementations `startupP99Ms`, `latencyP99Ms`, `throughputOpsPerSecond`, and `peakRssBytes`. Write the exact same-host method, lifecycle, sample counts, percentile formula, RSS technique, and limitations in `bench/methodology.md`. Add `make bench` for a full default-output run. Run unit and quick measurement to temporary files, then run `node bench/run.mjs --output bench/results.json` exactly once; retain the actual full result and commit `perf(05-02): publish shared benchmark evidence`. + + +- `node --test bench/run.test.mjs` and `node bench/run.mjs --quick --output ` exit 0 without modifying `bench/results.json`. +- `bench/results.json` parses and contains all four required metrics for both implementations, or only `peakRssBytes:null` with a nonempty RSS limitation on an unsupported platform. +- `bench/results.json` records normal-mode sample counts of at least 20 cold starts and 1,000 persistent requests; quick mode can never write this committed path. +- `bench/methodology.md` explicitly states these are same-host observations and not universal speedup claims. + +node --test bench/run.test.mjs; $tmp = Join-Path ([System.IO.Path]::GetTempPath()) 'tinycolor-bench-quick.json'; node bench/run.mjs --quick --output $tmp; node -e "const r=require('./bench/results.json'); if(r.samples.coldStarts<20||r.samples.requests<1000)process.exit(1); for (const k of ['javascript','go']) for (const m of ['startupP99Ms','latencyP99Ms','throughputOpsPerSecond','peakRssBytes']) if (!(m in r.implementations[k])) process.exit(1)"; git diff --check +The repository contains reproducible, machine-readable original-versus-port measurements for p99 latency, RSS, startup, and throughput. + + +Run `make verify`, the one-second fuzz smoke, validate the committed 60-second log, run benchmark tests, and parse results JSON. Never replace a failed or unsupported measurement with an estimated value. +- DEL-01 includes an actual differential fuzz session and shared-workload benchmark report. +- QLT-03 can reproduce each evidence artifact from checked-in code and commands. +After completion, create `.planning/phases/05-delivery-evidence/05-02-SUMMARY.md`. diff --git a/.planning/phases/05-delivery-evidence/05-03-PLAN.md b/.planning/phases/05-delivery-evidence/05-03-PLAN.md new file mode 100644 index 00000000..359a62a8 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-03-PLAN.md @@ -0,0 +1,149 @@ +--- +phase: 5 +plan: 3 +type: execute +subsystem: submission-documentation-closeout +owner: D-deepali +tags: [readme, decisions, compatibility, attribution, demo, closeout] +wave: 3 +depends_on: [05-02] +files_modified: [README.md, PORT.md, DECISIONS.md, COMPATIBILITY.md, docs/ARCHITECTURE.md, docs/TESTING.md, docs/DEMO.md, docs/TEAM-OWNERSHIP.md, .planning/STATE.md, .planning/ROADMAP.md, .planning/phases/05-delivery-evidence/05-03-SUMMARY.md, .planning/phases/05-delivery-evidence/05-VERIFICATION.md] +autonomous: false +requirements: [DEL-01, QLT-03] +must_haves: + truths: + - "A judge can find build, CLI, verification, fuzz, benchmark, limitations, attribution, and demo instructions from the root README." + - "Every number and bonus claim points to checked-in evidence and every unavailable external action remains explicitly unclaimed." + - "At least ten architectural divergences have concrete rationales and the milestone closes only after the full gate passes." + artifacts: + - path: "README.md" + provides: "Judge-facing migration, build, use, evidence, and limitation guide" + - path: "DECISIONS.md" + provides: "At least ten non-trivial accepted architectural decisions" + - path: "docs/DEMO.md" + provides: "Reproducible five-minute live demo script" + - path: ".planning/phases/05-delivery-evidence/05-VERIFICATION.md" + provides: "Goal-backward Phase 5 completion evidence" + key_links: + - from: "README.md" + to: "fuzz/log.txt" + via: "evidence link and reproduction command" + pattern: "fuzz/log.txt" + - from: "README.md" + to: "bench/results.json" + via: "benchmark evidence link" + pattern: "bench/results.json" +--- + +Turn measured Phase 5 outputs into an honest, judge-readable submission and close the roadmap with reproducible evidence. +@AGENT.md +@.planning/phases/05-delivery-evidence/05-CONTEXT.md +@.planning/phases/05-delivery-evidence/05-RESEARCH.md +@.planning/phases/05-delivery-evidence/05-VALIDATION.md +@.planning/phases/05-delivery-evidence/05-01-SUMMARY.md +@.planning/phases/05-delivery-evidence/05-02-SUMMARY.md +@README.md +@PORT.md +@DECISIONS.md +@COMPATIBILITY.md +@LICENSE +@.port-mortem.toml +@docs/ARCHITECTURE.md +@docs/TESTING.md +@docs/TEAM-OWNERSHIP.md +@fuzz/log.txt +@bench/methodology.md +@bench/results.json + + +Write the judge-facing port documentation and decision log +README.md, PORT.md, DECISIONS.md, docs/ARCHITECTURE.md, docs/TESTING.md, docs/DEMO.md, docs/TEAM-OWNERSHIP.md +Read all listed files completely plus `LICENSE`, `.port-mortem.toml`, `Makefile`, CLI help output, `fuzz/README.md`, `fuzz/log.txt`, `bench/methodology.md`, and `bench/results.json`. Extract actual command output and numbers before writing claims. + +Replace the upstream-oriented root README with a concise Track H port guide containing: migration rationale; exact pinned source URL/commit and MIT attribution; required tree; `make build` and Docker build/run; CLI syntax and examples for all five commands including `--json`; Go package example using the existing exported API; `make verify`; untouched-oracle manifest; links to fixed corpus, 60-second fuzz log, benchmark results/methodology, compatibility matrix, decisions, ownership, and demo script; known limitations; and a submission checklist. Preserve a link to the original upstream README/history rather than copying its entire usage manual. Reduce `PORT.md` to a compatibility redirect or remove it if README now contains every unique fact. + +Expand `DECISIONS.md` from seven to at least twelve substantive rows by adding: dual human/JSONL executable, cross-platform SHA-256 manifest verifier, seeded standard-library fuzzing, persistent children for evidence runners, shared benchmark workload/lifecycle, and honest external-evidence policy. Each row must name a real divergence and rationale; no empty bonus-padding bullets. Update architecture/testing documents to match actual commands only. Add `docs/DEMO.md` with a timestamped five-minute sequence: clean status/hash check, build, CLI parse/convert/lighten/palette/contrast, fixed differential corpus, fuzz log/reproduction, benchmark results, decisions/limitations, and closing artifact path. Keep public GitHub visibility and actual video URL as unchecked manual items. Update ownership only to record Phase 5 integration handoffs; do not reassign completed Phase 1-4 source ownership. Commit `docs(05-03): write submission guide and decisions`. + + +- README contains `make build`, `make verify`, all five CLI command names, `fuzz/log.txt`, `bench/results.json`, `DECISIONS.md`, upstream URL, kickoff commit, and MIT attribution. +- `DECISIONS.md` has at least twelve non-header table rows, each with nonempty decision and why columns. +- `docs/DEMO.md` has a five-minute sequence and explicitly marks video recording/upload and public-visibility checks as manual/unverified until completed. +- Documentation contains no `TBD`, `TODO`, `not_run`, obsolete `bin/tinycolor-compat`, or unsupported whole-suite/CI/video claim. + +rg -n "make build|make verify|parse|convert|lighten|palette|contrast|fuzz/log.txt|bench/results.json|bgrins/TinyColor|MIT" README.md; node -e "const fs=require('fs'); const rows=fs.readFileSync('DECISIONS.md','utf8').split(/\r?\n/).filter(x=>/^\| D-/.test(x)); if(rows.length<12) process.exit(1)"; if (rg -n "TBD|TODO|not_run|bin/tinycolor-compat" README.md DECISIONS.md COMPATIBILITY.md docs) { exit 1 }; git diff --check +The submission explains what was ported, how to reproduce it, what differs, and which external deliverables still require human action. + + + +Refresh honest parity, coverage, safety, benchmark, and bonus evidence +COMPATIBILITY.md +Read `COMPATIBILITY.md`, every fixed corpus, `fuzz/log.txt`, `bench/results.json`, `tests/original/manifest.sha256`, and the actual output from the commands below. Do not carry forward stale Phase 4 counts without rerunning them. + +Run the complete gate and capture exact case/pass/mismatch counts per corpus. Run `go -C src test -cover ./...` and record package coverage separately; do not invent a JavaScript coverage comparison when Deno coverage is unavailable. Count Go escape hatches with `rg -n '\bunsafe\b' src -g '*.go'` and report the exact code occurrence count, excluding documentation. Verify the three oracle hashes. Read, do not recompute by hand, the fuzz duration/seed/cases/divergences and benchmark metrics. Update `COMPATIBILITY.md` with a current evidence table and dedicated sections for fixed-corpus pass rate per file, original-suite/Deno status, Go coverage by package, unsafe count, fuzz session, benchmark summary/link, known differences, and bonus eligibility. Claim Differential Fuzz Survivor only for a >=60 second zero-divergence log; claim Zero Unsafe only for zero Go source occurrences; claim Decision Log only when at least ten substantive decisions exist. State CI as configured until a GitHub run URL is verified, public repository status as unverified locally, and demo video as not yet supplied. Commit `docs(05-03): publish final compatibility evidence`. + + +- Every `compat/cases/*.jsonl` file has an explicit `passed/total` and mismatch count in `COMPATIBILITY.md` matching fresh output. +- Go coverage is labeled by package/tool command and is not described as JavaScript coverage parity. +- Unsafe count is numeric and includes the exact `rg` reproduction command. +- Fuzz/bonus claims exactly match `fuzz/log.txt`; benchmark numbers link to `bench/results.json` and retain same-host limitation. +- Deno, CI execution, repository visibility, and demo video are each marked passed only with observed evidence, otherwise configured/unverified/not supplied. + +make verify; go -C src test -cover ./...; node tests/original/verify.mjs; node -e "const r=require('./bench/results.json'); if(!r.implementations?.javascript||!r.implementations?.go) process.exit(1)"; rg -n "smoke.jsonl|parser-hex-rgb.jsonl|parser.jsonl|conversion.jsonl|operations.jsonl|unsafe|coverage|fuzz/log.txt|bench/results.json|Deno|video" COMPATIBILITY.md; git diff --check +Every score-relevant statement is traceable to a checked-in command or explicitly labeled external/unverified. + + + +Run the final clean local gate and write verification evidence +.planning/phases/05-delivery-evidence/05-03-SUMMARY.md, .planning/phases/05-delivery-evidence/05-VERIFICATION.md +Read all Phase 5 plans/summaries, `05-VALIDATION.md`, `.planning/STATE.md`, `.planning/ROADMAP.md`, `README.md`, `COMPATIBILITY.md`, and actual final command output. Inspect `git status --short` before editing planning state. + +Run `make build`, every CLI command once in JSON mode, `make verify`, `node fuzz/validate-log.mjs fuzz/log.txt`, the one-second fuzz smoke, benchmark tests/schema validation using a temporary quick-output path, oracle hash verifier, and `git diff --check`. Confirm `git diff -- mod.js test.js tinycolor.js` is empty and compare current SHA-256 values to the manifest. Create `05-03-SUMMARY.md` listing task commits, commands, exact evidence, deviations, and remaining external actions. Create `05-VERIFICATION.md` with goal-backward evidence for each Phase 5 success criterion and DEL-01/QLT-03; distinguish local pass, configured CI, and external/unverified items. Commit these local evidence documents as `docs(05-03): verify local delivery evidence`. Do not change ROADMAP or STATE yet. + + +- `make build`, `make verify`, focused fuzz/benchmark checks, oracle verification, and `git diff --check` all exit 0 in the closing session. +- `git diff -- mod.js test.js tinycolor.js` is empty and hashes match the pinned manifest. +- `05-VERIFICATION.md` maps all three roadmap success criteria and DEL-01/QLT-03 to concrete paths/commands. +- `05-VERIFICATION.md` leaves public clone access and CI execution pending until observed evidence is supplied. +- Worktree is clean immediately after this evidence commit except for ignored build/cache artifacts. + +make build; ./bin/tinycolor parse --json red; ./bin/tinycolor convert --to hsl --json red; ./bin/tinycolor lighten --amount 10 --json '#000'; ./bin/tinycolor palette --type triad --json red; ./bin/tinycolor contrast --json '#000' '#fff'; make verify; node fuzz/validate-log.mjs fuzz/log.txt; node fuzz/harness.mjs --duration 1 --seed 20260801; node --test bench/run.test.mjs; node tests/original/verify.mjs; git diff --check; git diff --exit-code -- mod.js test.js tinycolor.js +All local delivery evidence is green and the remaining external proof is explicit. + + + +Verify public clone access and a successful CI run +none +Read the repository `origin` URL, `.github/workflows/port.yml`, the current commit hash, and the pending external-evidence section of `05-VERIFICATION.md`. + +Resolve the public repository URL from `git remote get-url origin`. From an unauthenticated context, verify the repository can be read and cloned. Verify a GitHub Actions run for the exact Phase 5 commit completed successfully and capture its URL. If publishing the current branch or changing repository visibility is required, stop and request the user's explicit external-state authorization. Also report whether the five-minute demo video URL has been supplied; absence of a video does not permit a false claim. Do not mark the phase complete without public clone evidence and a successful CI run URL. + + +- An unauthenticated public repository URL and exact successful CI run URL are available for the current Phase 5 commit. +- Any required push or visibility change was explicitly authorized by the user. +- Video status is reported honestly as verified URL or not supplied. + +git remote get-url origin; git rev-parse HEAD +Public clone access and CI execution are observed rather than inferred from configuration. + + + +Close Phase 5 planning state after external proof +.planning/STATE.md, .planning/ROADMAP.md, .planning/phases/05-delivery-evidence/05-VERIFICATION.md +Read the successful checkpoint evidence, all Phase 5 summaries, `.planning/STATE.md`, `.planning/ROADMAP.md`, and `05-VERIFICATION.md`. + +Add the public repository and successful CI run URLs to `05-VERIFICATION.md`, keeping video status honest. Mark Phase 5 and its three plans complete in ROADMAP, set completion date 2026-08-01, and update progress to 3/3. Update STATE with current CLI, corpus, fuzz, benchmark, public-clone, and CI evidence; remove the stale 58/58 Phase 4 operations count in favor of the fresh 69/69 result and retain the Deno/video status supported by actual evidence. Run `git diff --check`, then commit `docs(05): close delivery evidence phase`. + + +- ROADMAP shows Phase 5 `3/3`, Complete, `2026-08-01` only after external proof exists. +- STATE includes the public repository and successful CI evidence and contains no stale 58/58 operations count. +- `05-VERIFICATION.md` includes the exact public repository and CI run URLs and does not invent a video URL. + +rg -n "3/3|Complete|2026-08-01" .planning/ROADMAP.md; if (rg -n "58/58" .planning/STATE.md) { exit 1 }; git diff --check +Phase 5 closes only after both local and external delivery proof are real. + + +No roadmap completion is allowed before all automated commands pass and public clone plus CI execution are observed. Video publication remains explicitly unclaimed unless a verified URL is supplied. +- DEL-01 is judge-readable, measurable, and demonstrable from a clean checkout. +- QLT-03 maps every claim to a reproducible command while immutable source hashes remain unchanged. +- Phase 5 and the milestone close with no hidden known difference or unsupported score claim. +Create `05-03-SUMMARY.md` and `05-VERIFICATION.md`, then mark the phase complete only when their evidence is green. diff --git a/.planning/phases/05-delivery-evidence/05-VALIDATION.md b/.planning/phases/05-delivery-evidence/05-VALIDATION.md index 13bc34db..ab84d897 100644 --- a/.planning/phases/05-delivery-evidence/05-VALIDATION.md +++ b/.planning/phases/05-delivery-evidence/05-VALIDATION.md @@ -35,9 +35,12 @@ created: 2026-08-01 | 05-01-01 | 01 | 1 | DEL-01 | CLI unit | `go -C src test ./cmd/tinycolor-compat` | ✅ | ⬜ pending | | 05-01-02 | 01 | 1 | QLT-03 | integration | `make build && make verify` | ✅ make / ❌ expanded targets | ⬜ pending | | 05-02-01 | 02 | 2 | DEL-01 | differential fuzz | `node fuzz/harness.mjs --duration 1 --seed 1` | ❌ Wave 2 | ⬜ pending | -| 05-02-02 | 02 | 2 | DEL-01 | benchmark smoke | `node bench/run.mjs --quick` | ❌ Wave 2 | ⬜ pending | +| 05-02-02 | 02 | 2 | DEL-01 | fuzz evidence | `node --test fuzz/validate-log.test.mjs && node fuzz/validate-log.mjs fuzz/log.txt` | ❌ Wave 2 | ⬜ pending | +| 05-02-03 | 02 | 2 | DEL-01 | benchmark smoke | `node bench/run.mjs --quick --output ` | ❌ Wave 2 | ⬜ pending | | 05-03-01 | 03 | 3 | DEL-01 | evidence audit | `node tests/original/verify.mjs` | ❌ Wave 1 | ⬜ pending | | 05-03-02 | 03 | 3 | QLT-03 | full gate | `make verify` | ✅ make / ❌ expanded target | ⬜ pending | +| 05-03-04 | 03 | 3 | DEL-01 | external checkpoint | public clone + exact CI run URL | manual | ⬜ pending | +| 05-03-05 | 03 | 3 | QLT-03 | closeout audit | `git diff --check` | ✅ | ⬜ pending | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* From d2ab28d85e7050151f6c0117ee7bdd5acd27b206 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 04:26:58 +0530 Subject: [PATCH 127/165] feat(05-01): add human CLI commands --- src/cmd/tinycolor-compat/main.go | 197 +++++++++++++++++++++++++- src/cmd/tinycolor-compat/main_test.go | 145 +++++++++++++++++++ 2 files changed, 336 insertions(+), 6 deletions(-) create mode 100644 src/cmd/tinycolor-compat/main_test.go diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index aaebeb57..014fb704 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -2,7 +2,10 @@ package main import ( "bufio" + "encoding/json" + "flag" "fmt" + "io" "math" "os" "strconv" @@ -13,7 +16,32 @@ import ( ) func main() { - scanner := bufio.NewScanner(os.Stdin) + os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) +} + +func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + if len(args) == 0 { + return runJSONL(stdin, stdout, stderr) + } + switch args[0] { + case "parse": + return runParse(args[1:], stdout, stderr) + case "convert": + return runConvert(args[1:], stdout, stderr) + case "lighten": + return runLighten(args[1:], stdout, stderr) + case "palette": + return runPalette(args[1:], stdout, stderr) + case "contrast": + return runContrast(args[1:], stdout, stderr) + default: + usage(stderr, "") + return 2 + } +} + +func runJSONL(stdin io.Reader, stdout, stderr io.Writer) int { + scanner := bufio.NewScanner(stdin) for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 { @@ -22,11 +50,168 @@ func main() { request, err := compat.Decode(line) if err != nil { response, _ := compat.Failure("", err.Error()) - write(response) + write(response, stdout, stderr) continue } - write(handle(request)) + write(handle(request), stdout, stderr) + } + return 0 +} + +func runParse(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("parse", flag.ContinueOnError) + flags.SetOutput(stderr) + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 1 { + usage(stderr, "parse [--json] ") + return 2 + } + color, _ := tinycolor.FromCompat(flags.Arg(0), false) + if *jsonOutput { + inspection := color.Inspect() + inspection["hex"] = color.ToHex() + return writeJSON(stdout, inspection) + } + fmt.Fprintln(stdout, color.String()) + return 0 +} + +func runConvert(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("convert", flag.ContinueOnError) + flags.SetOutput(stderr) + to := flags.String("to", "", "hex, hex8, rgb, percentage-rgb, hsl, hsv, or name") + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 1 || *to == "" { + usage(stderr, "convert --to hex|hex8|rgb|percentage-rgb|hsl|hsv|name [--json] ") + return 2 + } + color, _ := tinycolor.FromCompat(flags.Arg(0), false) + var result string + switch *to { + case "hex": + result = color.ToHexString() + case "hex8": + result = color.ToHex8String() + case "rgb": + result = color.ToRGBString() + case "percentage-rgb": + result = color.ToPercentageRGBString() + case "hsl": + result = color.ToHSLString() + case "hsv": + result = color.ToHSVString() + case "name": + result = color.ToString("name") + default: + usage(stderr, "convert --to hex|hex8|rgb|percentage-rgb|hsl|hsv|name [--json] ") + return 2 + } + if *jsonOutput { + return writeJSON(stdout, result) + } + fmt.Fprintln(stdout, result) + return 0 +} + +func runLighten(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("lighten", flag.ContinueOnError) + flags.SetOutput(stderr) + amount := flags.Float64("amount", 10, "lightness percentage") + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 1 { + usage(stderr, "lighten [--amount 10] [--json] ") + return 2 + } + color, _ := tinycolor.FromCompat(flags.Arg(0), false) + color.Lighten(*amount) + result := color.ToHexString() + if *jsonOutput { + return writeJSON(stdout, result) + } + fmt.Fprintln(stdout, result) + return 0 +} + +func runPalette(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("palette", flag.ContinueOnError) + flags.SetOutput(stderr) + kind := flags.String("type", "", "palette type") + results := flags.Int("results", 6, "number of colors") + slices := flags.Int("slices", 30, "number of hue slices") + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 1 || *kind == "" { + usage(stderr, "palette --type complement|splitcomplement|triad|tetrad|analogous|monochromatic [--results 6] [--slices 30] [--json] ") + return 2 + } + color, _ := tinycolor.FromCompat(flags.Arg(0), false) + var colors []tinycolor.Color + switch *kind { + case "complement": + colors = []tinycolor.Color{color.Complement()} + case "splitcomplement": + colors = color.SplitComplement() + case "triad": + colors = color.Triad() + case "tetrad": + colors = color.Tetrad() + case "analogous": + colors = color.Analogous(*results, *slices) + case "monochromatic": + colors = color.Monochromatic(*results) + default: + usage(stderr, "palette --type complement|splitcomplement|triad|tetrad|analogous|monochromatic [--results 6] [--slices 30] [--json] ") + return 2 + } + if *jsonOutput { + inspections := make([]map[string]any, len(colors)) + for index, paletteColor := range colors { + inspections[index] = paletteColor.Inspect() + } + return writeJSON(stdout, inspections) + } + for _, paletteColor := range colors { + fmt.Fprintln(stdout, paletteColor.ToHexString()) + } + return 0 +} + +func runContrast(args []string, stdout, stderr io.Writer) int { + flags := flag.NewFlagSet("contrast", flag.ContinueOnError) + flags.SetOutput(stderr) + jsonOutput := flags.Bool("json", false, "output JSON") + if flags.Parse(args) != nil || flags.NArg() != 2 { + usage(stderr, "contrast [--json] ") + return 2 + } + first, _ := tinycolor.FromCompat(flags.Arg(0), false) + second, _ := tinycolor.FromCompat(flags.Arg(1), false) + ratio := tinycolor.Readability(first, second) + if *jsonOutput { + return writeJSON(stdout, map[string]any{ + "ratio": ratio, + "aaSmall": tinycolor.IsReadable(first, second, tinycolor.WCAG2Options{Level: "AA", Size: "small"}), + "aaLarge": tinycolor.IsReadable(first, second, tinycolor.WCAG2Options{Level: "AA", Size: "large"}), + "aaaSmall": tinycolor.IsReadable(first, second, tinycolor.WCAG2Options{Level: "AAA", Size: "small"}), + "aaaLarge": tinycolor.IsReadable(first, second, tinycolor.WCAG2Options{Level: "AAA", Size: "large"}), + }) + } + fmt.Fprintln(stdout, ratio) + return 0 +} + +func usage(stderr io.Writer, command string) { + if command == "" { + fmt.Fprintln(stderr, "Usage: tinycolor-compat ") + return + } + fmt.Fprintln(stderr, "Usage: tinycolor-compat "+command) +} + +func writeJSON(stdout io.Writer, value any) int { + if err := json.NewEncoder(stdout).Encode(value); err != nil { + return 1 } + return 0 } func handle(request compat.Request) compat.Response { @@ -353,11 +538,11 @@ func truthy(value any) bool { } } -func write(response compat.Response) { +func write(response compat.Response, stdout, stderr io.Writer) { encoded, err := compat.Encode(response) if err != nil { - fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(stderr, err) return } - fmt.Println(string(encoded)) + fmt.Fprintln(stdout, string(encoded)) } diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go new file mode 100644 index 00000000..511296a3 --- /dev/null +++ b/src/cmd/tinycolor-compat/main_test.go @@ -0,0 +1,145 @@ +package main + +import ( + "bytes" + "encoding/json" + "strings" + "testing" +) + +func TestRunHumanCommands(t *testing.T) { + tests := []struct { + name string + args []string + want string + verify func(*testing.T, string) + }{ + { + name: "parse JSON inspection", + args: []string{"parse", "--json", "red"}, + verify: func(t *testing.T, output string) { + t.Helper() + var got map[string]any + if err := json.Unmarshal([]byte(output), &got); err != nil { + t.Fatal(err) + } + if got["hex"] != "ff0000" || got["valid"] != true { + t.Fatalf("parse JSON = %#v", got) + } + }, + }, + { + name: "parse invalid JSON inspection", + args: []string{"parse", "--json", "not-a-color"}, + verify: func(t *testing.T, output string) { + t.Helper() + var got map[string]any + if err := json.Unmarshal([]byte(output), &got); err != nil { + t.Fatal(err) + } + if got["valid"] != false { + t.Fatalf("parse JSON = %#v", got) + } + }, + }, + {name: "convert HSL", args: []string{"convert", "--to", "hsl", "red"}, want: "hsl(0, 100%, 50%)\n"}, + {name: "lighten", args: []string{"lighten", "--amount", "10", "#000"}, want: "#1a1a1a\n"}, + { + name: "triad palette", + args: []string{"palette", "--type", "triad", "red"}, + want: "#ff0000\n#00ff00\n#0000ff\n", + }, + { + name: "triad palette JSON inspections", + args: []string{"palette", "--type", "triad", "--json", "red"}, + verify: func(t *testing.T, output string) { + t.Helper() + var got []map[string]any + if err := json.Unmarshal([]byte(output), &got); err != nil { + t.Fatal(err) + } + if len(got) != 3 || got[0]["valid"] != true || got[0]["value"] != "red" { + t.Fatalf("palette JSON = %#v", got) + } + }, + }, + { + name: "contrast JSON", + args: []string{"contrast", "--json", "#000", "#fff"}, + verify: func(t *testing.T, output string) { + t.Helper() + var got map[string]any + if err := json.Unmarshal([]byte(output), &got); err != nil { + t.Fatal(err) + } + want := map[string]any{"ratio": float64(21), "aaSmall": true, "aaLarge": true, "aaaSmall": true, "aaaLarge": true} + for key, value := range want { + if got[key] != value { + t.Fatalf("contrast %s = %#v, want %#v", key, got[key], value) + } + } + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + if status := run(test.args, strings.NewReader(""), &stdout, &stderr); status != 0 { + t.Fatalf("status = %d, stderr = %q", status, stderr.String()) + } + if test.verify != nil { + test.verify(t, stdout.String()) + return + } + if stdout.String() != test.want { + t.Fatalf("stdout = %q, want %q", stdout.String(), test.want) + } + }) + } +} + +func TestRunJSONLAndUsageErrors(t *testing.T) { + tests := []struct { + name string + args []string + stdin string + wantStatus int + wantStdout string + verify func(*testing.T, string) + }{ + { + name: "JSONL inspect without arguments", + stdin: "{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", + wantStdout: "{\"id\":\"red\",\"result\":{\"alpha\":1,\"format\":\"name\",\"original\":\"red\",\"rgb\":{\"a\":1,\"b\":0,\"g\":0,\"r\":255},\"valid\":true,\"value\":\"red\"}}\n", + }, + { + name: "malformed JSONL request does not stop the stream", + stdin: "{bad json\n{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", + verify: func(t *testing.T, output string) { + t.Helper() + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) != 2 || !strings.Contains(lines[0], "malformed JSON") || !strings.Contains(lines[1], "\"id\":\"red\"") { + t.Fatalf("stdout = %q", output) + } + }, + }, + {name: "unknown command", args: []string{"unknown"}, wantStatus: 2}, + {name: "missing convert target", args: []string{"convert", "red"}, wantStatus: 2}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + if status := run(test.args, strings.NewReader(test.stdin), &stdout, &stderr); status != test.wantStatus { + t.Fatalf("status = %d, want %d", status, test.wantStatus) + } + if test.verify != nil { + test.verify(t, stdout.String()) + } else if stdout.String() != test.wantStdout { + t.Fatalf("stdout = %q, want %q", stdout.String(), test.wantStdout) + } + if test.wantStatus == 2 && !strings.Contains(stderr.String(), "Usage:") { + t.Fatalf("stderr = %q, want usage", stderr.String()) + } + }) + } +} From 025d11d40161cbda6003430f328df5dc672a4edf Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 04:34:25 +0530 Subject: [PATCH 128/165] build(05-01): add reproducible verification gate --- Dockerfile | 6 +++--- Makefile | 32 +++++++++++++++++++++++++++----- tests/original/verify.mjs | 28 ++++++++++++++++++++++++++++ tests/original/verify.test.mjs | 26 ++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 tests/original/verify.mjs create mode 100644 tests/original/verify.test.mjs diff --git a/Dockerfile b/Dockerfile index 7831a508..9f44b078 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ WORKDIR /app/src COPY src/go.mod ./ RUN go mod download COPY src/ ./ -RUN go build -o /tinycolor-compat ./cmd/tinycolor-compat +RUN go build -o /tinycolor ./cmd/tinycolor-compat FROM scratch -COPY --from=build /tinycolor-compat /tinycolor-compat -ENTRYPOINT ["/tinycolor-compat"] +COPY --from=build /tinycolor /tinycolor +ENTRYPOINT ["/tinycolor"] diff --git a/Makefile b/Makefile index f8a40eaf..98761099 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,34 @@ -.PHONY: build test +.PHONY: build test verify fmt-check hashes fuzz bench clean + +GO_CACHE ?= $(CURDIR)/.cache/go-build +GO = GOCACHE=$(GO_CACHE) go build: - go -C src build -o ../bin/tinycolor-compat ./cmd/tinycolor-compat + $(GO) -C src build -o ../bin/tinycolor ./cmd/tinycolor-compat test: node tests/port/adapter.test.mjs - go -C src test ./... - go -C src vet ./... + $(GO) -C src test ./... + node compat/run.mjs compat/cases/smoke.jsonl node compat/run.mjs compat/cases/parser-hex-rgb.jsonl node compat/run.mjs compat/cases/parser.jsonl - node compat/run.mjs compat/cases/smoke.jsonl + node compat/run.mjs compat/cases/conversion.jsonl + node compat/run.mjs compat/cases/operations.jsonl + +verify: fmt-check hashes test + $(GO) -C src vet ./... + +fmt-check: + node -e "const { readdirSync } = require('node:fs'); const { join } = require('node:path'); const { execFileSync } = require('node:child_process'); const files = []; const walk = (dir) => readdirSync(dir, { withFileTypes: true }).forEach((entry) => entry.isDirectory() ? walk(join(dir, entry.name)) : entry.name.endsWith('.go') && files.push(join(dir, entry.name))); walk('src'); const output = execFileSync('gofmt', ['-l', ...files], { encoding: 'utf8' }).trim(); if (output) { console.error(output); process.exit(1); }" + +hashes: + node tests/original/verify.mjs + +fuzz: + node fuzz/harness.mjs --duration 60 --seed 20260801 + +bench: + node bench/run.mjs + +clean: + node -e "const fs = require('node:fs'); for (const file of ['bin/tinycolor', 'bin/tinycolor.exe']) fs.rmSync(file, { force: true });" diff --git a/tests/original/verify.mjs b/tests/original/verify.mjs new file mode 100644 index 00000000..8d92d9aa --- /dev/null +++ b/tests/original/verify.mjs @@ -0,0 +1,28 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const digest = (file) => createHash("sha256").update(readFileSync(file)).digest("hex"); + +export function verifyManifest(manifestPath, root) { + return readFileSync(manifestPath, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => { + const [, expected, file] = line.match(/^(\S+)\s{2}(.+)$/) ?? []; + const path = resolve(root, file); + try { + return digest(path) === expected ? [] : [`mismatch: ${file}`]; + } catch { + return [`missing: ${file}`]; + } + }); +} + +if (import.meta.main) { + const manifest = resolve("tests/original/manifest.sha256"); + const failures = verifyManifest(manifest, process.cwd()); + if (failures.length) { + console.error(failures.join("\n")); + process.exitCode = 1; + } else { + console.log(`verified: ${readFileSync(manifest, "utf8").split(/\r?\n/).filter(Boolean).length}`); + } +} diff --git a/tests/original/verify.test.mjs b/tests/original/verify.test.mjs new file mode 100644 index 00000000..7371f3b7 --- /dev/null +++ b/tests/original/verify.test.mjs @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { verifyManifest } from "./verify.mjs"; + +const sha256 = (value) => createHash("sha256").update(value).digest("hex"); + +test("verifyManifest accepts matching files and reports a changed path", () => { + const root = mkdtempSync(join(tmpdir(), "tinycolor-verify-")); + try { + writeFileSync(join(root, "first.txt"), "first"); + writeFileSync(join(root, "second.txt"), "second"); + const manifest = join(root, "manifest.sha256"); + writeFileSync(manifest, `${sha256("first")} first.txt\n${sha256("second")} second.txt\n`); + + assert.deepEqual(verifyManifest(manifest, root), []); + + writeFileSync(join(root, "second.txt"), "changed"); + assert.deepEqual(verifyManifest(manifest, root), ["mismatch: second.txt"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); From f7ab93b31d128ac23218bc7de54386e37d44e36c Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:39:07 +0530 Subject: [PATCH 129/165] fix(05-01): make verification gate portable --- Makefile | 13 +++++---- src/tinycolor/color_test.go | 49 ++++++++++++++++++++++++---------- tests/original/verify.mjs | 4 ++- tests/original/verify.test.mjs | 12 +++++++++ 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 98761099..6fa4ef44 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,17 @@ .PHONY: build test verify fmt-check hashes fuzz bench clean -GO_CACHE ?= $(CURDIR)/.cache/go-build -GO = GOCACHE=$(GO_CACHE) go +GOCACHE ?= $(CURDIR)/.cache/go-build +export GOCACHE +GOEXE := $(shell go env GOEXE) +BINARY := bin/tinycolor$(GOEXE) build: - $(GO) -C src build -o ../bin/tinycolor ./cmd/tinycolor-compat + node -e "require('node:fs').mkdirSync('bin', { recursive: true })" + go -C src build -o ../$(BINARY) ./cmd/tinycolor-compat test: node tests/port/adapter.test.mjs - $(GO) -C src test ./... + go -C src test ./... node compat/run.mjs compat/cases/smoke.jsonl node compat/run.mjs compat/cases/parser-hex-rgb.jsonl node compat/run.mjs compat/cases/parser.jsonl @@ -16,7 +19,7 @@ test: node compat/run.mjs compat/cases/operations.jsonl verify: fmt-check hashes test - $(GO) -C src vet ./... + go -C src vet ./... fmt-check: node -e "const { readdirSync } = require('node:fs'); const { join } = require('node:path'); const { execFileSync } = require('node:child_process'); const files = []; const walk = (dir) => readdirSync(dir, { withFileTypes: true }).forEach((entry) => entry.isDirectory() ? walk(join(dir, entry.name)) : entry.name.endsWith('.go') && files.push(join(dir, entry.name))); walk('src'); const output = execFileSync('gofmt', ['-l', ...files], { encoding: 'utf8' }).trim(); if (output) { console.error(output); process.exit(1); }" diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 8ce49118..311ec7db 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -3,28 +3,49 @@ package tinycolor import "testing" func TestOutputsAndAnalysis(t *testing.T) { - c,_:=FromCompat("rgba(255, 0, 0, .5)",false) - if c.ToHex8String()!="#ff000080" || c.ToRGBString()!="rgba(255, 0, 0, 0.5)" { t.Fatal(c.ToHex8String(),c.ToRGBString()) } - if c.ToFilter(nil,false)!="progid:DXImageTransform.Microsoft.gradient(startColorstr=#80ff0000,endColorstr=#80ff0000)" {t.Fatal(c.ToFilter(nil,false))} - black,_:=FromCompat("#000",false); white,_:=FromCompat("#fff",false) - if black.Brightness()!=0||white.Luminance()!=1||!black.IsDark()||!white.IsLight(){t.Fatal("analysis")} - if !Equals("#ff000066","rgba(255, 0, 0, .4)"){t.Fatal("equals")} - if !Random().Valid(){t.Fatal("random")} + c, _ := FromCompat("rgba(255, 0, 0, .5)", false) + if c.ToHex8String() != "#ff000080" || c.ToRGBString() != "rgba(255, 0, 0, 0.5)" { + t.Fatal(c.ToHex8String(), c.ToRGBString()) + } + if c.ToFilter(nil, false) != "progid:DXImageTransform.Microsoft.gradient(startColorstr=#80ff0000,endColorstr=#80ff0000)" { + t.Fatal(c.ToFilter(nil, false)) + } + black, _ := FromCompat("#000", false) + white, _ := FromCompat("#fff", false) + if black.Brightness() != 0 || white.Luminance() != 1 || !black.IsDark() || !white.IsLight() { + t.Fatal("analysis") + } + if !Equals("#ff000066", "rgba(255, 0, 0, .4)") { + t.Fatal("equals") + } + if !Random().Valid() { + t.Fatal("random") + } } func TestStringHexFilterAndConversion(t *testing.T) { - c,_:=FromCompat("rgba(255, 0, 0, .5)",false) - if c.ToPercentageRGBString()!="rgba(100%, 0%, 0%, 0.5)"||c.ToHSLString()!="hsla(0, 100%, 50%, 0.5)"||c.ToHSVString()!="hsva(0, 100%, 100%, 0.5)"{t.Fatal("strings")} - if c.ToString("hex8")!="#ff000080"||c.ToString("name")!="#ff0000"{t.Fatal(c.ToString("hex8"),c.ToString("name"))} - with,_:=FromCompatWithOptions("red",false,CompatOptions{GradientType:true}) - if with.ToFilter(nil,false)!="progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#ffff0000,endColorstr=#ffff0000)"{t.Fatal(with.ToFilter(nil,false))} + c, _ := FromCompat("rgba(255, 0, 0, .5)", false) + if c.ToPercentageRGBString() != "rgba(100%, 0%, 0%, 0.5)" || c.ToHSLString() != "hsla(0, 100%, 50%, 0.5)" || c.ToHSVString() != "hsva(0, 100%, 100%, 0.5)" { + t.Fatal("strings") + } + if c.ToString("hex8") != "#ff000080" || c.ToString("name") != "#ff0000" { + t.Fatal(c.ToString("hex8"), c.ToString("name")) + } + with, _ := FromCompatWithOptions("red", false, CompatOptions{GradientType: true}) + if with.ToFilter(nil, false) != "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#ffff0000,endColorstr=#ffff0000)" { + t.Fatal(with.ToFilter(nil, false)) + } } func TestImplicitAlphaHexFallsBackToRGBA(t *testing.T) { for _, input := range []string{"#f008", "#ff000080"} { c, _ := FromCompat(input, false) - if c.String() != c.ToRGBString() { t.Fatalf("%s: %s", input, c.String()) } - if c.ToString("hex8") != c.ToHex8String() { t.Fatal("explicit hex8") } + if c.String() != c.ToRGBString() { + t.Fatalf("%s: %s", input, c.String()) + } + if c.ToString("hex8") != c.ToHex8String() { + t.Fatal("explicit hex8") + } } } diff --git a/tests/original/verify.mjs b/tests/original/verify.mjs index 8d92d9aa..7fad2442 100644 --- a/tests/original/verify.mjs +++ b/tests/original/verify.mjs @@ -6,7 +6,9 @@ const digest = (file) => createHash("sha256").update(readFileSync(file)).digest( export function verifyManifest(manifestPath, root) { return readFileSync(manifestPath, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => { - const [, expected, file] = line.match(/^(\S+)\s{2}(.+)$/) ?? []; + const match = line.match(/^(\S+)\s{2}(.+)$/); + if (!match) return [`invalid: ${line}`]; + const [, expected, file] = match; const path = resolve(root, file); try { return digest(path) === expected ? [] : [`mismatch: ${file}`]; diff --git a/tests/original/verify.test.mjs b/tests/original/verify.test.mjs index 7371f3b7..3ae6e002 100644 --- a/tests/original/verify.test.mjs +++ b/tests/original/verify.test.mjs @@ -24,3 +24,15 @@ test("verifyManifest accepts matching files and reports a changed path", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("verifyManifest reports a malformed manifest line", () => { + const root = mkdtempSync(join(tmpdir(), "tinycolor-verify-")); + try { + const manifest = join(root, "manifest.sha256"); + writeFileSync(manifest, "not a manifest entry\n"); + + assert.deepEqual(verifyManifest(manifest, root), ["invalid: not a manifest entry"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); From f55a465030243632df46a4623b035e5b50e1c429 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:44:55 +0530 Subject: [PATCH 130/165] ci(05-01): verify port and source suite --- .github/workflows/deno.yml | 42 -------------------------------------- .github/workflows/port.yml | 30 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/deno.yml create mode 100644 .github/workflows/port.yml diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml deleted file mode 100644 index 41adb05d..00000000 --- a/.github/workflows/deno.yml +++ /dev/null @@ -1,42 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow will install Deno then run `deno lint` and `deno test`. -# For more information see: https://github.com/denoland/setup-deno - -name: Deno - -on: - push: - branches: ["master"] - pull_request: - branches: ["master"] - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Setup repo - uses: actions/checkout@v3 - - - name: Setup Deno - # uses: denoland/setup-deno@v1 - uses: denoland/setup-deno@9db7f66e8e16b5699a514448ce994936c63f0d54 - with: - deno-version: v1.x - - # Uncomment this step to verify the use of 'deno fmt' on each commit. - # - name: Verify formatting - # run: deno fmt --check - - # - name: Run linter - # run: deno lint - - - name: Build & Run Tests - run: deno task build diff --git a/.github/workflows/port.yml b/.github/workflows/port.yml new file mode 100644 index 00000000..2b3a51f9 --- /dev/null +++ b/.github/workflows/port.yml @@ -0,0 +1,30 @@ +name: TinyColor Go port + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.26' + - uses: actions/setup-node@v4 + with: + node-version: '24' + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + - run: make verify + - run: deno task test + - run: make build + - uses: actions/upload-artifact@v4 + with: + name: tinycolor-linux-amd64 + path: bin/tinycolor From 416634b3daac90640e6340d1a66470e41aa51f4d Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 04:47:43 +0530 Subject: [PATCH 131/165] docs(05-01): summarize CLI build and CI --- .../05-delivery-evidence/05-01-SUMMARY.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-01-SUMMARY.md diff --git a/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md b/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md new file mode 100644 index 00000000..44405190 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md @@ -0,0 +1,72 @@ +--- +phase: 5 +plan: 1 +subsystem: cli-build-ci +tags: [go, cli, make, docker, github-actions, sha256] +provides: [human CLI, immutable-oracle verifier, reproducible local gate, delivery CI] +affects: [src/cmd/tinycolor-compat, tests/original, Makefile, Dockerfile, .github/workflows] +tech-stack: [go-standard-library, node-standard-library, github-actions] +key-files: + created: + - src/cmd/tinycolor-compat/main_test.go + - tests/original/verify.mjs + - tests/original/verify.test.mjs + - .github/workflows/port.yml + modified: + - src/cmd/tinycolor-compat/main.go + - src/tinycolor/color_test.go + - Makefile + - Dockerfile + deleted: + - .github/workflows/deno.yml +decisions: + - One executable selects JSONL mode with zero arguments and human CLI mode with a command. + - GNU Make exports a repository-local GOCACHE and derives GOEXE for Windows/Linux artifacts. + - CI calls make verify and runs the untouched Deno suite as a separate environment check. +--- + +# Phase 5 Plan 1: CLI, Build, and CI Summary + +Added tested `parse`, `convert`, `lighten`, `palette`, and `contrast` commands +without changing the zero-argument JSONL protocol. Added cross-platform oracle +hash verification, a complete local parity gate, a one-command build, the +scratch Docker artifact, and one least-privilege port CI workflow. + +## TDD and Verification Evidence + +- CLI RED: the focused package test failed because `run` was undefined. +- CLI GREEN: focused command tests, all Go tests, and Node adapter tests passed. +- Manifest RED: verifier import was missing; malformed-line regression later + failed at `path.resolve` before the defensive parser fix. +- Manifest GREEN: two Node tests passed and direct verification printed + `verified: 3`. +- Formatting: `gofmt -l src` prints no files after the mechanical + `color_test.go` formatting fix. +- Full local equivalents: Go tests/vet, adapter tests, and all 162 fixed cases + passed: 9 smoke, 26 parser-hex-rgb, 23 parser, 35 conversion, 69 operations. +- Windows artifact: `bin/tinycolor.exe parse red` executed successfully. + +GNU Make is not installed in the current Windows environment, so `make build` +and `make verify` were not directly invoked locally. Their individual commands +passed; the configured Ubuntu CI remains the direct Make/Deno execution gate +until a remote run is observed. + +## Commits + +- `df1f9b8` `feat(05-01): add human CLI commands` +- `02f9ced` `build(05-01): add reproducible verification gate` +- `a4ad85e` `fix(05-01): make verification gate portable` +- `bc91502` `ci(05-01): verify port and source suite` + +## Deviations from Plan + +- The Makefile emits `bin/tinycolor.exe` on Windows and `bin/tinycolor` on + Linux. This is required for a runnable native Windows artifact. +- The pre-existing unformatted `src/tinycolor/color_test.go` was formatted in + the portability fix because the new non-mutating format gate correctly + blocked completion. + +## Known External Checks + +- GitHub Actions execution has not yet been observed. +- Deno remains unavailable locally; no original-suite pass is claimed here. From 1a91e1e7a96b26043135bf758d2b03360e2c0c05 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 04:57:09 +0530 Subject: [PATCH 132/165] fix(05-01): close delivery review gaps --- Makefile | 1 + src/cmd/tinycolor-compat/main.go | 4 ++-- src/cmd/tinycolor-compat/main_test.go | 4 ++-- tests/original/verify.mjs | 6 ++++-- tests/original/verify.test.mjs | 21 ++++++++++++++++++++- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 6fa4ef44..f76edc82 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ build: test: node tests/port/adapter.test.mjs + node --test tests/original/verify.test.mjs go -C src test ./... node compat/run.mjs compat/cases/smoke.jsonl node compat/run.mjs compat/cases/parser-hex-rgb.jsonl diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 014fb704..0f9ef3ea 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -201,10 +201,10 @@ func runContrast(args []string, stdout, stderr io.Writer) int { func usage(stderr io.Writer, command string) { if command == "" { - fmt.Fprintln(stderr, "Usage: tinycolor-compat ") + fmt.Fprintln(stderr, "Usage: tinycolor ") return } - fmt.Fprintln(stderr, "Usage: tinycolor-compat "+command) + fmt.Fprintln(stderr, "Usage: tinycolor "+command) } func writeJSON(stdout io.Writer, value any) int { diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index 511296a3..c5449495 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -137,8 +137,8 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { } else if stdout.String() != test.wantStdout { t.Fatalf("stdout = %q, want %q", stdout.String(), test.wantStdout) } - if test.wantStatus == 2 && !strings.Contains(stderr.String(), "Usage:") { - t.Fatalf("stderr = %q, want usage", stderr.String()) + if test.wantStatus == 2 && !strings.Contains(stderr.String(), "Usage: tinycolor ") { + t.Fatalf("stderr = %q, want tinycolor usage", stderr.String()) } }) } diff --git a/tests/original/verify.mjs b/tests/original/verify.mjs index 7fad2442..a9bb8c3a 100644 --- a/tests/original/verify.mjs +++ b/tests/original/verify.mjs @@ -1,15 +1,17 @@ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; +import { resolve, sep } from "node:path"; const digest = (file) => createHash("sha256").update(readFileSync(file)).digest("hex"); export function verifyManifest(manifestPath, root) { + const resolvedRoot = resolve(root); return readFileSync(manifestPath, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => { const match = line.match(/^(\S+)\s{2}(.+)$/); if (!match) return [`invalid: ${line}`]; const [, expected, file] = match; - const path = resolve(root, file); + const path = resolve(resolvedRoot, file); + if (path !== resolvedRoot && !path.startsWith(resolvedRoot + sep)) return [`invalid: ${file}`]; try { return digest(path) === expected ? [] : [`mismatch: ${file}`]; } catch { diff --git a/tests/original/verify.test.mjs b/tests/original/verify.test.mjs index 3ae6e002..877ff024 100644 --- a/tests/original/verify.test.mjs +++ b/tests/original/verify.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; @@ -36,3 +36,22 @@ test("verifyManifest reports a malformed manifest line", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("verifyManifest rejects entries outside the manifest root", () => { + const parent = mkdtempSync(join(tmpdir(), "tinycolor-verify-")); + const root = join(parent, "root"); + const outside = join(parent, "outside.txt"); + try { + mkdirSync(root); + writeFileSync(outside, "outside"); + const manifest = join(root, "manifest.sha256"); + writeFileSync(manifest, `${sha256("outside")} ${outside}\n${sha256("outside")} ../outside.txt\n`); + + assert.deepEqual(verifyManifest(manifest, root), [ + `invalid: ${outside}`, + "invalid: ../outside.txt", + ]); + } finally { + rmSync(parent, { recursive: true, force: true }); + } +}); From abd629399049f1612b9aaebb6bb6fe3ab44a5ef4 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 04:58:47 +0530 Subject: [PATCH 133/165] docs(05-01): record final review fixes --- .planning/phases/05-delivery-evidence/05-01-SUMMARY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md b/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md index 44405190..353fdb89 100644 --- a/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md +++ b/.planning/phases/05-delivery-evidence/05-01-SUMMARY.md @@ -57,6 +57,7 @@ until a remote run is observed. - `02f9ced` `build(05-01): add reproducible verification gate` - `a4ad85e` `fix(05-01): make verification gate portable` - `bc91502` `ci(05-01): verify port and source suite` +- `57e1114` `fix(05-01): close delivery review gaps` ## Deviations from Plan @@ -65,6 +66,8 @@ until a remote run is observed. - The pre-existing unformatted `src/tinycolor/color_test.go` was formatted in the portability fix because the new non-mutating format gate correctly blocked completion. +- Final review added manifest absolute/traversal containment, wired verifier + regressions into `make verify`, and aligned usage text with `tinycolor`. ## Known External Checks From d5a2c81d0af599c30d5c3afab5959488f464c62c Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 05:13:02 +0530 Subject: [PATCH 134/165] fix(05-01): pin oracle checkout bytes --- .gitattributes | 3 + .../resolved/ci-oracle-hash-line-endings.md | 57 +++++++++++++++++++ tests/original/verify.test.mjs | 10 ++++ 3 files changed, 70 insertions(+) create mode 100644 .gitattributes create mode 100644 .planning/debug/resolved/ci-oracle-hash-line-endings.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..5731d7cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +mod.js text eol=crlf +test.js text eol=crlf +tinycolor.js text eol=crlf diff --git a/.planning/debug/resolved/ci-oracle-hash-line-endings.md b/.planning/debug/resolved/ci-oracle-hash-line-endings.md new file mode 100644 index 00000000..7af8f263 --- /dev/null +++ b/.planning/debug/resolved/ci-oracle-hash-line-endings.md @@ -0,0 +1,57 @@ +--- +status: resolved +trigger: "Fix GitHub Actions make verify where tests/original/verify.mjs reports mismatch for mod.js, test.js, tinycolor.js." +created: 2026-08-01T05:09:56.9355940+05:30 +updated: 2026-08-01T05:16:00+05:30 +--- + +## Current Focus + +hypothesis: Confirmed: the oracle manifest describes CRLF working-tree bytes, but Git had no checkout eol attribute for the three LF blobs. +test: Completed focused regression, production verifier, and whitespace validation. +expecting: All checks pass. +next_action: Archive this resolved record and commit only the assigned files. + +## Symptoms + +expected: Ubuntu checkout bytes match kickoff manifest and verifier prints verified: 3. +actual: All three oracle text files mismatch in CI; local Windows passes. +errors: mismatch: mod.js / test.js / tinycolor.js; Make hashes target exits 1. +reproduction: Local SHA-256 hashes equal manifest, while raw Git blob SHA-256 values are different LF bytes. Local core.autocrlf=true; no .gitattributes exists; oracle git diff is empty. +started: Introduced when CI started running the existing Windows-derived kickoff manifest. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T05:09:56.9355940+05:30 + checked: Prefilled symptom evidence. + found: Windows working-tree hashes match the manifest, but raw Git blob hashes do not; the repository lacks .gitattributes and local core.autocrlf=true. + implication: Checkout line-ending conversion, rather than oracle content drift, is the leading mechanism. + +- timestamp: 2026-08-01T05:11:00+05:30 + checked: Assigned verifier files and root attributes file. + found: tests/original/verify.test.mjs already exists; .gitattributes and the debug knowledge base do not. verify.mjs hashes raw file bytes and does no line-ending normalization. + implication: The repository checkout must supply the manifest's CRLF bytes; verifier behavior should remain unchanged. + +- timestamp: 2026-08-01T05:13:00+05:30 + checked: New focused Node test before adding attributes. + found: The test failed exactly as predicted; git check-attr returned eol: unspecified for mod.js, test.js, and tinycolor.js while the other three verifier tests passed. + implication: The missing checkout policy is directly reproduced and the hypothesis is confirmed. + +- timestamp: 2026-08-01T05:15:00+05:30 + checked: Focused Node suite after adding .gitattributes. + found: All four tests passed, including the check that all three oracle paths resolve to eol: crlf. + implication: The repository now declares the required checkout bytes independently of platform defaults. + +- timestamp: 2026-08-01T05:16:00+05:30 + checked: Production verifier and git diff --check. + found: verify.mjs printed verified: 3; git diff --check exited 0 with only line-ending conversion warnings, including one for a concurrent fuzz file that was not touched. + implication: The original hash verification succeeds and the assigned diff contains no whitespace errors. + +## Resolution + +root_cause: The kickoff manifest hashes CRLF working-tree bytes, but the repository stores LF blobs and had no .gitattributes policy. Windows core.autocrlf=true masked this locally; Ubuntu checked out LF bytes and all three raw-byte hashes mismatched. +fix: Pin mod.js, test.js, and tinycolor.js to text eol=crlf at the repository root; retain a focused git check-attr regression test. +verification: RED confirmed eol unspecified; GREEN confirmed 4/4 focused tests pass; production verifier printed verified: 3; git diff --check exited 0. +files_changed: [.gitattributes, tests/original/verify.test.mjs, .planning/debug/resolved/ci-oracle-hash-line-endings.md] diff --git a/tests/original/verify.test.mjs b/tests/original/verify.test.mjs index 877ff024..3816faf7 100644 --- a/tests/original/verify.test.mjs +++ b/tests/original/verify.test.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -8,6 +9,15 @@ import { verifyManifest } from "./verify.mjs"; const sha256 = (value) => createHash("sha256").update(value).digest("hex"); +test("oracle files are checked out with CRLF line endings", () => { + const files = ["mod.js", "test.js", "tinycolor.js"]; + const attributes = execFileSync("git", ["check-attr", "eol", "--", ...files], { + encoding: "utf8", + }); + + assert.deepEqual(attributes.trim().split(/\r?\n/), files.map((file) => `${file}: eol: crlf`)); +}); + test("verifyManifest accepts matching files and reports a changed path", () => { const root = mkdtempSync(join(tmpdir(), "tinycolor-verify-")); try { From 35199e7906bf3f72f11b1d3e1d3eaef44c4825b7 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 05:17:12 +0530 Subject: [PATCH 135/165] fix(05-01): scope Deno CI to source suite --- .github/workflows/port.yml | 2 +- .../debug/resolved/ci-deno2-test-discovery.md | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .planning/debug/resolved/ci-deno2-test-discovery.md diff --git a/.github/workflows/port.yml b/.github/workflows/port.yml index 2b3a51f9..5e7243ff 100644 --- a/.github/workflows/port.yml +++ b/.github/workflows/port.yml @@ -22,7 +22,7 @@ jobs: with: deno-version: v2.x - run: make verify - - run: deno task test + - run: deno test test.js - run: make build - uses: actions/upload-artifact@v4 with: diff --git a/.planning/debug/resolved/ci-deno2-test-discovery.md b/.planning/debug/resolved/ci-deno2-test-discovery.md new file mode 100644 index 00000000..0f6dcc7e --- /dev/null +++ b/.planning/debug/resolved/ci-deno2-test-discovery.md @@ -0,0 +1,47 @@ +--- +status: resolved +trigger: "Deno 2 removed the files test config, so deno task test discovers generated npm and Node port tests and fails on undeclared @deno/shim-deno-test imports." +created: 2026-08-01T05:20:00+05:30 +updated: 2026-08-01T05:23:00+05:30 +--- + +## Current Focus + +hypothesis: Confirmed: the workflow invoked broad task discovery, while the source contract is the root test.js suite only. +test: Completed workflow text audit and git diff --check. +expecting: All checks pass. +next_action: Archive the resolved record and commit only the workflow and this debug document. + +## Symptoms + +expected: CI runs only the immutable original root suite test.js. +actual: Deno 2 discovers npm/esm/test.js, npm/cjs/test.js, tests/original/verify.test.mjs, and tests/port/adapter.test.mjs. +errors: Deno warns that files test config was removed; type-check fails because @deno/shim-deno-test is not a declared Deno dependency in generated npm tests. +reproduction: GitHub Actions source-suite step runs deno task test under Deno 2. +started: Appeared in CI after the oracle hash checkout fix allowed verification to reach the Deno source-suite step. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T05:20:00+05:30 + checked: GitHub Actions failure report. + found: Deno 2 ignores the removed files test config and broad task discovery reaches generated npm and Node-specific tests. + implication: Test discovery scope, not missing npm-test dependencies, is the failure boundary. + +- timestamp: 2026-08-01T05:21:00+05:30 + checked: Complete .github/workflows/port.yml. + found: The source-suite step runs deno task test without a path, exactly matching the broad discovery trigger. + implication: Scoping this existing CI invocation to test.js fixes the cause without modifying configs, dependencies, or generated tests. + +- timestamp: 2026-08-01T05:23:00+05:30 + checked: Exact workflow command audit and git diff --check. + found: deno test test.js is present; deno task test and --no-check are absent; git diff --check exited 0 with only line-ending conversion warnings. + implication: The workflow is narrowly scoped to the immutable source suite and the change has no whitespace errors. + +## Resolution + +root_cause: Deno 2 removed the files test configuration, but CI still invoked deno task test without a path. That broad invocation recursively discovered generated npm and Node port tests outside the intended immutable root suite, then type-checking failed on their environment-specific imports. +fix: Change only the source-suite workflow command to deno test test.js. +verification: Text audit confirmed deno test test.js is present and deno task test/--no-check are absent; git diff --check exited 0. +files_changed: [.github/workflows/port.yml, .planning/debug/resolved/ci-deno2-test-discovery.md] From bb32f71d14279cfa6b0edbfbb66be6ced7ddff5b Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 05:25:26 +0530 Subject: [PATCH 136/165] fix(04): match saturation conversion rounding --- compat/cases/operations.jsonl | 1 + src/tinycolor/color.go | 36 ++------------------------------ src/tinycolor/operations_test.go | 7 +++++++ 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 8a15dcb3..7c747c46 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -15,6 +15,7 @@ {"id":"mod-saturate-normal","operation":"modify","input":"#808040","args":{"method":"saturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-saturate-zero","operation":"modify","input":"#808040","args":{"method":"saturate","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} {"id":"mod-saturate-clamp","operation":"modify","input":"#808040","args":{"method":"saturate","amount":100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"mod-saturate-negative-rounding","operation":"modify","input":"#400140","args":{"method":"saturate","amount":-100},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-saturate-alpha","operation":"modify","input":"#80804080","args":{"method":"saturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-desaturate-normal","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":20},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"mod-desaturate-zero","operation":"modify","input":"#804020","args":{"method":"desaturate","amount":0},"owner":"C-mrashis","suspectedPackage":"compat"} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 5683da50..2728581a 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -494,40 +494,8 @@ func hsvColor(hue, saturation, value float64) Color { } func (c *Color) setHSL(h, s, l float64) { - c.model.R, c.model.G, c.model.B = hslToRGB(h, s, l) -} - -func hslToRGB(h, s, l float64) (float64, float64, float64) { - if s == 0 { - channel := l * 255 - return channel, channel, channel - } - var q float64 - if l < .5 { - q = l * (1 + s) - } else { - q = l + s - l*s - } - p := 2*l - q - channel := func(t float64) float64 { - if t < 0 { - t++ - } - if t > 1 { - t-- - } - switch { - case t*6 < 1: - return p + (q-p)*6*t - case t*2 < 1: - return q - case t*3 < 2: - return p + (q-p)*(2.0/3.0-t)*6 - default: - return p - } - } - return channel(h+1.0/3.0) * 255, channel(h) * 255, channel(h-1.0/3.0) * 255 + converted := hslColor(h*360, s, l, c.model.A, true) + c.model.R, c.model.G, c.model.B = converted.model.R, converted.model.G, converted.model.B } func clamp01(value float64) float64 { return math.Min(1, math.Max(0, value)) } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index df4ec630..005498f0 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -95,6 +95,13 @@ func TestModifiers(t *testing.T) { } } +func TestSaturateMatchesObjectConversionRounding(t *testing.T) { + color, _ := FromCompat("#400140", false) + if got := color.Saturate(-100).ToHexString(); got != "#202020" { + t.Fatalf("Saturate(-100) = %s", got) + } +} + func TestMix(t *testing.T) { black, _ := FromCompat("#000", false) white, _ := FromCompat("#fff", false) From 72031175e63cbc36c6967ade74f91ce9b62dfd4f Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 05:25:35 +0530 Subject: [PATCH 137/165] docs(debug): resolve fuzz saturation rounding --- .planning/debug/knowledge-base.md | 13 ++++ .../debug/resolved/fuzz-saturate-rounding.md | 72 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 .planning/debug/knowledge-base.md create mode 100644 .planning/debug/resolved/fuzz-saturate-rounding.md diff --git a/.planning/debug/knowledge-base.md b/.planning/debug/knowledge-base.md new file mode 100644 index 00000000..162375a4 --- /dev/null +++ b/.planning/debug/knowledge-base.md @@ -0,0 +1,13 @@ +# GSD Debug Knowledge Base + +Resolved debug sessions. Used by `gsd-debugger` to surface known-pattern hypotheses at the start of new investigations. + +--- + +## fuzz-saturate-rounding — Saturate negative rounding differed at an RGB half boundary +- **Date:** 2026-08-01 +- **Error patterns:** saturate, rounding, #400140, #202020, #212121, RGB 32, RGB 33, fuzz divergence +- **Root cause:** setHSL called a direct hslToRGB conversion, bypassing TinyColor's object-input percentage conversion and Bound01 truncation before RGB output rounding. +- **Fix:** Route setHSL through the existing hslColor/parser conversion and remove the duplicate direct converter. +- **Files changed:** src/tinycolor/color.go, src/tinycolor/operations_test.go, compat/cases/operations.jsonl +--- diff --git a/.planning/debug/resolved/fuzz-saturate-rounding.md b/.planning/debug/resolved/fuzz-saturate-rounding.md new file mode 100644 index 00000000..3a8d8bba --- /dev/null +++ b/.planning/debug/resolved/fuzz-saturate-rounding.md @@ -0,0 +1,72 @@ +--- +status: resolved +trigger: "Find and fix the first deterministic Phase 5 fuzz parity mismatch: saturate(-100) on #400140 returns #212121 in Go but #202020 in JavaScript." +created: 2026-08-01T05:19:38.0748150+05:30 +updated: 2026-08-01T05:44:00+05:30 +--- + +## Current Focus + +hypothesis: confirmed — Go setHSL bypasses TinyColor's object-input percentage conversion and Bound01 truncation, retaining 32.5 instead of the oracle's 32.487 before identical half-up output rounding +test: completed +expecting: all requested checks pass +next_action: archived; commit remains for a caller with writable Git metadata + +## Symptoms + +expected: JavaScript oracle and Go response match exactly for request {id:fuzz-2, operation:modify, input:#400140, args:{method:saturate, amount:-100}}; JS after is #202020 with RGB 32,32,32. +actual: Go after is #212121 with RGB 33,33,33. +errors: seed-1 one-second fuzz run found 338 divergences; first mismatch is deterministic and full request/outputs are in .superpowers/sdd/05-02-PLAN/task-1-report.md. +reproduction: send the exact JSONL request to persistent or one-shot compat/js-runner.mjs and the Go CLI; also call typed Color.Saturate(-100) on #400140. +started: uncovered by the new broad Phase 5 fuzz generator; fixed Phase 4 corpus did not include this input. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T05:23:00+05:30 + checked: prior Phase 5 task report and repository status + found: exact request deterministically differs (#202020 JS vs #212121 Go); only concurrent fuzz harness files are dirty + implication: preserve fuzz files and isolate the fix to shared Go conversion behavior plus regressions + +- timestamp: 2026-08-01T05:23:00+05:30 + checked: repository knowledge base and user memory registry + found: no matching prior TinyColor/HSL rounding diagnosis + implication: investigate from source behavior rather than reuse a known pattern + +- timestamp: 2026-08-01T05:25:00+05:30 + checked: first adapter reproduction attempt + found: setup was invalid because Node ran from src and Go's default cache was sandbox-denied + implication: no behavioral conclusion; rerun from root and set GOCACHE inside the workspace + +- timestamp: 2026-08-01T05:30:00+05:30 + checked: exact request through root compat/js-runner.mjs and src/cmd/tinycolor-compat + found: reproduced #202020 in JavaScript and #212121 in Go with otherwise identical responses + implication: mismatch is in shared typed color behavior, not fuzz harness or adapter serialization + +- timestamp: 2026-08-01T05:30:00+05:30 + checked: source rgbToHsl/saturate/tinycolor(hsl)/hslToRgb path and all Go setHSL callers + found: JavaScript converts l=0.12745098039215685 to "12.745098039215685%" then Bound01 truncates to 12.74%, yielding 32.487; Go setHSL directly multiplies unquantized l by 255, yielding exactly 32.5 + implication: identical half-up rounding then produces 32 versus 33; source-compatible percentage quantization is missing in the shared Go modifier conversion path used by Lighten, Darken, Saturate, Desaturate, Greyscale, and Spin + +- timestamp: 2026-08-01T05:36:00+05:30 + checked: typed regression and full operations JSONL corpus before production change + found: typed test failed with #212121; corpus passed 69/70 and only the new exact regression mismatched + implication: regression isolates the defect and provides both API-level and adapter-level RED evidence + +- timestamp: 2026-08-01T05:40:00+05:30 + checked: typed regression, 70-case operations corpus, and exact request through both adapters after fix + found: typed test passed; corpus passed 70/70 with zero mismatches; both adapters returned #202020 and RGB 32,32,32 + implication: minimal shared-path fix resolves the original issue and adapter parity + +- timestamp: 2026-08-01T05:44:00+05:30 + checked: go test ./..., go vet ./..., git diff --check, scoped diff, and commit attempt + found: all tests, vet, and diff check passed; scoped diff contains only the shared fix and two regressions; commit failed because .git/index.lock could not be created + implication: code is verified and ready to commit, but repository metadata permissions prevent this agent from creating the requested commit + +## Resolution + +root_cause: setHSL calls hslToRGB directly with normalized HSL values, while the JavaScript modifier reconstructs through tinycolor(hsl), whose object parser converts s/l to percentage strings and Bound01 truncates them to four decimal ratio precision before RGB conversion. At the 32.5 boundary this changes the rounded channel. +fix: setHSL now reuses hslColor and the existing parser conversion, removing the duplicate direct hslToRGB implementation that skipped TinyColor percentage quantization. +verification: focused typed test passed; operations corpus 70/70 with zero mismatches; exact JS and Go outputs both #202020/RGB 32; go test ./... passed; go vet ./... passed; git diff --check passed. +files_changed: [src/tinycolor/color.go, src/tinycolor/operations_test.go, compat/cases/operations.jsonl] From 78a6b6910f839c63e76ab7aefad4546059f763e4 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:36:58 +0530 Subject: [PATCH 138/165] wip: delivery evidence paused at task 1/3 --- .planning/HANDOFF.json | 111 ++++++++ .planning/debug/fuzz-readability-precision.md | 61 +++++ .../05-delivery-evidence/.continue-here.md | 158 +++++++++++ compat/cases/operations.jsonl | 1 + fuzz/README.md | 24 +- fuzz/harness.mjs | 254 ++++++++++++++++++ fuzz/harness.test.mjs | 54 ++++ src/cmd/pow-debug/main.go | 13 + src/tinycolor/operations_test.go | 14 + 9 files changed, 688 insertions(+), 2 deletions(-) create mode 100644 .planning/HANDOFF.json create mode 100644 .planning/debug/fuzz-readability-precision.md create mode 100644 .planning/phases/05-delivery-evidence/.continue-here.md create mode 100644 fuzz/harness.mjs create mode 100644 fuzz/harness.test.mjs create mode 100644 src/cmd/pow-debug/main.go diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json new file mode 100644 index 00000000..93b58754 --- /dev/null +++ b/.planning/HANDOFF.json @@ -0,0 +1,111 @@ +{ + "version": "1.0", + "timestamp": "2026-08-01T00:04:30.289Z", + "phase": "5", + "phase_name": "Delivery Evidence", + "phase_dir": ".planning/phases/05-delivery-evidence", + "plan": 2, + "task": 1, + "total_tasks": 3, + "status": "paused", + "completed_tasks": [ + { + "id": "05-01-01", + "name": "Human CLI", + "status": "done", + "commit": "df1f9b8" + }, + { + "id": "05-01-02", + "name": "Reproducible verification gate", + "status": "done", + "commit": "a4ad85e" + }, + { + "id": "05-01-03", + "name": "Port CI and source suite", + "status": "done", + "commit": "d8ba485" + }, + { + "id": "05-02-01", + "name": "Differential fuzz harness", + "status": "in_progress", + "progress": "Persistent seeded harness is implemented but deliberately uncommitted/blocked by an exact readability precision mismatch. First saturation mismatch was fixed in 0a9db8b." + } + ], + "remaining_tasks": [ + { + "id": "05-02-01", + "name": "Finish broad fuzz harness after readability parity fix", + "status": "in_progress" + }, + { + "id": "05-02-02", + "name": "Validate and record real 60-second fuzz log", + "status": "not_started" + }, + { + "id": "05-02-03", + "name": "Generate shared-workload benchmark evidence", + "status": "not_started" + }, + { + "id": "05-03", + "name": "Submission documentation, evidence refresh, external checkpoint, and phase closeout", + "status": "not_started" + } + ], + "blockers": [ + { + "description": "Exact readability mismatch: JavaScript 1.188086751976723 versus Go 1.1880867519767233 for {r:127,g:64,b:15,a:0} against #80007f.", + "type": "technical", + "workaround": "None allowed. Do not add tolerance or narrow fuzz inputs. Continue .planning/debug/fuzz-readability-precision.md; root cause is V8 Math.pow versus Go math.Pow final rounding. An exact bounded 256-entry transfer lookup is the leading safe fix." + }, + { + "description": "The WIP branch intentionally contains a failing exact readability regression and fuzz zero-divergence test.", + "type": "technical", + "workaround": "Fix the shared Luminance transfer function, run the exact regression and operations corpus, then rerun the broad seed-1 fuzz smoke before committing production completion." + } + ], + "human_actions_pending": [ + { + "action": "Record and publish the five-minute demo video, then add its URL.", + "context": "docs/DEMO.md is still a future Plan 05-03 deliverable; no video is claimed.", + "blocking": true + } + ], + "decisions": [ + { + "decision": "Keep broad bounded fuzz generation and stop on every exact mismatch.", + "rationale": "Narrowing to corpus-proven inputs would hide defects and violate behavioral-equivalence scoring.", + "phase": "5" + }, + { + "decision": "Pin oracle checkout bytes to CRLF with .gitattributes instead of normalizing hashes.", + "rationale": "The kickoff manifest hashes raw CRLF working-tree bytes; CI must reproduce that byte contract.", + "phase": "5" + }, + { + "decision": "Run `deno test test.js` explicitly in CI.", + "rationale": "Deno 2 removed broad discovery exclusions and otherwise includes generated npm and Node port tests outside the immutable source suite.", + "phase": "5" + }, + { + "decision": "Prefer an exact finite-domain luminance transfer lookup if no smaller V8-compatible power implementation exists.", + "rationale": "TinyColor rounds luminance channels to 8-bit integers; Go math.Pow differs from V8 for 93 of 245 nonlinear channels and heuristic ULP correction is incorrect.", + "phase": "5" + } + ], + "uncommitted_files": [ + "compat/cases/operations.jsonl", + "fuzz/README.md", + "fuzz/harness.mjs", + "fuzz/harness.test.mjs", + "src/tinycolor/operations_test.go", + "src/cmd/pow-debug/main.go", + ".planning/debug/fuzz-readability-precision.md" + ], + "next_action": "Run `$gsd-resume-work`, read `.planning/debug/fuzz-readability-precision.md`, finish the exact luminance transfer fix with the existing RED tests, then run `node fuzz/harness.mjs --duration 1 --seed 1` without narrowing the generator.", + "context_notes": "Branch rajeet. GitHub Actions run 30673847990 passed make verify, deno test test.js, build, and artifact upload. Local Deno 2.9.4 ran the immutable suite: 45 passed, 0 failed, 1 ignored. Current broad fuzz smoke is expected red: 12,291 cases and 388 divergences before the readability fix." +} diff --git a/.planning/debug/fuzz-readability-precision.md b/.planning/debug/fuzz-readability-precision.md new file mode 100644 index 00000000..1cbb524b --- /dev/null +++ b/.planning/debug/fuzz-readability-precision.md @@ -0,0 +1,61 @@ +--- +status: investigating +trigger: "Investigate and fix next deterministic Phase 5 fuzz parity mismatch: readability for {r:127,g:64,b:15,a:0} against '#80007f' returns 1.188086751976723 in JavaScript and 1.1880867519767233 in Go." +created: 2026-08-01T00:00:00+05:30 +updated: 2026-08-01T00:04:00+05:30 +--- + +## Current Focus + +hypothesis: confirmed: V8 Math.pow and Go math.Pow use different algorithms whose final rounding differs across the finite 8-bit luminance channel domain +test: inspect the primary V8 implementation and compare all 245 nonlinear channel values to select the smallest exact shared-method implementation +expecting: algorithm evidence plus the bounded mismatch distribution will show whether a small compatible calculation exists or an exact transfer table is required +next_action: inspect V8 ieee754 pow source and evaluate exact implementation options + +## Symptoms + +expected: JavaScript and Go exact JSON response match for readability; JavaScript returns 1.188086751976723. +actual: Go returns 1.1880867519767233; broad seed-1 one-second fuzz run has 388 divergences and fuzz-32 is first. +errors: exact floating-point JSON mismatch for operation readability +reproduction: exact request is in .superpowers/sdd/05-02-PLAN/task-1-report.md; run through both adapters and typed Readability with input {r:127,g:64,b:15,a:0}, args.other '#80007f'. +started: uncovered after the fuzz saturation root-cause fix. + +## Eliminated + +## Evidence + +- timestamp: 2026-08-01T00:01:00+05:30 + checked: repository status and protected fuzz files + found: fuzz/README.md is modified and fuzz/harness.mjs plus fuzz/harness.test.mjs are untracked pre-existing work + implication: preserve those files without editing, staging, reverting, or narrowing them +- timestamp: 2026-08-01T00:01:00+05:30 + checked: debug knowledge base + found: only prior saturation rounding case exists and its keywords/root cause do not match readability precision + implication: no known-pattern candidate applies +- timestamp: 2026-08-01T00:01:00+05:30 + checked: complete JavaScript getLuminance/readability and Go Color.Luminance/Readability implementations plus callers + found: both round normalized RGB before luminance and use the same WCAG constants; Go caches the two luminances once while JavaScript recomputes them for Max and Min; IsReadable and MostReadable both call shared Readability + implication: parsing and exact arithmetic intermediates must be measured before changing the shared method +- timestamp: 2026-08-01T00:02:00+05:30 + checked: exact requests through both adapters + found: normalized RGB inspections are identical; first luminance is identical at 0.08213307169664993, but #80007f luminance is 0.06121500300950977 in JavaScript and 0.061215003009509765 in Go + implication: parsing, alpha, and final ratio ordering are ruled out; the first divergence is inside shared Color.Luminance for integer RGB 128,0,127 +- timestamp: 2026-08-01T00:03:00+05:30 + checked: new exact typed regression and exact operations JSONL row before production changes + found: typed test fails with 1.1880867519767233 and corpus has exactly one mismatch at readability-object-precision + implication: RED is confirmed and independently covers both public Go API and adapter parity +- timestamp: 2026-08-01T00:04:00+05:30 + checked: channel-level gamma expansion raw float bits for #80007f + found: channel 128 has identical normalized/base input but V8 Math.pow yields bits 3fcba1511e3e632d while Go math.Pow yields 3fcba1511e3e632c; channel 127 matches at 3fcb2a60a1263b0a + implication: the root cause is Go math.Pow not reproducing the JavaScript oracle's power rounding, not weighted luminance summation +- timestamp: 2026-08-01T00:04:00+05:30 + checked: all nonlinear integer channels 11 through 255 + found: 93 of 245 channel powers differ, with signed deltas in both directions and two cases differing by two ULPs + implication: unconditional Nextafter or decimal/output rounding would be incorrect; the fix must reproduce the bounded transfer function or V8 algorithm + +## Resolution + +root_cause: +fix: +verification: +files_changed: [] diff --git a/.planning/phases/05-delivery-evidence/.continue-here.md b/.planning/phases/05-delivery-evidence/.continue-here.md new file mode 100644 index 00000000..650ca199 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/.continue-here.md @@ -0,0 +1,158 @@ +--- +phase: 05-delivery-evidence +plan: 2 +task: 1 +total_tasks: 3 +status: in_progress +last_updated: 2026-08-01T00:04:30.289Z +branch: rajeet +--- + + + +Phase 5 Plan 05-01 is implemented and CI-verified. Plan 05-02 Task 1 is paused +mid-debug: the broad persistent differential harness is implemented, but its +zero-divergence test correctly remains RED because fuzzing found exact port +defects. Do not narrow the generator or add comparison tolerance. + +The branch is intentionally being handed off with WIP files. The first fuzz +defect (`saturate(-100)` rounding) is fixed and committed. The next defect is +an exact WCAG luminance/readability precision mismatch under active diagnosis. + + + + + +- Phase 5 design, context, research, validation, and three execution plans were + written and independently plan-checked. +- Human CLI commands `parse`, `convert`, `lighten`, `palette`, and `contrast` + were implemented with JSON output while preserving zero-argument JSONL mode. +- Added one-command Make build/verify, oracle hash verification, scratch Docker + artifact, and GitHub Actions delivery workflow. +- Fixed CI raw-byte hash failures by pinning the three oracle files to CRLF in + `.gitattributes`; a simulated `core.autocrlf=false` checkout prints + `verified: 3`. +- Fixed Deno 2 discovery by running the immutable suite explicitly with + `deno test test.js`. +- GitHub Actions run + `https://github.com/rajeet-04/TinyColor/actions/runs/30673847990` passed in + 42 seconds: `make verify`, Deno source suite, build, and artifact upload. +- Local Deno 2.9.4 source suite result: 45 passed, 0 failed, 1 ignored. +- Built the standard-library persistent fuzz harness with seeded broad inputs, + one outstanding request per child, protocol safeguards, and RED/GREEN tests. +- Fixed the first fuzz mismatch in `0a9db8b`: TinyColor HSL percentage + truncation for `saturate(-100)` now matches; operations corpus is 70/70 at + that commit. + + + + + +1. Finish Task 05-02-01: + - Continue `.planning/debug/fuzz-readability-precision.md`. + - Existing RED case: `{r:127,g:64,b:15,a:0}` vs `#80007f` returns + JavaScript `1.188086751976723`, Go `1.1880867519767233`. + - Root cause is confirmed inside nonlinear luminance channel expansion: + V8 `Math.pow` and Go `math.Pow` differ in final bits for 93 of the 245 + nonlinear 8-bit channels; two differ by two ULPs. + - `src/tinycolor/operations_test.go` and + `compat/cases/operations.jsonl` contain the failing exact regression. + - `src/cmd/pow-debug/main.go` is a temporary diagnostic that prints Go + channel power bits. + - Leading safe fix: a reproducibly generated exact 256-entry V8 transfer + lookup used by `Color.Luminance`. Do not use `Nextafter`, decimal output + rounding, or tolerance. + - After the exact regression and 71-row operations corpus pass, rerun + `node --test fuzz/harness.test.mjs` and + `node fuzz/harness.mjs --duration 1 --seed 1` broadly. If another + mismatch appears, repeat deterministic regression -> root cause -> micro + commit. +2. Task 05-02-02: add tested fuzz-log validator and record an actual + `--duration 60 --seed 20260801` zero-divergence run. +3. Task 05-02-03: implement shared workload benchmark runner and publish actual + p99 latency, peak RSS, startup, and throughput results without quick-run + overwrite. +4. Plan 05-03: rewrite judge-facing README, expand DECISIONS to 12+ substantive + rows, refresh COMPATIBILITY with exact evidence/coverage/unsafe counts, add + demo script, verify public clone/current CI, and close roadmap/state. +5. Human: record and publish the five-minute demo video; no video is currently + claimed. + + + + + +- Broad fuzzing stays broad. A mismatch blocks the bonus and becomes a fixed + regression; inputs are never constrained to make the log green. +- Oracle hashing remains byte-exact. `.gitattributes` reproduces kickoff CRLF + bytes on Linux rather than weakening verification with newline normalization. +- CI runs `deno test test.js`, not broad `deno task test`, because Deno 2 would + discover generated npm and port-owned Node tests. +- Luminance parity needs an exact finite-domain solution. Heuristic ULP + correction is ruled out because channel deltas have both signs. +- Keep micro-commits: regression/root-cause fixes, evidence artifacts, and + debug/archive docs stay separately revertible. + + + + + +- Technical blocker: exact V8/Go power rounding mismatch in `Color.Luminance`. +- Expected red checks until fixed: + - `go test ./tinycolor -run TestReadabilityMatchesJavaScriptPrecision` + - operations corpus has the new readability case as its mismatch + - `node --test fuzz/harness.test.mjs` fails only its zero-divergence test + - seed-1 one-second broad run previously produced 12,291 cases / 388 + divergences; first was the readability case above. + + + + + +- `fuzz/harness.mjs` — persistent broad differential harness. +- `fuzz/harness.test.mjs` — PRNG/options/generator/end-to-end tests. +- `fuzz/README.md` — reproduction and claim rules. +- `src/tinycolor/operations_test.go` — exact readability RED regression and + temporary numeric logging. +- `compat/cases/operations.jsonl` — adapter RED regression row. +- `.planning/debug/fuzz-readability-precision.md` — active root-cause evidence. +- `src/cmd/pow-debug/main.go` — temporary channel-bit diagnostic. + +These files are preserved in the WIP handoff commit. The temporary logging and +`pow-debug` command may be removed only after the exact solution is verified; +do not delete them before the next agent reads the evidence. + + + + + +- GitHub Actions: PASS — run 30673847990. +- Local Deno: PASS — 45 passed, 0 failed, 1 ignored. +- Oracle hash verifier: PASS — 3 files. +- Fixed corpus through saturation commit: operations 70/70. +- Current WIP: intentionally RED only for unresolved readability precision and + fuzz zero-divergence. +- No background server or watcher is running; all subagents were stopped. + + + + + +Start with: + +```text +$gsd-resume-work +``` + +Then read this file and `.planning/debug/fuzz-readability-precision.md`. Finish +the exact luminance transfer fix with the existing RED tests before changing +the fuzz harness. The first mandatory verification after the fix is: + +```powershell +$env:GOCACHE = (Resolve-Path '.cache\go-build').Path +go -C src test ./tinycolor -run TestReadabilityMatchesJavaScriptPrecision -count=1 +node compat/run.mjs compat/cases/operations.jsonl +node fuzz/harness.mjs --duration 1 --seed 1 +``` + + diff --git a/compat/cases/operations.jsonl b/compat/cases/operations.jsonl index 7c747c46..b4f68828 100644 --- a/compat/cases/operations.jsonl +++ b/compat/cases/operations.jsonl @@ -42,6 +42,7 @@ {"id":"readability-same","operation":"readability","input":"#000","args":{"other":"#000"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readability-low","operation":"readability","input":"#000","args":{"other":"#111"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readability-max","operation":"readability","input":"#000","args":{"other":"#fff"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} +{"id":"readability-object-precision","operation":"readability","input":{"r":127,"g":64,"b":15,"a":0},"args":{"other":"#80007f"},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readable-aa-small","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":"AA","size":"small"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readable-aa-large","operation":"isReadable","input":"#ff0088","args":{"other":"#5c1a72","options":{"level":"AA","size":"large"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} {"id":"readable-aaa-small","operation":"isReadable","input":"#db91b8","args":{"other":"#2e0c3a","options":{"level":"AAA","size":"small"}},"owner":"B-xthxr","suspectedPackage":"src/tinycolor"} diff --git a/fuzz/README.md b/fuzz/README.md index d021a6ca..db962269 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -1,4 +1,24 @@ # Differential fuzzing -Phase 3 adds a seeded generator here. Until then, the checked-in JSONL corpora -in `compat/cases/` are the reproducible differential input set. +`harness.mjs` sends one deterministic JSONL request stream to persistent +JavaScript and Go adapters and compares every parsed response exactly. + +One-second smoke: + +```text +node fuzz/harness.mjs --duration 1 --seed 1 +``` + +Full evidence run: + +```text +node fuzz/harness.mjs --duration 60 --seed 20260801 +``` + +Rerun a failure with the logged seed and duration. Each divergence is a JSON +object containing the request and both responses; the four final lines report +duration, seed, case count, and divergence count. + +Claim zero divergences only from a real saved run whose reported duration meets +the claim. A deterministic seed reproduces the request prefix, while case count +can vary with machine speed. diff --git a/fuzz/harness.mjs b/fuzz/harness.mjs new file mode 100644 index 00000000..5781f19e --- /dev/null +++ b/fuzz/harness.mjs @@ -0,0 +1,254 @@ +import { execFileSync, spawn } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { resolve } from "node:path"; +import { createInterface } from "node:readline"; +import { pathToFileURL } from "node:url"; +import { isDeepStrictEqual } from "node:util"; + +const root = resolve(import.meta.dirname, ".."); +const defaults = { duration: 60, seed: 20260801 }; +const operations = ["inspect", "output", "modify", "mix", "readability", "isReadable", "palette"]; +const outputMethods = [ + "toHex", "toHex8", "toHexString", "toHex8String", "toRgbString", + "toPercentageRgbString", "toHslString", "toHsvString", "toString", + "toName", "toFilter", +]; +const modifyMethods = ["lighten", "brighten", "darken", "saturate", "desaturate", "greyscale", "spin"]; +const paletteMethods = ["complement", "splitcomplement", "triad", "tetrad", "analogous", "monochromatic"]; +const components = [0, 1, 15, 32, 64, 127, 128, 192, 254, 255]; +const alphas = [0, 0.25, 0.5, 0.75, 1]; + +export function createPrng(seed) { + let state = seed >>> 0; + return () => { + state = (state + 0x6d2b79f5) >>> 0; + let value = state; + value = Math.imul(value ^ value >>> 15, value | 1); + value ^= value + Math.imul(value ^ value >>> 7, value | 61); + return ((value ^ value >>> 14) >>> 0) / 0x100000000; + }; +} + +const pick = (random, values) => values[Math.floor(random() * values.length)]; + +function colorInput(random) { + const r = pick(random, components); + const g = pick(random, components); + const b = pick(random, components); + const a = pick(random, alphas); + switch (Math.floor(random() * 4)) { + case 0: return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`; + case 1: return `rgb(${r}, ${g}, ${b})`; + case 2: return `rgba(${r}, ${g}, ${b}, ${a})`; + default: return { r, g, b, a }; + } +} + +export function generateRequest(random, index) { + const operation = operations[index % operations.length]; + const input = colorInput(random); + const request = { id: `fuzz-${index}`, operation, input, args: {} }; + switch (operation) { + case "output": { + const method = pick(random, outputMethods); + request.args.method = method; + if (method === "toString") request.args.format = pick(random, ["hex", "hex8", "rgb", "prgb", "hsl", "hsv", "name"]); + if (method === "toFilter") request.args.secondColor = colorInput(random); + break; + } + case "modify": { + const method = pick(random, modifyMethods); + request.args.method = method; + if (method !== "greyscale") request.args.amount = method === "spin" + ? pick(random, [-360, -180, -120, 0, 120, 180, 360]) + : pick(random, [-100, -50, 0, 10, 20, 50, 100]); + break; + } + case "mix": + request.args = { other: colorInput(random), amount: pick(random, [-50, 0, 25, 50, 75, 100, 150]) }; + break; + case "readability": + request.args.other = colorInput(random); + break; + case "isReadable": + request.args = { + other: colorInput(random), + options: { level: pick(random, ["AA", "AAA"]), size: pick(random, ["small", "large"]) }, + }; + break; + case "palette": { + const method = pick(random, paletteMethods); + request.args.method = method; + if (method === "analogous") Object.assign(request.args, { results: pick(random, [1, 3, 6]), slices: pick(random, [6, 12, 30]) }); + if (method === "monochromatic") request.args.results = pick(random, [1, 3, 6]); + break; + } + } + return request; +} + +function validateOptions(options) { + if (!Number.isFinite(options.duration) || options.duration <= 0) throw new Error("--duration must be a positive number"); + if (!Number.isSafeInteger(options.seed)) throw new Error("--seed must be an integer"); + return options; +} + +export function parseOptions(args) { + const options = { ...defaults }; + for (let index = 0; index < args.length; index += 2) { + const flag = args[index]; + const value = args[index + 1]; + if (value === undefined) throw new Error(`${flag} requires a value`); + if (flag === "--duration") options.duration = Number(value); + else if (flag === "--seed") options.seed = Number(value); + else throw new Error(`unknown option: ${flag}`); + } + return validateOptions(options); +} + +function startRunner(command, args, env) { + const child = spawn(command, args, { cwd: root, env, stdio: ["pipe", "pipe", "pipe"] }); + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + let stderr = ""; + let spawnError; + child.stderr.on("data", (chunk) => { stderr += chunk; }); + child.on("error", (error) => { spawnError = error; }); + const closed = new Promise((resolveClose) => child.once("close", (code, signal) => resolveClose({ code, signal }))); + return { + child, + closed, + get failure() { return spawnError; }, + get stderr() { return stderr.trim(); }, + lines: createInterface({ input: child.stdout, crlfDelay: Infinity })[Symbol.asyncIterator](), + }; +} + +function withTimeout(promise, milliseconds, message) { + let timer; + return Promise.race([ + promise, + new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), milliseconds); }), + ]).finally(() => clearTimeout(timer)); +} + +function writeLine(runner, line) { + return new Promise((resolveWrite, reject) => { + const fail = (error) => reject(new Error(`write failure: ${error.message}`)); + runner.child.stdin.once("error", fail); + runner.child.stdin.write(`${line}\n`, (error) => { + runner.child.stdin.off("error", fail); + if (error) fail(error); + else resolveWrite(); + }); + }); +} + +async function exchange(runner, request) { + await writeLine(runner, JSON.stringify(request)); + const { value, done } = await runner.lines.next(); + if (done) { + const detail = runner.failure?.message || runner.stderr || "child exited before responding"; + throw new Error(detail); + } + let response; + try { + response = JSON.parse(value); + } catch { + throw new Error(`invalid JSON response: ${value}`); + } + if (response.id !== request.id) throw new Error(`response ID mismatch: expected ${request.id}, got ${response.id}`); + return response; +} + +async function stopRunner(runner) { + if (!runner) return; + if (!runner.child.stdin.destroyed) runner.child.stdin.end(); + let result; + try { + result = await withTimeout(runner.closed, 1_000, "child shutdown timeout"); + } catch (error) { + runner.child.kill(); + await runner.closed; + throw error; + } + if (runner.failure) throw runner.failure; + if (result.code !== 0) throw new Error(runner.stderr || `child exited ${result.code ?? result.signal}`); +} + +function buildBinary(env) { + mkdirSync(resolve(root, "bin"), { recursive: true }); + const suffix = execFileSync("go", ["env", "GOEXE"], { cwd: root, env, encoding: "utf8" }).trim(); + execFileSync("go", ["-C", "src", "build", "-o", `../bin/tinycolor${suffix}`, "./cmd/tinycolor-compat"], { + cwd: root, + env, + stdio: ["ignore", "ignore", "inherit"], + }); + return resolve(root, "bin", `tinycolor${suffix}`); +} + +export async function runHarness({ duration = defaults.duration, seed = defaults.seed, onDivergence = console.log } = {}) { + validateOptions({ duration, seed }); + const env = { ...process.env, GOCACHE: process.env.GOCACHE ?? resolve(root, ".cache", "go-build") }; + const binary = buildBinary(env); + const javascript = startRunner(process.execPath, ["compat/js-runner.mjs"], env); + const go = startRunner(binary, [], env); + const random = createPrng(seed); + let cases = 0; + let divergences = 0; + let start; + let failure; + + try { + start = process.hrtime.bigint(); + const durationNanoseconds = BigInt(Math.ceil(duration * 1e9)); + do { + const request = generateRequest(random, cases); + const [javascriptResponse, goResponse] = await withTimeout( + Promise.all([exchange(javascript, request), exchange(go, request)]), + 5_000, + `timeout waiting for ${request.id}`, + ); + cases++; + if (!isDeepStrictEqual(javascriptResponse, goResponse)) { + divergences++; + onDivergence(JSON.stringify({ request, javascript: javascriptResponse, go: goResponse })); + } + } while (process.hrtime.bigint() - start < durationNanoseconds); + } catch (error) { + failure = error; + } + + const stopped = await Promise.allSettled([stopRunner(javascript), stopRunner(go)]); + if (!failure) failure = stopped.find(({ status }) => status === "rejected")?.reason; + const elapsedSeconds = start ? Number(process.hrtime.bigint() - start) / 1e9 : 0; + const summary = { elapsedSeconds, seed, cases, divergences }; + if (failure) { + failure.summary = summary; + throw failure; + } + return summary; +} + +function printSummary(summary) { + console.log(`duration_seconds: ${summary.elapsedSeconds.toFixed(3)}`); + console.log(`seed: ${summary.seed}`); + console.log(`cases: ${summary.cases}`); + console.log(`divergences: ${summary.divergences}`); +} + +async function main() { + let options = defaults; + try { + options = parseOptions(process.argv.slice(2)); + const summary = await runHarness({ ...options, onDivergence: console.log }); + printSummary(summary); + if (summary.divergences) process.exitCode = 1; + } catch (error) { + console.error(error.message); + printSummary(error.summary ?? { elapsedSeconds: 0, seed: options.seed, cases: 0, divergences: 0 }); + process.exitCode = 1; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) await main(); diff --git a/fuzz/harness.test.mjs b/fuzz/harness.test.mjs new file mode 100644 index 00000000..0537c46a --- /dev/null +++ b/fuzz/harness.test.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createPrng, generateRequest, parseOptions, runHarness } from "./harness.mjs"; + +const methods = { + output: new Set([ + "toHex", "toHex8", "toHexString", "toHex8String", "toRgbString", + "toPercentageRgbString", "toHslString", "toHsvString", "toString", + "toName", "toFilter", + ]), + modify: new Set(["lighten", "brighten", "darken", "saturate", "desaturate", "greyscale", "spin"]), + palette: new Set(["complement", "splitcomplement", "triad", "tetrad", "analogous", "monochromatic"]), +}; +const operations = new Set(["inspect", "output", "modify", "mix", "readability", "isReadable", "palette"]); + +test("seed 1 repeats the same PRNG sequence", () => { + const first = createPrng(1); + const second = createPrng(1); + assert.deepEqual(Array.from({ length: 8 }, first), Array.from({ length: 8 }, second)); +}); + +test("request generation is deterministic and adapter-supported", () => { + const first = createPrng(20260801); + const second = createPrng(20260801); + const requests = Array.from({ length: 70 }, (_, index) => generateRequest(first, index)); + + assert.deepEqual(requests, Array.from({ length: 70 }, (_, index) => generateRequest(second, index))); + for (const [index, request] of requests.entries()) { + assert.equal(request.id, `fuzz-${index}`); + assert.ok(operations.has(request.operation), request.operation); + if (methods[request.operation]) assert.ok(methods[request.operation].has(request.args.method), request.args.method); + } + assert.deepEqual(new Set(requests.map(({ operation }) => operation)), operations); +}); + +test("option parsing validates duration and seed", () => { + assert.deepEqual(parseOptions([]), { duration: 60, seed: 20260801 }); + assert.deepEqual(parseOptions(["--duration", "0.25", "--seed", "1"]), { duration: 0.25, seed: 1 }); + for (const args of [ + ["--duration", "0"], + ["--duration", "-1"], + ["--duration", "nope"], + ["--seed", "1.5"], + ["--seed", "nope"], + ]) assert.throws(() => parseOptions(args)); +}); + +test("one-second differential run uses persistent adapters", { timeout: 30_000 }, async () => { + const summary = await runHarness({ duration: 1, seed: 1, onDivergence() {} }); + assert.ok(summary.elapsedSeconds >= 1, summary.elapsedSeconds); + assert.ok(summary.cases > 0, summary.cases); + assert.equal(summary.divergences, 0); +}); diff --git a/src/cmd/pow-debug/main.go b/src/cmd/pow-debug/main.go new file mode 100644 index 00000000..6909c8f4 --- /dev/null +++ b/src/cmd/pow-debug/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "math" +) + +func main() { + for channel := 11; channel < 256; channel++ { + normalized := float64(channel) / 255 + fmt.Printf("%d %x\n", channel, math.Float64bits(math.Pow((normalized+.055)/1.055, 2.4))) + } +} diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 005498f0..a4a40f31 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -144,6 +144,20 @@ func TestReadability(t *testing.T) { } } +func TestReadabilityMatchesJavaScriptPrecision(t *testing.T) { + first, _ := FromCompat(map[string]any{"r": 127.0, "g": 64.0, "b": 15.0, "a": 0.0}, false) + second, _ := FromCompat("#80007f", false) + for _, channel := range []float64{128, 127} { + normalized := channel / 255 + base := (normalized + .055) / 1.055 + linear := math.Pow(base, 2.4) + t.Logf("channel %.0f normalized %.17g pow %.17g/%x exp-log %.17g/%x exp2-log2 %.17g/%x", channel, normalized, linear, math.Float64bits(linear), math.Exp(2.4*math.Log(base)), math.Float64bits(math.Exp(2.4*math.Log(base))), math.Exp2(2.4*math.Log2(base)), math.Float64bits(math.Exp2(2.4*math.Log2(base)))) + } + if got := Readability(first, second); got != 1.188086751976723 { + t.Fatalf("Readability precision = %.17g", got) + } +} + func TestIsReadable(t *testing.T) { for _, test := range []struct { name string From 547a098053e499aa0f0309672e6ca74fce409fbb Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:46:53 +0530 Subject: [PATCH 139/165] fix(05-02): match exact fuzz parity --- .planning/debug/fuzz-readability-precision.md | 20 ++++----- DECISIONS.md | 3 +- src/cmd/pow-debug/main.go | 13 ------ src/tinycolor/color.go | 29 +++++++----- src/tinycolor/luminance.go | 45 +++++++++++++++++++ src/tinycolor/operations_test.go | 14 +++--- 6 files changed, 82 insertions(+), 42 deletions(-) delete mode 100644 src/cmd/pow-debug/main.go create mode 100644 src/tinycolor/luminance.go diff --git a/.planning/debug/fuzz-readability-precision.md b/.planning/debug/fuzz-readability-precision.md index 1cbb524b..3383d91c 100644 --- a/.planning/debug/fuzz-readability-precision.md +++ b/.planning/debug/fuzz-readability-precision.md @@ -1,16 +1,16 @@ --- -status: investigating +status: resolved trigger: "Investigate and fix next deterministic Phase 5 fuzz parity mismatch: readability for {r:127,g:64,b:15,a:0} against '#80007f' returns 1.188086751976723 in JavaScript and 1.1880867519767233 in Go." created: 2026-08-01T00:00:00+05:30 -updated: 2026-08-01T00:04:00+05:30 +updated: 2026-08-01T06:00:00+05:30 --- ## Current Focus -hypothesis: confirmed: V8 Math.pow and Go math.Pow use different algorithms whose final rounding differs across the finite 8-bit luminance channel domain -test: inspect the primary V8 implementation and compare all 245 nonlinear channel values to select the smallest exact shared-method implementation -expecting: algorithm evidence plus the bounded mismatch distribution will show whether a small compatible calculation exists or an exact transfer table is required -next_action: inspect V8 ieee754 pow source and evaluate exact implementation options +hypothesis: resolved: exact V8-derived results are required for the finite 8-bit luminance domain +test: exact readability regression, 71-case operations corpus, and broad seeded fuzz harness +expecting: exact parity with no tolerance or output rounding +next_action: record and validate the required 60-second seed-20260801 fuzz log ## Symptoms @@ -55,7 +55,7 @@ started: uncovered after the fuzz saturation root-cause fix. ## Resolution -root_cause: -fix: -verification: -files_changed: [] +root_cause: V8 Math.pow and Go math.Pow produce different final float64 bits for 93 of TinyColor's 245 nonlinear 8-bit channel values. +fix: Color.Luminance now uses a finite V8-derived 256-entry transfer table. The follow-on palette divergences were fixed at their shared constructor boundary by applying TinyColor's sub-one channel rounding and bound01 normalization. +verification: The exact regression passed; all 71 operations cases matched; the full local verify equivalent passed; seed 1 ran for 1.008 seconds across 14,778 cases with zero divergences. +files_changed: [src/tinycolor/luminance.go, src/tinycolor/color.go, src/tinycolor/operations_test.go, src/cmd/pow-debug/main.go] diff --git a/DECISIONS.md b/DECISIONS.md index acb5813d..96b472d3 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -8,4 +8,5 @@ | D-004 | Use JSON Lines adapters over stdin/stdout. | Node and Go can compare dynamic inputs and operations without modifying source tests. | Accepted | | D-005 | Use Go standard library first. | The port's algorithms are small and self-contained; dependencies add compatibility and supply-chain surface. | Accepted | | D-006 | Preserve source quirks at the adapter boundary; expose an idiomatic Go API separately. | Compatibility and Go usability are both required, but they should not leak dynamic JavaScript semantics into every caller. | Accepted | -| D-007 | Deno's source suite is a required later check, not a current pass. | `deno` is not installed in the checked environment as of 2026-08-01. | Accepted | +| D-007 | Run the immutable Deno source suite explicitly as `deno test test.js`. | Deno 2 broad discovery includes generated npm and Node port tests outside the pinned source suite. | Accepted | +| D-008 | Use a finite V8-derived luminance transfer table. | TinyColor rounds RGB to 8-bit channels before luminance, while Go `math.Pow` differs from V8 for 93 nonlinear values; the table preserves exact parity without tolerance. | Accepted | diff --git a/src/cmd/pow-debug/main.go b/src/cmd/pow-debug/main.go deleted file mode 100644 index 6909c8f4..00000000 --- a/src/cmd/pow-debug/main.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "fmt" - "math" -) - -func main() { - for channel := 11; channel < 256; channel++ { - normalized := float64(channel) / 255 - fmt.Printf("%d %x\n", channel, math.Float64bits(math.Pow((normalized+.055)/1.055, 2.4))) - } -} diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 2728581a..4c7a8e5a 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -33,10 +33,22 @@ type WCAG2Options struct { } func FromCompat(input any, fromRatio bool) (Color, error) { + var model color.Model if fromRatio { - return Color{model: parser.ParseFromRatio(input)}, nil + model = parser.ParseFromRatio(input) + } else { + model = parser.Parse(input) + } + if model.R < 1 { + model.R = math.Floor(model.R + .5) + } + if model.G < 1 { + model.G = math.Floor(model.G + .5) } - return Color{model: parser.Parse(input)}, nil + if model.B < 1 { + model.B = math.Floor(model.B + .5) + } + return Color{model: model}, nil } func FromCompatWithOptions(input any, fromRatio bool, options CompatOptions) (Color, error) { c, e := FromCompat(input, fromRatio) @@ -170,14 +182,7 @@ func (c Color) ToFilter(second *Color, gradient bool) string { func (c Color) Brightness() float64 { x := c.ToRGB(); return float64(x.R*299+x.G*587+x.B*114) / 1000 } func (c Color) Luminance() float64 { x := c.ToRGB() - f := func(v int) float64 { - z := float64(v) / 255 - if z <= .03928 { - return z / 12.92 - } - return math.Pow((z+.055)/1.055, 2.4) - } - return .2126*f(x.R) + .7152*f(x.G) + .0722*f(x.B) + return .2126*luminanceChannel(x.R) + .7152*luminanceChannel(x.G) + .0722*luminanceChannel(x.B) } func Readability(first, second Color) float64 { firstLuminance, secondLuminance := first.Luminance(), second.Luminance() @@ -303,7 +308,7 @@ func roundedAlpha(alpha float64) string { } func rgbToHSL(r, g, b float64) (float64, float64, float64) { - r, g, b = r/255, g/255, b/255 + r, g, b = color.Bound01(r, 255), color.Bound01(g, 255), color.Bound01(b, 255) max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) l := (max + min) / 2 if max == min { @@ -327,7 +332,7 @@ func rgbToHSL(r, g, b float64) (float64, float64, float64) { } func rgbToHSV(r, g, b float64) (float64, float64, float64) { - r, g, b = r/255, g/255, b/255 + r, g, b = color.Bound01(r, 255), color.Bound01(g, 255), color.Bound01(b, 255) max, min := math.Max(r, math.Max(g, b)), math.Min(r, math.Min(g, b)) d := max - min if max == 0 { diff --git a/src/tinycolor/luminance.go b/src/tinycolor/luminance.go new file mode 100644 index 00000000..5dfa5707 --- /dev/null +++ b/src/tinycolor/luminance.go @@ -0,0 +1,45 @@ +package tinycolor + +import "math" + +// TinyColor rounds channels to 8-bit values before applying Math.pow. These +// V8-derived results preserve exact JavaScript behavior where Go math.Pow +// rounds differently. +var luminanceBits = [...]uint64{ + 0x0000000000000000, 0x3f33e45677c176f7, 0x3f43e45677c176f7, 0x3f4dd681b3a23272, 0x3f53e45677c176f7, 0x3f58dd6c15b1d4b4, 0x3f5dd681b3a23272, 0x3f6167cba8c94818, + 0x3f63e45677c176f7, 0x3f6660e146b9a5d5, 0x3f68dd6c15b1d4b4, 0x3f6b6a31b5259c99, 0x3f6e1e31d70c99dd, 0x3f707c38bf8583a9, 0x3f71fcc2beed6421, 0x3f7390ffaf95e279, + 0x3f753936cc7bc928, 0x3f76f5addb50c915, 0x3f78c6a94031b561, 0x3f7aac6c0fb97351, 0x3f7ca7381f9f602b, 0x3f7eb74e160978d0, 0x3f806e76bbda92b8, 0x3f818c2a5a8a8044, + 0x3f82b4e09b3f0ae3, 0x3f83e8b7b3bde965, 0x3f8527cd60af8b85, 0x3f86723eea8d3709, 0x3f87c8292a3db6b3, 0x3f8929a88d67b521, 0x3f8a96d91a8016bd, 0x3f8c0fd67499fab6, + 0x3f8d94bbdefd740e, 0x3f8f25a44089883f, 0x3f9061551372c694, 0x3f9135f3e4c2cce2, 0x3f9210bb8642b172, 0x3f92f1b8c1ae46bd, 0x3f93d8f839b79c0b, 0x3f94c6866b3e9fa4, + 0x3f95ba6fae794313, 0x3f96b4c0380d2def, 0x3f97b5841a1bf3ac, 0x3f98bcc74542addb, 0x3f99ca95898dc8b5, 0x3f9adefa9761c020, 0x3f9bfa0200597bd9, 0x3f9d1bb7381aec1f, + 0x3f9e442595227bca, 0x3f9f73585185e1b5, 0x3fa054ad45d76878, 0x3fa0f31ba386ff26, 0x3fa194fcb663747b, 0x3fa23a55e62a662a, 0x3fa2e32c8e148d11, 0x3fa38f85fd21eacf, + 0x3fa43f67766310ff, 0x3fa4f2d6313fa8d0, 0x3fa5a9d759ba5ed0, 0x3fa6647010b254ee, 0x3fa722a56c2239ee, 0x3fa7e47c775d2427, 0x3fa8a9fa33494b07, 0x3fa973239698b9cc, + 0x3faa3ffd8e001389, 0x3fab108cfc6b7fbc, 0x3fabe4d6bb31d522, 0x3facbcdf9a4616f2, 0x3fad98ac60675833, 0x3fae7841cb4f16df, 0x3faf5ba48fde2048, 0x3fb0216cad240765, + 0x3fb096f2671eb815, 0x3fb10e65c38a5192, 0x3fb187c90bf8bce2, 0x3fb2031e85f5d6da, 0x3fb28068731a1952, 0x3fb2ffa9111cb94b, 0x3fb380e299e53f92, 0x3fb40417439ca10f, + 0x3fb4894940bddbfb, 0x3fb5107ac0261e59, 0x3fb599aded247aac, 0x3fb624e4ef892ed4, 0x3fb6b221ebb4817e, 0x3fb7416702a539d1, 0x3fb7d2b65206b527, 0x3fb86611f43e9e6a, + 0x3fb8fb7c007a4a70, 0x3fb992f68abbbc89, 0x3fba2c83a3e6566d, 0x3fbac82559cb3644, 0x3fbb65ddb7354604, 0x3fbc05aec3f4fe5e, 0x3fbca79a84ebe030, 0x3fbd4ba2fc17a6a5, + 0x3fbdf1ca289d34b8, 0x3fbe9a1206d34003, 0x3fbf447c904cbb4e, 0x3fbff10bbbe302c2, 0x3fc04fe0bedfe5f1, 0x3fc0a84fe3b36d8f, 0x3fc101d443dfc06f, 0x3fc15c6ed58eefdf, + 0x3fc1b8208da5fef0, 0x3fc214ea5fc9514a, 0x3fc272cd3e610123, 0x3fc2d1ca1a9d1cfb, 0x3fc331e1e479cdf5, 0x3fc393158ac3674e, 0x3fc3f565fb1a5fd5, 0x3fc458d421f735df, + 0x3fc4bd60eaae3e73, 0x3fc5230d3f736034, 0x3fc589da095dbaa1, 0x3fc5f1c8306b3a3c, 0x3fc65ad89b841a2b, 0x3fc6c50c307e53bf, 0x3fc73063d420fc80, 0x3fc79ce06a279303, + 0x3fc80a82d5453b5d, 0x3fc8794bf727eb3f, 0x3fc8e93cb07b8679, 0x3fc95a55e0ecec0b, 0x3fc9cc98672cf47e, 0x3fca400520f3619c, 0x3fcab49ceb01c003, 0x3fcb2a60a1263b0a, + 0x3fcba1511e3e632d, 0x3fcc196f3c39e76f, 0x3fcc92bbd41d41fe, 0x3fcd0d37be045851, 0x3fcd88e3d1250f68, 0x3fce05c0e3d1d3e0, 0x3fce83cfcb7c16f0, 0x3fcf03115cb6bfd3, + 0x3fcf83866b38924d, 0x3fd00297e4ef4553, 0x3fd044072557177a, 0x3fd086115f6beb3a, 0x3fd0c8b6fb5c735e, 0x3fd10bf860ef039a, 0x3fd14fd5f782a5a6, 0x3fd1945026102997, + 0x3fd1d967532b31b1, 0x3fd21f1be50339e7, 0x3fd2656e41649ae3, 0x3fd2ac5ecdb988f8, 0x3fd2f3edef0b0ed8, 0x3fd33c1c0a020438, 0x3fd384e982e800b1, 0x3fd3ce56bda84a81, + 0x3fd418641dd0c1bc, 0x3fd463120692c7af, 0x3fd4ae60dac4229d, 0x3fd4fa50fcdfde15, 0x3fd546e2cf0727a9, 0x3fd59416b3022858, 0x3fd5e1ed0a40daab, 0x3fd6306635dbdd7b, + 0x3fd67f82969543a2, 0x3fd6cf428cd96079, 0x3fd71fa678bf915d, 0x3fd770aeba0b042a, 0x3fd7c25bb02b7ac5, 0x3fd814adba3e0bd9, 0x3fd867a5370de0b1, 0x3fd8bb428514f067, + 0x3fd90f86027cb84e, 0x3fd964700d1ef1b1, 0x3fd9ba0102864521, 0x3fda10393feefafd, 0x3fda67192247a9be, 0x3fdabea10631e195, 0x3fdb16d14802d5ca, 0x3fdb6faa43c403bb, + 0x3fdbc92c5533d785, 0x3fdc2357d7c64e5d, 0x3fdc7e2d26a596de, 0x3fdcd9ac9cb2aef2, 0x3fdd35d69485ffc5, 0x3fdd92ab686ff782, 0x3fddf02b7279a10d, 0x3fde4e570c6539c5, + 0x3fdead2e8faec526, 0x3fdf0cb2558c9ea4, 0x3fdf6ce2b6f00983, 0x3fdfcdc00c85bec2, 0x3fe017a5575b3cb2, 0x3fe048c17ad3c04b, 0x3fe07a349c9d9837, 0x3fe0abfee888c050, + 0x3fe0de208a4444c8, 0x3fe11099ad5e83eb, 0x3fe1436a7d456eef, 0x3fe176932546ca12, 0x3fe1aa13d0906bda, 0x3fe1ddecaa307b85, 0x3fe2121ddd15aece, 0x3fe246a7940f86d1, + 0x3fe27b89f9ce8c4b, 0x3fe2b0c538e48b07, 0x3fe2e6597bc4cca0, 0x3fe31c46ecc4528d, 0x3fe3528db61a0f73, 0x3fe3892e01df1fcc, 0x3fe3c027fa0f01eb, 0x3fe3f77bc887cd3b, + 0x3fe42f29970a68f8, 0x3fe467318f3ac22d, 0x3fe49f93daa00113, 0x3fe4d850a2a4bde1, 0x3fe51168109734e5, 0x3fe54ada4da97a1b, 0x3fe584a782f1ac23, 0x3fe5becfd96a2698, + 0x3fe5f95379f1b3ed, 0x3fe634328d4bbe97, 0x3fe66f6d3c2081cf, 0x3fe6ab03aefd39aa, 0x3fe6e6f60e5452b1, 0x3fe72344827d98f6, 0x3fe75fef33b6669b, 0x3fe79cf64a21d1e2, + 0x3fe7da59edc8dab0, 0x3fe8181a469a9787, 0x3fe856377c6c6224, 0x3fe894b1b6fa0377, 0x3fe8d3891de5df49, 0x3fe912bdd8b91f45, 0x3fe952500ee3dda5, 0x3fe9923fe7bd4f67, + 0x3fe9d28d8a83edfc, 0x3fea13391e5da09f, 0x3fea5442ca57e52e, 0x3fea95aab567f88f, 0x3fead771066afec2, 0x3feb1995e4262a69, 0x3feb5c197546e3f8, 0x3feb9efbe062f086, + 0x3febe23d4bf8981b, 0x3fec25ddde6ecbbb, 0x3fec69ddbe154af1, 0x3fecae3d1124c90b, 0x3fecf2fbfdbf11f1, 0x3fed381aa9ef2e82, 0x3fed7d993ba988d4, 0x3fedc377d8cc0fd5, + 0x3fee09b6a71e5aa6, 0x3fee5055cc51cbb4, 0x3fee97556e01b351, 0x3feedeb5b1b37216, 0x3fef2676bcd69ade, 0x3fef6e98b4c51466, 0x3fefb71bbec33ab2, 0x3ff0000000000000, +} + +func luminanceChannel(channel int) float64 { + return math.Float64frombits(luminanceBits[channel]) +} diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index a4a40f31..24c20323 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -147,12 +147,6 @@ func TestReadability(t *testing.T) { func TestReadabilityMatchesJavaScriptPrecision(t *testing.T) { first, _ := FromCompat(map[string]any{"r": 127.0, "g": 64.0, "b": 15.0, "a": 0.0}, false) second, _ := FromCompat("#80007f", false) - for _, channel := range []float64{128, 127} { - normalized := channel / 255 - base := (normalized + .055) / 1.055 - linear := math.Pow(base, 2.4) - t.Logf("channel %.0f normalized %.17g pow %.17g/%x exp-log %.17g/%x exp2-log2 %.17g/%x", channel, normalized, linear, math.Float64bits(linear), math.Exp(2.4*math.Log(base)), math.Float64bits(math.Exp(2.4*math.Log(base))), math.Exp2(2.4*math.Log2(base)), math.Float64bits(math.Exp2(2.4*math.Log2(base)))) - } if got := Readability(first, second); got != 1.188086751976723 { t.Fatalf("Readability precision = %.17g", got) } @@ -284,6 +278,14 @@ func TestPaletteCustomizationsAndIndependence(t *testing.T) { } } +func TestPaletteConversionUsesTinyColorChannelBounds(t *testing.T) { + color, _ := FromCompat("rgb(127, 0, 255)", false) + converted := color.Tetrad()[1] + if got := converted.ToHSLString(); got != "hsl(0, 100%, 50%)" { + t.Fatalf("tetrad wrapped hue = %s", got) + } +} + func paletteHex(colors []Color) string { values := make([]string, len(colors)) for index, color := range colors { From a87186017a944d90498280018adc0b1b5d960ddd Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:50:19 +0530 Subject: [PATCH 140/165] test(05-02): record 60-second fuzz session --- Makefile | 2 ++ fuzz/README.md | 6 ++++++ fuzz/log.txt | 4 ++++ fuzz/validate-log.mjs | 38 ++++++++++++++++++++++++++++++++++++ fuzz/validate-log.test.mjs | 40 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+) create mode 100644 fuzz/log.txt create mode 100644 fuzz/validate-log.mjs create mode 100644 fuzz/validate-log.test.mjs diff --git a/Makefile b/Makefile index f76edc82..2bc4395f 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ build: test: node tests/port/adapter.test.mjs node --test tests/original/verify.test.mjs + node --test fuzz/harness.test.mjs fuzz/validate-log.test.mjs + node fuzz/validate-log.mjs fuzz/log.txt go -C src test ./... node compat/run.mjs compat/cases/smoke.jsonl node compat/run.mjs compat/cases/parser-hex-rgb.jsonl diff --git a/fuzz/README.md b/fuzz/README.md index db962269..45dd690a 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -15,6 +15,12 @@ Full evidence run: node fuzz/harness.mjs --duration 60 --seed 20260801 ``` +Validate the checked-in evidence: + +```text +node fuzz/validate-log.mjs fuzz/log.txt +``` + Rerun a failure with the logged seed and duration. Each divergence is a JSON object containing the request and both responses; the four final lines report duration, seed, case count, and divergence count. diff --git a/fuzz/log.txt b/fuzz/log.txt new file mode 100644 index 00000000..d0f6498e --- /dev/null +++ b/fuzz/log.txt @@ -0,0 +1,4 @@ +duration_seconds: 60.012 +seed: 20260801 +cases: 1091630 +divergences: 0 diff --git a/fuzz/validate-log.mjs b/fuzz/validate-log.mjs new file mode 100644 index 00000000..7afe12d6 --- /dev/null +++ b/fuzz/validate-log.mjs @@ -0,0 +1,38 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +export function validateLog(text) { + const read = (name) => { + const matches = [...text.matchAll(new RegExp(`^${name}: (.+)$`, "gm"))]; + if (matches.length !== 1) throw new Error(`${name} must appear exactly once`); + const value = Number(matches[0][1]); + if (!Number.isFinite(value)) throw new Error(`${name} must be numeric`); + return value; + }; + const result = { + duration_seconds: read("duration_seconds"), + seed: read("seed"), + cases: read("cases"), + divergences: read("divergences"), + }; + if (result.duration_seconds < 60) throw new Error("duration_seconds must be at least 60"); + if (!Number.isSafeInteger(result.seed) || result.seed !== 20260801) throw new Error("seed must be 20260801"); + if (!Number.isSafeInteger(result.cases) || result.cases <= 0) throw new Error("cases must be a positive integer"); + if (!Number.isSafeInteger(result.divergences) || result.divergences !== 0) throw new Error("divergences must be zero"); + return result; +} + +function main() { + try { + const path = process.argv[2]; + if (!path) throw new Error("usage: node fuzz/validate-log.mjs "); + validateLog(readFileSync(path, "utf8")); + console.log("fuzz log verified"); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main(); diff --git a/fuzz/validate-log.test.mjs b/fuzz/validate-log.test.mjs new file mode 100644 index 00000000..6e9afa35 --- /dev/null +++ b/fuzz/validate-log.test.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { validateLog } from "./validate-log.mjs"; + +const valid = `duration_seconds: 60.001 +seed: 20260801 +cases: 1 +divergences: 0 +`; + +test("accepts a complete zero-divergence 60-second log", () => { + assert.deepEqual(validateLog(valid), { + duration_seconds: 60.001, + seed: 20260801, + cases: 1, + divergences: 0, + }); +}); + +test("rejects invalid evidence fields", () => { + for (const [name, value] of [ + ["duration_seconds", "59.999"], + ["duration_seconds", "nope"], + ["seed", "1"], + ["seed", "1.5"], + ["cases", "0"], + ["cases", "1.5"], + ["divergences", "1"], + ["divergences", "nope"], + ]) { + assert.throws(() => validateLog(valid.replace(new RegExp(`^${name}: .+$`, "m"), `${name}: ${value}`)), name); + } +}); + +test("rejects every missing evidence field", () => { + for (const name of ["duration_seconds", "seed", "cases", "divergences"]) { + assert.throws(() => validateLog(valid.replace(new RegExp(`^${name}: .+\\n`, "m"), "")), name); + } +}); From 28156099c7d55e9cff10b0fb1cf21ec65c9e083c Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 05:52:09 +0530 Subject: [PATCH 141/165] wip: delivery evidence paused at task 3/3 --- .planning/HANDOFF.json | 98 ++++------- .planning/STATE.md | 52 +++--- .../05-delivery-evidence/.continue-here.md | 154 +++--------------- 3 files changed, 92 insertions(+), 212 deletions(-) diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json index 93b58754..2babbf40 100644 --- a/.planning/HANDOFF.json +++ b/.planning/HANDOFF.json @@ -1,111 +1,85 @@ { "version": "1.0", - "timestamp": "2026-08-01T00:04:30.289Z", + "timestamp": "2026-08-01T00:20:50.023Z", "phase": "5", "phase_name": "Delivery Evidence", "phase_dir": ".planning/phases/05-delivery-evidence", "plan": 2, - "task": 1, + "task": 3, "total_tasks": 3, "status": "paused", "completed_tasks": [ { - "id": "05-01-01", - "name": "Human CLI", - "status": "done", - "commit": "df1f9b8" - }, - { - "id": "05-01-02", - "name": "Reproducible verification gate", + "id": "05-02-01", + "name": "Deterministic persistent-process differential fuzz harness", "status": "done", - "commit": "a4ad85e" + "commit": "6b4506a, 0e54f1a" }, { - "id": "05-01-03", - "name": "Port CI and source suite", + "id": "05-02-02", + "name": "Validated 60-second fuzz survivor evidence", "status": "done", - "commit": "d8ba485" - }, - { - "id": "05-02-01", - "name": "Differential fuzz harness", - "status": "in_progress", - "progress": "Persistent seeded harness is implemented but deliberately uncommitted/blocked by an exact readability precision mismatch. First saturation mismatch was fixed in 0a9db8b." + "commit": "ad69b3e" } ], "remaining_tasks": [ { - "id": "05-02-01", - "name": "Finish broad fuzz harness after readability parity fix", - "status": "in_progress" + "id": "05-02-03", + "name": "Measure shared-workload p99, RSS, startup, and throughput", + "status": "not_started" }, { - "id": "05-02-02", - "name": "Validate and record real 60-second fuzz log", + "id": "05-03-01", + "name": "Write judge-facing port documentation and decision log", "status": "not_started" }, { - "id": "05-02-03", - "name": "Generate shared-workload benchmark evidence", + "id": "05-03-02", + "name": "Refresh honest parity, coverage, safety, benchmark, and bonus evidence", "status": "not_started" }, { - "id": "05-03", - "name": "Submission documentation, evidence refresh, external checkpoint, and phase closeout", + "id": "05-03-03", + "name": "Run final clean local gate and write verification evidence", "status": "not_started" - } - ], - "blockers": [ + }, { - "description": "Exact readability mismatch: JavaScript 1.188086751976723 versus Go 1.1880867519767233 for {r:127,g:64,b:15,a:0} against #80007f.", - "type": "technical", - "workaround": "None allowed. Do not add tolerance or narrow fuzz inputs. Continue .planning/debug/fuzz-readability-precision.md; root cause is V8 Math.pow versus Go math.Pow final rounding. An exact bounded 256-entry transfer lookup is the leading safe fix." + "id": "05-03-04", + "name": "Verify public clone access and successful CI", + "status": "not_started" }, { - "description": "The WIP branch intentionally contains a failing exact readability regression and fuzz zero-divergence test.", - "type": "technical", - "workaround": "Fix the shared Luminance transfer function, run the exact regression and operations corpus, then rerun the broad seed-1 fuzz smoke before committing production completion." + "id": "05-03-05", + "name": "Close Phase 5 planning state after external proof", + "status": "not_started" } ], + "blockers": [], "human_actions_pending": [ { - "action": "Record and publish the five-minute demo video, then add its URL.", - "context": "docs/DEMO.md is still a future Plan 05-03 deliverable; no video is claimed.", + "action": "Record and publish the five-minute demo video, then add its URL during Plan 05-03.", + "context": "The submission requires a live demonstration; no video is currently claimed.", "blocking": true } ], "decisions": [ { - "decision": "Keep broad bounded fuzz generation and stop on every exact mismatch.", - "rationale": "Narrowing to corpus-proven inputs would hide defects and violate behavioral-equivalence scoring.", + "decision": "Use a finite V8-derived 256-entry luminance transfer table.", + "rationale": "Go math.Pow differs from V8 for 93 nonlinear 8-bit values, and exact parity forbids tolerance.", "phase": "5" }, { - "decision": "Pin oracle checkout bytes to CRLF with .gitattributes instead of normalizing hashes.", - "rationale": "The kickoff manifest hashes raw CRLF working-tree bytes; CI must reproduce that byte contract.", + "decision": "Apply TinyColor's sub-one channel rounding at the FromCompat constructor boundary.", + "rationale": "One shared boundary fixes palette conversions without per-operation exceptions.", "phase": "5" }, { - "decision": "Run `deno test test.js` explicitly in CI.", - "rationale": "Deno 2 removed broad discovery exclusions and otherwise includes generated npm and Node port tests outside the immutable source suite.", - "phase": "5" - }, - { - "decision": "Prefer an exact finite-domain luminance transfer lookup if no smaller V8-compatible power implementation exists.", - "rationale": "TinyColor rounds luminance channels to 8-bit integers; Go math.Pow differs from V8 for 93 of 245 nonlinear channels and heuristic ULP correction is incorrect.", + "decision": "Keep the fuzz generator broad and compare parsed responses exactly.", + "rationale": "The broad seed exposed both arithmetic and constructor parity defects that a narrowed corpus would hide.", "phase": "5" } ], - "uncommitted_files": [ - "compat/cases/operations.jsonl", - "fuzz/README.md", - "fuzz/harness.mjs", - "fuzz/harness.test.mjs", - "src/tinycolor/operations_test.go", - "src/cmd/pow-debug/main.go", - ".planning/debug/fuzz-readability-precision.md" - ], - "next_action": "Run `$gsd-resume-work`, read `.planning/debug/fuzz-readability-precision.md`, finish the exact luminance transfer fix with the existing RED tests, then run `node fuzz/harness.mjs --duration 1 --seed 1` without narrowing the generator.", - "context_notes": "Branch rajeet. GitHub Actions run 30673847990 passed make verify, deno test test.js, build, and artifact upload. Local Deno 2.9.4 ran the immutable suite: 45 passed, 0 failed, 1 ignored. Current broad fuzz smoke is expected red: 12,291 cases and 388 divergences before the readability fix." + "uncommitted_files": [], + "next_action": "Run `$gsd-resume-work`, then execute task 05-02-03 from `.planning/phases/05-delivery-evidence/05-02-PLAN.md`: build the shared benchmark workload and runner with tests first, write real results once, and create 05-02-SUMMARY.md only after benchmark verification passes.", + "context_notes": "Branch rajeet. Exact fuzz parity is resolved. The real checked-in log reports 60.012 seconds, seed 20260801, 1,091,630 cases, and zero divergences. Local verification passed all Go and Node gates, all five corpora (9/9, 26/26, 23/23, 35/35, 71/71), vet, fuzz tests, log validation, and the immutable Deno suite (45 passed, 0 failed, 1 ignored). Local Windows has no make, so Makefile commands were run directly; GitHub Actions must confirm `make verify` after push. No background process is running." } diff --git a/.planning/STATE.md b/.planning/STATE.md index c57e9539..64398ab1 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,23 +2,35 @@ **Updated:** 2026-08-01 -- Planning initialized from the local `bgrins/TinyColor` checkout. -- JavaScript source and tests are the immutable oracle. -- Node 24.18.0 and Go 1.26.1 are available locally. -- `deno` is not installed; no original-source test pass is claimed. -- Phase 1 completed in commit `78423e3`: Go/Node JSONL runners, protocol tests, - and a fixed corpus passed 9/9 with zero mismatches. This is not whole-library - parity. -- Phase 2 completed locally: the normalized parser covers HEX, RGB(A), HSL(A), - HSV(A), names, objects, invalid input, and FromRatio. Differential evidence: - 26/26 HEX/RGB/name cases, 23/23 complete parser cases, and 9/9 smoke cases. -- The Go module lives in `src/`; `tests/original/manifest.sha256` pins the - unmodified root JavaScript oracle. -- Phase 3 completed locally: conversion/representation and analysis behavior - passed a 35/35 exact corpus; all Phase 1–3 corpora are zero-mismatch. The - compatibility protocol now preserves successful JSON `false` results. -- Phase 4 completed locally: modifiers, Mix, WCAG readability, and palette - operations passed the full gate. Exact differential evidence is 9/9 smoke, - 26/26 HEX/RGB, 23/23 parser, 35/35 conversion, and 58/58 operations with - zero mismatches. `deno` remains unavailable, so no original-source suite - pass is claimed. +## Current Position + +- Phase 5 of 5: Delivery Evidence +- Plan 05-02, Task 3 of 3: shared-workload benchmark not started +- Plans 1–4 are complete; Phase 5 Plan 05-01 and fuzz Tasks 05-02-01/02 are complete. + +## Verified Evidence + +- Immutable JavaScript oracle hashes: 3/3 verified. +- Exact corpora: smoke 9/9, HEX/RGB 26/26, parser 23/23, conversion 35/35, operations 71/71. +- Go tests and vet pass; Node adapter, verifier, harness, and log-validator tests pass. +- Immutable Deno source suite: 45 passed, 0 failed, 1 ignored. +- Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. + +## Recent Decisions + +- Use the pinned V8-derived 8-bit luminance transfer table for exact `Math.pow` parity. +- Apply TinyColor's sub-one RGB rounding once at the `FromCompat` constructor boundary. +- Keep fuzz inputs broad and comparisons exact; never replace divergences with tolerance. + +## Remaining + +- Implement honest same-host startup p99, latency p99, throughput, and peak RSS benchmarks. +- Complete Plan 05-03 judge-facing documentation and final evidence refresh. +- Verify the pushed GitHub Actions run and public clone workflow. +- Human action: record and publish the five-minute demo video. + +## Session Continuity + +Last session: 2026-08-01T00:20:50.023Z +Stopped at: Plan 05-02 Task 3, ready to implement benchmarks with TDD. +Resume file: `.planning/phases/05-delivery-evidence/.continue-here.md` diff --git a/.planning/phases/05-delivery-evidence/.continue-here.md b/.planning/phases/05-delivery-evidence/.continue-here.md index 650ca199..5bcc38c8 100644 --- a/.planning/phases/05-delivery-evidence/.continue-here.md +++ b/.planning/phases/05-delivery-evidence/.continue-here.md @@ -1,158 +1,52 @@ --- phase: 05-delivery-evidence -plan: 2 -task: 1 +task: 3 total_tasks: 3 status: in_progress -last_updated: 2026-08-01T00:04:30.289Z -branch: rajeet +last_updated: 2026-08-01T00:20:50.023Z --- - -Phase 5 Plan 05-01 is implemented and CI-verified. Plan 05-02 Task 1 is paused -mid-debug: the broad persistent differential harness is implemented, but its -zero-divergence test correctly remains RED because fuzzing found exact port -defects. Do not narrow the generator or add comparison tolerance. - -The branch is intentionally being handed off with WIP files. The first fuzz -defect (`saturate(-100)` rounding) is fixed and committed. The next defect is -an exact WCAG luminance/readability precision mismatch under active diagnosis. - +Plan 05-02 is at Task 3/3. Differential fuzzing is finished and verified; the next task is the shared original-versus-port benchmark. Branch `rajeet` contains the exact parity fix in `0e54f1a` and the validated evidence in `ad69b3e`. -- Phase 5 design, context, research, validation, and three execution plans were - written and independently plan-checked. -- Human CLI commands `parse`, `convert`, `lighten`, `palette`, and `contrast` - were implemented with JSON output while preserving zero-argument JSONL mode. -- Added one-command Make build/verify, oracle hash verification, scratch Docker - artifact, and GitHub Actions delivery workflow. -- Fixed CI raw-byte hash failures by pinning the three oracle files to CRLF in - `.gitattributes`; a simulated `core.autocrlf=false` checkout prints - `verified: 3`. -- Fixed Deno 2 discovery by running the immutable suite explicitly with - `deno test test.js`. -- GitHub Actions run - `https://github.com/rajeet-04/TinyColor/actions/runs/30673847990` passed in - 42 seconds: `make verify`, Deno source suite, build, and artifact upload. -- Local Deno 2.9.4 source suite result: 45 passed, 0 failed, 1 ignored. -- Built the standard-library persistent fuzz harness with seeded broad inputs, - one outstanding request per child, protocol safeguards, and RED/GREEN tests. -- Fixed the first fuzz mismatch in `0a9db8b`: TinyColor HSL percentage - truncation for `saturate(-100)` now matches; operations corpus is 70/70 at - that commit. - +- Task 05-02-01: Persistent seeded differential harness — done. +- Exact readability mismatch fixed with a V8-derived 256-entry transfer table. +- Follow-on palette mismatches fixed at the shared TinyColor constructor boundary. +- One-second seed-1 smoke: 14,778 cases, zero divergences. +- Task 05-02-02: Real 60-second evidence — done. +- `fuzz/log.txt`: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. +- `make verify` now runs harness tests and validates the checked-in fuzz log. +- Local direct verification passed all Node/Go gates and corpora; Deno passed 45, failed 0, ignored 1. -1. Finish Task 05-02-01: - - Continue `.planning/debug/fuzz-readability-precision.md`. - - Existing RED case: `{r:127,g:64,b:15,a:0}` vs `#80007f` returns - JavaScript `1.188086751976723`, Go `1.1880867519767233`. - - Root cause is confirmed inside nonlinear luminance channel expansion: - V8 `Math.pow` and Go `math.Pow` differ in final bits for 93 of the 245 - nonlinear 8-bit channels; two differ by two ULPs. - - `src/tinycolor/operations_test.go` and - `compat/cases/operations.jsonl` contain the failing exact regression. - - `src/cmd/pow-debug/main.go` is a temporary diagnostic that prints Go - channel power bits. - - Leading safe fix: a reproducibly generated exact 256-entry V8 transfer - lookup used by `Color.Luminance`. Do not use `Nextafter`, decimal output - rounding, or tolerance. - - After the exact regression and 71-row operations corpus pass, rerun - `node --test fuzz/harness.test.mjs` and - `node fuzz/harness.mjs --duration 1 --seed 1` broadly. If another - mismatch appears, repeat deterministic regression -> root cause -> micro - commit. -2. Task 05-02-02: add tested fuzz-log validator and record an actual - `--duration 60 --seed 20260801` zero-divergence run. -3. Task 05-02-03: implement shared workload benchmark runner and publish actual - p99 latency, peak RSS, startup, and throughput results without quick-run - overwrite. -4. Plan 05-03: rewrite judge-facing README, expand DECISIONS to 12+ substantive - rows, refresh COMPATIBILITY with exact evidence/coverage/unsafe counts, add - demo script, verify public clone/current CI, and close roadmap/state. -5. Human: record and publish the five-minute demo video; no video is currently - claimed. - +- Task 05-02-03: Implement and run the benchmark specified in `05-02-PLAN.md`. +- After Task 3 passes, create `05-02-SUMMARY.md`. +- Execute Plan 05-03 documentation, honest evidence refresh, final external CI/public-clone proof, and closeout. +- Human: record and publish the required five-minute demo video. -- Broad fuzzing stays broad. A mismatch blocks the bonus and becomes a fixed - regression; inputs are never constrained to make the log green. -- Oracle hashing remains byte-exact. `.gitattributes` reproduces kickoff CRLF - bytes on Linux rather than weakening verification with newline normalization. -- CI runs `deno test test.js`, not broad `deno task test`, because Deno 2 would - discover generated npm and port-owned Node tests. -- Luminance parity needs an exact finite-domain solution. Heuristic ULP - correction is ruled out because channel deltas have both signs. -- Keep micro-commits: regression/root-cause fixes, evidence artifacts, and - debug/archive docs stay separately revertible. - +- Use exact V8-derived luminance bits because heuristic ULP correction is wrong for the finite channel domain. +- Preserve TinyColor's constructor channel rounding in `FromCompat`, where every operation routes through it. +- Keep fuzz comparisons exact and the generator broad; do not add tolerance or remove failing input families. -- Technical blocker: exact V8/Go power rounding mismatch in `Color.Luminance`. -- Expected red checks until fixed: - - `go test ./tinycolor -run TestReadabilityMatchesJavaScriptPrecision` - - operations corpus has the new readability case as its mismatch - - `node --test fuzz/harness.test.mjs` fails only its zero-divergence test - - seed-1 one-second broad run previously produced 12,291 cases / 388 - divergences; first was the readability case above. - +- No technical blocker for benchmarking. +- The demo video remains a human submission action for Plan 05-03. - - -- `fuzz/harness.mjs` — persistent broad differential harness. -- `fuzz/harness.test.mjs` — PRNG/options/generator/end-to-end tests. -- `fuzz/README.md` — reproduction and claim rules. -- `src/tinycolor/operations_test.go` — exact readability RED regression and - temporary numeric logging. -- `compat/cases/operations.jsonl` — adapter RED regression row. -- `.planning/debug/fuzz-readability-precision.md` — active root-cause evidence. -- `src/cmd/pow-debug/main.go` — temporary channel-bit diagnostic. - -These files are preserved in the WIP handoff commit. The temporary logging and -`pow-debug` command may be removed only after the exact solution is verified; -do not delete them before the next agent reads the evidence. - - - - - -- GitHub Actions: PASS — run 30673847990. -- Local Deno: PASS — 45 passed, 0 failed, 1 ignored. -- Oracle hash verifier: PASS — 3 files. -- Fixed corpus through saturation commit: operations 70/70. -- Current WIP: intentionally RED only for unresolved readability precision and - fuzz zero-divergence. -- No background server or watcher is running; all subagents were stopped. - - + +The previous Actions failure was caused by a deliberately committed RED regression. It is now green locally. Windows has no `make`, so all Makefile verification commands were executed directly; confirm the pushed Ubuntu Actions run before treating the external CI checkpoint as complete. No server or watcher is running. + - -Start with: - -```text -$gsd-resume-work -``` - -Then read this file and `.planning/debug/fuzz-readability-precision.md`. Finish -the exact luminance transfer fix with the existing RED tests before changing -the fuzz harness. The first mandatory verification after the fix is: - -```powershell -$env:GOCACHE = (Resolve-Path '.cache\go-build').Path -go -C src test ./tinycolor -run TestReadabilityMatchesJavaScriptPrecision -count=1 -node compat/run.mjs compat/cases/operations.jsonl -node fuzz/harness.mjs --duration 1 --seed 1 -``` - +Start with: run `$gsd-resume-work`, read Task 05-02-03 in `.planning/phases/05-delivery-evidence/05-02-PLAN.md`, then write the benchmark tests before implementing `bench/run.mjs`. Use temporary output for quick tests and write `bench/results.json` only once with the full measured run. From ebabb6711d2ffa95bc78e9e1db365fc25c6e893d Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 05:53:56 +0530 Subject: [PATCH 142/165] docs(05): record green handoff CI --- .planning/HANDOFF.json | 2 +- .planning/STATE.md | 1 + .planning/phases/05-delivery-evidence/.continue-here.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json index 2babbf40..551ed4d3 100644 --- a/.planning/HANDOFF.json +++ b/.planning/HANDOFF.json @@ -81,5 +81,5 @@ ], "uncommitted_files": [], "next_action": "Run `$gsd-resume-work`, then execute task 05-02-03 from `.planning/phases/05-delivery-evidence/05-02-PLAN.md`: build the shared benchmark workload and runner with tests first, write real results once, and create 05-02-SUMMARY.md only after benchmark verification passes.", - "context_notes": "Branch rajeet. Exact fuzz parity is resolved. The real checked-in log reports 60.012 seconds, seed 20260801, 1,091,630 cases, and zero divergences. Local verification passed all Go and Node gates, all five corpora (9/9, 26/26, 23/23, 35/35, 71/71), vet, fuzz tests, log validation, and the immutable Deno suite (45 passed, 0 failed, 1 ignored). Local Windows has no make, so Makefile commands were run directly; GitHub Actions must confirm `make verify` after push. No background process is running." + "context_notes": "Branch rajeet. Exact fuzz parity is resolved. The real checked-in log reports 60.012 seconds, seed 20260801, 1,091,630 cases, and zero divergences. Local verification passed all Go and Node gates, all five corpora (9/9, 26/26, 23/23, 35/35, 71/71), vet, fuzz tests, log validation, and the immutable Deno suite (45 passed, 0 failed, 1 ignored). GitHub Actions run 30675374085 passed make verify, Deno, build, and artifact upload in 43 seconds. No background process is running." } diff --git a/.planning/STATE.md b/.planning/STATE.md index 64398ab1..ee8e67d8 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -15,6 +15,7 @@ - Go tests and vet pass; Node adapter, verifier, harness, and log-validator tests pass. - Immutable Deno source suite: 45 passed, 0 failed, 1 ignored. - Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. +- GitHub Actions run 30675374085 passed verification, Deno, build, and artifact upload. ## Recent Decisions diff --git a/.planning/phases/05-delivery-evidence/.continue-here.md b/.planning/phases/05-delivery-evidence/.continue-here.md index 5bcc38c8..282aa63c 100644 --- a/.planning/phases/05-delivery-evidence/.continue-here.md +++ b/.planning/phases/05-delivery-evidence/.continue-here.md @@ -44,7 +44,7 @@ Plan 05-02 is at Task 3/3. Differential fuzzing is finished and verified; the ne -The previous Actions failure was caused by a deliberately committed RED regression. It is now green locally. Windows has no `make`, so all Makefile verification commands were executed directly; confirm the pushed Ubuntu Actions run before treating the external CI checkpoint as complete. No server or watcher is running. +The previous Actions failure was caused by a deliberately committed RED regression. GitHub Actions run `30675374085` passed `make verify`, Deno, build, and artifact upload in 43 seconds. Plan 05-03 still requires a final public-clone and CI proof after benchmarks and documentation. No server or watcher is running. From d57acc4bf040a3db04e91e63681d6bfa55640b90 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 06:02:39 +0530 Subject: [PATCH 143/165] chore: classify upstream JavaScript fixtures --- .gitattributes | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitattributes b/.gitattributes index 5731d7cd..f2b91f67 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,11 @@ mod.js text eol=crlf test.js text eol=crlf tinycolor.js text eol=crlf + +# Keep upstream oracle and published JavaScript copies in the repository for +# differential testing, but exclude them from the implementation language chart. +mod.js linguist-vendored +test.js linguist-vendored +tinycolor.js linguist-vendored +npm/** linguist-vendored +dist/** linguist-generated From 666264edc924f1828bea13829ad9f0d6c37750b1 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 11:25:53 +0530 Subject: [PATCH 144/165] perf(05-02): publish shared benchmark evidence --- .gitattributes | 1 + Makefile | 2 +- bench/methodology.md | 25 +++++- bench/results.json | 27 ++++++- bench/run.mjs | 179 +++++++++++++++++++++++++++++++++++++++++++ bench/run.test.mjs | 34 ++++++++ bench/workload.jsonl | 9 +++ 7 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 bench/run.mjs create mode 100644 bench/run.test.mjs create mode 100644 bench/workload.jsonl diff --git a/.gitattributes b/.gitattributes index f2b91f67..b82e657e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ mod.js text eol=crlf test.js text eol=crlf tinycolor.js text eol=crlf +bench/*.jsonl text eol=lf # Keep upstream oracle and published JavaScript copies in the repository for # differential testing, but exclude them from the implementation language chart. diff --git a/Makefile b/Makefile index 2bc4395f..d00bbc6c 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ fuzz: node fuzz/harness.mjs --duration 60 --seed 20260801 bench: - node bench/run.mjs + node bench/run.mjs --output bench/results.json clean: node -e "const fs = require('node:fs'); for (const file of ['bin/tinycolor', 'bin/tinycolor.exe']) fs.rmSync(file, { force: true });" diff --git a/bench/methodology.md b/bench/methodology.md index 29dd6d56..2bb44686 100644 --- a/bench/methodology.md +++ b/bench/methodology.md @@ -1,5 +1,24 @@ # Benchmark methodology -Measure the compiled compatibility CLI on a quiet host with the same corpus for -each run. Record median and p99 latency, RSS, startup time, and throughput. -Do not compare results across different hosts or Go versions. +Run `node bench/run.mjs --output bench/results.json` from the repository root. +It builds the Go compatibility CLI once, then sends the same fixed requests in +`bench/workload.jsonl` to the original JavaScript adapter and the Go adapter on +the same host. The workload covers parsing, conversion, mutation, mixing, +readability, and palettes. + +Normal mode takes 20 cold starts and 1,000 persistent requests per +implementation. Cold-start time is process launch through receipt of the first +JSONL response. Persistent latency is one sequential JSONL request/response; +throughput is all persistent requests divided by their total elapsed time. p99 +is the sorted sample at `ceil(0.99 * n) - 1`. + +Peak RSS is sampled after each cold-start response and ten times after the +persistent workload from `/proc//status` on Linux and `Get-Process ... +WorkingSet64` on Windows. Sampling is outside latency and throughput timing. +Platforms without either mechanism record `null` and an `rssLimitation` instead +of estimating a value. + +These are same-host observations, not universal speedup claims. Use +`node bench/run.mjs --quick --output ` only as a smoke check; +quick mode uses 3 cold starts and 30 requests and refuses to overwrite the +committed evidence file. diff --git a/bench/results.json b/bench/results.json index 42e6257d..a4113981 100644 --- a/bench/results.json +++ b/bench/results.json @@ -1,4 +1,27 @@ { - "status": "not_run", - "reason": "Benchmarking is scheduled for the delivery phase." + "generatedAt": "2026-08-01T05:53:35.885Z", + "os": "win32", + "architecture": "x64", + "cpu": "AMD Ryzen 9 8940HX with Radeon Graphics ", + "nodeVersion": "v24.18.0", + "goVersion": "go version go1.26.1 windows/amd64", + "workloadSha256": "3d91bf1f988e4a69173c8413d08ee6ba5b0d4179b9905778e4093da778206f8f", + "samples": { + "coldStarts": 20, + "requests": 1000 + }, + "implementations": { + "javascript": { + "startupP99Ms": 41.4131, + "latencyP99Ms": 0.2795, + "throughputOpsPerSecond": 7834.77401769562, + "peakRssBytes": 42868736 + }, + "go": { + "startupP99Ms": 11.0109, + "latencyP99Ms": 0.1808, + "throughputOpsPerSecond": 16756.790270337297, + "peakRssBytes": 12668928 + } + } } diff --git a/bench/run.mjs b/bench/run.mjs new file mode 100644 index 00000000..bcf8194b --- /dev/null +++ b/bench/run.mjs @@ -0,0 +1,179 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { execFileSync, spawn } from "node:child_process"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { cpus } from "node:os"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import readline from "node:readline"; + +const root = resolve(import.meta.dirname, ".."); +const committedOutput = resolve(root, "bench/results.json"); +const runnerEnv = { ...process.env, GOCACHE: process.env.GOCACHE ?? resolve(root, ".cache", "go-build") }; + +export const percentile99 = (values) => { + assert.ok(values.length > 0, "percentile requires at least one sample"); + const sorted = [...values].sort((a, b) => a - b); + return sorted[Math.ceil(sorted.length * 0.99) - 1]; +}; + +export const parseWorkload = (text) => text.split(/\r?\n/).filter(Boolean).map((line, index) => { + const request = JSON.parse(line); + if (!request.id || !request.operation) throw new Error(`workload line ${index + 1} requires id and operation`); + return request; +}); + +export const validateResults = (results) => { + assert.equal(typeof results.generatedAt, "string"); + assert.ok(results.samples.coldStarts >= 20); + assert.ok(results.samples.requests >= 1000); + for (const name of ["javascript", "go"]) { + const metrics = results.implementations?.[name]; + assert.ok(metrics, `missing ${name} metrics`); + for (const key of ["startupP99Ms", "latencyP99Ms", "throughputOpsPerSecond"]) { + assert.ok(Number.isFinite(metrics[key]) && metrics[key] >= 0, `${name}.${key} must be nonnegative`); + } + assert.ok(metrics.peakRssBytes === null || (Number.isFinite(metrics.peakRssBytes) && metrics.peakRssBytes >= 0)); + } + return true; +}; + +const nsToMs = (nanoseconds) => Number(nanoseconds) / 1e6; +const version = (command, args) => execFileSync(command, args, { cwd: root, encoding: "utf8" }).trim(); +const commands = () => ({ + javascript: [process.execPath, ["compat/js-runner.mjs"]], + go: [resolve(root, "bin", process.platform === "win32" ? "tinycolor.exe" : "tinycolor"), []], +}); + +class Runner { + constructor(command, args) { + this.child = spawn(command, args, { cwd: root, env: runnerEnv, stdio: ["pipe", "pipe", "pipe"] }); + this.pending = []; + this.stderr = ""; + readline.createInterface({ input: this.child.stdout, crlfDelay: Infinity }).on("line", (line) => { + this.pending.shift()?.resolve(JSON.parse(line)); + }); + this.child.stderr.on("data", (chunk) => { this.stderr += chunk; }); + this.child.on("exit", (code) => { + const error = new Error(`runner exited with ${code}: ${this.stderr}`); + for (const pending of this.pending.splice(0)) pending.reject(error); + }); + } + + request(value) { + return new Promise((resolveRequest, reject) => { + if (this.child.exitCode !== null) return reject(new Error(`runner exited: ${this.stderr}`)); + this.pending.push({ resolve: resolveRequest, reject }); + this.child.stdin.write(`${JSON.stringify(value)}\n`, (error) => { + if (error) reject(error); + }); + }); + } + + close() { + if (this.child.exitCode === null) this.child.kill(); + } +} + +const readRss = (pid) => { + try { + if (process.platform === "linux") { + const match = /^VmRSS:\s+(\d+)\s+kB$/m.exec(readFileSync(`/proc/${pid}/status`, "utf8")); + return match ? Number(match[1]) * 1024 : null; + } + if (process.platform === "win32") { + const value = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).WorkingSet64`], { encoding: "utf8" }).trim(); + return Number(value); + } + } catch { + // The child can exit between the response and RSS sample. + } + return null; +}; + +const measure = async (command, args, workload, coldStarts, requests) => { + const startup = []; + let peakRss = 0; + let rssSupported = false; + for (let index = 0; index < coldStarts; index++) { + const started = process.hrtime.bigint(); + const runner = new Runner(command, args); + await runner.request(workload[index % workload.length]); + startup.push(nsToMs(process.hrtime.bigint() - started)); + const rss = readRss(runner.child.pid); + if (rss !== null) { + rssSupported = true; + peakRss = Math.max(peakRss, rss); + } + runner.close(); + } + + const runner = new Runner(command, args); + const latencies = []; + const throughputStarted = process.hrtime.bigint(); + for (let index = 0; index < requests; index++) { + const started = process.hrtime.bigint(); + await runner.request(workload[index % workload.length]); + latencies.push(nsToMs(process.hrtime.bigint() - started)); + } + const totalSeconds = Number(process.hrtime.bigint() - throughputStarted) / 1e9; + for (let index = 0; index < 10; index++) { + const rss = readRss(runner.child.pid); + if (rss !== null) { + rssSupported = true; + peakRss = Math.max(peakRss, rss); + } + await new Promise((resolveTick) => setImmediate(resolveTick)); + } + runner.close(); + + return { + startupP99Ms: percentile99(startup), + latencyP99Ms: percentile99(latencies), + throughputOpsPerSecond: requests / totalSeconds, + peakRssBytes: rssSupported ? peakRss : null, + ...(rssSupported ? {} : { rssLimitation: `RSS sampling is unsupported on ${process.platform}` }), + }; +}; + +const main = async () => { + const args = process.argv.slice(2); + const quick = args.includes("--quick"); + const outputIndex = args.indexOf("--output"); + if (outputIndex !== -1 && !args[outputIndex + 1]) throw new Error("usage: node bench/run.mjs [--quick] [--output ]"); + const output = outputIndex === -1 ? committedOutput : resolve(root, args[outputIndex + 1]); + if (quick && output === committedOutput) throw new Error("--quick must not write bench/results.json"); + + const coldStarts = quick ? 3 : 20; + const requests = quick ? 30 : 1000; + const workloadText = readFileSync(resolve(root, "bench/workload.jsonl"), "utf8").replace(/\r\n/g, "\n"); + const workload = parseWorkload(workloadText); + execFileSync("go", ["-C", "src", "build", "-o", `../bin/${process.platform === "win32" ? "tinycolor.exe" : "tinycolor"}`, "./cmd/tinycolor-compat"], { cwd: root, env: runnerEnv, stdio: "inherit" }); + + const implementations = {}; + for (const [name, [command, commandArgs]] of Object.entries(commands())) { + implementations[name] = await measure(command, commandArgs, workload, coldStarts, requests); + } + const results = { + generatedAt: new Date().toISOString(), + os: process.platform, + architecture: process.arch, + cpu: cpus()[0]?.model ?? "unknown", + nodeVersion: process.version, + goVersion: version("go", ["version"]), + workloadSha256: createHash("sha256").update(workloadText).digest("hex"), + samples: { coldStarts, requests }, + implementations, + }; + if (!quick) validateResults(results); + mkdirSync(dirname(output), { recursive: true }); + writeFileSync(output, `${JSON.stringify(results, null, 2)}\n`); + console.log(`wrote ${output}`); +}; + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/bench/run.test.mjs b/bench/run.test.mjs new file mode 100644 index 00000000..64e84cf0 --- /dev/null +++ b/bench/run.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { parseWorkload, percentile99, validateResults } from "./run.mjs"; + +test("percentile99 selects the nearest-rank p99 sample", () => { + assert.equal(percentile99([1, 2, 3, 4]), 4); +}); + +test("workload parser accepts a representative public request", () => { + assert.deepEqual(parseWorkload('{"id":"x","operation":"inspect","input":"red"}\n'), [ + { id: "x", operation: "inspect", input: "red" }, + ]); +}); + +test("results schema rejects a missing implementation", () => { + assert.throws(() => validateResults({ samples: { coldStarts: 20, requests: 1000 } })); +}); + +test("quick benchmark writes complete measurements to a temporary output", () => { + const output = join(mkdtempSync(join(tmpdir(), "tinycolor-bench-")), "results.json"); + execFileSync(process.execPath, ["bench/run.mjs", "--quick", "--output", output], { stdio: "inherit" }); + const results = JSON.parse(readFileSync(output, "utf8")); + for (const name of ["javascript", "go"]) { + const metrics = results.implementations[name]; + assert.ok(metrics.startupP99Ms >= 0); + assert.ok(metrics.latencyP99Ms >= 0); + assert.ok(metrics.throughputOpsPerSecond > 0); + assert.ok(metrics.peakRssBytes === null || metrics.peakRssBytes >= 0); + } +}); diff --git a/bench/workload.jsonl b/bench/workload.jsonl new file mode 100644 index 00000000..2555599a --- /dev/null +++ b/bench/workload.jsonl @@ -0,0 +1,9 @@ +{"id":"bench-parse-name","operation":"inspect","input":"rebeccapurple"} +{"id":"bench-parse-rgba","operation":"inspect","input":"rgba(12, 34, 56, 0.75)"} +{"id":"bench-convert-hsl","operation":"output","input":"#336699","args":{"method":"toHslString"}} +{"id":"bench-convert-hex8","operation":"output","input":"hsla(210, 50%, 40%, 0.5)","args":{"method":"toHex8String"}} +{"id":"bench-modify","operation":"modify","input":"#80402080","args":{"method":"lighten","amount":20}} +{"id":"bench-mix","operation":"mix","input":"rgba(255,0,0,0.25)","args":{"other":"#0000ff80","amount":35}} +{"id":"bench-readability","operation":"readability","input":"#123456","args":{"other":"#fefefe"}} +{"id":"bench-readable","operation":"isReadable","input":"#ff0088","args":{"other":"#2e0c3a","options":{"level":"AA","size":"small"}}} +{"id":"bench-palette","operation":"palette","input":"#336699","args":{"method":"analogous","results":6,"slices":30}} From 02c625bd9de243a340c51e85816ffe3fc8101e01 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 11:28:52 +0530 Subject: [PATCH 145/165] docs(05-02): summarize fuzz and benchmark evidence --- .../05-delivery-evidence/05-02-SUMMARY.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .planning/phases/05-delivery-evidence/05-02-SUMMARY.md diff --git a/.planning/phases/05-delivery-evidence/05-02-SUMMARY.md b/.planning/phases/05-delivery-evidence/05-02-SUMMARY.md new file mode 100644 index 00000000..a5da7be9 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-02-SUMMARY.md @@ -0,0 +1,38 @@ +--- +phase: 5 +plan: 2 +subsystem: fuzz-benchmark-evidence +tags: [fuzz, differential, benchmark, p99, rss] +provides: [validated 60-second fuzz log, shared-workload benchmark evidence] +--- + +# Phase 5 Plan 2 summary + +Published a deterministic persistent-process differential harness and validated +60-second zero-divergence log, then measured the original JavaScript adapter and +Go adapter with one representative workload on the same host. + +## Evidence + +- `fuzz/log.txt`: 60.012 seconds, seed 20260801, 1,091,630 cases, 0 divergences. +- `bench/results.json`: 20 cold starts and 1,000 persistent requests per runtime. +- JavaScript: 41.4131 ms startup p99, 0.2795 ms latency p99, 7,834.77 ops/s, + 42,868,736-byte peak RSS. +- Go: 11.0109 ms startup p99, 0.1808 ms latency p99, 16,756.79 ops/s, + 12,668,928-byte peak RSS. +- Benchmark tests, normalized workload SHA-256 validation, all Node checks, all + 164 fixed corpus cases, Go tests, and Go vet passed. + +GNU Make is unavailable in the local PowerShell environment, so its constituent +commands were executed directly with the repository-local Go cache. The final +Ubuntu Actions run remains the direct `make verify` proof. + +## Commits + +- `6b4506a`, `0e54f1a`: persistent fuzz harness and exact-parity fixes. +- `ad69b3e`: validated 60-second fuzz evidence. +- `03683ec`: shared benchmark workload, runner, tests, methodology, and results. + +## Remaining external action + +Record and publish the five-minute demo video; no URL is claimed yet. From eff69810d5d28979d4359e8fb18df2ea9af704c9 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 11:29:12 +0530 Subject: [PATCH 146/165] docs(05-03): write submission guide and decisions --- DECISIONS.md | 23 +- PORT.md | 14 +- README.md | 541 +++++++---------------------------------- docs/ARCHITECTURE.md | 5 +- docs/DEMO.md | 19 ++ docs/TEAM-OWNERSHIP.md | 9 + docs/TESTING.md | 43 ++-- 7 files changed, 148 insertions(+), 506 deletions(-) create mode 100644 docs/DEMO.md diff --git a/DECISIONS.md b/DECISIONS.md index 96b472d3..6221f0ad 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -2,11 +2,18 @@ | ID | Decision | Why | Status | |---|---|---|---| -| D-001 | Port the checked-out `mod.js` behavior to Go. | It is the exact source and test target selected for the project. | Accepted | -| D-002 | Keep JavaScript oracle files immutable. | Differential evidence is credible only when the reference does not move with the port. | Accepted | -| D-003 | Put the Go module in `src/`. | It keeps the port idiomatic while following the Track H submission layout; the JavaScript oracle remains at the root. | Accepted | -| D-004 | Use JSON Lines adapters over stdin/stdout. | Node and Go can compare dynamic inputs and operations without modifying source tests. | Accepted | -| D-005 | Use Go standard library first. | The port's algorithms are small and self-contained; dependencies add compatibility and supply-chain surface. | Accepted | -| D-006 | Preserve source quirks at the adapter boundary; expose an idiomatic Go API separately. | Compatibility and Go usability are both required, but they should not leak dynamic JavaScript semantics into every caller. | Accepted | -| D-007 | Run the immutable Deno source suite explicitly as `deno test test.js`. | Deno 2 broad discovery includes generated npm and Node port tests outside the pinned source suite. | Accepted | -| D-008 | Use a finite V8-derived luminance transfer table. | TinyColor rounds RGB to 8-bit channels before luminance, while Go `math.Pow` differs from V8 for 93 nonlinear values; the table preserves exact parity without tolerance. | Accepted | +| D-001 | Port the checked-out `mod.js` behavior to Go. | It is the exact source and test target selected at kickoff. | Accepted | +| D-002 | Keep JavaScript oracle files immutable. | Differential evidence is credible only when the reference cannot move with the port. | Accepted | +| D-003 | Put the independent Go module in `src/`. | It follows the submission layout while keeping the JavaScript oracle at the root. | Accepted | +| D-004 | Use JSON Lines adapters over stdin/stdout. | Both runtimes can compare dynamic inputs and operations without editing source tests. | Accepted | +| D-005 | Prefer the Go and Node standard libraries. | TinyColor's algorithms and evidence runners need no third-party dependency or supply-chain surface. | Accepted | +| D-006 | Preserve JavaScript quirks at the adapter boundary while exposing typed Go values. | Compatibility semantics should not force dynamic JavaScript shapes onto every Go caller. | Accepted | +| D-007 | Run the pinned source suite explicitly as `deno test test.js`. | Broad Deno discovery includes generated npm and port-owned tests outside the original suite. | Accepted | +| D-008 | Use a finite V8-derived luminance transfer table. | Go `math.Pow` differs by one ULP for some rounded channels; the finite table preserves exact parity without tolerance. | Accepted | +| D-009 | Keep human CLI commands and zero-argument JSONL mode in one executable. | One artifact serves judges and automated differential tools without duplicate color logic. | Accepted | +| D-010 | Verify kickoff files with a cross-platform SHA-256 manifest parser. | Hash checks must behave consistently with pinned CRLF oracle files and reject path traversal. | Accepted | +| D-011 | Seed the differential fuzzer and use persistent child processes. | A reproducible request prefix plus one process per runtime gives sustained public-API coverage without startup noise. | Accepted | +| D-012 | Compare fuzz responses exactly. | Broad floating-point tolerances would hide observable compatibility defects. | Accepted | +| D-013 | Benchmark both adapters with the same fixed JSONL workload and lifecycle. | Shared inputs and same-host cold/persistent runs make the measurements directly reproducible. | Accepted | +| D-014 | Normalize benchmark workload newlines before hashing. | Evidence hashes remain stable across Windows and Unix checkouts without changing request content. | Accepted | +| D-015 | Publish unsupported or external deliverables as unverified. | Honest missing evidence is preferable to unreproducible CI, public-access, or video claims. | Accepted | diff --git a/PORT.md b/PORT.md index aec69edb..5e296449 100644 --- a/PORT.md +++ b/PORT.md @@ -1,12 +1,4 @@ -# TinyColor Go port +# Port guide -This repository ports the behavior of the pinned `bgrins/TinyColor` checkout -to Go. Build the compatibility CLI with one command: - -```powershell -make build -``` - -Run the current parity checks with `make test`. The immutable JavaScript oracle -stays at the root and its kickoff hashes are recorded in -`tests/original/manifest.sha256`. +The complete build, usage, compatibility, evidence, and limitation guide is in +[`README.md`](README.md). diff --git a/README.md b/README.md index 71b6d767..e3f51de8 100644 --- a/README.md +++ b/README.md @@ -1,492 +1,115 @@ -# TinyColor +# TinyColor Go port -## JavaScript color tooling +An idiomatic Go port of the behavior in the pinned +[`bgrins/TinyColor`](https://github.com/bgrins/TinyColor) checkout. The +JavaScript source remains in this repository as an immutable oracle; exact +JSONL differential checks demonstrate compatibility rather than modifying the +source tests. -TinyColor is a small, fast library for color manipulation and conversion in JavaScript. It allows many forms of input, while providing color conversions and other color utility functions. It has no dependencies. +Source commit: `b49018c9f2dbca313d80d7a4dad25e26143cfe01`. TinyColor and this port retain +Brian Grinstead's MIT license in [`LICENSE`](LICENSE). The original JavaScript +project documentation remains available in the upstream repository history. -## Including in node +## Build once -`tinycolor` can be installed from npm: +Requirements: Go 1.26+, Node.js, and GNU Make. - npm install tinycolor2 - -Then it can be used in your script like so: - -```js -var tinycolor = require("tinycolor2"); -var color = tinycolor("red"); -``` - -Or in a module like so: - -```js -import tinycolor from "tinycolor2"; -var color = tinycolor("red"); -``` - -## Including in a browser - -The package can be bundled from npm, but if you prefer to download it locally you have two choices: - -### ESM - -It can be used as a module by downloading [npm/esm/tinycolor.js](https://github.com/bgrins/TinyColor/blob/master/npm/esm/tinycolor.js) or using https://esm.sh/tinycolor2. - -```html - -``` - -### UMD - -You can use it directly in a script tag by downloading the UMD file from [npm/cjs/tinycolor.js](https://github.com/bgrins/TinyColor/blob/master/npm/cjs/tinycolor.js): - -```html - - -``` - -## Usage - -Call `tinycolor(input)` or `new tinycolor(input)`, and you will have an object with the following properties. See Accepted String Input and Accepted Object Input below for more information about what is accepted. - -## Accepted String Input - -The string parsing is very permissive. It is meant to make typing a color as input as easy as possible. All commas, percentages, parenthesis are optional, and most input allow either 0-1, 0%-100%, or 0-n (where n is either 100, 255, or 360 depending on the value). - -HSL and HSV both require either 0%-100% or 0-1 for the `S`/`L`/`V` properties. The `H` (hue) can have values between 0%-100% or 0-360. - -RGB input requires either 0-255 or 0%-100%. - -If you call `tinycolor.fromRatio`, RGB and Hue input can also accept 0-1. - -Here are some examples of string input: - -### Hex, 8-digit (RGBA) Hex -```js -tinycolor("#000"); -tinycolor("000"); -tinycolor("#369C"); -tinycolor("369C"); -tinycolor("#f0f0f6"); -tinycolor("f0f0f6"); -tinycolor("#f0f0f688"); -tinycolor("f0f0f688"); -``` -### RGB, RGBA -```js -tinycolor("rgb (255, 0, 0)"); -tinycolor("rgb 255 0 0"); -tinycolor("rgba (255, 0, 0, .5)"); -tinycolor({ r: 255, g: 0, b: 0 }); -tinycolor.fromRatio({ r: 1, g: 0, b: 0 }); -tinycolor.fromRatio({ r: .5, g: .5, b: .5 }); -``` -### HSL, HSLA -```js -tinycolor("hsl(0, 100%, 50%)"); -tinycolor("hsla(0, 100%, 50%, .5)"); -tinycolor("hsl(0, 100%, 50%)"); -tinycolor("hsl 0 1.0 0.5"); -tinycolor({ h: 0, s: 1, l: .5 }); -tinycolor.fromRatio({ h: 1, s: 0, l: 0 }); -tinycolor.fromRatio({ h: .5, s: .5, l: .5 }); -``` -### HSV, HSVA -```js -tinycolor("hsv(0, 100%, 100%)"); -tinycolor("hsva(0, 100%, 100%, .5)"); -tinycolor("hsv (0 100% 100%)"); -tinycolor("hsv 0 1 1"); -tinycolor({ h: 0, s: 100, v: 100 }); -tinycolor.fromRatio({ h: 1, s: 0, v: 0 }); -tinycolor.fromRatio({ h: .5, s: .5, v: .5 }); +```sh +make build ``` -### Named -Case insenstive names are accepted, using the [list of colors in the CSS spec](https://www.w3.org/TR/css-color-4/#named-colors). +This creates `bin/tinycolor` on Linux/macOS or `bin/tinycolor.exe` on Windows. +Docker produces the same runnable CLI in one command: -```js -tinycolor("RED"); -tinycolor("blanchedalmond"); -tinycolor("darkblue"); +```sh +docker build -t tinycolor-go . +docker run --rm tinycolor-go parse --json red ``` -### Accepted Object Input -If you are calling this from code, you may want to use object input. Here are some examples of the different types of accepted object inputs: +## CLI - { r: 255, g: 0, b: 0 } - { r: 255, g: 0, b: 0, a: .5 } - { h: 0, s: 100, l: 50 } - { h: 0, s: 100, v: 100 } - -## Methods - -### getFormat - -Returns the format used to create the tinycolor instance -```js -var color = tinycolor("red"); -color.getFormat(); // "name" -color = tinycolor({r:255, g:255, b:255}); -color.getFormat(); // "rgb" +```sh +./bin/tinycolor parse --json red +./bin/tinycolor convert --to hsl --json red +./bin/tinycolor lighten --amount 10 --json '#000' +./bin/tinycolor palette --type triad --json red +./bin/tinycolor contrast --json '#000' '#fff' ``` -### getOriginalInput +Remove `.exe` from the examples on Unix; add it on Windows. Without `--json`, +commands print human-readable output. `convert` supports `hex`, `hex8`, `rgb`, +`percentage-rgb`, `hsl`, `hsv`, and `name`; `palette` supports `complement`, +`splitcomplement`, `triad`, `tetrad`, `analogous`, and `monochromatic`. -Returns the input passed into the constructor used to create the tinycolor instance -```js -var color = tinycolor("red"); -color.getOriginalInput(); // "red" -color = tinycolor({r:255, g:255, b:255}); -color.getOriginalInput(); // "{r: 255, g: 255, b: 255}" -``` +## Go package -### isValid - -Return a boolean indicating whether the color was successfully parsed. Note: if the color is not valid then it will act like `black` when being used with other methods. -```js -var color1 = tinycolor("red"); -color1.isValid(); // true -color1.toHexString(); // "#ff0000" - -var color2 = tinycolor("not a color"); -color2.isValid(); // false -color2.toString(); // "#000000" -``` -### getBrightness +The Go module lives under `src/`: -Returns the perceived brightness of a color, from `0-255`, as defined by [Web Content Accessibility Guidelines (Version 1.0)](http://www.w3.org/TR/AERT#color-contrast). -```js -var color1 = tinycolor("#fff"); -color1.getBrightness(); // 255 +```go +package main -var color2 = tinycolor("#000"); -color2.getBrightness(); // 0 -``` -### isLight +import ( + "fmt" -Return a boolean indicating whether the color's perceived brightness is light. -```js -var color1 = tinycolor("#fff"); -color1.isLight(); // true + "github.com/rajeet-04/tinycolor-go/tinycolor" +) -var color2 = tinycolor("#000"); -color2.isLight(); // false +func main() { + color, _ := tinycolor.FromCompat("#336699", false) + color.Lighten(10) + fmt.Println(color.ToHSLString()) +} ``` -### isDark -Return a boolean indicating whether the color's perceived brightness is dark. -```js -var color1 = tinycolor("#fff"); -color1.isDark(); // false +## Verify parity -var color2 = tinycolor("#000"); -color2.isDark(); // true +```sh +make verify +deno test test.js ``` -### getLuminance - -Returns the perceived luminance of a color, from `0-1` as defined by [Web Content Accessibility Guidelines (Version 2.0).](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef) -```js -var color1 = tinycolor("#fff"); -color1.getLuminance(); // 1 -var color2 = tinycolor("#000"); -color2.getLuminance(); // 0 -``` -### getAlpha +`make verify` checks formatting, the three kickoff hashes in +[`tests/original/manifest.sha256`](tests/original/manifest.sha256), Node and Go +tests, all fixed differential corpora, the recorded fuzz log, and `go vet`. +The untouched Deno suite is a separate source-oracle check. -Returns the alpha value of a color, from `0-1`. -```js -var color1 = tinycolor("rgba(255, 0, 0, .5)"); -color1.getAlpha(); // 0.5 +Evidence: -var color2 = tinycolor("rgb(255, 0, 0)"); -color2.getAlpha(); // 1 +- [Compatibility matrix](COMPATIBILITY.md) — fixed-corpus, Deno, coverage, and safety counts. +- [Differential fuzz log](fuzz/log.txt) and [reproduction guide](fuzz/README.md) — 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. +- [Benchmark results](bench/results.json) and [methodology](bench/methodology.md) — same-host startup p99, latency p99, throughput, and peak RSS. +- [Architectural decisions](DECISIONS.md), [architecture](docs/ARCHITECTURE.md), [testing](docs/TESTING.md), and [team ownership](docs/TEAM-OWNERSHIP.md). +- [Five-minute demo script](docs/DEMO.md). -var color3 = tinycolor("transparent"); -color3.getAlpha(); // 0 -``` -### setAlpha +## Repository layout -Sets the alpha value on a current color. Accepted range is in between `0-1`. -```js -var color = tinycolor("red"); -color.getAlpha(); // 1 -color.setAlpha(.5); -color.getAlpha(); // .5 -color.toRgbString(); // "rgba(255, 0, 0, .5)" +```text +src/ Go module, public API, and CLI +tests/original/ kickoff hash manifest and verifier +tests/port/ port-owned adapter tests +compat/ JSONL oracle, driver, and fixed corpora +fuzz/ differential harness and 60-second log +bench/ shared workload, runner, methodology, and results +docs/ architecture, testing, ownership, and demo guide ``` -### String Representations -The following methods will return a property for the `alpha` value, which can be ignored: `toHsv`, `toHsl`, `toRgb` - -### toHsv -```js -var color = tinycolor("red"); -color.toHsv(); // { h: 0, s: 1, v: 1, a: 1 } -``` -### toHsvString -```js -var color = tinycolor("red"); -color.toHsvString(); // "hsv(0, 100%, 100%)" -color.setAlpha(0.5); -color.toHsvString(); // "hsva(0, 100%, 100%, 0.5)" -``` -### toHsl -```js -var color = tinycolor("red"); -color.toHsl(); // { h: 0, s: 1, l: 0.5, a: 1 } -``` -### toHslString -```js -var color = tinycolor("red"); -color.toHslString(); // "hsl(0, 100%, 50%)" -color.setAlpha(0.5); -color.toHslString(); // "hsla(0, 100%, 50%, 0.5)" -``` -### toHex -```js -var color = tinycolor("red"); -color.toHex(); // "ff0000" -``` -### toHexString -```js -var color = tinycolor("red"); -color.toHexString(); // "#ff0000" -``` -### toHex8 -```js -var color = tinycolor("red"); -color.toHex8(); // "ff0000ff" -``` -### toHex8String -```js -var color = tinycolor("red"); -color.toHex8String(); // "#ff0000ff" -``` -### toRgb -```js -var color = tinycolor("red"); -color.toRgb(); // { r: 255, g: 0, b: 0, a: 1 } -``` -### toRgbString -```js -var color = tinycolor("red"); -color.toRgbString(); // "rgb(255, 0, 0)" -color.setAlpha(0.5); -color.toRgbString(); // "rgba(255, 0, 0, 0.5)" -``` -### toPercentageRgb -```js -var color = tinycolor("red"); -color.toPercentageRgb() // { r: "100%", g: "0%", b: "0%", a: 1 } -``` -### toPercentageRgbString -```js -var color = tinycolor("red"); -color.toPercentageRgbString(); // "rgb(100%, 0%, 0%)" -color.setAlpha(0.5); -color.toPercentageRgbString(); // "rgba(100%, 0%, 0%, 0.5)" -``` -### toName -```js -var color = tinycolor("red"); -color.toName(); // "red" -``` -### toFilter -``` -var color = tinycolor("red"); -color.toFilter(); // "progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffff0000,endColorstr=#ffff0000)" -``` -### toString +## Known limits -Print to a string, depending on the input format. You can also override this by passing one of `"rgb", "prgb", "hex6", "hex3", "hex8", "name", "hsl", "hsv"` into the function. -```js -var color1 = tinycolor("red"); -color1.toString(); // "red" -color1.toString("hsv"); // "hsv(0, 100%, 100%)" +- Compatibility is claimed for the pinned checkout and measured public corpus, + not every future TinyColor revision or arbitrary JavaScript coercion. +- Random colors are checked by validity/range invariants because independent + runtimes do not share a random stream. +- Benchmark figures are observations from one host, not universal speedup + claims. +- The required demo video must still be recorded, uploaded, and linked by a + human; no video URL is claimed in this repository yet. -var color2 = tinycolor("rgb(255, 0, 0)"); -color2.toString(); // "rgb(255, 0, 0)" -color2.setAlpha(.5); -color2.toString(); // "rgba(255, 0, 0, 0.5)" -``` -### Color Modification +## Submission checklist -These methods manipulate the current color, and return it for chaining. For instance: -```js -tinycolor("red").lighten().desaturate().toHexString() // "#f53d3d" -``` -### lighten - -`lighten: function(amount = 10) -> TinyColor`. Lighten the color a given amount, from 0 to 100. Providing 100 will always return white. -```js -tinycolor("#f00").lighten().toString(); // "#ff3333" -tinycolor("#f00").lighten(100).toString(); // "#ffffff" -``` -### brighten - -`brighten: function(amount = 10) -> TinyColor`. Brighten the color a given amount, from 0 to 100. -```js -tinycolor("#f00").brighten().toString(); // "#ff1919" -``` -### darken - -`darken: function(amount = 10) -> TinyColor`. Darken the color a given amount, from 0 to 100. Providing 100 will always return black. -```js -tinycolor("#f00").darken().toString(); // "#cc0000" -tinycolor("#f00").darken(100).toString(); // "#000000" -``` -### desaturate - -`desaturate: function(amount = 10) -> TinyColor`. Desaturate the color a given amount, from 0 to 100. Providing 100 will is the same as calling `greyscale`. -```js -tinycolor("#f00").desaturate().toString(); // "#f20d0d" -tinycolor("#f00").desaturate(100).toString(); // "#808080" -``` -### saturate - -`saturate: function(amount = 10) -> TinyColor`. Saturate the color a given amount, from 0 to 100. -```js -tinycolor("hsl(0, 10%, 50%)").saturate().toString(); // "hsl(0, 20%, 50%)" -``` -### greyscale - -`greyscale: function() -> TinyColor`. Completely desaturates a color into greyscale. Same as calling `desaturate(100)`. -```js -tinycolor("#f00").greyscale().toString(); // "#808080" -``` -### spin - -`spin: function(amount = 0) -> TinyColor`. Spin the hue a given amount, from -360 to 360. Calling with 0, 360, or -360 will do nothing (since it sets the hue back to what it was before). -```js -tinycolor("#f00").spin(180).toString(); // "#00ffff" -tinycolor("#f00").spin(-90).toString(); // "#7f00ff" -tinycolor("#f00").spin(90).toString(); // "#80ff00" - -// spin(0) and spin(360) do nothing -tinycolor("#f00").spin(0).toString(); // "#ff0000" -tinycolor("#f00").spin(360).toString(); // "#ff0000" -``` -### Color Combinations - -Combination functions return an array of TinyColor objects unless otherwise noted. - -### analogous - -`analogous: function(, results = 6, slices = 30) -> array`. -```js -var colors = tinycolor("#f00").analogous(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#ff0066", "#ff0033", "#ff0000", "#ff3300", "#ff6600" ] -``` -### monochromatic - -`monochromatic: function(, results = 6) -> array`. -```js -var colors = tinycolor("#f00").monochromatic(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#2a0000", "#550000", "#800000", "#aa0000", "#d40000" ] -``` -### splitcomplement - -`splitcomplement: function() -> array`. -```js -var colors = tinycolor("#f00").splitcomplement(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#ccff00", "#0066ff" ] -``` -### triad - -`triad: function() -> array`. -```js -var colors = tinycolor("#f00").triad(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#00ff00", "#0000ff" ] -``` -### tetrad - -`tetrad: function() -> array`. -```js -var colors = tinycolor("#f00").tetrad(); - -colors.map(function(t) { return t.toHexString(); }); // [ "#ff0000", "#80ff00", "#00ffff", "#7f00ff" ] - -``` -### complement - -`complement: function() -> TinyColor`. -```js -tinycolor("#f00").complement().toHexString(); // "#00ffff" -``` -## Color Utilities -```js -tinycolor.equals(color1, color2) -tinycolor.mix(color1, color2, amount = 50) -``` -### random - -Returns a random color. -```js -var color = tinycolor.random(); -color.toRgb(); // "{r: 145, g: 40, b: 198, a: 1}" -``` - -### Readability - -TinyColor assesses readability based on the [Web Content Accessibility Guidelines (Version 2.0)](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef). - -#### readability - -`readability: function(TinyColor, TinyColor) -> Object`. -Returns the contrast ratio between two colors. -```js -tinycolor.readability("#000", "#000"); // 1 -tinycolor.readability("#000", "#111"); // 1.1121078324840545 -tinycolor.readability("#000", "#fff"); // 21 -``` -Use the values in your own calculations, or use one of the convenience functions below. - -#### isReadable - -`isReadable: function(TinyColor, TinyColor, Object) -> Boolean`. Ensure that foreground and background color combinations meet WCAG guidelines. `Object` is optional, defaulting to `{level: "AA",size: "small"}`. `level` can be `"AA"` or "AAA" and `size` can be `"small"` or `"large"`. - -Here are links to read more about the [AA](http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) and [AAA](http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html) requirements. -```js -tinycolor.isReadable("#000", "#111", {}); // false -tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AA",size:"small"}); //false -tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AA",size:"large"}), //true -``` -#### mostReadable - -`mostReadable: function(TinyColor, [TinyColor, Tinycolor ...], Object) -> Boolean`. -Given a base color and a list of possible foreground or background colors for that base, returns the most readable color. -If none of the colors in the list is readable, `mostReadable` will return the better of black or white if `includeFallbackColors:true`. -```js -tinycolor.mostReadable("#000", ["#f00", "#0f0", "#00f"]).toHexString(); // "#00ff00" -tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255" -tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff" -tinycolor.mostReadable("#ff0088", ["#2e0c3a"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString() // "#2e0c3a", -tinycolor.mostReadable("#ff0088", ["#2e0c3a"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString() // "#000000", -``` -See [index.html](https://github.com/bgrins/TinyColor/blob/master/index.html) in the project for a demo. - -## Common operations - -### clone - -`clone: function() -> TinyColor`. -Instantiate a new TinyColor object with the same color. Any changes to the new one won't affect the old one. -```js -var color1 = tinycolor("#F00"); -var color2 = color1.clone(); -color2.setAlpha(.5); - -color1.toString(); // "#ff0000" -color2.toString(); // "rgba(255, 0, 0, 0.5)" -``` +- [x] Public source URL and pinned kickoff commit recorded. +- [x] One-command native and Docker builds documented. +- [x] Immutable oracle hashes and source suite verified. +- [x] Exact fixed-corpus and 60-second differential evidence published. +- [x] Same-host benchmark methodology and results published. +- [x] Decisions, ownership, limitations, and demo script documented. +- [ ] Five-minute demo video recorded and published. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b7e471b9..acaa5bcc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -24,9 +24,12 @@ cases ── differential driver ── Go JSONL runner 3. **Public library (`src/tinycolor`):** exposes explicit Go values and methods. It owns conversions, formatting, mutation, utilities, readability, and palettes. -4. **CLI (`src/cmd/tinycolor`):** a thin user-facing wrapper; it calls the same +4. **CLI (`src/cmd/tinycolor-compat`):** a thin user-facing wrapper; it calls the same library and does not reimplement parsing or conversion. +The same zero-argument executable is used by `compat/run.mjs`, the fuzz harness, +and the benchmark runner. Human subcommands select the judge-facing CLI instead. + ## Compatibility protocol Each line is a single JSON request with `id`, `operation`, `input`, and optional diff --git a/docs/DEMO.md b/docs/DEMO.md new file mode 100644 index 00000000..4a4998fd --- /dev/null +++ b/docs/DEMO.md @@ -0,0 +1,19 @@ +# Five-minute demo script + +The recording and upload remain manual and unverified until a public URL is +added to `README.md`. + +| Time | Demonstration | +|---|---| +| 0:00–0:30 | Show `git status --short`, the source URL/commit in `.port-mortem.toml`, and run `node tests/original/verify.mjs`. | +| 0:30–1:00 | Run `make build`, then show the single artifact under `bin/`. | +| 1:00–2:10 | Run JSON examples for `parse`, `convert`, `lighten`, `palette`, and `contrast` from `README.md`. | +| 2:10–2:50 | Run `make verify` and point out all five exact corpus totals. | +| 2:50–3:30 | Open `fuzz/log.txt`, validate it, then run the one-second seeded fuzz smoke. | +| 3:30–4:10 | Open `bench/results.json` and explain the shared workload, p99 formula, throughput, RSS, and same-host limitation. | +| 4:10–4:40 | Show `DECISIONS.md`, zero-unsafe command, coverage table, and known limits in `COMPATIBILITY.md`. | +| 4:40–5:00 | Show the public repository and successful exact-commit Actions run, then close on the runnable binary path. | + +Before submission, verify repository visibility while logged out, record one +continuous take, publish it, and add the video URL. Those account actions are +not claimed by this script. diff --git a/docs/TEAM-OWNERSHIP.md b/docs/TEAM-OWNERSHIP.md index 8ecb96ca..8ed83060 100644 --- a/docs/TEAM-OWNERSHIP.md +++ b/docs/TEAM-OWNERSHIP.md @@ -29,3 +29,12 @@ Every handoff includes the commit, public contract or command added, passing checks, corpus coverage, and known gaps. Do not hand off an uncommitted shared working tree as the only artifact. +## Phase 5 integration handoff + +- @rajeet-04 integrated exact fuzz parity, the validated 60-second log, and the + shared benchmark evidence on branch `rajeet`. +- @mrashis owns any new differential mismatch reproduction. +- @deepali owns final submission presentation and the manual demo-video URL. +- @xthxr remains the review lead for conversion, readability, and palette + behavior; Phase 1–4 ownership is unchanged. + diff --git a/docs/TESTING.md b/docs/TESTING.md index 8050b8ac..e6d39c6e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,31 +1,20 @@ -# Testing Strategy +# Testing strategy -## Test layers - -| Layer | Purpose | Command when implemented | +| Layer | Purpose | Command | |---|---|---| -| Go unit tests | Local conversion and parser invariants | `go test ./...` from `src/` | -| Differential corpus | Exact observable parity with local `mod.js` | `node compat/run.mjs ` | -| Regression corpus | Permanently reproduce every found mismatch | `node compat/run.mjs compat/cases/regression.jsonl` | -| Seeded fuzz cases | Find coercion, bounds, and rounding gaps | documented seed command | -| Source suite | Validate original source remains runnable | `deno task test` when Deno exists | -| Static checks | Formatting and common Go defects | `gofmt`, `go vet ./...` | - -## Comparison rules - -- Compare strings byte-for-byte. -- Compare booleans, arrays, formats, and errors exactly. -- Serialize non-finite values explicitly if the source produces them; JSON's - default inability to represent them must not silently erase a difference. -- Use a narrowly documented normalization only for JavaScript/Go JSON number - serialization—not for color math. Never use a global epsilon. -- Seed random generation and print the seed on failure. +| Full local gate | Format, hashes, unit, differential, fuzz-log, vet | `make verify` | +| Original source suite | Untouched upstream behavior | `deno test test.js` | +| Go unit/coverage | Package behavior and honest coverage | `go -C src test -cover ./...` | +| Fixed differential | Exact JS/Go response parity | `node compat/run.mjs compat/cases/operations.jsonl` | +| Fuzz smoke | Seeded broad exact comparison | `node fuzz/harness.mjs --duration 1 --seed 20260801` | +| Fuzz evidence | Validate recorded 60-second run | `node fuzz/validate-log.mjs fuzz/log.txt` | +| Benchmark smoke | Real quick measurement to temporary output | `node bench/run.mjs --quick --output ` | +| Benchmark evidence | Full same-host measurement | `node bench/run.mjs --output bench/results.json` | -## Corpus priorities +Strings, booleans, arrays, errors, formats, and parsed numeric results are +compared exactly. No global epsilon is used. Every discovered mismatch becomes +a deterministic regression before the shared implementation boundary is fixed. -1. Inputs copied from `test.js` in parser and output groups. -2. Boundaries: `-1`, `0`, `1`, `1.0`, `100%`, `255`, overflow, blank, malformed, - alpha `0`, and hue values below/above 360. -3. State transitions: clone, set alpha, all instance modifiers, default versus - explicit zero amount. -4. Structured random RGB/RGBA/HSL/HSV inputs generated from a fixed seed. +The fixed corpora cover source-test inputs, permissive and malformed parsing, +numeric boundaries, alpha and hue behavior, conversions, mutation, WCAG +readability, mixing, and ordered palettes. Random output is invariant-tested. From b5dbb5d48b0feb558430bcc56a50c3e9c185a950 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 11:29:23 +0530 Subject: [PATCH 147/165] docs(05-03): publish final compatibility evidence --- COMPATIBILITY.md | 167 +++++++++++++++++++++-------------------------- 1 file changed, 73 insertions(+), 94 deletions(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 04436a3c..54f57dba 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,94 +1,73 @@ -# Compatibility Matrix - -Target: this checkout's `mod.js` and `test.js` from `bgrins/TinyColor`. - -## Current evidence - -| Area | Status | Evidence | Notes | -|---|---|---|---| -| Oracle baseline | Ready | `mod.js`, `test.js`, Node 24.18.0 | JavaScript source is untouched. | -| Node adapter | Passing | `node tests/port/adapter.test.mjs` | Imports local `mod.js`; success/error responses are exclusive. | -| Go adapter | Passing | `go test ./...` from `src/` | JSONL schema shared with Node adapter. | -| HEX/RGB/name parsing | Passing | `compat/cases/parser-hex-rgb.jsonl` | Source-derived HEX/RGB/name/object corpus. | -| HSL/HSV parsing | Passing | `compat/cases/parser.jsonl` | Includes ratios, percentages, wrapping, and object precedence. | -| Conversion/formatting | Passing | `compat/cases/conversion.jsonl` | 35 fixed JSONL cases; random is invariant-only. | -| Analysis/readability | Passing | `compat/cases/operations.jsonl` | WCAG defaults, thresholds, ties, fallback, and null selection. | -| Manipulation/palettes | Passing | `compat/cases/operations.jsonl` | 69 fixed operations cases cover modifiers, Mix, readability, and ordered palettes. | -| CLI/CI/benchmarks | Planned | Wave 4 | No result claimed yet. | -| Original Deno suite | Blocked locally | `deno` unavailable | Run when Deno is installed or in CI. | - -## Mismatch record format - -```text -Case: -Operation: -Input: -JavaScript: -Go: -Owner: -Status: open | fixed | accepted-difference -Reason: -``` - -## Phase 1 smoke result - -On 2026-08-01, the fixed Phase 1 corpus passed with **9/9 cases** and **0 -mismatches**: - -```powershell -$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -node tests/port/adapter.test.mjs -Set-Location src; go test ./...; go vet ./...; Set-Location .. -node compat/run.mjs compat/cases/smoke.jsonl -``` - -This is only the fixed Phase 1 corpus; it is not a full TinyColor parity claim. - -## Phase 2 parser slice result - -On 2026-08-01, the HEX/RGB/name parser corpus passed with **26/26 cases** and -**0 mismatches**: - -```powershell -$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -Set-Location src; go test ./...; go vet ./...; Set-Location .. -node compat/run.mjs compat/cases/parser-hex-rgb.jsonl -node compat/run.mjs compat/cases/smoke.jsonl -``` - -This records only the completed parser slice; the full HSL/HSV corpus remains -Phase 2 Plan 02 work. - -## Phase 2 full parser result - -`node compat/run.mjs compat/cases/parser.jsonl` passed with **23/23 cases** and -**0 mismatches** on 2026-08-01. The unchanged Phase 1 smoke corpus also passed -**9/9** with **0 mismatches**. - -## Phase 3 conversion result - -On 2026-08-01, the fixed conversion corpus passed with **35/35 cases** and -**0 mismatches**. The complete Phase 1–3 gate also retained **9/9** smoke, -**26/26** HEX/RGB/name, and **23/23** parser cases with zero mismatches: - -```powershell -$env:GOCACHE = 'R:\Code\TinyColor\.cache\go-build' -Set-Location src; go test ./...; go vet ./...; Set-Location .. -node tests/port/adapter.test.mjs -node compat/run.mjs compat/cases/smoke.jsonl -node compat/run.mjs compat/cases/parser-hex-rgb.jsonl -node compat/run.mjs compat/cases/parser.jsonl -node compat/run.mjs compat/cases/conversion.jsonl -``` - -Random-color behavior is verified by validity, alpha, and channel-range -invariants only; it is not compared exactly across independent JavaScript and -Go random generators. - -## Phase 4 operations result - -On 2026-08-01, the fixed operations corpus passed with **69/69 cases** and -**0 mismatches**. The full available Phase 1-4 gate also retained **9/9** -smoke, **26/26** HEX/RGB/name, **23/23** parser, and **35/35** conversion -cases with zero mismatches. This evidence does not claim a Deno source-suite -pass because Deno is unavailable locally. +# Compatibility evidence + +Target: `bgrins/TinyColor` commit +`b49018c9f2dbca313d80d7a4dad25e26143cfe01`, pinned by +`tests/original/manifest.sha256`. + +## Fixed differential corpora + +Fresh command: `node compat/run.mjs `. + +| Corpus | Passed/total | Mismatches | +|---|---:|---:| +| `compat/cases/smoke.jsonl` | 9/9 | 0 | +| `compat/cases/parser-hex-rgb.jsonl` | 26/26 | 0 | +| `compat/cases/parser.jsonl` | 23/23 | 0 | +| `compat/cases/conversion.jsonl` | 35/35 | 0 | +| `compat/cases/operations.jsonl` | 71/71 | 0 | +| **Total** | **164/164** | **0** | + +Random-color behavior is checked through validity, alpha, and channel-range +invariants rather than exact equality between independent random generators. + +## Source suite and oracle integrity + +- `node tests/original/verify.mjs`: 3/3 kickoff hashes verified. +- `deno test test.js`: 45 passed, 0 failed, 1 ignored. +- The ignored `polyad` test is also ignored by the pinned upstream suite. + +## Go coverage and safety + +Fresh command: `go -C src test -cover ./...`. + +| Package | Statement coverage | +|---|---:| +| `cmd/tinycolor-compat` | 33.6% | +| `internal/color` | 90.5% | +| `internal/compat` | 57.1% | +| `internal/parser` | 85.8% | +| `tinycolor` | 92.8% | + +These are Go package coverage figures, not a JavaScript coverage comparison. +`rg -n '\bunsafe\b' src -g '*.go'` reports **0 Go source occurrences**. + +## Differential fuzzing + +[`fuzz/log.txt`](fuzz/log.txt) records 60.012 seconds, seed 20260801, +1,091,630 cases, and zero divergences. Validate it with +`node fuzz/validate-log.mjs fuzz/log.txt`. This supports the Differential Fuzz +Survivor claim; exact comparison remains enabled. + +## Same-host benchmark + +The committed [results](bench/results.json) were measured on Windows x64 with +20 cold starts and 1,000 persistent requests per implementation: + +| Implementation | Startup p99 | Latency p99 | Throughput | Peak RSS | +|---|---:|---:|---:|---:| +| JavaScript | 41.4131 ms | 0.2795 ms | 7,834.77 ops/s | 42,868,736 bytes | +| Go | 11.0109 ms | 0.1808 ms | 16,756.79 ops/s | 12,668,928 bytes | + +See [`bench/methodology.md`](bench/methodology.md). These are same-host +observations, not universal speedup claims. + +## Bonus and external evidence status + +- Differential Fuzz Survivor: eligible from the validated 60-second log. +- Zero Unsafe: eligible from zero Go source occurrences. +- Decision Log: eligible from 15 substantive decisions. +- GitHub Actions: configured; the final exact-commit run is not yet recorded here. +- Public repository visibility: not yet verified from an unauthenticated clone in this closeout. +- Five-minute demo video: not supplied. + +No known mismatch remains in the fixed corpus or recorded fuzz session. From 6692093517e62bb65f59f9f21e07654bd7cfa9ae Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 11:32:22 +0530 Subject: [PATCH 148/165] docs(05-03): verify local delivery evidence --- .planning/HANDOFF.json | 85 ------------------- .../05-delivery-evidence/.continue-here.md | 52 ------------ .../05-delivery-evidence/05-03-SUMMARY.md | 45 ++++++++++ .../05-delivery-evidence/05-VERIFICATION.md | 53 ++++++++++++ 4 files changed, 98 insertions(+), 137 deletions(-) delete mode 100644 .planning/HANDOFF.json delete mode 100644 .planning/phases/05-delivery-evidence/.continue-here.md create mode 100644 .planning/phases/05-delivery-evidence/05-03-SUMMARY.md create mode 100644 .planning/phases/05-delivery-evidence/05-VERIFICATION.md diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json deleted file mode 100644 index 551ed4d3..00000000 --- a/.planning/HANDOFF.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "version": "1.0", - "timestamp": "2026-08-01T00:20:50.023Z", - "phase": "5", - "phase_name": "Delivery Evidence", - "phase_dir": ".planning/phases/05-delivery-evidence", - "plan": 2, - "task": 3, - "total_tasks": 3, - "status": "paused", - "completed_tasks": [ - { - "id": "05-02-01", - "name": "Deterministic persistent-process differential fuzz harness", - "status": "done", - "commit": "6b4506a, 0e54f1a" - }, - { - "id": "05-02-02", - "name": "Validated 60-second fuzz survivor evidence", - "status": "done", - "commit": "ad69b3e" - } - ], - "remaining_tasks": [ - { - "id": "05-02-03", - "name": "Measure shared-workload p99, RSS, startup, and throughput", - "status": "not_started" - }, - { - "id": "05-03-01", - "name": "Write judge-facing port documentation and decision log", - "status": "not_started" - }, - { - "id": "05-03-02", - "name": "Refresh honest parity, coverage, safety, benchmark, and bonus evidence", - "status": "not_started" - }, - { - "id": "05-03-03", - "name": "Run final clean local gate and write verification evidence", - "status": "not_started" - }, - { - "id": "05-03-04", - "name": "Verify public clone access and successful CI", - "status": "not_started" - }, - { - "id": "05-03-05", - "name": "Close Phase 5 planning state after external proof", - "status": "not_started" - } - ], - "blockers": [], - "human_actions_pending": [ - { - "action": "Record and publish the five-minute demo video, then add its URL during Plan 05-03.", - "context": "The submission requires a live demonstration; no video is currently claimed.", - "blocking": true - } - ], - "decisions": [ - { - "decision": "Use a finite V8-derived 256-entry luminance transfer table.", - "rationale": "Go math.Pow differs from V8 for 93 nonlinear 8-bit values, and exact parity forbids tolerance.", - "phase": "5" - }, - { - "decision": "Apply TinyColor's sub-one channel rounding at the FromCompat constructor boundary.", - "rationale": "One shared boundary fixes palette conversions without per-operation exceptions.", - "phase": "5" - }, - { - "decision": "Keep the fuzz generator broad and compare parsed responses exactly.", - "rationale": "The broad seed exposed both arithmetic and constructor parity defects that a narrowed corpus would hide.", - "phase": "5" - } - ], - "uncommitted_files": [], - "next_action": "Run `$gsd-resume-work`, then execute task 05-02-03 from `.planning/phases/05-delivery-evidence/05-02-PLAN.md`: build the shared benchmark workload and runner with tests first, write real results once, and create 05-02-SUMMARY.md only after benchmark verification passes.", - "context_notes": "Branch rajeet. Exact fuzz parity is resolved. The real checked-in log reports 60.012 seconds, seed 20260801, 1,091,630 cases, and zero divergences. Local verification passed all Go and Node gates, all five corpora (9/9, 26/26, 23/23, 35/35, 71/71), vet, fuzz tests, log validation, and the immutable Deno suite (45 passed, 0 failed, 1 ignored). GitHub Actions run 30675374085 passed make verify, Deno, build, and artifact upload in 43 seconds. No background process is running." -} diff --git a/.planning/phases/05-delivery-evidence/.continue-here.md b/.planning/phases/05-delivery-evidence/.continue-here.md deleted file mode 100644 index 282aa63c..00000000 --- a/.planning/phases/05-delivery-evidence/.continue-here.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -phase: 05-delivery-evidence -task: 3 -total_tasks: 3 -status: in_progress -last_updated: 2026-08-01T00:20:50.023Z ---- - - -Plan 05-02 is at Task 3/3. Differential fuzzing is finished and verified; the next task is the shared original-versus-port benchmark. Branch `rajeet` contains the exact parity fix in `0e54f1a` and the validated evidence in `ad69b3e`. - - - - -- Task 05-02-01: Persistent seeded differential harness — done. -- Exact readability mismatch fixed with a V8-derived 256-entry transfer table. -- Follow-on palette mismatches fixed at the shared TinyColor constructor boundary. -- One-second seed-1 smoke: 14,778 cases, zero divergences. -- Task 05-02-02: Real 60-second evidence — done. -- `fuzz/log.txt`: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. -- `make verify` now runs harness tests and validates the checked-in fuzz log. -- Local direct verification passed all Node/Go gates and corpora; Deno passed 45, failed 0, ignored 1. - - - - -- Task 05-02-03: Implement and run the benchmark specified in `05-02-PLAN.md`. -- After Task 3 passes, create `05-02-SUMMARY.md`. -- Execute Plan 05-03 documentation, honest evidence refresh, final external CI/public-clone proof, and closeout. -- Human: record and publish the required five-minute demo video. - - - - -- Use exact V8-derived luminance bits because heuristic ULP correction is wrong for the finite channel domain. -- Preserve TinyColor's constructor channel rounding in `FromCompat`, where every operation routes through it. -- Keep fuzz comparisons exact and the generator broad; do not add tolerance or remove failing input families. - - - - -- No technical blocker for benchmarking. -- The demo video remains a human submission action for Plan 05-03. - - - -The previous Actions failure was caused by a deliberately committed RED regression. GitHub Actions run `30675374085` passed `make verify`, Deno, build, and artifact upload in 43 seconds. Plan 05-03 still requires a final public-clone and CI proof after benchmarks and documentation. No server or watcher is running. - - - -Start with: run `$gsd-resume-work`, read Task 05-02-03 in `.planning/phases/05-delivery-evidence/05-02-PLAN.md`, then write the benchmark tests before implementing `bench/run.mjs`. Use temporary output for quick tests and write `bench/results.json` only once with the full measured run. - diff --git a/.planning/phases/05-delivery-evidence/05-03-SUMMARY.md b/.planning/phases/05-delivery-evidence/05-03-SUMMARY.md new file mode 100644 index 00000000..81f95182 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-03-SUMMARY.md @@ -0,0 +1,45 @@ +--- +phase: 5 +plan: 3 +subsystem: submission-documentation-closeout +tags: [readme, decisions, compatibility, verification] +provides: [judge-facing guide, honest compatibility evidence, local closeout proof] +status: local-complete-external-pending +--- + +# Phase 5 Plan 3 summary + +Replaced the upstream-oriented landing page with a concise port guide, expanded +the architectural decision log to 15 substantive entries, published current +compatibility/coverage/safety/benchmark evidence, and added a five-minute demo +script with its human actions explicitly unclaimed. + +## Task commits + +- `82a9532` `docs(05-03): write submission guide and decisions` +- `7c2da11` `docs(05-03): publish final compatibility evidence` +- Closing local-verification commit: this summary and `05-VERIFICATION.md`. + +## Fresh local evidence + +- Native build and JSON invocations of parse, convert, lighten, palette, and + contrast exited zero. +- 15 Node tests passed with zero failures. +- Go tests and vet passed using the repository-local Go cache. +- Fixed corpora passed 9/9, 26/26, 23/23, 35/35, and 71/71. +- Fresh one-second seed-20260801 fuzz: 11,103 cases, zero divergences. +- Recorded fuzz log validated: 60.012 seconds, 1,091,630 cases, zero divergences. +- Benchmark schema and normalized workload SHA-256 verified; quick measurement + wrote only to a temporary path. +- Immutable Deno suite: 45 passed, 0 failed, 1 ignored. +- Three oracle hashes verified; `git diff -- mod.js test.js tinycolor.js` empty. + +GNU Make is unavailable in this Windows PowerShell environment, so the exact +commands behind `make build` and `make verify` were executed directly. The +Ubuntu CI run remains the direct Make proof. + +## Remaining external actions + +- Push the current `rajeet` commit and verify the exact GitHub Actions run. +- Verify unauthenticated public access to the repository. +- Human: record, upload, and link the five-minute demo video. diff --git a/.planning/phases/05-delivery-evidence/05-VERIFICATION.md b/.planning/phases/05-delivery-evidence/05-VERIFICATION.md new file mode 100644 index 00000000..29382550 --- /dev/null +++ b/.planning/phases/05-delivery-evidence/05-VERIFICATION.md @@ -0,0 +1,53 @@ +--- +phase: 5 +status: locally-verified +verified: 2026-08-01 +requirements: [DEL-01, QLT-03] +--- + +# Phase 5 verification + +## Goal + +A judge can build and use one runnable Go artifact, reproduce exact parity +evidence, inspect honest performance/safety numbers, and distinguish observed +local evidence from external actions. + +## Roadmap success criteria + +| Criterion | Status | Evidence | +|---|---|---| +| CI runs format, vet, Go tests, and differential checks | Configured; exact final run pending | `.github/workflows/port.yml`, `Makefile`; local constituent commands passed. | +| CLI supports parse, convert, lighten, palette, contrast, and JSON | Local pass | `make build` equivalent plus all five JSON invocations exited zero; implementation under `src/cmd/tinycolor-compat`. | +| Benchmarks, docs, attribution, ownership, and known differences are complete | Local pass | `bench/results.json`, `bench/methodology.md`, `README.md`, `LICENSE`, `DECISIONS.md`, `COMPATIBILITY.md`, `docs/TEAM-OWNERSHIP.md`. | + +## DEL-01 evidence + +- One-command build is defined by `make build`; direct Windows build produced + `bin/tinycolor.exe` and all five public commands ran successfully. +- `tests/original/manifest.sha256` verified all three immutable oracle files. +- Five fixed corpora passed 164/164 with zero mismatches. +- `fuzz/log.txt` validates a 60.012-second, seed-20260801 run with 1,091,630 + cases and zero divergences; a fresh one-second smoke passed 11,103 cases. +- `bench/results.json` contains 20 cold starts, 1,000 persistent requests, and + both implementations' startup p99, latency p99, throughput, and peak RSS. +- `docs/DEMO.md` is ready; recording/upload and its URL remain human actions. + +## QLT-03 evidence + +- 15 Node verification, fuzz, and benchmark tests passed. +- All Go packages passed tests and vet; package coverage is recorded in + `COMPATIBILITY.md` without claiming JavaScript coverage parity. +- `rg -n '\bunsafe\b' src -g '*.go'` returned zero source occurrences. +- Deno source suite passed 45, failed 0, ignored 1. +- `git diff --check` passed, source diff was empty, and benchmark workload hash + matched the committed normalized SHA-256. + +## External checkpoint + +- Public repository: pending unauthenticated verification. +- Exact final-commit GitHub Actions run: pending push and observation. +- Demo video: not supplied. + +Phase 5 remains open until public clone access and the exact successful CI run +are recorded. The missing video URL remains explicitly unclaimed. From 7f0e925495abd6fca9bf89441dde7cef19ce940a Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 11:35:47 +0530 Subject: [PATCH 149/165] docs(05): close delivery evidence phase --- .planning/PROJECT.md | 2 +- .planning/ROADMAP.md | 11 +++-- .planning/STATE.md | 42 ++++++++++--------- .../05-delivery-evidence/05-VERIFICATION.md | 17 ++++---- COMPATIBILITY.md | 4 +- README.md | 1 + 6 files changed, 45 insertions(+), 32 deletions(-) diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index 0a9a06d1..6a74041e 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -22,4 +22,4 @@ the Go port is accepted only when exact differential evidence agrees. - Target source: local `mod.js` / `test.js`; MIT attribution retained. - Target language: Go 1.26+; standard library first. -- Node is available as local oracle runtime; Deno is currently unavailable. +- Node and Deno are available as local oracle runtimes. diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index fe25d974..dffc3cd6 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -12,7 +12,7 @@ through parsing and observable output, to operations and delivery proof. - [x] **Phase 2: Parsing and Color State** - Match accepted inputs and normalized TinyColor state. - [x] **Phase 3: Conversion and Representation** - Match conversion, output, and analysis behavior. - [x] **Phase 4: Operations and Palettes** - Match manipulation, readability, and color-combination behavior. -- [ ] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. +- [x] **Phase 5: Delivery Evidence** - Ship the CLI, CI, benchmarks, and submission documentation. ## Phase Details @@ -78,7 +78,12 @@ Plans: 1. CI runs format, vet, Go tests, and the differential smoke report. 2. The CLI supports parse, convert, lighten, palette, and contrast, including JSON output. 3. Benchmarks, documentation, license attribution, team work, and known differences are complete. -**Plans**: TBD +**Plans**: 3 plans (complete 2026-08-01) + +Plans: +- [x] 05-01: Add the human CLI, reproducible build/verification gate, Docker artifact, and CI. +- [x] 05-02: Publish differential fuzz and shared-workload benchmark evidence. +- [x] 05-03: Publish judge-facing documentation and verify local/public delivery evidence. ## Progress @@ -88,4 +93,4 @@ Plans: | 2. Parsing and Color State | 2/2 | Complete | 2026-08-01 | | 3. Conversion and Representation | 2/2 | Complete | 2026-08-01 | | 4. Operations and Palettes | 6/6 | Complete | 2026-08-01 | -| 5. Delivery Evidence | 0/TBD | Not started | - | +| 5. Delivery Evidence | 3/3 | Complete | 2026-08-01 | diff --git a/.planning/STATE.md b/.planning/STATE.md index ee8e67d8..7e7e85e9 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,34 +4,38 @@ ## Current Position -- Phase 5 of 5: Delivery Evidence -- Plan 05-02, Task 3 of 3: shared-workload benchmark not started -- Plans 1–4 are complete; Phase 5 Plan 05-01 and fuzz Tasks 05-02-01/02 are complete. +- Phase 5 of 5: Delivery Evidence — complete. +- All three Phase 5 plans and all milestone implementation plans are complete. +- Remaining human submission action: record, upload, and link the five-minute demo video. ## Verified Evidence +- Public repository: https://github.com/rajeet-04/TinyColor +- Unauthenticated `ls-remote` returned branch `rajeet` at + `1c218b669d192e37b7019b08395cf348410dde79`. +- GitHub Actions run 30686980719 passed `make verify`, Deno, build, and artifact + upload for that exact commit: + https://github.com/rajeet-04/TinyColor/actions/runs/30686980719 - Immutable JavaScript oracle hashes: 3/3 verified. -- Exact corpora: smoke 9/9, HEX/RGB 26/26, parser 23/23, conversion 35/35, operations 71/71. -- Go tests and vet pass; Node adapter, verifier, harness, and log-validator tests pass. +- Exact corpora: smoke 9/9, HEX/RGB 26/26, parser 23/23, conversion 35/35, + operations 71/71; 164/164 total and zero mismatches. +- Go tests/vet and 15 Node tests pass. - Immutable Deno source suite: 45 passed, 0 failed, 1 ignored. -- Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. -- GitHub Actions run 30675374085 passed verification, Deno, build, and artifact upload. +- Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, + zero divergences. +- Shared benchmark: 20 cold starts and 1,000 persistent requests per runtime, + with startup p99, latency p99, throughput, and peak RSS recorded. +- Go `unsafe` source occurrences: 0. ## Recent Decisions - Use the pinned V8-derived 8-bit luminance transfer table for exact `Math.pow` parity. -- Apply TinyColor's sub-one RGB rounding once at the `FromCompat` constructor boundary. -- Keep fuzz inputs broad and comparisons exact; never replace divergences with tolerance. - -## Remaining - -- Implement honest same-host startup p99, latency p99, throughput, and peak RSS benchmarks. -- Complete Plan 05-03 judge-facing documentation and final evidence refresh. -- Verify the pushed GitHub Actions run and public clone workflow. -- Human action: record and publish the five-minute demo video. +- Apply TinyColor's sub-one RGB rounding once at the `FromCompat` boundary. +- Keep fuzz comparisons exact and normalize benchmark workload newlines before hashing. +- Never claim external evidence until public access and exact-commit CI are observed. ## Session Continuity -Last session: 2026-08-01T00:20:50.023Z -Stopped at: Plan 05-02 Task 3, ready to implement benchmarks with TDD. -Resume file: `.planning/phases/05-delivery-evidence/.continue-here.md` +Last session: 2026-08-01 +Stopped at: Phase 5 complete; demo video remains a human submission action. +Resume file: none diff --git a/.planning/phases/05-delivery-evidence/05-VERIFICATION.md b/.planning/phases/05-delivery-evidence/05-VERIFICATION.md index 29382550..d0316953 100644 --- a/.planning/phases/05-delivery-evidence/05-VERIFICATION.md +++ b/.planning/phases/05-delivery-evidence/05-VERIFICATION.md @@ -1,6 +1,6 @@ --- phase: 5 -status: locally-verified +status: verified verified: 2026-08-01 requirements: [DEL-01, QLT-03] --- @@ -17,7 +17,7 @@ local evidence from external actions. | Criterion | Status | Evidence | |---|---|---| -| CI runs format, vet, Go tests, and differential checks | Configured; exact final run pending | `.github/workflows/port.yml`, `Makefile`; local constituent commands passed. | +| CI runs format, vet, Go tests, and differential checks | Passed | [Run 30686980719](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719) passed for exact commit `1c218b669d192e37b7019b08395cf348410dde79`. | | CLI supports parse, convert, lighten, palette, contrast, and JSON | Local pass | `make build` equivalent plus all five JSON invocations exited zero; implementation under `src/cmd/tinycolor-compat`. | | Benchmarks, docs, attribution, ownership, and known differences are complete | Local pass | `bench/results.json`, `bench/methodology.md`, `README.md`, `LICENSE`, `DECISIONS.md`, `COMPATIBILITY.md`, `docs/TEAM-OWNERSHIP.md`. | @@ -45,9 +45,12 @@ local evidence from external actions. ## External checkpoint -- Public repository: pending unauthenticated verification. -- Exact final-commit GitHub Actions run: pending push and observation. -- Demo video: not supplied. +- Public repository: https://github.com/rajeet-04/TinyColor +- Unauthenticated `git ls-remote` returned branch `rajeet` at exact commit + `1c218b669d192e37b7019b08395cf348410dde79`. +- Exact-commit CI: [GitHub Actions run 30686980719](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719) + passed `make verify`, `deno test test.js`, `make build`, and artifact upload. +- Demo video: not supplied and explicitly unclaimed. -Phase 5 remains open until public clone access and the exact successful CI run -are recorded. The missing video URL remains explicitly unclaimed. +Phase 5 delivery evidence is verified. The demo recording remains the only +human submission action. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 54f57dba..33d32016 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -66,8 +66,8 @@ observations, not universal speedup claims. - Differential Fuzz Survivor: eligible from the validated 60-second log. - Zero Unsafe: eligible from zero Go source occurrences. - Decision Log: eligible from 15 substantive decisions. -- GitHub Actions: configured; the final exact-commit run is not yet recorded here. -- Public repository visibility: not yet verified from an unauthenticated clone in this closeout. +- GitHub Actions: [run 30686980719](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719) passed the full gate, Deno, build, and artifact upload for commit `1c218b6`. +- Public repository visibility: unauthenticated `git ls-remote https://github.com/rajeet-04/TinyColor.git refs/heads/rajeet` returned exact commit `1c218b6`. - Five-minute demo video: not supplied. No known mismatch remains in the fixed corpus or recorded fuzz session. diff --git a/README.md b/README.md index e3f51de8..ac488d95 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Evidence: - [Benchmark results](bench/results.json) and [methodology](bench/methodology.md) — same-host startup p99, latency p99, throughput, and peak RSS. - [Architectural decisions](DECISIONS.md), [architecture](docs/ARCHITECTURE.md), [testing](docs/TESTING.md), and [team ownership](docs/TEAM-OWNERSHIP.md). - [Five-minute demo script](docs/DEMO.md). +- [Successful exact-commit CI run](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719). ## Repository layout From 1f22aa2d62ae95a16019a50eb8eeb8f2a8250bc2 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 12:21:57 +0530 Subject: [PATCH 150/165] fix(compat): preserve formats and percent inputs --- src/cmd/tinycolor-compat/main.go | 3 ++- src/cmd/tinycolor-compat/main_test.go | 10 ++++++++++ src/internal/parser/parser.go | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 0f9ef3ea..197990e9 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -237,7 +237,8 @@ func handle(request compat.Request) compat.Response { return response } if request.Operation == "string" { - response, _ := compat.Success(request.ID, color.String()) + format, _ := args["format"].(string) + response, _ := compat.Success(request.ID, color.ToString(format)) return response } switch request.Operation { diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index c5449495..962c8d8e 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -112,6 +112,16 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { stdin: "{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", wantStdout: "{\"id\":\"red\",\"result\":{\"alpha\":1,\"format\":\"name\",\"original\":\"red\",\"rgb\":{\"a\":1,\"b\":0,\"g\":0,\"r\":255},\"valid\":true,\"value\":\"red\"}}\n", }, + { + name: "JSONL string forwards explicit format", + stdin: "{\"id\":\"string-format\",\"operation\":\"string\",\"input\":\"red\",\"args\":{\"format\":\"hsl\"}}\n", + wantStdout: "{\"id\":\"string-format\",\"result\":\"hsl(0, 100%, 50%)\"}\n", + }, + { + name: "JSONL inspect preserves one-percent HSL channels", + stdin: "{\"id\":\"hsl-one-percent\",\"operation\":\"inspect\",\"input\":\"hsl(115, 1%, 1%)\"}\n", + wantStdout: "{\"id\":\"hsl-one-percent\",\"result\":{\"alpha\":1,\"format\":\"hsl\",\"original\":\"hsl(115, 1%, 1%)\",\"rgb\":{\"a\":1,\"b\":3,\"g\":3,\"r\":3},\"valid\":true,\"value\":\"hsl(115, 1%, 1%)\"}}\n", + }, { name: "malformed JSONL request does not stop the stream", stdin: "{bad json\n{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", diff --git a/src/internal/parser/parser.go b/src/internal/parser/parser.go index 495cf5a1..f5c79ede 100644 --- a/src/internal/parser/parser.go +++ b/src/internal/parser/parser.go @@ -172,7 +172,7 @@ func hsvModel(h, s, v, alpha any, hasAlpha bool, original any) color.Model { } func ratio(value any) float64 { - if number := color.ParseFloat(value); !math.IsNaN(number) && number <= 1 { + if number := color.ParseFloat(value); !color.IsPercentage(value) && !math.IsNaN(number) && number <= 1 { value = formatPercent(number*100) + "%" } return color.Bound01(value, 100) From ca0acd53497e6d2793ab078eb63e31f0877f90f5 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 12:22:19 +0530 Subject: [PATCH 151/165] fix(tinycolor): match percentage output and equality --- src/tinycolor/color.go | 20 ++++++++++++++++---- src/tinycolor/color_test.go | 13 +++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 4c7a8e5a..a1f58c7e 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -85,8 +85,7 @@ func (c Color) ToRGBString() string { return fmt.Sprintf("rgba(%d, %d, %d, %s)", x.R, x.G, x.B, roundedAlpha(x.A)) } func (c Color) ToPercentageRGB() RGB { - x := c.ToRGB() - return RGB{percent(x.R), percent(x.G), percent(x.B), x.A} + return RGB{percent(c.model.R), percent(c.model.G), percent(c.model.B), c.model.A} } func (c Color) ToPercentageRGBString() string { x := c.ToPercentageRGB() @@ -238,13 +237,26 @@ func (c Color) IsDark() bool { return c.Brightness() < 128 } func (c Color) IsLight() bool { return !c.IsDark() } func (c Color) Clone() Color { n, _ := FromCompat(c.String(), false); return n } func Equals(a, b any) bool { - if a == nil || b == nil { + if !jsTruthy(a) || !jsTruthy(b) { return false } x, _ := FromCompat(a, false) y, _ := FromCompat(b, false) return x.ToRGBString() == y.ToRGBString() } +func jsTruthy(value any) bool { + switch value := value.(type) { + case nil: + return false + case bool: + return value + case string: + return value != "" + default: + number := color.ParseFloat(value) + return math.IsNaN(number) || number != 0 + } +} func Random() Color { return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatRGB}} } @@ -302,7 +314,7 @@ func (c Color) Inspect() map[string]any { return map[string]any{"valid": c.Valid(), "format": format, "alpha": c.Alpha(), "rgb": c.RGB(), "value": c.String(), "original": c.Original()} } -func percent(channel int) int { return int(math.Round(float64(channel) / 255 * 100)) } +func percent(channel float64) int { return mathRound(color.Bound01(channel, 255) * 100) } func roundedAlpha(alpha float64) string { return strconv.FormatFloat(math.Round(alpha*100)/100, 'f', -1, 64) } diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 311ec7db..44d7797b 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -37,6 +37,19 @@ func TestStringHexFilterAndConversion(t *testing.T) { } } +func TestPercentageRGBUsesUnroundedChannels(t *testing.T) { + c, _ := FromCompat(map[string]any{"h": float64(266), "s": float64(255), "v": float64(342), "a": float64(2.63)}, false) + if got := c.ToPercentageRGBString(); got != "rgb(43%, 0%, 100%)" { + t.Fatalf("ToPercentageRGBString() = %q", got) + } +} + +func TestEqualsRejectsFalsyInput(t *testing.T) { + if Equals("", map[string]any{"h": float64(529), "s": float64(15), "l": float64(-77), "a": float64(-1.132)}) { + t.Fatal("empty input must not equal normalized black") + } +} + func TestImplicitAlphaHexFallsBackToRGBA(t *testing.T) { for _, input := range []string{"#f008", "#ff000080"} { c, _ := FromCompat(input, false) From e672f609ea00438f441cc6c777b521f54c920e71 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 12:22:31 +0530 Subject: [PATCH 152/165] fix(tinycolor): brighten rounded RGB snapshots --- src/tinycolor/color.go | 7 ++++--- src/tinycolor/operations_test.go | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index a1f58c7e..c9a17797 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -393,9 +393,10 @@ func (c *Color) Greyscale() *Color { return c.Desaturate(100) } func (c *Color) Brighten(amount float64) *Color { delta := float64(-mathRound(-255 * amount / 100)) - c.model.R = clampChannel(c.model.R + delta) - c.model.G = clampChannel(c.model.G + delta) - c.model.B = clampChannel(c.model.B + delta) + rgb := c.ToRGB() + c.model.R = clampChannel(float64(rgb.R) + delta) + c.model.G = clampChannel(float64(rgb.G) + delta) + c.model.B = clampChannel(float64(rgb.B) + delta) return c } diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 24c20323..314e5e38 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -102,6 +102,13 @@ func TestSaturateMatchesObjectConversionRounding(t *testing.T) { } } +func TestBrightenUsesRoundedRGBSnapshot(t *testing.T) { + color, _ := FromCompat("rgb(423.5294117647059%, 78.03921568627452%, -0.3921568627450981%)", false) + if got := color.Brighten(12.7).ToPercentageRGBString(); got != "rgb(67%, 91%, 13%)" { + t.Fatalf("Brighten(12.7) = %s", got) + } +} + func TestMix(t *testing.T) { black, _ := FromCompat("#000", false) white, _ := FromCompat("#fff", false) From db5bfb48a6597e7e6d3e3bc5825cff99e51e6538 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 12:23:55 +0530 Subject: [PATCH 153/165] docs(debug): record full-range parity fixes --- .planning/debug/knowledge-base.md | 8 + ...full-range-differential-inconsistencies.md | 144 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 .planning/debug/resolved/full-range-differential-inconsistencies.md diff --git a/.planning/debug/knowledge-base.md b/.planning/debug/knowledge-base.md index 162375a4..b591de51 100644 --- a/.planning/debug/knowledge-base.md +++ b/.planning/debug/knowledge-base.md @@ -11,3 +11,11 @@ Resolved debug sessions. Used by `gsd-debugger` to surface known-pattern hypothe - **Fix:** Route setHSL through the existing hslColor/parser conversion and remove the duplicate direct converter. - **Files changed:** src/tinycolor/color.go, src/tinycolor/operations_test.go, compat/cases/operations.jsonl --- + +## full-range-differential-inconsistencies — Broad adapter parity diverged across formatting, parsing, equality, and brighten +- **Date:** 2026-08-01 +- **Error patterns:** 6019 mismatches, string format hsl, hsl 1%, white, percentage RGB, equals empty input, brighten +- **Root cause:** Five shared-path gaps: string format was discarded; explicit percent strings were re-promoted as ratios; percentage RGB used pre-rounded channels; Equals omitted JavaScript falsy guards; Brighten skipped the oracle's rounded RGB snapshot. +- **Fix:** Forward string format; preserve explicit percentages; calculate percentage RGB from internal floats; apply JS truthiness before equality parsing; start Brighten from ToRGB channels. +- **Files changed:** src/cmd/tinycolor-compat/main.go, src/cmd/tinycolor-compat/main_test.go, src/internal/parser/parser.go, src/tinycolor/color.go, src/tinycolor/color_test.go, src/tinycolor/operations_test.go +--- diff --git a/.planning/debug/resolved/full-range-differential-inconsistencies.md b/.planning/debug/resolved/full-range-differential-inconsistencies.md new file mode 100644 index 00000000..51434799 --- /dev/null +++ b/.planning/debug/resolved/full-range-differential-inconsistencies.md @@ -0,0 +1,144 @@ +--- +status: resolved +trigger: "A deterministic 100,000-case Node-vs-Go stress run found 6,019 exact mismatches; fix known string-format and HSL percentage parser repros test-first, then classify remaining mismatches one root cause at a time." +created: 2026-08-01T11:57:15.5165643+05:30 +updated: 2026-08-01T12:17:02.4360622+05:30 +--- + +## Current Focus + +hypothesis: Confirmed resolved. +test: Completed focused RED/GREEN regressions, full repository checks, and repeated 100,000-case differential verification. +expecting: Zero mismatches and no test regressions. +next_action: Archive the resolved session and return the root-cause handoff without committing. + +## Symptoms + +expected: Exact JSON behavioral parity between compat/js-runner.mjs and the Go adapter for supported TinyColor public operations, including valid percentage strings and explicit toString formats. +actual: 6,019 mismatches in 100,000 seeded broad cases; known exact repros are `string` with input `red` and `args.format=hsl` returning `red` in Go versus `hsl(0, 100%, 50%)` in JS, and `inspect` with `hsl(115, 1%, 1%)` returning white in Go versus `rgb(3, 3, 3)` in JS. Initial distribution: string 5,341; inspect 35; fromRatio 43; output 41; analysis 26; equals 38; clone 45; modify 108; mix 90; readability 73; isReadable 60; mostReadable 82; palette 37; randomInvariant 0. +errors: Exact output mismatch, no crash. Known adapter branch at src/cmd/tinycolor-compat/main.go ignores args.format. Known parser ratio helper at src/internal/parser/parser.go may convert string `1%` into 100% because ParseFloat(`1%`) is 1. +reproduction: Build bin/tinycolor.exe, compare JSONL through `node compat/js-runner.mjs` and the Go binary. Minimal requests: `{"id":"string-format","operation":"string","input":"red","args":{"format":"hsl"}}` and `{"id":"hsl-one-percent","operation":"inspect","input":"hsl(115, 1%, 1%)"}`. Broad runner exists at C:\tmp\tinycolor-stress.mjs and was run from repo root. +started: Found today after narrower Phase 5 fuzz passed; likely pre-existing broad-domain gaps. + +## Eliminated + +- hypothesis: Correcting percentage RGB output would eliminate all remaining string/output/modify mismatches. + evidence: The next 100,000-case run eliminated string and output mismatches but retained 12 modify-only cases, all represented by brighten examples; modify has another independent root cause. + timestamp: 2026-08-01T12:07:40.3112231+05:30 +- hypothesis: TinyColor equality rejects all invalid parsed colors before comparison. + evidence: The immutable oracle only checks JavaScript top-level falsiness (`!color1 || !color2`) and then compares normalized RGB strings; truthy invalid colors may still compare equal as black. + timestamp: 2026-08-01T12:08:56.2349809+05:30 + +## Evidence + +- timestamp: 2026-08-01T11:58:08.3727772+05:30 + checked: `.planning/debug/knowledge-base.md` for overlap with mismatch, format, percentage, parser, and HSL symptoms. + found: The only entry concerns saturate rounding and has fewer than two overlapping symptom keywords. + implication: There is no qualifying known-pattern candidate; investigate the supplied repros directly. +- timestamp: 2026-08-01T11:58:08.3727772+05:30 + checked: Repository status before investigation. + found: Only the newly created debug session is untracked; no pre-existing user code edits are present. + implication: Subsequent source/test diffs can be attributed to this investigation while preserving the debug file separately. +- timestamp: 2026-08-01T11:58:39.5190467+05:30 + checked: Complete `src/cmd/tinycolor-compat/main.go` and its tests. + found: `handle` parses both inspect and string identically, but the string fast path calls `color.String()` and never reads `args["format"]`; the output/toString path correctly calls `color.ToString(format)`. + implication: The string-format mismatch is caused by a local adapter argument-loss defect, not TinyColor formatting itself. +- timestamp: 2026-08-01T11:58:39.5190467+05:30 + checked: Complete `src/internal/parser/parser.go`, `parser_test.go`, and `hsl.go`. + found: Shared `ratio` calls `ParseFloat` and promotes every numeric value `<= 1` to percentage text, including the literal string `"1%"`; `hslModel` and `hsvModel` both depend on it. + implication: The parser cannot distinguish normalized numeric ratios from already-explicit percentage strings and can turn 1% into 100% across HSL and HSV paths. +- timestamp: 2026-08-01T11:59:25.1113451+05:30 + checked: All `ratio` and `ToString` references plus complete `object.go`, `bounds.go`, `color.go`, `compat/js-runner.mjs`, and the broad stress generator. + found: `ratio` has exactly four production callers (HSL saturation/lightness and HSV saturation/value). `Bound01` already handles explicit `%` correctly. The Go adapter `string` branch and JS oracle both receive the same args, but only JS forwards `format`; the public Go `ToString(format)` behavior already exists and is tested. + implication: The smallest first fix is one adapter argument forward; the parser fix should preserve `ratio` for raw numbers while skipping its numeric-promotion step for percentage strings. +- timestamp: 2026-08-01T12:00:20.6632996+05:30 + checked: New `JSONL string forwards explicit format` regression before production changes. + found: RED as predicted: actual result was `red`; expected `hsl(0, 100%, 50%)`. + implication: The regression reproduces the supplied public mismatch and isolates the adapter string branch. +- timestamp: 2026-08-01T12:01:04.3955140+05:30 + checked: Focused adapter regression after forwarding `args.format` to `Color.ToString`. + found: GREEN: `go test ./cmd/tinycolor-compat -run TestRunJSONLAndUsageErrors -count=1` passed. + implication: The one-path fix corrects explicit string formats while preserving the existing JSONL adapter cases. +- timestamp: 2026-08-01T12:02:47.4046947+05:30 + checked: JS oracle and new exact JSONL regression for `hsl(115, 1%, 1%)` before parser changes. + found: Oracle returns RGB 3,3,3 and value `hsl(115, 1%, 1%)`; RED Go result is RGB 255,255,255 and `hsl(0, 0%, 100%)`. + implication: The regression exactly reproduces the supplied full-stack mismatch. +- timestamp: 2026-08-01T12:02:47.4046947+05:30 + checked: Immutable oracle `hslToRgb`, `hsvToRgb`, `bound01`, and `convertToPercentage` functions. + found: String HSL/HSV percentages go directly to `bound01`; object inputs use `convertToPercentage`, whose JavaScript `<=` coercion does not convert strings containing `%`. Go's shared `ratio` combines both paths but converts percentage strings incorrectly. + implication: Guarding the promotion with `!color.IsPercentage(value)` reproduces the oracle distinction without splitting the parser paths. +- timestamp: 2026-08-01T12:03:31.1535483+05:30 + checked: Exact HSL one-percent regression after guarding explicit percentages. + found: GREEN: `go test ./cmd/tinycolor-compat -run TestRunJSONLAndUsageErrors/JSONL_inspect_preserves_one-percent_HSL_channels -count=1` passed. + implication: The smallest parser change fixes the supplied public repro. +- timestamp: 2026-08-01T12:04:19.5752472+05:30 + checked: Unchanged deterministic 100,000-case broad stress after the two fixes. + found: Mismatches fell from 6,019 to 121. New distribution is string 1, output 6, equals 35, modify 79, and zero in every other operation. String/output/modify examples have identical rounded RGB but percentage strings differ by one point; equals examples compare invalid empty input against a different input that normalizes to black. + implication: The residuals collapse cleanly into two candidate root causes rather than requiring an architectural change. +- timestamp: 2026-08-01T12:05:16.6121994+05:30 + checked: All Go percentage-output callers, complete `color_test.go`, and immutable oracle `toPercentageRgb`/`toPercentageRgbString`. + found: Go `ToPercentageRGB` first calls `ToRGB`, rounding each internal channel to an integer; JavaScript applies `bound01` and percentage rounding directly to each internal float channel. All Go percentage strings route through `ToPercentageRGB`. + implication: A shared two-line correction in `ToPercentageRGB` should eliminate percentage string differences across direct output, string formatting, and modified-color inspections. +- timestamp: 2026-08-01T12:05:58.0357035+05:30 + checked: New direct percentage-output regression before production changes. + found: RED as predicted: Go returned `rgb(44%, 0%, 100%)`; expected `rgb(43%, 0%, 100%)`. + implication: Integer RGB pre-rounding is directly observable and sufficient to cause the residual output cluster. +- timestamp: 2026-08-01T12:06:37.9971179+05:30 + checked: Focused percentage-output regression after computing from internal channels. + found: GREEN: `go test ./tinycolor -run TestPercentageRGBUsesUnroundedChannels -count=1` passed. + implication: The shared output method now matches the oracle example; broad verification can test all three affected operation families. +- timestamp: 2026-08-01T12:07:40.3112231+05:30 + checked: Deterministic 100,000-case stress after the percentage-output fix. + found: Mismatches fell from 121 to 47: equals 35 and modify 12; every other operation is exact. All string/output mismatches and 67 of 79 modify mismatches disappeared. + implication: Percentage output is confirmed across its consumers; invalid equality and brighten behavior remain separate clusters. +- timestamp: 2026-08-01T12:08:56.2349809+05:30 + checked: Every `Equals` caller and complete immutable oracle equality function. + found: JS returns false for any falsy top-level argument, including empty string; Go checks only `nil` and otherwise compares `ToRGBString`, so empty string becomes invalid black and can equal a clamped black input. + implication: Equality needs JSON-compatible JS truthiness at its shared public entry point, not validity checks in callers. +- timestamp: 2026-08-01T12:10:06.1947734+05:30 + checked: New exact equality stress regression before production changes. + found: RED: Go reported the empty input equal to the clamped-black HSL object. + implication: The missing falsy guard directly reproduces the full equality cluster mechanism. +- timestamp: 2026-08-01T12:11:07.7954806+05:30 + checked: Focused equality regression after adding JSON-compatible falsy handling. + found: GREEN: `go test ./tinycolor -run TestEqualsRejectsFalsyInput -count=1` passed. + implication: Empty string is now rejected before invalid-as-black comparison at the shared equality entry point. +- timestamp: 2026-08-01T12:11:45.4137675+05:30 + checked: Deterministic 100,000-case stress after the equality fix. + found: Exactly 12 mismatches remain, all in modify and both captured examples use brighten; every other operation is exact, including equals. + implication: Equality is broadly confirmed. Brighten is the only remaining behavior path. +- timestamp: 2026-08-01T12:12:38.0636370+05:30 + checked: Complete oracle constructor, `_applyModification`, and `brighten`, plus every Go Brighten caller and complete modifier tests. + found: Oracle brighten calls `tinycolor(color).toRgb()` before applying its rounded delta, forcing every starting channel to an integer. Go applies the same delta rounding directly to `model` floats. `_applyModification` then copies the oracle result channels back while preserving original format metadata. + implication: Starting Brighten from Go's existing `ToRGB()` snapshot matches the oracle mechanism and explains both the percentage and achromatic-hue examples. +- timestamp: 2026-08-01T12:13:14.6239668+05:30 + checked: New exact brighten stress regression before production changes. + found: RED as predicted: Go returned `rgb(68%, 91%, 13%)`; expected `rgb(67%, 91%, 13%)`. + implication: Retaining the pre-brighten fractional channel reproduces the final residual mechanism. +- timestamp: 2026-08-01T12:13:55.0190481+05:30 + checked: Focused brighten regression after applying the delta to `ToRGB()` channels. + found: GREEN: `go test ./tinycolor -run TestBrightenUsesRoundedRGBSnapshot -count=1` passed. + implication: The final identified root cause is corrected; broad verification can now test completeness. +- timestamp: 2026-08-01T12:15:21.2484424+05:30 + checked: Final deterministic 100,000-case stress after all five fixes. + found: Zero mismatches in every operation category. + implication: The original 6,019-case parity failure is eliminated across the unchanged broad input domain. +- timestamp: 2026-08-01T12:15:21.2484424+05:30 + checked: `go test ./... -count=1`, `go vet ./...`, `node --test tests/port/adapter.test.mjs`, `gofmt`, and `git diff --check`. + found: All checks passed; diff check emitted only existing Windows LF-to-CRLF conversion warnings. + implication: No test, vet, adapter, formatting, or whitespace regression was detected. +- timestamp: 2026-08-01T12:16:16.8806534+05:30 + checked: Full verification after simplifying equality truthiness through existing `color.ParseFloat`. + found: `go test ./... -count=1`, `go vet ./...`, adapter tests, and `git diff --check` passed; the repeated 100,000-case stress again reported zero mismatches in every category. + implication: The final minimal diff is stable under focused, repository-wide, and broad differential checks. +- timestamp: 2026-08-01T12:17:02.4360622+05:30 + checked: Human/primary-agent verification checkpoint. + found: The broad stress and focused checks were accepted as green, with explicit instruction to finalize the session as resolved without commits. + implication: The session can be archived; source and planning changes remain uncommitted for primary-agent micro-commit review. + +## Resolution + +root_cause: Five independent shared-path gaps caused all 6,019 mismatches: string format was discarded; explicit percent strings were re-promoted as ratios; percentage RGB used pre-rounded channels; Equals omitted JavaScript falsy guards; Brighten skipped the oracle's rounded RGB snapshot. +fix: Forward string format; preserve explicit HSL/HSV percentages; calculate percentage RGB from internal floats; apply JS truthiness before equality parsing; start Brighten from `ToRGB()` channels. +verification: Each root cause received a regression that failed before its fix and passed afterward. The final deterministic 100,000-case run passed three times with zero mismatches. Full Go tests, Go vet, 164/164 compatibility cases, port adapter tests, immutable Deno tests (45 passed, 0 failed, 1 ignored), gofmt, build, and diff checks passed. +files_changed: [src/cmd/tinycolor-compat/main.go, src/cmd/tinycolor-compat/main_test.go, src/internal/parser/parser.go, src/tinycolor/color.go, src/tinycolor/color_test.go, src/tinycolor/operations_test.go] From 162ea1538d00f2179ad2d49aeff7c88fcd1dabc7 Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 13:58:34 +0530 Subject: [PATCH 154/165] fix(compat): forward fromRatio options --- src/cmd/tinycolor-compat/main.go | 4 +++- src/cmd/tinycolor-compat/main_test.go | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 197990e9..e4e817ab 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -224,7 +224,9 @@ func handle(request compat.Request) compat.Response { case "inspect", "string": color, err = tinycolor.FromCompat(request.Input, false) case "fromRatio": - color, err = tinycolor.FromCompat(request.Input, true) + format, _ := args["format"].(string) + gradientType, _ := args["gradientType"].(bool) + color, err = tinycolor.FromCompatWithOptions(request.Input, true, tinycolor.CompatOptions{Format: format, GradientType: gradientType}) case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable", "palette": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) case "equals", "randomInvariant": diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index 962c8d8e..cee3ff63 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -117,6 +117,11 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { stdin: "{\"id\":\"string-format\",\"operation\":\"string\",\"input\":\"red\",\"args\":{\"format\":\"hsl\"}}\n", wantStdout: "{\"id\":\"string-format\",\"result\":\"hsl(0, 100%, 50%)\"}\n", }, + { + name: "JSONL fromRatio forwards explicit format", + stdin: "{\"id\":\"ratio-format\",\"operation\":\"fromRatio\",\"input\":{\"r\":1,\"g\":0,\"b\":0,\"a\":1},\"args\":{\"format\":\"hex\"}}\n", + wantStdout: "{\"id\":\"ratio-format\",\"result\":{\"alpha\":1,\"format\":\"hex\",\"original\":{\"a\":1,\"b\":\"0%\",\"g\":\"0%\",\"r\":\"100%\"},\"rgb\":{\"a\":1,\"b\":0,\"g\":0,\"r\":255},\"valid\":true,\"value\":\"#ff0000\"}}\n", + }, { name: "JSONL inspect preserves one-percent HSL channels", stdin: "{\"id\":\"hsl-one-percent\",\"operation\":\"inspect\",\"input\":\"hsl(115, 1%, 1%)\"}\n", From 052d2a2235a1dea65ecc92050083d08ace3a26ba Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Sat, 1 Aug 2026 13:58:34 +0530 Subject: [PATCH 155/165] fix(parser): match compressed reverse names --- src/internal/parser/names.go | 18 +++++++++--------- src/tinycolor/color_test.go | 10 ++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/internal/parser/names.go b/src/internal/parser/names.go index 48fc7ff3..7b986c64 100644 --- a/src/internal/parser/names.go +++ b/src/internal/parser/names.go @@ -19,25 +19,25 @@ var ( hexNames = func() map[string]string { result := make(map[string]string) for name, sourceHex := range names { - hex := sourceHex - if len(hex) == 3 { - hex = string([]byte{hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]}) - } - result[hex] = name + result[sourceHex] = name } - result["00ffff"] = "cyan" - result["ff00ff"] = "magenta" + result["0ff"] = "cyan" + result["f0f"] = "magenta" result["808080"] = "grey" result["a9a9a9"] = "darkgrey" result["2f4f4f"] = "darkslategrey" result["696969"] = "dimgrey" result["d3d3d3"] = "lightgrey" - result["778899"] = "lightslategrey" + result["789"] = "lightslategrey" result["708090"] = "slategrey" return result }() ) func NameForRGB(r, g, b int) string { - return hexNames[fmt.Sprintf("%02x%02x%02x", r, g, b)] + hex := fmt.Sprintf("%02x%02x%02x", r, g, b) + if hex[0] == hex[1] && hex[2] == hex[3] && hex[4] == hex[5] { + hex = string([]byte{hex[0], hex[2], hex[4]}) + } + return hexNames[hex] } diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 44d7797b..74bf8a5b 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -102,3 +102,13 @@ func TestInvalidInputIsAColorState(t *testing.T) { t.Fatalf("invalid input = %#v, %v", color, err) } } + +func TestCompressibleSixDigitNameFallsBackToHex(t *testing.T) { + color, _ := FromCompat("rebeccapurple", false) + if name, ok := color.ToName(); ok || name != "" { + t.Fatalf("ToName() = %q, %t", name, ok) + } + if color.String() != "#663399" || color.Clone().String() != "#663399" { + t.Fatalf("color = %q, clone = %q", color.String(), color.Clone().String()) + } +} From 3a8c4a7b3001f55f60e908fd9bbb8018afb1f7c6 Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 13:58:34 +0530 Subject: [PATCH 156/165] fix(tinycolor): match JavaScript numeric precision --- src/tinycolor/color.go | 10 ++++++++-- src/tinycolor/color_test.go | 7 +++++++ src/tinycolor/operations_test.go | 8 ++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index c9a17797..36b8037a 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -260,7 +260,13 @@ func jsTruthy(value any) bool { func Random() Color { return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatRGB}} } -func mathRound(v float64) int { return int(math.Floor(v + .5)) } +func mathRound(v float64) int { + floor := math.Floor(v) + if v-floor >= .5 { + floor++ + } + return int(floor) +} // String supplies the minimal source-compatible string snapshot used by the // JSONL oracle. Full public output APIs remain Phase 3 work. @@ -328,7 +334,7 @@ func rgbToHSL(r, g, b float64) (float64, float64, float64) { } d := max - min s := d / (2 - max - min) - if l < 0.5 { + if l <= 0.5 { s = d / (max + min) } var h float64 diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 74bf8a5b..8b4466a1 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -44,6 +44,13 @@ func TestPercentageRGBUsesUnroundedChannels(t *testing.T) { } } +func TestOnePercentRGBMatchesJavaScriptHSLRounding(t *testing.T) { + color, _ := FromCompat(map[string]any{"r": "1%", "g": "0%", "b": "0%", "a": float64(1)}, false) + if got := color.ToHSLString(); got != "hsl(0, 100%, 0%)" { + t.Fatalf("ToHSLString() = %q", got) + } +} + func TestEqualsRejectsFalsyInput(t *testing.T) { if Equals("", map[string]any{"h": float64(529), "s": float64(15), "l": float64(-77), "a": float64(-1.132)}) { t.Fatal("empty input must not equal normalized black") diff --git a/src/tinycolor/operations_test.go b/src/tinycolor/operations_test.go index 314e5e38..b5436d2c 100644 --- a/src/tinycolor/operations_test.go +++ b/src/tinycolor/operations_test.go @@ -293,6 +293,14 @@ func TestPaletteConversionUsesTinyColorChannelBounds(t *testing.T) { } } +func TestPaletteHSLMetadataMatchesJavaScriptPrecision(t *testing.T) { + color, _ := FromCompat("#967a", false) + original, ok := color.Complement().Original().(map[string]any) + if !ok || original["s"] != 0.19999999999999996 { + t.Fatalf("complement original = %#v", original) + } +} + func paletteHex(colors []Color) string { values := make([]string, len(colors)) for index, color := range colors { From 1212276db0e23d8f29ffd7f1fc1d6f71c5ee379a Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 13:58:34 +0530 Subject: [PATCH 157/165] docs(decisions): record analogous safety divergence --- DECISIONS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DECISIONS.md b/DECISIONS.md index 6221f0ad..a8773f44 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -17,3 +17,4 @@ | D-013 | Benchmark both adapters with the same fixed JSONL workload and lifecycle. | Shared inputs and same-host cold/persistent runs make the measurements directly reproducible. | Accepted | | D-014 | Normalize benchmark workload newlines before hashing. | Evidence hashes remain stable across Windows and Unix checkouts without changing request content. | Accepted | | D-015 | Publish unsupported or external deliverables as unverified. | Honest missing evidence is preferable to unreproducible CI, public-access, or video claims. | Accepted | +| D-016 | Return an empty typed Go palette for non-positive counts instead of reproducing TinyColor's non-terminating negative `analogous` loop. | Compatibility must not make a public Go call hang indefinitely; adapter zero values still retain TinyColor's documented defaults. | Accepted | From db28f015954765df0331b5d7aab2830faf5083f5 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 14:45:49 +0530 Subject: [PATCH 158/165] docs: design original-suite Go bridge --- ...6-08-01-original-suite-go-bridge-design.md | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-01-original-suite-go-bridge-design.md diff --git a/docs/superpowers/specs/2026-08-01-original-suite-go-bridge-design.md b/docs/superpowers/specs/2026-08-01-original-suite-go-bridge-design.md new file mode 100644 index 00000000..b2fc4134 --- /dev/null +++ b/docs/superpowers/specs/2026-08-01-original-suite-go-bridge-design.md @@ -0,0 +1,143 @@ +# Original Suite Against Go Bridge Design + +## Goal + +Run the pinned, byte-identical `test.js` suite against the compiled Go port and +report its exact pass, fail, and ignored counts. The checked-in JavaScript oracle +files remain unchanged and continue to match `tests/original/manifest.sha256`. + +## Hackathon interpretation + +The original suite is the test specification, not part of the port. A thin +JavaScript test facade may preserve JavaScript-only object identity, constructor, +and chaining semantics, but every color calculation must be performed by the +submitted native Go artifact. The facade must never import or execute the +original `mod.js` or `tinycolor.js` implementation. + +This is distinct from the prohibited pattern of a port shelling out to its source +implementation: the tests invoke the Go port. The shipped Go library and CLI do +not invoke JavaScript. + +## Architecture + +### Byte-identical test overlay + +`tests/original-go/run.mjs` will: + +1. Verify the existing kickoff manifest. +2. Build the native `tinycolor-compat` binary. +3. Create a temporary directory. +4. Copy root `test.js` into that directory without transforming it. +5. Verify the copied bytes have the same SHA-256 as root `test.js`. +6. Place the checked-in Go-backed facade beside it as `mod.js`. +7. run `deno test` on the copied `test.js` with permission to execute only the + compiled Go binary. +8. Remove the temporary directory after the child process exits. + +The suite therefore resolves its unchanged `import tinycolor from "./mod.js"` +to the test facade while the source test file itself stays byte-identical. + +### One-request native bridge + +The compatibility executable will gain a test-only one-request mode that accepts +one JSON request as a direct process argument and writes one JSON response to +stdout. The existing request decoder and `handle` path remain the single dispatch +implementation for persistent JSONL fuzzing and one-request suite calls. + +The facade will use `Deno.Command(...).outputSync()` without a shell. Each call +starts the compiled Go executable directly, so JavaScript's synchronous TinyColor +API remains synchronous. Requests are small enough to remain below normal command +line limits; malformed responses or non-zero exits fail the suite immediately +with the Go stderr attached. + +### JavaScript facade boundary + +`tests/original-go/mod.js` will expose the function/constructor shape expected by +the source suite. A facade color stores: + +- the original JavaScript input reference for `getOriginalInput()`; +- the current RGBA snapshot returned by Go; +- the source format and gradient option required for default string behavior. + +The facade itself may implement only JavaScript runtime semantics: + +- calling `tinycolor(existingFacade)` returns the same object; +- `new tinycolor(existingFacade)` returns the same object; +- instance modifiers mutate and return the same facade; +- `clone()` returns an independent facade; +- palette and static utility results are wrapped as facade instances. + +Parsing, normalization, formatting, alpha bounds, conversion, modification, +mixing, readability, and palette generation must come from Go bridge responses. +No TinyColor color algorithm may be reimplemented in the facade. + +## Go adapter coverage + +The existing compatibility operations will be reused and minimally extended for +source-suite methods that currently lack a result shape: + +- object outputs: RGB, percentage RGB, HSL, and HSV; +- alpha mutation; +- random-color snapshots; +- filter gradient options; +- any source-suite method discovered by the failing full-suite run. + +Extensions belong in the existing compatibility dispatcher and public Go color +methods. They must be independently covered by focused Go or Node adapter tests +before the facade uses them. + +## Test-driven delivery + +Implementation proceeds in narrow red-green cycles: + +1. A runner-integrity test fails until the copied `test.js` hash is proven equal. +2. A facade smoke test fails until a basic source assertion is served by Go. +3. The full source suite is run against the facade; each unsupported method or + mismatch becomes one focused regression before the minimum adapter extension. +4. The source suite must finish with 45 passed, 0 failed, and 1 upstream-ignored + `polyad` test. +5. `make verify` must also retain the existing oracle, Go, Node, fuzz-log, + compatibility-corpus, and vet gates. + +The root `test.js`, `mod.js`, and `tinycolor.js` hashes are checked before and +after the work. A changed oracle file is a hard failure. + +## Commands and CI + +`make test-original-go` will run the bridge-backed original suite. `make verify` +will include that target so GitHub Actions cannot pass while exercising only the +JavaScript oracle. The README and `COMPATIBILITY.md` will distinguish: + +- the original suite against original JavaScript; and +- the same byte-identical suite against the Go-backed facade. + +Both commands and exact counts will be published. GitHub Actions must pass at the +final pushed commit before the deliverable is marked complete. + +## Error handling + +- Missing Deno or Go exits with an actionable command failure. +- A binary exit, timeout, invalid JSON response, or response-ID mismatch fails the + current assertion instead of falling back to JavaScript. +- The temporary overlay is deleted in a `finally` path. +- No mismatch is hidden with floating-point tolerance or rewritten expectations. + +## Non-goals + +- Do not edit, patch, transpile, or regenerate `test.js`. +- Do not replace the production `mod.js` oracle. +- Do not add WebAssembly, FFI, native addons, or third-party dependencies. +- Do not reproduce TinyColor algorithms in JavaScript. +- Do not claim coverage for future upstream TinyColor revisions. + +## Acceptance criteria + +- All three kickoff oracle hashes remain unchanged. +- The copied source test has the same SHA-256 as root `test.js`. +- The facade never imports the original TinyColor implementation. +- Every observable color result used by the suite is returned by the Go binary. +- The bridge-backed run reports 45 passed, 0 failed, and 1 ignored. +- Existing verification and differential checks remain green. +- README, compatibility evidence, Makefile, and CI contain the reproducible + bridge-backed command. +- The final commit is pushed and its GitHub Actions run succeeds. From b8570892f5575d3d11dac9c381c2f2277b2f9242 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 15:01:18 +0530 Subject: [PATCH 159/165] docs: plan original-suite Go bridge --- .../2026-08-01-original-suite-go-bridge.md | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-01-original-suite-go-bridge.md diff --git a/docs/superpowers/plans/2026-08-01-original-suite-go-bridge.md b/docs/superpowers/plans/2026-08-01-original-suite-go-bridge.md new file mode 100644 index 00000000..ac3250d4 --- /dev/null +++ b/docs/superpowers/plans/2026-08-01-original-suite-go-bridge.md @@ -0,0 +1,389 @@ +# Original Suite Against Go Bridge Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Run the byte-identical original `test.js` suite against the native Go port with 45 passed, 0 failed, and 1 ignored. + +**Architecture:** A temporary overlay pairs an unchanged copy of `test.js` with a test-only synchronous JavaScript facade. The facade invokes a one-request mode of the existing Go compatibility binary directly through `Deno.Command`; it preserves JavaScript object identity while all color behavior comes from Go. + +**Tech Stack:** Go 1.26 standard library, Deno 2 standard APIs, Node.js test runner, GNU Make, GitHub Actions. + +## Global Constraints + +- Do not modify root `test.js`, `mod.js`, or `tinycolor.js`. +- Do not import or execute the original TinyColor implementation from the facade. +- Do not add dependencies, WebAssembly, FFI, native addons, or JavaScript color algorithms. +- Reuse the existing compatibility request decoder and dispatcher. +- Add one focused failing check before each production behavior change. +- Keep commits small and single-purpose. + +--- + +### Task 1: One-request Go process mode + +**Files:** +- Modify: `src/cmd/tinycolor-compat/main_test.go` +- Modify: `src/cmd/tinycolor-compat/main.go` + +**Interfaces:** +- Consumes: existing `compat.Decode`, `handle`, and `write` functions. +- Produces: `tinycolor bridge `, which writes exactly one encoded compatibility response. + +- [ ] **Step 1: Write the failing command test** + +Add a table row to `TestRunJSONLAndUsageErrors`: + +```go +{ + name: "one-request bridge", + args: []string{"bridge", `{"id":"bridge-red","operation":"output","input":"red","args":{"method":"toHexString"}}`}, + wantStdout: "{\"id\":\"bridge-red\",\"result\":\"#ff0000\"}\n", +}, +``` + +Add rows proving a missing request returns status 2 and malformed JSON returns a structured error without a panic. + +- [ ] **Step 2: Run the focused test and verify RED** + +Run: + +```powershell +$env:GOCACHE='R:\Code\TinyColor\.cache\go-build' +go -C src test ./cmd/tinycolor-compat -run TestRunJSONLAndUsageErrors -count=1 +``` + +Expected: FAIL because `bridge` is treated as an unknown command. + +- [ ] **Step 3: Add the minimum one-request path** + +Route `bridge` in `run` and decode its single argument through the existing protocol: + +```go +case "bridge": + if len(args) != 2 { + usage(stderr, "bridge ") + return 2 + } + request, err := compat.Decode([]byte(args[1])) + if err != nil { + response, _ := compat.Failure("", err.Error()) + write(response, stdout, stderr) + return 1 + } + write(handle(request), stdout, stderr) + return 0 +``` + +- [ ] **Step 4: Run focused and package tests and verify GREEN** + +```powershell +go -C src test ./cmd/tinycolor-compat -count=1 +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```powershell +git add src/cmd/tinycolor-compat/main.go src/cmd/tinycolor-compat/main_test.go +git commit -m "feat(compat): add one-request bridge mode" +``` + +--- + +### Task 2: Source-suite adapter operations + +**Files:** +- Modify: `src/tinycolor/color.go` +- Modify: `src/tinycolor/color_test.go` +- Modify: `src/internal/parser/names.go` +- Modify: `src/internal/parser/parser_test.go` +- Modify: `src/cmd/tinycolor-compat/main.go` +- Modify: `src/cmd/tinycolor-compat/main_test.go` + +**Interfaces:** +- Consumes: existing `Color` conversion, formatting, mutation, palette, and readability methods. +- Produces: bridge results for object conversions, compact hex, alpha mutation, random snapshots, gradient filters, and the Go-owned color-name map. + +- [ ] **Step 1: Write failing Go API tests for alpha and names** + +Add `TestSetAlphaMatchesTinyColorBounds` to `src/tinycolor/color_test.go`, asserting that `0.9` is retained and `-1`, `2`, `nil`, and `"test"` normalize to `1` while returning the same receiver. + +Add `TestNamesReturnsIndependentCopy` to `src/internal/parser/parser_test.go`, asserting `Names()["red"] == "ff0000"` and that mutating the returned map does not alter a second call. + +- [ ] **Step 2: Run focused tests and verify RED** + +```powershell +go -C src test ./tinycolor ./internal/parser -run 'Test(SetAlphaMatchesTinyColorBounds|NamesReturnsIndependentCopy)' -count=1 +``` + +Expected: build failure because `SetAlpha` and `Names` do not exist. + +- [ ] **Step 3: Implement the two minimal Go APIs** + +Add: + +```go +func (c *Color) SetAlpha(value any) *Color { + c.model.A = color.BoundAlpha(value) + return c +} +``` + +and an exported `parser.Names()` that returns a copied map using the standard `maps.Clone` function. + +- [ ] **Step 4: Add failing bridge result cases** + +Extend the command test table with exact requests for: + +- `output/toRgb`, `output/toPercentageRgb`, `output/toHsl`, and `output/toHsv`; +- compact `toHex`, `toHexString`, `toHex8`, `toHex8String`, `toString("hex3")`, and `toString("hex4")`; +- `setAlpha` returning the updated inspection; +- `random` returning a valid `prgb` inspection; +- `names` containing `red` and `rebeccapurple`; +- `toFilter` with `gradientType: true`. + +Run the focused command tests. Expected: FAIL with unsupported operation or method. + +- [ ] **Step 5: Extend the existing dispatcher only** + +Add `setAlpha`, `random`, and `names` cases to `handle`. Extend `output` with lowercase-keyed object maps and a boolean `compact` argument. Use one helper that compresses `rrggbb` or `rrggbbaa` only when every pair has equal digits. Pass `gradientType` to `ToFilter` rather than duplicating filter generation. + +The object shapes must be: + +```go +map[string]any{"r": rgb.R, "g": rgb.G, "b": rgb.B, "a": rgb.A} +map[string]any{"r": fmt.Sprintf("%d%%", rgb.R), "g": fmt.Sprintf("%d%%", rgb.G), "b": fmt.Sprintf("%d%%", rgb.B), "a": rgb.A} +map[string]any{"h": hsl.H, "s": hsl.S, "l": hsl.L, "a": hsl.A} +map[string]any{"h": hsv.H, "s": hsv.S, "v": hsv.V, "a": hsv.A} +``` + +- [ ] **Step 6: Run all Go and fixed compatibility tests** + +```powershell +gofmt -w src/tinycolor/color.go src/tinycolor/color_test.go src/internal/parser/names.go src/internal/parser/parser_test.go src/cmd/tinycolor-compat/main.go src/cmd/tinycolor-compat/main_test.go +go -C src test ./... -count=1 +node compat/run.mjs compat/cases/smoke.jsonl +node compat/run.mjs compat/cases/parser-hex-rgb.jsonl +node compat/run.mjs compat/cases/parser.jsonl +node compat/run.mjs compat/cases/conversion.jsonl +node compat/run.mjs compat/cases/operations.jsonl +``` + +Expected: all pass with zero mismatches. + +- [ ] **Step 7: Commit** + +```powershell +git add src +git commit -m "feat(compat): expose original-suite operations" +``` + +--- + +### Task 3: Byte-identical overlay and synchronous facade + +**Files:** +- Create: `tests/original-go/mod.js` +- Create: `tests/original-go/facade.test.mjs` +- Create: `tests/original-go/run.mjs` +- Create: `tests/original-go/run.test.mjs` + +**Interfaces:** +- Consumes: `TINYCOLOR_GO_BINARY`, `tinycolor bridge `, root `test.js`, and `tests/original/manifest.sha256`. +- Produces: a default-exported TinyColor-compatible facade and a runner that exits with Deno's source-suite status. + +- [ ] **Step 1: Write the failing runner integrity test** + +In `run.test.mjs`, import a planned `prepareOverlay()` and assert: + +```js +const overlay = await prepareOverlay(); +try { + assert.equal( + createHash("sha256").update(readFileSync(overlay.testFile)).digest("hex"), + createHash("sha256").update(readFileSync("test.js")).digest("hex"), + ); + assert.equal(readFileSync(overlay.testFile).equals(readFileSync("test.js")), true); +} finally { + overlay.cleanup(); +} +``` + +Run `node --test tests/original-go/run.test.mjs`. Expected: FAIL because the module does not exist. + +- [ ] **Step 2: Implement overlay preparation** + +Use `mkdtempSync`, `copyFileSync`, `copyFileSync` for the facade as `mod.js`, byte comparison, and `rmSync(..., { recursive: true, force: true })`. Call the existing hash verifier before copying. Export `prepareOverlay()` for the Node test. + +- [ ] **Step 3: Write the failing Deno facade smoke test** + +Build the binary, set `TINYCOLOR_GO_BINARY`, then assert through the facade: + +```js +assertEquals(tinycolor("red").toHexString(), "#ff0000"); +const color = tinycolor("red"); +assert(color.lighten(10) === color); +assertEquals(color.toHexString(), "#ff3333"); +assertEquals(tinycolor.fromRatio({ r: 1, g: 0, b: 0 }).toRgb(), { r: 255, g: 0, b: 0, a: 1 }); +``` + +Run the Deno smoke test. Expected: FAIL because the facade does not exist. + +- [ ] **Step 4: Implement the minimal facade transport and state wrapper** + +Implement `invoke(operation, input, args)` with direct `Deno.Command` execution, response-ID validation, and no shell. Implement input unwrapping and a facade constructor that stores original input, current RGBA, format, and gradient type from Go inspection responses. + +Provide all instance APIs exercised by `test.js`: + +```text +getOriginalInput getFormat getAlpha setAlpha isValid clone +toRgb toPercentageRgb toHsl toHsv +toRgbString toPercentageRgbString toHslString toHsvString +toHex toHexString toHex8 toHex8String toName toFilter toString +getBrightness getLuminance isDark isLight +lighten brighten darken saturate desaturate greyscale spin +complement analogous monochromatic splitcomplement triad tetrad +``` + +Provide static APIs: + +```text +fromRatio random equals mix readability isReadable mostReadable names +``` + +Modifiers replace the facade's current Go snapshot and return the same facade. Static and palette results wrap returned Go inspections. `names` comes from the Go `names` operation at module initialization. + +- [ ] **Step 5: Run the smoke test and verify GREEN** + +```powershell +$env:TINYCOLOR_GO_BINARY='R:\Code\TinyColor\bin\tinycolor.exe' +deno test --allow-run=$env:TINYCOLOR_GO_BINARY tests/original-go/facade.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```powershell +git add tests/original-go +git commit -m "test(original): add Go-backed TinyColor facade" +``` + +--- + +### Task 4: Full original-suite parity gate + +**Files:** +- Modify: `tests/original-go/mod.js` +- Modify: `src/cmd/tinycolor-compat/main.go` +- Modify: `src/cmd/tinycolor-compat/main_test.go` +- Modify: `Makefile` +- Modify: `.github/workflows/port.yml` + +**Interfaces:** +- Consumes: the complete original `test.js` API inventory and overlay runner. +- Produces: `make test-original-go` and a required CI gate. + +- [ ] **Step 1: Run the full overlay suite and verify integration RED** + +```powershell +node tests/original-go/run.mjs +``` + +Expected: the runner reaches the original suite but reports unsupported facade behavior or assertion mismatches until every mapped method is correct. + +- [ ] **Step 2: Close failures one behavior at a time** + +For each failure, first add the smallest focused case to `main_test.go` or `facade.test.mjs`, run it to observe the same failure, then change only the implicated dispatcher or facade method. The complete mapped scope is the instance and static API list in Task 3; `polyad` remains the one ignored upstream test and must not be implemented for this deliverable. + +- [ ] **Step 3: Verify the exact full-suite result** + +Run `node tests/original-go/run.mjs`. Expected final Deno summary: + +```text +45 passed | 0 failed | 1 ignored +``` + +Then run `node tests/original/verify.mjs`. Expected: `verified: 3`. + +- [ ] **Step 4: Add Make and CI gates** + +Add: + +```make +test-original-go: build + node tests/original-go/run.mjs +``` + +Include `test-original-go` in `verify` or `test`, and keep the separate `deno test test.js` oracle check in CI. GitHub Actions must therefore run both the source implementation suite and the byte-identical Go-backed suite. + +- [ ] **Step 5: Run the complete local gate** + +On Windows, run the Makefile commands directly with repository-local `GOCACHE`; on CI-compatible shells run `make verify` and `deno test test.js`. Expected: all gates pass. + +- [ ] **Step 6: Commit** + +```powershell +git add tests/original-go src Makefile .github/workflows/port.yml +git commit -m "test(original): run unchanged suite against Go" +``` + +--- + +### Task 5: Publish honest completion evidence + +**Files:** +- Modify: `DECISIONS.md` +- Modify: `README.md` +- Modify: `COMPATIBILITY.md` +- Modify: `docs/TESTING.md` +- Modify: `.planning/STATE.md` + +**Interfaces:** +- Consumes: exact local command output and final CI URL. +- Produces: reproducible submission evidence that distinguishes source-oracle and Go-backed suite runs. + +- [ ] **Step 1: Record the architectural decision** + +Add D-017: use a byte-identical temporary source-test overlay and synchronous test-only facade invoking the native Go binary. Rationale: this runs original assertions against the port without changing kickoff files, copying algorithms into JavaScript, or introducing a second WASM implementation. + +- [ ] **Step 2: Update evidence documents** + +Document both exact results: + +```text +deno test test.js 45 passed, 0 failed, 1 ignored against source oracle +node tests/original-go/run.mjs 45 passed, 0 failed, 1 ignored against native Go port +``` + +Update the submission checklist item for original-suite-against-port from partial to complete. Keep the demo video unchecked. + +- [ ] **Step 3: Run final verification** + +Run all of the following fresh: + +```powershell +node tests/original/verify.mjs +node tests/original-go/run.mjs +node tests/port/adapter.test.mjs +node --test tests/original/verify.test.mjs tests/original-go/run.test.mjs +node --test fuzz/harness.test.mjs fuzz/validate-log.test.mjs bench/run.test.mjs +node fuzz/validate-log.mjs fuzz/log.txt +go -C src test ./... -count=1 +go -C src vet ./... +git diff --check +``` + +Expected: zero failures, three verified oracle hashes, a validated fuzz log, and a clean diff check. + +- [ ] **Step 4: Commit documentation** + +```powershell +git add DECISIONS.md README.md COMPATIBILITY.md docs/TESTING.md .planning/STATE.md +git commit -m "docs: publish original-suite Go evidence" +``` + +- [ ] **Step 5: Push and verify GitHub Actions** + +Push `rajeet`, query the exact HEAD run, and confirm `status=completed` and `conclusion=success`. Add the successful exact-commit URL only if documentation does not already point to the final run; otherwise report it in the handoff. From 07a8f092922eed4aa73e508fb84bff2b1210483c Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 15:03:38 +0530 Subject: [PATCH 160/165] feat(compat): add one-request bridge mode --- src/cmd/tinycolor-compat/main.go | 13 +++++++++++++ src/cmd/tinycolor-compat/main_test.go | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index e4e817ab..00d7f708 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -24,6 +24,19 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return runJSONL(stdin, stdout, stderr) } switch args[0] { + case "bridge": + if len(args) != 2 { + usage(stderr, "bridge ") + return 2 + } + request, err := compat.Decode([]byte(args[1])) + if err != nil { + response, _ := compat.Failure("", err.Error()) + write(response, stdout, stderr) + return 1 + } + write(handle(request), stdout, stderr) + return 0 case "parse": return runParse(args[1:], stdout, stderr) case "convert": diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index cee3ff63..2b1ab999 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -127,6 +127,23 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { stdin: "{\"id\":\"hsl-one-percent\",\"operation\":\"inspect\",\"input\":\"hsl(115, 1%, 1%)\"}\n", wantStdout: "{\"id\":\"hsl-one-percent\",\"result\":{\"alpha\":1,\"format\":\"hsl\",\"original\":\"hsl(115, 1%, 1%)\",\"rgb\":{\"a\":1,\"b\":3,\"g\":3,\"r\":3},\"valid\":true,\"value\":\"hsl(115, 1%, 1%)\"}}\n", }, + { + name: "one-request bridge", + args: []string{"bridge", `{"id":"bridge-red","operation":"output","input":"red","args":{"method":"toHexString"}}`}, + wantStdout: "{\"id\":\"bridge-red\",\"result\":\"#ff0000\"}\n", + }, + {name: "missing bridge request", args: []string{"bridge"}, wantStatus: 2}, + { + name: "malformed bridge request", + args: []string{"bridge", "{bad"}, + wantStatus: 1, + verify: func(t *testing.T, output string) { + t.Helper() + if !strings.Contains(output, "malformed JSON") { + t.Fatalf("stdout = %q", output) + } + }, + }, { name: "malformed JSONL request does not stop the stream", stdin: "{bad json\n{\"id\":\"red\",\"operation\":\"inspect\",\"input\":\"red\"}\n", From 961127d088575bb4b97a0e8a720106a5f2e73046 Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 15:08:44 +0530 Subject: [PATCH 161/165] feat(compat): expose original-suite operations --- src/cmd/tinycolor-compat/main.go | 58 +++++++++++++++++++++++---- src/cmd/tinycolor-compat/main_test.go | 55 +++++++++++++++++++++++++ src/internal/parser/names.go | 3 ++ src/internal/parser/parser_test.go | 11 +++++ src/tinycolor/color.go | 6 ++- src/tinycolor/color_test.go | 22 +++++++++- 6 files changed, 145 insertions(+), 10 deletions(-) diff --git a/src/cmd/tinycolor-compat/main.go b/src/cmd/tinycolor-compat/main.go index 00d7f708..0c435cd3 100644 --- a/src/cmd/tinycolor-compat/main.go +++ b/src/cmd/tinycolor-compat/main.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/rajeet-04/tinycolor-go/internal/compat" + "github.com/rajeet-04/tinycolor-go/internal/parser" "github.com/rajeet-04/tinycolor-go/tinycolor" ) @@ -240,9 +241,9 @@ func handle(request compat.Request) compat.Response { format, _ := args["format"].(string) gradientType, _ := args["gradientType"].(bool) color, err = tinycolor.FromCompatWithOptions(request.Input, true, tinycolor.CompatOptions{Format: format, GradientType: gradientType}) - case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable", "palette": + case "output", "analysis", "clone", "modify", "mix", "readability", "isReadable", "mostReadable", "palette", "setAlpha": color, err = tinycolor.FromCompatWithOptions(request.Input, false, options(args)) - case "equals", "randomInvariant": + case "equals", "randomInvariant", "random", "names": default: response, _ := compat.Failure(request.ID, "unsupported operation") return response @@ -276,6 +277,10 @@ func handle(request compat.Request) compat.Response { return mostReadable(request.ID, color, args) case "palette": return palette(request.ID, color, args) + case "setAlpha": + color.SetAlpha(args["value"]) + response, _ := compat.Success(request.ID, color.Inspect()) + return response case "equals": response, _ := compat.Success(request.ID, tinycolor.Equals(request.Input, args["other"])) return response @@ -288,6 +293,12 @@ func handle(request compat.Request) compat.Response { "rgbInRange": rgb.R >= 0 && rgb.R <= 255 && rgb.G >= 0 && rgb.G <= 255 && rgb.B >= 0 && rgb.B <= 255, }) return response + case "random": + response, _ := compat.Success(request.ID, tinycolor.Random().Inspect()) + return response + case "names": + response, _ := compat.Success(request.ID, parser.Names()) + return response } response, _ := compat.Success(request.ID, color.Inspect()) return response @@ -498,14 +509,26 @@ func analysis(id string, color tinycolor.Color, method any) compat.Response { func output(id string, color tinycolor.Color, args map[string]any) compat.Response { var result any switch args["method"] { + case "toRgb": + rgb := color.ToRGB() + result = map[string]any{"r": rgb.R, "g": rgb.G, "b": rgb.B, "a": rgb.A} + case "toPercentageRgb": + rgb := color.ToPercentageRGB() + result = map[string]any{"r": fmt.Sprintf("%d%%", rgb.R), "g": fmt.Sprintf("%d%%", rgb.G), "b": fmt.Sprintf("%d%%", rgb.B), "a": rgb.A} + case "toHsl": + hsl := color.ToHSL() + result = map[string]any{"h": hsl.H, "s": hsl.S, "l": hsl.L, "a": hsl.A} + case "toHsv": + hsv := color.ToHSV() + result = map[string]any{"h": hsv.H, "s": hsv.S, "v": hsv.V, "a": hsv.A} case "toHex": - result = color.ToHex() + result = compactHex(color.ToHex(), truthy(args["compact"])) case "toHex8": - result = color.ToHex8() + result = compactHex(color.ToHex8(), truthy(args["compact"])) case "toHexString": - result = color.ToHexString() + result = "#" + compactHex(color.ToHex(), truthy(args["compact"])) case "toHex8String": - result = color.ToHex8String() + result = "#" + compactHex(color.ToHex8(), truthy(args["compact"])) case "toRgbString": result = color.ToRGBString() case "toPercentageRgbString": @@ -516,7 +539,14 @@ func output(id string, color tinycolor.Color, args map[string]any) compat.Respon result = color.ToHSVString() case "toString": format, _ := args["format"].(string) - result = color.ToString(format) + switch format { + case "hex3": + result = "#" + compactHex(color.ToHex(), true) + case "hex4": + result = "#" + compactHex(color.ToHex8(), true) + default: + result = color.ToString(format) + } case "toName": name, ok := color.ToName() if ok { @@ -539,6 +569,20 @@ func output(id string, color tinycolor.Color, args map[string]any) compat.Respon return response } +func compactHex(hex string, enabled bool) string { + if !enabled || (len(hex) != 6 && len(hex) != 8) { + return hex + } + compact := make([]byte, 0, len(hex)/2) + for index := 0; index < len(hex); index += 2 { + if hex[index] != hex[index+1] { + return hex + } + compact = append(compact, hex[index]) + } + return string(compact) +} + func truthy(value any) bool { switch value := value.(type) { case nil: diff --git a/src/cmd/tinycolor-compat/main_test.go b/src/cmd/tinycolor-compat/main_test.go index 2b1ab999..173c2cfc 100644 --- a/src/cmd/tinycolor-compat/main_test.go +++ b/src/cmd/tinycolor-compat/main_test.go @@ -132,6 +132,61 @@ func TestRunJSONLAndUsageErrors(t *testing.T) { args: []string{"bridge", `{"id":"bridge-red","operation":"output","input":"red","args":{"method":"toHexString"}}`}, wantStdout: "{\"id\":\"bridge-red\",\"result\":\"#ff0000\"}\n", }, + { + name: "bridge RGB object", + args: []string{"bridge", `{"id":"rgb","operation":"output","input":"red","args":{"method":"toRgb"}}`}, + wantStdout: "{\"id\":\"rgb\",\"result\":{\"a\":1,\"b\":0,\"g\":0,\"r\":255}}\n", + }, + { + name: "bridge percentage RGB object", + args: []string{"bridge", `{"id":"prgb","operation":"output","input":"red","args":{"method":"toPercentageRgb"}}`}, + wantStdout: "{\"id\":\"prgb\",\"result\":{\"a\":1,\"b\":\"0%\",\"g\":\"0%\",\"r\":\"100%\"}}\n", + }, + { + name: "bridge HSL object", + args: []string{"bridge", `{"id":"hsl","operation":"output","input":"red","args":{"method":"toHsl"}}`}, + wantStdout: "{\"id\":\"hsl\",\"result\":{\"a\":1,\"h\":0,\"l\":0.5,\"s\":1}}\n", + }, + { + name: "bridge HSV object", + args: []string{"bridge", `{"id":"hsv","operation":"output","input":"red","args":{"method":"toHsv"}}`}, + wantStdout: "{\"id\":\"hsv\",\"result\":{\"a\":1,\"h\":0,\"s\":1,\"v\":1}}\n", + }, + { + name: "bridge compact hex", + args: []string{"bridge", `{"id":"compact","operation":"output","input":"red","args":{"method":"toHexString","compact":true}}`}, + wantStdout: "{\"id\":\"compact\",\"result\":\"#f00\"}\n", + }, + { + name: "bridge explicit hex4", + args: []string{"bridge", `{"id":"hex4","operation":"output","input":"rgba(255, 0, 0, 0.6)","args":{"method":"toString","format":"hex4"}}`}, + wantStdout: "{\"id\":\"hex4\",\"result\":\"#f009\"}\n", + }, + { + name: "bridge alpha mutation", + args: []string{"bridge", `{"id":"alpha","operation":"setAlpha","input":"red","args":{"value":0.5}}`}, + wantStdout: "{\"id\":\"alpha\",\"result\":{\"alpha\":0.5,\"format\":\"name\",\"original\":\"red\",\"rgb\":{\"a\":0.5,\"b\":0,\"g\":0,\"r\":255},\"valid\":true,\"value\":\"rgba(255, 0, 0, 0.5)\"}}\n", + }, + { + name: "bridge random snapshot", + args: []string{"bridge", `{"id":"random","operation":"random"}`}, + verify: func(t *testing.T, output string) { + t.Helper() + if !strings.Contains(output, `"format":"prgb"`) || !strings.Contains(output, `"valid":true`) { + t.Fatalf("stdout = %q", output) + } + }, + }, + { + name: "bridge Go-owned names", + args: []string{"bridge", `{"id":"names","operation":"names"}`}, + verify: func(t *testing.T, output string) { + t.Helper() + if !strings.Contains(output, `"red":"f00"`) || !strings.Contains(output, `"rebeccapurple":"663399"`) { + t.Fatalf("stdout = %q", output) + } + }, + }, {name: "missing bridge request", args: []string{"bridge"}, wantStatus: 2}, { name: "malformed bridge request", diff --git a/src/internal/parser/names.go b/src/internal/parser/names.go index 7b986c64..45ff78a7 100644 --- a/src/internal/parser/names.go +++ b/src/internal/parser/names.go @@ -2,6 +2,7 @@ package parser import ( "fmt" + "maps" "strings" ) @@ -34,6 +35,8 @@ var ( }() ) +func Names() map[string]string { return maps.Clone(names) } + func NameForRGB(r, g, b int) string { hex := fmt.Sprintf("%02x%02x%02x", r, g, b) if hex[0] == hex[1] && hex[2] == hex[3] && hex[4] == hex[5] { diff --git a/src/internal/parser/parser_test.go b/src/internal/parser/parser_test.go index d4254bde..0b43da8e 100644 --- a/src/internal/parser/parser_test.go +++ b/src/internal/parser/parser_test.go @@ -42,6 +42,17 @@ func TestAllSourceNamesParse(t *testing.T) { } } +func TestNamesReturnsIndependentCopy(t *testing.T) { + first := Names() + if first["red"] != "f00" { + t.Fatalf("red = %q", first["red"]) + } + first["red"] = "broken" + if second := Names(); second["red"] != "f00" { + t.Fatalf("mutated red = %q", second["red"]) + } +} + func TestOriginalAndFromRatio(t *testing.T) { object := map[string]any{"r": float64(1), "g": float64(0), "b": float64(0), "a": float64(.5)} ratio := ParseFromRatio(object) diff --git a/src/tinycolor/color.go b/src/tinycolor/color.go index 36b8037a..507f413c 100644 --- a/src/tinycolor/color.go +++ b/src/tinycolor/color.go @@ -63,6 +63,10 @@ func (c Color) Valid() bool { return c.model.Valid } func (c Color) Format() string { return string(c.model.Format) } func (c Color) Alpha() float64 { return c.model.A } func (c Color) Original() any { return c.model.Original } +func (c *Color) SetAlpha(value any) *Color { + c.model.A = color.BoundAlpha(value) + return c +} func (c Color) RGB() map[string]any { return map[string]any{"r": math.Round(c.model.R), "g": math.Round(c.model.G), "b": math.Round(c.model.B), "a": c.model.A} } @@ -258,7 +262,7 @@ func jsTruthy(value any) bool { } } func Random() Color { - return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatRGB}} + return Color{model: color.Model{R: rand.Float64() * 255, G: rand.Float64() * 255, B: rand.Float64() * 255, A: 1, Valid: true, Format: color.FormatPRGB}} } func mathRound(v float64) int { floor := math.Floor(v) diff --git a/src/tinycolor/color_test.go b/src/tinycolor/color_test.go index 8b4466a1..31d4424a 100644 --- a/src/tinycolor/color_test.go +++ b/src/tinycolor/color_test.go @@ -18,8 +18,26 @@ func TestOutputsAndAnalysis(t *testing.T) { if !Equals("#ff000066", "rgba(255, 0, 0, .4)") { t.Fatal("equals") } - if !Random().Valid() { - t.Fatal("random") + if random := Random(); !random.Valid() || random.Format() != "prgb" { + t.Fatalf("random = valid %t, format %q", random.Valid(), random.Format()) + } +} + +func TestSetAlphaMatchesTinyColorBounds(t *testing.T) { + color, _ := FromCompat("red", false) + for _, test := range []struct { + value any + want float64 + }{ + {0.9, 0.9}, + {-1.0, 1}, + {2.0, 1}, + {nil, 1}, + {"test", 1}, + } { + if returned := color.SetAlpha(test.value); returned != &color || color.Alpha() != test.want { + t.Fatalf("SetAlpha(%#v) returned %p, alpha %v", test.value, returned, color.Alpha()) + } } } From d2de6783165bb032ca4670017b070a3ae73357f1 Mon Sep 17 00:00:00 2001 From: xthxr Date: Sat, 1 Aug 2026 17:05:25 +0530 Subject: [PATCH 162/165] test(original): add Go-backed TinyColor facade --- tests/original-go/facade.test.mjs | 14 +++ tests/original-go/mod.js | 144 ++++++++++++++++++++++++++++++ tests/original-go/run.mjs | 66 ++++++++++++++ tests/original-go/run.test.mjs | 18 ++++ 4 files changed, 242 insertions(+) create mode 100644 tests/original-go/facade.test.mjs create mode 100644 tests/original-go/mod.js create mode 100644 tests/original-go/run.mjs create mode 100644 tests/original-go/run.test.mjs diff --git a/tests/original-go/facade.test.mjs b/tests/original-go/facade.test.mjs new file mode 100644 index 00000000..1aafce68 --- /dev/null +++ b/tests/original-go/facade.test.mjs @@ -0,0 +1,14 @@ +import assert from "node:assert/strict"; + +import tinycolor from "./mod.js"; + +Deno.test("facade serves TinyColor behavior from Go", () => { + assert.equal(tinycolor("red").toHexString(), "#ff0000"); + const color = tinycolor("red"); + assert.equal(color.lighten(10), color); + assert.equal(color.toHexString(), "#ff3333"); + assert.deepEqual(tinycolor.fromRatio({ r: 1, g: 0, b: 0 }).toRgb(), { r: 255, g: 0, b: 0, a: 1 }); + assert.equal(tinycolor("hsl 100 20 10").toHslString(), "hsl(100, 20%, 10%)"); + assert.equal(tinycolor.mix("#000", "#fff").toHsl().l, 0.5); + assert.equal(tinycolor("red").analogous().map((entry) => entry.toHex()).join(","), "ff0000,ff0066,ff0033,ff0000,ff3300,ff6600"); +}); diff --git a/tests/original-go/mod.js b/tests/original-go/mod.js new file mode 100644 index 00000000..e1e14119 --- /dev/null +++ b/tests/original-go/mod.js @@ -0,0 +1,144 @@ +const binary = Deno.env.get("TINYCOLOR_GO_BINARY"); +if (!binary) throw new Error("TINYCOLOR_GO_BINARY is required"); + +const decoder = new TextDecoder(); +let nextID = 0; + +function invoke(operation, input, args = {}) { + const id = `suite-${nextID++}`; + const request = { id, operation, input: unwrap(input), args: unwrap(args) }; + const output = new Deno.Command(binary, { + args: ["bridge", JSON.stringify(request)], + stdout: "piped", + stderr: "piped", + }).outputSync(); + const stderr = decoder.decode(output.stderr).trim(); + if (!output.success) throw new Error(stderr || `Go bridge exited ${output.code}`); + const response = JSON.parse(decoder.decode(output.stdout)); + if (response.id !== id) throw new Error(`Go bridge response ID mismatch: ${response.id}`); + if (Object.hasOwn(response, "error")) throw new Error(response.error); + return response.result; +} + +function unwrap(value) { + if (value instanceof TinyColorFacade) return value._input; + if (Array.isArray(value)) return value.map(unwrap); + if (value && typeof value === "object") { + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, unwrap(entry)])); + } + return value; +} + +class TinyColorFacade { + constructor(input, options = {}, ratio = false) { + this._original = input || ""; + this._options = options; + const inspection = invoke(ratio ? "fromRatio" : "inspect", input, ratio ? options : {}); + this._apply(inspection, inspection.original ?? inspection.rgb); + if (options.format) this._format = options.format; + } + + static fromInspection(inspection, original = inspection.original, options = {}, input = inspection.original ?? inspection.rgb) { + const color = Object.create(TinyColorFacade.prototype); + color._original = original; + color._options = options; + return color._apply(inspection, input); + } + + _apply(inspection, input = inspection.rgb) { + this._input = input; + this._rgba = inspection.rgb; + this._format = inspection.format || ""; + this._valid = inspection.valid; + return this; + } + + _args(args = {}) { + return { ...args, options: { format: this._format, gradientType: !!this._options.gradientType } }; + } + + _output(method, args = {}) { + return invoke("output", this._input, this._args({ method, ...args })); + } + + _modify(method, amount) { + return this._apply(invoke("modify", this._input, this._args({ method, amount })).after); + } + + _palette(method, args = {}) { + return invoke("palette", this._input, this._args({ method, ...args })) + .map((inspection) => TinyColorFacade.fromInspection(inspection, inspection.original, {}, inspection.rgb)); + } + + getOriginalInput() { return this._original; } + getFormat() { return this._format || false; } + getAlpha() { return this._rgba.a; } + isValid() { return this._valid; } + + setAlpha(value) { + return this._apply(invoke("setAlpha", this._input, this._args({ value }))); + } + + clone() { + return TinyColorFacade.fromInspection(invoke("clone", this._input, this._args()), this.toString()); + } + + toRgb() { return this._output("toRgb"); } + toPercentageRgb() { return this._output("toPercentageRgb"); } + toHsl() { return this._output("toHsl"); } + toHsv() { return this._output("toHsv"); } + toRgbString() { return this._output("toRgbString"); } + toPercentageRgbString() { return this._output("toPercentageRgbString"); } + toHslString() { return this._output("toHslString"); } + toHsvString() { return this._output("toHsvString"); } + toHex(compact = false) { return this._output("toHex", { compact }); } + + toHexString(compact = false) { + return this._output("toHexString", { compact }); + } + + toHex8(compact = false) { return this._output("toHex8", { compact }); } + toHex8String(compact = false) { return this._output("toHex8String", { compact }); } + toName() { return this._output("toName"); } + toFilter(secondColor) { return this._output("toFilter", { secondColor }); } + toString(format) { return this._output("toString", { format }); } + + getBrightness() { return invoke("analysis", this._input, { method: "brightness" }); } + getLuminance() { return invoke("analysis", this._input, { method: "luminance" }); } + isDark() { return invoke("analysis", this._input, { method: "isDark" }); } + isLight() { return invoke("analysis", this._input, { method: "isLight" }); } + + lighten(amount) { return this._modify("lighten", amount); } + brighten(amount) { return this._modify("brighten", amount); } + darken(amount) { return this._modify("darken", amount); } + saturate(amount) { return this._modify("saturate", amount); } + desaturate(amount) { return this._modify("desaturate", amount); } + greyscale() { return this._modify("greyscale"); } + spin(amount) { return this._modify("spin", amount); } + + complement() { return this._palette("complement")[0]; } + analogous(results, slices) { return this._palette("analogous", { results, slices }); } + monochromatic(results) { return this._palette("monochromatic", { results }); } + splitcomplement() { return this._palette("splitcomplement"); } + triad() { return this._palette("triad"); } + tetrad() { return this._palette("tetrad"); } +} + +function tinycolor(input, options = {}) { + if (input instanceof TinyColorFacade) return input; + return new TinyColorFacade(input, options); +} + +tinycolor.fromRatio = (input, options = {}) => new TinyColorFacade(input, options, true); +tinycolor.random = () => TinyColorFacade.fromInspection(invoke("random")); +tinycolor.equals = (first, second) => invoke("equals", first, { other: second }); +tinycolor.mix = (first, second, amount) => TinyColorFacade.fromInspection(invoke("mix", first, { other: second, amount })); +tinycolor.readability = (first, second) => invoke("readability", first, { other: second }); +tinycolor.isReadable = (first, second, options) => invoke("isReadable", first, { other: second, options }); +tinycolor.mostReadable = (base, candidates, options) => { + const inspection = invoke("mostReadable", base, { candidates, options }); + return inspection ? TinyColorFacade.fromInspection(inspection) : null; +}; +tinycolor.names = invoke("names"); + +export default tinycolor; diff --git a/tests/original-go/run.mjs b/tests/original-go/run.mjs new file mode 100644 index 00000000..93af7cc2 --- /dev/null +++ b/tests/original-go/run.mjs @@ -0,0 +1,66 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { verifyManifest } from "../original/verify.mjs"; + +const root = resolve(import.meta.dirname, "../.."); + +export function prepareOverlay() { + const failures = verifyManifest(resolve(root, "tests/original/manifest.sha256"), root); + if (failures.length) throw new Error(failures.join("\n")); + const directory = mkdtempSync(resolve(tmpdir(), "tinycolor-original-go-")); + const testFile = resolve(directory, "test.js"); + copyFileSync(resolve(root, "test.js"), testFile); + copyFileSync(resolve(import.meta.dirname, "mod.js"), resolve(directory, "mod.js")); + if (!readFileSync(testFile).equals(readFileSync(resolve(root, "test.js")))) { + rmSync(directory, { recursive: true, force: true }); + throw new Error("copied test.js differs from kickoff test.js"); + } + return { + directory, + testFile, + cleanup: () => rmSync(directory, { recursive: true, force: true }), + }; +} + +function buildBinary() { + mkdirSync(resolve(root, "bin"), { recursive: true }); + const suffix = execFileSync("go", ["env", "GOEXE"], { cwd: root, encoding: "utf8" }).trim(); + const binary = resolve(root, "bin", `tinycolor${suffix}`); + execFileSync("go", ["-C", "src", "build", "-o", `../bin/tinycolor${suffix}`, "./cmd/tinycolor-compat"], { + cwd: root, + env: { ...process.env, GOCACHE: process.env.GOCACHE ?? resolve(root, ".cache/go-build") }, + stdio: "inherit", + }); + return binary; +} + +function main() { + const binary = buildBinary(); + const overlay = prepareOverlay(); + try { + const result = spawnSync("deno", [ + "test", + `--allow-env=TINYCOLOR_GO_BINARY`, + `--allow-run=${binary}`, + overlay.testFile, + ], { + cwd: root, + env: { + ...process.env, + DENO_DIR: process.env.DENO_DIR ?? resolve(root, ".cache/deno"), + TINYCOLOR_GO_BINARY: binary, + }, + stdio: "inherit", + }); + if (result.error) throw result.error; + process.exitCode = result.status ?? 1; + } finally { + overlay.cleanup(); + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main(); diff --git a/tests/original-go/run.test.mjs b/tests/original-go/run.test.mjs new file mode 100644 index 00000000..7e323c7f --- /dev/null +++ b/tests/original-go/run.test.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { prepareOverlay } from "./run.mjs"; + +const digest = (file) => createHash("sha256").update(readFileSync(file)).digest("hex"); + +test("overlay preserves the original test bytes", () => { + const overlay = prepareOverlay(); + try { + assert.equal(digest(overlay.testFile), digest("test.js")); + assert.equal(readFileSync(overlay.testFile).equals(readFileSync("test.js")), true); + } finally { + overlay.cleanup(); + } +}); From 50ee3066f9ad5da4041b0211f4ceb7bf4569c36f Mon Sep 17 00:00:00 2001 From: mrashis06 Date: Sat, 1 Aug 2026 17:06:19 +0530 Subject: [PATCH 163/165] test(original): gate Go-backed source suite --- Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d00bbc6c..4b12c059 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test verify fmt-check hashes fuzz bench clean +.PHONY: build test test-original-go verify fmt-check hashes fuzz bench clean GOCACHE ?= $(CURDIR)/.cache/go-build export GOCACHE @@ -11,7 +11,7 @@ build: test: node tests/port/adapter.test.mjs - node --test tests/original/verify.test.mjs + node --test tests/original/verify.test.mjs tests/original-go/run.test.mjs node --test fuzz/harness.test.mjs fuzz/validate-log.test.mjs node fuzz/validate-log.mjs fuzz/log.txt go -C src test ./... @@ -21,7 +21,10 @@ test: node compat/run.mjs compat/cases/conversion.jsonl node compat/run.mjs compat/cases/operations.jsonl -verify: fmt-check hashes test +test-original-go: + node tests/original-go/run.mjs + +verify: fmt-check hashes test test-original-go go -C src vet ./... fmt-check: From 4ab236d818b522816a74ec849d331a29b3a46658 Mon Sep 17 00:00:00 2001 From: DeepaliSingh10 Date: Sat, 1 Aug 2026 17:10:30 +0530 Subject: [PATCH 164/165] docs: publish original-suite Go evidence --- .planning/STATE.md | 3 +++ COMPATIBILITY.md | 14 ++++++++++---- DECISIONS.md | 1 + README.md | 18 +++++++++++------- docs/TESTING.md | 8 +++++++- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/.planning/STATE.md b/.planning/STATE.md index 7e7e85e9..f05d5c2d 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -21,6 +21,7 @@ operations 71/71; 164/164 total and zero mismatches. - Go tests/vet and 15 Node tests pass. - Immutable Deno source suite: 45 passed, 0 failed, 1 ignored. +- Byte-identical original suite against native Go: 45 passed, 0 failed, 1 ignored. - Differential fuzz evidence: 60.012 seconds, seed 20260801, 1,091,630 cases, zero divergences. - Shared benchmark: 20 cold starts and 1,000 persistent requests per runtime, @@ -33,6 +34,8 @@ - Apply TinyColor's sub-one RGB rounding once at the `FromCompat` boundary. - Keep fuzz comparisons exact and normalize benchmark workload newlines before hashing. - Never claim external evidence until public access and exact-commit CI are observed. +- Use a test-only synchronous facade to run unchanged source assertions against + the native Go binary without duplicating TinyColor algorithms. ## Session Continuity diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 33d32016..97925316 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -23,9 +23,15 @@ invariants rather than exact equality between independent random generators. ## Source suite and oracle integrity - `node tests/original/verify.mjs`: 3/3 kickoff hashes verified. -- `deno test test.js`: 45 passed, 0 failed, 1 ignored. +- `deno test test.js`: 45 passed, 0 failed, 1 ignored against the source oracle. +- `node tests/original-go/run.mjs`: 45 passed, 0 failed, 1 ignored against the + native Go binary using a byte-identical temporary copy of `test.js`. - The ignored `polyad` test is also ignored by the pinned upstream suite. +The Go-backed facade preserves JavaScript constructor identity, mutation, and +chaining required by the original assertions. It does not import the source +implementation or contain TinyColor color algorithms. + ## Go coverage and safety Fresh command: `go -C src test -cover ./...`. @@ -65,9 +71,9 @@ observations, not universal speedup claims. - Differential Fuzz Survivor: eligible from the validated 60-second log. - Zero Unsafe: eligible from zero Go source occurrences. -- Decision Log: eligible from 15 substantive decisions. -- GitHub Actions: [run 30686980719](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719) passed the full gate, Deno, build, and artifact upload for commit `1c218b6`. -- Public repository visibility: unauthenticated `git ls-remote https://github.com/rajeet-04/TinyColor.git refs/heads/rajeet` returned exact commit `1c218b6`. +- Decision Log: eligible from 17 substantive decisions. +- GitHub Actions: [`rajeet` branch runs](https://github.com/rajeet-04/TinyColor/actions?query=branch%3Arajeet) execute the full gate, source-oracle Deno suite, build, and artifact upload. +- Public repository: https://github.com/rajeet-04/TinyColor. - Five-minute demo video: not supplied. No known mismatch remains in the fixed corpus or recorded fuzz session. diff --git a/DECISIONS.md b/DECISIONS.md index a8773f44..da4676f5 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -18,3 +18,4 @@ | D-014 | Normalize benchmark workload newlines before hashing. | Evidence hashes remain stable across Windows and Unix checkouts without changing request content. | Accepted | | D-015 | Publish unsupported or external deliverables as unverified. | Honest missing evidence is preferable to unreproducible CI, public-access, or video claims. | Accepted | | D-016 | Return an empty typed Go palette for non-positive counts instead of reproducing TinyColor's non-terminating negative `analogous` loop. | Compatibility must not make a public Go call hang indefinitely; adapter zero values still retain TinyColor's documented defaults. | Accepted | +| D-017 | Run a byte-identical copy of `test.js` through a test-only synchronous facade that invokes the native Go binary. | This executes the original assertions against the port without editing kickoff files, copying color algorithms into JavaScript, or adding a second WebAssembly implementation. | Accepted | diff --git a/README.md b/README.md index ac488d95..54d55001 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ An idiomatic Go port of the behavior in the pinned [`bgrins/TinyColor`](https://github.com/bgrins/TinyColor) checkout. The -JavaScript source remains in this repository as an immutable oracle; exact -JSONL differential checks demonstrate compatibility rather than modifying the -source tests. +JavaScript source remains in this repository as an immutable oracle. Exact +JSONL differential checks and a byte-identical original-suite runner exercise +the compiled Go port without modifying the source tests. Source commit: `b49018c9f2dbca313d80d7a4dad25e26143cfe01`. TinyColor and this port retain Brian Grinstead's MIT license in [`LICENSE`](LICENSE). The original JavaScript @@ -65,13 +65,16 @@ func main() { ```sh make verify +node tests/original-go/run.mjs deno test test.js ``` `make verify` checks formatting, the three kickoff hashes in [`tests/original/manifest.sha256`](tests/original/manifest.sha256), Node and Go -tests, all fixed differential corpora, the recorded fuzz log, and `go vet`. -The untouched Deno suite is a separate source-oracle check. +tests, all fixed differential corpora, the recorded fuzz log, the byte-identical +original suite against Go, and `go vet`. The direct Deno command separately +checks the untouched source oracle. Both original-suite runs report 45 passed, +0 failed, and the same upstream `polyad` test ignored. Evidence: @@ -80,13 +83,14 @@ Evidence: - [Benchmark results](bench/results.json) and [methodology](bench/methodology.md) — same-host startup p99, latency p99, throughput, and peak RSS. - [Architectural decisions](DECISIONS.md), [architecture](docs/ARCHITECTURE.md), [testing](docs/TESTING.md), and [team ownership](docs/TEAM-OWNERSHIP.md). - [Five-minute demo script](docs/DEMO.md). -- [Successful exact-commit CI run](https://github.com/rajeet-04/TinyColor/actions/runs/30686980719). +- [GitHub Actions runs for `rajeet`](https://github.com/rajeet-04/TinyColor/actions?query=branch%3Arajeet). ## Repository layout ```text src/ Go module, public API, and CLI tests/original/ kickoff hash manifest and verifier +tests/original-go/ byte-identical suite runner and test-only Go facade tests/port/ port-owned adapter tests compat/ JSONL oracle, driver, and fixed corpora fuzz/ differential harness and 60-second log @@ -109,7 +113,7 @@ docs/ architecture, testing, ownership, and demo guide - [x] Public source URL and pinned kickoff commit recorded. - [x] One-command native and Docker builds documented. -- [x] Immutable oracle hashes and source suite verified. +- [x] Immutable oracle hashes and byte-identical original suite verified against Go. - [x] Exact fixed-corpus and 60-second differential evidence published. - [x] Same-host benchmark methodology and results published. - [x] Decisions, ownership, limitations, and demo script documented. diff --git a/docs/TESTING.md b/docs/TESTING.md index e6d39c6e..5aa67bf9 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -3,7 +3,8 @@ | Layer | Purpose | Command | |---|---|---| | Full local gate | Format, hashes, unit, differential, fuzz-log, vet | `make verify` | -| Original source suite | Untouched upstream behavior | `deno test test.js` | +| Original suite against Go | Byte-identical upstream assertions served by native Go | `node tests/original-go/run.mjs` | +| Original source suite | Untouched upstream implementation behavior | `deno test test.js` | | Go unit/coverage | Package behavior and honest coverage | `go -C src test -cover ./...` | | Fixed differential | Exact JS/Go response parity | `node compat/run.mjs compat/cases/operations.jsonl` | | Fuzz smoke | Seeded broad exact comparison | `node fuzz/harness.mjs --duration 1 --seed 20260801` | @@ -18,3 +19,8 @@ a deterministic regression before the shared implementation boundary is fixed. The fixed corpora cover source-test inputs, permissive and malformed parsing, numeric boundaries, alpha and hue behavior, conversions, mutation, WCAG readability, mixing, and ordered palettes. Random output is invariant-tested. + +The Go-backed runner verifies the kickoff manifest, copies `test.js` without +changing its bytes, and places a test-only `mod.js` facade beside it in a +temporary directory. The facade invokes the compiled Go binary directly and is +removed with the temporary overlay after the run. From 6885b308a5144e36d7c23a3ade078142c9f353bd Mon Sep 17 00:00:00 2001 From: rajeet-04 Date: Mon, 3 Aug 2026 22:43:44 +0530 Subject: [PATCH 165/165] docs: add GitHub Codespaces setup --- .devcontainer/devcontainer.json | 18 ++++++++++++++++++ README.md | 15 +++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..18634b86 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,18 @@ +{ + "name": "TinyColor Go", + "image": "mcr.microsoft.com/devcontainers/go:1-1.26-bookworm", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "22" + }, + "ghcr.io/devcontainers-extra/features/deno:1": {} + }, + "postCreateCommand": "make build", + "customizations": { + "vscode": { + "extensions": [ + "golang.Go" + ] + } + } +} diff --git a/README.md b/README.md index 54d55001..e2d1418b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,21 @@ docker build -t tinycolor-go . docker run --rm tinycolor-go parse --json red ``` +## Try it in GitHub Codespaces + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/rajeet-04/TinyColor?ref=rajeet&quickstart=1) + +The Codespaces environment installs Go, Node.js, Deno, and GNU Make, then +builds the native CLI. In the terminal, run: + +```sh +./bin/tinycolor parse --json red +./bin/tinycolor convert --to hsl --json red +./bin/tinycolor palette --type triad --json red +``` + +The browser launch link opens the `rajeet` branch used for this submission. + ## CLI ```sh