← back to pin list

15d. Two tiers — fast estimate and firm quote

kerfmaster pin list · Estimator & quoting

intentmixedWhat we want to build. Never claimed the present, so it cannot be stale — and must never be read as a description of the code.

Content last changed 2026-08-22 — computed from the item itself, not typed.

Contract — Report stage

This contract describes a stage we intend to build — it is NOT a description of the code.

takes gcode

makes delivery

fails if A fast estimate presented as a firm quote.

Contract last changed 2026-08-06 — computed, not typed. Dated separately from the text above, so neither date can speak for the other.

The tension

Inside sales needs an answer in under a minute, on the phone. Running heal → QC → leads → cut order → nest → post takes minutes (the novi board build is about 2.5). Both are legitimate needs and they want different tools.

Tier 1 — fast estimate (seconds)

From geometry alone, before the full pipeline: bounding box, total path length, contour count as a proxy for pierces, feature-size distribution as a proxy for the acceleration penalty. Good enough to say "about $X, firm quote in ten minutes".

Tier 2 — firm quote (minutes)

The real posted program and the real nest. Counted, not approximated. This is the number that goes on paper.

The rule that keeps this safe

Tier 1 is calibrated against Tier 2, and is biased to come in at or above it. Every firm quote generated is another calibration point for the fast one, automatically. A fast estimate that reads low is worse than useless: sales has already said the number out loud.

The fast estimate should carry a stated confidence band, not a bare figure, and the band should widen on the file characteristics that historically predict badly — very small features, very high pierce counts, unusual material/thickness combinations with thin calibration data.

Where it lives

Natural fit alongside the uploader (item 14d): a file arrives, preflight runs in seconds, and a fast estimate falls out of the same pass. A customer could see indicative pricing at upload time, and sales sees it before picking up the phone. Preflight and estimate are one pass over the same geometry — build them together rather than twice.

Note the dependency: a fast estimate still needs scale (item 14b). No scale, no quote. Another reason the scale gate comes first.

⚑ Build state, 2026-08-21 (piece 2 of the customer-surface order): both tiers exist. fast_estimate.py measures raw geometry in under a second (TL-1016-82: 0.8 s for 1,807 in of path, 433 loops, units detected and stated) and estimates through calibration.json, which is written ONLY by calibrate_estimate.py from real (source file, posted program) pairs — 23 points on first run against the existing build corpus. The at-or-above rule is asserted at calibration time and held on all 23; the confidence band is implemented as ruled: low edge = median ratio, headline = max ratio, widened ×1.25 per warning flag (thin data, pierce density or small-feature fraction outside the calibrated range, uncalibrated material — the padding constants are provenance-tagged assumed in the file until the corpus can fit them). Stress case: the novi sign (out of corpus, uncalibrated material) fired two flags, widened ×1.56, and its headline AND band-low both cleared the real firm count — the safe direction held. Money goes through the same tables.rate() gate as the firm tier and blocks on the same named rows. Re-posted sheets pair to their newest post, so re-posts don’t break pairing. What the ratios already taught: firm beam-on runs ~2–4% over raw path (leads added minus healing shrink), and the 1/2″ job runs 6.6% over because lead length scales with thickness — when the corpus diversifies, per-material factors are the natural refinement. NOT yet wired: the automatic rerun after every post (belongs to the preflight-flow piece); today the harness is rerun by hand.

⚑ 2026-08-21, piece 3: “preflight and estimate are one pass over the same geometry” is now literal: fast_estimate.measure() accepts a pre-parsed file and preflight.run() parses ONCE, feeds that parse to the measurement, runs every gate check on the same inch-space data, and the estimate (and indicative price, same RateMissing behaviour) falls out of the run when the gate passes. And “every firm quote generated is another calibration point, automatically” is wired: post_nest.run() reruns calibrate_estimate.refresh() after every post, records the result in stats.json and on the build page, and a harness failure is reported there rather than failing the post. Verified by re-posting build 040: the archived program predated the 4:30 circle-start ruling, the re-post complies with it, audits clean, and the calibration point refreshed itself (len ratio 1.021 → 1.015) with at-or-above PASS on all 23 points.

⚑ 2026-08-22 — the fast tier now composes the Q29 roll-up. calibrate_estimate.py derives four firm-side shape factors from the same corpus walk, none typed in: rapid_per_pierce (firm rapid_in / firm pierces; max 8.671 in, median 3.161 in — measured), accel_penalty (firm accel-model minutes / naive beam-÷-feed minutes at the CURRENT table rates — RATE-DEPENDENT by design, refreshing on every run since every post recalibrates; max 1.023, median 1.015 from 21 of 23 points — the 0.187″ and 0.500″ points have no cutting rates yet, skipped and logged), tab_per_loop (post-stats part_tabs+slug_tabs / fast loops; max 0.636, median 0.203 — measured), and util_low (33.8% — the WORST utilization among MULTI-SHEET nests only, currently 2 observations; one-sheet nests are excluded because their utilization measures the job’s size, not achievable packing — the corpus floor of 0.6% would quote a real 5-sheet job at ~390 sheets, noise not bias; the exclusion and its reason ride in the source_note). fast_estimate.rollup_items() emits quote.price()-shaped line items biased at-or-above from these factors — cut = beam-on HIGH / feed × accel_penalty_max, rapid = pierces HIGH × rapid_per_pierce_max, sheets HIGH = ceil(qty × bbox area / (sheet × util_low)), tabs = loops × tab_per_loop_max (one-per-loop as a stated true ceiling if the corpus ever lacks tab counts) — and rollup.compose() consumes them unchanged, provenance audit included. Every item prices or names what it waits on: a missing shop row → the dotted rates.json row; a missing corpus factor → “calibration: <what>”; no quantity → “customer: quantity (upload form field)”. A blocked roll-up that says why, never a fudged total.