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:
2026-09-25 06:57:29 -07:00
parent 4ca2ef57a7
commit 49c22c9385
106 changed files with 5502 additions and 8863 deletions
+42 -1
View File
@@ -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');
});
+27 -9
View File
@@ -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);
}