← back to pin list

14d. The uploader — how the automation would work

kerfmaster pin list · Preflight — how files come in

intentWhat 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-23 — computed from the item itself, not typed.

Contract — Intake stage

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

takes customer-file

makes identified-file

fails if A report card that does not end in one clear question or a release.

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

1 open question blocks this pin

Requested by Aristide, 2026-08-01. The intake automation that runs the 14c checks and enforces the 14 gate.

Flow

  1. Upload form. The file, plus required fields: material, thickness, quantity, and finished dimension (with an explicit "drawn at 1:1, actual size" option). The delivery rules from 14a are shown on the form, at the moment they are relevant — not in a PDF nobody opens.
  2. Ingest. Hash and store. The original is immutable and is never modified — everything downstream is a derived artifact. This is already our house rule for the comparison board (original as the grey base layer) and it is the same principle: the customer's file is evidence.
  3. Identify. Format, producing application, units where declared, geometry counts.
  4. Preflight. The 14c checks, automatically, in seconds. No human in the loop yet.
  5. Report card = a comparison board. Original art as the grey base, our reading over it, findings underneath, one of the three outcomes from item 14 at the top.
  6. BLOCKED → the customer is asked one clear question (usually the scale one) with the board link so they can see what we saw. The job waits. It does not enter preprocessing in any form.
  7. PASS WITH NOTES → queued for a human glance at the D-list items only, not a full manual review.
  8. PASS → released into preprocessing: heal → QC → bridges → leads/starts/tabs/cut order → nest → post.

Design notes

Open

Build state, 2026-08-21: steps 3–5 of this flow (identify, preflight, report data with the three outcomes and the one question) exist as preflight.run() — and the fast estimate rides the same pass (15d). The FORM, ingest/versioning, and the notification channel remain intent, gated by the phase ruling. “Internal upload uses the identical gate” is now real in the useful sense: the CLI and any future uploader call the same function.

⚑ Build state, 2026-08-22 — the flow EXISTS as a live dev site

Commissioned by Aristide's customer-surface rulings (7g, 2026-08-21/22) and 'continue on client facing interface.' Built as uploader/intake.py (flow logic — accounts, tenancy, submit→preflight→estimate→roll-up, job store; a lego block, no HTML) + uploader/server.py (presentation only), the tables-editor split. Live at cadmaster-upload.aristide.mycloudally.com behind a pre-go-live dev token (the seed-price ruling leans on that gate), systemd cadmaster-uploader (:8096). What runs: minimal-info account (email + name — outgoing mail unwired, recorded to outbox.json); tenant resolved from the Host header via shops.json (tenant zero = a stand-in demo shop, provenance says never to present it as real); upload → immutable hashed original → preflight's three outcomes → report card with gray source preview (DXF only), customer-language notes DERIVED from the note categories (the raw preflight texts are shop-sheet wording and never reach the page), and the Q29-composed indicative price where the shop's tables allow — today the live table correctly blocks on the OWED operator rate, so the page says 'price to follow.' Verified end-to-end on BNM76282 (PASS WITH NOTES, 14.35×50.14 in read back) and EAGLES.ai (BLOCKED, the one scale question, no price shown). Known gaps, said not hidden: vector uploads get no preview (a wrong picture beats none only in reverse); no per-quote emails.

Approval round-trip BUILT (record, 2026-08-22): intake.approve() closes the loop — only the job's own customer, never on a BLOCKED file, only when a price was actually SHOWN (an approval of a number the customer never saw is worthless as evidence), idempotent. What is approved is the high-side estimate as displayed, recorded as approved_estimate_usd; the page states the consequence at the moment of approval: if the exact price comes out higher, the shop asks again before cutting — so the standing 'exact price for your approval before anything is cut' survives. Approved jobs land in the shop's data/<shop>/queue.json (role-neutral, staged — this file IS what the shop works from until a shop-side page exists) and the confirmation email intent is recorded in outbox.json. Verified end-to-end with a /tmp rates overlay (test $50/hr, never a shop number): composed $150.04, wrong-customer/no-price/BLOCKED all refused, idempotent re-approve, all three page states rendered through server.job_page. The live demo tenant still shows 'price to follow' + 'approval opens the moment your price is ready' — correct OWED behavior until the real operator rate row is filled.

Sales-vs-operator flow (ruling + record, 2026-08-23): Aristide: 'small shop sales = operator / large shop sales is separate. both have to flow' and 'for small shops we wire it together with a click. for larger shops, it is 2 steps.' An approval is a SALES event first — the price re-ask promise is sales work, not operator work. Built same day: each shop in shops.json carries flow = combined (the small-shop default — the customer's approval click lands the queue entry already 'released to production', sales and operator wired together) or two_step (the entry lands 'with sales'; intake.release() is sales' click, idempotent, moving it to 'released to production'). Unknown flow values raise rather than land a job on the wrong desk. Verified both flows end-to-end with the /tmp rates overlay ($50/hr test figure): combined → released in one step, two_step → with sales then released, idempotent re-release, unknown job refused. Known gap, said not hidden: the re-ask flow itself (exact price above the approved estimate goes back to the customer before release) is not built; release() is the plain confirm.

Per-shop tables threaded through the price (record, 2026-08-23): the gap above is closed — fast_estimate.rollup_items() and every knob and provenance read in rollup.compose() now take the shop's tables location (shops.json, Q29), so the line items and the composition quote the SAME shop's layer: base rates.json → $CADMASTER_TABLES → $CADMASTER_TABLES/<location>/, deep-merged — a shop layer holds only the rows it overrides. price_fast() threads too. Verified with a test overlay (throwaway figures, never shop numbers): same file priced $150.04 on default tables and $139.38 on a shop layer overriding hourly + margin; default path unchanged (regression), CLI callers (quote.py, approval.py) untouched — location defaults to None. Recorded behaviour, said not hidden: a `tables` value pointing at a dir with no rates.json silently prices from the merged base — an absent layer file legitimately means 'no overrides', so a typo'd location is not caught here; the registry entry is ours to type carefully.