feat(greenfield): rebuild SlabUploader M1-M3 on clean foundation, all gates green
Clean re-implementation of the full product (not a port of the old M3 tree): - P0 foundation: 13-table schema + per-call sqlite factory (thread-safe), FastAPI app (health, JSON logging + secret redaction, CORS, startup guard). - P1 M1 Capture: mask/sheetKeep, geometry/rect (min-area rect), measure, png crop/inventory/overlay, EXIF decodeOrientedBitmap. - P2 M2 Photos: stateful capture session (batch, pick-cal, apply mask/crop to batch) + photos-ready lineup. - P3 M3 Create: real backend (crypto, woo client, taxonomy sync, pricing, inference Call1/Call2, review, publish, settings, slabs, photos) + real FE review/publish/settings screens consuming the API (no hardcoded Woo ids). - P4 gate exit: check_requirements + check_coverage both exit 0 (137/155 IDs, 0 gaps); backend 130/130, frontend 150/150, build green. Cite: docs/REQUIREMENTS.md (155 rows), docs/MILESTONES.md M1-M3.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { blackKeep, greenKeep } from './sheetKeep.ts';
|
||||
import { blackKeep, greenKeep, autoPickSheet, keepForSheet } from './sheetKeep';
|
||||
|
||||
// --- Black sheet sensitivity (#74) ---
|
||||
|
||||
@@ -57,3 +57,44 @@ test('greenKeep: s=0 does not eat mild wood greens; s=100 widens sheet match (#7
|
||||
assert.equal(greenKeep(border.r, border.g, border.b, 0), true, 'delta20 kept at s=0');
|
||||
assert.equal(greenKeep(border.r, border.g, border.b, 100), false, 'delta20 sheet at s=100');
|
||||
});
|
||||
|
||||
// --- Auto sheet pick ---
|
||||
|
||||
test('autoPickSheet: green-dominant edge samples pick green', () => {
|
||||
const samples = [
|
||||
{ r: 40, g: 180, b: 50 },
|
||||
{ r: 30, g: 160, b: 45 },
|
||||
{ r: 45, g: 170, b: 55 },
|
||||
];
|
||||
assert.equal(autoPickSheet(samples), 'green');
|
||||
});
|
||||
|
||||
test('autoPickSheet: near-black edge samples pick black', () => {
|
||||
const samples = [
|
||||
{ r: 5, g: 5, b: 5 },
|
||||
{ r: 8, g: 7, b: 6 },
|
||||
{ r: 10, g: 9, b: 8 },
|
||||
];
|
||||
assert.equal(autoPickSheet(samples), 'black');
|
||||
});
|
||||
|
||||
test('autoPickSheet: empty samples default to black (safer drop floor)', () => {
|
||||
assert.equal(autoPickSheet([]), 'black');
|
||||
});
|
||||
|
||||
// --- Sheet selector resolution ---
|
||||
|
||||
test('keepForSheet: explicit green/black route to the matching predicate', () => {
|
||||
const green = keepForSheet('green', 50);
|
||||
const black = keepForSheet('black', 50);
|
||||
assert.equal(green(40, 180, 50), false, 'green sheet dropped');
|
||||
assert.equal(black(40, 180, 50), true, 'green wood kept by black rule');
|
||||
assert.equal(black(5, 5, 5), false, 'black sheet dropped');
|
||||
assert.equal(green(140, 110, 80), true, 'wood kept by green rule');
|
||||
});
|
||||
|
||||
test('keepForSheet: auto resolves from samples', () => {
|
||||
const greenSamples = [{ r: 40, g: 180, b: 50 }];
|
||||
const auto = keepForSheet('auto', 50, greenSamples);
|
||||
assert.equal(auto(40, 180, 50), false, 'auto picks green for green samples');
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* Sheet-vs-slab keep predicates used by the mask worker.
|
||||
* Extracted so unit tests can cover sensitivity curves without the worker.
|
||||
* Sheet-vs-slab keep predicates for the M1 capture mask (CAPTURE-30..60).
|
||||
* Pure pixel tests: given one source pixel and a sensitivity, decide whether
|
||||
* the pixel belongs to the slab (keep) or the sheet (drop). The caller owns
|
||||
* the bitmap walk and the bitset; this module owns the sensitivity curves.
|
||||
*/
|
||||
|
||||
/** Keep slab pixels on a green sheet. Higher s widens the green-sheet match. */
|
||||
/** Keep slab pixels on a green sheet. Higher sensitivity widens the green-sheet match. */
|
||||
export function greenKeep(r: number, g: number, b: number, s: number): boolean {
|
||||
const t = Math.max(0, Math.min(100, s)) / 100;
|
||||
// s=0 removes only strongly green sheet; increasing sensitivity widens the match.
|
||||
@@ -19,18 +21,34 @@ export function greenKeep(r: number, g: number, b: number, s: number): boolean {
|
||||
export function blackKeep(r: number, g: number, b: number, s: number): boolean {
|
||||
const t = Math.max(0, Math.min(100, s)) / 100;
|
||||
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
// Old floor 40 ate dark wood at minimum sensitivity. Floor 10 isolates near-black.
|
||||
// Floor 10 isolates near-black; the old floor of 40 ate dark wood at minimum sensitivity.
|
||||
const isSheet = lum < 10 + 140 * t;
|
||||
return !isSheet;
|
||||
}
|
||||
|
||||
export type SheetKind = 'green' | 'black';
|
||||
|
||||
/** Pick the sheet type from a sample of edge pixels: whichever match is stronger wins. */
|
||||
export function autoPickSheet(
|
||||
samples: { r: number; g: number; b: number }[],
|
||||
): 'green' | 'black' {
|
||||
let gr = 0, gl = 0;
|
||||
): SheetKind {
|
||||
let green = 0;
|
||||
let black = 0;
|
||||
for (const p of samples) {
|
||||
if (p.g - Math.max(p.r, p.b) > 20) gr++;
|
||||
if (0.2126 * p.r + 0.7152 * p.g + 0.0722 * p.b < 70) gl++;
|
||||
if (p.g - Math.max(p.r, p.b) > 20) green++;
|
||||
if (0.2126 * p.r + 0.7152 * p.g + 0.0722 * p.b < 70) black++;
|
||||
}
|
||||
return gl >= gr ? 'black' : 'green';
|
||||
return black >= green ? 'black' : 'green';
|
||||
}
|
||||
|
||||
/** Resolve a sheet selector to a keep predicate for the given sensitivity. */
|
||||
export function keepForSheet(
|
||||
sheet: 'auto' | SheetKind,
|
||||
sensitivity: number,
|
||||
samples?: { r: number; g: number; b: number }[],
|
||||
): (r: number, g: number, b: number) => boolean {
|
||||
const kind = sheet === 'auto' ? autoPickSheet(samples ?? []) : sheet;
|
||||
return kind === 'green'
|
||||
? (r, g, b) => greenKeep(r, g, b, sensitivity)
|
||||
: (r, g, b) => blackKeep(r, g, b, sensitivity);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user