Vibecode Luminar Neo
track this build4 phases, 12 steps, beginner friendly0%The visible AI photo editor loop is buildable, but a credible replacement needs more than the first screen. Luminar Neo earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Luminar Neo. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Luminar Neo · indie build A non-destructive photo editor whose hard problems are a pipeline that shows at preview size what it will produce at full size, colour handled in a working space rather than in whatever the file happened to be, and a recipe format that still opens years later, with the AI tools as optional local models rather than the foundation. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript for the interface only | the web view displays pixels and never processes them | | Pipeline | Rust, operating on a linear floating point working buffer | eight-bit gamma-encoded arithmetic bands after three adjustments | | Edits | An ordered recipe of operations in a versioned sidecar beside the file | the recipe is the document and the pixels are derived from it | | Masks | Parametric where possible, compressed bitmaps referenced from the sidecar otherwise | a full-resolution mask per adjustment makes the sidecar larger than the photograph | | Models | Optional local segmentation models loaded from disk | the editor is complete without them and a photograph never leaves the machine | | Formats | JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile | raw decoding is a separate multi-year project | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface and runs its tests. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Rust toolchain and the Tauri 2 prerequisites** · free - Why: The pixel pipeline runs in Rust, and the interface is a Tauri shell around it. - Get it: rustup.rs for the toolchain, then the Tauri 2 prerequisites page for your platform. - Verify: cargo --version prints and a blank Tauri 2 application launches - [ ] **A folder of real photographs including the awkward ones** · free - Why: A sideways portrait, a file with no profile, a 24 megapixel frame and a gradient sky are where orientation, colour and banding go wrong, and a stock sample has none of them. - Get it: Your own photographs, including at least one of each of those cases, in JPEG and TIFF. - Verify: At least a thousand files with the four awkward cases identified - [ ] **A second image viewer that honours embedded profiles** · free - Why: The only way to know an export's colour is right is to open it somewhere you did not write. - Get it: Any mature viewer or editor with colour management on your platform. - Verify: A known profiled fixture displays identically in it and in your editor - [ ] **A local segmentation model whose licence permits this use** (optional) · free - Why: Subject and sky masks come from it, and the editor must work identically when it is absent. - Get it: A published segmentation model in a format your runtime loads, with its licence file kept beside it. - Verify: It produces a mask for a portrait and the editor still works when the file is deleted ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Raw decoding. - Generative sky or object replacement. - A cloud library or mobile companion. - Print and soft-proofing colour management. - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling If one of those is essential to you, that is the reason to keep paying for Luminar Neo, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Luminar Neo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a photo editor that never touches the original and previews exactly what it exports to replace Luminar Neo. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is never modified. Not by export, which writes a new file. Not by a star rating or a caption, which live in the sidecar. Not by a crop, a rename or a batch. Every command the application exposes is followed in the test suite by a check that the bytes of the original are the bytes that were imported, and that check runs against every operation the editor has. The reason is that an edit is an opinion and a photograph is evidence of a moment that cannot be retaken. An editor that bakes an adjustment into the file has decided for you, permanently, and the first time a batch export overwrites a folder of originals is the last time that person trusts it with anything. The second rule follows from the first. The AI tools are optional models that run on this machine. The editor is complete without them, a missing model degrades to a manual mask rather than an error, and no photograph is ever sent anywhere to be processed. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript for the interface only · the web view displays pixels and never processes them - Pipeline: Rust, operating on a linear floating point working buffer · eight-bit gamma-encoded arithmetic bands after three adjustments - Edits: An ordered recipe of operations in a versioned sidecar beside the file · the recipe is the document and the pixels are derived from it - Masks: Parametric where possible, compressed bitmaps referenced from the sidecar otherwise · a full-resolution mask per adjustment makes the sidecar larger than the photograph - Models: Optional local segmentation models loaded from disk · the editor is complete without them and a photograph never leaves the machine - Formats: JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile · raw decoding is a separate multi-year project ### Data model (create this before Phase 1) - photos(id, path, content_hash, width, height, orientation, embedded_profile, imported_at) · a reference to the original, never a copy of it - recipes(photo_id, schema_version, ops_json, updated_at) · the sidecar, one per photograph, operations in application order - an op carries kind, params_json, mask_id and enabled, and every spatial parameter is a fraction of the image size rather than a count of pixels - masks(id, photo_id, kind, params_json, bitmap_ref) · brush and gradient masks are parametric; a model's output is a compressed bitmap - versions(photo_id, name, recipe_snapshot_json, created_at) · named states of the recipe, not copies of pixels - exports(id, photo_id, recipe_hash, output_path, format, profile, metadata_policy, at) · every export records what produced it - models(name, path, sha256, licence, loaded) · no rows means manual masks only, which is a complete mode - The original's hash is checked after every command in the suite, so the one promise this editor makes is tested rather than intended. ### Phase 1 · Importing without touching anything Build: Import JPEG, TIFF and PNG by reference. Read the orientation tag and the embedded colour profile, hash the file, and create a sidecar. Ratings, captions and every other piece of metadata go in the sidecar. Add a suite that hashes the original after every command the application exposes. Trap: Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. Done when: Importing a folder of a thousand photographs creates a sidecar per file and modifies no original, verified by hashing before and after. A portrait photographed sideways displays upright because the orientation tag was applied on import. The embedded profile is read and recorded, and a file without one is recorded as untagged rather than assumed. Every command in the application is followed in the suite by a hash comparison against the imported bytes. Do not build yet: No adjustments, masks, models, versions or export. ### Phase 2 · A preview that is the export, smaller Build: Build the pipeline in Rust over a linear floating point buffer: exposure, white balance, contrast, curves, hue and saturation per range, crop and straighten. Render the preview at viewport resolution and the export at full resolution through the same code, with every spatial parameter relative to image size. Assert in the suite that a downscaled export matches the preview. Trap: A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. Done when: Every adjustment operates on linear floating point data and a gradient survives ten stacked adjustments without visible banding, checked by histogram. An export downscaled to preview size matches the preview within a stated tolerance, asserted in the suite for every operation including the spatial ones. Moving a slider updates the preview within a frame budget on a 24 megapixel file without blocking the interface. Crop and straighten compose correctly with every other operation whatever their position in the recipe. Do not build yet: No masks, models, versions, batch or export to disk. ### Phase 3 · Masks, including the ones a model draws Build: Brush, gradient and luminosity masks stored parametrically. An optional local segmentation model that proposes a subject or sky mask, stored as a compressed bitmap the person then refines with the brush. With no model present the same panel offers manual masks and says why the button is absent. Nothing is uploaded anywhere. Trap: Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. Done when: Brush and gradient masks are stored as parameters and re-render identically after reopening. A model mask is a compressed bitmap that the brush can refine, and the refinement survives reopening. Removing the model files leaves the editor fully working with manual masks and the reason shown in the panel, tested by deleting them. The process makes no network connections, verified by running the editor with networking disabled. Do not build yet: No versions, batch, history or export yet. ### Phase 4 · History, versions and doing it to a hundred files Build: Treat the recipe as the history: undo removes or disables the last operation, redo restores it, and a named version snapshots the recipe. Copy a recipe to a selection of photographs, applying the global adjustments and skipping crop and masks, and say which were skipped. Render the batch through the same pipeline in the background with progress and cancellation. Trap: Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. Done when: Undo and redo across fifty operations reproduce the recipe exactly, asserted by comparing recipes. A named version reopens to the same pixels as when it was saved, compared numerically. Pasting a recipe to another photograph applies global adjustments, skips crop and masks, and reports what was skipped. A batch of a hundred files renders in the background, can be cancelled, and cancellation leaves no partial output file. Do not build yet: No export to disk, profile conversion or metadata policy yet. ### Phase 5 · Export that keeps the colour and survives the years Build: Export to JPEG and TIFF, converting from the working space to a chosen output profile and embedding it. Offer a metadata policy per export: keep everything, keep everything except location, or strip. Refuse to write to the original's path. Version the sidecar schema, migrate old versions on open, and keep a fixture sidecar from every schema version in the suite. Export the recipe itself as readable text. Trap: Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures. Done when: An exported JPEG carries the chosen profile and renders identically in a second application, verified against a fixture. The location-stripping policy removes coordinates and nothing else, checked by reading the metadata back. An export whose path resolves to the original is refused with the reason. A sidecar from every previous schema version opens, migrates and renders the same pixels, asserted against stored fixtures. Do not build yet: Nothing after this. ### Out of scope (and why) - Raw decoding, which is a per-camera project with its own demosaicing, profiles and lens tables, and where every open-source darkroom has spent a decade. - Generative replacement of skies or objects, which produces a picture of something that did not happen and belongs in a different kind of tool with a different label on the output. - A cloud library or mobile companion, which turns a private editor into a service holding other people's photographs. - Print and soft-proofing colour management, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That the original file is never modified, and the suite hashes it after every command - That preview and export run through one pipeline with spatial parameters relative to image size, so the export matches the preview - That processing happens in a linear floating point working space and converts to an embedded output profile on export - That the AI tools are optional local models, the editor is complete without them, and no photograph leaves the machine - That the sidecar schema is versioned with a fixture from every version in the suite, because the recipes are the years of work ===== AGENTS.md ===== # Agent instructions · Luminar Neo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript for the interface only, Rust, operating on a linear floating point working buffer, An ordered recipe of operations in a versioned sidecar beside the file, Parametric where possible, compressed bitmaps referenced from the sidecar otherwise, Optional local segmentation models loaded from disk, JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. ===== BUILD_PLAN.md ===== # Build plan · Luminar Neo A non-destructive photo editor whose hard problems are a pipeline that shows at preview size what it will produce at full size, colour handled in a working space rather than in whatever the file happened to be, and a recipe format that still opens years later, with the AI tools as optional local models rather than the foundation. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing without touching anything A photograph is referenced, oriented and profiled, and its bytes are never changed. ### Steps 1. Reference the file, never copy or rewrite it The path and the hash are what you keep; the bytes stay where they were. Files: `src-tauri/src/library.rs`, `src-tauri/src/sidecar.rs` 2. Apply orientation and read the profile on import A sideways portrait and an untagged file are both routine. Files: `src-tauri/src/decode.rs` 3. Hash the original after every command in the suite The promise is tested against each operation, not stated once. Files: `src-tauri/tests/untouched.rs` ### Done when - [ ] A thousand imports modify no original, verified by hash - [ ] Sideways photographs display upright - [ ] A missing profile is recorded as untagged, not assumed - [ ] Every command is followed by a hash check in the suite ### Watch out - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. ## Phase 2 · A preview that is the export, smaller What the screen shows at viewport size is what the file contains at full size. ### Steps 1. Work in linear float, convert at the edges Gamma-encoded eight-bit arithmetic bands, and the banding appears after the third adjustment. Files: `src-tauri/src/pipeline/mod.rs`, `src-tauri/src/colour.rs` 2. Make spatial parameters relative to image size A radius in pixels is the reason previews lie. Files: `src-tauri/src/pipeline/ops.rs` 3. Compare export against preview in the suite Numerically, per operation, with a stated tolerance. Files: `src-tauri/tests/preview_matches_export.rs` ### Done when - [ ] Ten stacked adjustments produce no banding on a gradient - [ ] A downscaled export matches the preview within tolerance - [ ] Slider changes render within the frame budget on 24 megapixels - [ ] Crop composes correctly in any recipe position ### Watch out - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. ## Phase 3 · Masks, including the ones a model draws Adjustments apply to part of an image, and a model's mask is a starting point you can edit. ### Steps 1. Store parametric masks as parameters A brush stroke is points and a radius, not a six megapixel bitmap. Files: `src-tauri/src/masks.rs` 2. Make a model's mask editable It is a proposal, and a portrait needs the missing hairs. Files: `src-tauri/src/models/segment.rs`, `src/panels/MaskPanel.tsx` 3. Degrade to manual masks without the model Tested by deleting the model files, not by reading the code. Files: `src-tauri/tests/no_model.rs` ### Done when - [ ] Parametric masks re-render identically after reopening - [ ] A model mask can be refined by brush and the edit persists - [ ] Removing the model leaves the editor working with a stated reason - [ ] The process makes no network connections ### Watch out - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. ## Phase 4 · History, versions and doing it to a hundred files Any step can be undone or revisited, and a look transfers to other photographs without transferring the crop. ### Steps 1. Make the recipe the history Undo is a change to a list, not a stack of pixel buffers. Files: `src-tauri/src/recipe.rs`, `src/state/history.ts` 2. Transfer looks, not framing Global adjustments copy; crop and masks do not, and the skip is reported. Files: `src-tauri/src/transfer.rs` 3. Render batches in the background, cancellable With no partial file left behind on cancel. Files: `src-tauri/src/batch.rs` ### Done when - [ ] Fifty undos and redos reproduce the recipe exactly - [ ] A named version reopens to identical pixels - [ ] Recipe transfer skips crop and masks and says so - [ ] Cancelling a batch leaves no partial file ### Watch out - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. ## Not in this build - Raw decoding. - Generative sky or object replacement. - A cloud library or mobile companion. - Print and soft-proofing colour management. ## After v1, if you want it - Lens correction from a published profile database, applied as one more operation in the recipe - Local denoising and upscaling models under the same optional-model rule - A watched-folder import for a shoot in progress, which the reference-only library already suits ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. References and hashes. Disposable; the sidecars beside the files are the record. LIBRARY_DB=~/.local/share/photos/library.db # Required. Appended to the photograph's name. Never written into the photograph. SIDECAR_SUFFIX=.recipe.json # Required. The linear floating point space every operation runs in. WORKING_SPACE=linear_srgb # Required. Converted to and embedded on export. A file without one looks wrong everywhere. DEFAULT_EXPORT_PROFILE=sRGB # Required. Per export. Location stripping removes coordinates and nothing else. METADATA_POLICY_DEFAULT=keep_except_location # Optional. Optional local models. Empty means manual masks, which is a complete mode. MODELS_DIR=~/.local/share/photos/models # Required. Present as a constant. There is no other value. WRITE_ORIGINALS=never
You are building a lean indie version of Luminar Neo. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Luminar Neo · indie build A non-destructive photo editor whose hard problems are a pipeline that shows at preview size what it will produce at full size, colour handled in a working space rather than in whatever the file happened to be, and a recipe format that still opens years later, with the AI tools as optional local models rather than the foundation. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript for the interface only | the web view displays pixels and never processes them | | Pipeline | Rust, operating on a linear floating point working buffer | eight-bit gamma-encoded arithmetic bands after three adjustments | | Edits | An ordered recipe of operations in a versioned sidecar beside the file | the recipe is the document and the pixels are derived from it | | Masks | Parametric where possible, compressed bitmaps referenced from the sidecar otherwise | a full-resolution mask per adjustment makes the sidecar larger than the photograph | | Models | Optional local segmentation models loaded from disk | the editor is complete without them and a photograph never leaves the machine | | Formats | JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile | raw decoding is a separate multi-year project | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface and runs its tests. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Rust toolchain and the Tauri 2 prerequisites** · free - Why: The pixel pipeline runs in Rust, and the interface is a Tauri shell around it. - Get it: rustup.rs for the toolchain, then the Tauri 2 prerequisites page for your platform. - Verify: cargo --version prints and a blank Tauri 2 application launches - [ ] **A folder of real photographs including the awkward ones** · free - Why: A sideways portrait, a file with no profile, a 24 megapixel frame and a gradient sky are where orientation, colour and banding go wrong, and a stock sample has none of them. - Get it: Your own photographs, including at least one of each of those cases, in JPEG and TIFF. - Verify: At least a thousand files with the four awkward cases identified - [ ] **A second image viewer that honours embedded profiles** · free - Why: The only way to know an export's colour is right is to open it somewhere you did not write. - Get it: Any mature viewer or editor with colour management on your platform. - Verify: A known profiled fixture displays identically in it and in your editor - [ ] **A local segmentation model whose licence permits this use** (optional) · free - Why: Subject and sky masks come from it, and the editor must work identically when it is absent. - Get it: A published segmentation model in a format your runtime loads, with its licence file kept beside it. - Verify: It produces a mask for a portrait and the editor still works when the file is deleted ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Raw decoding. - Generative sky or object replacement. - A cloud library or mobile companion. - Print and soft-proofing colour management. - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling If one of those is essential to you, that is the reason to keep paying for Luminar Neo, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Luminar Neo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a photo editor that never touches the original and previews exactly what it exports to replace Luminar Neo. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is never modified. Not by export, which writes a new file. Not by a star rating or a caption, which live in the sidecar. Not by a crop, a rename or a batch. Every command the application exposes is followed in the test suite by a check that the bytes of the original are the bytes that were imported, and that check runs against every operation the editor has. The reason is that an edit is an opinion and a photograph is evidence of a moment that cannot be retaken. An editor that bakes an adjustment into the file has decided for you, permanently, and the first time a batch export overwrites a folder of originals is the last time that person trusts it with anything. The second rule follows from the first. The AI tools are optional models that run on this machine. The editor is complete without them, a missing model degrades to a manual mask rather than an error, and no photograph is ever sent anywhere to be processed. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript for the interface only · the web view displays pixels and never processes them - Pipeline: Rust, operating on a linear floating point working buffer · eight-bit gamma-encoded arithmetic bands after three adjustments - Edits: An ordered recipe of operations in a versioned sidecar beside the file · the recipe is the document and the pixels are derived from it - Masks: Parametric where possible, compressed bitmaps referenced from the sidecar otherwise · a full-resolution mask per adjustment makes the sidecar larger than the photograph - Models: Optional local segmentation models loaded from disk · the editor is complete without them and a photograph never leaves the machine - Formats: JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile · raw decoding is a separate multi-year project ### Data model (create this before Phase 1) - photos(id, path, content_hash, width, height, orientation, embedded_profile, imported_at) · a reference to the original, never a copy of it - recipes(photo_id, schema_version, ops_json, updated_at) · the sidecar, one per photograph, operations in application order - an op carries kind, params_json, mask_id and enabled, and every spatial parameter is a fraction of the image size rather than a count of pixels - masks(id, photo_id, kind, params_json, bitmap_ref) · brush and gradient masks are parametric; a model's output is a compressed bitmap - versions(photo_id, name, recipe_snapshot_json, created_at) · named states of the recipe, not copies of pixels - exports(id, photo_id, recipe_hash, output_path, format, profile, metadata_policy, at) · every export records what produced it - models(name, path, sha256, licence, loaded) · no rows means manual masks only, which is a complete mode - The original's hash is checked after every command in the suite, so the one promise this editor makes is tested rather than intended. ### Phase 1 · Importing without touching anything Build: Import JPEG, TIFF and PNG by reference. Read the orientation tag and the embedded colour profile, hash the file, and create a sidecar. Ratings, captions and every other piece of metadata go in the sidecar. Add a suite that hashes the original after every command the application exposes. Trap: Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. Done when: Importing a folder of a thousand photographs creates a sidecar per file and modifies no original, verified by hashing before and after. A portrait photographed sideways displays upright because the orientation tag was applied on import. The embedded profile is read and recorded, and a file without one is recorded as untagged rather than assumed. Every command in the application is followed in the suite by a hash comparison against the imported bytes. Do not build yet: No adjustments, masks, models, versions or export. ### Phase 2 · A preview that is the export, smaller Build: Build the pipeline in Rust over a linear floating point buffer: exposure, white balance, contrast, curves, hue and saturation per range, crop and straighten. Render the preview at viewport resolution and the export at full resolution through the same code, with every spatial parameter relative to image size. Assert in the suite that a downscaled export matches the preview. Trap: A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. Done when: Every adjustment operates on linear floating point data and a gradient survives ten stacked adjustments without visible banding, checked by histogram. An export downscaled to preview size matches the preview within a stated tolerance, asserted in the suite for every operation including the spatial ones. Moving a slider updates the preview within a frame budget on a 24 megapixel file without blocking the interface. Crop and straighten compose correctly with every other operation whatever their position in the recipe. Do not build yet: No masks, models, versions, batch or export to disk. ### Phase 3 · Masks, including the ones a model draws Build: Brush, gradient and luminosity masks stored parametrically. An optional local segmentation model that proposes a subject or sky mask, stored as a compressed bitmap the person then refines with the brush. With no model present the same panel offers manual masks and says why the button is absent. Nothing is uploaded anywhere. Trap: Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. Done when: Brush and gradient masks are stored as parameters and re-render identically after reopening. A model mask is a compressed bitmap that the brush can refine, and the refinement survives reopening. Removing the model files leaves the editor fully working with manual masks and the reason shown in the panel, tested by deleting them. The process makes no network connections, verified by running the editor with networking disabled. Do not build yet: No versions, batch, history or export yet. ### Phase 4 · History, versions and doing it to a hundred files Build: Treat the recipe as the history: undo removes or disables the last operation, redo restores it, and a named version snapshots the recipe. Copy a recipe to a selection of photographs, applying the global adjustments and skipping crop and masks, and say which were skipped. Render the batch through the same pipeline in the background with progress and cancellation. Trap: Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. Done when: Undo and redo across fifty operations reproduce the recipe exactly, asserted by comparing recipes. A named version reopens to the same pixels as when it was saved, compared numerically. Pasting a recipe to another photograph applies global adjustments, skips crop and masks, and reports what was skipped. A batch of a hundred files renders in the background, can be cancelled, and cancellation leaves no partial output file. Do not build yet: No export to disk, profile conversion or metadata policy yet. ### Phase 5 · Export that keeps the colour and survives the years Build: Export to JPEG and TIFF, converting from the working space to a chosen output profile and embedding it. Offer a metadata policy per export: keep everything, keep everything except location, or strip. Refuse to write to the original's path. Version the sidecar schema, migrate old versions on open, and keep a fixture sidecar from every schema version in the suite. Export the recipe itself as readable text. Trap: Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures. Done when: An exported JPEG carries the chosen profile and renders identically in a second application, verified against a fixture. The location-stripping policy removes coordinates and nothing else, checked by reading the metadata back. An export whose path resolves to the original is refused with the reason. A sidecar from every previous schema version opens, migrates and renders the same pixels, asserted against stored fixtures. Do not build yet: Nothing after this. ### Out of scope (and why) - Raw decoding, which is a per-camera project with its own demosaicing, profiles and lens tables, and where every open-source darkroom has spent a decade. - Generative replacement of skies or objects, which produces a picture of something that did not happen and belongs in a different kind of tool with a different label on the output. - A cloud library or mobile companion, which turns a private editor into a service holding other people's photographs. - Print and soft-proofing colour management, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That the original file is never modified, and the suite hashes it after every command - That preview and export run through one pipeline with spatial parameters relative to image size, so the export matches the preview - That processing happens in a linear floating point working space and converts to an embedded output profile on export - That the AI tools are optional local models, the editor is complete without them, and no photograph leaves the machine - That the sidecar schema is versioned with a fixture from every version in the suite, because the recipes are the years of work ===== AGENTS.md ===== # Agent instructions · Luminar Neo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript for the interface only, Rust, operating on a linear floating point working buffer, An ordered recipe of operations in a versioned sidecar beside the file, Parametric where possible, compressed bitmaps referenced from the sidecar otherwise, Optional local segmentation models loaded from disk, JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. ===== BUILD_PLAN.md ===== # Build plan · Luminar Neo A non-destructive photo editor whose hard problems are a pipeline that shows at preview size what it will produce at full size, colour handled in a working space rather than in whatever the file happened to be, and a recipe format that still opens years later, with the AI tools as optional local models rather than the foundation. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing without touching anything A photograph is referenced, oriented and profiled, and its bytes are never changed. ### Steps 1. Reference the file, never copy or rewrite it The path and the hash are what you keep; the bytes stay where they were. Files: `src-tauri/src/library.rs`, `src-tauri/src/sidecar.rs` 2. Apply orientation and read the profile on import A sideways portrait and an untagged file are both routine. Files: `src-tauri/src/decode.rs` 3. Hash the original after every command in the suite The promise is tested against each operation, not stated once. Files: `src-tauri/tests/untouched.rs` ### Done when - [ ] A thousand imports modify no original, verified by hash - [ ] Sideways photographs display upright - [ ] A missing profile is recorded as untagged, not assumed - [ ] Every command is followed by a hash check in the suite ### Watch out - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. ## Phase 2 · A preview that is the export, smaller What the screen shows at viewport size is what the file contains at full size. ### Steps 1. Work in linear float, convert at the edges Gamma-encoded eight-bit arithmetic bands, and the banding appears after the third adjustment. Files: `src-tauri/src/pipeline/mod.rs`, `src-tauri/src/colour.rs` 2. Make spatial parameters relative to image size A radius in pixels is the reason previews lie. Files: `src-tauri/src/pipeline/ops.rs` 3. Compare export against preview in the suite Numerically, per operation, with a stated tolerance. Files: `src-tauri/tests/preview_matches_export.rs` ### Done when - [ ] Ten stacked adjustments produce no banding on a gradient - [ ] A downscaled export matches the preview within tolerance - [ ] Slider changes render within the frame budget on 24 megapixels - [ ] Crop composes correctly in any recipe position ### Watch out - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. ## Phase 3 · Masks, including the ones a model draws Adjustments apply to part of an image, and a model's mask is a starting point you can edit. ### Steps 1. Store parametric masks as parameters A brush stroke is points and a radius, not a six megapixel bitmap. Files: `src-tauri/src/masks.rs` 2. Make a model's mask editable It is a proposal, and a portrait needs the missing hairs. Files: `src-tauri/src/models/segment.rs`, `src/panels/MaskPanel.tsx` 3. Degrade to manual masks without the model Tested by deleting the model files, not by reading the code. Files: `src-tauri/tests/no_model.rs` ### Done when - [ ] Parametric masks re-render identically after reopening - [ ] A model mask can be refined by brush and the edit persists - [ ] Removing the model leaves the editor working with a stated reason - [ ] The process makes no network connections ### Watch out - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. ## Phase 4 · History, versions and doing it to a hundred files Any step can be undone or revisited, and a look transfers to other photographs without transferring the crop. ### Steps 1. Make the recipe the history Undo is a change to a list, not a stack of pixel buffers. Files: `src-tauri/src/recipe.rs`, `src/state/history.ts` 2. Transfer looks, not framing Global adjustments copy; crop and masks do not, and the skip is reported. Files: `src-tauri/src/transfer.rs` 3. Render batches in the background, cancellable With no partial file left behind on cancel. Files: `src-tauri/src/batch.rs` ### Done when - [ ] Fifty undos and redos reproduce the recipe exactly - [ ] A named version reopens to identical pixels - [ ] Recipe transfer skips crop and masks and says so - [ ] Cancelling a batch leaves no partial file ### Watch out - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. ## Not in this build - Raw decoding. - Generative sky or object replacement. - A cloud library or mobile companion. - Print and soft-proofing colour management. ## After v1, if you want it - Lens correction from a published profile database, applied as one more operation in the recipe - Local denoising and upscaling models under the same optional-model rule - A watched-folder import for a shoot in progress, which the reference-only library already suits ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. References and hashes. Disposable; the sidecars beside the files are the record. LIBRARY_DB=~/.local/share/photos/library.db # Required. Appended to the photograph's name. Never written into the photograph. SIDECAR_SUFFIX=.recipe.json # Required. The linear floating point space every operation runs in. WORKING_SPACE=linear_srgb # Required. Converted to and embedded on export. A file without one looks wrong everywhere. DEFAULT_EXPORT_PROFILE=sRGB # Required. Per export. Location stripping removes coordinates and nothing else. METADATA_POLICY_DEFAULT=keep_except_location # Optional. Optional local models. Empty means manual masks, which is a complete mode. MODELS_DIR=~/.local/share/photos/models # Required. Present as a constant. There is no other value. WRITE_ORIGINALS=never
You are building a production product version of Luminar Neo. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Luminar Neo · product brief ## Problem The visible AI photo editor loop is buildable, but a credible replacement needs more than the first screen. Luminar Neo earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome An editor that has never modified an original, whose preview is the export at a smaller size, whose exports carry a real colour profile, and whose recipes will still open after the schema has moved on. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - sample user-owned assets ## Explicit non-goals for v1 - Raw decoding. - Generative sky or object replacement. - A cloud library or mobile companion. - Print and soft-proofing colour management. - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling ## Success criteria - No original is confirmed modified by any command, by hash - A downscaled export is confirmed to match the preview within tolerance - The editor is confirmed complete with the model files deleted - Exports are confirmed to carry an embedded profile and render identically elsewhere - Every past sidecar version is confirmed to open and render the same pixels ===== BRIEF.md ===== # Build brief · Luminar Neo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a photo editor that never touches the original and previews exactly what it exports to replace Luminar Neo. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is never modified. Not by export, which writes a new file. Not by a star rating or a caption, which live in the sidecar. Not by a crop, a rename or a batch. Every command the application exposes is followed in the test suite by a check that the bytes of the original are the bytes that were imported, and that check runs against every operation the editor has. The reason is that an edit is an opinion and a photograph is evidence of a moment that cannot be retaken. An editor that bakes an adjustment into the file has decided for you, permanently, and the first time a batch export overwrites a folder of originals is the last time that person trusts it with anything. The second rule follows from the first. The AI tools are optional models that run on this machine. The editor is complete without them, a missing model degrades to a manual mask rather than an error, and no photograph is ever sent anywhere to be processed. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript for the interface only · the web view displays pixels and never processes them - Pipeline: Rust, operating on a linear floating point working buffer · eight-bit gamma-encoded arithmetic bands after three adjustments - Edits: An ordered recipe of operations in a versioned sidecar beside the file · the recipe is the document and the pixels are derived from it - Masks: Parametric where possible, compressed bitmaps referenced from the sidecar otherwise · a full-resolution mask per adjustment makes the sidecar larger than the photograph - Models: Optional local segmentation models loaded from disk · the editor is complete without them and a photograph never leaves the machine - Formats: JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile · raw decoding is a separate multi-year project ### Data model (create this before Phase 1) - photos(id, path, content_hash, width, height, orientation, embedded_profile, imported_at) · a reference to the original, never a copy of it - recipes(photo_id, schema_version, ops_json, updated_at) · the sidecar, one per photograph, operations in application order - an op carries kind, params_json, mask_id and enabled, and every spatial parameter is a fraction of the image size rather than a count of pixels - masks(id, photo_id, kind, params_json, bitmap_ref) · brush and gradient masks are parametric; a model's output is a compressed bitmap - versions(photo_id, name, recipe_snapshot_json, created_at) · named states of the recipe, not copies of pixels - exports(id, photo_id, recipe_hash, output_path, format, profile, metadata_policy, at) · every export records what produced it - models(name, path, sha256, licence, loaded) · no rows means manual masks only, which is a complete mode - The original's hash is checked after every command in the suite, so the one promise this editor makes is tested rather than intended. ### Phase 1 · Importing without touching anything Build: Import JPEG, TIFF and PNG by reference. Read the orientation tag and the embedded colour profile, hash the file, and create a sidecar. Ratings, captions and every other piece of metadata go in the sidecar. Add a suite that hashes the original after every command the application exposes. Trap: Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. Done when: Importing a folder of a thousand photographs creates a sidecar per file and modifies no original, verified by hashing before and after. A portrait photographed sideways displays upright because the orientation tag was applied on import. The embedded profile is read and recorded, and a file without one is recorded as untagged rather than assumed. Every command in the application is followed in the suite by a hash comparison against the imported bytes. Do not build yet: No adjustments, masks, models, versions or export. ### Phase 2 · A preview that is the export, smaller Build: Build the pipeline in Rust over a linear floating point buffer: exposure, white balance, contrast, curves, hue and saturation per range, crop and straighten. Render the preview at viewport resolution and the export at full resolution through the same code, with every spatial parameter relative to image size. Assert in the suite that a downscaled export matches the preview. Trap: A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. Done when: Every adjustment operates on linear floating point data and a gradient survives ten stacked adjustments without visible banding, checked by histogram. An export downscaled to preview size matches the preview within a stated tolerance, asserted in the suite for every operation including the spatial ones. Moving a slider updates the preview within a frame budget on a 24 megapixel file without blocking the interface. Crop and straighten compose correctly with every other operation whatever their position in the recipe. Do not build yet: No masks, models, versions, batch or export to disk. ### Phase 3 · Masks, including the ones a model draws Build: Brush, gradient and luminosity masks stored parametrically. An optional local segmentation model that proposes a subject or sky mask, stored as a compressed bitmap the person then refines with the brush. With no model present the same panel offers manual masks and says why the button is absent. Nothing is uploaded anywhere. Trap: Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. Done when: Brush and gradient masks are stored as parameters and re-render identically after reopening. A model mask is a compressed bitmap that the brush can refine, and the refinement survives reopening. Removing the model files leaves the editor fully working with manual masks and the reason shown in the panel, tested by deleting them. The process makes no network connections, verified by running the editor with networking disabled. Do not build yet: No versions, batch, history or export yet. ### Phase 4 · History, versions and doing it to a hundred files Build: Treat the recipe as the history: undo removes or disables the last operation, redo restores it, and a named version snapshots the recipe. Copy a recipe to a selection of photographs, applying the global adjustments and skipping crop and masks, and say which were skipped. Render the batch through the same pipeline in the background with progress and cancellation. Trap: Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. Done when: Undo and redo across fifty operations reproduce the recipe exactly, asserted by comparing recipes. A named version reopens to the same pixels as when it was saved, compared numerically. Pasting a recipe to another photograph applies global adjustments, skips crop and masks, and reports what was skipped. A batch of a hundred files renders in the background, can be cancelled, and cancellation leaves no partial output file. Do not build yet: No export to disk, profile conversion or metadata policy yet. ### Phase 5 · Export that keeps the colour and survives the years Build: Export to JPEG and TIFF, converting from the working space to a chosen output profile and embedding it. Offer a metadata policy per export: keep everything, keep everything except location, or strip. Refuse to write to the original's path. Version the sidecar schema, migrate old versions on open, and keep a fixture sidecar from every schema version in the suite. Export the recipe itself as readable text. Trap: Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures. Done when: An exported JPEG carries the chosen profile and renders identically in a second application, verified against a fixture. The location-stripping policy removes coordinates and nothing else, checked by reading the metadata back. An export whose path resolves to the original is refused with the reason. A sidecar from every previous schema version opens, migrates and renders the same pixels, asserted against stored fixtures. Do not build yet: Nothing after this. ### Out of scope (and why) - Raw decoding, which is a per-camera project with its own demosaicing, profiles and lens tables, and where every open-source darkroom has spent a decade. - Generative replacement of skies or objects, which produces a picture of something that did not happen and belongs in a different kind of tool with a different label on the output. - A cloud library or mobile companion, which turns a private editor into a service holding other people's photographs. - Print and soft-proofing colour management, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That the original file is never modified, and the suite hashes it after every command - That preview and export run through one pipeline with spatial parameters relative to image size, so the export matches the preview - That processing happens in a linear floating point working space and converts to an embedded output profile on export - That the AI tools are optional local models, the editor is complete without them, and no photograph leaves the machine - That the sidecar schema is versioned with a fixture from every version in the suite, because the recipes are the years of work ===== ARCHITECTURE.md ===== # Architecture · Luminar Neo ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript for the interface only | the web view displays pixels and never processes them | | Pipeline | Rust, operating on a linear floating point working buffer | eight-bit gamma-encoded arithmetic bands after three adjustments | | Edits | An ordered recipe of operations in a versioned sidecar beside the file | the recipe is the document and the pixels are derived from it | | Masks | Parametric where possible, compressed bitmaps referenced from the sidecar otherwise | a full-resolution mask per adjustment makes the sidecar larger than the photograph | | Models | Optional local segmentation models loaded from disk | the editor is complete without them and a photograph never leaves the machine | | Formats | JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile | raw decoding is a separate multi-year project | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Library | References, hashes and sidecars. | Any store, provided the original is referenced and never rewritten. | | Pipeline | Linear float operations at any resolution. | Any implementation, provided preview and export share one code path. | | Masks | Parametric masks and editable model bitmaps. | Any model, provided its output is a proposal the brush can change. | | Recipe | History, versions and transfer. | Any format, provided every schema version has a fixture in the suite. | | Export | Profile conversion and metadata policy. | Any encoder, provided the profile is embedded and the original's path is refused. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_DB` · required · References and hashes. Disposable; the sidecars beside the files are the record. - `SIDECAR_SUFFIX` · required · Appended to the photograph's name. Never written into the photograph. - `WORKING_SPACE` · required · The linear floating point space every operation runs in. - `DEFAULT_EXPORT_PROFILE` · required · Converted to and embedded on export. A file without one looks wrong everywhere. - `METADATA_POLICY_DEFAULT` · required · Per export. Location stripping removes coordinates and nothing else. - `MODELS_DIR` · optional · Optional local models. Empty means manual masks, which is a complete mode. - `WRITE_ORIGINALS` · required · Present as a constant. There is no other value. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Luminar Neo product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript for the interface only, Rust, operating on a linear floating point working buffer, An ordered recipe of operations in a versioned sidecar beside the file, Parametric where possible, compressed bitmaps referenced from the sidecar otherwise, Optional local segmentation models loaded from disk, JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. - Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures. ===== MILESTONES.md ===== # Delivery milestones · Luminar Neo Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Importing without touching anything A photograph is referenced, oriented and profiled, and its bytes are never changed. ### Steps 1. Reference the file, never copy or rewrite it The path and the hash are what you keep; the bytes stay where they were. Files: `src-tauri/src/library.rs`, `src-tauri/src/sidecar.rs` 2. Apply orientation and read the profile on import A sideways portrait and an untagged file are both routine. Files: `src-tauri/src/decode.rs` 3. Hash the original after every command in the suite The promise is tested against each operation, not stated once. Files: `src-tauri/tests/untouched.rs` ### Done when - [ ] A thousand imports modify no original, verified by hash - [ ] Sideways photographs display upright - [ ] A missing profile is recorded as untagged, not assumed - [ ] Every command is followed by a hash check in the suite ### Watch out - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. ## M2 · A preview that is the export, smaller What the screen shows at viewport size is what the file contains at full size. ### Steps 1. Work in linear float, convert at the edges Gamma-encoded eight-bit arithmetic bands, and the banding appears after the third adjustment. Files: `src-tauri/src/pipeline/mod.rs`, `src-tauri/src/colour.rs` 2. Make spatial parameters relative to image size A radius in pixels is the reason previews lie. Files: `src-tauri/src/pipeline/ops.rs` 3. Compare export against preview in the suite Numerically, per operation, with a stated tolerance. Files: `src-tauri/tests/preview_matches_export.rs` ### Done when - [ ] Ten stacked adjustments produce no banding on a gradient - [ ] A downscaled export matches the preview within tolerance - [ ] Slider changes render within the frame budget on 24 megapixels - [ ] Crop composes correctly in any recipe position ### Watch out - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. ## M3 · Masks, including the ones a model draws Adjustments apply to part of an image, and a model's mask is a starting point you can edit. ### Steps 1. Store parametric masks as parameters A brush stroke is points and a radius, not a six megapixel bitmap. Files: `src-tauri/src/masks.rs` 2. Make a model's mask editable It is a proposal, and a portrait needs the missing hairs. Files: `src-tauri/src/models/segment.rs`, `src/panels/MaskPanel.tsx` 3. Degrade to manual masks without the model Tested by deleting the model files, not by reading the code. Files: `src-tauri/tests/no_model.rs` ### Done when - [ ] Parametric masks re-render identically after reopening - [ ] A model mask can be refined by brush and the edit persists - [ ] Removing the model leaves the editor working with a stated reason - [ ] The process makes no network connections ### Watch out - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. ## M4 · History, versions and doing it to a hundred files Any step can be undone or revisited, and a look transfers to other photographs without transferring the crop. ### Steps 1. Make the recipe the history Undo is a change to a list, not a stack of pixel buffers. Files: `src-tauri/src/recipe.rs`, `src/state/history.ts` 2. Transfer looks, not framing Global adjustments copy; crop and masks do not, and the skip is reported. Files: `src-tauri/src/transfer.rs` 3. Render batches in the background, cancellable With no partial file left behind on cancel. Files: `src-tauri/src/batch.rs` ### Done when - [ ] Fifty undos and redos reproduce the recipe exactly - [ ] A named version reopens to identical pixels - [ ] Recipe transfer skips crop and masks and says so - [ ] Cancelling a batch leaves no partial file ### Watch out - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. ## M5 · Export that keeps the colour and survives the years (production only) Files leave with the right profile and the chosen metadata, and today's sidecar opens in a future version. ### Steps 1. Convert to an output profile and embed it A file without a colour space looks wrong everywhere. Files: `src-tauri/src/export.rs`, `src-tauri/src/colour.rs` 2. Implement the metadata policy and read it back Location stripping removes coordinates and nothing else. Files: `src-tauri/src/metadata.rs`, `src-tauri/tests/metadata_policy.rs` 3. Keep a fixture sidecar per schema version The recipes are the years of work; the pixels are reproducible from them. Files: `src-tauri/src/sidecar_migrate.rs`, `fixtures/sidecars/` ### Done when - [ ] Exports carry the profile and render identically elsewhere - [ ] Location stripping removes only coordinates - [ ] Writing to the original's path is refused - [ ] Every past sidecar version opens and renders the same pixels ### Watch out - Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures. ===== OPERATIONS.md ===== # Operations · Luminar Neo ## Backup The originals are already in whatever backup the photographer has. The sidecars are small, sit beside them and are the years of editing decisions, so confirm the backup includes them rather than only the image formats. ## Restore Restore and confirm each sidecar still resolves to its photograph by hash, since a restored folder with renamed files leaves recipes pointing at nothing, and the hash is what reconnects them. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: the original untouched after every command, the preview matching the export, and every past sidecar version opening. Preview time on the largest file is the number that degrades quietly. ## Incident checklist A modified original is the incident this build exists to prevent, and if it happens the hash log says which command did it. A wrong-looking export is almost always a missing profile rather than a wrong pipeline, and the second viewer settles it in seconds. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No original is confirmed modified by any command, by hash - [ ] A downscaled export is confirmed to match the preview within tolerance - [ ] The editor is confirmed complete with the model files deleted - [ ] Exports are confirmed to carry an embedded profile and render identically elsewhere - [ ] Every past sidecar version is confirmed to open and render the same pixels ## Launch constraint Do not market omitted Luminar Neo capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. References and hashes. Disposable; the sidecars beside the files are the record. LIBRARY_DB=~/.local/share/photos/library.db # Required. Appended to the photograph's name. Never written into the photograph. SIDECAR_SUFFIX=.recipe.json # Required. The linear floating point space every operation runs in. WORKING_SPACE=linear_srgb # Required. Converted to and embedded on export. A file without one looks wrong everywhere. DEFAULT_EXPORT_PROFILE=sRGB # Required. Per export. Location stripping removes coordinates and nothing else. METADATA_POLICY_DEFAULT=keep_except_location # Optional. Optional local models. Empty means manual masks, which is a complete mode. MODELS_DIR=~/.local/share/photos/models # Required. Present as a constant. There is no other value. WRITE_ORIGINALS=never
# Luminar Neo · indie build A non-destructive photo editor whose hard problems are a pipeline that shows at preview size what it will produce at full size, colour handled in a working space rather than in whatever the file happened to be, and a recipe format that still opens years later, with the AI tools as optional local models rather than the foundation. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript for the interface only | the web view displays pixels and never processes them | | Pipeline | Rust, operating on a linear floating point working buffer | eight-bit gamma-encoded arithmetic bands after three adjustments | | Edits | An ordered recipe of operations in a versioned sidecar beside the file | the recipe is the document and the pixels are derived from it | | Masks | Parametric where possible, compressed bitmaps referenced from the sidecar otherwise | a full-resolution mask per adjustment makes the sidecar larger than the photograph | | Models | Optional local segmentation models loaded from disk | the editor is complete without them and a photograph never leaves the machine | | Formats | JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile | raw decoding is a separate multi-year project | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface and runs its tests. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Rust toolchain and the Tauri 2 prerequisites** · free - Why: The pixel pipeline runs in Rust, and the interface is a Tauri shell around it. - Get it: rustup.rs for the toolchain, then the Tauri 2 prerequisites page for your platform. - Verify: cargo --version prints and a blank Tauri 2 application launches - [ ] **A folder of real photographs including the awkward ones** · free - Why: A sideways portrait, a file with no profile, a 24 megapixel frame and a gradient sky are where orientation, colour and banding go wrong, and a stock sample has none of them. - Get it: Your own photographs, including at least one of each of those cases, in JPEG and TIFF. - Verify: At least a thousand files with the four awkward cases identified - [ ] **A second image viewer that honours embedded profiles** · free - Why: The only way to know an export's colour is right is to open it somewhere you did not write. - Get it: Any mature viewer or editor with colour management on your platform. - Verify: A known profiled fixture displays identically in it and in your editor - [ ] **A local segmentation model whose licence permits this use** (optional) · free - Why: Subject and sky masks come from it, and the editor must work identically when it is absent. - Get it: A published segmentation model in a format your runtime loads, with its licence file kept beside it. - Verify: It produces a mask for a portrait and the editor still works when the file is deleted ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Raw decoding. - Generative sky or object replacement. - A cloud library or mobile companion. - Print and soft-proofing colour management. - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling If one of those is essential to you, that is the reason to keep paying for Luminar Neo, and the README should say so rather than pretend.
# Build brief · Luminar Neo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a photo editor that never touches the original and previews exactly what it exports to replace Luminar Neo. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is never modified. Not by export, which writes a new file. Not by a star rating or a caption, which live in the sidecar. Not by a crop, a rename or a batch. Every command the application exposes is followed in the test suite by a check that the bytes of the original are the bytes that were imported, and that check runs against every operation the editor has. The reason is that an edit is an opinion and a photograph is evidence of a moment that cannot be retaken. An editor that bakes an adjustment into the file has decided for you, permanently, and the first time a batch export overwrites a folder of originals is the last time that person trusts it with anything. The second rule follows from the first. The AI tools are optional models that run on this machine. The editor is complete without them, a missing model degrades to a manual mask rather than an error, and no photograph is ever sent anywhere to be processed. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript for the interface only · the web view displays pixels and never processes them - Pipeline: Rust, operating on a linear floating point working buffer · eight-bit gamma-encoded arithmetic bands after three adjustments - Edits: An ordered recipe of operations in a versioned sidecar beside the file · the recipe is the document and the pixels are derived from it - Masks: Parametric where possible, compressed bitmaps referenced from the sidecar otherwise · a full-resolution mask per adjustment makes the sidecar larger than the photograph - Models: Optional local segmentation models loaded from disk · the editor is complete without them and a photograph never leaves the machine - Formats: JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile · raw decoding is a separate multi-year project ### Data model (create this before Phase 1) - photos(id, path, content_hash, width, height, orientation, embedded_profile, imported_at) · a reference to the original, never a copy of it - recipes(photo_id, schema_version, ops_json, updated_at) · the sidecar, one per photograph, operations in application order - an op carries kind, params_json, mask_id and enabled, and every spatial parameter is a fraction of the image size rather than a count of pixels - masks(id, photo_id, kind, params_json, bitmap_ref) · brush and gradient masks are parametric; a model's output is a compressed bitmap - versions(photo_id, name, recipe_snapshot_json, created_at) · named states of the recipe, not copies of pixels - exports(id, photo_id, recipe_hash, output_path, format, profile, metadata_policy, at) · every export records what produced it - models(name, path, sha256, licence, loaded) · no rows means manual masks only, which is a complete mode - The original's hash is checked after every command in the suite, so the one promise this editor makes is tested rather than intended. ### Phase 1 · Importing without touching anything Build: Import JPEG, TIFF and PNG by reference. Read the orientation tag and the embedded colour profile, hash the file, and create a sidecar. Ratings, captions and every other piece of metadata go in the sidecar. Add a suite that hashes the original after every command the application exposes. Trap: Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. Done when: Importing a folder of a thousand photographs creates a sidecar per file and modifies no original, verified by hashing before and after. A portrait photographed sideways displays upright because the orientation tag was applied on import. The embedded profile is read and recorded, and a file without one is recorded as untagged rather than assumed. Every command in the application is followed in the suite by a hash comparison against the imported bytes. Do not build yet: No adjustments, masks, models, versions or export. ### Phase 2 · A preview that is the export, smaller Build: Build the pipeline in Rust over a linear floating point buffer: exposure, white balance, contrast, curves, hue and saturation per range, crop and straighten. Render the preview at viewport resolution and the export at full resolution through the same code, with every spatial parameter relative to image size. Assert in the suite that a downscaled export matches the preview. Trap: A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. Done when: Every adjustment operates on linear floating point data and a gradient survives ten stacked adjustments without visible banding, checked by histogram. An export downscaled to preview size matches the preview within a stated tolerance, asserted in the suite for every operation including the spatial ones. Moving a slider updates the preview within a frame budget on a 24 megapixel file without blocking the interface. Crop and straighten compose correctly with every other operation whatever their position in the recipe. Do not build yet: No masks, models, versions, batch or export to disk. ### Phase 3 · Masks, including the ones a model draws Build: Brush, gradient and luminosity masks stored parametrically. An optional local segmentation model that proposes a subject or sky mask, stored as a compressed bitmap the person then refines with the brush. With no model present the same panel offers manual masks and says why the button is absent. Nothing is uploaded anywhere. Trap: Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. Done when: Brush and gradient masks are stored as parameters and re-render identically after reopening. A model mask is a compressed bitmap that the brush can refine, and the refinement survives reopening. Removing the model files leaves the editor fully working with manual masks and the reason shown in the panel, tested by deleting them. The process makes no network connections, verified by running the editor with networking disabled. Do not build yet: No versions, batch, history or export yet. ### Phase 4 · History, versions and doing it to a hundred files Build: Treat the recipe as the history: undo removes or disables the last operation, redo restores it, and a named version snapshots the recipe. Copy a recipe to a selection of photographs, applying the global adjustments and skipping crop and masks, and say which were skipped. Render the batch through the same pipeline in the background with progress and cancellation. Trap: Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. Done when: Undo and redo across fifty operations reproduce the recipe exactly, asserted by comparing recipes. A named version reopens to the same pixels as when it was saved, compared numerically. Pasting a recipe to another photograph applies global adjustments, skips crop and masks, and reports what was skipped. A batch of a hundred files renders in the background, can be cancelled, and cancellation leaves no partial output file. Do not build yet: No export to disk, profile conversion or metadata policy yet. ### Phase 5 · Export that keeps the colour and survives the years Build: Export to JPEG and TIFF, converting from the working space to a chosen output profile and embedding it. Offer a metadata policy per export: keep everything, keep everything except location, or strip. Refuse to write to the original's path. Version the sidecar schema, migrate old versions on open, and keep a fixture sidecar from every schema version in the suite. Export the recipe itself as readable text. Trap: Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures. Done when: An exported JPEG carries the chosen profile and renders identically in a second application, verified against a fixture. The location-stripping policy removes coordinates and nothing else, checked by reading the metadata back. An export whose path resolves to the original is refused with the reason. A sidecar from every previous schema version opens, migrates and renders the same pixels, asserted against stored fixtures. Do not build yet: Nothing after this. ### Out of scope (and why) - Raw decoding, which is a per-camera project with its own demosaicing, profiles and lens tables, and where every open-source darkroom has spent a decade. - Generative replacement of skies or objects, which produces a picture of something that did not happen and belongs in a different kind of tool with a different label on the output. - A cloud library or mobile companion, which turns a private editor into a service holding other people's photographs. - Print and soft-proofing colour management, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That the original file is never modified, and the suite hashes it after every command - That preview and export run through one pipeline with spatial parameters relative to image size, so the export matches the preview - That processing happens in a linear floating point working space and converts to an embedded output profile on export - That the AI tools are optional local models, the editor is complete without them, and no photograph leaves the machine - That the sidecar schema is versioned with a fixture from every version in the suite, because the recipes are the years of work
# Agent instructions · Luminar Neo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript for the interface only, Rust, operating on a linear floating point working buffer, An ordered recipe of operations in a versioned sidecar beside the file, Parametric where possible, compressed bitmaps referenced from the sidecar otherwise, Optional local segmentation models loaded from disk, JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand.
# Build plan · Luminar Neo A non-destructive photo editor whose hard problems are a pipeline that shows at preview size what it will produce at full size, colour handled in a working space rather than in whatever the file happened to be, and a recipe format that still opens years later, with the AI tools as optional local models rather than the foundation. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing without touching anything A photograph is referenced, oriented and profiled, and its bytes are never changed. ### Steps 1. Reference the file, never copy or rewrite it The path and the hash are what you keep; the bytes stay where they were. Files: `src-tauri/src/library.rs`, `src-tauri/src/sidecar.rs` 2. Apply orientation and read the profile on import A sideways portrait and an untagged file are both routine. Files: `src-tauri/src/decode.rs` 3. Hash the original after every command in the suite The promise is tested against each operation, not stated once. Files: `src-tauri/tests/untouched.rs` ### Done when - [ ] A thousand imports modify no original, verified by hash - [ ] Sideways photographs display upright - [ ] A missing profile is recorded as untagged, not assumed - [ ] Every command is followed by a hash check in the suite ### Watch out - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. ## Phase 2 · A preview that is the export, smaller What the screen shows at viewport size is what the file contains at full size. ### Steps 1. Work in linear float, convert at the edges Gamma-encoded eight-bit arithmetic bands, and the banding appears after the third adjustment. Files: `src-tauri/src/pipeline/mod.rs`, `src-tauri/src/colour.rs` 2. Make spatial parameters relative to image size A radius in pixels is the reason previews lie. Files: `src-tauri/src/pipeline/ops.rs` 3. Compare export against preview in the suite Numerically, per operation, with a stated tolerance. Files: `src-tauri/tests/preview_matches_export.rs` ### Done when - [ ] Ten stacked adjustments produce no banding on a gradient - [ ] A downscaled export matches the preview within tolerance - [ ] Slider changes render within the frame budget on 24 megapixels - [ ] Crop composes correctly in any recipe position ### Watch out - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. ## Phase 3 · Masks, including the ones a model draws Adjustments apply to part of an image, and a model's mask is a starting point you can edit. ### Steps 1. Store parametric masks as parameters A brush stroke is points and a radius, not a six megapixel bitmap. Files: `src-tauri/src/masks.rs` 2. Make a model's mask editable It is a proposal, and a portrait needs the missing hairs. Files: `src-tauri/src/models/segment.rs`, `src/panels/MaskPanel.tsx` 3. Degrade to manual masks without the model Tested by deleting the model files, not by reading the code. Files: `src-tauri/tests/no_model.rs` ### Done when - [ ] Parametric masks re-render identically after reopening - [ ] A model mask can be refined by brush and the edit persists - [ ] Removing the model leaves the editor working with a stated reason - [ ] The process makes no network connections ### Watch out - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. ## Phase 4 · History, versions and doing it to a hundred files Any step can be undone or revisited, and a look transfers to other photographs without transferring the crop. ### Steps 1. Make the recipe the history Undo is a change to a list, not a stack of pixel buffers. Files: `src-tauri/src/recipe.rs`, `src/state/history.ts` 2. Transfer looks, not framing Global adjustments copy; crop and masks do not, and the skip is reported. Files: `src-tauri/src/transfer.rs` 3. Render batches in the background, cancellable With no partial file left behind on cancel. Files: `src-tauri/src/batch.rs` ### Done when - [ ] Fifty undos and redos reproduce the recipe exactly - [ ] A named version reopens to identical pixels - [ ] Recipe transfer skips crop and masks and says so - [ ] Cancelling a batch leaves no partial file ### Watch out - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. ## Not in this build - Raw decoding. - Generative sky or object replacement. - A cloud library or mobile companion. - Print and soft-proofing colour management. ## After v1, if you want it - Lens correction from a published profile database, applied as one more operation in the recipe - Local denoising and upscaling models under the same optional-model rule - A watched-folder import for a shoot in progress, which the reference-only library already suits
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. References and hashes. Disposable; the sidecars beside the files are the record. LIBRARY_DB=~/.local/share/photos/library.db # Required. Appended to the photograph's name. Never written into the photograph. SIDECAR_SUFFIX=.recipe.json # Required. The linear floating point space every operation runs in. WORKING_SPACE=linear_srgb # Required. Converted to and embedded on export. A file without one looks wrong everywhere. DEFAULT_EXPORT_PROFILE=sRGB # Required. Per export. Location stripping removes coordinates and nothing else. METADATA_POLICY_DEFAULT=keep_except_location # Optional. Optional local models. Empty means manual masks, which is a complete mode. MODELS_DIR=~/.local/share/photos/models # Required. Present as a constant. There is no other value. WRITE_ORIGINALS=never
# Luminar Neo · product brief ## Problem The visible AI photo editor loop is buildable, but a credible replacement needs more than the first screen. Luminar Neo earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome An editor that has never modified an original, whose preview is the export at a smaller size, whose exports carry a real colour profile, and whose recipes will still open after the schema has moved on. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - sample user-owned assets ## Explicit non-goals for v1 - Raw decoding. - Generative sky or object replacement. - A cloud library or mobile companion. - Print and soft-proofing colour management. - large template and asset libraries - color-management edge cases - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling ## Success criteria - No original is confirmed modified by any command, by hash - A downscaled export is confirmed to match the preview within tolerance - The editor is confirmed complete with the model files deleted - Exports are confirmed to carry an embedded profile and render identically elsewhere - Every past sidecar version is confirmed to open and render the same pixels
# Build brief · Luminar Neo The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a photo editor that never touches the original and previews exactly what it exports to replace Luminar Neo. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is never modified. Not by export, which writes a new file. Not by a star rating or a caption, which live in the sidecar. Not by a crop, a rename or a batch. Every command the application exposes is followed in the test suite by a check that the bytes of the original are the bytes that were imported, and that check runs against every operation the editor has. The reason is that an edit is an opinion and a photograph is evidence of a moment that cannot be retaken. An editor that bakes an adjustment into the file has decided for you, permanently, and the first time a batch export overwrites a folder of originals is the last time that person trusts it with anything. The second rule follows from the first. The AI tools are optional models that run on this machine. The editor is complete without them, a missing model degrades to a manual mask rather than an error, and no photograph is ever sent anywhere to be processed. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript for the interface only · the web view displays pixels and never processes them - Pipeline: Rust, operating on a linear floating point working buffer · eight-bit gamma-encoded arithmetic bands after three adjustments - Edits: An ordered recipe of operations in a versioned sidecar beside the file · the recipe is the document and the pixels are derived from it - Masks: Parametric where possible, compressed bitmaps referenced from the sidecar otherwise · a full-resolution mask per adjustment makes the sidecar larger than the photograph - Models: Optional local segmentation models loaded from disk · the editor is complete without them and a photograph never leaves the machine - Formats: JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile · raw decoding is a separate multi-year project ### Data model (create this before Phase 1) - photos(id, path, content_hash, width, height, orientation, embedded_profile, imported_at) · a reference to the original, never a copy of it - recipes(photo_id, schema_version, ops_json, updated_at) · the sidecar, one per photograph, operations in application order - an op carries kind, params_json, mask_id and enabled, and every spatial parameter is a fraction of the image size rather than a count of pixels - masks(id, photo_id, kind, params_json, bitmap_ref) · brush and gradient masks are parametric; a model's output is a compressed bitmap - versions(photo_id, name, recipe_snapshot_json, created_at) · named states of the recipe, not copies of pixels - exports(id, photo_id, recipe_hash, output_path, format, profile, metadata_policy, at) · every export records what produced it - models(name, path, sha256, licence, loaded) · no rows means manual masks only, which is a complete mode - The original's hash is checked after every command in the suite, so the one promise this editor makes is tested rather than intended. ### Phase 1 · Importing without touching anything Build: Import JPEG, TIFF and PNG by reference. Read the orientation tag and the embedded colour profile, hash the file, and create a sidecar. Ratings, captions and every other piece of metadata go in the sidecar. Add a suite that hashes the original after every command the application exposes. Trap: Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. Done when: Importing a folder of a thousand photographs creates a sidecar per file and modifies no original, verified by hashing before and after. A portrait photographed sideways displays upright because the orientation tag was applied on import. The embedded profile is read and recorded, and a file without one is recorded as untagged rather than assumed. Every command in the application is followed in the suite by a hash comparison against the imported bytes. Do not build yet: No adjustments, masks, models, versions or export. ### Phase 2 · A preview that is the export, smaller Build: Build the pipeline in Rust over a linear floating point buffer: exposure, white balance, contrast, curves, hue and saturation per range, crop and straighten. Render the preview at viewport resolution and the export at full resolution through the same code, with every spatial parameter relative to image size. Assert in the suite that a downscaled export matches the preview. Trap: A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. Done when: Every adjustment operates on linear floating point data and a gradient survives ten stacked adjustments without visible banding, checked by histogram. An export downscaled to preview size matches the preview within a stated tolerance, asserted in the suite for every operation including the spatial ones. Moving a slider updates the preview within a frame budget on a 24 megapixel file without blocking the interface. Crop and straighten compose correctly with every other operation whatever their position in the recipe. Do not build yet: No masks, models, versions, batch or export to disk. ### Phase 3 · Masks, including the ones a model draws Build: Brush, gradient and luminosity masks stored parametrically. An optional local segmentation model that proposes a subject or sky mask, stored as a compressed bitmap the person then refines with the brush. With no model present the same panel offers manual masks and says why the button is absent. Nothing is uploaded anywhere. Trap: Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. Done when: Brush and gradient masks are stored as parameters and re-render identically after reopening. A model mask is a compressed bitmap that the brush can refine, and the refinement survives reopening. Removing the model files leaves the editor fully working with manual masks and the reason shown in the panel, tested by deleting them. The process makes no network connections, verified by running the editor with networking disabled. Do not build yet: No versions, batch, history or export yet. ### Phase 4 · History, versions and doing it to a hundred files Build: Treat the recipe as the history: undo removes or disables the last operation, redo restores it, and a named version snapshots the recipe. Copy a recipe to a selection of photographs, applying the global adjustments and skipping crop and masks, and say which were skipped. Render the batch through the same pipeline in the background with progress and cancellation. Trap: Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. Done when: Undo and redo across fifty operations reproduce the recipe exactly, asserted by comparing recipes. A named version reopens to the same pixels as when it was saved, compared numerically. Pasting a recipe to another photograph applies global adjustments, skips crop and masks, and reports what was skipped. A batch of a hundred files renders in the background, can be cancelled, and cancellation leaves no partial output file. Do not build yet: No export to disk, profile conversion or metadata policy yet. ### Phase 5 · Export that keeps the colour and survives the years Build: Export to JPEG and TIFF, converting from the working space to a chosen output profile and embedding it. Offer a metadata policy per export: keep everything, keep everything except location, or strip. Refuse to write to the original's path. Version the sidecar schema, migrate old versions on open, and keep a fixture sidecar from every schema version in the suite. Export the recipe itself as readable text. Trap: Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures. Done when: An exported JPEG carries the chosen profile and renders identically in a second application, verified against a fixture. The location-stripping policy removes coordinates and nothing else, checked by reading the metadata back. An export whose path resolves to the original is refused with the reason. A sidecar from every previous schema version opens, migrates and renders the same pixels, asserted against stored fixtures. Do not build yet: Nothing after this. ### Out of scope (and why) - Raw decoding, which is a per-camera project with its own demosaicing, profiles and lens tables, and where every open-source darkroom has spent a decade. - Generative replacement of skies or objects, which produces a picture of something that did not happen and belongs in a different kind of tool with a different label on the output. - A cloud library or mobile companion, which turns a private editor into a service holding other people's photographs. - Print and soft-proofing colour management, which needs measured profiles and a calibrated display to mean anything. ### README must contain - That the original file is never modified, and the suite hashes it after every command - That preview and export run through one pipeline with spatial parameters relative to image size, so the export matches the preview - That processing happens in a linear floating point working space and converts to an embedded output profile on export - That the AI tools are optional local models, the editor is complete without them, and no photograph leaves the machine - That the sidecar schema is versioned with a fixture from every version in the suite, because the recipes are the years of work
# Architecture · Luminar Neo ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript for the interface only | the web view displays pixels and never processes them | | Pipeline | Rust, operating on a linear floating point working buffer | eight-bit gamma-encoded arithmetic bands after three adjustments | | Edits | An ordered recipe of operations in a versioned sidecar beside the file | the recipe is the document and the pixels are derived from it | | Masks | Parametric where possible, compressed bitmaps referenced from the sidecar otherwise | a full-resolution mask per adjustment makes the sidecar larger than the photograph | | Models | Optional local segmentation models loaded from disk | the editor is complete without them and a photograph never leaves the machine | | Formats | JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile | raw decoding is a separate multi-year project | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Library | References, hashes and sidecars. | Any store, provided the original is referenced and never rewritten. | | Pipeline | Linear float operations at any resolution. | Any implementation, provided preview and export share one code path. | | Masks | Parametric masks and editable model bitmaps. | Any model, provided its output is a proposal the brush can change. | | Recipe | History, versions and transfer. | Any format, provided every schema version has a fixture in the suite. | | Export | Profile conversion and metadata policy. | Any encoder, provided the profile is embedded and the original's path is refused. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_DB` · required · References and hashes. Disposable; the sidecars beside the files are the record. - `SIDECAR_SUFFIX` · required · Appended to the photograph's name. Never written into the photograph. - `WORKING_SPACE` · required · The linear floating point space every operation runs in. - `DEFAULT_EXPORT_PROFILE` · required · Converted to and embedded on export. A file without one looks wrong everywhere. - `METADATA_POLICY_DEFAULT` · required · Per export. Location stripping removes coordinates and nothing else. - `MODELS_DIR` · optional · Optional local models. Empty means manual masks, which is a complete mode. - `WRITE_ORIGINALS` · required · Present as a constant. There is no other value. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Luminar Neo product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript for the interface only, Rust, operating on a linear floating point working buffer, An ordered recipe of operations in a versioned sidecar beside the file, Parametric where possible, compressed bitmaps referenced from the sidecar otherwise, Optional local segmentation models loaded from disk, JPEG, TIFF and PNG in, JPEG and TIFF out with an embedded profile. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. - Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures.
# Delivery milestones · Luminar Neo Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Importing without touching anything A photograph is referenced, oriented and profiled, and its bytes are never changed. ### Steps 1. Reference the file, never copy or rewrite it The path and the hash are what you keep; the bytes stay where they were. Files: `src-tauri/src/library.rs`, `src-tauri/src/sidecar.rs` 2. Apply orientation and read the profile on import A sideways portrait and an untagged file are both routine. Files: `src-tauri/src/decode.rs` 3. Hash the original after every command in the suite The promise is tested against each operation, not stated once. Files: `src-tauri/tests/untouched.rs` ### Done when - [ ] A thousand imports modify no original, verified by hash - [ ] Sideways photographs display upright - [ ] A missing profile is recorded as untagged, not assumed - [ ] Every command is followed by a hash check in the suite ### Watch out - Writing a star rating into the file's own metadata block seems harmless, and it rewrites the file, changes its hash and on some formats re-encodes the image, so the first thing a photographer notices is that every original was modified on the day it was imported. Everything you know about a photograph lives in the sidecar, and the suite proves the original is untouched after every operation. ## M2 · A preview that is the export, smaller What the screen shows at viewport size is what the file contains at full size. ### Steps 1. Work in linear float, convert at the edges Gamma-encoded eight-bit arithmetic bands, and the banding appears after the third adjustment. Files: `src-tauri/src/pipeline/mod.rs`, `src-tauri/src/colour.rs` 2. Make spatial parameters relative to image size A radius in pixels is the reason previews lie. Files: `src-tauri/src/pipeline/ops.rs` 3. Compare export against preview in the suite Numerically, per operation, with a stated tolerance. Files: `src-tauri/tests/preview_matches_export.rs` ### Done when - [ ] Ten stacked adjustments produce no banding on a gradient - [ ] A downscaled export matches the preview within tolerance - [ ] Slider changes render within the frame budget on 24 megapixels - [ ] Crop composes correctly in any recipe position ### Watch out - A sharpening radius of two pixels means something different on a 1,200 pixel preview and a 6,000 pixel export, so the preview looks crisp and the file looks untouched, and the person exports four times before deciding the tool is broken. Express every spatial parameter as a fraction of the image dimensions, run one pipeline at both sizes, and compare the results numerically in the suite. ## M3 · Masks, including the ones a model draws Adjustments apply to part of an image, and a model's mask is a starting point you can edit. ### Steps 1. Store parametric masks as parameters A brush stroke is points and a radius, not a six megapixel bitmap. Files: `src-tauri/src/masks.rs` 2. Make a model's mask editable It is a proposal, and a portrait needs the missing hairs. Files: `src-tauri/src/models/segment.rs`, `src/panels/MaskPanel.tsx` 3. Degrade to manual masks without the model Tested by deleting the model files, not by reading the code. Files: `src-tauri/tests/no_model.rs` ### Done when - [ ] Parametric masks re-render identically after reopening - [ ] A model mask can be refined by brush and the edit persists - [ ] Removing the model leaves the editor working with a stated reason - [ ] The process makes no network connections ### Watch out - Treating the model's mask as the mask, uneditable, means every hair it missed and every window it included is permanent, and a subject mask that is ninety percent right is useless for a portrait. Store the model's output as a bitmap the brush can add to and subtract from, and keep the parametric masks parametric so the sidecar stays small. ## M4 · History, versions and doing it to a hundred files Any step can be undone or revisited, and a look transfers to other photographs without transferring the crop. ### Steps 1. Make the recipe the history Undo is a change to a list, not a stack of pixel buffers. Files: `src-tauri/src/recipe.rs`, `src/state/history.ts` 2. Transfer looks, not framing Global adjustments copy; crop and masks do not, and the skip is reported. Files: `src-tauri/src/transfer.rs` 3. Render batches in the background, cancellable With no partial file left behind on cancel. Files: `src-tauri/src/batch.rs` ### Done when - [ ] Fifty undos and redos reproduce the recipe exactly - [ ] A named version reopens to identical pixels - [ ] Recipe transfer skips crop and masks and says so - [ ] Cancelling a batch leaves no partial file ### Watch out - Pasting a recipe including its crop and its subject mask onto a photograph framed differently produces a crop through the subject's face and a mask over the wrong region, and it does so on a hundred files at once. Transfer only the operations that do not depend on where things are in the frame, list the ones skipped, and let the person add them by hand. ## M5 · Export that keeps the colour and survives the years (production only) Files leave with the right profile and the chosen metadata, and today's sidecar opens in a future version. ### Steps 1. Convert to an output profile and embed it A file without a colour space looks wrong everywhere. Files: `src-tauri/src/export.rs`, `src-tauri/src/colour.rs` 2. Implement the metadata policy and read it back Location stripping removes coordinates and nothing else. Files: `src-tauri/src/metadata.rs`, `src-tauri/tests/metadata_policy.rs` 3. Keep a fixture sidecar per schema version The recipes are the years of work; the pixels are reproducible from them. Files: `src-tauri/src/sidecar_migrate.rs`, `fixtures/sidecars/` ### Done when - [ ] Exports carry the profile and render identically elsewhere - [ ] Location stripping removes only coordinates - [ ] Writing to the original's path is refused - [ ] Every past sidecar version opens and renders the same pixels ### Watch out - Exporting sixteen-bit linear data straight into an eight-bit JPEG without converting to an output profile produces a dark, flat file that looks nothing like the preview, and the report will be that the colours are wrong when in fact the file has no colour space at all. Convert to the output profile, embed it, and check the export in a second application against the suite's fixtures.
# Operations · Luminar Neo ## Backup The originals are already in whatever backup the photographer has. The sidecars are small, sit beside them and are the years of editing decisions, so confirm the backup includes them rather than only the image formats. ## Restore Restore and confirm each sidecar still resolves to its photograph by hash, since a restored folder with renamed files leaves recipes pointing at nothing, and the hash is what reconnects them. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: the original untouched after every command, the preview matching the export, and every past sidecar version opening. Preview time on the largest file is the number that degrades quietly. ## Incident checklist A modified original is the incident this build exists to prevent, and if it happens the hash log says which command did it. A wrong-looking export is almost always a missing profile rather than a wrong pipeline, and the second viewer settles it in seconds. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No original is confirmed modified by any command, by hash - [ ] A downscaled export is confirmed to match the preview within tolerance - [ ] The editor is confirmed complete with the model files deleted - [ ] Exports are confirmed to carry an embedded profile and render identically elsewhere - [ ] Every past sidecar version is confirmed to open and render the same pixels ## Launch constraint Do not market omitted Luminar Neo capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. References and hashes. Disposable; the sidecars beside the files are the record. LIBRARY_DB=~/.local/share/photos/library.db # Required. Appended to the photograph's name. Never written into the photograph. SIDECAR_SUFFIX=.recipe.json # Required. The linear floating point space every operation runs in. WORKING_SPACE=linear_srgb # Required. Converted to and embedded on export. A file without one looks wrong everywhere. DEFAULT_EXPORT_PROFILE=sRGB # Required. Per export. Location stripping removes coordinates and nothing else. METADATA_POLICY_DEFAULT=keep_except_location # Optional. Optional local models. Empty means manual masks, which is a complete mode. MODELS_DIR=~/.local/share/photos/models # Required. Present as a constant. There is no other value. WRITE_ORIGINALS=never
$ choose a build depth, inspect the files, then open the complete pack in your agent
Luminar Neo: Creative users pay for speed, output fidelity, nondestructive editing, and years of edge-case work hidden behind direct manipulation.
xlarge template and asset libraries
xcolor-management edge cases
xcloud collaboration and mobile apps
xhigh-fidelity color, format, and export handling
xpixel-perfect professional tooling
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Luminar Neo →· no votes, no pay-to-list · just what's real
Luminar Neo pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| desktop only | custom | — | Perpetual desktop licence for 2 desktop devices; AI tools included for 365 days. |
| all platforms | custom | — | Perpetual cross-device licence for 3 devices; AI tools included for 365 days. |
| max | custom | — | Perpetual all-platform licence plus 1 year of Prime/assets. |
free tierno free tier; a 30-day money-back guarantee is not a free plan
billingone-time licence plus optional/renewing annual Prime access
hidden costsThe perpetual editor does not mean perpetual generative/AI access: included AI tools last 365 days, then Prime renewal is $33–$59/year by package. Future major upgrades may also be paid; the displayed sale ends August 31, 2026.
verified 2026-08-14 · source ↗
Vibecode Luminar Neo
Kinda. The core of Luminar Neo is buildable in a weekend with the prompt on this page, but there are real gaps: large template and asset libraries, color-management edge cases. Read the honest list above before committing.
How much does Luminar Neo cost?
Luminar Neo's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Luminar Neo?
Honestly: large template and asset libraries; color-management edge cases; cloud collaboration and mobile apps; high-fidelity color, format, and export handling; pixel-perfect professional tooling. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Luminar Neo?
Yes: darktable (A complete non-destructive darkroom with masks, effects, lens correction, and denoise; automation is technical rather than magical.) RapidRAW (Local RAW editing with AI masks, denoise, presets, blur, healing, and batch work; fewer canned sky-and-glow tricks, much less rent.) Affinity (A powerful free desktop photo editor for adjustments, masks, effects, and compositing; no catalog and no marketplace of one-click extensions.) All 3 curated free alternatives are at vibecodeit.com/luminar-neo/alternatives. The prompt is for when you want it exactly your way.