Provenance — read first
Grounded in three sources: (1) KL Chan's verbatim failed WhatsApp thread (repro-klchan.ts), (2) the workshop domain memory, (3) a two-round interview with a simulated seasoned KL workshop owner ("Uncle Tan"). The persona is a design tool to surface floor reality fast — not ground truth. Every number is a hypothesis to confirm with the real pilot owner (§ Calibrate).
1 · The verdict, in one line
Verdict
Sell days and blocks, not 30-minute slots. A block is "available" only when the right bay-type, a capable person, and block headroom all exist — never "any empty bay." Short predictable jobs get an exact wait-time; everything else gets a drop-off block. The scarce resource is expert-hours, not bays.
This overturns the brief's "exact-time interval planner (B) as the default." The default output is drop-off day + block (D-shaped); exact slots survive only as a second mode for short wait-jobs.
2 · The model
2.1 · Unit of availability — day + block
Two blocks per working day, each shown open / tight / full. The customer picks a day + block to drop the car, not a minute to start work.
| Block | Window | Note |
| Morning drop | open → ~12:00 | The 8:30–9:30 drop-off crush is the real constraint |
| Afternoon drop | ~14:00 → 17:00 | Rush over; reserve released |
| Lunch | 13:00–14:00 | Dead — no wait-jobs offered |
2.2 · "Available" = a three-bucket test (replaces "any empty bay")
A block is offerable for a job only when all three hold:
- 1Right bay-type free. service → Bay 1/2; big job → Bay 4; tyre/alignment → Bay 3 only (Bay 3 is not a general lift — "basically a different shop").
- 2A capable person free. Only 2 staff do heavy/diagnostic → collapses to a heavy-jobs-per-day cap (≈2). Empty bay + no capable man = not available.
- 3Block headroom. The block is not at its "full" count (§2.3).
"Any empty bay = available" is a lie — it is the root of the KL Chan disaster.
2.3 · Block capacity — the open / tight / full counting rule
A dumb engine can add these up; no eyeballing the yard. A heavy job counts as 2.
| Block | Online bays | ~Bay-hrs | open | tight | full |
| Morning | 2 (3rd held till 15:00) | ~7 | 0–3 cars | 4–5 | 6 |
| Afternoon | 3 (reserve released) | ~9 | 0–4 cars | 5–6 | 7 |
No-show overbooking
~1 in 5 morning bookings ghost. The engine should overbook the morning drop by ~20% against the cap + a same-morning "still coming?" confirm — otherwise half the mornings sit with an empty bay after turning real cars away.
2.4 · Bimodal booking style — chosen by the JOB, not the shop
- WAITExact clock slot. ≤~1 h, predictable, no diagnosis, customer waits: puncture (~20–30 min), tyre change (~30–45), alignment (~45), balancing, wiper, bulb, battery, oil top-up. Offered only ~10:00+ and after 14:00 (never in the crush or lunch).
- DROPDay + block. Full service, brakes beyond pads, aircon, anything diagnostic, may-grow, or > ~1 h. Collect in the evening.
The line: predictable AND under ~1 h AND no diagnosis → WAIT; otherwise → DROP.
2.5 · Job classification at booking (from the customer's words)
| Class | Booking effect | Example complaints |
| HEAVY | block; consumes the ≤2/day expert cap | "check engine light", "gearbox jerking/slipping", "misfire", "engine knock/noise", electrical, "transmission" |
| LIGHT | flow it (wait or block per §2.4) | "change tyre", "puncture", "alignment", "balancing", "wiper", "bulb", "battery", "oil top-up", "service A", known "brake pads" |
| UNKNOWN | book as light + tag "may grow"; do NOT consume the heavy cap until inspection | "aircon not cold", "brake noise", "Service B", any "noise / vibration / pulling / leak" |
2.6 · Duration = bay time, not calendar time; multi-day = sessions + yard
- ·The quote is not the collection time. A "1.5 h" Service B occupies a bay ~2–3 h but is queued → collect after 5pm.
- ·Multi-day jobs do not hog a bay. Teardown in the bay for hours; then the car waits in the yard and the bay frees. Model = bay booked for work sessions, car parked in the yard for the wait. Never "freeze a bay for 3 days."
2.7 · Reserve capacity
Regulars/walk-ins don't use the bot, so the reserve is trivial: the bot may not book more than 2 service bays' worth of morning capacity; the 3rd bay's morning hours stay out of the online pool until 15:00, then release.
2.8 · Collection promise — exact bot wording
| Case | Bot says |
| Normal | "Drop before 9:30 — ready to collect after 5pm today; we'll WhatsApp if it's done earlier." |
| May-grow | "Leave it in the morning. We inspect and WhatsApp by noon with what it needs and a collection time — plan for after 5pm; if bigger, next morning." |
| Parts / multi-day | "This one needs a part — leave the car, we confirm the part tomorrow and give an exact collection day, usually 2–3 working days. We update at each step." |
Rule: earliest promise "after 5pm today"; never a clock finish before mid-afternoon; for may-grow/parts promise an update, not a finish.
2.9 · Conversational fixes (the exact KL Chan failures)
- ·"What day this week is available?" → recommend the best 1–2 days + a drop block, don't dump a list: "Wednesday's quiet — drop it Wed morning. Friday afternoon's also open."
- ·Look-ahead: this week + next (~10–12 working days). If full, jump forward and say so.
- ·Named date not open → never "no slot." Bounce to nearest open: "30th morning's full — nearest is 1st morning or 30th afternoon, which one?"
3 · The kill list — what dies
- ✕The 3-slot cap (
DEFAULT_SLOT_LIMIT = 3).
- ✕Exact 30-minute slots as the default output.
- ✕"Any active bay free" = available (
bays.find(b => !busy)).
- ✕Freezing a bay for the full span of a multi-day job.
- ✕"No slot" dead-ends — replaced by nearest-open bounce.
- ✕Treating "9am" as an exclusive per-bay booking instead of a drop-off window with a headcount cap.
4 · What this means for the #403 engine choice
The four options weren't the real question — the output shape was.
- DDefault output = day + block drop-off, computed with open/tight/full counts.
- A/BSecond output = exact wait-slots for short predictable jobs, selected per job class (§2.4).
- BCore engine = B-shaped capacity model under both: per-bay-type free intervals, duration + buffer, per-class caps, reserve, multi-day = bay-sessions.
- mode
bookingMode is per-JOB-CLASS (wait | drop), not just per-tenant.
- CCP-SAT solver stays parked. "Count the man" reduces to a heavy-per-day cap — rule-based. No technician scheduling needed; the solver's cost (separate runtime, latency, unexplainable "infeasible") isn't justified.
Net
Build the B-core; emit D (block) and wait-slot outputs; drop A and C.
5 · Schema lock — the three-way split
The brief's single bayManagement blob shadowed the relational tables. Correct homes:
// Bay table — extend the existing bayType
bayType: "service" | "tyre_alignment" | "big_job" // Bay 3 = tyre_alignment (not a general lift)
// ServiceItem table — extend
jobClass: "wait" | "light" | "heavy" | "unknown" // from service_type + name
bookingStyle: "appointment" | "block" // derived from jobClass (§2.4)
durationMin: { min, max } // wait = tight range; drop = estimate for bay-hours
bufferMin: int // cleanup / test-drive pad
mayGrow: bool // unknown jobs: tag, reclassify after inspection
countsAgainstHeavyCap: bool // heavy jobs only
// Organization.settings.bayManagement — org-wide policy only (parsed like `booking`)
blocks: [ { key:"am", start:"08:30", end:"12:00" },
{ key:"pm", start:"14:00", end:"17:00" } ]
blockCapacity: { am:{ tight:4, full:6 }, pm:{ tight:5, full:7 } } // cars; heavy=2
reserve: { holdBayHoursMorning: 1, releaseAfter: "15:00" }
heavyPerDay: 2
morningOverbookPct: 20
waitSlotWindows: [ "10:00-12:00", "14:00-17:00" ] // no wait-slots in crush / lunch
lookaheadDays: 12
collectionPromise: { normal, mayGrow, partsWait } // wording templates
bookingMode: "auto" // auto = pick wait/drop per jobClass; can be forced per tenant
6 · Open calibration — only the REAL pilot owner (KL Chan) can confirm
Uncle Tan gives the general model; these numbers must be validated against KL Chan's actual floor before we build.
- 1Drop-off or appointment default? Does KL Chan run "leave the car, collect evening" (→ block is default) or genuinely promise clock times (→ appointment is default, block second)? This one answer decides §2.1.
- 2His real block boundaries + capacity numbers.
- 3His heavy-per-day cap and who his "expert" staff are.
- 4His no-show rate and whether to overbook mornings.
- 5Confirm the bay map (Bay 3 = Tyre & Alignment); clean up junk "ZZ APA Bay 9".
- 6His wait-vs-drop service list.