BUILD-LOG.md — production build progress (autonomous)
Started 2026-09-02/03. Every revision: committed + deployed healthy at
trading.tynolabs.com, no half-stuck state.
Revision 149 — a state factory now RUNS in tests; C71 verified and closed (2026-09-27)
tests/helpers/runes.ts: bun test can now import a *.svelte.ts module for
real — esbuild strips the types, svelte's compileModule compiles the runes, the
output is imported from node_modules/.cache (bare specifiers still resolve), the
factory runs inside Svelte's own effect_root + flush, and a deliberately small DOM
stub records whatever it could not answer. The gap it closes: a claim like "the screen
shows the selected quote's own party" could only be pinned as source text, which proves
nothing (PLANNING §316).
C71 (quote preview Edit) verified and closed. Executed: 6 new cases drive the real
factory with a stale sessionStorage draft — carried quality/party/design/qty/rate win,
the page lands on the summary, a second Edit shows the second quote, the other entry's
draft survives, ?fresh=1 still starts blank, plus a control that the draft IS the
entry when no quote is carried. Mutation-checked both ways: pre-fix
draftAppliesToEntry ⇒ 4 fail, summary snap removed ⇒ 2 fail. Live: 3 quotes at
/quote/new?quoteId= all 200 with banner/quality/party matching the DB (two sharing a
quality show different parties), /orders/new?quoteId= 200 and prefilled, no
/quote/edit?quoteId= link anywhere, bun run screens 75/75 (80/80).
Suite 2728 pass / 0 fail / 0 errors, check clean, mounted roots destroyed so no
late async effect leaks into the next test file.
Revision 150 — C80 verified; the customer's share message said "Quote Ref: draft" (2026-09-27)
Verification found three real defects, all fixed. The edit screen's WhatsApp Share read
form?.quoteNo ?? 'draft' — form is the last POST's echo, so on a plain open the customer
got the literal word draft instead of the quote number. The edit loader never preselected the
opened quote's design (?quoteId= row was read afterdesignId was computed, so the quality
field opened empty and the message lost its Quality line — ?reuse= always had it). And dates
were printed raw (2026-10-04) by the edit form and the wizard while the list formatted them,
so one quote read two ways — dates are now printed once, in the shared builder.
Fourth, found while checking: the ?quoteId= prefill read estQty from inputs_json only
(the SQL never selected the est_qty column), so seeded/migrated quotes reached the wizard and
its share message with no quantity. Column fallback added, guarded by PRAGMA table_info.
New verifier:tests/helpers/share-surfaces.ts runs all three share surfaces over the real
loaders and real rows in a child process against a VACUUM INTO snapshot — a child process
because the list and edit loaders auto-expire quotes on read, so calling them from a test
against data.db would write to the live database. 9 new cases read the message each surface
would actually send (shape, no costing, no junk, decoded wa.me prefill, one spelling of every
date). Mutation-checked per fix: raw dates ⇒ 3 fail · 'draft' ⇒ 1 · no quoteRow.designId
⇒ 1 · no est_qty fallback ⇒ 1. Two older format pins updated to the new (intended) date
spelling. Suite 2738 pass / 0 fail, check clean, bun run screens 75/75.
Revision 151 — C103 verified live; the production forms nav is four real, role-filtered forms (2026-09-27)
The row was six source-text pins with verify: -; nothing had ever opened the screens. Opened them:
all four Production nav entries render for owner and production and for nobody else (checker sees
none — the role filter is live); each entry opens 200 on a real record with its own heading
(trading?orderId=e2e-so-1, job-commercial?orderId=o-4, job-details?orderId=o-4,
production-order?orderId=o-5); the job-work outsource contract block is on Commercial and Details
and on neither other; the order record page carries all four links. bun run screens 75/75.
Three new cases execute the four loadProductionForm calls against the real database: every row a side
offers is a kind it may offer, and the four sides between them claim every order kind with OUTSOURCE
claimed by exactly two. Mutation-checked (Details pointed at SO ⇒ 3 fail). Suite green.
Finding recorded, not changed (owner call): ?orderId= naming an order already past the
pending-ops condition opens the form blank and silently ignores the order — the record page still
offers the link. Out of C103's acceptance; queued as a decision rather than improvised on.
Revision 152 — C104 answered: the old guided form was still live under the new summary (2026-09-27)
The 2026-09-25 reopen was right. /bookings/new served BookingSummary + BookingWizardand then, visibly, the
whole old guided form: <form method="POST" action="?/create"> with no hidden, sec-yarn/sec-bags/sec-rate,
"Step 1 of 3", every input typed straight into it, a sticky Check & save bar. The row's own test passed while
every word of it was true.
Fixed the way the sibling screens do it: the POST form is class="hidden" and carries the same 14 field names
with the same values; the 264 lines of guided markup are gone, not hidden. Live now: the form tag reads
class="hidden", sec-*/"Step 1 of 3"/the sticky bar are absent, the summary rows render, the popup opens on
"Step 1 of 14", and zero non-hidden inputs on the page.
Each removed affordance was landed somewhere else, not dropped: the error alert moved to the summary (and now
lists per-field messages, which is what made a rejected save silent — a second defect the live check caught);
the review sheet's "tap a row to edit" opens that field's popup (FIELD_META[k].step); the section cards open their
group's first field; the C14 source chip hangs off the summary row (one tap still checks every field from a source);
inputmode/maxlength, the supplier-list/lotno-list datalists, the per-field error line, the warp
sizing-multiple warning and the rate board's provenance all moved into the wizard popup.
32 pins across 11 files described the removed markup. Every one was re-read and either re-pointed at the surface
the behaviour moved to (tests/helpers/bookingSrc.ts, the quoteSrc/C88 trick) or rewritten to assert the new
equivalent. Nothing was deleted to go green. Mutation-checked: alert back to _errors only ⇒ 1 fail, popup closes on
failure again ⇒ 1 fail. Suite 2744 pass / 0 fail, check clean, bun run screens 75/75.
Page 1,038 → 561 lines (dead guided machinery removed). Note for future curl checks: a POST without
Accept: text/html returns SvelteKit's JSON, not the page.
Revision 153 — C95 re-quote verified end to end (no build needed) (2026-09-27)
The row sat pending with "round killed before merge; nothing shipped". The code was there; nobody had run it. New
tests/helpers/requote-probe.ts runs the real loader (?quoteId=&requote=1), the real compiled state factory and the
real create action in a child process on a VACUUM INTO snapshot (the save writes, and data.db is the live one).
All three acceptance checks hold on a real quote: opens prefilled on the summary (quality/party/design/qty/4+4 yarn
rows carried, wizard shut); only rate + validity move (asked to change quality, party and rows, it refuses, and the
wizard cannot be reopened); create inserts a newQT-260927-5WV4 with the same design/party/quantity at the new
rate, count 898 → 899, and every field of the source row compares equal afterwards. Live: the URL returns 200 with the
"Re-quote QT-…" banner and the "Only the final rate, validity + payment dhara can change" line, wizard closed, and no
/quote/edit?reuse= anywhere. Mutation-checked: isRequote off ⇒ 2 fail, insert turned into an UPDATE ⇒ 2 fail.
Suite 2749 pass / 0 fail, check clean.
Noted: a few quotes point at a design_id that no longer exists (test residue), so those re-quote with a blank
quality. Real quotes are fine; the probe picks a quote whose design exists, since a dangling id proves nothing.
Revision 154 — C110 built: /tippans/new is a summary + a one-field popup (2026-09-27)
The row had no test file at all and was pending ("round killed before merge"). Built it the way bookings
(§319) and quote/new already work: new src/lib/components/tippan/TippanSummary.svelte (four sections of read-only
rows, required count, Clear + Check, the C14 source chip) and TippanWizard.svelte — whose 23 steps are DATA
(field + kind), so every field is one tap from its row and the component stays one screen long. Numbers carry
inputmode + pattern + steppers + NumberWheel; long masters type ahead; the beams step is the floor-beam picker;
the solver's three answers share one step with Calculate on it, and editing one drops the "auto" flag.
The page keeps the prefills, the C14 gate, the solver wiring, ?/assign and the hidden POST form with all 25 field
names. Solver, record page, print and amend untouched.
Two real gaps closed while wiring: the page had no shell-header hide / body lock behind the popup (bookings has
it), and the server's fail(400, {error}) had nowhere to land once the inputs were hidden — the message now maps to
the step that owns the field.
Live: the form tag reads class="hidden" with all 25 names, sec-*/"Step 1 of 3"/the sticky bar are gone, summary
rows + popup render, and the only non-hidden named inputs are the separate "Assign later" card's two selects.
26 pins across 11 files re-read and re-pointed or rewritten; where a pin caught a real gap (patterns, C14 gates,
datalist, loom chips, quality block) the capability was added to the popup rather than the assertion loosened.
Suite 2759 pass / 0 fail, check clean, bun run screens 75/75, docs/OWNERSHIP.md updated.
Revision 148 — the six dev-system rows built by hand + two gate bugs a live run caught (2026-09-26)
Owner order: build every queue/development-system row by hand and take it off the board. All six are
done, each with a regression test that fails pre-fix, full suite 2451/0, check clean, pushed.
C124bun run build is the one true build command (the package.json script is the
bun-runtime invocation; every script/brief/doc calls it; bun run build verified in 30s).
C125 new scripts/check-remote.sh fails CLOSED when the main checkout's origin is not
GitHub — wired into swarm.sh before any work and deploy.sh before pull. Stops the
"pushes silently went to /tmp" class of failure.
C122 a worker that finds a row already correct writes .swarm-noop; the row goes to
independent verification instead of scoring a failed run, and the 3-strike counter is untouched.
C115 the board now shows the planned work order (#1 = worked now), splits worker working
from under verification as separate columns with their own counts, and gives the owner
↑/↓ reprioritise that the next plan honours. Deployed live.
C127 a worker that finishes early takes its OWN feature area's next rows in the SAME worktree
(no new worktree, no node_modules re-link, no .env, no sync). Live proof 18:24:22 —
ws2 finished early — taking its own area's next row(s): C80 C98 (same worktree, area=quote).
C128 ready areas now ship on a cadence (2 areas or 15 min) instead of a round barrier, each
area shipped at most once, red still reverts; plus measured worker sizing (start SWARM_MAX, −1
after 2 red gates, +1 after 3 green, floor 1 / ceiling MAX+1, shown in queue-watch status).
Live proof 18:33:43 — ship: ws1 ws2 fired while a third area had not run yet.
Two real bugs the live run caught (both fixed + pinned): the gate ran the suite with
--parallel=2 while every test file shares one data.db → SQLiteError: database is locked
and the ship went red (parallelism dropped, fail-fast kept); and workers were committing the
pipeline's own bookkeeping (a stray area file was merged into master) — worktree-root
bookkeeping is now anchored in .gitignore.
Housekeeping: PLANNING's C126 section was renumbered 298 → 301 because a parallel session
also took 298 (the model pin); numbers are unique again, that entry's text is untouched.
Board after: 27 rows — 11 pending, 2 doing, 6 done, 0 blocked. No app code changed in
C122/C124/C125/C127/C128, so no deploy was needed for them.
Thermal status fixed for sensor-less VPS (unknown/unknown/1, was bogus
critical/0); verifiers actually parallel now (missing & + cap 1→2);
swarm/watcher resolve the opencode binary portably (Pi path broke every VPS
worker in ~4s); run_timeout quiet without its file; gitignore covers
data.db.bak* (a stray backup blocked the clean-tree gate). Line runs
headless (SWARM_ATTACH=0), 2 workers + 2 verifiers, batch 3, 12/h caps —
first live pass (C108/C121/C68) implementing with green gates. Suite 2361/0,
check clean. Committed locally; push pending GitHub creds on the VPS.
/queue is now a table (Item · Status · Needs your action · Ref), needs-you
first, keeping the §266 reason logic + 10s refresh. New append-only
docs/QUESTIONS.md + queue-next.ts --ask/--answer let the runner ask and
the owner answer (answering releases the row). Board banner lists open
questions; browser notification opt-in. Merged (not replacing) the §266
.queue-needs.json feature. Tests 23 in the queue files; check clean.
scripts/queue-watch.sh + docs/QUEUE-AUTO.md + /auto-on/auto-off
commands. While armed (.queue-auto) it peeks at the board every 60s and,
when there is runnable/triage work, starts the runner headlessly
(opencode run --agent runner --auto --attach 127.0.0.1:4100). Guards:
never when disarmed/empty, 6 starts/hour cap, 1h timeout, stop kill-switch.
queue-next.ts --has-work (exit 0/1) + hasWork() (unit-tested) drive the
check. Runtime files gitignored. Suite 2122/0, check 0 errors. Tooling only.
Warp/weft popup split in two steps: count (±1 hold-stepper, one decimal)
share (only when that side has >1 row), then rate. Rate is a searchable
list of every mill's daily board rate for the row's yarn (pure
boardEntriesForYarn, count then yarn-type token), a ★ coin remembering
the last ticket picked for the same quality+yarn (localStorage), and a
±0.25 hold-accelerating stepper with Excl/Incl. Load returns yarnList +
rateBoardAll. Full suite 2119 green, check 0 errors.
Revision 133 — Android back on quote calc returns to summary (PLANNING §258)
Wizard phases are state, not history entries, so Back on the calc screen
left the route. beforeNavigate now cancels a popstate while on calc
and steps back to the summary; a second Back leaves normally. Pinned in
tests/quote-list-wizard.test.ts. Full suite 2104 green.
§254 filtered carried steps out of steps, so the summary rows'
gotoStep no-op'd and the ✕ clear inside lead/quality/party was
unreachable. Steps now stay whole and are skipped on Next (§253 pattern);
every summary row reopens its step to clear/change. Pinned in
tests/quote-list-wizard.test.ts. Full suite 2103 green.
Revision 131 — New quote always starts blank (PLANNING §256)
The §240s draft restored the previous entry on every /quote/new mount.
The list button now links ?fresh=1; the page clears the draft and
replaceStates the URL clean so a reload keeps the new entry's draft.
Pinned in tests/quote-list-wizard.test.ts. Full suite 2102 green.
Revision 130 — quote counts fall back to the master count (PLANNING §255)
602/622 designs keep only the legacy single count column and no warp/weft
rows JSON, so quote entry prefilled blank counts. Pure masterCountRows
(masters.ts) now derives one row from the count; /quote/new (designData)
and /quote/edit (resolveQuoteInput) use it when the row JSON is absent.
Previous-quote precedence unchanged. Pinned in tests/masters.test.ts +
tests/quote-list-wizard.test.ts. Full suite 2101 green, check 0 errors.
/quote/new: a picked lead prefills quality/party/warp/weft/job, so
those popups are skipped — pickLead and the ?leadId=/draft snap
land on the first open step (all-carried → summary) and wizNext jumps
carried steps. Pure isQuoteWizardStepCarried + firstOpenQuoteWizardStep
(quoteLogic) pinned in tests/quote-list-wizard.test.ts. Full suite
2097 green, check clean (0 errors).
Revision 128 — other % quote-only (PLANNING §252)
Master field removed (form + default); wizard uses typed adjust or
5.5 — stored 6.5 can no longer override the quote.
Yarn/adjust/quality-add popups dock above the keyboard and keep the
shell header hidden (chromeHidden); every popup input scrolls into
view. FOCUS-ENTRY §4b records the copy-paste popup shape (7 rules).
Full suite 2088 green. Deployed (DEPLOY_HOST_FORCE=1; health 303).
The satin screenshot's With GST 35.04 exposed it: blank row/sizing GST
fell to 0% (materials escaped GST when the yarn master had none).
Engine row/sizing fallback is now the global % → With GST 36.42 on the
same quote. Basic unchanged. Deployed (DEPLOY_HOST_FORCE=1; 303).
Owner ruling: the sheet's 120/1852 warp shortcut (no Lasa term) is the
error, not the engine. Reverted to lasa/l2l×0.5905; COSTING.md + tests
restored. Kept: L2L-0 fallback guard, jobGst fixed-5. Deployed
(DEPLOY_HOST_FORCE=1; health 303).
Sheet C20 verbatim: warpKg = ends × pct × 120/1852 / count / L2L (Lasa
has no term in the sheet's warp math; old allowance ran ~0.25% high).
Reconciles the photo (30.73/52.20/55.07) and the §5 trace exact.
L2L ≤ 0 falls back (was ÷0 → Infinity); duplicate jobGstPct key fixed
(job GST fixed 5 per §240k). COSTING.md corrected. Deployed
(DEPLOY_HOST_FORCE=1; health 303).
Matcher tries party acronyms + design aliases; leadLogic fuzzy core
(typos/plurals/reorder) + per-device lead-dictate-mem correction
memory that wins over heuristics. No LLM in the dictation path (gateway
model stays on capture parsing). tests/lead-dictate-smart.test.ts
(10 tests). Check green, 2022/2022 tests. Deployed.
Revision 110 — wizard docks at keyboard, nav hides (PLANNING §234e)
Card items-end above the keyboard (no dead gap, page visible above);
shell nav hidden while the wizard is open. FOCUS-ENTRY amended. Check
green, 2009/2009 tests. Deployed.
Revision 109 — keyboard collapse that actually fires (PLANNING §234d)
Tallest-viewport baseline (innerHeight shrinks with the keyboard under
resizes-content, so it could never be the reference); touch-only,
orientation resets. FOCUS-ENTRY amended. Check green, 2008/2008 tests.
Deployed.
visualViewport watch hides step header/titles/progress, pins the card
up and opacifies the backdrop while the keyboard is open. FOCUS-ENTRY
amended. Check green, 2008/2008 tests. Deployed.
Pick shows as a removable green chip above the box (party/quality);
box stays empty so coins stay visible and tapping reopens search.
FOCUS-ENTRY amended. Check green, 2007/2007 tests. Deployed.
Compact [&_td]:py-3 touch rows; wizard search-box prefill (later
corrected by §234b into a chip); qty/rate coins fill + highlight
without advancing; FOCUS-ENTRY amended.
tests/leads-wizard-fill.test.ts (4 tests). Check green, 2007/2007
tests. Deployed.
Revision 105 — fix search Enter + status legend (PLANNING §233b)
Root cause: use:enhance ignores GET forms, so debounced submits were
full reloads stealing focus. Toolbar is a plain GET form again with an
onSearchInput hook; leads live-filters client-side, Enter runs the
server search; quiet status-dot legend line. Check green, 2003/2003
tests. Deployed.
Revision 104 — preview mirrors check-save; live search (PLANNING §233)
Leads preview popup rebuilt in the success-card language; ListToolbar
autosubmit (debounced in-place GET submit) with leads opted in;
server search matches every word across party/broker/quality/alias/
dates/lead-no/status. tests/leads-preview-search.test.ts (5 tests).
Check green, 2002/2002 tests. Deployed.
Compact SmartTables use table-sm + table-fixed w-full with
column-clipped cells: finger-sized rows, no horizontal scroll.
Check green, 1997/1997 tests. Deployed.
Revision 102 — leads status dot, headers wrap, no h-scroll (PLANNING §232b)
SmartTable dot column (colored status dots + word tooltips); compact
headers wrap; leads status text tag dropped. tests/leads-status-dot.test.ts
(3 tests). Check green, 1997/1997 tests. Deployed.
Revision 101 — swipe cards with WhatsApp/Quote reveal (PLANNING §232)
SmartTable rows follow the finger (translateX, ±140px) over a
row-sized reveal layer (green WhatsApp right-drag, blue Quote
left-drag); fire past 60px, ease back 180ms, pan-y touch-action.
tests/leads-swipe.test.ts (3 tests). Check green, 1994/1994 tests.
Deployed.
Build fix: use bun --bun node_modules/.bin/vite build (the npm-run
wrapper crashes when Node's ESM loader meets bun:sqlite); vite
ssr.external ['bun:sqlite'].
Revision 2 (46b0860)
Runtime fixes: bun-sqlite .all() is synchronous (removed .then chains);
waiver threshold filtered in JS (column .gt unavailable on the binding).
E2E verified live: party + yarn + design + SO created through the forms;
lots_computed correct; amendments + events written.
Dashboard cards wired to pages. deploy.sh chowns build artifacts after
sudo builds so dev builds survive.
DEV-DB NOTE: pre-existing auth users in data.db were found missing (dev
churn; likely a drizzle-push table rebuild). App wrote new users fine.
Flag for migration hygiene: re-create/normalize users post-migration;
never rely on dev rows.
Revision 4 (720b47e + 96f102a)
/api/capture intake → parseCaptureText → draft (intent/order, count+type+
ticket, qty, rate, partyHint) — never auto-commits; drafts page gains
"Create SO from draft" (best-effort prefill).
Gatepass status flow: create (intimated) → guard /transporter tap pages →
picked/partial → delivered → YARN_IN delivered auto-creates the LOT
(park-lot when not contract-marked — verified live: set 8888 → parked lot).
/lots page with park-lot → contract assignment (B4).
Tippan krill SOLVER (tippanSolver): ends ÷ krill → parts options, cones
per part, measure; live "⚙ Solve krill plan" button on the tippan page.
Guard/transporter minimal mobile flows (zero-typing taps; vehicle input).
Revision 5 (40f9d6e → da204f8) — COMPLETE APP
Dashboard rewritten per BOARDS-A spec: YOUR TODOS strip + section card
grid (🏢Company, 🏷️SO, 🤝JW, 📦Stock&Sell, 🧶B, 🪵C) with live counts.
Home / navbar slimmed: identity/role/sign-out/quick links moved into
the shell; page keeps badges + Overview heading.
New dep: lucide-svelte (shell icons). Verified: 44/44 bun tests green;
vite build clean; DOM checks on a local built server (tokens, sidebar,
active states, mobile drawer, hover bar) all pass. Reference:
github.com/skpawar1305/gray_fabric_erp · PLANNING §45.
Revision 7 — Masters converted to the reference screen language
/masters reworked to the GrayFabric ERP masters pattern (daisyUI-only,
Repo rule 4): page header (title + subtitle), tabs-box tab bar, and per
tab — panel header (name + description + count badge), white bordered
card with a real table table-sm (slate thead, badges, right-aligned
numeric columns, row-hover inset bar) or a reference-style empty state
(icon avatar circle + "Add first …" outline button), and a labeled
collapsible add-form (collapse-arrow; auto-open when the list is
empty; form-control labels; grouped design form with section
headings). Redundant navbar (← Home) removed — the shell sidebar
covers navigation. Deep links /masters?tab=loom|design now open the
right tab (fixed the home dashboard links). Form errors surface in a
daisyUI alert via $page.form (use:enhance). Append-only principle
kept: no edit/delete. Verified: build clean, DOM checks pass, an
end-to-end defect create lands in the table.
Revision 8 — every screen converted (PLANNING §48)
All 24 routes reskinned to the GrayFabric ERP visual language, each
designed for its job (docs/UIPATTERN.md): list screens → bordered table
cards + empty states + count badges; entry screens → form-first labeled
cards with sections; queue screens → tap-friendly action cards; docs =
index cards + reading container. Page-level navbars/←Home removed app-
wide (shell sidebar owns navigation). Masters/§47 pattern extended by 6
parallel agents (orders/bookings/lots/beams/tippans/gatepasses/samples/
drafts/approvals/hishob/docs) + direct (capture/checking/dispatch/guard/
transporter/demo/UploadDrop). Field names, actions, use:enhance, ids and
script logic untouched everywhere (cross-checked: all 30 action
references match +page.server.ts).
Pre-existing bug fixed: /tippans/new 500 (data.tips[0] with no tips in
load) → data.tips?.[0]; route 200 again.
Verified: 44/44 tests, clean build, 24/24 routes DOM-checked, live
sweep all 200 (was already serving the new build). Screenshots:
/tmp/uiv/shots/full/*.png.
Revision 9 — BOARDS-A dashboard slice lands on the real home (PLANNING §50)
The gap closed: home was still the Phase-0 "Overview" scaffold (emoji
cards + quick-entry strip). Built the frozen BOARDS-A dashboard as the
entry surface: src/lib/server/board.ts (one dataset read, honest-first
— every KPI is a real query, never a fabricated number), home
(+page.svelte/+page.server.ts) = master dashboard — period bar
(binds dated stats: gate in/out, slips, drafts; stock/beam positions
live) · attention strip (drafts, owner approvals, gate pickups, park
lots, hishob, ready-not-out, beams in repair) · the nine BOARDS-A
sections as card grids · per-role composition (owner full; planner/
godown/checker/production/guard/sales land on their own minimal board,
"See all sections" = ?full=1).
Table standard on board tables: new SmartTable.svelte — click-to-
sort any column (asc/desc arrow), per-column search, filter selects on
enum columns (DASHBOARDS §5.1 / PLANNING §30). /api/dump?slug= CSV
export per component (DUMPS.md: "DUMP — not a report", as-of stamped).
Seed extended: 6 graded rolls + 1 dispatch pre-slip so E/F boards show
life. Verified: 53/53 tests green (2 new-navigation guards intact),
clean build, live sweep planned post-deploy. Deferred (documented §50):
realtime SSE hub, SmartTable rollout to older list screens, sidebar
menu consolidation, dump station UI, AI report bot.
Revision 11 — list standard rolled to all list screens (work-surface item 1, PLANNING §51)
8 list routes upgraded to the /orders standard (ListToolbar + server-side
q/search · filter selects · click-to-sort headers · 50/page LIMIT/OFFSET ·
count + totals · filter options via selectDistinct · leftJoin display names),
via two parallel agents + review:
/bookings — q on bookingNo/supplier/broker/yarn name; filters status,
warpWeftTag; totals bags + net kg; sort bookingNo/date/net/status/createdAt.
/gatepasses — double aliased party join (source+target); q on gpNo/setNo/
vehicle/both party names; filters kind/direction/status; totals bags + net;
all mark* + create actions preserved.
/beams — q on beamNo/flangeNo/setNo/sizingName/loomNo/party/design; filters
status/location; totals totalMtrs + sizedMtrs; inline create + repairOut
preserved (also fixed: create form's design select renders now — the load
returned an Object.fromEntries map but the form iterates an array).
/lots — q on lotNo/yarn/party/orderNo; filters status/owner/location;
totals bags + net; assignParkLot per-row form preserved.
/tippans — q on tipanNo/sizer/weaver/orderNo (2 aliased party joins);
filters status/yarnSource; totals warpMtrs.
/samples — q on sampleNo/party/design/courier; filters status/chargeType;
totals qty; create + recordCourier preserved (create-form selects fixed the
same map-vs-array way).
/hishob — q on orderNo/party; filter status; no totals (statement list);
generate (dup check) + owner-gated print link preserved.
/drafts — q on rawText/id; filters status/intent; confirm /
createOrderFromDraft / discard preserved; queue now shows all statuses via
the toolbar instead of the old hard row-gate (confirmed/discarded visible
when filtered).
Build hygiene en route: drizzle-orm 0.45.2 does not export alias from the
root under Bun's ESM loader (only drizzle-orm/sqlite-core) — fixed in
gatepasses + tippans; same-function import, semantics unchanged.
Verifications: build clean, 53/53 tests green, every preserved action spot-
checked, deployed live via deploy.sh, all 8 routes param-smoked 200.
Queue screens (approvals, guard, transporter) intentionally stay
action-focused; they get the toolbar when batch ops land (item 5), not now.
Next (item 2): record detail pages + amend/approve/print verbs.
Owner changed the UI reference from gray_fabric_erp (§45-48) to
github.com/skpawar1305/textile-erp-nextjs: retuned design tokens +
rebuilt the app shell chrome; every per-screen pattern (UIPATTERN.md)
untouched, daisyUI-only + light-only pins intact (ui.test.ts guard green).
Theme (src/app.css): zinc-100 workspace (#f4f4f5 base-200),
zinc-200 borders, zinc-800 content; primary = indigo-600 #4f46e5
(links/pills/CTAs/badges), secondary zinc-200 pair, accent sky-500;
radius-selector/field/box 0.5/0.5/1.25rem (20px cards like the
reference); Inter via Google Fonts (app.html) + --font-sans in
@theme; explicit zinc-50 thead strip; tbody hover keeps the inset bar,
re-tinted indigo-600.
Shell (src/routes/+layout.svelte): white sidebar w-72, rounded-xl
nav items, indigo-100 active pill + left indicator line, 10px uppercase
zinc group headers, indigo→sky gradient brand block ("T"), desktop
collapse to a w-20 icon rail (toggle in header), mobile off-canvas
drawer + backdrop, sticky glassy header (bg-white/80 backdrop-blur-xl)
with menu toggle + user chip (name/role + gradient initials avatar),
Logout moved back to the sidebar footer (red hover), subtle sky-200
glow, main content inside max-w-7xl. Route hrefs and ids unchanged.
Inter loading is runtime-only (falls back to system-ui offline); no new
deps; no schema/action changes.
Verifications: bun test green (ui.test.ts incl.), vite build clean,
deployed live via deploy.sh, home + key routes spot-checked.
Revision 13 — declutter: plain language, less on screen (PLANNING §53)
Owner: "all pages look too confusing" → full declutter pass, Pareto-first.
Sidebar: Tools grab-bag removed from the work nav; Masters standalone;
Docs/Demo/Upload/Backup/Reports → tiny "Utility" links in the sidebar
footer; "Desk" group renamed "Guard & transport". 22 → 17 nav items.
26 of 29 route pages reworded (all except home/demo/docs clean as-is),
after a full read-through audit: killed internal refs ((round 28),
appendage, BOARDS/DUMPS), arrow flow-speak, ALLCAPS clauses, "OCR later"
roadmaps, and raw dev output (parsed JSON, LLM/heuristic chips, gsm-wt).
Heading cleanup: duplicated panel h2s merged into page titles with count
badges (beams, lots, tippans, gatepasses). Column/field/button renames
per audit (e.g. repair out → GP → repair out (gatepass); add beam
(floor) → add beam; create gatepass (intimated) → create gatepass;
⚙ solve krill plan → calculate krill plan; dump → download data).
docs/DESIGN.md added (source of truth for tokens/layout/components per
the Textile ERP NextJS reference), linked from STATUS.md + UIPATTERN.md;
/demo sign-in follows the DESIGN §6 auth pattern (brand block, centred
card, block indigo CTA).
Migration 0006: tenant/setup columns on all 24 business tables
(defaults trading/setup-4; enforcement staged per PLATFORM).
Overnight "whole system, no phases" build closed: the slices shipped in
this run (revs 16–26) plus the Trading Capture APK scaffold
(android-capture/) — see PLANNING §57–§67 for the ledger.
Final checks: 53/53 tests green, build clean, full-route sweep 200 via
deploy.sh, live service restarted, working tree clean.
Revision 27 — QA rounds 5–7 (PLANNING §73–§75)
Lot→yarn mapping + procure count-key matched; dispatch F1 create→load→
deliver chain + order/party/no-redispatch guards; beams/lots/tippans
record pages; checking R→reject + beam-scoped roll numbers + checking
report print.
/quote costing screen (weight/cost/margin ladders, order prefill);
per-form CSV dump slugs + ⬇ CSV on all 9 lists; COMPANY gstin config on
prints; party master field gaps (9 fields).
Record dead-ends closed: Amend modal + Amendments section (orders/[id]
pattern, append-only ?/amend) on beams/lots/tippans records; recon
detail Print gated behind owner approval (awaits owner approval badge +
btn-disabled when pending + non-owner) and Expected/Actual/Variance
rebuilt from raw JSON pre blocks into a human Bags/Kg table.
Shell + floor fixes: global search visible on mobile (was hidden sm:flex); guard vehicle input is per-row state (was one shared $state
mirrored across all rows).
Verified on dev PC (NOT prod — no service/Caddy here): 53/53 unit tests
green, vite build clean (build-only dummy secret via env, never
written), built-server smoke /demo 200 · / 303→/demo · /docs 200.
Committed + pushed from dev; prod deploy (deploy.sh + journey suite) runs
on the server. Dev-note: node_modules/.env/data.db were absent on this
PC — reinstalled, schema pushed to local data.db; seed-demo.ts hardcodes
the prod path so it only runs on the server.
Revision 30 — demo scaffold removed, /signin is the auth entry (PLANNING §78)
The /demo route (DEMO-branded auth card + todos scaffold) is deleted
entirely — no nav entry, no public-prefix, no redirect target anywhere.
New src/routes/signin/ carries the auth entry (DESIGN §6 card, sign-in/
sign-up tabs, better-auth client calls, no todos/branding; after auth
goto('/'); signed-in visitors get "Open the app" / Sign out).
All anonymous traffic now bounces to /signin: +layout.server.ts
PUBLIC_PREFIXES + 303, hooks.server.ts POST gate public path + 401
location, layout.svelte sidebar/header "Sign in" buttons.
journey-test.ts gate assertion updated (loc.includes('/signin')) and
smoke page swapped /demo → /signin. Inert dev todos table remains
in schema (no UI; no drop migration).
Docs: PLANNING §78; QA-GAPS public-surfaces list + DESIGN §6 mention
re-pointed to /signin.
Owner directive: delete all existing data, fill from the backed-up .bak,
extend the schema wherever it lacks info. Snapshot the live DB first
(backups/data.db.pre-import-*).
The .bak is a SQL Server (CB4Demo) full backup. Pi kernel is 16KB-page →
no SQL Server here; restored in Docker SQL Server 2022 on the owner's
Ubuntu PC (192.168.1.19), sqlcmd-dumped 169 tables → CSVs (catalog 2,520
cols) → backups/mytextile-dump-all.zip + /tmp/mt-bak/all on the Pi.
Imported (scripts/import-mytextile.ts, idempotent insert-or-replace,
deterministic mt-* ids): party 464 · yarn 89 · design 602 (kg/m factors
seed the engine, owner-verify) · JW order 909 · beam 6,686 (17 on-loom
marks from live ProductionPlanning) · production_plan 36 · lot 2,047
(open yarn stock, aggregate of 286k stock-take rows). Archive history does
NOT ride over (MIGRATION §1.2/§3).
/movements: yarn transfer docs (7 kinds, party/order selects, item
rows lot/bags/cones/kg) — list + append-only create.
/lots/[id]: Allocate + Reserve (verbal, next-day expiry) against open
orders with event log.
Nav: Planning + Movements in sidebar, Import in utility footer; roles in
ROLE_PAGES. No schema change (tables existed). Journey 53/53; build green;
deployed via deploy.sh.
Revision 37 — deep verification (PLANNING §85)
scripts/deep-check-migration.ts committed — full import fidelity audit:
0 real mismatches across party/design/beam/order/lot/plan; semantic
distributions documented; new-screen functional tests (movements create
validation, reserve expiry, role gates) all pass.
Revision 38 — masters editable in place + type-check tool switch (§86–§87)
Masters Edit = same form code as Add (owner §86): six reusable form
components (Party/Yarn/Design/Loom/Flange/DefectForm.svelte) power both
the Add forms and a row's "Edit fields" panel on /masters?tab=&id=;
update actions (updateParty/…/updateDefect) update the row in place,
audited via evt before/after diff of changed fields only. Shared value
builders in src/lib/server/masters.ts (create+update can't drift).
tests/masters.test.ts (15 cases) green; full suite 68 pass.
svelte-fast-check replaces svelte-check (§87): package.json check
script = bun --bun svelte-fast-check --incremental (~3s cold, 0 type
errors; svelte-check dep removed); AGENTS.md/STACK.md/DEPLOY.md updated.
Live verification caught a real bug: the shared create path didn't
await the insert — drizzle .values() is a thenable and only executes
on await, so master creates had logged events but never written rows.
Fixed (await on insert + update) and verified end-to-end live (create →
edit → DB + evt-diff asserts → QA rows removed).
Deployed via deploy.sh; live-verified on /masters (add + edit round-trip).
static/manifest.webmanifest share_target now declares
"enctype": "application/x-www-form-urlencoded" — silences Chrome's
"Enctype should be set…" manifest validator warning; GET share behavior
unchanged.
content-script.js:104 Failed to get subsystem status for purpose Object
documented in PLANNING §80/§88: browser-extension injected script, not
app code (no such file in the repo). Deployed via deploy.sh.
Revision 40 — masters record opens at top (§89)
Detail card moved above the list (under the tabs) with a "← Back to
list" link — /masters?tab=party&id=… now visibly opens the record
instead of burying it below 468 rows (~900 KB of list HTML). Deployed
via deploy.sh.
Revision 41 — masters rows open in a dialog (§91)
Row click opens a daisyUI modal dialog with the full record (fields +
Edit-fields + Amend); Esc / backdrop / ✕ / tab-switch close it. Deep
links (?tab=&id=) keep working (SSR renders the dialog open). Deployed
via deploy.sh.
Revision 42 — masters tab-switch revert fix (§92)
$effect URL-sync now untrack()s its state reads — tapping Yarns on
/masters?tab=party no longer gets reverted by the URL param. Deployed
via deploy.sh.
Revision 43 — masters follows the textile-erp-nextjs layout (§93)
List screens on /masters converted to the reference ergonomics: compact
headers + right CTA, per-tab client-side search box (all fields),
plain bordered tables (no card, table not table-sm), right-aligned
"Open" text-link actions per row; dialog/tabs/add-forms retained.
Deployed via deploy.sh.
Revision 44 — orders date fix: timestamp mode + data normalize + guard (§94)
All app timestamp columns moved to timestamp_ms; 4,395 second-epoch
values normalized to ms (scripts/normalize-timestamps-ms.ts, idempotent;
DB backed up). Orders list/detail/print now show real booking dates
(2021–2026); 6 old-system junk dates ("45") render as "—" via
src/lib/server/dateGuard.ts (also bounds the importer). tests:
timestamps.test.ts (73 green). Deployed via deploy.sh.
Revision 45 — sveltekit-superforms adopted for plain entry forms (§95)
New standard for plain record forms: route-local zod schema.ts,
superValidate (load + action) with zod4/zod4Client adapters,
client superForm + its enhance; field errors under inputs,
form-level refine errors in errors._errors as the alert. First
slice: /gatepasses/new, /bookings/new, /rates (junk numerics now
error instead of silent || 0; rates' 200-with-error action is now a
proper fail(400, { form })). Bespoke forms (orders/new, capture,
movements) intentionally stay on use:enhance. tests/forms.test.ts
(+10); 83 green. Installed sveltekit-superforms@2.30.2 + zod@4.5.4.
Deployed via deploy.sh.
hishob "Generate statement": zod schema + superValidate; business
checks (contract not found / already generated) now setError on the
orderId field; picker gained an explicit "— pick a contract —"
placeholder (no more accidental first-contract generate).
transporter per-row pick: one load form + per-row
PickCard.svelte (own superForm instance each); id rides the DOM,
partial checkbox via boolean coerce; page's form: any prop gone.
Owner report: "create gatepass button on /gatepasses/new doesn't do
anything". Root-caused live with headless Chromium: (1) the tunnel door's
Basic Auth silently 401'd POSTs without the cached header — removed
completely per owner (app has its own auth; Caddy reloaded, backup
Caddyfile.bak-no-basic-auth-20260906); (2) the real dead click:
validators: zod4Client(schema) in superForm options hangs the enhanced
submit in the bundled runtime on every affected screen (proved by
interception test — validators removed → submit works).
Fix: dropped client validators on the 5 screens that had them
(/gatepasses/new, /bookings/new, /hishob, /rates, transporter
PickCard.svelte); server-side superValidate (same schema, same messages)
is now the single validation pass; error display unchanged.
Regression pin: tests/no-client-validators.test.ts (bans client
validators until the bundle issue is fixed + browser-proven) — 94/94
green, type-check clean, build clean, deployed via deploy.sh.
E2E verified live: fill party + set no → Create → record created &
redirected; empty submit → server error surfaces. Docs: PLANNING §99,
UIPATTERN "Entry forms" recipe updated.
Revision 49 — five entry forms moved to kit-native enhance (PLANNING §100)
After rev 48 (validators removed, create button working), the superforms
client failure display still silently no-op'd (server rejects correctly,
but errors never render: form HTML unchanged, no console errors — both
via applyAction/$page and via superforms' direct update path; orders/new
proved kit's native pattern renders). Converted the five plain entry
forms — /gatepasses/new, /bookings/new, /hishob, /rates, transporter
PickCard — to use:enhance from '$app/forms' + the Svelte 5 action
form prop with $derived(form?.form?.errors) rendering. Server
actions/schemas untouched (same superValidate + fail(400, {form}) +
setError). Per-row PickCard error targeted by returned data.id.
Regression pins extended (tests/no-client-validators.test.ts: five
forms must not import superforms + no validators: anywhere) — 99/99
green, type-check clean, build clean, deployed via deploy.sh.
E2E headless Chromium: valid create → redirect + record row; empty
submit → red alert "Source or target party is required", no junk row.
Docs: PLANNING §99–§100, UIPATTERN "Entry forms".
Owner completed the Phase 0 10-minute spot-check (PHASES-GUIDE.md): logged-
out browse bounces to sign-in · no Edit/Delete outside the audited masters ·
guard exit tap writes its event trace · rev 47 paper trail. Verification
Sheet signed 2026-09-06. COMPLETION.md Phase 0 → done-verified; STATUS
refreshed; next is Phase 1 (database complete).
Revision 51 — gatepass form is kind-driven (PLANNING §101)
/gatepasses/new: selecting Kind re-shapes the form (labels, parties,
goods section, auto direction) per full-flow.html/FIELDMAP — YARN_IN/
YARN_OUT full yarn fields; BEAM_OUT sizer-only; BEAM_IN from-sizer +
sizing set no + warp count; BEAM_REPAIR/BEAM_RETURN party + set no;
WINDING to-winder + sets + bags/cones/net; DIRECT count+bags. All
within the existing gatepass schema (hidden inputs ride the POST);
server action unchanged. Deep per-kind fields (beam grids, cut-mark,
winding empties/ring-wt, direct expected date) deferred to their
Phase-4 screens. 99/99 tests green, build clean, deployed via
deploy.sh, verified live per kind.
Revision 52 — Phase 2 audit round 1: engine verified to sheet + high fixes (PLANNING §102, COMPLETION §Phase-2 ledger)
yarnMath — the crucial one: warp leg was +0.25% off the owner sheet
because the l2l default was 104 (K 110/104=1.0577) instead of the sheet
pair 110/104.25 (K=1.05516 — which the seeds already carried). Corrected
defaults (schema/masters/yarnMath) + data-fixed 58 live design rows.
Trace re-run: C31/C33/D33 within ≤0.003%, ladder ≤0.007%. Warping-cost
line implemented (spec §3, was hardcoded 0); invalid counts no longer
poison the ladder with Infinity. Tests re-pinned (trace + new pins).
tippanSolver: krill=0 previously LOOPED FOREVER (Infinity parts);
input guards now throw; soft end-slack was dead code — now proposes the
slack-reduced part count. Regression tests added.
intake/capture: "please make GP…" forwards misrouted to intent=order
→ gatepass intent scanned first; draft images stored the literal string
"${CAPTURE_DIR}?name" → real file + new whitelisted GET route
/capture-img/ (draft thumbnails now render); /capture actions now
session-gated with createdBy set (multipart previously bypassed the hook).
111/111 tests green, type-clean, build clean; deployed via deploy.sh.
Approval role gate moved INTO the service (approveReconciliation/
approveHishob throw unless owner/admin); all callers pass the session
role; reconciliations/[id] approve now routes through the single ops
implementation (no inline duplicate).
Approvals are transactional (status flip + event in one db.transaction)
and re-approval-safe (no second event on already-approved records).
api/amend derives the actor from the session (anonymous → 401); a
client-supplied by is never trusted.
Regression pins: tests/ops-role.test.ts (6 static pins) — 117/117
green, type-clean, build clean, deployed via deploy.sh; journey suite
re-run in the same release.
Capture parser extracts gatepass/direct-delivery goods+logistics (bags,
gross, net, vehicle, transport, destination, expected date) in heuristics
and the LLM JSON path (numbers coerced, never strings); "direct delivery"
now triggers the gatepass intent. /capture confirm line summarises GP
goods; sample buttons include GP + DD forwards.
Fixed along the way: mtrs was in the interface but never returned
(ReferenceError); gross/net-pair and transporter-vs-vehicle capture
traps. Regression pins: tests/intake.test.ts +9 — 122/122 green,
type-clean, build clean, deployed via deploy.sh.
computeCosting gains the spec-6b row path: per-warp/weft material rates
with per-row GST% and incl/excl toggles (incl strips: base=rate/1+g);
per-row sizing rate support ready on the same rows. DesignWeights now
also returns per-row kg/m (warpRowKg/weftRowKg). Legacy single-rate path
is byte-identical when rows carry no rates → the verified COSTING §5
trace is untouched (all previous pins green).
CostingResult adds gstTotal + warpRowCosts/weftRowCosts (ex-GST base +
GST per row) for the future row-aware quote UI; totalInclGst =
totalExGst + gstTotal (row toggles + remainder at the global %).
OCR: tesseract exit code checked — a failed run keeps the image for
manual fix (was: could drop the photo silently).
/api/events ingestion dedupe by clientEventId (retries are silent
no-ops, dedupe: true); unique index deferred to the Phase-1 migration.
Amendments: note-only amends no longer write byte-identical before/after
snapshots (reason IS the correction); appendAmendment gains a genuine
fields diff path for the Phase-3 field-level amend form.
The Verification Sheet spot-check prep exposed that the Quote screen's
computeCosting call never passed pick — the Job-work ₹/m line was
permanently ₹0 and totals were short by exactly the job charge (sheet
C28 5.76). Fixed (pick: d.pick) + regression pin (tests/phase2-round5
.test.ts). Also added a seeded "COSTING SHEET EXAMPLE" design
(e2e-design-costing, seed-e2e.ts) so the owner's 10-min check is one
link: /quote?designId=e2e-design-costing&mtrs=1000 → compare the ladder
to the sheet's C31/C33/D33 (66.82 / 71.16 / 74.72).
134/134 green; deployed via deploy.sh; live-verified the ladder now
matches the sheet cell-for-cell.
TICKETS list drops 'super'/'gold' (quality adjectives, not mills —
'super quality' prose no longer yields ticket SUPER; 'ssm gold' kept).
LLM coercion path now applies the same numeric sanity bounds as the
heuristics (absurd LLM count/rate/bags → undefined).
Pins: tests/intake.test.ts +4 — 138/138 green, type-clean, build clean,
deployed via deploy.sh. Phase-2 autonomous work is now EXHAUSTED — the
phase waits only on the owner's 10-min spot-check + sign (one link) and
the four decisions on the Verification Sheet.
Audits consolidated (masters · form-coverage · tenancy/migrations/data
agents + in-session direct checks): tenancy 27/27 seams PASS, migration
series 0000–0008 valid with zero drift (snapshot == live DDL), imported
data intact (909·6688·2048·468 with fixtures accounted), masters 5/5 KEEP
(Party/Yarn/Loom/Flange complete; Design GAP list below).
masters fixes: warpingRatePaise (was never written → the quote's
warping leg was always ₹0 — same class as the pick bug), glm, gstPct,
warpShrinkagePct now read in buildDesignValues + have DesignForm inputs;
multi-row JSON now carries rate/gstPct/inclusive quints through
parse+store (spec 6b rows); PartyForm gains the Transport category.
Migration 0009 (applied, dedicated review of the rebuilt design table
— drizzle's generated rebuild selected new cols from the old table, a
broken SQL; corrected copy + manual apply, 609 rows preserved):
design.gst_pct · design.warp_shrinkage_pct · beam.kg · booking.gross ·
gatepass.partner_gp_no · gatepass/movement.transfer_year ·
dispatch_slip delivery_address/bill_no/tag_nos · unique index
evt_client_event_id_idx (makes /api/events dedupe collision-safe).
docs/VALUE-SETS.md — the formal status/dropdown value-set table (every
enum column, formal set + live range, governance: new values edit the doc
schema comment + pins in the same commit; no DB CHECK constraints — the
app layer is the contract, owner-approved in the sheet).
docs/VERIFICATION-PHASE1.md — Phase-1 Verification Sheet (What/Why/How
proven/5-min spot-check table: party/yarn/design fields incl. everything
the paper has; create one of each; imported counts intact) + the two
owner gates (GSTIN · bale/lump) + deferred list.
STATUS pointer updated. Write-site sweep showed beam.status hosts two
lifecycles and roll.grade includes WIP 'W' — recorded in VALUE-SETS as
owner/Phase-4 mapping items (no risky enum-typing sweep).
Ran scripts/clean-journey-residue.ts (backup backups/data.db.pre-residue-
clean): live counts now match the documented import claims exactly
(order 909 + 2 fixtures · beam 6,688 · lot 2,048 · party 468); journey
re-run 53/53 green on the cleaned DB.
tests/phase1-schema.test.ts (4 pins): migration series 0000–0009 present,
evt dedupe index declared, DesignForm↔builder symmetry (no unpersisted
field), audit-flagged costing fields authorable.
tests/phase1-schema.test.ts +2: every business table (≥27) carries
tenantId+setupId seams; the live data.db really reflects migration 0009
(design.gst_pct/warp_shrinkage_pct + evt_client_event_id_idx present via
PRAGMA) — the pins fail if the DB ever drifts from the applied series.
147/147 green, journey 53/53.
Revision 63 — masters Add forms become modal dialogs (owner: not below the table)
All six Add actions (party/yarn/design/loom/flange/defect) now open a
daisyUI <dialog class="modal"> via showModal — centered, mobile-friendly
— instead of a <details> collapse 28,000px below the tall table (the
"Add button doesn't work" report). Save closes the dialog (onSaved wiring,
single-brace normalized); backdrop/✕ close too. Rates keeps its top-of-
page collapse with the instant-scroll fix from rev ~61. Pin updated to the
dialog contract (148 tests green).
Revision 64 — masters uniqueness guard (owner: "masters have to have unique values")
Every create/rename on the five masters (party, yarn, design, loom,
flange, defect) now checks for an existing record with the same name
(case-insensitive, trimmed) and returns a 409: "«Name» already exists in
masters — open the existing record instead of adding a duplicate." The
dialog stays open, the error shows at the top of the page. Renames
exclude self. Imported legacy duplicates (e.g. two "A B ENTERPRISES")
remain untouched — a future legacy-dedupe is an owner decision.
Pins: tests/phase1-schema.test.ts +2 (guard presence per action + live-
data dup query) — 151/151 green, deployed.
Add dialogs are now bottom-sheets on phones (modal-bottom), centered on
larger screens, with the box constrained to the viewport
(max-h-[85dvh] overflow-y-auto) so the long forms scroll INSIDE the
dialog instead of overflowing the screen (owner: "doesn't restrict itself
to the screen of mobile").
The duplicate/validation error now renders INSIDE the open dialog (and
the record dialog), not only behind the modal at the page top (owner:
"error msg not visible").
Inline name-availability: /api/master-name + a shared UniqueNameCheck
component wired into the five single-identity forms (party name, design
qualityName, loom loomNo, flange flangeNo, defect name) — a debounced
350ms check warns "already exists" WHILE TYPING, before submit (owner:
"error should be shown while entering the name"); the server 409 stays
the backstop. Pins updated (+5) — 153/153 green, deployed.
create/update actions return the entry id; the Add forms pass it up via
onSaved; the page shows a green toast "✓ Added — entry "
for ~4s and flashes the new row (row-highlight: 2.5s green fade, app.css
keyframes) across all six master tables. Pins updated (+1 describe) —
153/153, deployed.
Revision 67 — Excel-style per-column sort/filter via upgraded SmartTable (owner)
SmartTable upgraded to the Excel-style popover: click a column header →
a panel with sort (▲/▼), a search bar, and MULTI-SELECT checkboxes of
that column's distinct values (plus a ⌖ badge when filtered + Clear/Apply).
Now takes optional actions (an "Open" link) and rowClass (row
highlight). Masters migrated off its six hand-built tables onto
SmartTable (per-column sort + search + multi-select now on every master
column; add-row highlight preserved via rowClass). Boards already used it.
Pins updated (151→153); deployed. Remaining list screens (orders,
gatepasses, beams, lots, movements, bookings, samples, drafts, dispatch,
leads, rates, reconciliations) roll onto SmartTable next.
SmartTable (shared list standard) upgraded: per-column header popover with
sort + search + MULTI-SELECT checkboxes (masters live first); per-row
actions slot (allows POST-form buttons in any column).
All 12 list screens now use SmartTable (Excel-style filters on every
column): orders, gatepasses, beams, lots, movements, bookings, samples,
drafts, dispatch, leads, rates, reconciliations.
Create-confirmations: new record → "✓ Added — … entry " toast + the
new row flashes (row-highlight) — masters (all 6) + orders (via
?created=). Same pattern reusable for gatepasses/others.
Restoration pass (the first migration lost per-row actions + resolved
values — now restored): drafts Create-SO/Confirm/Discard · beams
Repair-out · lots Assign-contract · dispatch Load/Delivered/Print ·
samples Record-courier · leads Convert/move/SO · movements party/order
names + items summary · reconciliations party name + parsed kg ·
rates ₹ + incl/excl · bookings + orders ₹ · gatepass party fallback.
/rates SSR threw "null is not an object (evaluating 'form.gstIncluded')":
the Add-rate GST select bound form.gstIncluded where form is the
action-result prop (undefined on GET) — a latent bug from the native
conversion. Bound to local $state instead; page healthy.
Double gutter removed (the big one): the shell <main> carried
p-4 sm:p-8 while all 42 wrapped pages carry their own DESIGN §4
wrapper (min-h-screen bg-base-200 p-4 sm:p-6) — every screen rendered
with two stacked gutters. Layout main no longer pads; pages own their
wrapper. Pages without a wrapper are unaffected (print sheets are
self-contained max-width + margin auto; upload/confirm/signin carry
their own wrappers).
Dashboard: attention strip follows the DESIGN §5 alert pattern
(soft bg-primary/5 tint + hairline border-primary/20 + 4px left
accent instead of the loud border-2 border-primary); period bar gains
the missing border-base-300 hairline; KPI numbers text-base →
text-xl per the DESIGN stat-card scale, tiles solid bg-base-200.
SmartTable a11y: header filter buttons carry aria-haspopup +
aria-expanded; Escape closes the open filter popover.
Verified on dev PC: type-check 0 errors, vite build clean, 151/153
tests pass (2 remaining fails are server-only live-data pins needing
the prod data.db). Committed + pushed from dev; prod deploy via
deploy.sh runs on the server.
Revision 71 — masters lookup ergonomics (user: masters "not good for ergonomics")
Tap-anywhere-to-open: SmartTable gains an opt-in rowHref prop —
the whole row navigates to the record (mouse + Enter/Space keyboard,
cursor-pointer affordance); taps on links/buttons/inputs inside the
row keep their own behavior. Wired on all six masters tables; the
"Open" link stays as the visible affordance. Other list screens
untouched (prop is opt-in).
A→Z by default: SmartTable gains initialDir (default desc,
so all existing screens behave exactly as before); masters pass
asc — 468 parties no longer open Z-first.
Search no longer leaks across tabs: switching tabs clears the
per-tab search box (a party query no longer greeted you with an empty
yarn list).
Entry forms untouched (numeric keyboards already in place).
Verified on dev PC: type-check 0 errors (only the known benign
warning classes), vite build clean, 151/153 tests (same 2
server-only live-data pins). Committed + pushed from dev; prod deploy
via deploy.sh runs on the server.
Revision 72 — dashboard + signin to the reference visual language (user: reference "so good")
Studied the actual reference source (textile-erp-nextjs, cloned to
/tmp — not the live site): our shell already mirrors it; the feel gap
is the dashboard stat-card language + login roominess. Closed within
the frozen BOARDS-A structure (no spec/data changes):
Dashboard cards: emoji tiles → lucide icon tiles (per-card map by
id, Package fallback so future cards stay safe); airier cards
(card-body p-5, grid gap-4); section headers gain dividers;
hero text-3xl font-extrabold (DESIGN §3 allows the 30px dashboard
hero); fade-in entrance (new app.css keyframes, reference pattern).
Grade-mix card renders reference-style progress bars (A/B/C-R
shares from its own pct KPIs, tone-colored fills) instead of the
generic mini-tiles — the first true visual on the home.
Signin: roomier hero (text-4xl/5xl), rounded-3xl card, large
rounded-2xl inputs with proper labels/placeholders, password
show/hide eye toggle, large CTA with login icon.
Verified on dev PC: type-check 0 errors with no new warnings,
vite build clean, 151/153 tests (same 2 server-only pins).
Committed + pushed from dev; prod deploy via deploy.sh on the server.
Deliberately NOT done: reference-style charts across the dashboard
(needs time-series plumbing + a spec note — say the word and it is
the next slice).
Revision 73 — Pareto slice: one production-pulse trend + sidebar toggle in the sidebar
User direction: Pareto (80/20). Of all the reference's chart
language, the single glanceable visual is one trend — not charts
everywhere. Deliberately NOT built: per-card charts, board-page
charts, planner visuals.
"Last 14 days" panel on the home (reference bar-chart pattern,
zero deps): FG mtrs graded vs slip mtrs out the door (only
loaded/delivered slips count — planned slips are promises, not
movement), daily bars with hover tooltips, 14-day totals, honest
empty note when quiet. Data via a new trend field on BoardData
(board.ts buildTrend, same dataset, zero-filled days).
Sidebar toggle moved into the sidebar (user): the collapse/expand
chevron now rides the sidebar's own top edge (desktop only); the
content header keeps just the mobile drawer button. Sidebar remains
full-height; the rail state is unchanged.
Verified on dev PC: type-check 0 errors, vite build clean, 151/153
tests (same 2 server-only pins). Committed + pushed from dev; prod
deploy via deploy.sh on the server.
Revision 74 — brand row is the sidebar toggle (user: the floating dot was worse)
The 24px floating edge button is gone. The whole sidebar brand strip
(h-16, full width) is now one toggle button with hover wash,
keyboard support, and aria-expanded — a far bigger target than
either the dot or the old header hamburger, and it lives inside the
full-height sidebar as asked. Chevron affordance + tooltip included;
mobile drawer behavior untouched.
Verified on dev PC: type-check 0 errors, vite build clean, 151/153
tests (same 2 server-only pins). Committed + pushed from dev; prod
deploy via deploy.sh on the server.
Revision 75 — brand + toggle move to the content header (user)
Reverted revs 73/74 sidebar-toggle experiments (edge dot, then
whole-row toggle — both rejected). Per explicit answers: the sidebar
carries NO brand row at all (pure nav from viewport top); the content
header holds the brand (T tile + Trading wordmark, hidden on phones)
and the classic hamburger, which collapses to the icon rail on
desktop and opens the drawer on mobile.
Verified on dev PC: type-check 0 errors, vite build clean, 151/153
tests (same 2 server-only pins). Committed + pushed from dev; prod
deploy via deploy.sh on the server.
Revision 76 — burger moves inside the sidebar (user)
The sidebar gains its own top strip (h-16, aligned with the content
header) holding the burger: left-aligned when expanded, centered on
the icon rail when collapsed. Header keeps the brand; its button is
mobile-drawer-only again. No floating dots, no row-as-button.
Verified on dev PC: type-check 0 errors, vite build clean, 151/153
tests (same 2 server-only pins). Committed + pushed from dev; prod
deploy via deploy.sh on the server.
PLANNING §113. List: Kind in plain words (Sale/Job-work/Outsource/
Stock + one-line legend), Booked instead of Date, Rate as ₹/m, full
status pills, totals split by unit (mtrs vs beams never summed), new
Progress column (rolls ready/sent from roll + dispatch aggregates).
New form: 4 numbered sections, single type select (hidden+typeSelect
pair removed), Design is a Combobox like Party, plain hints on every
jargon field (dhara, 3 day-fields disambiguated), price & terms
collapsed-optional; only Party + Qty required (matches server).
Detail: 19-row Terms dump → Contract / Quantity & delivery / Price &
terms cards (null rows hidden); header shows order value + progress;
designName bug fixed (d.name → qualityName); Events + Amendments
merged into one humanized History timeline; new Linked documents card
(lots/beams/tippans/rolls/dispatches/hishob, read-only, capped).
No schema change, append-only intact.
Verified on dev PC: type-check 0 errors, vite build clean (needs a
throwaway BETTER_AUTH_SECRET in this shell — no .env here; the secret
is $env/dynamic/private so nothing leaks into the bundle), 151/153
tests (same 2 server-only pins). Committed + pushed from dev; prod
deploy via deploy.sh on the server.
Docs: AGENTS.md Mandate 1, docs/DEPLOY.md loop, docs/STATUS.md Rule 0
now say ALWAYS deploy — every code/docs turn ends in a deploy.sh run;
parallel sessions coordinate via git status + pull-rebase, never by
skipping the deploy.
Code: keyed {#each} blocks with repeatable keys (dashboard KPIs/trend/
attention, reports rows, SmartTable rows, board/search/imports/rates
chips) now suffix the list index, fixing the each_key_duplicate crash.
§114's hardening missed the real crasher: /orders builds two totals
with label 'ordered' (mtrs + beams, §113 split) and ListToolbar keyed by
(t.label) — duplicate by construction. Fixed in the shared component:
totals/filters/options keyed by label + '|' + index.
Revision 80 — UI uniformity pass (PLANNING §116)
All create CTAs now btn-primary + Plus icon (orders, gatepasses,
tippans, bookings, rates, leads, reconciliations); masters h1/h2 to
standard sizes; redundant thead classes removed (strip is global CSS).
Markup-only, no behavior change.
Revision 81 — orders segregated by kind tabs (PLANNING §117)
Kind tabs with counts (All/Sale/Job-work/Outsource/Stock) above the
toolbar; server kindCounts GROUP BY; Kind dropdown removed; dynamic
heading + kind-aware empty state.
Revision 82 — segregation tabs on 5 more lists (PLANNING §118)
Beams/status, lots/owner, gatepasses/direction, tippans/status,
bookings/warp-weft: tabs with counts + GROUP BY query each; tabbed
dropdown removed from toolbar. Typecheck 0 errors.
Owner: "/quote has no input fields — refer the app screenshot and the
yellow cells." Engine side shipped: YarnRow gains sizRate/sizGstPct/
sizInclusive (the app's per-warp Siz. Rate line); computeCosting gains
jobGstPct/warpingGstPct per-line overrides (default = global GST, so
the verified C31/C33/D33 trace is byte-identical); GST regrouped as
row-material + row-sizing + job + warping + remainder-at-global.
Pins: tests/yarnMath.test.ts +4 (row sizing replace/fallback, override
deltas, legacy single-% identical); the quote pick pin now accepts
pick: d.pick or pick: input.pick (crew rebuild transition).
Suite 167/168 — sole red is the Phase-1 evt-index pin (fix-crew
migration-0010 lane, data.db state, not this slice). check 0 errors.
The /quote screen itself ships with the fix crew's bible-A2 rebuild
(quote table + save action, migration 0010) — which already consumes
these engine fields. NOT DEPLOYED: deploy.sh aborts on the red pin;
deploy waits for the crew's lane to go green.
Recovery: this slice was swept into stash@{0} by the crew's 854a4a4
commit mid-session; restored here are only the crew-independent files
(yarnMath + 2 test files + docs). Crew: do NOT stash pop blindly
(restored paths conflict) — check out remaining files selectively;
and the stashed quote server has a fatal export function resolveQuoteInput/calcQuote (/quote 500s until unexported).
Recovered 11 families' route work from stash@{0}; unexported 7 fatal
server value-exports; rebuilt leadLogic/dispatchLogic/hishobLegs +
35 new test pins; schema re-applied + 0010_first_amazoness; evt dedupe
index restored; red schema pin path fixed. Suite 183 green, check 0
errors, build green.
Revision 85 — beam-gatepass OCR test + hardening (PLANNING §137)
Revision 86 — C14 navigator confirm-chips, all prefilled forms (PLANNING §166)
Inline ✓ chip per prefilled field (source-labelled), tap-confirms, editing
counts as handled, review/save gate blocks until confirmed-or-edited,
"Confirm all from " fast path; empty fields chipless. 17 screens
via 11 lanes. Beam-end + leads agent brace corruptions fixed at deploy.
bun run check 0 errors, bun test 1431/0 (142 new C14), vite build
green, deployed 1b649ef, live verified.
Main 5cb3c1c: lead auto-lost rule, quote row cleanup (C28–C30/C32–C34/
C37/C39), C35/C36 kept with calc-use proof, 18 stale pins trued.
check clean, bun test 1589/0, builds green, both services healthy.
Sidebar: single Yarn → Gatepasses button replaced by a Gatepasses
category (All + 8 kind-filtered links, same roles); active-pill
matches ?kind=. check clean (263 benign warnings), build green,
main c5e95cc live; unauthenticated /gatepasses 303→signin (no 500).
Revision 96 — D2 weft-issue route (PLANNING §200)
New weft_issue table (migration 0017) + /weft-issue screen: scan
bag barcode → lot card (total/issued/remaining; remaining = lot.cones
− SUM issues, pre-screen movement rows not counted) → tap cones
(chips 8/12/16/24 + numeric, over-issue guarded) + weighed net →
loom picker (shifts from loom master; beam/contract/design/party
auto from the loom's onLoom beam, overridable) → type PRODUCTION|
SOLD (sold needs buyer + payment mode) → confirm. Party-loan auto
when bag-party ≠ beam-party (row + weft-issue:* event; netting stays
a hishob-prep step). Live per-beam tile (issued vs standard from beam
mtrs × design weft kg/m), weft-issues dump slug, Production-sidebar
entry, demo seed row; seed beam loom_nos fixed to loom-number
convention (was loom-id). tests/weft-issue 8/8 (caught a real
isIssueType tuple bug pre-deploy); full suite 1995/0; check clean;
build green, main ce73107 live; /weft-issue 303 anon (no 500); ledger
Removed the SOLD type from /weft-issue (enum, server branches, buyer
payment-mode fields, weft-issue:sold event, type picker —
production-to-loom only; type col stays defaulting PRODUCTION, no
migration). FIELDMAP model + open-Q #2 and MIGRATION mapping row
corrected. Suite 1995/0, check clean, build green, main 3329671 live;
/weft-issue 303 anon (no 500).
Gate-in persisted (inward NEED-HOOKs closed): markPicked writes
vehicle + pickupDate + arrived_bags (new col); markDelivered writes
actual bags/net + pickupPhoto. Record-page step() takes the same
evidence per tap (due/picked/delivered/confirmed/reconciled) into
columns + event payloads; DIRECT delivered fires the lot hook.
bag table (migration 0018) + weft_issue.bag_id: N = lot.bags
(0 → single bag), cones remainder-first, net 2dp remainder-last,
LOTNO-Bnn Code39-safe barcodes, lot-id-tail disambiguation on reused
lot nos; created with the lot + self-heal; remaining always derived
(append-only intact).
G4 print: /lots/[id]/print-bags label sheet with real dependency-free
Code39 SVG barcodes; linked from lots/[id] Bags card, inward
delivered rows, GP record lot card. Labels-beats-documents exception
pinned in p3-records (mandate-4 document-print ban stands).
Bag-grain consume: weft-issue scans bag first (bag card + per-bag
remaining, lot fallback stays); stocktake validates against bag/lot
masters + unmarked-barcode list; movements lot field resolves bag
barcodes (bag ref rides items JSON).
Backfill 2049 lots → 2108 bags, 0 dup barcodes, cone sums match;
caught + fixed test-fixture orphan leaks (bag cleanup in 4 afterAlls).
Suite 2011/0, check clean, build green, main live; /lots + /weft-issue
303 anon (no 500).
Revision 120 — /leads/new DeepSeek understand: mic + photo, no heuristics (PLANNING §242)
Step 0 → POST /api/lead-understand (auth-gated): DeepSeek
deepseek-flash on https://api.deepseek.com fills party/quality/qty/
rate/notes from speech or enquiry-chit photo (vision → transcript);
failure fills nothing. In-browser mic + capture="environment" camera
(≤1600px JPEG). All LLM use off the opencode gateway (key 401s there).
tests/leadUnderstand.test.ts (8 tests). Suite green at deploy;
full-run order-flakes are live-DB pollution, pass in isolation.
AGENTS.md mandate 1 now carries the owner always-deploy rule (§243).
Deployed 3a2a0fc (force, off-host; includes in-tree GST yarn work).
Revision 121 — deploy host = the Pi (dsh.tynolabs.com); gate accepts /home/fp/trading (PLANNING §259)
scripts/deploy.sh: host gate now accepts /home/fp/trading (live
checkout since 2026-09-07) and legacy /home/fp/sites/trading; error
text uses bun --bun run dev. Before this, deploy.sh refused on the very
host that serves the site.
AGENTS.md (mandate 1 + "Where it runs") and docs/DEPLOY.md
(architecture diagram, deploy-host gate, DATABASE_PATH, .env, backup
path) now state that the Pi isdsh.tynolabs.com and the live
checkout is /home/fp/trading; README.md scp path + dev command fixed.
Verified by a real deploy dec66ee (pull → tests green → build → restart
→ health 200 authed → commit → push).
Added scripts/queue-next.ts (parse Pending + reopen → docs/QUEUE.md,
blocked-owner auto-hold, non-overlapping batch planner with hot-file
serialization; --sync/--list/--next/--set/--reopen), docs/OWNERSHIP.md,
docs/PROMPTING.md, docs/QUEUE.md (generated, 42 rows), and the
.opencode agents/commands (runner primary; lane + verifier
subagents; /next, /queue).
Independent verification is now structural: a verifier subagent proves
each row on the running app and appends a reopen on failure — the
implementer never closes its own work.
tests/queue-next.test.ts (13) green; suite 2117/0, bun run check exit 0.
Tooling/docs only — no runtime change; doc-only commit + push.
Revision 135 — runner loops until the board drains (PLANNING §262)
.opencode/agent/runner.md: continuous assembly-line behaviour — after
each verified batch it immediately re-syncs and runs the next; explicit
stop conditions (drained / blocked-owner / same row fails twice / red gate /
budget) prevent infinite loops. Config-only change; restart opencode to
load.
Revision 137 — quote weight displays use sized and costing bases (C62/C63)
Quote new and quote edit now show fabric weight from sized warp plus weft
base, primary warp weight including sizing, and a separate unsized warp
weight for costing. Added focused C62/C63 regression tests.
Full suite 2124 green, check clean, production build successful.
Quote weight displays use the precise upward two-decimal owner convention
(75.2567 -> 75.27) without changing engine math or POST values.
Added /ui <form> and its focus-entry rollout agent for the signed
focus-entry pattern. Full suite 2129 green; independently verified live.
Revision 139 — quote save, demo rows, preview actions, and production operations (C67/C68/C72/C77)
Fixed quote save persistence and append-only edit/re-quote handling, added
rerunnable C68 demo quote rows, corrected quote preview Reuse/Re-quote links,
and surfaced production Operations completion from the order navigation.
Full suite 2144 green, check clean, production build successful, and the
deployed service restarted. C67, C72, and C77 independently verified live;
C68 remains open because live preview rows lacked visible party/quality
values from the existing masters.
Added scoped quote-term memory and regression coverage, plus demo-master
upserts and focused coverage for quote demos, row swipe routing, and expiry.
Full suite 2147 green, check clean, and production build successful. C69
independently verified. C68 failed twice on live demo-row visibility and
C73/C82 failed current verification; those rows remain reopened.
Closed four rows with independent live evidence (authed HTTP 200s on
/quote/new, /quote/edit, /quote, /orders/new, /orders after the
5b67fcb deploy + restart): C83 orders sales+operations focus-entry
(c83/c77/order-fields tests green, wizard/NumberWheel/docking/highlight
markers live in source), C63 edit weight bases (75.2567→75.27,
sized+costing+weft lines), C65 /ui command + focus-entry agent contract,
C67 save/re-quote (server actions proven in-test, created-nav/highlight
markers present).
Full suite 2167 green, check clean, production build successful, service
restarted, pushed 5b67fcb. Hydrated phone-width browser proof remains
unavailable (no browser MCP on this host) — noted per row, same as prior
accepted audits.
C68 fixed by running the owner-decided seed (c68MasterDesignIds from the
live design table): all 10 demo quotes now point at real masters and the
live /quote renders every row with party + quality + rate. Root cause of
the saga: seed-demo.ts writes the local data.db (no Turso configured —
live reads the same file), but the script was never re-run after the
master-id fix landed, so stale hardcoded design ids stayed live.
C82 verified functionally, not just by source guards: a temp instance of
the current build with 31 probe quotes expired 28/28 stale open/approved
rows (including rows beyond the newest-20 window) with one
quote:auto-expired event each (from preserved); fresh opens and stale
accepted untouched. Open reopen archived.
C62/C72 audit-only: weight bases + preview Reuse/Re-quote links proven via
focused tests, source markers and authed live 200s.
Full suite 2167 green (one transient single-test flake, green on two
re-runs), check clean. No code changed — doc close only, pushed direct.
C73: left-swipe→order-prefill behaviour was already live; fixed the two
stale "to edit" wordings (row tip + code comment) and extended the
regression test to pin the copy.
C76: root-caused the seed's silent data loss — yarn/design inserts
omit created_at/updated_at (NOT NULL, no default), so insert or ignore skipped them forever (this also explains C68's missing demo
designs). Added the timestamps plus 8 fixed-id demo rate rows (61 CCW +
40S CW, several mills/tickets, dated, both GST flags) with
tests/c76-rate-board-seed.test.ts (double-seed, popup matcher proof).
Live /rates shows the board with supplier/ticket/rate/GST.
C64/C77 audit-only: terms popup drill + production operations nav proven
via focused tests, markers and authed live 200s.
Full suite 2168 green, check clean, build + restart + health, pushed
6974b50.
swarm.sh workers are now visible: opencode run --dir <wt> --attach http://127.0.0.1:4100 --agent worker (title swarm <ws>), so the owner can
watch each worker's live turns in the opencode UI. Headless fallback when the
server is unreachable; knobs SWARM_ATTACH/SWARM_SERVER/SWARM_SERVER_USER/
SWARM_SERVER_PASS.
Server-side DB sandbox: each worktree .env pins DATABASE_PATH=<wt>/data.db
clears TURSO_*; proven that --dir makes the server execute tools with
cwd = the worktree (shell probe: cwd + worktree data.db).
Two gate blockers fixed: worktree .env now carries BETTER_AUTH_SECRET
(build failed without it) and each worktree gets its own node_modules
per-package symlinks (no shared .vite-temp; cleared before builds).
status/worker.log/.swarm-brief.md untracked + gitignored.
Tests tests/swarm-visible-workers.test.ts (6). End-to-end verified with a
live attached worker session in a scratch worktree.
The attach arg order (--dir/--attach before --agent) made the watcher's
pgrep -f 'opencode run --agent' guards miss running workers. queue-watch.sh
now matches --agent worker|runner and status.sh counts opencode run .
tests/swarm-visible-workers.test.ts pins the patterns (7 tests).
Revision 146 — browser password off trading, app login is the door (PLANNING §294)
/etc/caddy/Caddyfile (VPS, host-local): site-wide basicauth on :4103
moved into the @dsh handle only; trading.tynolabs.com proxies bare to
the app. Validated + reloaded; loopback + public curls prove no-creds /
→ 303 /signin, /signin → 200, dsh still 401-without/200-with creds.
Repo: docs/DEPLOY.md + AGENTS.md + docs/APP.md + docs/STATUS.md
stop teaching a trading door; scripts/deploy.sh health check drops
-u org:parag123. Doc-only change — no app build/restart needed.
Revision 147 — every agent, watcher and verifier on space-bunny-free (PLANNING §298)
One model across the whole trading line, per owner: opencode-go/space-bunny-free.
opencode.json (model + small_model); all 5 agents in .opencode/agent/
(runner, worker, verifier, lane, focus-entry); all 7 commands in
.opencode/command/; shell defaults SWARM_MODEL + SWARM_VERIFY_MODEL
(scripts/swarm.sh) and QUEUE_MODEL (scripts/queue-watch.sh).
Verifier no longer splits to deepseek-v4.1-flash — independence is
prompt-enforced (blind review, evidence per check, reopen-on-fail,
edit: deny), not model-enforced.
tests/c109-agent-modes.test.ts case 2 rewritten: pins all five agents to
model: opencode-go/space-bunny-free (was verifier=DeepSeek / lane=Muse), so
one-agent drift now fails the suite. Stale "DeepSeek" prose in runner.md /
audit.md corrected. AGENTS.md mandate line updated.
wt/ws* untouched by design — swarm.sh rebuilds them from master each round.
Proof: c109 2 pass; full suite 2387 pass / 0 fail; bun run check 0 errors
(291 pre-existing warnings). deploy.sh restarts queue-watch, so the watcher
picks the new model up on its next start.
C98 [reopen 2026-09-27] wizard contact step lists parties AND brokers together.
The 2026-09-25 scoped-summary fix filtered the UNSCOPED wizard step to
category === 'Party' as well, so brokers were unreachable from the one
search and the coins listed Sizing/OwnUnit masters. Rows + coins now come
from a new pure contactPickerRows(contacts, scope, query, limit) in
src/routes/quote/quoteLogic.ts: scope '' → Party + Broker, a scoped
summary row stays kind-filtered, non-contact masters (Sizing/Weaver/OwnUnit)
never show. Scoped-pick behaviour, saved partyId/brokerId/initiator
and the share text untouched (MUST NOTs).
Proof — the page was opened, not just read: chromium/CDP against the
live site signed in as owner, /quote/new?fresh=1 walked to step 7 of 7.
'UMA TEXCOM' / 'JATHAR SIZING' / 'RAJESHWARI STATIONERY' / 'VISHNU SIZERS'
now list (badge broker); 'MUNDA' lists party hits and broker MUNDA;
'JATHAR' lists JATHAR SIZING + JATHAR + VISHNU SIZERS-JATHAR. Regulars
coins = SDM TEXTILES / NITIN MILLS / UMA TEXCOM / JATHAR SIZING (no
Sizing/OwnUnit, brokers present). Pick broker → summary Broker [enquiry from] UMA TEXCOM; pick party → Party [enquiry from] BHARAT RATHI GROUP;
the summary's broker-scoped row still adds a broker to a party-held quote
(2026-09-25 fix intact). Zero page errors.
c98 test file 18/18 (the old test pinned the buggy filter line as text —
it now pins real behaviour: combined search, scoped rows, coins = contacts
only); bun run check 0 errors; full suite green in deploy.sh; live-check
29/29; deployed 1e1231d.
2026-09-27 survival pass (PLANNING §310): freed the host and cut the cost layer.
Host: reaped 2 orphaned vite dev servers (1.07 GB of 6 GB, swap had been at
507/512 MB) — available memory 3616 → 4427 MB, swap 507 → 110 MB. swapoff is
blocked in this container (exit 16) and was not needed once the reapers landed.
C99 payment-terms UI work SHELVED on stash@{0} (orders/new + 5 tests, 7 files
incl. the untracked new test); the schema half was deliberately KEPT and
committed as 23d5ab3 because data.db already carries payment_term_id +
payment_dhara — leaving it uncommitted would have made the next
drizzle-kit push drop both columns. Verified after push: all columns kept, 971
order rows intact, evt_client_event_id_idx restored, WAL re-asserted.
Cost: scripts/quiet.sh (2,047/5,120 lines → 31, exit code + errors preserved
and mutation-checked) wired into AGENTS.md, worker.md and the rendered brief;
reaper for orphaned dev servers; hourly budget now billed on shipped work.
.gitignore typo /.swarm-lock → /.swarm.lock fixed (it was making the tree
dirty, which blocks every swarm round). bun run check 0 errors; full suite
2,620 pass / 0 fail across 203 files; live-check green; deployed.
Incident: pkill -9 -x bun during cleanup killed the production server (the
live app is bun run build/index.js, also ppid 1) — restarted immediately, no
data impact, and the reaper now excludes it by name with a test that fails if
the exclusion is removed.
2026-09-27 cost pass 2 (PLANNING §311): build ONCE per round + a fenced step
budget. Measured a SUCCESSFUL build for the first time — 44.5 s wall / 61 s CPU
(4,181 modules), 6.4x the CPU of the whole 2,620-test suite (9.5 s). §310's
"the gates were never the problem" was measured on a build that crashed at 15 s
and is corrected here. The build ran once per workstream AND again in deploy.sh;
removed from the workstream gate (blast radius unchanged, and deploy builds
before the restart so a break costs no downtime). The suite and the integration
typecheck are KEPT — they are what makes a round safe. Step budget
SWARM_STEP_BUDGET=40 added to the brief + worker.md and MEASURED after each
worker from the session store (step budget ok / EXCEEDED); past workers ran
168 and 97 steps, so it would have fired. c119/c120 updated to assert intent
rather than old literal wording. tests/c137-cost-control.test.ts +11 tests,
mutation-checked both ways. check 0 errors; suite 2,631 pass / 0 fail.
C99 — payment terms as ONE input on the order sales form (2026-09-27)
Restored from stash@{0} (shelved by the §310 survival pass) and finished. The
build failure was a duplicate let paymentDays at +page.svelte:132, but the
diff showed the author had deleted cdPct and left a stale assignment
(cdPct = '', a silent phantom global) plus two dead string keys that would
have rendered an empty "Cash discount" review row. Fixed all three; the old
cdPct/paymentDays inputs stay gone and the schema keeps both columns for
history and other writers. tests/c99-order-payment-dhara.test.ts was already
written and passes 15/15; the 4 other shelved test files pass with it (71 pass /
0 fail across 5 files). check 0 errors (291 pre-existing warnings); build
green. Finding logged as PLANNING §312: check exits 0 on a .svelte
redeclaration that build rejects — the cheap-gate cost fix cannot drop the
build for rows that change top-level script bindings. Board row left at
pending on purpose (runner-owned ledger; verifier marks done, watcher is off).
Builder batch 12:07 — 4.2M tokens/row (was 23M), and two defects that ate both rounds (2026-09-27)
Measured batch: 1 session for 3 rows, 12,458,855 tokens, ~4.2M/row vs ~23M
(5.5x better); 98.5% of every token is context re-read. Shipped zero rows
for two reasons, both fixed:
swarm.sh gate (scripts/swarm.sh:442): the quiet.sh guard tested the ROOT
copy while the command ran inside the worktree, so every worker on a worktree
without the wrapper ran green tests then died on "No such file or directory"
=> gatefail => requeued => retried. Guard now tests $wt/scripts/quiet.sh.
tests/c138-swarm-gate-wrapper-path.test.ts (5 tests, mutation-checked).
c137 asserted the bug it was named for. It pinned the ROOT-relative guard
verbatim, so it passed green while the bug was live; my correct fix turned the
suite red and reverted a 20-min round. Assertion corrected, name unchanged.
Stale worktrees: wt/ws1..3 were 8h old, never refreshed (swarm.sh only
creates if missing) and held 23 unmerged commits, some duplicating shipped
rows; ws1/ws2 conflicted with the order form. Archived to
archive/swarm-ws{1,2,3}-2026-09-27 (795/796/792 commits, nothing lost) and
rebuilt from master.
Shipped C100/C102/C112/C71 (ce417ec) after repointing 5 stale
airtight-commercial contracts at C102's type-fixed forms, adding a redirect
contract, and removing a stray {/if} in OrderEntryForm.svelte (100 vs 99).
Live-check 29/29. Board 22 done / 4 verify / 4 pending, nothing stranded.
deploy.sh pulled without --rebase-merges: a plain rebase DROPS merge
commits, silently discarding conflict resolution — it reverted the repaired
test file and reported success. Reproduced in a scratch repo; fixed.
scripts/svelte-syntax.ts (new, wired into the worker gate): check and
the 2,709-test suite both pass a .svelte file with unbalanced blocks or a
redeclared let; only vite build rejects them, and it runs once per round,
so each typo costs a whole round. Same compiler, 1.4 s on 6 files, instead of
61 s CPU. tests/c139-durable-gates.test.ts (10 tests, mutation-checked).
tests/c134-ask-but-guess.test.ts now allows done — C100 shipped, and done
is not "parked". Suite 2709 pass / 0 fail.
Screen check covers all 75 screens (was capped at 24 with every [id]
route skipped, so 51 screens — every order/beam/lot/statement page — were
never opened by anything). Dynamic screens are opened on a real record found
from the route's own loader and mapped through the schema; doors are followed,
sign-in redirects are not; the one state-gated screen (hishob print, locked
until owner approval) is handled by the app's own rule. 75/75 in 4.3 s, zero
tokens, owner-runnable: bun run screens [-- --only orders].
tests/c141-screen-check-every-screen.test.ts (7 tests, mutation-checked both
ways: ok = true ⇒ 1 fail, old [id] skip ⇒ 4 fail). Suite 2722 / 0 fail.
Revision 148 — two quote-screen slips onto the queue board (PLANNING §327)
Owner-reported on the quote screen, queued rather than hand-built (owner's
workflow: "do it" = queue it): C132 the preview popup's Reuse opens
/orders/new?quoteId= (the orders screen's C42 Reuse, duplicated with the
Convert to SO button beside it) and must open the prefilled quote form instead;
C133Re-quote locks warp/weft rates, construction, party and quality
(C95's rate+validity-only rule) and its save mints a fresh quote number with no
link to the quote it came from, so a re-quote must be fully editable, keep the
original quote's reference number, and record every change field by field
without erasing any earlier record.
Rows added to the owner's shelf (docs/CORRECTIONS.md), projected onto the
board with bun scripts/queue-next.ts --sync (32 rows), and given targets,
acceptance and priority with --set so bun scripts/queue-next.ts --next
plans them — C133 P0 first, C132 P1 second, serialized because they share
QuotePreview.svelte + the new-quote wizard. No code touched.
Revision 149 — a conflicted workstream is a state the supervisor can see (PLANNING §328)
The 09:51 round built C132 + C133 on swarm/ws1 and shipped nothing: merge swarm/ws1 conflicted, the log said "resolve manually", and both rows sat at
doing with no worker alive. Nothing could see them — the claim release only
runs when the board has runnable work (a doing row has none), the stall alarm
keys off the same thing, the stalled rule needs 45 minutes and a commit on
master that a conflicted branch never has, and /queue showed the rows under
"Worker working" for a worker that had exited. Fourth measured lying board
(04:26, 05:08, 07:02, 09:51) and the first caused by a merge conflict rather than
a dead worker.
Now: swarm.sh records the conflict (.queue-conflicts/<ws>.json — branch,
base, unmerged files from git diff --diff-filter=U, rows) and flags those rows
conflicted; stuckRows reports them immediately, naming the branch;
--unblock-brief tells the unblocker to LAND the branch, not rebuild the row,
with the exact commands; reconcile self-heals (merged branch -> verify,
released claim -> re-stated conflicted, unreachable branch -> pending);
planBatches never hands a conflicted row to a fresh worker; /queue has its
own "Merge conflict" column. A successful merge clears the record.
Second bug found on the way: workedSince used git log --since=YYYY-MM-DD, and
a bare date matches NOTHING on this host (--since=2026-09-28 → 0 commits;
--since=2026-09-28T00:00:00Z → all), so the supervisor's evidence function
answered "no work" for every row — tests/c133-stuck-supervisor.test.ts was red
on master because its fixture ("C98 has commits from today") had decayed by a
day. Full ISO timestamps now; that fixture is self-calibrating (reads the newest
commit on master and the row id it names) so it cannot rot on a date again.
The live 09:51 conflict is recorded, so the supervisor is dispatched on its next
tick with the branch in hand. Suite 2825/0 (was 2824/1, the pre-existing
workedSince failure).
Revision 150 — the conflict flag must survive the projection, and the column must be on the page (PLANNING §329)
Owner-reported while watching the board, both invisible to the test suite:
--sync regenerated QUEUE.md from the owner's sheet, where C132/C133 still read
pending, so the new conflicted flag was wiped every round — and a pending
row with targets is runnable, meaning the next round would have rebuilt work that
already exists on swarm/ws1. buildQueue now treats conflicted as an
engine-owned working state the sheet's own working statuses cannot clobber (a
terminal/advanced sheet state still wins). And /queue builds its row list by
walking a hardcoded ORDER of column keys, so the new column's cards were
INVISIBLE on the page rather than merely unsorted; conflicted is now first in
ORDER and the header strip counts it. Suite 2830/0.
Revision 151 — the conflict is landed: C132 shipped from swarm/ws1 (PLANNING §330)
The supervisor landed the branch §328 recorded instead of rebuilding the row.
swarm/ws1 held C114 + C132 + C133 above the merge base and C114 was already on
master, so two files conflicted. src/lib/server/orderDocRead.ts (add/add)
resolved to master — its copy is a superset (it has the prefix argument
C114's later callers pass). src/routes/quote/new/quoteNewState.svelte.ts
(content) kept master's C113 draftPrefill carry and took the workstream's
C133 comment. The rest merged clean, including the ~15 if (isRequote) return;
locks C133 removes. src/routes/quote/+page.svelte,
src/routes/orders/+page.svelte and QuoteListTable.svelte are byte-identical
across the merge, so the C42 orders Reuse and the left-swipe were never touched.
C132 proved by opening things. The preview popup is client-side, so the real
component was rendered with the real row from the page's own loader: Reuse →
/quote/new?quoteId=<that quote>, Convert to SO → /orders/new?quoteId=<that quote>. The live chunks on 4102 are byte-identical to the local build and the
quote node now carries 3 quote-form hrefs against 2 order-form ones (before the
fix the component alone had 2). /quote and /quote/new?quoteId= serve 200 as
owner. The save ran end to end on a VACUUM INTO copy of data.db — the form
arrived with the quote's own party, quality, construction, warp/weft rows and
rates, job and terms, every field was editable, ?/create inserted a NEW quote
(QT-260928-BILV) and the source row compared byte-identical; the live DB was
not written to. tests/c132-quote-preview-reuse.test.ts 9/9 (c71/c72/c133 with
it 51/51), check clean, live-check 80/80, screens 75/75. Conflict record
cleared; C132 → done, C133 → verify (it rode the same branch and is not
settled here).
Revision 152 — the AI reads are queued onto the OpenCode Go subscription (PLANNING §331)
Queued C139 ("Change the DeepSeek api used for oct. Use opencode go spacebunny
instead"), not hand-implemented: the gateway that feeds slip/photo scanning
(/api/order-ocr, /api/capture-ocr), spoken order typing
(/api/order-understand) and lead dictation (/api/lead-understand) moves from
DeepSeek to the opencode-go provider this repo's own agents already use,
model space-bunny-free, endpoint https://opencode.ai/zen/go/v1. Owner answers
taken this turn: all three reads move, the new service is the only one (no
DeepSeek key as a second fallback), and it ships through the queue board. The
row is runner-go — --next lists it as batch 4, 9 targets / 10 acceptance
checks, and Q3 is recorded beside it without parking the row.
Four probes against the real gateway first, so the row carries measured facts
rather than guesses: /zen/go/v1/models lists space-bunny-free; a call with no
x-opencode-session header returns MissingSessionID (every call site must send
one — nothing in src/ does today); a PNG sent as an image_url data URL is
accepted (vision works, so photo reading stays on the LLM path); and
response_format: json_object returns parseable JSON — but 164 of 180 completion
tokens were reasoning_tokens, and at max_tokens: 120 the same shape returned
empty content with finish_reason: "length". That last number is the trap:
the three call sites budget 500/600/700 tokens, and on a harder slip a budget spent
on thinking comes back empty, which the current code answers from heuristics. The
row therefore requires a non-empty parsed answer from a real slip, not just a
Written into the row as stated assumptions: the key is the one already on this
host (~/.local/share/opencode/auth.json, provider opencode-go) — nothing new is
bought; the DeepSeek credential stays in .env unused so the switch is
reversible in one edit; old saved rows keep their stored read-by label and their
saved text; the c112/c113/c114 gateway/model pins are retargeted deliberately
and their no-gateway/unreachable fallback tests keep passing unchanged.
Doc-only turn: docs/CORRECTIONS.md (C139 row), docs/QUEUE.md (synced +
--set target/acceptance/notes), docs/QUESTIONS.md (Q3), docs/PLANNING.md
(§331). No code, no .env edit, no deploy — the runner builds it.
Revision 153 — a stale shipped marker had made ws1 un-shippable for two days (2026-09-30)
Symptom: verification 0, nothing reaching it. Six rows (C135, C136, C137, C138,
C140, C141) pinned at doing with their work finished and committed on
swarm/ws1, unmerged, and every round logging shipped 1 workstream(s) while
shipping nothing.
Cause: wt/ws1/shipped, dated Sep 28 10:54. shipped is a within-round latch
and nothing cleared it between rounds, so ship_ready skipped the lane for good
— and the summary counted that stale file as a fresh ship, which is why the stall
matched its own report.
scripts/swarm.sh: round-start sweep clears shipped with status and
.swarm-noop; the summary counts only markers newer than the round start
(stat -c %Y … -ge "$t0"); and a worker noop only retires a row to
verification when the worktree has nothing ahead of master — a lane holding
unmerged commits ships its branch instead of pointing verifiers at a live app
that never got the work (the failure this would have caused next).
tests/swarm-stale-shipped-marker.test.ts: 8 cases, red pre-fix. Keeps the
within-round latch, unmark_unshipped, and the C122 clean-worktree noop intact.
Rows recovered to pending; .queue-run-timeout back to 3600 (it was 900
against a 3600 s worker — the same stranded-rows state from the other end).
PLANNING §332 has the full account, including why editing a running
swarm.sh corrupts the round (bash reads scripts by byte offset).
C142 — the actual-picks field on the Trading SO and the Production Order too
As told: "Queue add actual pick field in trading order and production order
too like in job work order wizard. Pre fill it with the actual picks used for
the same quality + same party last time." Pre-fix, opening the three forms
over HTTP showed the field on job work only, and the create action threw the
posted value away for every other kind (type === 'JW' || 'OUTSOURCE' ? … : null).
The picks step is no longer a job-only wizard step; jobRate still is. So each
order kind asks picks in the same place job work does — one own-field step
just before the rate — and a sale / production step gets the stepper, the
coins and typing on top, with its own hint (the job-work hint is left byte-
for-byte alone, because there the rate really is job rate × picks).
lastPicksFor / picksSuggestions carry the ladder the row records: the most
recent order for THAT quality AND THAT party, else that quality alone, else
blank. Only rows that store picks count — a design master's construction pick
count is a different number and is never a prefill. The fill is a badged
prefill the user confirms; nothing is auto-committed.
The server keeps the entered picks on every kind now (picks = numbers.picks).
jobRate stays job-work-only, so rate still equals what was entered on a
sale / production and job work still runs resolveRate — proved end to end:
a posted SO keeps rate 41 with picks 4400, and a posted JW still saves
0.35 × 4400 = 1540.
The order record's ?/complete action honoured picks for job work only, so
a Picks box rendered on a sale would have swallowed its input; it now records
picks on every kind while autoRate stays gated on the job-work kinds.
tests/c142-picks-field-all-order-forms.test.ts 20/20 (fails pre-fix: the
import alone did not resolve). c100's "a sale has no picks popup" assertion
is the one existing expectation C142 inverts, and it now asserts the opposite.
28 order-related test files green, check clean, bun run screens -- --only orders 7/7, live-check 12/12.
C154 — the order form's attachment field (the manual contract PDF/image)
The upload card (OrderOcrUpload, shared by every order form that already had
it) now asks, per file, "Read it" (the C114 OCR path, unchanged) or
"Just attach" (saved on the order, never read, nothing prefills, no gateway
call). The file input takes several files — a contract is often more than one.
New table order_attachment (migration 0031_c154_order_attachment.sql, one
CREATE TABLE): the file the user added, its name/size/date, whether it was
read, and which order it belongs to. /api/order-ocr saves the file with the
existing saveDoc into the existing screenshots/capture and stages the row;
the order's own save (sales/production create, or production ?/complete)
claims the staged rows for that signed-in user only (6 h window).
The order record gained an Attachments card: name, size, date, a
read / attached-not-read badge and a link that opens the file through the
existing /capture-img/[name] route. "Take off" raises C152's change rail
(attachment-remove): a non-owner leaves it pending on /approvals, an
approved decision flags the row and writes the amendment — no file is ever
erased and no row is ever deleted.
No order economics moved: a posted order keeps exactly the rate and quantity
that were typed, whatever was attached.
tests/c154-order-attachment-field.test.ts 17/17 (pre-fix: 15 fail — the
served order form has no data-order-attach-choice and the service file does
not exist). C114/C112/C149/C152/C141/C113/C102/C155/C156 green, check clean,
bun run screens -- --only orders|production|approvals all open, live-check
whole.
C154 — unblocked and re-queued behind C157; the real blocker queued as C176 (supervisor, 2026-10-01)
Owner answered Q12: C154 waits. The checks are not brought forward for it;
the worker moves on to the next row. C154 blocked-owner → pending, and an
owner-order override now plans C157 first, C154 second inside the one
sequential hot group they share (src/lib/components/orders/OrderEntryForm.svelte).
Not marked done, and that is deliberate. C154's own bar is that a person
opening the live form sees the attachment field. Its code is on master
(5b0aa3b, drizzle/0031_c154_order_attachment.sql), c154 is 17/17 and
bun run screens 75/75 — but the live site still serves a stale build, so the
row stays open until a deploy actually carries it.
Measured the red instead of trusting the brief: 3390 pass / 2 fail, and both
are one cause.tests/order-fields-visible.test.ts:58 and
tests/c138-jobwork-brokerage-passon.test.ts:156 assert the pre-C151 summary-row
handler; 2aeff8a (§340, on master) changed it to onclick={() => { tap(); openWiz(rowStep(r)); }}.
No behaviour is broken — C151's own pins (tests/c151-focus-entry-doc-copy-exact.test.ts:413,485)
already assert the new string, so the form is right and two older pins are stale.
The "two board/reaper checks" are not red on master. In isolation against a
dirty tree they fail, but at HEAD the reaper pins match the shipped narrow
rule; they belong to a concurrent session's uncommittedscripts/swarm.sh +
test edit. Left alone — not C154's and not a gate blocker here.
C176 queued and pre-checked (tests/order-fields-visible.test.ts,
tests/c138-jobwork-brokerage-passon.test.ts, scripts/swarm.sh): update the
two pins to the shipped markup, keep both halves asserted (tap()androwStep(r), plus the rowStep(r) < 0 static branch), no src/ change, and
do not loosen them into a bare openWiz( that would pass if a row opened the
wrong popup. It sits in its own disjoint workstream, so master goes
green without waiting on C157.
Docs: PLANNING §341. bun --smol test 3390/2 (the 2 are C176's, unchanged by
this commit — doc/queue only).
C175 closed as verified by supervisor (no code change): the row's own work
was already merged and pushed (7edba19 + the shell's no-step guard from C174)
and is live — openPage('/beams') and openPage('/beams/<id>') both 200 with
the summary, the wizard open at step 1 of 3, the C6b entry with no wizard, all
POST names in the hidden forms and no autofocus; tests/c175-beam-card-focus-entry
13/13; live-check 80/80 (75/75 screens). The thing that was actually stuck was
a stale interactive rebase in the main tree that made every deploy.sh
abort — recovered with git rebase --abort (no ref moved, nothing lost).
Docs: PLANNING §343. Not claimed here: the phone-width-with-keyboard walk
(no touch browser on this host) and the two judgement calls in §339.
Row line replaces the swarm (2026-10-02, owner order).scripts/swarm.sh,
scripts/unblock.sh, wt/* worktrees + swarm/* branches, and the
worker/lane/runner/unblocker/verifier agents are deleted. Rows now run
strictly top-to-bottom of the Pending table, one row per fresh
opencode run --agent row session (scripts/row-run.sh once|drain), main
checkout, never two sessions alive. Per-row gate is ONLY bunx tsc --noEmit
(comparative — tsc is red repo-wide on pre-existing errors, so a session
ships when it adds none on its own files); the full suite + bun run build
run ONCE after the drain. A question defers its row (--ask, board shows
deferred + full question + per-question answer box, ?/answer re-queues);
only hard stops hold blocked-owner. The watcher (queue-watch.sh) only
starts drains, one session at a time; a pinned row waits its turn (append).
Two incident fixes ride along: the systemd unit no longer re-touches
.queue-auto on every start (disarm now survives restarts), and drain
refuses when non-board files are dirty (an armed watcher once autostashed a
whole refactor out from under its author). 19 swarm-pinning test files
deleted, c198-row-line added (11 pins). Touched-area tests 153/153 green;
full suite not re-run here (slow box) — the drain's final gate covers it.
/queue slimmed (2026-10-02, owner: "still too much"). The board drops all
builder chrome: the 7-badge count wall, evidence/assumed/page-proof badges,
Ref column, acceptance-check details and P0 badge are gone. What remains:
a Now line, the question banner (full text + per-question answer box),
stall/save-failure alerts, and one compact block per row (id + summary +
one state word + Close ✓, with Reopen/Delete behind one tap). c129/c164/
c198/queue-close-button repointed; touched-area tests green.
Pareto auto-answers + followable /queue (2026-10-02, owner orders). Q14–Q18
answered on the record (C175 keeps its 3-step wizard; BEAM_REPAIR/BEAM_RETURN
fold into B5 movements; WINDING stays under C5; C180's read-only fields stay
read-only) — defer queue empty. Standing rule flipped: sessions Pareto
auto-answer every question (obvious 80/20 reading, Q+A on the record,
assumption on the row) and never wait; only hard stops hold. /queue
restructured for a human: every card carries its sheet number (#N), sections
read Fixing now → Needs you → Up next (5) → Later (collapsed) → Finished
(collapsed); builder badges were already gone. Touched-area tests green.
Ship cadence: live every 3 rows (2026-10-02, owner order). The drain ships
via scripts/deploy.sh every ROW_SHIP_EVERY (default 3) rows plus the
remainder at the end — suite + build + restart + health + push each time, so
trading.tynolabs.com tracks the board. A red ship stops the drain. A
line-triggered deploy (ROW_LINE_DEPLOY=1) skips the watcher's deferred
self-restart, which would otherwise kill the live drain and its row session
mid-row. Drain watchdog raised to 2h (ships make drains longer). Touched-area
tests green.
C179 — the A4b field scope, proved by opening the screens (2026-10-02). The
production order (stock & sell) asked a buyer, a broker, a rate, payment terms,
brokerage, pass-on and a delivery block on BOTH faces; full-flow.html row A4b
says "NO customer/party (internal)" and sells out of FG stock at avg cost. Fixed
by kind and by side off the existing specs (SALES_ONLY_STEPS + COMMERCIAL_KINDS
for the order form, PROD_CONTRACT_SIDES / PROD_TERMS_SIDES +
productionOpImportantFields(side) for the operations form, partyMissing no
longer demanded of a PROD order) — nothing stored cleared, no POST name, action
or method moved, the trading / commercial / details sides and SO / JW / OUTSOURCE
byte-identical. The row's regression test gained a page-proof block: the real
screens opened over HTTP (stock order asks nothing commercial, Trading SO and Job
Work SO ask everything, prodorder ops screen has no Buyer / Rate & terms /
Quantity & delivery, a stock order with no party and no rate saves, a broker-only
SO still blocks on its buyer). Fail-pre-fix proven by reverting the eight lines
(11 fail) and restoring (30/30). c149 / c103 / c148 / c147 / c141 / c167 83/83,
unedited; tsc comparative — no new error on the touched files. PLANNING §346.
Suite + build + live deploy stay with the drain's ship gate.
C182 — the MODE flag retires as an order-entry question (PLANNING §347).
A trading SO now asks nothing about make-to-order vs from stock on either
face, and the production operations trading side asks none either: the
ORDER_FIELD_SPEC entry, the background <select name="mode">, the field
box, the prefilled badge and STEP_LESS_ENTERABLE line all went, and so
did the production side's spec entry, its MTO/STOCK field box and its
PRODUCTION_OP_FIELD_SURFACES mapping — static/full-flow.html A4 loses the
MODE flag clause in the same turn. Nothing stored is touched: the column, its
validators, the record page's Make / stock row and put('mode', …) carry,
and the hidden input that posts the picked order's value all stay; the hidden
input now declares its reason in PRODUCTION_OP_INTENTIONALLY_HIDDEN. Stale
pins were re-pointed to the shipped reality (c167's list is pinned by name,
c14-orders / c143 / c149 / c179 / fullflow-orders lose only the mode name)
rather than skipped. tests/c182-no-mode-flag.test.ts proves the case by
opening the four real screens over HTTP. 189/189 across the nine touched-area
files; tsc comparative — no new error on the touched files. Q19 answered by
assumption (both faces go; the record page keeps the control). Suite + build +
live deploy stay with the drain's ship gate.
C183 — the booking form asks no GST %: the yarn master carries it (PLANNING §348).
Picking a yarn (or arriving with ?yarnId=) fills the booking's gstPct
from yarn.gst_pct, falling back to the linked order's design.gst_pct — the
fabric master, i.e. the schema's own "null inherits the fabric GST" rule. The
value shows on the summary with the prefilled chip naming its master
(✓ yarn master / ✓ fabric master) and is confirmed like any carried value.
BookingWizard.svelte lost its gst step (14 → 13 steps; FIELD_OF_STEP,
payment term and remark renumbered, FOCUS_STEPS 13) and with it the
gstPct/gstCoins props; the field is edited in its own §4b popup on the
page (z-[60], docked on kbOpen, {#if !kbOpen} chrome, no name on its
controls, Done button), opened by the summary row through openWiz(-1) — the
same step-less marker C180 settled, so a field with no step still owns a way
to edit. The last booking's GST survives as a one-tap coin there, never as the
prefill. +page.server.ts gained ONE selected column
(fabricGstPct: design.gstPct) on the join it already had; the GST
derivation, the columns and every stored booking are byte-identical, and the
POST name gstPct still posts. c104's step list lost gst and nothing else.
tests/c183-booking-gst-prefilled-not-asked.test.ts opens the real form for
all three master shapes and posts two bookings to pin the amounts: 11/11,
9 of the 11 failing before the row; tsc comparative — no new error on the
touched files. Q20 answered by assumption (no linked order = no fabric master
= blank and optional, as today). Suite + build + screens + live deploy stay
with the drain's ship gate.
C184 — the yarn booking form's only index is its summary, not three big section cards (PLANNING §349).
The owner saw "the three sections big buttons ... at the bottom of the yarn
booking form" and asked why the screen was off docs/FOCUS-ENTRY.md. It was:
§2 puts every extra card between the title and the summary and lists no
section menu, and §3's summary already opens each field's own pop-up from its
own row. The cards were a second index that jumped to a wizard STEP, and once
BookingSummary grew its own Clear/Save bar they fell below the whole form —
the last thing on the page. Deleted: the grid grid-cols-2 gap-2 card grid
and everything that existed only to feed it — SECS, menu, secDone,
yarnSummary, bagsSummary, rateSummary, the already-unused fieldsLeft,
and openSec. Kept: WIZ_STEP_OF_SEC, because the check-and-save sheet's
jump-back still resolves through gotoSec(sec, step) to a field's own popup.
The capture-draft hint moved up beside the procure hint, where §2 part 3 puts
a fill hint. Nothing else moved: 13 wizard steps, every summary row/label/
required mark, the hidden real <form> (byte-identical POST names/values/
use:enhance, action="?/create"), the C14 gate and the GST maths are as
they were, and guided-gatepass/guided-checking — other screens with their
own menus — were read, never touched. docs/FOCUS-ENTRY.md §2 now carries the
rule: there is no section menu, on any focus-entry form, ever.
tests/c184-booking-no-section-cards.test.ts opens the real form over HTTP as
an owner and asserts the skeleton in order with nothing but the summary between
the title and the hidden form: 13/13, 5 of them failing before the row;
tests/guided-bookings.test.ts re-pointed on that one case only;
bun run screens -- --only bookings 2/2 PASS; tsc comparative — no new error
on the touched files. Suite + build + full screens + live deploy stay with the
drain's ship gate.
C185 — the yarn booking's check-and-save saves; its tick gate was a dead end there (PLANNING §350).
As told: "why the section of needs your tick - save blocked in check and save
page on yarn booking? remove it it is not letting me save the page". That
section was the C14 save gate ported onto /bookings/new: it went FIRST in the
sheet as Needs your ✓ — save blocked (n) and confirmReview() returned early
while any carried field was unconfirmed — so the sheet's Save did nothing. A
booking opened from a prefill is born with EVERY tracked field carrying a
source, i.e. born blocked. Measured on a real prefill booking (procure
?yarnId&bags + the order forward-link ?orderId + a last booking of the same
product): 13 of 13 fields counted, and the sheet gave no way out — no ✓ of its
own (the mark-only chip is on the summary rows, outside the sheet), and a gate
row was built without its step, so tapping one opened the SECTION's first step:
3 of 13 landed on their own field, 10 opened a different one. Removed from this
form only; its Save now saves. Kept: every prefilled badge (a carried value
still names its source and stays changeable), the three sheet groups with each
row's own step, the jump back to a field's own popup (gotoSec,
WIZ_STEP_OF_SEC, step: -1 → the GST's own box), and gate(), so a bare Enter
still opens the sheet first and nothing auto-saves. Untouched: all 14 hidden
POST fields in order, method="POST", action="?/create", use:enhance, the
13 wizard steps, every summary row/label, the server validation and the GST
derivation byte-for-byte, no stored booking rewritten; the three ORDER forms keep
their C14 "To confirm" gate and /api/approve + /approvals are as they were.
docs/FOCUS-ENTRY.md §18 now carries it as a NAMED EXCEPTION with its reason and
§7.1 points at it, so the pattern and the exception are both visible.
tests/c185-booking-no-tick-gate.test.ts opens the real prefill form over HTTP
as an owner (badges intact, nothing booked by opening it, Save gated on the
required fields only) and books a booking through a real ?/create POST of the
carried bytes: 16/16, 2 of them failing before the row. c14-bookings and
c162 lost only the pin on this form's gate and gained one that pins its
absence; c14-orders and every other form's pins were read, never touched. Q22
answered by assumption (the ✓ chip stays tappable, mark-only). tsc comparative
— no new error on the touched files. Suite + build + full screens + live deploy
stay with the drain's ship gate.
Owner rule 2026-10-03 — Pareto settles everything; the hard-stop/hold list is
retired (PLANNING §351)."add to docs to auto settle everything using pareto".
Rule 2 ("questions are Pareto auto-answered, always") already answered nearly
every ambiguity inside the row session, but five cases still told a session to
--hold and park the row blocked-owner: deleting/overwriting data,
money·GST·costing maths, breaking a row's MUST NOT, deploying to production,
changing what a customer is charged or promised. All five are answered now,
because their Pareto reading is already the safe one — leave that data / money /
approval alone, do the rest of the row, write the decision and the untouched
thing into the answer and the row's notes. A hold was only ever justified when
the fast reading and the cautious reading differ; in all five they are the same
reading, so the question was a note, not a fork. Docs only: AGENTS.md,
docs/OWNERSHIP.md rule 2 + rule 4's stale "waits for the answer" clause,
.opencode/agent/row.md step 8, the docs/QUESTIONS.md header, and
docs/QUEUE-AUTO.md (a question on the board is a notice, not a stop). No
engine change and no test touched; blocked-owner still exists for older rows
and the board's own actions — what is retired is new rows parking on it.
The same turn scoped it: the auto-answer belongs to the corrections
sequence (board rows under .opencode/agent/row.md + the drain) only — a
custom session the owner opens for their own task (/ui, discuss, a
one-off request) asks and waits, and never touches the board, the queue
machinery or --assume. Stated in AGENTS.md, docs/OWNERSHIP.md rule 2a
and row.md step 8 so every place a session reads the rule carries the same
boundary.
C186 — G0: the inward gatepass carries its SECOND photo (the confirmation evidence)
static/full-flow.html names two photos on G0; the form had one
(image = "Gatepass photo" 【PH】). Added Confirmation photo 【PH/OCR】 —
the sizer / weaver WhatsApp photo — as its own summary row with its own popup,
on ?kind=YARN_IN only.
Not a wizard step, not required. The confirmation arrives after delivery,
so per §11 rule 5 it is a popup row; WIZARD_IDS.YARN_IN is still
['sourcePartyId', 'countYarn'] and a real create with no evidence still lands.
Persists on the creation event, read off raw FormData like beamType
(Q23): no column, no zod change, no schema change, no migration. The GP photo
keeps gatepass.image and both filenames land side by side on the record.
Defect fixed en route (Q24):FocusEntry calls onfile(id, file) and the
page passed a one-argument handler, so the existing GP-photo upload was
posting the string image as the file. The handler routes on the row id now.
docs/FOCUS-ENTRY.md §5 gains step type 7 (photo / evidence file) with the
three rules. Untouched: every other G0 field, the status ladder, the POST names
(confirmPhoto added, nothing renamed), action="?/create", use:enhance,
the zod schema, the other seven kinds, and no stored gatepass rewritten.
Proof: tests/c186-gatepass-yarn-in-confirmation-evidence.test.ts 11/11
(9 fail pre-fix), opening ?kind=YARN_IN over HTTP as owner and POSTing two
real gatepasses — one with both photos, one with none. tsc comparative: 28
pre-existing errors before and after, none on the touched files. Suite, build,
screens and the live deploy stay with the drain's ship gate.
C187 (G0b, the DIRECT delivery intimation) — the gatepass form's DIRECT kind now
asks G0b's two destinations only (premises default, named sizer; no our godown on
that kind, the stored destPlace enum untouched), asks no gross/net kg (count +
bags only, the weights belong to the inward leg; YARN_IN / YARN_OUT / WINDING keep
theirs), and carries G0b's close-out: the sizer confirmation photo 【PH/OCR】 and
the new GP counter-copy signed (after delivery) 【T】 tap, both optional, neither
a wizard step, so an intimation never waits on its delivery. The tap posts as one
appended name, counterCopy, read off raw FormData onto the gatepass:DIRECT
creation event — no zod field, no column, no migration, no server validation
change. Q25 (the close-out lives on this form; /inward untouched) and Q26 (one
appended POST name) taken by Pareto and recorded. Proof: 18 cases in
tests/c187-gatepass-direct-delivery.test.ts, 10 of them failing pre-fix, opening
the real screen over HTTP as owner and POSTing two real DIRECT gatepasses.
tsc comparative: 28 pre-existing errors before and after, none on the touched
files. Suite, build, screens and the live deploy stay with the drain's ship gate.
C188 (B0b, the beam-outward gate tap) — ?kind=BEAM_OUT was carrying B0 (the
planner's intimation) and B0b (the guard's tap) at once, so the guard had to
re-type the planner's numbers before Save would enable. It now asks B0b's list
only: the beams the intimation pre-assigned (tap/scan, label says so), the actual
date and the transporter — the wizard is two steps (targetPartyId, beamIds)
instead of five — plus a static Send confirmation row ("saving this tap records
that the beams were sent") and a static Planned on the intimation (B0) row
naming the four fields this form no longer asks. #beams, beam type, expected date
and vehicle keep their POST names, zod fields, columns and event payload keys; the
gatepass_link write for the tapped flanges and
beamOutFlangeError(d.beamCount, d.beamIds, floorCodes) are byte-identical, so a
forged count that disagrees with the taps is still rejected. The other seven kinds
keep their own fields; nothing auto-commits. Q27 (the mandatory consignee sizer
stays) taken by Pareto and recorded. FOCUS-ENTRY §5 gains step type 9 (zero-entry
gate tap) and §13b's B0b row moves from ⛔ to ✅ on /gatepasses/new?kind=BEAM_OUT.
Proof: tests/c188-gatepass-beam-out-zero-entry.test.ts 15/15, 6 failing pre-fix,
opening the real screen over HTTP as owner (B0b's rows served, the planner's
absent) and POSTing a real zero-entry BEAM_OUT gatepass plus one that the
reconciliation refuses. tsc comparative: 28 pre-existing errors before and after,
none on the touched files. Suite, build, screens and the live deploy stay with the
drain's ship gate.
C189 (2026-10-03) B5b — the yarn-outward gatepass (/gatepasses/new?kind=YARN_OUT)
now asks B5b's four things only: the barcode taps (lotIds, required), the
party/weaver, the vehicle and the guard's taps. The count, mill ticket, bags,
total cones, gross and net it inherited from YARN_IN are an inward receipt's
fields and came off that kind (goods: {}), with a static row naming where they
land (yarn in, G0 — or a movement B5 when stock is weighed going out). YARN_IN
and WINDING keep their weights byte-identical; the six hidden inputs, the zod
schema, every server validation and the lot gatepass_link write are untouched,
so a POST that still carries them is stored exactly as before.
Proof: tests/c189-gatepass-yarn-out-b5b.test.ts 14/14, 3 failing pre-fix,
opening the real screen over HTTP as owner (B5b's four rows served with their
flow badges, none of the six goods rows anywhere) and POSTing a real B5b-only
YARN_OUT (it lands with its lot link, payload.linkCount 1, no goods invented).
tsc comparative: no new error on the touched files (only the repo-wide
bun:test module error every test file has). Suite, build, screens and the live
deploy stay with the drain's ship gate.
C190: BEAM_IN (the sizing return, C3 step 1) now asks the row's own list — the
guard's beam/flange taps on the physical ids (flangeNo first), the sizer, and
mtrs/beam · ends · quality off one sizing-slip scan that only ever prefills and
that the user confirms per row. Fixed: the taps had no row at all on this kind
(showBeamPick excluded it, so beamIds was a POST name with nowhere to be
tapped and the wizard read "Step 1 of 2"), the three values were not on their
【OCR → …】 modes, and the one OCR handler ignored the row id — scanning the
quality wrote the VEHICLE no. The full hishob stays out (a static row names C3
step 2). POST contract byte-identical (34 names), server/zod untouched, the
auto-lot hook untouched, C173's sizing screen untouched, the other seven kinds
unchanged. Proof: tests/c190-gatepass-beam-in-c3.test.ts 16/16 (8 failing on
the pre-fix source), opening the real screen over HTTP as owner and POSTing a
real BEAM_IN (it lands with its beam link and its three values on the creation
event); the five sibling gatepass suites re-run green (66/66). tsc
comparative: no new error on the touched files. Suite, build, screens and the
live deploy stay with the drain's ship gate.
2026-10-03 — row line: a drain that idles clean no longer freezes the watcher
Symptom: C192 sat pending with six runnable rows while the watcher was armed and
healthy. ps showed its only child as sleep 10 — the fingerprint fast path — and
no .queue-stall alert, because the stall alarm sits behind that same skip.
Cause: board_fingerprint() hashed only the four board files plus drain liveness. A
drain refused for a dirty tree (row-run.sh drain returns 0 as a clean idle) exits
without touching either, so the fingerprint froze and queue-watch.sh's loop took
sleep "$POLL"; continue indefinitely: no row, no alarm. Measured cold start 57 min
(04:24 refusal → C192 started 05:21).
Fix: the fingerprint now carries a time bucket (…:$(( $(date +%s) / tick ))), so the
cheap skip still holds inside one tick and work is re-checked every tick, as the loop
did before the fingerprint existed. Proof: tests/queue-watch-rearm-after-idle-drain
3/3, 2 of them failing on the pre-fix script (an unchanged board now yields
changed across a tick; inside one tick it still yields same). tsc comparative:
the new test file carries only the repo-wide bun:test/Bun lines every test file has.
The watcher picks the change up on its next start — the running one keeps its old
inode (the file was replaced atomically), and the drain's ship gate restarts the unit.
C192 — BEAM_RETURN: a beam back to its party is B5's return-to-party clause (2026-10-03)
Q33, answered by Pareto: folds into B5; no new flow row, no retirement.static/full-flow.html already carries the clause "returns to party" on
B5 (Movements — stock that moves WITHOUT a buy/sell doc), and PLANNING's
own beam list calls it "Beam returns to party / transfers (party-owned
beams)". A new row would duplicate C170's shipped movements form; retiring the
kind is barred by the row's MUST NOT while the question is open.
The gatepass, not /movements: a movement's items are yarn lots
(movements/+page.server.ts resolves every item row to a lot / bag barcode),
so a beam leg cannot post there. What the gatepass adds is the physical outward
leg — the guard's exit taps on the flange nos, with vehicle and date.
Changed: B5's field cell names the beam leg and the kind that carries it; the
kind's served hint and its beam-tap caption name B5 (the caption previously
read "C2 pre-assign", a tippan outward to a sizer). C191's loose pin on
that leftover caption string is re-pointed at C2b's own caption.
Untouched: every field, both wizard steps, all 34 POST names in order,
action / method / use:enhance / zod / every server validation,
/movements, the auto-lot hook, every stored gatepass, the other seven kinds.
Nothing deleted.
Proof: tests/c192-gatepass-beam-return.test.ts 13/13 (5 fail pre-fix), the
page opened over HTTP as owner plus a real ?/create POST that lands with the
party, the tapped flange and the set no; the five sibling gatepass suites
green (64/64); bun run screens -- --only gatepasses 4/4. tsc comparative:
no new error on the touched files (only the repo-wide bun:test / bun:sqlite
lines every test file carries). Suite, build and the live deploy stay with the
drain's ship gate.
C193 — WINDING: the sizing → winder outward leg is its own gatepass row, C5b (2026-10-03)
Q34, answered by Pareto: it is its own flow row (C5b), not part of C5.C5 is the receipt of what the winder wound (empties, ring wt, rec net
wt, the net/gross difference, the set closing) and C3 step 2 already
promises a different doc — "after final hishob the system ALLOWS creating the
WINDING-YARN GATEPASS → issued to the winder". The leg between them had a
form (?kind=WINDING) and no name. Not C5 (that would make one row carry both
a gatepass and a reconciliation), not a B5 movement (items there are yarn
lots; C3 says the thing created is a gatepass), not retired (a live kind is not
removed by a map row).
Changed: static/full-flow.html gains C5b Winding-yarn outward gatepass
(sizing → winder) immediately before C5, drafted from what the form asks —
winder 【S】, sizing/warping set no 【P】, bags/cones 【N】, gross wt + net wt
【N】, transfer year 【P】, vehicle 【SC/N】, transport, gatepass photo, GP no +
GP date, the status ladder — and it says in words that the empties, ring wt,
received net wt, the net/gr difference and the set closing are the receipt's.
Nothing renumbered; C5's field cell is byte-identical (its "when" cell
now names C5b, and C3's WINDING-YARN GATEPASS carries the identity (C5b)).
Changed on the form: one hint sentence (the kind is C5b; the reconciliation
still lives on the winding receipt screen) and one static row, "Empties,
ring wt, received net (not this gatepass)". A static row posts nothing.
Untouched: /winding (C174's screen) byte-identical with its empties, ring
wt, received net and its auto diff / set / GP rows; KIND_CFG.WINDING's keys
and values; the wizard's two steps; all 34 POST names in order; action /
method / use:enhance / zod / every server validation; the auto-lot hook
['YARN_IN','WINDING','DIRECT']; every stored gatepass; the other seven
kinds. Nothing deleted.
Proof: tests/c193-gatepass-winding-kind.test.ts 17/17, 8 failing on the
pre-fix source (proven by stashing this row's four source files), the page
opened over HTTP as owner on ?kind=WINDING and on /winding, plus a real
?/create POST that lands the winder, the set no, the weights and the FY.
The five sibling gatepass suites + C174 green (75/76 — the one red is C172's
pre-existing B0 pin, re-worded by C188, recorded by C191 and owned by C196).
bun run screens 80/80. tsc comparative: no new error on the touched files
(only the repo-wide bun:test / bun:sqlite lines every test file carries).
Suite, build and the live deploy stay with the drain's ship gate.
C194 — G2 transporter pickup: the due-for-collection list is on the focus-entry pattern (2026-10-03)
Q35, answered by Pareto: the list was already there — fix it, do not fork a
second one./transporter's load is every outward gatepass in
intimated | due | partial, so an intimation is on the board the moment it is
created (no filing step, no second status), and ?/pick writes
picked | partial + the date, note and photo on G0's own ladder. The
recheck that queued the row ("no G2 anywhere in the code") had looked for a
due-for-collection status; docs/PLANNING.md §361 records this.
Changed: src/routes/transporter/PickCard.svelte is now the focus-entry
pattern — a read-only sectioned summary ("Beams / boxes to lift", "Pickup":
pickup · date · note · photo) over a hidden POST form, one field per
pop-up, no section cards, committed by the single Picked ✓ tap. The row
chrome in src/routes/transporter/+page.svelte loses its card (the list is
keyed by row id now, so a list refresh cannot carry one row's state into
another's). /gatepasses's footer sentence became real links to /guard and
/transporter.
Fixed on the way: the lot label lookup in
src/routes/transporter/+page.server.ts tried the lot id alone, while B5b
files gatepass_link under the picked barcode / lot no — so every box to lift
on a yarn-outward intimation read lot: —. It now mirrors the beam lookup
(id → lot no → barcode). Read-only, no write, no validation.
Docs: static/full-flow.html's G2 cell keeps its whole list byte-identical and
gains the two facts this row settled (built from every open outward intimation,
no filing step; rides G0's ladder, no second status); docs/FOCUS-ENTRY.md
§13b gains the G2 Transporter pickup row as ✅ on the signed UI at
/transporter?/pick (§13's own table untouched).
Proof: tests/c194-transporter-pickup.test.ts 16/16, 7 failing on the
pre-fix source (the flow-map cases, the served-row shape, the pop-up shape,
the partial value, the partial POST value and the §13b row), proving it by
OPENING /transporter and /gatepasses over HTTP as a signed-in owner and by
real POSTs — a ?/create outward intimation with a beam code and a lot code
lands and is served with both lines named, a ?/pick tap lands picked +
date + note + photo while every field of the gatepass stays byte-equal, the
partial tap lands partial, a picked row leaves the board, and a forged id /
an inbound row / an already-delivered row are all still refused. bun run screens -- --only transporter green.
MUST NOT kept: the eight kinds, all 34 POST field names, action / method /
use:enhance, both zod schemas and every server validation; the transporter's
five POST names and its three refusals; nothing auto-commits; no gatepass
deleted or re-numbered; /guard, /dispatch (C161) and /gatepasses/[id]
untouched. tsc comparative: no new error on the touched files (only the
repo-wide bun:test / bun:sqlite lines every test file carries). Suite,
build and the live deploy stay with the drain's ship gate.
C195 — G4 gate inward: the plate is read by the camera, the inward code finds the delivery (2026-10-03)
What: full-flow row G4 "Gate inward" landed on top of C169 (which had
already shipped the /inward focus-entry receipt, so this row corrects that
shape and never forks it). Two field behaviours changed:
the vehicle no is a plate-OCR field — mode: 'plate', its own mode in
focus/FocusEntry.svelte, rendering the new
src/lib/components/gate/PlateScan.svelte: the camera target is PRIMARY,
the typed entry is the FALLBACK behind Type it instead, the read value is
shown plate-shaped with a check it line. Applied on the gate arrival (G4's
own leg, /inward) and on the gatepass form's vehicle row, which was
mode: 'text' + scan: true — free typing with a scan button under it.
scan: true stays, so a scan still routes on the row it sits on (C190).
the inward code got its own scan field at the head of /inward: it is
the code on the gatepass the transporter carries, so it is a lookup — it
finds the delivery on the board, scrolls it up and highlights it, and a miss
says so. No new POST name, no column, no migration; the arrival tap
still posts id · vehicle · arrivedBags to ?/markPicked byte-identically
with the plate widget filling the same vehicle name behind it.
the barcodes print automatically 【A】 — stated on the gate leg with
nothing to fill: no barcode input on either surface, and C169's barcodes
static row on the B2 receipt is untouched. partial/all from pickup (G2)
is already the row's own status badge, so nothing new was needed.
Which screen is G4's entry: /inward (the gate arrival). The gatepass
form is G0's intimation and gained nothing G4 owns — no inward code, no
barcode field. Recorded in docs/FOCUS-ENTRY.md §17b and in PLANNING 362.
Proof: tests/c195-gate-inward-g4.test.ts 17/17, 12 failing on the pre-fix
source, proving it by OPENING /inward and
/gatepasses/new?kind=YARN_IN over HTTP as a signed-in owner — the served
/inward carries the inward-code scan, the plate target on the arrival tap and
the automatic-print line, and the served gatepass row is Vehicle no badged
SC/N; opening /inward writes nothing. bun run screens -- --only inward
1/1 and --only gatepass 4/4 green against 127.0.0.1:4102.
Two stale pins corrected in passing, both about this exact field:
tests/c14-gatepass.test.ts and tests/statusact-forms.test.ts asserted
onscan={ocrVehicle} after C190 moved the wiring to the id-routing
onscan={ocrField}; they were red at HEAD and now pin the handler that is
really wired.
MUST NOT kept: the gatepass status vocabulary, the eight kinds, their POST
names, action / method / use:enhance, both zod schemas and every server
validation; the inward page's markPicked / markDelivered contracts; no
driver face/biometric work (G4 marks it 【TBD】 — no face, biometric or
door-control code on either surface); no existing inward record rewritten;
nothing auto-commits. tsc comparative: no new error on the touched files
(only the repo-wide bun:test / bun:sqlite lines every test file carries).
Suite, build and the live deploy stay with the drain's ship gate.
2026-10-03 · C196 · the gatepass status table corrected to the truth:
docs/FOCUS-ENTRY.md §13's /gatepasses/new line no longer claims the form is
un-migrated (it is on the C168 shell, 402ba69 — §2 title, §3 sectioned
summary, §4 per-kind wizard, hidden POST form untouched), lists the per-kind
audit one row per kind (YARN_IN C186 · DIRECT C187 · BEAM_OUT C188 · YARN_OUT
C189 · BEAM_IN C190 · BEAM_REPAIR C191 · BEAM_RETURN C192 · WINDING C193) and
names the still-open gate (the §12 items 7–8 phone-width evidence with the
keyboard actually open) instead of claiming the form is finished. Docs-only: no
other FOCUS-ENTRY.md line rewritten (the five sibling §13 rows are asserted
byte-equal), no form or component touched, all 34 POST names / action /
method / use:enhance / both zod schemas unchanged, no kind added or removed.
C130 evidence: all eight kinds opened over HTTP as a signed-in owner —
tests/c196-focus-entry-gatepass-status.test.ts 14/14, 4 failing pre-fix, the
eight page cases green before and after. Two stale pins corrected in passing:
tests/c151-focus-entry-doc-copy-exact.test.ts (required the §13 gatepass line
to read ⏳, red since C168) and tests/c172-focus-entry-flow-coverage.test.ts
(B0's pin, red before this row and logged as owned by C196 by C191/C193; plus
its prefix-matching rowFor that made rowFor('B5') read C189's B5b row) —
12/12. bun run screens -- --only gatepasses 4/4. Two Pareto calls on the
record: Q36 (the per-kind audit is done — the row is doc truth, and the open
gate is named, not collapsed) and Q37 (the B0 pin is corrected test-side,
never by rewriting another doc line). tsc comparative: no new error on the
touched files (only the repo-wide bun:test / import.meta.dir lines every
test file carries). Suite, build and the live deploy stay with the drain's ship
gate.
2026-10-03 · C197 · the summary value slot carries a value, never an
instruction: FocusEntry.svelte grows one prop (staticNote, rendered only in
that field's own pop-up's sub) and a static row that has a note becomes
tappable, so the sentence the owner saw on the beam-in GP no
(Auto on save — the next GP number) is now a neutral Auto in the slot and a
tap-through read-only pop-up. Nine call sites corrected (beam card · inward ·
gatepasses · movements · stock take · samples · sizing · winding): every
auto-number row reads Auto/Automatic/—, every real computed value
(beam meter, party, cones, gross/net, kind, totals, set counts, variance, the
carried-order line) reads exactly as before, and no POST name, action,
method, use:enhance, zod schema, event, field or row count changed. Six
rows keep their text on purpose — C188–C193's five flow rows (each proved over
the served page by its own row test) and the status ladder — and are named as
exceptions in docs/FOCUS-ENTRY.md §18 with the row that pins each. §3 now
states the rule. C130 evidence: tests/c197-no-instructions-in-field-rows.test.ts
30/30, 24 failing pre-fix with the sources stashed — both halves fail, the
nine call sites AND the served pages opened over HTTP (gatepass ×4, /beams,
/sizing, /winding, /lots/stocktake; /movements, /samples, /inward
hold their entry behind client state, so theirs are source-pinned as c170/c166/
c169 already do). Sibling suites green: c186 · c187 · c190 · c195 · c196 ·
c14-small · c14-movements · c14-gatepass · c40 · fullflow-small · fullflow-gatepass
· fullflow-movements · gatepass-kinds · beamout-roles · chip-suggest ·
revpicker-buy · guided-small · guided-samples · ui (280/280); two stale
staticValue: source pins in c186/c187 re-pointed at staticNote: (same
sentence, same intent). bun run screens 75/75 on 127.0.0.1:4102. Three
failures in the FOCUS-ENTRY-reading set (c144 rule 4 · c194 §13b · c184 §6) are
pre-existing — identical with everything stashed. tsc comparative: no new
error on the touched files (only the repo-wide bun:test line every test file
carries). Suite, build and the live deploy stay with the drain's ship gate.
C181 shipped but reddened the suite: Bun module mocks leak, so the suite runs --isolate (2026-10-03)
The deploy aborted at step 1/6 — 17 red, all from one stub.deploy.sh pulled
C197 and ran the suite, which failed with C106's live-data cases answering "user
u1, even for a request with no cookie at all". C106 passed alone (11/11) and
failed after C181 (7 fail of the pair). Cause: tests/c181-order-ocr-single-affordance.test.ts:34
calls mock.module('#lib/server/session', …) at module scope — Bun module mocks
are process-global, so that stub answered every later file that imports the same
module, forever.
Probed what actually stops it (Bun 1.4.2).mock.restore() does not undo a
mock of a module consumed through a #lib/* alias — probed three ways (restore,
re-register the real exports, mock by relative path instead of the alias) and all
three still leaked. What works is --isolate (a fresh module registry per file),
measured on 20 files: 32.9s → 34.9s, ~6%.
Applied everywhere the suite starts: package.jsontest, scripts/deploy.sh
step 1/6, scripts/row-run.sh ship, scripts/promote-stable.sh step 3/6, plus the
commands a human or a row session copies (AGENTS.md, docs/TESTS.md,
docs/DEPLOY.md, .opencode/agent/row.md). tests/module-mock-hygiene.test.ts is
the regression guard (fails pre-fix on all four invocations + four docs); c181's
afterAll keeps a mock.restore() as hygiene with the finding written beside it,
not as the fix. No product code touched — the leak was in the test harness, so the
suite's own gate is where it is fixed (PLANNING §365).
Eight stale pins standing between master and a deploy — each re-pointed at the row that superseded it (2026-10-03)
The reds, and what each one actually was.C197 shipped with the row gate
(bunx tsc --noEmit) but its full suite never ran, so nine pins froze on markup
that later rows moved. None was a live bug: each was a pin reading a string a
newer, owner-told row had changed.
c137 · c138 (production order's payment / brokerage popups) — C179 (§346,
owner-told against full-flow A4b) made a production order a stocking order:
no buyer, no rate, no payment terms, no brokerage, sold later at avg cost. Both
pins now hold the SO half unchanged and assert PROD is asked none of the six
(tapOpensPopup -1, summaryRowPopup 'none', orderFormHas false).
c142 (picks step "just before the rate") — the rate is gone from PROD, so
the pin now holds just before the rate on SO/JW/OUTSOURCE and, for PROD, that
picks is the last step right after the quantity it counts.
c176 (one handler on the summary row) — C180 put the single-field box inside
the rowStep(r) < 0 branch, so the static <div> is now that branch's {:else}.
The pin demands the whole chain in render order, which is stronger than the
old {#if rowStep(r) < 0}\s*<div. docs/FOCUS-ENTRY.md §3 was carrying the
pre-C180 shape — the spec would have built a form where a stepped-out field
cannot be edited — so §3's block and a rule for it were corrected too.
c189 (BEAM_OUT Send confirmation) — C197 gave the automatic rows their kind
badge back, and a static row with a note renders as a button (badge shown) while
one without stays a plain <div> (no badge). Both halves now pinned, read from
the two sides; the file confirms the doc and the markup agree.
c194 (FOCUS-ENTRY §13 "the /gatepasses/new row is still ⏳") — every gatepass
row went ✅ when its row shipped (C188/C189/C190/C193). The pin now asks §13's own
vocabulary: every /gatepasses/new row in §13 is live, so a later row cannot
quietly un-ship its neighbour.
c184 (the gatepass test's four section titles in one string) — C188 split that
pin into a loop plus a per-kind photo/vehicle conditional. Each title is now
demanded both in the gatepass test and in the page.
c144 §9 rule 4 (body lock while ANY popup is open) — C180 added fieldPop to
the guard. The pin is now asked of the guard itself: every popup the form opens
must be named in it, which also fails when a later row adds a popup and forgets
the lock.
precheck-record-never-skips-a-row — the case pinned twelve ids; all twelve are
now done, so it could only ever be true for one board state. It now asks the rule
(--first never hands out a row that needs a pre-check), and C198 carries a
real pre-check record: read queue-watch.sh (:72-93 fingerprint has no git HEAD or
stamps file; the fp == last_fp skip at :104-107 sits above the reap, the thermal
tick and the stall alarm), confirmed row-run.sh stays byte-identical, and noted
the service-restart requirement on the live script.
Verified per file, then the whole suite: every one of the ten reds above green, with
the neighbours that share each pin (guided-gatepass, c151, c176, c194,
c189) re-run alongside. Nothing in src/ moved — ten pins, two docs, no product.
/queue restored to the full Correction board (2026-10-04, owner: "revert it back
to the original state").src/routes/queue/+page.svelte is back to a39bc9d's
version — each row again shows its own wording (one line + "full wording"), its real
status, its acceptance checks, the evidence/assumption/page-proof badges, the counts
wall, and Needs-your-action with the deferred/question lines. Reason: 22daae6
(2026-10-02) replaced the description with a generic area label and collapsed nine
statuses into five words, so verifying, ready, scoping and conflicted all
read "in line" — a merged-but-unshipped row looked like untouched work. No BUILD-LOG
line, no PLANNING log and no test covered that edit; PLANNING §367 now states the
rule. One correctness addition on top of the restore: conflicted: 'badge-error' in
statusStyle (it previously rendered an unstyled badge). Loader, ORDER,
buildBoard, visibleRows and all four actions untouched; the page still gets its
rows from the same data.rows. Four pin files re-pointed at the restored shape
(queue-close-button, c164-queue-precheck, c198-row-line,
c129-proof-by-opening) — 56/56 green, tsc comparative clean, svelte-syntax
clean. The plain header/section wording from 2026-10-02 is dropped with the revert:
the board is one table again, as it was.
C198 (2026-10-05): the correction line now wakes on a completed row.
scripts/queue-watch.sh keeps only its loop shell and command dispatch; the
tick — fingerprint (now with git rev-parse HEAD + the stamps file), reap
without skip, stall alarm and thermal above the cheap skip — lives in the new
scripts/queue-watch-lib.sh, proved behaviourally by
tests/c198-watcher-wakes-on-completed-row.test.ts (5/5, red pre-fix).
scripts/row-run.sh byte-identical. Watcher restarted via
queue-watch.service; docs/QUEUE-AUTO.md states the new guarantee. Suite +
build + live deploy stay with the ship gate (blocked on the stale Oct-4 tree
per Q40, not this row).
C199 (2026-10-05): the order-form OCR read works again. thinking: { type: 'disabled' } is deleted from all four gateway bodies (the gateway
400s the field; re-proved live with the real key: 400 with, 200 without) and
a gateway refusal now fails visibly WITH the HTTP status instead of reading
as "nothing matched". tests/c199-order-ocr-read-fails.test.ts 10/10 (7 red
pre-fix); siblings 144/144 + lead 34/34, so all four AI reads still answer.
Suite + build + live deploy stay with the ship gate.
C200: the New tippan button is proved on the running app, and it was the
destination that was broken, not the button. /tippans/new threw
each_key_duplicate while booting (its loom datalist keyed each on a loom
NUMBER, which the master repeats), which aborted hydration for the whole
screen, and the page bound wizStep/wizSearch on a wizard that declared
step/search, so every summary row opened step 1. Both fixed in
TippanWizard.svelte alone; live re-proof: both buttons land on the form,
rows jump to their own field, 0 client exceptions.
tests/c200-tippans-new-button.test.ts 5/5 (2 red pre-fix); siblings 149/149;
bun run screens -- --only tippans 4/4, live-check 9/9; tsc comparative
clean. Suite + build + live deploy stay with the ship gate.
C201 — G0 and G0b now ask exactly what their flow cells list. The plate and
the transporter leave the two inward kinds (G4 /inward and G2 /transporter
own them, and a static row says so, its value slot carrying G4 · G2 and the
sentence riding staticNote); the typed total cones leaves yarn-in (G0 lists
cones per bag + bags; B6/C5b count cones); the mill lot and the cones per bag
leave the direct-delivery intimation (G0/B1 own them); and each cell badges the
fields it serves — DIRECT's count/bags are 【N】 typed, DIRECT's confirmation
photo is 【PH】, YARN_IN keeps 【S】 and 【PH/OCR】. vehicle, transport,
millLotNo and cones keep their hidden inputs, zod fields and columns, so a
booking carry or OCR hand-off still stores exactly as before.
tests/c201-gatepass-g0-g0b-flow-fields.test.ts 15/15 (11 red pre-fix), walking
the G0/G0b cells parsed out of static/full-flow.html at run time over the
served pages; gatepass family 208/208 + 58/58 with nine prior-row pins
re-pointed at the moved shapes; tsc comparative clean (src/ errors 146 → 146).
Suite, build, live deploy and bun run screens stay with the drain's ship gate.
Queue adds its own rows; a self-contained watcher works them (2026-10-05)
/queue gained a + new correction button (engine --add appends to the
Pending table, sets the target, regenerates QUEUE.md, logs it). A new
scripts/row-watch.sh runs under queue-watch.service: serial, one
opencode run session per oldest pending row (the row is marked doing while
it runs), Pareto questions on the record, then deploy.sh --no-suite + commit
push, mark done, delete the session. Sessions are titled row C<n> and swept
automatically. The board dropped the Needs-action / Ref / You-decide columns and
hides zero-count status badges; the demo row C62 was removed. bun run check
clean; live deploy green.
Faster correction rows: fast model, no forced UI proof (2026-10-05)
Owner: C62 ran too long; each correction must finish fast. scripts/row-watch.sh
now defaults the row worker to opencode-go/deepseek-v4.1-flash (C64) and no
longer tells it to run the slow bun run screens UI proof unless the change is
purely visual and nothing else can prove it (C65). Per-row timeout cut
7200s→3600s so a stuck row cannot hold the serial line for two hours. The
watcher reloads this config only on restart, so queue-watch.service was
restarted; the in-flight C63 session was re-run under the new settings.
New correction: screen URL instead of route, no Priority field (C63, 2026-10-05)
/queue's "New correction" dialog now labels its target "Screen URL" with a
type=url address-bar placeholder and a hint to paste the full https://… link
rather than the route, and the Priority select is removed. The row cards no
longer show a P0 badge, ?/create no longer forwards --priority, and
queue-next.ts --add drops the flag (usage names screen/url). Internal
planning priority is left as-is (Q49/A49 assumption). Proved by fetching the
served /queue page (HTTP 200): name="priority" count 0, "Screen URL" and
"not the route (src/routes/…)" present; queue-next.ts --list/--add still run.
UI tests last-resort rule made durable (C66, 2026-10-05)
C66 ("UI tests should be done, only if there's no other option") was a restatement
of C65, but C65 had only patched the watcher prompt while AGENTS.md still
called screen proof the default. The rule now lives in three places: the
row-watch.sh worker prompt step 3 (prove with the narrowest non-UI check; run
bun run screens only when there is no other option), AGENTS.md §
Verification, and conditional wording in docs/FOCUS-ENTRY.md §9. Q50/A50
assumption recorded. No app code touched; bash -n scripts/row-watch.sh clean,
bunx tsc --noEmit comparative clean.
Order-slip uploads read on deepseek-v4.1-flash (C67, 2026-10-05)
The /orders/new/trading upload felt stuck because the shared AI gateway was on
space-bunny-free, a reasoning model that burns its token budget thinking and
would run into the 45 s timeout before the one retry. The gateway
(src/lib/server/llmGateway.ts) now owns one model default,
LLM_MODEL_DEFAULT = 'deepseek-v4.1-flash', plus an llmModel() helper; the
four call sites (intake.ts, orderDocRead.ts, order-understand, order-ocr)
and lead-understand use it instead of their own space-bunny-free constant,
and the UI read-via labels report the configured model. .env.example and the
host .env set CAPTURE_LLM_MODEL=deepseek-v4.1-flash. Proved non-UI: the
gateway models list contains the model, and a real readDocumentWithReport text
read returned HTTP 200 with parseable JSON in ~4 s. Reversible by restoring the
env value; no schema, append-only data untouched. PLANNING §376. bunx tsc --noEmit shows no new error in any touched file; full suite/deploy left to the
watcher.
Sidebar segregates the new-order forms; each form names its kind (C68, 2026-10-06)
The Orders sidebar group had grown to mix the orders list and reference pages
with the three type-fixed order-entry forms. +layout.svelte now keeps
Orders (list + reference; its child renamed All Orders) and a separate
New order group for Trading SO / Job Work SO / Production Order — same
hrefs, icons and roles, and active() yields the /orders/new/* subtree to the
form's own entry so All Orders does not highlight beside it. The shared
OrderEntryForm.svelte shows the kind's
plain-language purpose line under the title (kindHelp), so SO/JW/PROD is
no longer the only clue. Display-only: wizard, POST names and validation
untouched. PLANNING §377, question Q52. Proved non-UI: bunx tsc --noEmit adds
no error in the two changed files and bun run check compiles with 0 errors;
suite/deploy left to the watcher.
Type-fixed order pages drop their dead rows (C69, 2026-10-06)
OrderEntryForm.svelte no longer shows the redundant kind-code and derived
open · auto status badges on the three type-fixed new-order forms, and their
summary plus save review drop the fixed Kind row and the derived Status row
(the Production Order's now-empty "Kind & setup" section goes with them). The
legacy combined form keeps both. Display-only; the hidden type input, POST
names, wizard and validation are untouched. PLANNING §378, question Q53. Proved
non-UI: a bun check of orderFieldSectionsFor plus the filter passes,
bunx tsc --noEmit adds no error, bun run check compiles with 0 errors;
suite/deploy left to the watcher.
The three type-fixed order links rejoin the Orders sidebar group (C70, 2026-10-06)
C68's separate New order sidebar group (Trading SO / Job Work SO /
Production Order) was the Orders list's own content read twice, so
+layout.svelte deletes it and restores the three links as children of
Orders, right after All Orders. Hrefs, icons, role filter and active()
are untouched; display-only and reversible, one group for finding and creating.
PLANNING §379, question Q54. Proved non-UI: a bun assertion over the navigation
literal passes (no standalone New order group, the three hrefs inside Orders),
bun scripts/svelte-syntax.ts src/routes/+layout.svelte compiles clean, and
bunx tsc --noEmit adds no error in the changed file; suite/deploy left to the
watcher.
The new-order links come off the sidebar entirely (C71, 2026-10-06)
C70 had merged C68's separate New order group into Orders; C71 is the
stronger restatement, so +layout.svelte deletes the three type-fixed entries
(Trading SO / Job Work SO / Production Order) from the Orders group and the
now-unused FilePlus import, and drops the active('/orders') yield for
/orders/new/* it existed for. Creation stays reachable from the /orders page
buttons and drafts row. Display-only, one file, reversible. PLANNING §380,
question Q55. Proved non-UI: a bun assertion over the navigation literal passes
(no /orders/new/ href, All Orders kept, FilePlus gone),
bun scripts/svelte-syntax.ts src/routes/+layout.svelte compiles clean, and
bunx tsc --noEmit adds no error in the changed file; suite/deploy left to the
watcher.
Orders are separated by kind in the sidebar (C72, 2026-10-06)
After C71 left the Orders group with one combined All Orders entry, C72 asks
for the orders to be separated. +layout.svelte adds one child per kind right
after All Orders — Sale/orders?type=SO, Job-work/orders?type=JW,
Outsource/orders?type=OUTSOURCE, Stock/orders?type=PROD — the
existing filtered list views the /orders page already tabs by (§117), and
active('/orders') yields while a ?type= (or ?operations=) filter is on,
the /gatepassesAll yields to the kind rule. No new-order create link
returns (C71) and no route, POST name, form or validation changes. Display-only,
one file, reversible. PLANNING §381, question Q56. Proved non-UI: a bun
assertion over the navigation literal passes (one child per kind, hrefs
/orders?type=…), bun scripts/svelte-syntax.ts src/routes/+layout.svelte
compiles clean, and bunx tsc --noEmit adds no error in the changed file;
suite/deploy left to the watcher.
Order views leave the sidebar; each tab creates its own kind (C73, 2026-10-06)
C72's per-kind sidebar entries come off: +layout.svelte deletes All Orders,
Sale, Job-work, Outsource and Stock, renames the now-orderless group to
Commercial (Leads/Hishob/Samples/Quote/Reports stay reachable), and drops the
orphaned active('/orders') branch. orders/+page.svelte binds its header and
empty-state buttons to an fType-keyed NEW_ORDER map, so the Sale tab reads
New Trading SO (/orders/new/trading), Job-work New Job Work SO
(/orders/new/job-work), Stock New Production Order
(/orders/new/production-order); OUTSOURCE keeps the legacy combined route and
All keeps Trading SO. Labels/hrefs only. Display-only, two files, reversible.
PLANNING §382, question Q57. Proved non-UI: a bun assertion over both files
passes (no order hrefs or Orders label in the layout, reference links kept, four
NEW_ORDER entries + route files present, both buttons bound, no hardcoded
Trading SO left), bun scripts/svelte-syntax.ts compiles both clean, and
bunx tsc --noEmit adds no error; suite/deploy left to the watcher.
Only All Orders leaves the sidebar, not the order kinds (C74, 2026-10-06)
C73 overshot; the owner meant only the All Orders entry. +layout.svelte
renames the group Commercial back to Orders and re-adds Sale
(/orders?type=SO), Job-work (/orders?type=JW), Outsource
(/orders?type=OUTSOURCE) and Stock (/orders?type=PROD) beside the kept
Leads/Hishob/Samples/Quote/Reports. Plain /orders stays out; the generic
query-aware active() still highlights a kind view, so the orphaned
active('/orders') branch stays dropped. The C73 per-kind create-button fix
stands. Labels/hrefs only. Display-only, one file, reversible. PLANNING §383,
question Q58/A58. Proved non-UI: a bun assertion over the layout + orders page
passes (group Orders, no Commercial/All Orders/plain /orders href, four
kind hrefs, five reference links, four page tabs + create routes),
bun scripts/svelte-syntax.ts src/routes/+layout.svelte compiles clean, and
bunx tsc --noEmit adds no error in the file; suite/deploy left to the watcher.
The orders table drops its redundant Kind column (C75, 2026-10-06)
After C68–C74 gave each order kind its own sidebar entry, the list's Kind
column only restated the page, so it goes: orders/+page.svelte removes
{ key: 'kindLabel', label: 'Kind' } from wideColumns and the dead
kindLabel row value; the job work table never had it. The Kind field stays
in the record popup and on the detail page (fields, not the column named).
No route/POST/loader change. Display-only, one file, reversible. PLANNING
§384, question Q59/A59. Proved non-UI: the real Svelte compiler run over the
file yields a wideColumns literal with orderNo/partyName and no
kindLabel (pre-fix source fails the same check), bun scripts/svelte-syntax.ts
compiles it clean, and bunx tsc --noEmit adds no error; suite/deploy left to
the watcher.
Bookings list stops crowding the phone row with buttons (C76, 2026-10-06)
The compact bookings table had already dropped to 4–5 columns (C162), but its
SmartTable actions slot still packed up to three buttons per row (Cancel /
Record gatepass / Attach to order) into a sixth table-fixed cell, wrapping
them on a phone. bookings/+page.svelte drops the actions fragment and sets
showActions={false}, and moves those actions into the row's record popup
(Record gatepass as the popup action; Cancel/Attach as popup buttons that close
it and open their own dialog). The floating bottom Cancel button is deleted. No
route/POST/loader change. Display-only, one file, reversible. PLANNING §385,
question Q60/A60. Proved non-UI: the real Svelte compiler run over the file
yields output with no $$slots.actions, showActions: false, and the three
moved actions still present (pre-fix source fails the same check),
bun scripts/svelte-syntax.ts compiles it clean, and bunx tsc --noEmit adds
no error; suite/deploy left to the watcher.
Gate guard row stops crowding the exit form onto the phone line (C77, 2026-10-06)
The /guard tap queue packed each row's identity block (GP no, kind, party,
Next badge) and its ?/exit form (vehicle input + camera + Exited ✓) into
one justify-between line, which overflows a ~360 px phone. guard/+page.svelte
makes the identity block a plain <div> and drops the form to its own
full-width row beneath it; the input is now min-w-0 flex-1 and the camera
label + submit shrink-0, so the action fits one line. The twin /transporter
row already had this shape. No route/loader/action/POST-name/validation change;
the per-beam reconfirm chips, the DISPATCH due-list and the OCR handler are
untouched. Display-only, one file, reversible. PLANNING §386, question
Q61/A61. Proved non-UI: compiled the component with the real Svelte compiler
(svelte/compiler, generate: 'client') and asserted no justify-between, the
identity div and ?/exit form as separate siblings, min-w-0 flex-1 input,
shrink-0 camera/submit, and the ?/exit / id / vehicle / Exited
contract still present (pre-fix source fails the same check),
bun scripts/svelte-syntax.ts compiles it clean, and bunx tsc --noEmit adds
no error; suite/deploy left to the watcher.
Build + deploy once per correction batch, not per row (C78, 2026-10-06)
The live line (queue-watch.service runs scripts/row-watch.sh) re-built and
re-restarted the host after every finished row. row-watch.sh now calls
scripts/deploy.sh --commit-only per row (pull --rebase + commit + push only)
and reserves the full build + restart + live-wholeness deploy for the moment the
board has no runnable row left; the new --commit-only mode skips suite,
apply-db, build, restart, health and live-check but keeps the mandatory
commit+push and the C125 remote guard. Because bash freezes the running
watcher's copy of row-watch.sh at start and the row-line deploy never
restarted it, deploy.sh step 6 now schedules the deferred watcher restart when
the board is DRAINED — so the first post-C78 drained deploy (this one) reloads
the batched loop; it still skips while a row is queued. Pareto Q62/A62: every
row is still committed immediately (a growing uncommitted pile would be
autostashed by the next session's pull); only the deploy is batched. No app code
touched. Proved non-UI: a sandbox runs the real row-watch.sh once against
stubbed deploy/queue/opencode — another runnable row queued -> --commit-only,
board drained -> --no-suite (pre-fix HEAD deploys in both, so the check is red
before the fix); deploy.sh --commit-only with stubbed git/bun/systemctl does 0
bun calls / 0 restarts / commit+push, while --no-suite does 2 bun calls + 1
restart; and the row-line step 6 with stubbed systemd-run schedules the deferred
reload only when drained (queued -> 0, drained -> 1). bash -n clean on both
scripts; suite/deploy left to the watcher. PLANNING §387, Q62/A62.
Flash is the default model for all agents (PLANNING §388, 2026-10-06)
Owner order: deepseek-v4.1-flash everywhere by default. scripts/row-run.sh
(--agent row) moved off space-bunny-free to opencode-go/deepseek-v4.1-flash
(ROW_MODEL still overrides); project opencode.json restored pinning
model + small_model to flash. row-watch.sh and the app LLM gateway were
already on flash. bash -n clean, opencode.json parses, no space-bunny-free
default left in scripts/.
Sizing tippan asks the order first, and gates everything on it (C79, 2026-10-06)
/tippans/new already carried the order, but as the optional Weaver
contract step at the bottom of the wizard, so the planner met the program and
yarn steps before the order that should have driven them. The order is now the
wizard's first, compulsory step (TippanWizard.svelte: STEPS[0] is
weaverOrderId, retitled "Production order / Trading sale order?",
required: true, with an empty-state line when no order is open and a disabled
Next until one is picked). +page.svelte gates every other entry point on
orderChosen: openWiz clamps to step 0, wizNext will not leave step 0,
checkAndSave reopens step 0, requiredMissing counts the order so Save stays
disabled, and the summary gains a required Production / trading order row
whose sibling rows are disabled (TippanSummarylocked/rowLocked). Relevant
fields still fill from the order through the existing onWeaverOrder()
(quality, party, loom), now fired on the pick via the wizard's new
onPickOrder. The hidden form's names/order, the server validations, the
solver, the review sheet, print/amend and the Assign-later card are untouched;
Pareto Q63/A63 keeps create accepting a null order (entry gate, not a new
validator). Proved non-UI: bun scripts/svelte-syntax.ts compiles the three
touched .svelte files clean; a source-contract script asserts the step order,
the required label, every gate and the summary lock (17 assertions fail on the
pre-fix blob, pass after); bunx tsc --noEmit adds no error on the touched
files (only the pre-existing +page.server.ts(324,6) null-reed error) and
bun run check stays at its pre-existing 322 warnings / 0 errors. PLANNING
§389. Suite/build/deploy left to the watcher.
Sizing tippan orders read by quality · party and accept several at once (C81, 2026-10-06)
/tippans/new's order step (C79's first, compulsory step) no longer lists
job-work contracts and no longer names orders by number. +page.server.ts
filters the open orders to the non-job kinds (JW/OUTSOURCE dropped) and
enriches each with its quality (alias || qualityName) and party name; the
wizard picker, the summary row, the review row and the Assign-later select
all render quality · party. The step is now a multi-select: taps toggle
orders and fill the fields from the first picked, and the selection rides a
new nullable tippan.weaver_order_ids JSON column (drizzle 0032) while
weaver_order_id stays the first order so print/detail/?weaverOrderId=
are unchanged. Pareto Q64–Q66/A64–A66 (exclude OUTSOURCE too; persist the
selection; picker-only gate). Proved non-UI: a Bun script against a temp
libSQL DB seeds SO/PROD/JW/OUTSOURCE orders and asserts the load returns only
the two production/trading orders with quality+party, and that ?/create
with weaverOrderIds=SO1,PROD1 writes weaver_order_id=SO1 and the JSON
array — all checks pass; bun scripts/svelte-syntax.ts compiles the two
touched .svelte files clean; bunx tsc --noEmit adds no new error and
bun run check stays 0 errors. PLANNING §390. Suite/build/deploy left to
the watcher.
Sizing tippan assigns yarn from booking / in-transit gatepass / direct delivery / free stock (C82, 2026-10-06)
/tippans/new gains one optional field: the yarn the tippan draws on before
it reaches the sizer. +page.server.ts builds yarnOptions from the four
states the owner named — open/partial booking rows, YARN_IN gatepasses in
intimated|due (not picked up), DIRECT intimations in intimated|due (not
received), and lot rows with status='available' — each pre-labelled and
ref'd; closed bookings, picked GPs and allocated lots are excluded. The
wizard gets a new optional list step, "Which yarn to assign?", at the head of
the Yarn-issue block (step 12, shifting gpId onward one), the summary and
review sheets get a matching row, and the hidden form carries the pick as
yarnAssign (source:ref). ?/create resolves the pick against its source
table and writes assignSource/assignRef/assignLabel into the existing
tippan.yarn_issue JSON (no migration); the record Terms and the print
sheet show Yarn assigned. Pareto Q67–Q69/A67–A69. Proved non-UI against a
throwaway libSQL DB (drizzle-kit push --force): a Bun script seeds an open
and a closed booking, an intimated and a picked YARN_IN, a due DIRECT, an
available and an allocated lot, asserts load().yarnOptions offers exactly
the available four with their labels, then drives the real ?/create action
with yarnAssign=stock:… and asserts the row's yarn_issue JSON carries
assignSource/assignRef/assignLabel and the tippan:created event names
the assignment, while an unknown ref returns 400 — 16/16 checks pass.
bun scripts/svelte-syntax.ts compiles the four touched .svelte files
clean; bunx tsc --noEmit adds no error (the only +page.server.ts line is
the pre-existing null-reed one, now at 435). PLANNING §391. Suite/build/
deploy left to the watcher.
Sizing tippan yarn assignment can be partial — bags, loose cones, auto weight (C83, 2026-10-06)
/tippans/new's C82 yarn assignment gains a partial draw. The wizard adds
"Which yarn to assign?" → "How many bags?" → "How many loose cones?" → a
read-only "Draw weight" step (step count 24 → 27), plus the matching summary and
review rows; the hidden form carries assignBags/assignCones/assignNet.
src/lib/yarnPartial.ts is the one shared arithmetic (yarnSourceRates,
partialYarnWeight): weight = (bags × cones-per-bag + loose cones) × per-cone
kg, read from the assigned source (booking perConeWt/bagWt; gatepass & lot
net ÷ cones), with the form's Per-cone kg / packing as fallback. load() now
enriches every yarnOptions candidate with packing/perConeKg; ?/create
captures the resolved source's figures, parses the two quantities and recomputes
the weight as arbiter (a posted assignNet is ignored), storing all three in
the existing tippan.yarn_issue JSON — no migration — and the record Terms +
print sheet show a "Yarn draw" row. Pareto Q70/A70 (the weight is a cone count ×
per-cone rate, not a proportion of the source's net). Proved non-UI against a
scratch copy of the DB (VACUUM INTO, DATABASE_PATH pointed at it): 15/15 — the
pure rates/weight cases, load() offering a booking candidate's packing 20 /
per-cone 0.25, and two real ?/create POSTs, one storing 2 bags + 5 loose =
11.25 kg and one proving a posted 999 is recomputed to 5. bun scripts/svelte-syntax.ts compiles the four touched .svelte files clean;
bunx tsc --noEmit adds no error (the only +page.server.ts line is the
pre-existing null-reed one, now at 521). PLANNING §392. Suite/build/deploy
left to the watcher.
Sizing tippan assign-later uses the yarn-assignment picker and fills the yarn issue (C84, 2026-10-06)
/tippans/new's "Assign later" card no longer asks for a gatepass: it offers
the same C82 yarnOptions list (booking / in-transit gatepass / direct delivery
/ free stock lot) plus C83's partial draw (Bags, Loose cones, a read-only Draw
weight), keeping the weaver-contract select. The wizard's "Which yarn to
assign?" step gains an "Assign later — no yarn yet" row (clears the pick; the
tippan still saves unassigned, no new field). ?/assign resolves the pick
through a new shared resolveYarnAssign() in +page.server.ts (also used by
?/create, so the label can't drift) and merges the source's Count/Mill/
Packing/Bags/Cones/Net into the existing tippan.yarn_issue JSON next to
assignSource/assignRef/assignLabel/assignBags/assignCones/assignNet;
the weight is server-recomputed (a posted assignNet is ignored) and the
tippan:assigned event carries the source label + draw. Null source fields never
wipe an existing value (Pareto Q71/A71). Proved non-UI against a scratch
data.db copy: one real ?/assign POST (gatepass:c84-gp, 2 bags + 5 cones)
set issued and stored count 60s, mill MILL-X, packing 20, bags 10,
cones 40, net 100, draw 112.5 kg — 14/14 checks. tsc adds no error (the
only +page.server.ts line is the pre-existing null-reed one, now at 580).
PLANNING §393. Suite/build/deploy left to the watcher.
rev cost-ledger (2026-10-06): pegging-vs-valuation split live in code
order_cost ledger (migration 0033, applied locally + evt index + WAL; Turso applies at deploy): optional order assignment on booking form (create + attach) and order form (SO/PROD only, never JW) writes the cost row, first fill locks; blank writes nothing. Order record page gains physical-vs-locked reconciliation cards; /reports gains locked-cost margin table + costbasis CSV. Allocation/dispatch/lot paths untouched and import nothing cost-side. Proved on scratch DB copy 16/16; svelte-syntax clean; tsc adds no error. PLANNING §394. Suite/build/deploy left to the watcher.
C85 (2026-10-06): yarn compulsory at create, per-line beam plan (own vs job-work), job-work contract assignable later
/tippans/new: the C82 assign-later path is removed — yarn assignment is required in the wizard and at ?/create. The single #Beams count becomes a per-line plan {kind: own|job, beams, mtr} stored in program JSON; an optional JW/OUTSOURCE picker at create and the repurposed "Assign the job-work contract later" card (?/assign, now contract-only — no BEAM_OUT, never issues) hold the contract. Record + print show the plan. Proved on a scratch DB copy with real sign-in + action POSTs — 13/13 checks; bun run check clean; tsc adds no error. PLANNING §395. Suite/build/deploy left to the watcher.
C111 (2026-10-12): gatepass creation screen drops the kind chip row
/gatepasses/new: removed the overflow-x-auto<nav> of eight kind chips (and the now-dead KIND_ORDER/kindHref helpers) — the eight movements are not types of one document, and the kind is chosen on the list page before the form (?kind=, PLANNING §193). Form body, wizard, hidden POST form and every server action byte-identical; header badge and "Previous {kind} records" unchanged. Proved non-UI: bunx tsc --noEmit adds no error in the touched file; bun run check 0 errors (328 pre-existing warnings). PLANNING §396. Suite/build/deploy left to the watcher.
C112 (2026-10-12): the direct-delivery intimation is booking-driven — booking no + yarn + trader + rate from the booking, bags show booked/received context
/gatepasses/new?kind=DIRECT: the booking picker becomes the first wizard step (wizard bookingPick → sourcePartyId → expectedDate → bags), its label/sub add the rate and name the yarn+trader+rate the booking brings; three read-only Goods rows render once a booking is picked (Booking says: yarn name/count/type/mill/trader/rate · Total bags booked · Already received direct for this yarn, date-wise); the bags row is relabelled "Bags to be received", its sub repeats booked + received-so-far, and previous DIRECT bag counts for the same yarn become suggestion chips; destination wording names factory premises / sizing masters. +page.server.ts load only: openBookings gains rate/yarnType, new read-only directHistory (last 200 DIRECT GPs' count/date/bags). No column, no zod field, no POST name touched. Pareto Q76/A76: rate/type display-only, "already received" matched by yarn count (booking link is event-only). Proved non-UI: bunx tsc --noEmit adds no error in the touched files; bun run check 0 errors (328 pre-existing warnings). PLANNING §397. Suite/build/deploy left to the watcher.
C112 reopen (2026-10-12): DIRECT intimation is exactly its four fields, and the booking gates the wizard
/gatepasses/new?kind=DIRECT: the form now renders only the four fields the owner names — booking (label/sub carry booking no + yarn count/type/mill name + trader + rate), bags to be received, location (new: factory premises + sizing masters, still posting destPlace+targetPartyId), expected date. Removed on DIRECT: the count row, the separate trader step (sourcePartyId), the targetPartyId row, the order/contract row, the GP-date row, the "Prefilled by" row, and §397's three read-only Goods rows (their context moved into the booking pick label and the bags popup sub with the already-received date-wise lines). FocusEntry.svelte gains an optional blockNextIds prop (default empty — no other form changes): the booking step's Next is disabled until a booking is chosen, and no background field is filled first. Server action, zod schema, columns and every POST name byte-identical (countYarn/sourcePartyId/targetPartyId/sourceOrderId fill from the picked booking, never asked). Pareto Q77/A77: the close-out evidence rows (C186/C187 photos + GP counter-copy) and universal Kind/GP-no/Status statics stay — not the four fields, not named, required by the flow. Proved non-UI: bun scripts/svelte-syntax.ts clean on both files; bun run check 0 errors. PLANNING §398. Suite/build/deploy left to the watcher.
C113 (2026-10-12): G0 yarn-in is booking-driven — the yarn booking / job-work number is primary, bags popup carries the booked / received context
/gatepasses/new?kind=YARN_IN: the booking row now uses the C112 identity label/option (booking no · yarn count+type+mill · trader · rate) and is the PRIMARY, compulsory field on a fresh / OCR-opened form — WIZARD_IDS.YARN_IN = ['bookingPick','sourcePartyId','countYarn'], required on the booking and bags rows, and blockNextIds={BLOCK_NEXT} (the prop C112 added to FocusEntry.svelte but never passed here) gates the wizard and every other row until a booking is picked. A carried yarn-in (?bookingId=/?orderId=/?reconId=) keeps its carried flow and is never blocked. The bags row is "Bags to be received" with the C112 popup sub (yarn text, booked total or to be received for a rate-less job-work booking, already-received date-wise, prior-bag suggestions); the destination picker is relabelled "Location of pickup" from the godown list (still posts destPlace+targetPartyId). +page.server.ts: the receipt history is now kind IN ('DIRECT','YARN_IN') (receiptHistory, was DIRECT-only directHistory) so the popup sees the yarn's real receipts. No schema/zod/POST/column/action change; OCR + image storage already exist (GP photo upload + /capture intake). Pareto Q78/A78 + assumed note on the row: keep G0's C201 goods fields, relabel the godown picker, job work = a rate-less booking. Proved non-UI: bunx tsc --noEmit adds no error in the two changed files (repo-wide tsc red, pre-existing); bun run check 0 errors; a read-only data.db query confirms the G0+G0b history. PLANNING §399. Suite/build/deploy left to the watcher.
C114 (2026-10-12): the wizard's last "Done" closes the wizard only — it never auto-saves
FocusEntry.svelte (shared focus-entry shell): the last-step branch of wizNext() dropped if (requiredMissing === 0) onsave?.() and now only sets wizOpen = false — so after the last wizard field the user lands on the main form with its Save button, matching docs/FOCUS-ENTRY.md §18 ("Wizard 'Done' only closes the wizard; the save leg is always the bottom bar's Save"). Affects every form composing the shell (gatepasses/movements/sizing/winding/stocktake/samples/inward/beams). No POST name, zod field, column, server action or use:enhance touched. Proved non-UI: compiled the real component with svelte/compiler and asserted the compiled wizNext last-step branch closes the wizard and never references onsave (passes fixed, fails pre-fix); bun run check 0 errors. PLANNING §400. Suite/build/deploy left to the watcher.
C115 (2026-10-12): the sizing tippan's background form asks only the specified yarn fields
/tippans/new: the yarn-issue block still asked the old gatepass-given fields (Yarn GP · Source · Count · Mill · Packing · Bags · Cones · Net kg) alongside the C82/C85 compulsory yarn assignment. C84 says the assignment fills the whole yarn issue, so those eight superseded steps are removed from TippanWizard.svelte, the sectioned summary and the review sheet — the Yarn issue block is now Yarn assigned · Assigned bags · Loose cones · Draw weight (wizard steps 28 → 20, WIZ_STEP_OF renumbered, the C14 GP chips for the removed fields dropped). +page.server.ts now fills tippan.yarn_issue count/mill/packing/bags/cones/net from the assigned source's figures (with the old typed/gpRow fallbacks for a carried/legacy POST) and derives the record's gatepassId link from a gatepass/direct assignment. No POST name, zod field, column, server action or use:enhance changed; the hidden inputs stay. Pareto Q79/A79 (target read as the tippan entry surface; C116 left to its row). Proved non-UI: bun scripts/svelte-syntax.ts clean on both files; bun run check 0 errors; bunx tsc --noEmit adds no error in the touched files (pre-existing null-reed only). PLANNING §401. Suite/build/deploy left to the watcher.
C113 (reopen) (2026-10-12): G0 yarn-in shows only the owner's fields — the C112 DIRECT reduction applied
src/routes/gatepasses/new/+page.svelte: the yarn-in receipt still showed every C201 G0 row after the booking-primary delivery, so the reopen makes G0 show only what the owner listed, exactly as C112 did for G0b. On kind === 'YARN_IN': the GP-date row and the "Prefilled by" static are dropped (the date still posts, defaulted to today); KIND_CFG.YARN_IN.goods drops to { bags: true } so the count, ticket, mill lot, cones-per-bag, gross and net rows are not asked; the "From party (trader)" and "To party" rows are deleted (the trader comes from the booking via pickBooking → sourcePartyId) and the ONE "Location of pickup" picker now carries the godown list — our godown / premises / the named sizers — posting the existing destPlace + targetPartyId names so a sizer is still pickable without a second row; Endorse to, PGP no and the B6 Mill-lot rows are dropped. Kept: Kind/GP-no/Status, the booking picker (required + BLOCK_NEXT on a fresh/OCR yarn-in), the bags-to-be-received popup, the gatepass photo and the close-out evidence (confirmation photo, signed counter-copy); WIZARD_IDS.YARN_IN = ['bookingPick','bags','destPlace']. The hidden form still mounts every POST name, and receiptHistory still spans G0+G0b — no POST name, zod field, column or server action changed; a carried yarn-in (?bookingId=/?orderId=/?reconId=) is never blocked and still saves. Pareto Q80/A80 (drop GP-date + Prefilled by as C112; merge the sizer into the one location picker). Proved non-UI: bunx tsc --noEmit adds no error in the touched file (repo-wide tsc pre-existing-red) and bun run check 0 errors. PLANNING §402. Suite/build/deploy left to the watcher.
C116 (2026-10-12): the sizing tippan's yarn picker offers no direct-delivery records
/tippans/new: the C82 yarn-assignment picker listed in-transit DIRECT direct-delivery intimations alongside bookings, yarn-in gatepasses and stock lots; C116 drops them. +page.server.ts narrows the available-gatepass query to kind = 'YARN_IN' and every gatepass candidate labels as "Yarn in …" (the direct: id / "Direct delivery" label removed from yarnOptions). The wizard's yarn-step sub and the yarnOptions prop comment drop direct delivery from the source list. resolveYarnAssign's direct branch (and the GP link derived from it) is untouched, so a carried/legacy direct POST still saves. No POST name, zod field, column, server action or use:enhance changed. Pareto Q81/A81. Proved non-UI: bun scripts/svelte-syntax.ts clean on the touched file; bunx tsc --noEmit adds no error in the touched files (pre-existing null-reed only). PLANNING §403. Suite/build/deploy left to the watcher.
C115 (reopen) (2026-10-13): the gatepass background POST form mounts only the kind's visible fields
/gatepasses/new (owner, target corrected from /tippans/new): C112/C113 reduced the visible DIRECT/G0 forms but the hidden transport <form> still mounted all 34 POST names, so it carried more than the surface showed. src/routes/gatepasses/new/+page.svelte now renders the hidden form from a per-kind hiddenNames list: the reduced booking-driven kinds mount only their visible fields + the universal kind/direction/gpDate markers + the close-out evidence — DIRECT = kind, direction, gpDate, expectedDate, bookingId, bags, destPlace, targetPartyId, image, confirmPhoto, counterCopy (11), YARN_IN the same minus expectedDate (10) — plus a carried contract/set-no the surface cannot show; every other kind keeps its full set byte-for-byte. src/routes/gatepasses/new/+page.server.ts?/create fills the booking's own figures from the picked booking (resolved supplier → sourcePartyId, yarn master → countYarn/ticket, lotNoHint → millLotNo, orderId → sourceOrderId); a posted value still wins. Location keeps destPlace+targetPartyId (the sizer IS the location value); vehicle/transport are dropped on the reduced kinds. No zod field, column, action or use:enhance changed. Pareto Q82/A82. Proved non-UI (dev server, signed-in session): SSR of ?kind=DIRECT mounts exactly 11 names and ?kind=YARN_OUT still 34; a reduced-field ?/create POST stored source_party_id/count_yarn=40/ticket=IMP derived from the booking with none posted; svelte-syntax clean, bun run check 0 errors, bunx tsc --noEmit no error in gatepasses/new. PLANNING §404. Suite/build/deploy left to the watcher.
C116 (reopen) (2026-10-13): the gatepass new form drops its previous-records card, and the tippan yarn picker gets direct delivery back
/gatepasses/new (owner, target corrected from /tippans/new): the C58 "Previous {kind} records" card above the form (the owner saw "Previous Direct delivery records" on the direct-delivery kind) is deleted from src/routes/gatepasses/new/+page.svelte along with the recentOfKind derived; its only consumer gone, the load-only recent query + return key are removed from +page.server.ts. In the same turn the earlier mis-targeted C116 change is reverted on /tippans/new: +page.server.ts returns the available-gatepass query to kind IN ('YARN_IN','DIRECT') and the yarnOptions gatepass candidates regain the direct:<id> / "Direct delivery …" option (source direct), and TippanWizard.svelte's yarn-step sub + yarnOptions prop comment list direct-delivery intimations again (C82). No POST name, zod field, column, server action or use:enhance touched on either screen. Pareto Q83/A83. Proved non-UI: svelte-syntax clean on both touched .svelte; bunx tsc --noEmit no error in the touched files (pre-existing null-reed only). PLANNING §405. Suite/build/deploy left to the watcher.
C115 (reopen, second landing) (2026-10-13): the gatepass new form's VISIBLE photo/vehicle section matches the reduced kinds
/gatepasses/new?kind=DIRECT (owner, target gatepasses/new): the prior C115 fixed only the hidden background POST form, so the visible form still showed the whole "Photo & vehicle (you can skip)" section on the reduced kinds (the owner's actual complaint). In src/routes/gatepasses/new/+page.svelte the section is now conditional — ...(!bookingDriven || kind === 'YARN_IN' ? [ … ] : []) — so DIRECT (direct-delivery intimation) renders no photo/vehicle row at all (just Booking/Bags/Location/Expected date + the Kind/GP-no/Status markers), G0 (YARN_IN) keeps only the required-per-C113 Gatepass photo row under the photo-only title "Gatepass photo (you can skip)" (matching BEAM_OUT), and every other kind (WINDING/YARN_OUT/BEAM_OUT/BEAM_IN/BEAM_REPAIR/BEAM_RETURN) keeps its Vehicle no + Transport rows byte-identically. The dropped G0/DIRECT rows also leave BOOKING_HIDDEN: DIRECT = kind, direction, gpDate, expectedDate, bookingId, bags, destPlace, targetPartyId (8 names, no image/confirmPhoto/counterCopy), YARN_IN = the same plus image (no expectedDate, no confirmPhoto/counterCopy); FULL_HIDDEN is untouched. No zod field, column, server action or use:enhance changed; ?/create already reads confirmPhoto/counterCopy from raw FormData and nulls when absent. Pareto Q84/A84 (G0 title + image stays optional at entry). Proved non-UI (dev server, signed-in session): SSR ?kind=DIRECT renders no photo/vehicle label and mounts exactly 8 hidden names; ?kind=YARN_IN renders the single Gatepass photo row and mounts 8 names incl. image; ?kind=BEAM_IN still renders Vehicle no + Transport and mounts all 34; bunx tsc --noEmit adds no error in gatepasses/new. PLANNING §406. Suite/build/deploy left to the watcher.
C117 (2026-10-13): the direct-delivery intimation shares the drop location a no-login confirm link and takes its acknowledgement
/gatepasses/new?kind=DIRECT (owner): the G0b direct-delivery intimation is now shareable to its drop location as the existing no-login /c/gp/<id> link via the reused WhatsAppLink component. src/routes/gatepasses/[id]/+page.svelte points the record's share at the target party's mobile when kind === 'DIRECT' (sizer drop location; bare premises has no party → no link) with a direct-delivery message; every other kind keeps its in/out target. src/routes/gatepasses/+page.svelte adds the same share to the post-save created alert so the creator sends it immediately after save when the new GP is a DIRECT intimation, and src/routes/gatepasses/+page.server.ts adds targetMobile to its row pick (read-only). src/routes/c/[entity]/[id]/+page.server.ts lets the recipient attach a photo/PDF (optional; .txt rejected) + free-text remarks (optional) on /c/gp; confirming writes the file into the screenshots bin (same naming as the owner drop) and appends one gatepass:received-confirmed event carrying remarks + the ack name, and moves a DIRECT intimation intimated|due|partial|picked|delivered → confirmed (out-bound delivered flip and generic inbound no-flip untouched). +page.svelte gains the file + remarks inputs (enctype=multipart/form-data) and treats confirmed as done; the record's load reads the event back and the page renders an Acknowledgement (from drop location) card with remarks + a View acknowledgement link. Pareto Q85/A85. Proved non-UI (dev server, no login): inserted DIRECT GP c117testgp at intimated; POST /c/gp/c117testgp?/confirm multipart (PNG + remarks) → 200, status confirmed, event payload {from:intimated,to:confirmed,by:"external link",remarks:"Gate received in good condition",ack:"2026…-ack.png"}, file on disk; .txt ack → "acknowledgement must be an image or PDF"; re-GET → "Confirmed ✓ — thank you."; bunx tsc --noEmit adds no error in the touched files. PLANNING §407. Suite/build/deploy left to the watcher.
C118 (2026-10-13): a direct delivery shows only Drop pending / Drop completed
/gatepasses/new?kind=DIRECT (owner): a G0b direct-delivery intimation now displays exactly two statuses — Drop pending on creation, Drop completed when the drop location acknowledges (C117) — and no other chain word anywhere. src/lib/labels.ts gains gpStatusWords(kind, status): for kind === 'DIRECT' it collapses intimated|due|partial|picked → Drop pending and delivered|confirmed|reconciled → Drop completed; every other kind returns statusWords unchanged. The stored gatepass.status column, zod, POST names and every existing query are untouched (the live DIRECT rows are intimated, C117 writes confirmed), so this is display-only and reversible. gatepasses/new/+page.svelte status static now reads Drop pending → Drop completed (on the drop location's acknowledgement); gatepasses/[id]/+page.svelte maps the header badge + Terms row, swaps the seven-step chain for a two-badge strip and sets next = null (no manual chain tap), and the automatic-inward card keys off direct completion; gatepasses/+page.svelte renders the mapped word in the status column and dialog (statusLabel); inward/+page.server.ts excludes DIRECT from its intimated-chain queue; lib/server/board.ts and lib/server/queue.ts map the word in the gate table and home intimations board. Pareto: display-only mapping (no new DB values, no migration) so legacy rows collapse correctly and nothing else moves; /inward drops DIRECT because its two-state flow is completed by the acknowledgement (C117) or the guard (C119), not the G4 chain. Proved non-UI: bun /tmp/c118-proof.ts — 10 cases pass, DIRECT's eight chain values collapse to exactly {Drop pending, Drop completed} while YARN_IN/BEAM_OUT are unchanged; bun run check exits 0; bunx tsc --noEmit adds no error in the touched files. PLANNING §408. Suite/build/deploy left to the watcher.
C119 (2026-10-13): a factory-premises direct delivery is received at the guard
/guard (owner): a G0b direct-delivery intimation whose drop location is our own factory premises now appears in the guard login as to be received and, on receipt, creates the yarn inward and moves to Drop completed. src/routes/guard/+page.server.tsload adds inbound — inbound DIRECT rows with targetPartyId IS NULL in a pending state (a named-sizer drop keeps its target party and is confirmed by the C117 external link, so it never shows here); a new receive action (no new screen/route) requires vehicle no + driver photo (photo saved into the screenshots bin on the same naming contract as the C117 ack), reuses the C117 DIRECT received-confirmed move (intimated|due|partial|picked -> confirmed = C118 Drop completed), appends gatepass:received-confirmed with by:'guard' + vehicle + driverPhoto, and calls the existing createLotsFromGatepass hook so the lot (barcode = GP no) is created. Forward-only: a second receipt rejects; a non-premises/forged id rejects. src/routes/guard/+page.svelte renders the To be received — at our premises section (daisyUI card + use:enhance, vehicle input + driver-photo file picker) and broadens the empty state to Nothing at the gate right now when both queues are empty. No schema, zod field, column, POST name or other action touched. Pareto Q87/A87 (no existing guard receipt action — exit is outbound, beamConfirm is the beam re-tap — so reuse the existing screen + the C117 move + the lots hook). Proved non-UI: /tmp/c119-proof.ts exercises the real load + receive on a temp DB copy — 8/8 (lists as to-be-received; missing vehicle/no-photo each 400; receive -> confirmed, vehicle+photo stored, lot auto-created, event by guard; re-receive 400). svelte-syntax clean; bunx tsc --noEmit adds no error in the touched files. PLANNING §409. Suite/build/deploy left to the watcher.
C120 (2026-10-13): direct-delivery yarn identity — auto from a yarn booking, user-picked for job work
/gatepasses/new?kind=DIRECT (owner): the G0b intimation now shows the yarn identity in the two shapes the owner named. When a rated booking is picked the Goods section renders an AUTO read-only static row Yarn (count + type + mill name) (count + typeCode · mill ticket, derived via bookingYarnIdentity); when a rate-less open booking (job work — existing isJobBooking, and the live BK-MLP-1 has yarn_id null) is picked, Yarn count (countYarn), Yarn type (yarnTypeCode, the yarn-master Type vocabulary KW/CW/CCW/KCW/Dn/Ly/Sl/Other) and Mill name (ticket) render required and join the wizard between booking and bags — for a yarn booking they do not render, so its four-step wizard is unchanged (WIZARD drops ids with no row). gatepasses/new/+page.server.ts load adds the yarn master's typeCode to openBookings (display/prefill only); the create action reads raw yarnTypeCode and rides it on the creation event (no column, no zod field, exactly the beamType/confirmPhoto pattern). Only countYarn/ticket post their existing names; yarnTypeCode is appended to BOOKING_HIDDEN.DIRECT. Nothing else moves — no schema, zod, column or other POST name. Pareto Q88/A88: job work = the existing rate-less open booking, not a new job-work-order picker (C130 owns that). Proved non-UI: bunx tsc --noEmit adds no error in the two touched files (147 pre-existing repo-wide); svelte-syntax clean on the touched .svelte; read-only data.db query confirms both branches (22 rated bookings, 1 rate-less yarn_id-null job work, type vocab CCW/CW/Dn/KCW/KW/Ly/Sl). PLANNING §410. Suite/build/deploy left to the watcher.
C121 (2026-10-13): the direct-delivery form's number row is renamed and moved to the top
/gatepasses/new?kind=DIRECT (owner): the G0b intimation's read-only number row now reads Direct delivery no ("Auto on save — the next direct delivery number") and sits at the TOP of the form, immediately after the Kind row. src/routes/gatepasses/new/+page.svelte moves the gpNoStatic row into the first section right after kindStatic under a kind === 'DIRECT' guard, and guards the original Goods-section row with kind !== 'DIRECT' so the other seven kinds keep their existing GP no row in place (only one ever renders; no duplicate id at runtime). Display-only: no POST name, schema, column or server action moves. Pareto: correction names DIRECT, so only DIRECT's presentation changes — other kinds untouched, one file, reversible. Proved non-UI: bunx tsc --noEmit adds no error in the touched file; signed-in SSR fetch of the real dev pages (/tmp/opencode/c121-proof.ts) — DIRECT renders Direct delivery no with GP no absent and Kind before it, YARN_OUT still renders GP no and no Direct delivery no. PLANNING §411. Suite/build/deploy left to the watcher.
C122 (2026-10-13): the direct-delivery status is a colour-coded tag at the top of the form
/gatepasses/new?kind=DIRECT (owner): the G0b intimation's Status now rides the TOP of the form (right after Kind) as a colour-coded daisyUI badge, not a field-like row. src/lib/labels.ts gains gpStatusBadge(kind, status) (badge-warning pending / badge-success completed / badge-ghost otherwise; null for every other kind); src/lib/components/focus/FocusEntry.svelte adds an optional badge on FocusField and renders a static value as <span class="badge {r.badge}"> when set (both the plain and the staticNote value slots); src/routes/gatepasses/new/+page.svelte adds a DIRECT-only top statusStatic row (gpStatusWords(kind,'intimated') = Drop pending, gpStatusBadge = badge-warning, no staticNote) and guards the foot row with kind !== 'DIRECT' so the other seven kinds keep their existing plain Status line and position. Display-only: no POST name, schema, column or server action moves. Pareto Q90/A90: ordering is Kind → Direct delivery no → Status (both C121/C122 name the top; keeps C121's accepted placement) and the badge stays a non-tappable tag (no note) so it is not a field row; only DIRECT changes, one .svelte + shared component/helper, reversible. Proved non-UI: bun -e asserts the four DIRECT word/badge pairs and the non-DIRECT null; bunx tsc --noEmit adds no error in the touched files. PLANNING §412. Suite/build/deploy left to the watcher.
C131 (2026-10-13): the party master drops its broker-name field
/masters?tab=party (owner): the Broker name field is retired from the party form. src/routes/masters/PartyForm.svelte removes the label + input; src/routes/masters/+page.svelte drops ['Broker name', 'brokerName'] from MASTER_FIELDS.party so the record dialog no longer shows the removed field; src/lib/server/masters.tsbuildPartyValues stops reading brokerName (create → column default null, update → key absent so a legacy value is never nulled on an unrelated edit); static/masters-fields.html drops the row and its count (19 → 18). No schema/column/migration: party.broker_name and stored values stay, fully reversible. Pareto Q91/A91 (form only, keep data). Proved non-UI: bunx tsc --noEmit adds no error in the touched files; runtime bun /tmp/opencode/c131-proof.ts confirms the builder no longer emits brokerName. PLANNING §413. Suite/build/deploy left to the watcher.
C132 (2026-10-13): the party master drops its opening-balance field
/masters?tab=party (owner): the Opening balance ₹ field is retired from the party form. src/routes/masters/PartyForm.svelte removes the label + input and its client numeric check; src/routes/masters/+page.svelte drops ['Opening balance ₹', 'openingBalance'] from MASTER_FIELDS.party so the record dialog no longer shows it; src/lib/server/masters.tsbuildPartyValues stops reading openingBalance (create → column default 0, update → key absent so a legacy value is never rewritten on an unrelated edit); src/routes/masters/+page.server.ts drops the now-dead server validation; static/masters-fields.html drops the row and its count (18 → 17). No schema/column/migration: party.opening_balance and stored values stay, fully reversible. Pareto Q92/A92 (form only, keep data). Proved non-UI: bunx tsc --noEmit adds no error in the touched files; runtime bun /tmp/opencode/c132-proof.ts confirms the builder no longer emits openingBalance. PLANNING §414. Suite/build/deploy left to the watcher.
C133 (2026-10-13): the party master drops its due-days field
/masters?tab=party (owner): the Due days (credit) field is retired from the party form. src/routes/masters/PartyForm.svelte removes the label + input and its client digit check; src/routes/masters/+page.svelte drops ['Due days', 'dueDays'] from MASTER_FIELDS.party (record dialog) and the dueDays column from the party-list SmartTable (both display surfaces); src/lib/server/masters.tsbuildPartyValues stops reading dueDays (create → column default 0, update → key absent so a legacy value is never rewritten on an unrelated edit); src/routes/masters/+page.server.ts drops the now-dead server validation and its only-remaining helper maybeInt0; static/masters-fields.html drops the row and its count (17 → 16). No schema/column/migration: party.due_days and stored values stay, fully reversible. Pareto Q93/A93 (form + display, keep data). Proved non-UI: bunx tsc --noEmit adds no error in the touched files; runtime bun /tmp/opencode/c133-proof.ts confirms the builder no longer emits dueDays. PLANNING §415. Suite/build/deploy left to the watcher.
C134 (2026-10-13): the party form's mobile/email rules are relevant at the keyboard
/masters?tab=party (owner): the mobile and email validation rules already existed — client (PartyForm.svelte) and server (+page.server.tspartyErrors) both enforce mobile /^[6-9]\d{9}$/ (+91-aware) and /^[^\s@]+@[^\s@]+\.[^\s@]+$/ (PLANNING §141) — so this row makes them relevant at the keyboard instead of re-adding them. src/routes/masters/PartyForm.svelte: the mobile input gains an oninput that strips every character outside its own pattern ([0-9+\- ]) as it is typed, and both the mobile and email inputs clear their stale submit-time error on edit; every existing regex, message, POST name, schema, zod field and server rule is byte-identical. Pareto Q94/A94 (rules exist → keep them, add only input relevance). Proved non-UI: real signed-in ?/createParty POSTs (/tmp/opencode/c134-proof.ts) reject a bad email and a bad/alpha mobile, accept +91 98765 43210, and create no record (ALL PASS); bunx tsc --noEmit adds no error on the touched file; svelte-syntax compiles it clean; a headless 390×844 run of the form confirms the strip (98a76b54c3210 → 9876543210) and the error-clear. PLANNING §416. Suite/build/deploy left to the watcher.
Pipeline (2026-10-13): a row is committed, not closed, until the live build serves it
Owner: rows were reading done the moment they were committed, even though the live build lagged (C131/C132/C133 were done while only C131 was live). New committed status between "worker done" and "live". scripts/queue-next.ts gains committed (a terminal-for-work state: never runnable, never precheck, never stranded, skipped by owner-needs/stuck/reconcile/stale) and --promote-committed (flips every committed row to done — the only close path). scripts/deploy.sh, on a full deploy only (after the live-wholeness check, before the commit), runs --promote-committed; --commit-only never promotes. scripts/row-watch.sh marks the finished row committed on a commit-only deploy and done only when .deployed-head == HEAD (a full build just landed). src/lib/server/queueBoard.ts + src/routes/queue/+page.server.ts + src/routes/queue/+page.svelte give committed its own board column/badge ("Committed — not live"), and it is NOT hidden by the closed toggle (boardRows.ts hides only done). scripts/line-status.sh reports the count. Proved non-UI: bun scripts/queue-next.ts --promote-committed flips a fixture committed->done; bun run check clean (0 errors) on the touched files; the board mapping is a pure switch. PLANNING §417. Suite/build/deploy left to the ship gate.
C135 (2026-10-13): the party master adds "Godown" to its Category list
/masters?tab=party (owner): Godown joins the Category vocabulary. src/routes/masters/PartyForm.svelte appends 'Godown' to the client CATS guard and the option {#each}; src/routes/masters/+page.server.ts appends it to PARTY_CATEGORIES and its error message. The value set is mirrored in static/masters-fields.html, docs/VALUE-SETS.md (Misc → party.category) and the column comment in src/lib/server/schema-foundation.ts. No schema/column/migration/POST name moves — party.category is free-text defaulting to Party, so existing rows are untouched; additive and reversible. Downstream pickers unchanged (the gatepasses trader list still filters only Sizing/Weaver/Transport). Pareto Q95/A95 (category label only; no downstream filter changes). Proved non-UI: signed-in dev-server probe (/tmp/opencode/c135-proof.ts, ALL PASS) — /masters?tab=party renders <option value="Godown">, ?/createParty with category=Godown returns only the blank-name failure, category=Bogus returns the message listing Godown; bunx tsc --noEmit stays at 147 pre-existing errors (none added in touched files). PLANNING §418. Suite/build/deploy left to the watcher.
C136 (2026-10-13): the party mobile gets a country-code picker
/masters?tab=party (owner): the Mobile field now carries a country-code <select> joined to it, defaulting to India (+91). New shared module src/lib/countryCodes.ts (COUNTRY_CODES, DEFAULT_DIAL, isKnownDial, splitStoredMobile) is imported by both src/routes/masters/PartyForm.svelte (the select + the client mirror) and src/routes/masters/+page.server.ts (partyErrors). Storage is kept compatible with every consumer (orderLogic.ts/quoteLogic.ts/leads/gatepasses strip non-digits and only prefix 91 to a bare 10-digit): +91 keeps storing bare 10 digits & the legacy 6-9 rule byte-identical; any other known code stores <dial><local> and validates a 4-15 digit local number. Edit splits the stored value back (splitStoredMobile, longest dial first, ≤10 digits = India). No schema/column/migration/POST-name moves — party.mobile unchanged, additive and reversible. Pareto Q96/A96 (persist the code; keep +91 storage identical; short shared whitelist). Proved non-UI: bun -e round-trips splitStoredMobile on Indian/+91/international inputs (all correct); bun run check shows no issue in the touched files (bunx tsc --noEmit adds no error there either). PLANNING §419. Suite/build/deploy left to the watcher.
C137 (2026-10-13): the party master drops its Phone field
/masters?tab=party (owner): the separate free-text Phone input is gone; Mobile (with its §419 country-code picker) stays. src/routes/masters/PartyForm.svelte removes the visible Phone label/input, the PHONE_RE client mirror and the phone error line; src/routes/masters/+page.server.ts removes the matching partyErrors check and PHONE_RE const. party.phone remains a column (buildPartyValues still reads it) — no schema/column/migration/POST-name moves. To avoid the update action (masterAction sets every field) silently nulling legacy phones, a hidden <input name="phone" value={value?.phone ?? ''}> round-trips the existing value on edit; the read-only Phone row in MASTER_FIELDS stays so legacy data is visible. Pareto Q97/A97 (drop the editable field + validation only; keep the data and the detail row; hidden round-trip). Proved non-UI: signed-in dev-server probe /tmp/opencode/c137-proof.ts (ALL PASS) — no visible phone input renders but a hidden round-trip one does, and a bogus phone=not-a-phone create POST now returns only party name required with no phone-format failure; bunx tsc --noEmit adds no error in the touched files (pre-existing repo errors unchanged). PLANNING §420. Suite/build/deploy left to the watcher.
C138 (2026-10-13): the party master adds an MSME (Udyam Aadhar) number
/masters?tab=party (owner): the party record gains an optional MSME (Udyam Aadhar) no. field next to PAN/GSTIN. src/lib/server/schema-foundation.ts adds a nullable msme text column to party; src/lib/server/masters.tsbuildPartyValues reads it (str(fd,'msme') || null, blank → null) so create and update share one builder; src/routes/masters/PartyForm.svelte adds the input after GSTIN; src/routes/masters/+page.svelte adds the ['MSME (Udyam Aadhar)','msme'] row to MASTER_FIELDS.party; static/masters-fields.html lists it and moves the count 16 → 17; migration drizzle/0034_young_martin_li.sql (ALTER TABLE party ADD msme text;). Additive and reversible (nullable column, no data rewritten, no POST-name moves, no list column). Deliberately no format validation — Udyam numbers mix letters/digits/dashes and vary, so a strict regex would reject valid entries; a rule can be its own row like C140. Pareto Q98/A98 (free text, form + detail only, smallest change). Proved non-UI: bun /tmp/opencode/c138-proof.ts (PASS) — the builder carries msme through and stores null when blank; bunx tsc --noEmit adds no error in the touched files (normalised diff vs baseline is empty). PLANNING §421. Suite/build/deploy left to the watcher.
C139 (2026-10-13): the party master drops its old "Own unit" toggle
/masters?tab=party (owner): the legacy Own unit checkbox is gone (Category now carries OwnUnit/Godown, §418). src/routes/masters/PartyForm.svelte removes the visible toggle and replaces it with a hidden round-trip <input name="isOwnUnit" value={value?.isOwnUnit === true ? 'on' : 'off'}> so the update action (masterAction sets every field) never silently flips a legacy own unit off — and the gatepasses/new trader filter (!p.isOwnUnit) keeps working. chk() already reads 'on', so create still stores false. The Own unit list column and MASTER_FIELDS.party detail row stay. No schema/column/migration/POST-name moves — party.is_own_unit and stored values are untouched, fully reversible. Pareto Q99/A99 (retire the form toggle only; keep data/column/detail; hidden round-trip). Proved non-UI: signed-in dev-server probe /tmp/opencode/c139-proof.ts (ALL PASS) — the party tab renders no visible own-unit checkbox and no Own unit toggle label but a hidden round-trip input, an own-unit ?/createParty row saves is_own_unit=1, and editing another field leaves it 1; bunx tsc --noEmit adds no error in the touched file (pre-existing repo errors unchanged). PLANNING §422. Suite/build/deploy left to the watcher.
C140 (2026-10-13): party PAN + GSTIN normalise and validate at the keyboard
/masters?tab=party (owner): PAN and GSTIN already validate on both sides from §141 (client PartyForm.svelte + server partyErrors: PAN 5 letters+4 digits+1 letter, GSTIN 15-char with Z at position 11), so C140 makes those existing rules relevant at the keyboard (mirror of C134/§416). New pure module src/lib/partyIds.ts exports the two shared regexes, normalizePan (uppercase, strip non-alphanumerics, cap at 10) and normalizeGstin (same, cap at 15); src/routes/masters/PartyForm.svelte binds PAN/GSTIN to reactive panVal/gstinVal, normalises on input and clears the stale submit-time error on edit. Every existing rule, message, POST name and server check is byte-identical; no schema/migration/server change — one new module + one .svelte, additive and reversible. Pareto Q100/A100 (rules predate the correction; keep them, add input relevance only). Proved non-UI: bun /tmp/opencode/c140-proof.ts (ALL PASS) runs the exact normalisers the form uses (lowercase/dashes/spaces/junk → canonical uppercase alphanumeric, cap 10/15, normalised values pass the regexes, malformed still fail); bun scripts/svelte-syntax.ts src/routes/masters/PartyForm.svelte compiles clean; bunx tsc --noEmit adds no error in the touched files. PLANNING §423. Suite/build/deploy left to the watcher.
C141 (2026-10-13): the party edit form carries its correction remark; the standalone Amend is gone
/masters?tab=party (owner): the record dialog's three asks — remove the Amend button after edit, Cancel and Save side by side, correction remark field alongside the other fields — are settled by folding the correction into the edit form for the party tab. src/routes/masters/PartyForm.svelte gains an optional onCancel prop, an optional Correction remark field (name="note", shown in edit mode in the field grid) and a Cancel button beside Save changes in its action row. src/routes/masters/+page.svelte renders the party edit form with onCancel, hides the whole footer while editing on the party tab, and no longer renders the standalone ?/amend form there (the other master tabs keep their existing Amend control + toggle byte-identical — the target names ?tab=party). src/routes/masters/+page.server.tsmasterAction reads the note and, when present, writes appendAmendment with the genuine field diff as fields and the remark as reason (same append-only audit the old Amend wrote; an edit with no remark writes none). The remark carries its validation on both surfaces — maxlength="200" on the input and a masterAction length check that fails before any write. No schema/column/migration/POST-name moves — a new optional form field + a dialog branch + one masterAction branch, additive and reversible. Pareto Q101/A101 (drop the standalone Amend on party only; keep existing behaviour on every other tab; leave the amend action itself in place). Proved non-UI: signed-in dev-server probe /tmp/opencode/c141-proof.ts (ALL PASS) — a plain edit writes no amendment; an edit carrying a remark writes exactly one amendment whose reason is the remark and whose before/after carry the changed field ({"category":"Party"} → {"category":"Broker"}); the party record dialog renders Edit fields but no action="?/amend" and no standalone correction-note input; bun scripts/svelte-syntax.ts compiles both edited .svelte files clean; bunx tsc --noEmit adds no error in the touched files. PLANNING §424. Suite/build/deploy left to the watcher.
C142 (2026-10-13): the party Acronym field drops its Suggest button
/masters?tab=party (owner): the Suggest button beside the acronym input is gone. src/routes/masters/PartyForm.svelte renders the acronym as a plain input like its sibling fields — the button, its title="Suggest from the party name", and the join/join-item wrappers are removed. The name="acronym" POST field, the field itself and the auto-suggest-from-name $effect (fills the acronym from the party name until the user types, guarded by acronymTouched) are byte-identical — the correction names the button only. No POST name, schema, column, migration or server action moves; markup only, reversible. Pareto Q102/A102 (drop the visible button, keep the typing-time prefill). Proved non-UI: the Svelte compiler's generate:'server' output for PartyForm.svelte (/tmp/opencode/c142-proof.ts, ALL PASS) still emits name="acronym" + defaultAcronym and the untouched name="note"/name="countryCode"/name="mobile" fields, with no Suggest label and no Suggest from the party name title; bunx tsc --noEmit adds no error in the touched file. PLANNING §425. Suite/build/deploy left to the watcher.
C143 (2026-10-13): the design master drops its whole "Rates & rolls" section
/masters?tab=design (owner; the row's ?tab=payment-terms target is a batch mislabel — the wording names a design-master section and C144–C150 all edit the design master, Pareto Q103/A103): the Rates & rolls block is gone. src/routes/masters/DesignForm.svelte removes the heading plus Warp rate, Weft rate, Sizing rate, Job rate (ps) (with nested Warping ₹/mtr, GLM, GST %, Warp shrinkage %), Mtr per roll and Rolls per lot, and drops their client submit-time rules in the same file. src/routes/masters/+page.svelte drops the same six columns (warpRate,weftRate,sizRate,jobRatePaise,mtrPerRoll,rollsPerLot) from MASTER_FIELDS.design. Construction, yarn counts and weights & costing (Lasa/L2L/size add-on/weft waste/GSM target/UTL/HSN/Mode) are byte-identical; GSM/UTL stay (C149). No schema/column/migration/POST-name moves; the server builder buildDesignValues and designErrors are left untouched so the quote screens' and orders' fallbacks keep working (recorded assumption: editing now re-applies the builder defaults 100/6 for mtr-per-roll/rolls-per-lot). Proved non-UI: the Svelte compiler's generate:'server' output for both edited .svelte files (/tmp/opencode/c143-proof.ts, ALL PASS) shows no Rates & rolls heading and none of the ten removed input names in the form, every untouched design input still emits, and the record field list keeps gsmTarget/modePref but no longer carries warpRate/mtrPerRoll/rollsPerLot; bunx tsc --noEmit adds no error in the touched files. PLANNING §426. Suite/build/deploy left to the watcher.
C144 (2026-10-13): the design master prefills Weft waste % at 5 on a new record
/masters?tab=design (owner; the row's ?tab=payment-terms target is the same batch mislabel as C143, Pareto Q104/A104): the design form's Weft waste % input now prefills 5 on a new record. src/routes/masters/DesignForm.svelte's one field changes from value={value?.weftWastePct ?? ''} to value={value?.weftWastePct ?? '5'}, mirroring its sibling Size add-on % (?? '10'); the label already read Weft waste % (5) and the schema default is design.weftWastePct = 5. One file, markup only — no schema/column/migration/POST-name moves; the server, designErrors and the 0–100 client mirror are byte-identical. /quote/new (prefills 5 via EFF_DEF) and /quote/edit (builder defaults 5) are deliberately untouched. Proved non-UI: the Svelte compiler's generate:'server' output for DesignForm.svelte (/tmp/opencode/c144-proof.ts, ALL PASS) binds $.attr('value', value?.weftWastePct ?? '5'), keeps the sibling ?? '10' and the field's validation; bunx tsc --noEmit adds no error in the touched file. PLANNING §427. Suite/build/deploy left to the watcher.
C145 (2026-10-13): the design master edits warp/weft counts in a grid, not JSON
/masters?tab=design (owner; ?tab=payment-terms is the C143–C150 batch mislabel, Q103–Q105): new src/routes/masters/DesignRowsGrid.svelte renders one row per warp/weft count with columns Sr No. · Count · Unit · % · Permissible counts (free comma-separated counts shown as removable badges; add/remove rows) and posts the same warpRowsJson/weftRowsJson hidden names. DesignForm.svelte drops both raw-JSON <textarea>s for the grid and mirrors the permissible rule client-side; src/lib/server/masters.tsparseRowsJson reads permissible:[…] (object key or array index 6, non-positives dropped) and +page.server.tscheckRowsJson rejects a non-list or any count ≤ 0, with the row cast corrected to Record<string, unknown> | unknown[] (six pre-existing never errors cleared). Proved non-UI: real parseRowsJson/buildDesignValues round-trip 7/7 (/tmp/opencode/c145-proof.ts) and Svelte server-generate 13/13 (/tmp/opencode/c145-svelte-proof.ts); bunx tsc --noEmit adds no error in the touched files. PLANNING §428. Suite/build/deploy left to the watcher.
C146 (2026-10-13): the design master's warp/weft rows lose their raw-JSON field wording
/masters?tab=design (owner; ?tab=payment-terms is the C143–C150 batch mislabel, Q103–Q106): C146's named fields — the "Warp rows — multi-warp (JSON, optional)" / "Weft rows — multi-weft (JSON, optional)" raw-JSON <textarea>s — were already removed by C145, which replaced them with the warp/weft grid (Q105/A105). This row completes the removal's remaining surface, reversibly: src/routes/masters/DesignForm.svelte relabels the two grids from "Warp rows — multi-warp"/"Weft rows — multi-weft" to plain Warp/Weft, and static/masters-fields.html drops the JSON claim (the trigger sentence and the Warp rows / Weft rows (JSON) field row now describe the grid). No schema/column/migration, no POST-name move, no server-validation change, no stored value touched — warp_rows/weft_rows and the warpRowsJson/weftRowsJson hidden names are byte-identical. Pareto Q106/A106 (remove the JSON presentation, keep C145's grid; deleting the grid would undo an owner-requested field). Proved non-UI: Svelte generate:'server' compile of the real form 11/11 (/tmp/opencode/c146-proof.ts); bunx tsc --noEmit adds no error in the touched files. PLANNING §429. Suite/build/deploy left to the watcher.
/masters?tab=design (owner; ?tab=payment-terms is the C143–C150 batch mislabel, Q103–Q107): the design master's quality name is now DERIVED and sits with the Alias at the top of the form. New pure module src/lib/designQuality.ts exports composeQualityName(fields) building the stored convention width``reed/denting*pick/warp*weft-weave (missing parts omitted; denting defaults 2 when reed present) and is shared by both surfaces. src/lib/server/masters.tsbuildDesignValues composes qualityName from the construction fields and ignores the posted quality_name (never hand-typed); src/routes/masters/+page.server.ts drops the "quality name required" rule and dup-checks the derived value; src/routes/masters/DesignForm.svelte shows the name as a read-only preview that recomputes on every input beside the still-manual **Alias** at the top, and the old mid-form Alias field plus the now-circular denting hint are removed; static/masters-fields.html marks the name derived. No schema/column/migration/POST-name moves — one new module + the derivation; legacy names normalise to the canonical string on their next (append-only, audited) edit and a design's id remains its order/quote identity. Pareto Q107/A107 (derive rather than park). Proved non-UI: bun /tmp/opencode/c147-proof.ts 9/9 (canonical/no-width/no-weave/blank-denting, builder ignoring a hand-typed name, derived name still parsing denting, client preview === server-stored); bun scripts/svelte-syntax.ts src/routes/masters/DesignForm.svelte clean; bunx tsc --noEmit adds no error in the touched files. PLANNING §430. Suite/build/deploy left to the watcher.
C148 (2026-10-13): the design master drops the Pick (PPI) field; the quality name writes the literal "Pick"
/masters?tab=design (owner; ?tab=payment-terms is the C143–C150 batch mislabel, Q103–Q108): C148 removes the design master's visible Pick (PPI) input and replaces the pick number in the derived quality name with the literal word Pick. src/lib/designQuality.ts composes width``reed/denting*Pick/warp*weft-weave (pick slot is the constant "Pick"; QualityNameFields.pick and both call-site passes are gone); src/routes/masters/DesignForm.svelte drops the input and its client rule, keeps **Reed** as a standalone field, and adds a hidden pick carry on edit; +page.server.tsdesignErrors drops the pick requirement; +page.svelte drops the Pick row from MASTER_FIELDS.design; static/masters-fields.html updates the format and drops the Pick row. The pick column still feeds the costing engine (computeDesignWeights weft weight → stored weftKgPerM read by /orders/new, /procure, /weft-issue, hishob print), so Pareto Q108/A108 carries the stored pick silently through the hidden input on edit — no committed value overwritten, no weight zeroed; a new record legitimately has no PPI. No schema/column/migration; reversible. Consequence recorded: designs differing only by pick now share a name (dup-guard may 409) — alias differentiates, id stays the identity. Proved non-UI: /tmp/opencode/c148-proof.ts 19/19 (literal Pick composition, posted pick ignored, builder derives with no pick, hidden carry preserves pick=70 and a non-zero weft weight, denting still parses, Svelte server output has no Pick (PPI)/visible pick input but keeps the hidden carry); bun scripts/svelte-syntax.ts src/routes/masters/DesignForm.svelte clean; bunx tsc --noEmit adds no error in the touched files; bun run check 0 errors. PLANNING §431. Suite/build/deploy left to the watcher.
C149 (2026-10-13): the design master drops the GSM target and UTL fields
/masters?tab=design (owner; ?tab=payment-terms is the C143–C150 batch mislabel, Q103–Q109): the design master no longer asks for GSM target or UTL. src/routes/masters/DesignForm.svelte removes both inputs and their client optRate(…) rules (and the now-unused helper); src/routes/masters/+page.server.tsdesignErrors drops the two maybeRate(fd, …) checks; src/routes/masters/+page.svelte drops the ['GSM target','gsmTarget'], ['UTL','utl'] row from MASTER_FIELDS.design; static/masters-fields.html drops both from its table (row now reads just HSN). Pareto Q109/A109 — the row does not settle whether the storedgsm_target/utl should survive an edit, and /checking/report reads design.gsm_target; the safe reading removes the two keys from buildDesignValues, so an edit's .set({…}) omits those columns and drizzle leaves the committed values untouched (drizzle skips absent keys — a null would have wiped them), while a new design has none. No hidden carry, no schema/column/migration/POST-name move; reversible. Consequence: new designs show — in the checking report's GSM-target column; historical rows unchanged. Proved non-UI: bun /tmp/opencode/c149-proof.ts 9/9 — the Svelte server-generate output of the real form has no gsmTarget/utl input and no GSM target / UTL label while keeping HSN, and buildDesignValues returns no gsmTarget/utl keys even when both are posted (HSN + construction still build); bun scripts/svelte-syntax.ts compiles both edited forms clean; bunx tsc --noEmit adds no error in the touched files. PLANNING §432. Suite/build/deploy left to the watcher.
C150 (2026-10-13): the Denting label drops the EPI formula hint
/masters?tab=design (owner; ?tab=payment-terms is the C143–C150 batch mislabel, Q103/A103): the correction shows the same string on both sides (a paste typo), so Pareto Q110/A110 reads "instead of X write only Y" as removing the trailing EPI formula. src/routes/masters/DesignForm.svelte shortens the Denting field's one <span> label from Denting (ends per dent) — EPI = reed × denting ÷ 2 to Denting (ends per dent); the input name denting, its 2 placeholder/default, the client optPos('denting') rule, the server designErrors rule and the stored denting column are byte-identical (the EPI relationship still lives in docs/COSTING.md). No schema/column/migration, one label text, reversible. static/masters-fields.html has no Denting row so it is untouched. Proved non-UI: bun /tmp/opencode/c150-proof.ts 8/8 — the Svelte server-generate output of the real form keeps the plain label + denting input + 2 placeholder + client rule, while EPI = reed / denting ÷ 2 are gone from both output and source. PLANNING §433. Suite/build/deploy left to the watcher.
C152 (2026-10-13): the design master's warp/weft grid draws counts from the yarn master, shares 100% automatically, and picks permissible counts as tags
/masters?tab=design (owner; /tippans/new on the sheet and ?tab=party on the queue are the C143–C152 batch mislabel, Q111/A111, same as C145–C151): the warp/weft count becomes a dropdown fed by the yarn master — src/routes/masters/+page.svelte derives yarnOpts from data.yarns and passes the new yarns prop through DesignForm to both DesignRowsGrid grids; picking a count also sets the row's unit, and a stored count not in the master stays a one-off option so a legacy row is never blanked. The % field is widened/centred, a live % total badge shows the sum, and add/remove re-shares the side so it totals 100 (distribute: 100/n at 2 dp with the remainder on the last row — 2 rows → 50/50). Permissible counts are multi-selected from the yarn master as removable tags (permOptions + a per-row checkbox dropdown), stored under the same permissible: number[] JSON key. The 100% rule is enforced on both surfaces — client DesignForm.submit and server rowsPctTotal + designErrors reject a side off 100 by >0.5 (Pareto Q112/A112: delete re-shares too); a legacy wrong-sum design prompts a fix on its next edit without its stored values being rewritten. No schema/column/migration, no POST-name move, static/masters-fields.html's grid row now names the dropdown/auto-share/tags. Proved non-UI: /tmp/opencode/c152-proof.ts 19/19 — Svelte server-generate output carries countOptions/permOptions/distribute/togglePerm/% total, the old free-text count input and e.g. 60, 61 permissible box are gone, buildDesignValues round-trips two warp rows at 60/40 + permissible with a non-zero warpKgPerM, parseRowsJson still reads index 6, and the yarns prop is wired to both grids/dialogs; bun scripts/svelte-syntax.ts compiles the three .svelte files clean; bunx tsc --noEmit adds no error in the touched files. PLANNING §434. Suite/build/deploy left to the watcher.
C153 (2026-10-13): the design master's L2L label drops its legacy (104) code
/masters?tab=design (owner; /tippans/new on the sheet and ?tab=party on the queue are the C143–C153 batch mislabel, Q103–Q111/Q113): the only L2L (104) label in the tree is on the design master form, so src/routes/masters/DesignForm.svelte shortens its L2L field <span> from L2L (104) to L2L. Pareto Q113/A113 keeps the sibling legacy tags (Lasa (110), Size add-on % (10), Weft waste % (5)) as they are — the row names one field and one code. The name="l2l" input, its inputmode/pattern, the client optPos('l2l') rule, the server designErrorsl2l rule and the stored l2l column are byte-identical (the 104.25 default stays in schema/yarnMath/COSTING). No field added, no schema/migration. Proved non-UI: bun /tmp/opencode/c153-proof.ts 7/7 — the Svelte generate:'server' output carries the plain L2L label and no L2L (104), still emits the l2l input name + client rule, and keeps the siblings untouched. PLANNING §435. Suite/build/deploy left to the watcher.
C154 (2026-10-13): the loom master drops the Shifts field
/masters?tab=loom (owner; ?tab=party is the C139–C155 batch mislabel, Q103–Q113): the Loom tab no longer asks for Shifts. src/routes/masters/LoomForm.svelte removes the Shifts <select>, its client 1–4 rule and the header comment's mention; src/routes/masters/+page.server.tsloomErrors drops the maybeIntRange(fd, 'shifts', …) check (the helper still serves defectErrors); src/lib/server/masters.tsbuildLoomValues stops reading fd.get('shifts') — so an edit's .set({…}) omits the column and drizzle leaves the committed value alone, while an insert falls to the DB default 2; src/routes/masters/+page.svelte drops the Shifts column, the MASTER_FIELDS.loom row and the "…and shifts" tab description; static/masters-fields.html drops the Shifts row (Loom now "3 fields"). Pareto Q114/A114 — the row names one field, so the stored loom.shifts column (default 2) is kept and the movements/weft-issue shift pickers and capacity board stay untouched (the data still drives them; keeping the column is the reversible 80/20). No field added, no new validation surface; no schema/column/migration, no POST-name move. Proved non-UI: NODE_PATH=node_modules bun /tmp/opencode/c154-proof.ts 10/10 — buildLoomValues ignores a posted shifts=4 and keeps loomNo/class/type/active, and the Svelte generate:'server' output of the real form has no shifts input/label while the three real inputs remain; bun scripts/svelte-syntax.ts src/routes/masters/LoomForm.svelte src/routes/masters/+page.svelte clean; bunx tsc --noEmit adds no error in the touched files. PLANNING §436. Suite/build/deploy left to the watcher.
C155 (2026-10-13): the masters tables drop the Open column — the whole row opens the record
/masters?tab=party (owner; ?tab=party is the C139–C155 batch mislabel, Q103–Q115): every masters tab rendered one SmartTable with both an actions={{ label: 'Open', href: … }} column and a rowHref, and SmartTable already opens the record on a row tap (rowHref, PLAN §112 — a click navigates unless it lands on the row's own controls; the row is keyboard-focusable). So the Open link was a duplicate. src/routes/masters/+page.svelte deletes all seven actions props (party, yarn, design, loom, flange, defect, payment-terms) and keeps each rowHref; the record dialog, the ?tab=&id= deep link and SmartTable's showActions default are unchanged. Pareto Q115/A115 — the row names the masters tables, so the Open links on /tippans and /reconciliations (not master tables) are left as they are. No field added, no new validation surface; no schema/migration, reversible. Proved non-UI: bun scripts/svelte-syntax.ts src/routes/masters/+page.svelte compiles clean; a scan finds zero label: 'Open' and all seven rowHref props still present. PLANNING §437. Suite/build/deploy left to the watcher.
C156 (2026-10-14): the design master's warp/weft grid fits a phone — stacked cards below sm, table unchanged at sm+
/masters → Design tab → Add/Edit design dialog (owner; the warp/weft grid is the only wide element on the phone's full-width modal-bottom). src/routes/masters/DesignRowsGrid.svelte gains a mobile card layout (space-y-2 sm:hidden): each row is a card with the side + row number + delete on top, Count full-width, Unit and Share % side by side, Permissible counts below; the old table table-sm is wrapped in hidden … sm:block and kept as the desktop view. The per-row controls are Svelte 5 snippets (countField/unitField/pctField/permField/delField) rendered by both layouts against the same rows state, so there is one implementation per field; unit/pct use explicit onchange/oninput rather than bind: inside a snippet. Hidden POST input {name}/value={json}, the JSON shape, the auto-100% distribute, the % total badge and the permissible tags/count dropdown are byte-identical; DesignForm.svelte and +page.server.ts (100%-total rule) are untouched. Pareto Q116/A116 — mobile-only, desktop table preserved, reversible. No field added, no new validation surface; no schema/migration. Proved non-UI: bun scripts/svelte-syntax.ts src/routes/masters/DesignRowsGrid.svelte compiles clean; the Svelte client compile emits both the sm:hidden card container and the sm:block table container with only the pre-existing line-53 warning; bun run check exits 0 with the same 333 warnings; bunx tsc --noEmit adds no error in the touched file. PLANNING §438. Suite/build/deploy left to the watcher.
C157 (2026-10-14): the design master's permissible-count dropdown is searchable
/masters → Design tab → Add/Edit design dialog (owner; the warp/weft grid exists only on the design master — same C143–C156 batch mislabel, Q111). src/routes/masters/DesignRowsGrid.svelte: the permissible-count + counts dropdown (C152) now carries a search input — Row gains a transient permQuery, the <ul> opens with a sticky li holding an input.input.input-xs with inputmode="search", and the option {#each} filters permOptions through permMatch(r.permQuery, p) (blank = all, else substring on the count); the empty state reads "No matching count" / "No yarn counts in masters". The picker remains a multi-select (checkboxes + removable tags), so the stored permissible: number[] shape is unchanged. Pareto Q117/A117 — keep the multi-select and filter it (the field stores a list), and no minimum query length (2–3 digit counts should narrow on the first character). permQuery is UI-only: the hidden {name}/value={json}, the JSON shape, DesignForm.submit, +page.server.ts (permissible + 100%-total rules), the auto-100% distribute, the % total badge and the C156 phone/desktop layouts are byte-identical. No field added, no new validation surface; no schema/migration, reversible. Proved non-UI (node_modules/.c157-proof.ts, removed after): real Svelte client compile emits the search input + permMatch filter + checkbox list + both layouts, and the executed in-source permMatch gives blank→all, "6"→60/61, "61"→61, "9"→none (11/11); bunx tsc --noEmit adds no error in the touched file. PLANNING §439. Suite/build/deploy left to the watcher.
C158 (2026-10-14): the design master drops the Mode field
/masters → Design tab (owner). The design master's Mode<select name="modePref"> (BOTH / MTO / STOCK) is removed from every surface: src/routes/masters/DesignForm.svelte loses the select, the MODES const and the client modePref rule; +page.server.ts loses the DESIGN_MODES const and its designErrors() check; +page.svelte loses the ['Mode','modePref'] record-detail row and the SmartTablemodePref column; src/lib/server/masters.ts drops modePref from buildDesignValues(). Pareto (Q118/A118): keep the design.mode_pref column + default('BOTH') (no migration, reversible) — the sales-order form still prefills mode from the design (orders/[id]/+page.server.ts:229), and with the key gone from the builder an edit never overwrites the stored value while a new design takes the BOTH default, exactly like the old blank submit. Field removed, not added, so no new validation surface. Proved non-UI: bunx tsc --noEmit adds no error in the four touched files (only the pre-existing recursive-drizzle any diagnostics in schema-foundation.ts, untouched). PLANNING §440. Suite/build/deploy left to the watcher.
C159 (2026-10-15): migrate the app to SvelteKit 3
Repo-wide (owner). The app moves to @sveltejs/kit@^3.1.0 / svelte@^5.57.1 / sveltekit-superforms@^3.0.0 / zod@^4.6.7; the community svelte-adapter-bun@1.0.1 (Kit-2 Adapter API) is replaced by first-party @sveltejs/adapter-bun@^1.0.0 (same build/index.js output, so project@trading's ExecStart is untouched). svelte.config.js is deleted — its vitePreprocess()+adapter() config moves into vite.config.ts's sveltekit({…}); tsconfig.json now extends $app/tsconfig. The $lib alias is gone: ~196 files import through the package.json#lib subpath with .js extensions; $app/environment→$app/env, $app/stores→$app/state, ActionResult from $app/forms; src/env.ts (defineEnvVars) feeds $app/env/private (ORIGIN/BETTER_AUTH_SECRET); invalidateAll→refreshAll (~50 calls) and gotokeepFocus→reset:false / replaceState→replace. Pareto Q119/A119 (official adapter — the community one can't build on Kit 3; keep the still-working deprecated json()/text(); the delta and enhanced-foreign-action findings are false positives here). Proved non-UI: bun run build green on @sveltejs/adapter-bun (4094 modules, exit 0) and the built server boots on a spare port (/→303, /signin→200, cookieless POST /api/events→403); bunx tsc --noEmit = 140 errors, below the 147 baseline; bun run check exit 0 / 333 warnings (unchanged). No schema/migration; reversible via the dep pins + svelte.config.js. PLANNING §441. Suite/build/deploy left to the watcher. Deploy follow-up: the official adapter builds event.url from the request Host header, so loopback probes in scripts/live-check.ts presented origin http://127.0.0.1:4102 — which better-auth's isAuthPath rejects against ORIGIN (404 on every /api/auth/*, so the signed-in screen check reported "could not sign in"). The check now sends ORIGIN's Host header on every probe, exactly as Caddy forwards it; sign-in is confirmed working through the real domain (200 + __Secure-better-auth.session_token) and bun scripts/live-check.ts is 80/80.
C161 (2026-10-17): the type-fixed order forms' saved read-back stops 500ing
/orders/new/trading (owner; "new trading SO button showing 500 error"). The save flow on all three type-fixed order forms ends in src/lib/components/orders/OrderEntryForm.svelte's C157 enhanceGate, which after the action's redirect reads the order number back from GET /api/orders/[id] (OrderEntryForm.svelte:1260; ProductionOpForm.svelte:781 does the same for production ops). That endpoint selected order.quality — a column the order table does not have (an order's quality is the linked design master's qualityName) — so drizzle's orderSelectedFields threw (TypeError: Object.entries requires that input parameter not be null or undefined) and every call returned HTTP 500, reproduced on the running build 127.0.0.1:4102 as a signed-in owner for a real id, a junk id and no id. Fix: src/routes/api/orders/[id]/+server.ts selects design.qualityName and leftJoin(design, eq(order.designId, design.id)) (the /orders + /samples pattern); the response shape and both callers are untouched and an unknown id still returns {"order":null}. Pareto (Q120/A120): keep the field and source it correctly rather than delete it. No new field, so no new validation surface; no schema/migration, reversible. Proved non-UI: the route's own GET against /root/trading/data.db returns 200 with the order number + the real quality name for a real order and {"order":null} for a missing id; bunx tsc --noEmit adds no error in the file. PLANNING §442. Suite/build/deploy left to the watcher.
C162 (2026-10-17): the quote wizard's warp/weft reed space prefills from the last used RS for the quality
/quote → /quote/new (owner: "for warp and weft reed space, prefill the field by the last used RS for the quality in the quality field"). The wizard's Warp reed space (wrs) and Weft reed space (frs) always showed the quality master via eff() (adj ?? designData). The per-design last-quote scan in src/routes/quote/new/+page.server.ts (lastQuoteRows, already used for the last quote's warp/weft rows) now also reads inputs_json.warpReedSpace/weftReedSpace (the key both the wizard create and the old /quote/editsave write) through an asNum guard and stores wrs/frs on the same entry; prefillRates() in src/routes/quote/new/quoteNewState.svelte.ts seeds adj.wrs/adj.frs from it — a value that differs from the design master is carried, one equal to the master or with no quote history falls through to the master as before, so the summary's "edited" badge stays honest. Called on hand-picked quality, lead pick, ?leadId= preselect and dictation (all routes through prefillRates). Pareto Q121/A121 — newest quote per design_id, adjustment-only, no field added so no new validation surface, no schema/migration, reversible. Proved non-UI: the route's own load() against /root/trading/data.db returns lastQuoteRows with finite wrs/frs for 3/4 designs (e.g. 63"84/3*70/32*32-2/1-TWILL → wrs=66 frs=66); bunx tsc --noEmit adds no error in either file (140 errors = pre-change baseline). PLANNING §443. Suite/build/deploy left to the watcher.
C163 (2026-10-17): the quote wizard's job rate prefills from the last used job work for the quality
/quote → /quote/new (owner: "for job work field, prefill the field by the last used job work for the quality in the quality field"). The wizard's job step (jobRate, paise) always showed the design master designData[designId].jobRatePaise. The per-design last-quote scan in src/routes/quote/new/+page.server.ts (lastQuoteRows, already carry-ing the last quote's rows + C162's reed spaces) now also reads inputs_json.jobRatePaise through its existing asNum guard and stores job on the same entry; prefillRates() in src/routes/quote/new/quoteNewState.svelte.ts seeds jobRate from it (master fallback when there is no history). Both writers of inputs_json carry the key (wizard create spreads ...input; old /quote/editsave spreads the same QuoteInput). Set directly, not as an adj (job rate has no "edited" badge, unlike C162's wrs/frs). Called on hand-picked quality, lead pick, ?leadId= preselect and dictation. Pareto Q122/A122 — no field added so no new validation surface, no schema/migration, reversible. Proved non-UI: the route's own load() against /root/trading/data.db returns lastQuoteRows with finite job for all 4 designs with history (d-mt-11 → 7, c67-save-design → 12, d-mt-360 → 20, d-mt-1 → 18); bunx tsc --noEmit adds no error in either file (140 errors = pre-change baseline). PLANNING §444. Suite/build/deploy left to the watcher.
C164 (2026-10-17): the quote wizard's warp sizing rate prefills from the yarn master
/quote → /quote/new (owner: "warp sizing rate should prefill from the yarn master"). The wizard's warp rows read yarn.siz_rate only when the quality was picked, for the count that quote/design row carried; entering or stepping a different count in the warp popup left the sizing cell blank or holding the old count's rate. New pure yarnSizingRatePrefill() in src/routes/quote/quoteLogic.ts (master rate only into a blank cell or one this prefill wrote, never over a typed/history value); src/routes/quote/new/quoteNewState.svelte.ts tracks the auto-written value per warp row (sizAuto) and syncWarpSizing() runs on the count stepper, on "Rate →" and on the count field's blur, re-prefilling when the count moves to another master count and clearing only an auto value when it leaves the master; src/lib/components/quote/YarnPopup.svelte's count onblur confirms it. The old /quote/edit whole-name yarn pick also carries the yarn's sizing rate into a blank per-warp cell (src/routes/quote/edit/+page.server.ts + quoteEditState.svelte.ts). Pareto Q123/A123 — quote/design precedence kept, no field added so no new validation surface; no schema/migration, reversible. Proved non-UI: 7/7 helper cases against /root/trading/data.db (40 CW IMP = ₹30/kg; blank→30, typed 25 kept, auto 30→20 on count change, no master→no change); bun scripts/svelte-syntax.ts src/lib/components/quote/YarnPopup.svelte clean; bunx tsc --noEmit adds no error in any touched file (140 = pre-change baseline). PLANNING §445. Suite/build/deploy left to the watcher.
C165 (2026-10-17): the quote wizard shows the second warp/weft row of a legacy multi-count quality
/quote → /quote/new (owner: "for quality with multiple warp or weft, the second warp and weft fields are not appearing. correct that"). The wizard prefills warp/weft rows from the design's warp_rows/weft_rows JSON grid rows when present, else from the legacy single-count warp_count/weft_count string via masterCountRows. The JSON path was already correct (a 2-warp design fills two warp rows; the summary renders Warp 1 · 40s + Warp 2 · 20s — opened, not read). The legacy path dropped every count after the first: 25 imported designs join counts with + (34+30SLUB, 20S + 20S Lycra, 40/1+40Lycra, 16+220D, …) and masterCountRows parsed only the leading number. src/lib/server/masters.ts now splits the count string on + into one {count, system, pct} row per part (leading number; den/tex suffix → system, else Ne), sharing 100% evenly (50/50, last row takes the remainder) like the design grid's distribute; a string without + returns exactly the old single row (pct 100), so every other design is byte-identical. Both carriers read it (/quote/newdesignData and /quote/editresolveQuoteInput). Pareto Q124/A124 — the JSON grid is already the authoritative multi-row carrier and was proved working, so only the + string needed a reader; prefilled not auto-committed (no new validation surface), no schema/migration, one file, reversible. Proved non-UI: masterCountRows 11/11 against real values (34+30SLUB → 34,30; singles unchanged) and the route's own load() returns two weftRows for d-mt-64, with the rendered summary showing Weft 1 · 34s + Weft 2 · 30s and /quote/edit?designId=d-mt-64 carrying the same two rows; bunx tsc --noEmit adds no error in the touched file (140 = pre-change baseline). PLANNING §446. Suite/build/deploy left to the watcher.
C166 (2026-10-17): the quote wizard drops the sizing-GST field
/quote → /quote/new (owner: "remove sizing GST field"). The wizard's full form behind the steps (src/lib/components/quote/NewSummary.svelte) showed two tap-to-edit rows per warp: warp sizing ₹/kg and warp siz GST % — the latter opened the adjust popup via s.openYarnField('warp', i, 'sizGst'). §240l had already made sizing GST master-owned and hidden in quote, but this summary row survived. Removed the Warp n siz GST tap-row (NewSummary.svelte) and narrowed openYarnField to field: 'sizRate', deleting its sizGst branch plus the now-unused GST_COINS (src/routes/quote/new/quoteNewState.svelte.ts). The stored RowEd.sizGst, the yarnMasterForsizGstPct prefill, the server row shape and the engine's sizGstPct are untouched (blank = follow Fabric GST), so existing quotes cost identically. /quote/edit's own per-warp Siz GST % input stays — the row names /quote only. Pareto Q125/A125 — "remove the field" = the visible field on the named surface, keep the value plumbing; no field added so no new validation, no schema/migration, two files, reversible. Proved non-UI: bunx tsc --noEmit = 140 errors before and after (0 added; wizard file stays at its 12), bash scripts/quiet.sh bun run check reports no error/warning in either touched file. PLANNING §447. Suite/build/deploy left to the watcher.
C160 (2026-10-17): the design master's weave is picked from — or added to — a weave sub-master
/masters design tab (owner: weave chosen from a dropdown fed by the weave master; the weave master is a sub-master of the design master, never a separate tab, updated from design entries; a new weave typed into the field is added and offered next time; no duplicate weaves, values unique case-insensitively). The weave field was free text with no list and no de-dup. src/routes/masters/+page.server.ts now projects the sub-master from the design rows — designWeaves returns the distinct, trimmed weave values de-duped case-insensitively (first spelling wins) and sorted, exposed via load() as weaves; createDesign/updateDesign canonicalise a typed case-variant to the stored spelling before saving, and designErrors validates the field (WEAVE_RE, 1–20 letters/digits with space / _ -). src/routes/masters/DesignForm.svelte turns the input into a pick-or-type <datalist> fed by weaves with a client rule mirroring the server and an inline error slot; +page.svelte passes data.weaves into the Add and edit forms. Derived from committed design rows, so it cannot drift, needs no migration, and case-insensitive uniqueness falls out of the projection (fewest files, reversible). Proved non-UI: bunx tsc --noEmit adds no error in any touched file. The row session was killed mid-work by a deploy-scheduled queue-watch restart before it could write its docs; the code had already landed in 711c8b9 (swept into the
C161 watcher commit by deploy.sh's git add -A), so this line and PLANNING §448 close it.
C167 (2026-10-17): the new-tippan form drops its mill/ticket field
/tippans/new (owner: "Remove mill/ticket"). The C110/C115 focus-entry surface
still carried the tippan's old yarn-issue mill field (yMill, the yarn's mill
ticket) as dead plumbing — C115 had removed its wizard step and its
summary/review rows but left the page $state, the hidden POST input, the
TippanWizard prop/bind, the gatepass carry (g.ticket → yMill), the
PREFILLABLE entry and the create action's raw(fd, 'yMill') read. Removed all
of it: src/routes/tippans/new/+page.svelte (state, reset, PREFILLABLE, hidden
input, bind:yMill), src/lib/components/tippan/TippanWizard.svelte (prop +
type), src/routes/tippans/new/+page.server.ts (mill: r.issue.mill || gpRow?.ticket || null). The tippan's stored yarnIssue.mill is untouched —
still derived from the assigned yarn or its gatepass — so the record page, the
print and existing rows are byte-identical. Pareto Q126/A126: remove the entry
field only, keep the value plumbing; no field added, so no new validation; no
schema/migration; three files, reversible. Proved non-UI:
bun scripts/svelte-syntax.ts clean on both touched .svelte files; a
compile-and-assert proof over the compiled transport form shows the mill/ticket
name gone while yCount/yPacking/yBags/yCones/yNet are still carried;
bunx tsc --noEmit adds no error in the touched files (pre-existing row.reed
null only). PLANNING §449. Suite/build/deploy left to the watcher.
C168 (2026-10-17): the mill/ticket master is projected from the rate board
/masters (yarn tab / rate board; owner: "Create a new master of ticket/mill …
the entries in this master comes from rate board. If there is a new mill in the
rate board, it auto enters this master … whenever yarn is fetched, it shows all
permutations of yarn and mills"). The mill/ticket master is a derived
projection of the rate board's ticket column — distinct, case-insensitively
de-duped (first spelling canonical) and sorted, read live on every load(), so a
new board ticket auto-enters the master with no other action (mirrors C160's
weave sub-master; no new table/tab/migration). It feeds a pick-or-type
<datalist> on the yarn master's Ticket / mill input, so any count+type shows
every mill on file (the permutations) while a fresh mill can still be typed. Three
files: src/routes/masters/+page.server.ts (boardMills + mills in load),
src/routes/masters/YarnForm.svelte (datalist from a mills prop),
src/routes/masters/+page.svelte (pass data.mills to Add + Edit). No field
added, so no new validation surface; no schema change, stored yarns untouched,
reversible. Proved non-UI: the route's exported load() returns
mills = ["A1710","NITIN","S215","SANATHAN","U2931"]; on a scratch DB copy an
inserted rate with ticket "NEWMILL" appears in the very next load() (auto-
enters); bunx tsc --noEmit adds no error in the touched files (29 pre-existing
errors elsewhere only). PLANNING §450. Suite/build/deploy left to the watcher.
C169 (2026-10-17): the design master's weave is a visible searchable dropdown; the weave sub-master shows on the design tab
/masters?tab=design (owner; the sheet's /tippans/new target is the C143–C157
batch mislabel — a tippan's weave comes from its picked design, so the only weave
input is the design master's form — Q128/A128). Two files, derived-only, no
schema/migration. src/routes/masters/DesignForm.svelte: the Weave <datalist>
is replaced by an explicit searchable dropdown — focus/type filters the weave
master case-insensitively (blank = all, max 12) as tappable rows above the input
(FOCUS-ENTRY §4), a tap picks the stored spelling, and a new value is kept as a
new weave the server adds on save; name="weave", maxlength, the client rule
and the POST contract are byte-identical. src/routes/masters/+page.svelte: the
design tab gains a read-only Weave master panel listing the count and every
distinct data.weaves value (C160's projection, case-insensitive de-dup) as
badges, with a line naming it a sub-master fed by designs. No field added, so no
new validation surface; reversible. Proved non-UI: real Svelte compile of both
files clean; generated DesignForm code has no datalist, keeps the weave input,
and carries the filter + weaveMatches results + pickWeave handler; generated
+page.svelte carries the Weave master panel + data.weaves badges (13/13,
node_modules/.c169-proof.ts, removed after); bunx tsc --noEmit adds no error
in either touched file. PLANNING §451. Suite/build/deploy left to the watcher.
C166 (cont., 2026-10-18): the sizing-GST field also leaves /quote/edit
Owner, on live: "C166 can't see on live." C166 had removed the wizard's
Warp n siz GST summary row on /quote/new (Q125/A125 kept /quote/edit
untouched), but /quote/edit's warp rows still showed a visible Siz GST %
input (src/lib/components/quote/EditWarpRows.svelte) — the screen a person
reaches from /quote to change a quote — so the removal was invisible there.
Owner confirmed (custom session, 2026-10-18) the field should go there too. One
file: the Siz GST % label + input becomes a hiddenwsg{i} input carrying
the row's prefilled sg, so the stored value still posts and the engine's
sizGstPct path, the server validation (checkQuoteNumwsg{i}), the prefill
from yarn master/last quote and the costing are byte-identical (blank still
follows Fabric GST). Same split as C166: visible field gone, value plumbing
kept; no field added ⇒ no new validation; no schema/migration; reversible.
Proved non-UI: bunx tsc --noEmit adds no error in the touched file; live
signed-in probe shows /quote/edit with 0 Siz GST/siz GST matches while the
hidden wsg input still posts the value. PLANNING §452. Suite/build/deploy left
to the watcher.
C168 (cont., 2026-10-18): the mill/ticket master panel shows on the yarn tab
Owner, on live: "C168 … i cannot see the new master of mill/ticket." C168 had
projected the rate-board tickets into the yarn master's Ticket / mill field as a
pick-or-type <datalist>, which is invisible until focused and shows nothing on
a phone — so the master existed but could not be seen. The yarn tab
(src/routes/masters/+page.svelte) now carries a read-only Mill / ticket
master panel after the yarns table, listing the count and every distinct
data.mills value as badges, exactly mirroring C169's Weave master panel. No
server change (load() already returns mills), no field added ⇒ no new
validation, no schema/migration, reversible.
Proved non-UI: bun scripts/svelte-syntax.ts src/routes/masters/+page.svelte
compiles clean; live signed-in probe of 127.0.0.1:4102 shows
/masters?tab=yarn carrying the panel heading and the five badge values
A1710 NITIN S215 SANATHAN U2931. PLANNING §453. Suite/build/deploy left to the
watcher.
C170 (2026-10-18): the trading order's "We must deliver in (days)" opens its own pop-up
Owner, on live: "we must deliver in field is not opening up pop up for data
entry" on /orders/new/trading. The payment wizard step in
src/routes/orders/new/orderLogic.ts still listed dueDays in its fields,
but that step's popup renders only the dhara + slab controls and no delivery-days
box. So on a trading SO a tap on the We must deliver in (days) row resolved
to the payment step and opened that popup, where the deadline could not be
entered; summaryRowPopup returned 'wizard' and C180's single-field box (the
STEP_LESS_ENTERABLE.dueDays registration and its stepLessControl case) was
never reached. JW / OUTSOURCE / PROD were already correct (their payment step
is filtered out).
One line: dueDays leaves the payment step's fields. The field is already
registered step-less with its own popup, so the row now opens exactly that box
and still posts through the hidden name="dueDays" input — no field added ⇒ no
new validation (server readNum('dueDays', …, {integer:true, min:0}) and the
client inputmode="numeric" guard unchanged), no schema/migration, reversible;
payment entry untouched.
Proved non-UI: bun /tmp/opencode/c170-proof.ts imports the module and prints
dueDays step index = -1 | tapOpensPopup = -1 | summaryRowPopup = field for SO
(pre-fix 8 | 8 | wizard). bunx tsc --noEmit adds no error in the touched
file. PLANNING §454. Suite/build/deploy left to the watcher.
C171 (2026-10-18): the market dhara prefills brokerage on the trading order
Owner, on live: "when mumbai dhara is selected brokerage is to be prefilled as
1% and for ahmedabad dhara 2%" on /orders/new/trading. A pure lookup
(DHARA_BROKERAGE_PCT + dharaBrokeragePct) in
src/routes/orders/new/orderLogic.ts, applied by pickDhara in
OrderEntryForm.svelte, prefills the brokerage the selected market dhara pays —
Mumbai 1%, Ahmedabad 2%. It is a badged PREFILL (src.brokeragePct = 'dhara')
in the save gate, fires only while touched.brokeragePct is false so a typed
number is never overwritten, and a dhara not in the map (Job Dhara, a new
market) leaves brokerage alone. No field added ⇒ no new validation
(brokeragePct keeps its existing server readNum check and client decimal
guard), no schema/migration, reversible (drop the map + the pickDhara block).
Proved non-UI: bun /tmp/opencode/c171-check.ts → Mumbai "1", Ahmedabad "2"
(case/space-insensitive), Job Dhara / new market / blank → null.
bunx tsc --noEmit adds no error in the touched files (the one orderLogic error
is pre-existing). PLANNING §455. Suite/build/deploy left to the watcher.
C172 (2026-10-18): brokerage gets a slot wheel + 1% / 2% coins on the trading order
Owner, on live (/orders/new/trading): "brokerage should have slot scroll entry
with suggestion coins of 1%, 2%". The brokerage wizard step in
src/lib/components/orders/OrderEntryForm.svelte now renders a NumberWheel
(0–100, step 1 = one slot per percent) beside the existing decimal input, plus
1% / 2% coin buttons that set brokeragePct on tap and mark themselves active.
The wheel writes the picked whole percent; the input still accepts a fractional
value. WIZARD_STEPS' brokerage subtitle in
src/routes/orders/new/orderLogic.ts now says "Scroll 1% a slot, tap 1% or 2%,
or type any %." No field added ⇒ no validation change (brokeragePct keeps its
server readNum 0–100 and client decimal guard), no schema/migration,
reversible. Proved non-UI: bun run check exits 0 (333 pre-existing warnings).
PLANNING §456. Suite/build/deploy left to the watcher.
C168/C169 (cont., 2026-10-18): the mill/ticket and weave masters move to their own tabs; the yarn master drops its Ticket/mill field
Owner, on live: "yes pls remove it from there … i also cannot see the
mill/ticket master or the weave master." The masters were panels at the bottom
of the Yarn / Design tabs — below a 600+ row table, effectively invisible — so
they are now top-level tabs on /masters: Mill / ticket (tab=mill-ticket)
and Weave (tab=weave), read-only lists of data.mills / data.weaves
(count + badges); the inline panels are removed. The yarn master's Ticket /
mill field leaves Add/Edit (Table column + detail field too): a yarn is
count + type, the mill is its own master; the Edit form keeps the stored ticket
in a hidden input so no row is wiped, and buildYarnValues builds the name as
count type (+ ticket when present). Client + server "ticket required" rules
removed. No schema/migration, no field added, reversible.
Proved non-UI: bun scripts/svelte-syntax.ts compiles both touched .svelte
files clean; bunx tsc --noEmit adds no error in any touched file; live
signed-in probe shows /masters?tab=mill-ticket and ?tab=weave rendering their
lists and /masters?tab=yarn carrying no Ticket / mill input. PLANNING §457.
Suite/build/deploy left to the watcher.
C173 (2026-10-18): pass-on on the trading order scrolls a 0.25%-a-slot wheel with 0.5% / 2% coins
Owner, on live (/orders/new/trading): "brokerage pass on should have
suggestion coins 0.5%, 2% and should have slot scroll entry with per scroll
change of 0.25%". The pass-on wizard step now mirrors the C172 brokerage step:
a NumberWheel with min 0, max 100, step 0.25 (one slot = 0.25%) opening
at the current passOnPct, the decimal input beside it kept as typed
(inputmode="decimal", pattern="[0-9]+(\.[0-9]+)?", 0–100), and two coins
0.5% / 2% that set the field on tap and show active when equal. Coin taps
never auto-advance. No field added ⇒ server readNum('passOnPct',{min:0,max:100})
and the client decimal / 0–100 guard stay unchanged; the wizard step subtitle
now names the 0.25% slot. No schema/migration, reversible.
Proved non-UI: bun run check (svelte-fast-check) exits 0 — the touched
component compiles with no new error (333 pre-existing warnings); bunx tsc --noEmit adds no error in either touched file. Suite/build/deploy left to the
watcher. PLANNING §458.
C174 (2026-10-18): the orders list/operations page stops 500ing on an inherited-property sort value
Owner, on live: "order operations page shwing 500" (/orders?operations=1).
The load looked ?sort= up in SORT_COLS with a bare truthiness test, so an
inherited Object.prototype name (__proto__, constructor, toString, …)
passed the test, kept itself as sort, and its .col was undefined — drizzle
emitted order by desc, SQLite refused (no such column: desc) and the route
500ed. Fix: src/routes/orders/+page.server.ts accepts only the map's OWN keys
(Object.hasOwn(SORT_COLS, rawSort)), falling back to createdAt for inherited /
unknown values; every real sort key, dir, the query and the response are
untouched, no field/schema/migration, reversible. Assumed (Pareto): only the
target file — the same idiom on nine sibling list loads is a separate follow-up
(Q130/A130).
Proved non-UI: the running build returns 500 for /orders?sort=__proto__ and the
journal shows Failed query: … order by desc / SQLITE_ERROR: no such column: desc; the guarded query runs clean (falls back to createdAt) and
?sort=orderNo still sorts. bunx tsc --noEmit adds no error in the file.
PLANNING §459. Suite/build/deploy left to the watcher.
C175 (2026-10-18): the trading order's "We must deliver in" field becomes a start–end date window
Owner, on live: "we must deliver in field should have start date and end date …
suggestions of ready [dates] … today, +2, +7, +15, +30, +40 days and +/- button
to add/less one date" (/orders/new/trading). The C170 single-field popup held
one whole-day number box; it now shows a Start date and an End date,
each with today/+2/+7/+15/+30/+40 suggestion chips and a −/+ one-day
stepper. The window resolves to the same posted value: the whole-day span
dueDays (end − start, clamped ≥0, blank stays blank), still carried by the
hidden name="dueDays" input, so the server rule (integer, min:0) and the
client pattern="[0-9]+" guard are untouched — no field added, no schema or
migration. The start defaults to the booking day (already persisted as
orderDate); the summary row reads the window as start → end. Helpers
addDaysIso / deliverSpanDays / DELIVER_CHIP_DAYS live in
src/routes/orders/new/orderLogic.ts.
Proved non-UI: a bun script over those helpers passes (today+2 → 2026-10-20,
+40 → 2026-11-27, −1 → 2026-10-17; span 7/0/0-reversed/'' blank).
bunx tsc --noEmit adds no error in the touched files (the sole error in
orderLogic.ts is the untouched pre-existing v === '' comparison) and
bun run check reports 0 errors. PLANNING §460. Suite/build/deploy left to the
watcher.
C176 (2026-10-18): the mill/ticket master gets an Add button and a hand-entered list
Owner, on /masters: "add button in mill/ticket master". C168/§457 held that
master to a read-only projection of the rate board, so the row adds the missing
capability: the master is now the union of the board's distinct rate tickets
(auto, C168) and a new insert-only mill table entered by hand. A hand-added
mill is a case-insensitive duplicate of any existing board or stored mill → 409;
an empty or non-conforming name → 400 (mill / ticket must be 1-40 letters or digits (spaces, . & / - _ allowed)), checked server-side in
src/routes/masters/+page.server.ts and client-side in the new
src/routes/masters/MillForm.svelte (which also wires UniqueNameCheck via the
mill type added to /api/master-name). The mill/ticket tab gets the standard
header Add button + dialog. New table mill
(drizzle/0035_conscious_firebird.sql); buildMillValues in
src/lib/server/masters.ts. No rate lives here, so the quote popup is untouched.
Proved non-UI: a bun script calling the route's own load/actions.createMill
against a scratch data.db — new mill enters the returned list, case-variant is
409, junk is 400, builder returns the expected values. bunx tsc --noEmit adds
no error in the touched files (error set unchanged before/after) and
bun run check reports only pre-existing warnings. PLANNING §461.
Suite/build/deploy left to the watcher.
C177 (2026-10-18): the weave master gets an Add button and a hand-entered list
Owner, on /masters: "add button in weave master". C160/§448 held that master
to a read-only projection of the design master's weave column, so the row adds
the missing capability: the master is now the union of the designs' distinct
weave values (auto, C160) and a new insert-only weave table entered by hand. A
hand-added weave is a case-insensitive duplicate of any existing design or stored
weave → 409; an empty or non-conforming name → 400 (weave must be 1-20 letters or digits (e.g. PLAIN, 2/1 TWILL)), checked server-side in
src/routes/masters/+page.server.ts and client-side in the new
src/routes/masters/WeaveForm.svelte (which also wires UniqueNameCheck via the
weave type added to /api/master-name). The weave tab gets the standard header
Add button + dialog. New table weave (drizzle/0036_robust_harpoon.sql);
buildWeaveValues in src/lib/server/masters.ts; knownWeaves() now reads the
union so a typed design weave still canonicalises to a stored spelling.
Proved non-UI: a bun script calling the route's own load/actions.createWeave
against a scratch data.db — new weave enters the returned list, a case-variant
is 409, a design weave in the wrong case is 409, junk is 400, builder returns the
expected values. bunx tsc --noEmit adds no error in the touched files (error set
unchanged before/after). PLANNING §462. Suite/build/deploy left to the watcher.
C178 (2026-10-18): the inherited-property sort 500 closed on the remaining list loads
Owner, on live: "ANY ITEM IN THE TABLE LEADS TO ERROR 500"
(/orders?type=SO). /orders was already guarded by C174 and opens clean (all
897 SO record pages 200, no client error on the sign-in-and-click walk), so the
refiled crash is the same class on the sibling list loads C174/A130 left as the
follow-up. On the running build, /bookings, /tippans, /gatepasses, /lots,
/beams, /samples, /drafts and /hishob all 500 for a non-own sort
(__proto__, constructor, toString, hasOwnProperty): the inherited member
is truthy, sort keeps it, SORT_COLS[sort].col is undefined, drizzle emits
order by desc and SQLite refuses (no such column: desc). Fix: the same
one-line own-key guard C174 used, applied to the eight loads
(src/routes/{hishob,bookings,tippans,gatepasses,lots,beams,samples,drafts}/+page.server.ts),
each keeping its own default (createdAt, bookings date); real sort keys,
dir, queries and responses unchanged, no field/schema/migration, reversible.
Proved non-UI: running build → the eight sibling pages 500 for the four
inherited sort names; dev server on the patched source → all eight 200 for those
and 200 on the base load, /orders unaffected. bunx tsc --noEmit adds no error
in the touched files (29 pre-existing errors repo-wide, none in them). PLANNING
§463. Suite/build/deploy left to the watcher.
C179: the Trading SO gets its operations head start back, optional. C147/C148 had
taken the production-department terms off every sales face; the owner wants the
salesperson to hand over what the contract copy already states. Four fields return
to the ONE spec both faces read (ORDER_FIELD_SPEC, kinds:['SO'],
required:false): cut size (lotSizeMtrPerRoll), rolls, dispatchCity,
promisedDays — out of ORDER_INTENTIONALLY_HIDDEN, into STEP_LESS_ENTERABLE,
each a summary row opening its own single-field box (OrderEntryForm.stepLessControl).
The order-slip reader (/api/order-ocr) now reads the four too and
fillFromDictation marks them draft so they wait in the save gate. Nothing on
the operations side changed: same field names, same validateOpsFields appendage,
same ?/complete before→after amendment; a blank field still lands the order on
the operations queue. perPieceMtr + tolerance stay operations-only.
Proved non-UI: orderFieldsFor({type:'SO'}) carries the four names, the JW list
does not; summaryRowPopup(name,'SO')==='field'; validateOrderFields accepts and
reads them (lot 100, rolls 50, promise 7). bunx tsc --noEmit adds no error in
the touched files; bun run check exit 0. PLANNING §464. Suite/build/deploy left
to the watcher.
C180: the Job Work SO gets the same operations head start C179 gave the Trading
SO, optional. The four fields join ORDER_FIELD_SPEC by kind (['SO','JW'],
required:false): cut size (lotSizeMtrPerRoll), rolls, dispatchCity,
promisedDays — each a summary row opening its own single-field box, a blank one
never blocking Save and still landing the order on the operations queue. C148 had
removed the wizard's source step (and with it the contract-copy OCR read) from
every non-trading kind; wizardStepsFor keeps source for SO and JW, with a
JOB_SOURCE_STEP ("Read the contract copy?") for JW and the quote/lead picker
rendered only when t === 'SO', so the OCR/dictation panels are reachable and
C148's no-picker decision stands. Nothing on the operations side changed: same
field names, same validateOpsFields appendage, same ?/complete before→after
amendment.
Proved non-UI: wizardStepsFor('JW') carries source; orderFieldsFor({type:'JW'})
carries the four names while OUTSOURCE/PROD do not; summaryRowPopup(name,'JW')==='field';
validateOrderFields accepts and reads them (lot 100, rolls 50, promise 7) and
rejects 0/2.5/-3. bunx tsc --noEmit adds no error in the touched files; bun run check exit 0. PLANNING §465. Suite/build/deploy left to the watcher.
C181: the Stock (Production) order gets the same operations head start C179/C180
gave the Trading and Job Work SOs, optional. The four fields join
ORDER_FIELD_SPEC by kind (['SO','JW','PROD'], required:false): cut size
(lotSizeMtrPerRoll), rolls, dispatchCity, promisedDays — each a summary
row opening its own single-field box, a blank one never blocking Save and the
buyer-less PROD order always landing on the operations queue. wizardStepsFor
now keeps source for PROD too (the existing JOB_SOURCE_STEP "Read the
contract copy?" serves it) with the quote/lead picker still rendered only when
t === 'SO', so the OCR/dictation panels are reachable and C148's no-picker
decision stands. The single-field inputs gain the sibling client pattern rules
(decimal cut size, whole-number rolls/promise); the server rules were already
kind-agnostic. Nothing on the operations side changed: same field names, same
validateOpsFields appendage, same ?/complete before→after amendment.
Proved non-UI: wizardStepsFor('PROD') carries source; orderFieldsFor({type:'PROD'})
carries the four names while OUTSOURCE does not; summaryRowPopup(name,'PROD')==='field';
validateOrderFields accepts and reads them (lot 100, rolls 10, promise 7),
accepts a blank head start and rejects lotSize 0 / rolls 0 / rolls 2.5 / promise
-3; validateOpsFields accepts the same values with partyMissing:false.
bunx tsc --noEmit adds no error in the touched files; bun run check exit 0.
PLANNING §466. Suite/build/deploy left to the watcher.
C182: the Job Work SO now asks loom width and panna first, as told, on boththe wizard and the form. The loom step in WIZARD_STEPS moves to the head of
the list, so wizardStepsFor('JW') opens on "Loom width and panna?"
(loomWidth + panna) ahead of the contract-read source step and the
buyer/quality asks; the step is JOB_ONLY_STEPS, so the Trading SO and the
Production Order are untouched and the job-work-style OUTSOURCE form takes the
same order. In ORDER_FIELD_SPEC the two fields' positions move 51/52 → 1/2
so the form's Kind section and its check-and-save rows lead with them, and the
background section renders the two inputs ahead of the legacy loomType select
and the beams source. Reorder only — same field names, POST ids, optionality and
validation (server inSet against the loom-master widths + whole-number panna
≥ 1; client master <select> + min="1" input). Q136 asked and Pareto-answered
(A136: "first" is literally the first popup; OUTSOURCE shares the spec), recorded
as the row's assumed:. Proved non-UI by a bun pure-logic check: JW order is
loom > source > buyer > broker > quality > bookingDate > qty > jobRate > picks > rate, JW kind-section leads loomWidth > panna, and SO/PROD carry no loom
step/fields — PASS. bunx tsc --noEmit adds no error in the touched files;
bun run check exit 0. PLANNING §467. Suite/build/deploy left to the watcher.
C178 (reopened, 2026-10-18): the row popup's children snippet — the real "any item → 500"
Owner, on live: "ANY ITEM IN THE TABLE LEADS TO ERROR 500" (/orders?type=SO).
The C178 sort guard (above) closed a different, unlinked crash; the owner's
report is the row popup. src/lib/components/RecordDialog.svelte declared
children?: Snippet in its $props() type but never destructured it, so the
template's {#if children}{@render children()}{/if} referenced an undefined
name — compiled to a bare children, it threw ReferenceError: children is not defined the instant a row tap set open, and SvelteKit showed its error page.
Every list popup that passes content crashed the same way. Fix: one word — add
children to the destructuring. No markup/prop/caller change; no schema. Found
and proved with a real headless Chrome over CDP against live: before, the SO
table loaded clean but tbody tr.click() logged ReferenceError: children is not defined; after, the popup opens with no console error. bun run check exit
0; bunx tsc --noEmit unchanged. PLANNING §468. Suite/build/deploy left to the
watcher.
C184 (2026-10-18): the new-quote form's payment dhara row moves to the end
Owner, on live: "PAYMENT DHARA FIELD KEEP IN THE END" (/quote/new). The
payment dhara is the summary/full-form "Payment dhara" row (C97 picker); it was
the second section, right after Enquiry. Reorder only — same openDhara() popup,
same value, no field added, no validation owed. NewSummary.svelte now renders
the Payment section last, after Construction (and its cost card), just above the
← Steps / Calculate → actions, so the form reads Enquiry → Warp → Weft → Job →
Construction → Payment dhara (matching /quote/edit's terms fieldset, where the
dhara sits last). The wizard's dhara step already ends the step list (C183), so
it is untouched. Q137 asked and Pareto-answered (A137: the summary row is the
field; end = last section of the full form; C183's step/prefill wiring is C183's
scope); recorded as the row's assumed:. Proved non-UI by a Svelte-compiler AST
check of the <h4> order — fixed PASS [Enquiry,Warp yarns,Weft yarns,Job, Construction,Payment], pre-fix FAIL (Payment index 1). bun scripts/svelte-syntax.ts clean; bunx tsc --noEmit adds no error in the touched
file. PLANNING §469. Suite/build/deploy left to the watcher.
C185 (2026-10-18): the calc screen shows cost and weight per warp and weft when there are several
Owner, on live: "WARP AND WEFT COST AND WEIGHT IN THE SECOND PAGE OF COSTING
FORM SHOULD BE SHOWN FOR EACH WARP AND WEFT INCASE THERE ARE MULTIPLE"
(/quote/new). The "second page" is the wizard's calc screen
(NewCalc.svelte, phase === 'calc') — the only screen showing warp/weft cost
and weight; it listed only the smashed totals. Display-only change: when a side
carries more than one valid yarn row, NewCalc now adds a "Warp — each yarn" /
"Weft — each yarn" card, one line per row with its own ₹/m cost and g/m
weight; totals and single-row sides are unchanged, no field added, no validation
owed. Figures reuse the engine's per-row arrays already in the ?/calc payload
(C.warpRowCosts/C.weftRowCosts, W.warpRowKg/W.weftRowKg), filtered by
positive count so the index matches. Q138 asked and Pareto-answered (A138: the
calc screen is the second page; enrich it, leave summary). Proved non-UI:
/tmp/opencode/c185-check.ts (2 warp + 2 weft rows) asserts the per-row arrays
exist and sum to the totals (weights exact; costs ₹29.21 / ₹9.27), prints the
rendered lines. bun scripts/svelte-syntax.ts src/lib/components/quote/NewCalc.svelte
clean; bunx tsc --noEmit adds no error in the touched file. PLANNING §470.
Suite/build/deploy left to the watcher.
C186 (2026-10-18): estimated mtr becomes a 1000-mtr slot scroll with the quality's own sale-order coins
Owner, on live: "SHOW SCROLL ENTRY FOR ESTIMATED MTR WITH EACH STEP AS 1000 MTR
AND SHOW SUGGESTION COINTS FROM PREVIOUS SALE ORDERS OF THE SAME QUALITY"
(/quote/new). The estimated quantity — the Est qty (mtrs) term opened from
the calc screen into TermsPopup — was a plain numeric box. It now uses the
shared NumberWheel: the scroll steps in 1000-mtr slots (EST_QTY_SLOT),
the band shows the current value as-is (existing/typed figures are never rounded
— only the scroll steps), it is typeable, and the range is 0 .. the server's
1,000,000-mtr cap. The
coins are previous SALE orders (SO) of the same quality: order rows,
design_id + positive metre qty, most recent first, de-duplicated, max four.
The server reads them once into estQtyHints: Record<designId, number[]>; the
reading is the pure saleQtyHints in quoteLogic.ts (EST_QTY_SLOT /
EST_QTY_MAX / estQtyWheelValue beside it). No order history falls back to the
browser's last-used estimate; neither means no coins row. The popup's top
suggestion strip is suppressed for estQty so no value shows twice. No field
added — estQty already posts and already validates (junk, <0, >1000000
fail), so no new validation owed. Proved non-UI: /tmp/opencode/c186-check.ts
runs the pure readers (dedupe/unit/positive/cap, value-kept/0-floor/cap-clamp,
1000 slots);
bun scripts/svelte-syntax.ts src/lib/components/quote/TermsPopup.svelte clean;
bunx tsc --noEmit adds no error in the touched files; a read-only data.db
query confirms real type='SO' rows with design+qty exist. PLANNING §471.
Suite/build/deploy left to the watcher.
C187 (2026-10-18): the Delivery end term offers the standard window-length coins
Owner, on live: "DELIVERY END SHOW SUGGESTION COINS OF +10 DAYS, +20 DAYS, +30
DAYS, +40 DAYS, +" (/quote/new). The Delivery end term — the dEnd row
that opens TermsPopup from the calc screen — shared C105's coins with Delivery
start (the quality's two typical day offsets, "Typical for this quality"). It now
has its own coins: the standard window lengths +10 / +20 / +30 / +40 / +50 /
+60 days, the pure DELIVERY_END_DAY_STEPS + deliveryEndCoins(startDate, …)
in quoteLogic.ts, rendered by a dEnd branch split out of the shared
dStart/dEnd branch in TermsPopup; the dStart row keeps C105's coins
byte-identical. The base date is the Pareto call (Q140/A140): steps are added
to the chosen Delivery start when set, else today, so a suggested end
never lands before its own start (with no start it is the plain "today + N" the
wording implies); the trailing "+" continues the step of ten to +50 and +60, and
the UTC date-only formatting keeps an IST morning from shifting a coin a day. No
field, no new validation — deliveryEnd already posts and already passes
isDate in the create action; coins are suggestions that fill the draft, the
date input is still edited by hand, and no auto-commit. Proved non-UI:
/tmp/opencode/c187-check.ts (ladder, no-start = today+10…+60, start wins over
today, junk start falls back, 00:30-local no off-by-one, custom steps, ISO
values); bun scripts/svelte-syntax.ts src/lib/components/quote/TermsPopup.svelte
clean; bunx tsc --noEmit adds no error in the touched files. PLANNING §472.
Suite/build/deploy left to the watcher.
C188 (2026-10-18): the test-fixture orders are purged from data.db
Owner, on live: /orders showed test-fixture numbers like
SO-C135-1791194656869 and SO-C111-1791195431185 — rows the removed
c111/c135/c154 suites inserted directly into the shared data.db as
SO-C<correction>-Date.now(), where the app's real number is SO-YYMMDD-XXXX.
One-shot purge scripts/purge-fixture-orders.ts, run once on the live host:
back up data.db, then in a single transaction delete the rows matching the
acceptance globs (SO-C[0-9]*-[0-9]*, SO-AIR*, SO-AM-*, SO-ML-*,
SO-E2E*; no real SO-YYMMDD-XXXX can match) together with their linked
children — the 19 order-referencing columns plus evt/amendment where
entity='order'. Q141/A141 (Pareto): 344 matched, 196 referenced (all
fixture — e2e-probe events, c111 ops-complete amendments, SO-E2E-2701
rolls/dispatches); the acceptance's "shows no fixture number" wins over "none
referenced is removed", read as no referenced child is left dangling, not
a referenced fixture stays. Proved non-UI: the run printed candidates 344 →
order=344 removed with evt 433 / amendment 72 / order_attachment 71 / dispatch_slip 24 / roll 3 / lot 1 → remaining fixture orders: 0 → orders now: 1899; backup data.db.bak-C188-20261018123834; idempotent (second run = 0
candidates). No src/ change, so tsc is untouched. PLANNING §473.
Suite/build/deploy left to the watcher.
C189 (2026-10-18): the app's LLM reads use the official DeepSeek API
Owner handed over an official DeepSeek key: use DeepSeek-V4.1-Flash (text +
vision) for every LLM read in the app. All AI reads already funnel through one
gateway (src/lib/server/llmGateway.ts), so this is a provider swap: the live
host .env moves to CAPTURE_LLM_URL=https://api.deepseek.com with the given
key, and the model id becomes deepseek-flash (the official API's only flash
name; the old deepseek-v4.1-flash 400s). LLM_MODEL_DEFAULT updated; stale
"opencode go" provider comments in intake.ts, orderDocRead.ts,
captureOcr.ts, the two *-understand routes, OcrIngest.svelte and
leads/new/+page.svelte now say DeepSeek. .env is never committed; the
previous opencode-go values stay commented in it for one-edit reversibility, and
.env.example documents the url + model with an empty key. Q142/A142
(Pareto): the key is NOT committed — it lives in the host .env (the live
host); never commit secrets, fewest files, reversible. Proved non-UI:
GET https://api.deepseek.com/models (the key) → deepseek-flash with
input_modalities [text, image]; a real call through the app's own code
(intake.parseCaptureWithLLM) → by = llm; intake.parseLeadEnquiryWithLLM
with an image_url data URL → fields filled (vision). bunx tsc --noEmit adds
no error in the touched files. PLANNING §474. Suite/build/deploy left to the
watcher.
C190 (2026-10-18): the sample rate is one 0.25-a-slot scroll with the quality's order-rate coins
Owner: on /samples the rate was two wheels (rupees + paise); it is now a single
scroll. New FocusEntry mode rate renders one NumberWheel (min 0,
max f.max ?? 999, step f.step ?? 0.25, typeable) plus the field's suggestion
coins; money (the generic rupee-and-paise widget) is untouched. The sample form's
rate row switches to mode:'rate', step:0.25, max:999, with chips from a new
load field ratesByDesign — the distinct recent order rates of each quality
(most recent first, capped at 6), read off the same recent-orders array the
existing rateByDesign prefill already uses, so every coin is a real order rate
and follows the selected quality. Coin taps fill without auto-advancing. No field
added ⇒ no new validation: rate keeps strictNum('rate','Rate',{min:0}) and a
typed fractional rate posts unchanged. Q143/A143 (Pareto): wheel 0..999 step
0.25 typeable (a higher rate is typed), coins = up to 6 distinct recent order
rates — fewest files, existing validation untouched, reversible. Proved non-UI:
bunx tsc --noEmit adds no error in samples/+page.server.ts,
samples/+page.svelte, focus/FocusEntry.svelte (baseline had none). PLANNING
§475. Suite/build/deploy left to the watcher.
C193 (2026-10-18): the sample's auto entry number is shown at the top of the form
/samples (owner): a sample's auto entry number (sampleNo, minted
SP-YYMMDD-XXXX by makeDocNo('SP') on create) was the last row of the form's
last section ("Charge or waive"), so the user only met it after every field. The
read-only sampleNoStatic row (label Sample no, value Auto, note Auto on
save — the next sample number) moves into a new first section titled
"Sample", before "From (context)", so it is the top of the form. Display-only:
mode:'static', no POST name/schema/column/action moves — the value stays the
neutral Auto marker, exactly as the GP no / Movement no / Direct
delivery no rows read on the sizing, winding, movements and gatepass forms (a
signed non-step line per C197, so the correction's "this need not be a field").
No field added ⇒ no new validation: strictNum('qty'…), the design/party
existence checks and the charge/waive rules are untouched. Q144/A144 (Pareto):
"top of the form" is a new first summary section named "Sample" holding the
existing row, value left as Auto — one .svelte file, existing behaviour
kept, reversible. Proved non-UI: bun scripts/svelte-syntax.ts src/routes/samples/+page.svelte clean; bunx tsc --noEmit adds no error in the
touched file; a signed-in SSR fetch of the dev form (/tmp/opencode/c193-proof.ts,
/samples?q=__c193_nomatch__ so the create form renders) shows the form open,
exactly one Sample no row, and it before From (context) with the first
section heading Sample. PLANNING §476. Suite/build/deploy left to the watcher.
C194 (2026-10-18): Hishob and Reports leave the Orders sidebar group for their own Reports group
/hishob (owner): the sidebar Orders group carried the order-kind views
(Sale/Job-work/Outsource/Stock) plus Leads/Samples/Quote and the two report
screens Hishob (/hishob) and Reports (/reports). The correction says
the report screens do not belong under Orders ("these are report[s]").
+layout.svelte removes the Hishob and Reports children from the Orders
group and adds a new top-level Reports group (roles
owner/planner/sales/accounts) right after Orders holding both; the order kinds
and Leads/Samples/Quote stay. Display-only: no route, page, POST name, schema,
column, action or role filter moves. Q145/A145 (Pareto): the Orders group has
had no Orders child since C74, so "ORDERS" is read as the Reports link — Hishob
and Reports are the group's two report entries — and the two are moved (not just
deleted) so neither screen is stranded; one file, reversible. Proved non-UI
(C65/C66): bun scripts/svelte-syntax.ts src/routes/+layout.svelte clean; a bun
assertion over the navigation literal (/tmp/nav-assert.ts) shows neither
/hishob nor /reports under Orders, both under Reports, and the Orders kinds
plus Leads/Samples/Quote kept; bunx tsc --noEmit adds no error in the changed
file. PLANNING §477. Suite/build/deploy left to the watcher.
C195 (2026-10-18): the booking shows the order's yarn grid and can spread bags over several production orders
/bookings/new (owner): selecting a production order now shows a grid of the
order's own yarn — warp/weft · count+type · the kg already assigned by open
bookings against that order, expandable to each entry's date + mill ticket — and
several production orders can be selected, with the bags step asking how many
bags go to each ("in the qty field"). New pure module src/lib/server/orderYarn.ts
(buildOrderYarn/parseYarnRows) builds the lines from the design's
warp_rows/weft_rows, matching bookings by count+system+type and warp/weft
side; the load returns orderYarn for every open SO/PROD order and
OrderYarnGrid.svelte renders it in wizard step 0. The selection is a list
(orderSel; a PROD adds, an SO/pending replaces; orderId stays the primary);
the only new POST field is orderSplit (raw JSON [{orderId,bags}], sent only
for ≥2 orders), asked per order in the bags step and summed into bags, stored
additively in a new nullable booking.order_split column (migration
0038_nosy_doctor_octopus.sql). Server validates the split field by field
(readable JSON · ≥2 · no repeats · each an open SO/PROD · whole bags > 0 · sum =
posted bags); client keeps each input digits-only. Q146/A146 (Pareto):
"already assigned" = non-cancelled bookings against that order (the only order→yarn link);
primary keeps the existing cost-basis link and no extra order touches the ledger;
"production order" = type = 'PROD' (the grid also shows for an SO, harmless);
additive/reversible. Proved non-UI (C65/C66): bun scripts/svelte-syntax.ts on
the three changed/added .svelte files clean; a bun script over buildOrderYarn
(synthetic two-row warp, warp/both/weft mixes, a wrong-type booking) yields 1200
kg, newest-first entries, 0 for the unmatched row and the 40s CW / 20s
labels; the same build against the live open orders produces the grid lines;
bunx tsc --noEmit adds no error in the changed files. PLANNING §478.
Suite/build/deploy left to the watcher.
C196 (2026-10-18): the booking's cones/bag + bag wt prefill follows the yarn identity (count+system+type+mill)
/bookings/new (owner): cones per bag and bag wt must come from the last
booking of the same yarn, where "same yarn" is its count+type+mill — not one
master row. The history map was keyed on the exact yarn.id, so a sibling
master for the same yarn (two 61 CCW Sanathan rows exist) prefilled nothing.
Now the load leftJoins yarn, selects count/system/type/ticket and builds a
second map lastPackByYarn keyed count|countSystem|typeCode|ticket (last
booking wins); the client derives yarnIdentity + packHint, and the prefill
effect, the srcFor "last booking" chips and the wizard's lastCpb/lastBagWt
coins read packHint. The commercial fields (supplier/broker/lot/payment
term/rate) keep exact-id matching — unchanged. Q147/A147 + Q148/A148
(Pareto): identity includes the count system; only cones/bag + bag wt move to
identity matching (physical properties of the yarn); reversible, two files.
Proved non-UI (C65/C66): the real route load (mocked superforms/superforms-
adapters/@sveltejs/kit, live data.db) returns
61|Ne|CCW|Sanathan → { conesPerBag: 18, bagWt: 45 } while two masters share
that identity; bunx tsc --noEmit adds no error in the changed files.
PLANNING §479. Suite/build/deploy left to the watcher.
C197 (2026-10-18): the booking rate is one 0.25-a-slot scroll, coins from the yarn identity + the day's rate board
/bookings/new (owner): the rate must be a single scroll stepping ₹0.25, with
suggestion coins for the last used rate of the same yarn (count+type+mill) and
the day's rate board; a coin tap must not advance — only Next does. The rate
step's rupee+paise NumberWheel pair (0–4999 + 0–99) is replaced by one
NumberWheel min 0 max 999 step 0.25 typeable writing ratePerKg directly
(the FOCUS-ENTRY rate mode); rateParts/setRatePart removed from
BookingWizard.svelte and +page.svelte. rateChips now read packHint
(the C196 count|countSystem|typeCode|ticket identity) instead of the
exact-id lastForProduct; the server's lastPackByYarn map gained
ratePerKg (already selected). Coins still fillStay; auto-prefill unchanged.
Q149/A149 + Q150/A150 (Pareto): one wheel 0–999 step 0.25 (live max
rate 450); typed rates stay any value; only the coins move to identity
matching; four files, reversible. The shared NumberWheel also learned to
honour a fractional step (infer decimals, format slots, parse a typed float)
so typing a quarter rate no longer collapses to a clamped integer; integer-step
wheels are unchanged. Proved non-UI (C65/C66): the real route
load (mocked superforms, live data.db) returns lastPackByYarn rows all
carrying ratePerKg with 61|Ne|CCW|Sanathan → 253 across two sibling
masters, and the coin derivation yields ["258.75","253"]; the wheel's decimal
rules (0.25 → 2, 253.5 → "253.50", typed 253.5 → 253.5) are asserted by a
bun script; svelte-syntax.ts compiles the three changed .svelte files clean;
bunx tsc --noEmit adds no error in the changed files. PLANNING §480.
Suite/build/deploy left to the watcher.
C198 (owner, 2026-10-18): the trading SO form reorders to the owner's sequence —
booking date → quote/lead → broker → buyer → quality → rate → picks → qty →
payment/brokerage/pass-on, with the remark and yarn cost basis last. Reorder only
(WIZARD_STEPS order + ORDER_FIELD_SPEC section positions/memberships and the
four section titles in orderLogic.ts, plus the FIELD_SECTION jump map in
OrderEntryForm.svelte); no field, POST name, value or validation changed. C182's
loom-first job work wizard is preserved. Proved by a pure-logic bun script
(/tmp/opencode/c198-proof.ts) asserting the SO wizard order, the SO summary flat
order and the JW loom-first guard (all PASS); svelte-syntax.ts clean; tsc --noEmit adds no error in the touched files. PLANNING §481.
C199 (owner, 2026-10-18): the job work SO form reorders to the owner's sequence —
buyer's order no → booking date → system date → loom width (from master) → no. of
panna → beams whose → broker → buyer → quality → rate → picks → job rate → qty →
counted in → send as → deliver-in days → cut size → rolls → payment dhara → cash
discount/days → brokerage → capture draft → remark. One loom width for job work:
the duplicate legacy loomType class retires (loomWidth stays); dispatchCity,
promisedDays and contractFile leave job work. ORDER_FIELD_SPEC kinds/order in
orderLogic.ts plus the OrderEntryForm.svelte loom-block gate (was
shows('loomType'), now shows('loomWidth') || shows('beamsSource') with the
loomType select gated on its own shows); no field added, no POST name/value/
validation changed; C182's loom-first job work wizard preserved. Trading SO (C198)
and legacy OUTSOURCE forms left intact (Q152/A152). Proved by a pure-logic bun
script (/tmp/opencode/c199-proof.ts) asserting the JW summary order, the JW
removals, the JW wizard order and the SO/OUTSOURCE guards (all PASS);
svelte-syntax.ts clean; tsc --noEmit adds no error in the touched files.
PLANNING §482.
C200: the job work form's "Link a capture draft" field returns and can take an
upload. C199 had narrowed contractFile to OUTSOURCE, but the draft-link select
sat inside that same {#if shows('contractFile')} block, so it vanished from
job work; the two now have separate blocks (draft link gated on
shows('sourceDraftId'), lit for JW+OUTSOURCE). The block also gains a photo/PDF
input that posts to /api/capture-ocr, links the draft it writes as sourceDraftId
and prefills party/quality from the read's masters — the wizard's order-OCR
upload is unchanged. Proved by a pure-logic bun script (/tmp/opencode/c200-proof.ts)
asserting sourceDraftId is carried by JW/OUTSOURCE while contractFile is not on
JW (all PASS); svelte-syntax clean; check + tsc add no error in the touched file.
PLANNING §483.
Rate lists: /rates gains a daily rate_sheet parent + paste → review →
confirm import and the trader comparison matrix (cheapest ex-GST, GST 5%). Rows
stay append-only; a same-day revision supersedes the earlier sheet. The parser
is offline-heuristic with a DeepSeek fallback only when it finds no priced row;
PDF/image OCR and per-trader shorthand learning are deferred by the owner.
Proved by a real HTTP parse → confirm (4 rows) → revise → confirm (2 rows) run
against the dev app (first sheet superseded, second confirmed), plus check 0
and a successful build. PLANNING §484.
C201: the New booking form gains an optional Own firm row, picked from the
party master's own-party type (category='OwnUnit'). The wizard's last step
(index 13) lists the own units as one-tap buttons with a "Skip — no firm"
escape; a summary row, a review row and a hidden POST input carry it. The create
action stores the name in a new nullable booking.firm_name text column
(migration 0039ALTER TABLE booking ADD firm_name text) and logs it on
booking:created. schema.ts adds firmName (trim, min 1, max 120, optional);
the action re-checks master membership (a non-blank value must be an OwnUnit
party, else 400 with a field error that opens the own-firm step). Pareto
Q154/A154 + the row's assumed:: "own party type" = category='OwnUnit' (not
the isOwnUnit flag), field optional (reversible to required), placed last to
avoid clashing with the pending C207–C210 reorder rows, no auto-prefill. Proved
non-UI: bunx tsc --noEmit adds no error in the touched files (repo-wide
pre-existing-red), bun run check 0 errors, and a bun script against a
VACUUM INTO copy with the migration applied runs the exact zod schema +
drizzle membership predicate (own name OK+stored, blank OK, whitespace/long
rejected, own hit vs non-own no hit, insert+read round-trips firm_name).
Suite/build/deploy left to the watcher. PLANNING §485.
C192 (2026-10-18): the sample form's purpose field is removed
/samples (owner reopen): the free-text purpose field is gone from the form,
the draft prefill and the create action — the SECTIONS row, purpose/
purposeAuto state, srcOf, values, onfield branch, reset lines and hidden
POST input in samples/+page.svelte; the pick/prefill echo and the
sample_order insert in samples/+page.server.ts. Q155/A155 (Pareto): only
the form path is removed — the capture-draft confirm still writes the nullable
sample_order.purpose from the slip and the column stays, so existing data is
preserved and the change is reversible (two files, no migration). No field added
⇒ no new validation; the qty/rate/charge/party/design checks are untouched.
Proved non-UI: a bun script copied data.db to a temp file, called the real
create action with no purpose field → ok:true, row stored with
purpose null; svelte-syntax clean; bunx tsc --noEmit adds no error in the
touched files. PLANNING §486. Suite/build/deploy left to the watcher.
C192 re-verify (2026-10-18): the purpose field is still gone; the reopen is archived
Re-checked the committed removal on d0b295f: no purpose reference remains in
samples/+page.svelte / +page.server.ts. A bun script on a copy of data.db
called the real create action with no purpose field → ok:true, the row
stored with qty 12, rate 52.25, purpose null. The reopen's claim (field
still live) is stale, so queue-next.ts --resolve-reopen C192 archived it; the
capture-draft write and nullable column stay per Q155/A155. PLANNING §487.
Suite/build/deploy left to the watcher.
C203 — the quote wizard's L2L prefills from the quality's last actual value, and its coin leads with it (owner, 2026-10-18)./quote → /quote/new: L2L always showed the design master. The per-design last-quote scan in src/routes/quote/new/+page.server.ts (lastQuoteRows, already carrying the last quote's rows + C162 reed spaces + C164 job rate) now also reads inputs_json.l2l via its existing asNum guard and stores l2l on the same entry. prefillRates() in src/routes/quote/new/quoteNewState.svelte.ts seeds adj.l2l from it (mirrors wrs/frs — differing value carried, equal/no-history falls to the master); the L2L popup's coins now run through l2lCoinList() (design's last actual L2L first, then common masterCoins, deduped, max 5). Pareto Q156/A156 — L2L is stored only on design master + quote inputs_json, never on an order row, so "the last actual L2L for a finished order" reads as the design's most recent quote. No field added so no new validation surface; no schema/migration; two files; reversible. Proved non-UI: the route's own load() against /root/trading/data.db returns finite l2l on 4/4 designs with history (d-mt-11 → 107, c67-save-design → 104.25, d-mt-360 → 107, d-mt-1 → 100), and the prefill resolves as expected (c67-save-design has no master L2L so it carries 104.25; the rest equal their master and fall through); bunx tsc --noEmit adds no error in either file. PLANNING §488. Suite/build/deploy left to the watcher.
Order pickers → historic table (owner 2026-10-18, PLANNING §489)
New OrderTablePicker.svelte (SmartTable-based, inline/modal, single/multi,
optional onpick). Applied first to /bookings/new: the order step is now a
table of open job-work + sale orders showing order no, date, party, broker,
quality, qty, delivery, rate and status (was a search list of open SO/PROD);
server load and create/split validation switched PROD→JW. bun run check
exit 0, no error in changed files. Rest of the order pickers to follow.
C206 — create a missing buyer/broker from the picker (owner, 2026-10-18).Combobox.svelte gained createLabel/oncreate(query) (+ row in the matches
list and the no-match state). OrderEntryForm.svelte opens the existing
PartyForm as a quick modal (actionBase="/masters", refresh={false}, server
error shown in the modal) from the wizard buyer and broker steps, the background
buyer combobox and the background broker select; the typed name prefills, the
category defaults to Party / Weaver (OUTSOURCE) / Broker, and the saved row is
appended locally and selected (buyer advances, broker stays). No new order
field, so no new validation — PartyForm's existing client+server rules are
reused. Pareto Q157/A157. Proved non-UI: bun run check exit 0, no warning in
the three touched files, and bunx tsc --noEmit adds no error in them
(repo-wide 139 pre-existing errors). PLANNING §490. Suite/build/deploy left to
the watcher.
C209: /bookings/new now shows the system booking no. (BK-YYMMDD-XXXX,
marked generated on save) and the system date (from the server clock, load
returns systemDate) in a strip at the top, and the trader's booking no.
and booking date are the form's first two wizard steps (before order
assignment). Two new nullable booking columns (trader_booking_no,
booking_date, migration 0040); schema.ts adds traderBookingNo (optional,
trim, max 60, regex letters/digits/space /._-) and bookingDate (optional,
shared isRealDate), mirrored client-side by pattern/maxlength and
type=date. The wizard grew to 16 steps and every FIELD_META step,
FIELD_OF_STEP, WIZ_STEP_OF_SEC and summary/review row shifted by two; the
review sheet gains a leading Booking group. Both fields ride the hidden POST
and are logged on booking:created. Pareto Q158 + assumed:: optional on both,
booking date defaults to the system date untouched-only, reversible. Proved
non-UI: bun run check exit 0, bunx tsc --noEmit adds no error in the touched
files, and a bun script against a VACUUM INTO copy migrated with
drizzle-kit push round-tripped the row and confirmed the zod rules (valid
accepted, #bad! and 2026-02-30 rejected, blanks allowed). Suite/build/deploy
left to the watcher. PLANNING §491.
C200 reopen (owner 2026-10-18: "C200 does not work"): the first C200 pass put the
draft select + upload inside the background editable form, which is permanently
class="hidden" in focus-entry; the visible summary row for sourceDraftId
stayed in STEP_LESS_LOCKED, so tapping it did nothing. sourceDraftId is now
in STEP_LESS_ENTERABLE and the summary tap opens its own single-field box with
the draft picker + a slip upload that posts to /api/capture-ocr and links the
draft it writes. The hidden form still carries name="sourceDraftId" for the
POST. Proved live at 390×844 with an owner session: the row opens
"Link a capture draft", the upload returned HTTP 200, added the draft and set
the select to its id; no page/console errors. Deployed 07fd355; live-check
80/80 and screens 75/75 green.
C212 (owner 2026-10-18: yarn booking should open a table, not a drop-down): on
/gatepasses/new?kind=YARN_IN the booking field now renders a scrollable daisyUI
table of pending yarn bookings (booking no · yarn · trader · rate · bags ·
Pending/Part received) with a filter box, tapped-row select; the load reads
booking.status in ('open','partial') so part-received bookings stay until fully
received. Added table mode + columns/cells to focus/FocusEntry.svelte
(pick unchanged); the shared booking field uses it on both booking-driven kinds.
Proved by bunx tsc --noEmit (no new errors in touched files), clean
svelte-syntax compile, and a libSQL query showing the pending set. Suite / build
/ deploy left to the watcher.
C214 (owner 2026-10-18: OCR the gatepass received, save its document, add a
dictation field): on /gatepasses/new?kind=YARN_IN the upload section gained
three optional rows — ocrDoc (photo/PDF read by /api/capture-ocr, prefills
bags/vehicle/transport only, file kept for the record), document (photo/PDF
uploaded to /upload-file, the saved gatepass copy) and dictateText (Web
Speech textarea, max 2000, saved with the record and only prefills an explicit
bags/plate mention). All three ride the creation event payload (no column, no
migration) and read back on /gatepasses/<id>; hidden YARN_IN now also mounts
vehicle/transport. focus/FocusEntry.svelte gained the dictate mode and the
accept/maxlength props. Proved by bunx tsc --noEmit (no new errors in
touched files), bun run check exit 0, and a Bun schema/prefill script (valid
uploads accepted, .exe/.txt and over-long dictation rejected).
Suite / build / deploy left to the watcher. PLANNING §493.
C215 (owner 2026-10-18: change the form name to "Yarn Inward Intimation"): the
YARN_IN gatepass form's kind label was 'Yarn in' → 'Yarn Inward Intimation' in
gatepasses/new/+page.svelte (KIND_CFG.YARN_IN.label, which feeds the form
badge, the FocusEntry title and the saved dialog's Kind row) and mirrored in
gatepasses/+page.svelteNEW_KINDS (the "New gatepass" menu). The generic
<h1>New gatepass</h1> and the sidebar nav entry are untouched. Proved by
bunx tsc --noEmit (no new errors in the two touched files). Suite / build /
deploy left to the watcher. PLANNING §494.
C216 (owner 2026-10-18: yarn inward intimation number and system date on the
top): on /gatepasses/new?kind=YARN_IN the form now heads with two static rows
— Yarn inward intimation no (GP-YYMMDD-XXXX, the makeDocNo('GP') shape,
auto on save) and System date (the server's date) — in
gatepasses/new/+page.svelte; the route load in gatepasses/new/+page.server.ts
now returns systemDate, and YARN_IN's old foot-of-goods GP no static row is
dropped for that kind only. No field added, so no new validation. Proved by a
signed-in dev-server fetch of the page (both labels + the doc-no shape present,
old GP no label gone) plus bun run check exit 0 and no new tsc error.
Suite / build / deploy left to the watcher. PLANNING §495.
C217 (owner 2026-10-18: reorder the yarn inward intimation — booking, yarn,
GP no., pickup location, bags, remarks): on /gatepasses/new?kind=YARN_IN the
first section now runs bookingPick → a new read-only Yarn (count + type +
mill name) row (prefilled from the picked booking via bookingYarnIdentity,
mirroring DIRECT) → the C216 intimation-no + system-date rows; the pickup
location and bags keep their rows/POST names, and a new optional Remark
textarea (max 200) sits after the bags. The wizard follows the same order
(bookingPick → destPlace → bags). gatepasses/new/schema.ts caps remark at
200; it rides the creation event payload (no column/migration) and shows in the
record's Terms card (gatepasses/[id]/+page.svelte); the hidden POST mounts the
new name for YARN_IN only. Proved by /tmp/opencode/c217-proof.ts through the
real schema (absent/200 valid, 201 rejected), clean svelte-syntax compile of
the two .svelte files, and no new tsc error. Suite / build / deploy left to
the watcher. PLANNING §496.
C218 (owner 2026-10-18: remove kind — it is a standalone form for yarn inward):
on /gatepasses/new?kind=YARN_IN the Kind marker row is dropped — the
kindStatic row in gatepasses/new/+page.svelte is now behind
kind !== 'YARN_IN', and the after-save summary omits its Kind line for the
same kind. Every other kind keeps its Kind row; the C215 form-name badge, the
direction marker, the hidden POST kind/direction markers, resolveKind and
the list menu are untouched. No field added, so no new validation. Proved by a
signed-in SSR fetch (YARN_IN HTML has 0 Kind, YARN_OUT/DIRECT still 1) plus
clean svelte-syntax and no new tsc error. Suite / build / deploy left to the
watcher. PLANNING §497.
C219 (owner 2026-10-18: waive amount not needed — waive means complete waive):
on /samples the new-sample form's Charge or waive section no longer asks a
Waive ₹ number — picking Waive shows no further field (rate follows only when
charging), the wizard is partyId → designId → qty → chargeChoice → rate, the
after-save summary reads Waive (complete), and the hidden POST drops
waiverAmt. The create action (samples/+page.server.ts) drops the waiverAmt
parse/> 0 check and writes chargeType='waive', waiverAmt=0, rate=null
(auto-approved, waiverBy/waiverApprovedBy='user'); a charge still requires a
rate. The list Charge badge now reads chargeType so a complete waive shows
green waive (legacy rows keep their ₹… + warning tone). No new field, so no
new validation. Proved by /tmp/opencode/c219.ts driving the real action against
a data.db copy (waive → charge_type=waive, waiver_amt=0, rate=null; charge →
charge_type=charged, rate=95.5), plus no new tsc error. Suite / build /
deploy left to the watcher. PLANNING §498.
C220 (owner 2026-10-18: remove rate source): on /samples the new-sample form's
Charge or waive section no longer shows the read-only Rate source row; the
Rate ₹/m field still appears when Charge is picked (Waive stays complete from
C219) and the wizard is unchanged. Provenance is kept underneath — the client
rateFromOrder state, hidden POST name and rate_from_order column are
untouched, so saves still record a recent-order rate. One .svelte file; no
server/schema change and no new field, so no new validation. Recorded reading
Q165/A165 (--assume): remove only the visible row, keep the stored flag.
Proved by a signed-in dev SSR fetch of /samples?draftId=… (200; 0 Rate source, 1 Rate ₹/m, /tmp/opencode/c220.ts PASS), clean svelte-syntax, and
no new tsc error. Suite / build / deploy left to the watcher. PLANNING §499.
C221 (owner 2026-10-18: remove from context complete section): on /samples the
new-sample form no longer shows the whole From (context) section — the From lead picker, the From order picker and the read-only Capture draft row are
gone, along with the now-unreachable leadPick/orderPick carry plumbing and
the client navigator state that only served them. samples/+page.server.ts
drops the open-lead query, orderOptions and the lead/inArray imports;
recent still feeds the rate prefill + suggestion coins. The hidden draftId
POST, the query-param prefill and requestSource: 'capture' are byte-identical,
so a capture draft still opens prefilled and saves the same. No new field, so no
new validation. Recorded reading Q166/A166 (--assume): remove the complete
section including the draft row, keep the draft save path, two files, reversible.
Proved by a signed-in dev SSR fetch of /samples?draftId=… (/tmp/opencode/c221.ts
PASS, 11/11 — section absent, other sections + hidden draftId present), clean
svelte-syntax, and no new tsc error. Suite / build / deploy left to the
watcher. PLANNING §500.
C222 (owner 2026-10-18: "the rate scroll … last digit doesnt get erased with
backspace"): the rate wheel is the shared NumberWheel (step={0.25},
typeable). Its typeable centre reset the text to fmt(value) on a
non-parsing entry (so an emptied field snapped the last digit back) and the
sync effect re-canonicalised text on every value change while typing (so a
0.25-step decimal turned 8.7 into 8.70 mid-type and ate the next digit).
Fixed in NumberWheel.svelte: the sync effect returns early while typing
(the focused input owns its text), onType leaves empty/partial text as typed,
and the text canonicalises to fmt(value) on blur; range clamp unchanged. All
typeable wheels (rate, money, quantity, order, tippan) get the fix from the one
component. No field added, no new validation, no UI/markup change. Recorded
reading Q167/A167 (--assume): fix the shared typeable input, sync-off while
typing, canonicalise on blur. Proved non-UI by modelling the typing state
machine old vs new (/tmp/opencode/nw-proof.ts, ALL PASS: old last digit
stuck + 8.7→8.70; new empty, fresh 8, 8.75 kept then canonicalised) and
bunx tsc --noEmit adds no error. Suite / build / deploy left to the watcher.
PLANNING §501.
C223 (owner 2026-10-18: "In case broker not selected mark broker as direct
while saving and prefill brokerage as 1 and pass on field as 1"): C101's
convention had been rewritten client-side and the server-side DIRECT handling
dropped, so a direct order saved its broker blank. +page.server.ts now accepts
an incoming 'DIRECT' as "no broker" (it is not a party row), and stores
brokerName = 'DIRECT' for every non-PROD order that named no broker — the
buyer is already required by that point — with brokerage/pass-on defaulting to
1 / 1 only when empty (a typed value wins); direct + the terms join the
order:created payload. DIRECT_PASS_ON_PCT moves 100 → 1 (C223's own number;
C173's 0.5%/2% pass-on coins show pass-on is absolute %, not a share of the
brokerage) and the "prefills 100%" subtitles read 1%. directOrderDefaults now
reads our own 'DIRECT' as "no broker" so the form effect no longer clears and
re-fills it forever (§368's loop), and the broker <select> gains a DIRECT
option so the value displays and posts. No new field, no new validation,
append-only, light-mode, no new screen. Recorded reading Q168/A168 (--assume):
no broker + buyer + non-PROD = DIRECT on save, brokerage 1, pass-on 1; 'DIRECT'
counts as empty; typed value wins; PROD unchanged. Proved non-UI by importing
orderLogic.ts under bun -e (empty and 'DIRECT' both yield
{DIRECT,1,1}; real broker/no buyer/PROD yield null, so the effect is stable)
and bunx tsc --noEmit adds no error. Suite / build / deploy left to the
watcher. PLANNING §502.
C224 (owner 2026-10-18: "The reuse button open edit form. It should just open a
new form with the fields prefilled"): C132 pointed the quote preview's Reuse at
the same href as Edit (/quote/new?quoteId=<id>), which is C71's preview-Edit
carry — so Reuse showed the "Editing " banner. Reuse now links to
/quote/new?quoteId=<id>&reuse=1; the server still builds the full
quotePrefill but leaves quoteCtx null under reuse=1, so the page has no
edit carry, no re-quote flag and the save mints a fresh quote number. Edit
(?quoteId= alone) and Re-quote (&requote=1) are untouched. No new field, no
new validation, append-only, light-mode, no new screen. Recorded reading
(--assume): Reuse opens the same prefilled summary form as a new quote (no
edit carry, fresh number), Edit stays as-is. Proved non-UI by importing
src/routes/quote/new/+page.server.ts against a real local quote id: ?quoteId=
gives quoteCtx non-null with the prefill, ?quoteId=&reuse=1 gives the same
prefill (same party + design) with quoteCtx === null; bunx tsc --noEmit adds
no error. Suite / build / deploy left to the watcher. PLANNING §503.
C225 (owner 2026-10-18: "For the sales user following fields are compulsory for
saving Buyer Quality Rate Picks Quantity We must delivery in days Payment dhara
Cd Brokerage % Pass on % For production user the following fields are compulsory
for the entry made by the sales user Buyer order no Send as Mtr per roll Rolls"):
the Trading SO form's compulsory fields are now role-scoped. orderLogic.ts
gains roleRequiredFields(type, role) (SO only: production → partyContractNo /
deliveryForm / lotSizeMtrPerRoll / rolls, sales → designId / rate / picks /
dueDays / paymentDhara / paymentCombo / brokeragePct / passOnPct, else none);
validateOrderFields takes opts.role and fails each empty listed field (string
"… is required", numeric null, deliveryForm blank); the create action passes the
session creatorRole; OrderEntryForm reads the session role into the field-spec
context (red *) and into fieldsLeft so the Save gate reflects it. Quantity is
already mandatory and Buyer stays buyer-or-broker (§250). No new field,
append-only, light-mode, no new screen. Recorded reading Q169/A169 (--assume):
role-scoped on the one Trading SO form; other roles/kinds unchanged. Proved
non-UI by importing orderLogic.ts under bun (22/22: both lists, all missing
fields flagged, full forms clean, owner/no-role/JW unaffected) and bunx tsc --noEmit adds no error in the touched files. Suite / build / deploy left to the
watcher. PLANNING §504.
C226 (owner 2026-10-18: "Remove fields sales promise Send to city Move we must
delivery in days at the place of sales promise"): the Trading SO drops the
"Send to city" and "Sales promise" operations head-start rows and moves "We must
deliver in (days)" into the promise's slot. orderLogic.tsORDER_FIELD_SPEC
narrows dispatchCity and promisedDays to kinds:['PROD'] (the Production
order keeps both — its target was not named) and moves dueDays from the qty
section/order 40 to the terms section/order 30, the place the sales promise
held (this also aligns the summary with the section form, which already rendered
the dueDays box under Price). OrderEntryForm.svelte: the two hidden POST
inputs are gated by shows(...) and fillFromDictation only fills them where
the field is on the form, so an SO never posts/restores/reads-in a city or a
promise while PROD stays byte-identical; FIELD_SECTION.dueDays flips to
terms. No field added, no POST name or validation changed, append-only,
light-mode, no new screen. Recorded reading Q170/A170 (--assume): SO-scoped
removal, PROD unchanged, dueDays takes the promise's terms/30 slot. Proved
non-UI (C65/C66) by importing orderLogic.ts under bun (25/25: SO lacks both,
PROD keeps both, dueDays section/order, SO qty/terms placement, PROD field-box
popups, sales-role validation still requires dueDays) and bunx tsc --noEmit
adds no error in the two touched files (the one orderLogic.ts error is
pre-existing). Suite / build / deploy left to the watcher. PLANNING §505.
C227 (owner 2026-10-18: "Booking no. Field after ocr in wizard"): the Trading SO
wizard now asks the buyer's booking / order no. immediately after the OCR step.
orderLogic.tsWIZARD_STEPS gains a bookingNo step right after source,
owning the existing partyContractNo field (fields:['partyContractNo']) — no
new field/column/POST name, so the summary row's "Buyer's order no" now opens
this step; wizardStepsFor scopes it to type === 'SO' so Job Work
(C199)/Outsource/Stock keep their step sets. validateOrderFields gives the
reference a standard shape (blank optional; 60 chars max; first
letter/digit; A-Za-z0-9 + space / . _ - #), mirrored client-side by
maxlength/pattern on the new wizard input, the section box and the step-less
box. OrderEntryForm.svelte adds the step branch (input + error + hint) and the
client-side rules. Append-only, light-mode, no new screen. Recorded reading
Q171/A171 (--assume): "Booking no." = the buyer's booking/order no already
stored as partyContractNo (not the yarn cost-basis booking), as a Trading
SO-only step after OCR. Proved non-UI (C65/C66) by importing orderLogic.ts
under bun (11/11: SO step order, partyContractNo opens the step, JW/PROD
unchanged, reference validation accept/reject) plus bun scripts/svelte-syntax.ts clean and bunx tsc --noEmit no new error in the two
touched files. Suite / build / deploy left to the watcher. PLANNING §506.
C228 — /orders/new/trading rate step: the wizard's rate wheel became a ±
stepper at ₹0.25 a tap (RATE_STEP_SIZE, rounded to 2 dp, 0–999) with a
typeable centre, so typed decimals are kept instead of floored. Section rate
input and the ops popup wheel unchanged; no server/POST change. Proved non-UI
(C65/C66): bun scripts/svelte-syntax.ts clean and bunx tsc --noEmit no error
in OrderEntryForm.svelte. Suite / build / deploy left to the watcher.
PLANNING §507.
C229 — /orders/new/trading rate is shown and stored at 2 decimal points:
formatRate() in orderLogic.ts normalises every rate path (quote/lead/draft/
history prefills, job-rate × picks, coins, wizard stepper + section input) and
validateOrderFields rounds the submitted rate to 2 dp server-side. Pattern
tightened to two decimals; no POST-name change, ops popup untouched. Proved
non-UI (C65/C66): one-off bun import of orderLogic.ts (6/6: 33.5→"33.50",
33.456→"33.46", empty/null/letters→null, server 33.456→33.46, letters still
fail), bun scripts/svelte-syntax.ts clean, bunx tsc --noEmit no new error in
the touched files. Suite / build / deploy left to the watcher. PLANNING §508.
C230 — /orders/new/trading picks step: the wizard ± stepper now moves 1 pick
per tap (PICK_STEP_SIZE 100 → 1, typeable centre, clamped at 0, hint follows
the constant). The suggestion coins are the existing C142 same-quality + party
ladder and are unchanged; job work's bare picks input and its job-rate × picks
maths, the production-operation form, and the server/POST are untouched. Proved
non-UI (C65/C66): a one-off bun import of orderLogic.ts (7/7 — pair-first
coins, quality fallback, empty without quality, last picks, ±1 arithmetic,
floor at 0), bun scripts/svelte-syntax.ts clean and bunx tsc --noEmit no new
error in OrderEntryForm.svelte. Suite / build / deploy left to the watcher.
PLANNING §509.
C231 — /orders/new/trading has ONE source picker instead of the two quote/lead
drop-downs: the shared OrderTablePicker with Lead/Quote tabs (the bookings
C195 historic-table pattern), used inline in the wizard's source step too. The
load now offers only quotes with no generated SO and validity not past, and
only leads with no generated quote / not converted; picking one clears the
other and the create action rejects both in one POST. Proved non-UI (C65/C66):
a one-off bun script seeded a temp DB copy (2 leads, 6 quotes incl. expired,
same-day, SO-linked and lead-linked) and called the real load() — it offered
exactly the valid, same-day, approved and lead-linked quotes plus the unquoted
lead, excluding the expired/SO-generated quotes and the quoted lead. bunx tsc --noEmit no new error in the touched files. Suite / build / deploy left to
the watcher. PLANNING §510.
C232 — /orders/new/trading: the OCR wizard step (source) now holds only the
dictation + upload cards (title "Read the order slip?", fields: []); the C231
one-field two-tab lead/quote picker moves onto a new Trading-SO-only step
sourceLink placed immediately after it (before bookingNo), with the same
hidden quoteId/leadId posts, validation and form Kind-section picker
untouched. onQuote()/onLead() now fill party and quality only where still
blank, so a slip read by OCR/dictation (src: 'draft') is never overwritten —
the quote/lead is just linked; with no read present the link prefills as before.
Proved non-UI (C65/C66): a one-off bun import of orderLogic.ts (14/14 —
step order, field ownership, tap/summary mapping, JW/PROD exclusion, carried
skip), bun scripts/svelte-syntax.ts clean and bunx tsc --noEmit no new
error in the touched files. Suite / build / deploy left to the watcher.
PLANNING §511.
C233 — /orders/new/trading: the wizard's brokerage step is now a − / + join
stepper moving 0.25% a tap (clamped 0–100, two decimals) instead of the
NumberWheel scroll; typed values, the 1%/2% coins and the direct-order 1%
prefill are unchanged, and the summary popup keeps its plain input. Proved
non-UI (C65/C66): bunx tsc --noEmit no new error in
OrderEntryForm.svelte. Suite / build / deploy left to the watcher.
PLANNING §512.
C234 — /orders/new/trading: the wizard's pass-on step mirrors C233 — a − / +
join stepper moving 0.25% a tap (clamped 0–100, two decimals) instead of the
NumberWheel scroll; typed values, the 0.5%/2% coins, the direct-order 1%
prefill and the summary popup's plain input are unchanged. Proved non-UI
(C65/C66): bunx tsc --noEmit no new error in OrderEntryForm.svelte. Suite /
build / deploy left to the watcher. PLANNING §513.
C235 — /orders: the row-tap record popup gains a Cancel order button
(hidden when already cancelled) that opens a cancellation-remarks modal and
posts /api/amendkind: 'delete' with ownerGate: true, reusing the record
page's append-only cancellation — the order is flagged cancelled, never erased,
and stays openable with its history; a non-owner leaves it pending on
/approvals; remarks required (trimmed) and capped at 500 chars on input and
server-side; the list refreshes via invalidateAll. Proved non-UI (C65/C66):
bunx tsc --noEmit no new error in src/routes/orders/+page.svelte. Suite /
build / deploy left to the watcher. PLANNING §514.
C236 — /orders/new/production-order: the Stock (Production) order becomes the
staged sales→production entry C225 built for the Trading SO. roleRequiredFields
now covers PROD (sales → booking date/buyer/broker/quality/rate/picks/dueDays/
payment dhara/CD/brokerage/pass-on; production → buyer's order no/send-as/cut
size/rolls), enforced in validateOrderFields and mirrored in the form; the
sales block (plus the booking date and the quote/lead link) is back on the PROD
field spec and wizard; the quote/lead picker locks once the fields are keyed by
hand with no link (sourcePickerLocked); and the production completion
(?/complete + ProductionOpForm prodorder side) must fill the four physical
fields. Proved non-UI (C65/C66): a bun script over orderLogic.ts, 76/76
passing; bun scripts/svelte-syntax.ts clean on the three changed .svelte
files; bunx tsc --noEmit no new error in the touched files. Suite / build /
deploy left to the watcher. PLANNING §515.
C237 — /orders/new/trading: the send-as field's rolls/lumps options become
two daisyUI radio buttons instead of a drop-down. The field owns no wizard step,
so the operator sees it in the §4b single-field pop-up; that select is now a
centred radio radio-primary group bind:grouped to deliveryForm, radios
carrying no name (the hidden real form still posts the value once). No field
added, so validation is unchanged (deliveryForm required + inSet DELIVERY_FORMS on both surfaces); append-only, light mode, one file
(src/lib/components/orders/OrderEntryForm.svelte). Proved non-UI (C65/C66):
bun scripts/svelte-syntax.ts clean on the changed file and bun run check
(quiet-wrapped) exits 0 with no error. Suite / build / deploy left to the
watcher. PLANNING §516.
C238 — /orders/new/trading: the rolls field's visible name becomes
Rolls/Lumps. The one ORDER_FIELD_SPEC label drives both the summary row
and the §4b pop-up, and the role-required / numeric-validation labels in
orderLogic.ts follow it, so a bad value says "Rolls/Lumps must be a number…";
the rules (whole number, > 0 when present) are unchanged. Append-only, light
mode, one file (src/routes/orders/new/orderLogic.ts). Proved non-UI
(C65/C66): a bun script over orderFieldSpec + validateOrderFields confirms
the label and the validation message, and bunx tsc --noEmit adds no error in
the changed file. Suite / build / deploy left to the watcher. PLANNING §517.
C239 — /orders/new/trading: the "How many picks" suggestion coins now come
from the same quality alone. picksSuggestions filters by designId only
(most recent first, de-duplicated, up to four; party no longer preferred or
required), the coins also render under the §4 Price section's plain picks
input (non-job-work, same as the wizard step), and the load adds a dedicated
picksHistory (up to 200 recent orders that stored picks) so a quality whose
last order is older than the 40-row generic recent list still gets coins. The
C142 lastPicksFor prefill is unchanged; no field/POST/schema/validation
change. Proved non-UI (C65/C66): a one-off bun script over orderLogic.ts
(6/6 — quality-only order, party ignored, null/0 skipped, empty without/unknown
quality, prefill unchanged) and a second over the real data.db picks history
(4iqu71skz6 → [4800,4400,2200], d-mt-345 → [65], d-mt-1 → [60]);
bun scripts/svelte-syntax.ts clean on the changed component and
bunx tsc --noEmit no new error in the three touched files. Suite / build /
deploy left to the watcher. PLANNING §518.
C240 — /orders: the wide orders table now shows the party and quality
aliases instead of the full master names. partyCell (acronym → name → via
broker) and qualityCell (alias → quality name → —) were already computed for
the Job-work tab; the wide column set now uses them for Party and Quality. The
record popup still reads the full partyName / designName. Display-only, no
field/POST/schema/query/validation change, one file
(src/routes/orders/+page.svelte). Proved non-UI (C65/C66): bunx tsc --noEmit
adds no error in the changed .svelte (repo-wide run stays red on pre-existing
errors). Suite / build / deploy left to the watcher. PLANNING §519.
C241 — /orders: the wide orders table now leads with Booking date (the
wizard's own order.orderDate, C135) as its first column, displayed d MMM yy
and sorted by the raw date (sortBy: 'orderDate'); — when absent. The
trailing "Booked" column (the system createdAt) and the C140 four-column
Job-work table are unchanged. Display-only, one file
(src/routes/orders/+page.svelte). Proved non-UI (C65/C66):
bun scripts/svelte-syntax.ts clean on the changed component and
bunx tsc --noEmit adds no error in it (repo-wide run stays red on pre-existing
errors). Suite / build / deploy left to the watcher. PLANNING §520.
C242 — /orders: the wide table's Status cell now shows short badge words
(Open / In Prod / Delivered / Closed / Pending / Confirmed / Cancelled) instead
of the full C111 phrases, and the Progress cell leads with one abbreviated
fact (3 rl · 1.2k m sent, else ready metres, else lots/beams) instead of the
full aggregate string — both columns were wide enough to force horizontal
scroll on a phone. The record popup keeps the full progress breakdown and full
status. Display-only, one file (src/routes/orders/+page.svelte). Proved non-UI
(C65/C66): bun scripts/svelte-syntax.ts clean on the changed component and
bunx tsc --noEmit adds no error in it (repo-wide run stays red on pre-existing
errors). Suite / build / deploy left to the watcher. PLANNING §521.
C243 — /orders: the wide orders table no longer has a trailing actions column
(Open / Complete, Reuse, Requote) — it was the far-right cell of the
horizontally scrolling card, so the Open details button was the one off-screen.
showActions is false on every tab and the button lives only in the row's
record popup, which already carries Open full record / Complete operations,
Reuse, Requote, Book yarn, New tippan, Pre-slip and Hishob. Display-only, one
file (src/routes/orders/+page.svelte). Proved non-UI (C65/C66):
bun scripts/svelte-syntax.ts clean on the changed component and
bunx tsc --noEmit adds no error in it (repo-wide run stays red on pre-existing
errors). Suite / build / deploy left to the watcher. PLANNING §522.
C244 — /orders/new/trading: the lead/quote picker's wizard step now opens the
same modal OrderTablePicker (popup, Lead/Quote tabs) as the Kind section
instead of an inline table. The empty table was a data-scope bug, not a render
bug: the live DB has no status-new/quoted/followup leads (auto-lost moved
them to lost, which the Leads screen still lists) and its two open quotes
are past their printed validity (C231 hid them although the Quote list shows
them). The load and the create action now share the list screens' "open" sets —
leads new|quoted|followup|lost with no generated quote and not
converted/deleted, quotes open|approved|draft|sent with no generated SO, no
validity-date cutoff — so a shown row never fails on save. Proved non-UI
(C65/C66): the real load() against /root/trading/data.db returned 0 leads /
0 quotes before and 34 leads (lost) / 8 quotes (open,draft,sent) after;
bun scripts/svelte-syntax.ts clean and bunx tsc --noEmit adds no error in
the touched files. Suite / build / deploy left to the watcher. PLANNING §523.
C245 — /orders/new/trading: both faces of the picks field now step one pick
per tap. C230 had already taken the wizard's "How many picks?" ± stepper from
PICK_STEP_SIZE = 100 to 1; the §4 Price section input still had no ±
buttons, so it now renders the same join stepper (− / typeable centre / +,
bumpPicks(±PICK_STEP_SIZE), clamped at 0, whole picks). Job work keeps its
bare input and rate = jobRate × picks; the POST name, the [0-9]+ pattern and
the server rule are unchanged. The quality-only coins (C239, picksSuggestions
by designId alone over the 200-row picksHistory) already satisfy the second
sentence and are untouched. One file
(src/lib/components/orders/OrderEntryForm.svelte). Proved non-UI (C65/C66): a
one-off bun script over the real data.db history gave 4iqu71skz6 → [4400, 2200, 4800] and no design → []; bun scripts/svelte-syntax.ts clean and
bunx tsc --noEmit adds no error in the changed file. Suite / build / deploy
left to the watcher. PLANNING §524.
C246 — /masters?tab=loom: loom class and loom type are now separate
insert-only reference masters (loom_class, loom_type, migration 0042) with
their own tabs, Add buttons/forms and UniqueNameCheck types. Each master is
the union of the built-in values, values already on looms and hand-added rows,
so existing looms (including the dirty legacy AIRJET class) keep a valid
option. LoomForm/the record dialog now take classes/types from load()
and both server (loomErrors) and client validation reject a value that is not
in its master; class names normalise to a 2-3 digit width (210 cm → 210),
type names to upper case. Proved non-UI (C65/C66): a bun script applied the
migration to a copy of data.db and exercised the route's own
load/createLoomClass/createLoomType/createLoom — add/normalise/409/400
and the loom guards all behaved; bun scripts/svelte-syntax.ts clean and
bunx tsc --noEmit adds no error in the touched files. Suite / build / deploy
left to the watcher. PLANNING §525.
C247 — /masters?tab=loom: the loom type master is now the combined
width + type master (loom_type.width), listed as Width + Loom type in its
own tab and fed by the Loom class master for widths; loom gains a nullable
loom_type_id link and the loom form has ONE "Loom type" select
(width|type) instead of separate class/type fields, while still filling the
legacy loom_class/loom_type text columns so every downstream screen is
unchanged. Legacy blank-width type rows apply to all class widths, values
already on looms stay selectable, and the dirty AIRJET pair still saves. Pair
validation runs server-side (loomErrors, loomTypeErrors) and client-side;
duplicate is width + type (409, width-scoped UniqueNameCheck). Migration
0043 adds the two columns; append-only, light mode. Proved non-UI (C65/C66)
with the route's own load/actions on a pushed copy of data.db (add/dup/400,
bad pair, dirty pair, exact link, blank-width expansion); svelte-syntax clean
and tsc adds no error in the touched files. Suite / build / deploy left to
the watcher. PLANNING §526.
C248 — /orders/new/job-work: panna now prefills from the last order for the
same loom width + quality, whichever party, not from quality + party; the
old quality/party panna remains only as the blank-width fallback. Changing the
loom width re-runs the lookup and clears an untouched history panna, and the
main form's panna input now marks itself touched on typing. No schema change,
append-only. Proved non-UI with a pannaFor script (width/quality matches,
cross-party, unseen width, second quality) plus tsc adding no error in the
touched files. Suite / build / deploy left to the watcher. PLANNING §527.
C250 — /orders: every row whose production/operations details are incomplete
now carries a derived Details pending chip (Operations column beside workflow
Status; Ops column on the compact job-work table). The list load reuses the
production screens' pendingOpsCondition() over the visible ids — no stored
status, so it cannot drift. For role === 'production' the New/Add buttons are
hidden and each pending row gets a prefilled Fill details button (SO →
production/trading, OUTSOURCE → production/job-commercial, PROD →
production/production-order, JW → its order record operations wizard). SmartTable
now renders a slot-only actions header and skips empty badge cells. No schema
change, append-only, light mode. Proved non-UI (C65/C66): load() returned role
user and its opsPending set matched an independent pendingOpsCondition()
query 50/50; bun run check clean, tsc adds no error in the touched files.
Suite / build / deploy left to the watcher. PLANNING §528.
C251 (owner 2026-10-19: "Order entry wizard, role-scoped fields: the sales user
sees only his compulsory sales fields in the wizard and the production user only
the production fields; one field spec with role-aware visibility and requiredness,
enforced server-side and client-side"): ORDER_FIELD_SPEC gains
roles?: ('sales'|'production')[] and orderFieldShown drops a field for a role
not listed; production keeps buyer order no / send as / mtr-per-roll / rolls,
sales keeps the commercial+fabric side, and kind / status / system date / buyer /
qty / counted-in / remark stay universal so each role can book. The summary, the
section form, the review sheet and the wizard all read the same role-aware spec
(wizardStepsFor/wizardStepIndexForField/tapOpensPopup/summaryRowPopup
take the role; the booking-no step opens for a production user too). A
role-hidden carried field is posted by one hidden-input block in
OrderEntryForm, so nothing is blanked or overwritten and saving stays
append-only. roleRequiredFields extends the production set to the Job Work SO
(production must fill the four physical fields; JW sales requiredness stays []).
No new field, column, migration, screen or theme. Recorded reading Q189/A189
(--assume): spec-level visibility on both faces; universal buyer/qty; hidden
fields post carries; JW production compulsory four. Proved non-UI by importing
orderLogic.ts (64/64: role field sets for SO/PROD/JW, owner no-op, required
fields visible, role-scoped wizard steps, server validation for SO/JW); bunx tsc --noEmit adds no error in the touched files. Suite / build / deploy left to
the watcher. PLANNING §529.
C252 (owner 2026-10-19: "Production entry wizard, role-scoped fields across all
four production screens … the production user sees only his own fields and the
sales user only his"). PRODUCTION_OP_FIELD_SPEC gains role lists; the four
/production/* loads pass the session role; ProductionOpForm filters its
summary/pop-ups by role and marks/gates only that role's required fields;
validateOpsFields enforces the same set from ?/complete (production four /
sales seven; owner/other roles unchanged). Hidden values still post through the
always-present hidden form, so saving stays append-only. Proved non-UI by
importing orderLogic.ts (62/62: role visibility and requiredness on all four
sides, required fields visible, server errors for blank sets); bunx tsc --noEmit adds no error in the touched files. Suite / build / deploy left to the
watcher. PLANNING §530.
C253 (owner 2026-10-19: "rate per meter is auto calculated in job work order and
need not be in the wizard. Rate per meter = Job rate per pick*picks/100").
rateFromJobRate now divides by 100 (COSTING §3); a JOB_AUTO_STEPS set drops
the rate step from the JW wizard, the background Price field renders read-only
and always follows job rate × picks (onJobRate), the rate summary row no longer
opens a popup (STEP_LESS_LOCKED), and create ignores a posted JW rate in favour
of the derivation. OUTSOURCE keeps its editable form and explicit-rate-wins rule.
No new field/column/migration/screen/theme; POST name + validation unchanged.
Proved non-UI by importing orderLogic.ts (9/9: ÷100 maths, JW wizard has no
rate step while SO/OUTSOURCE keep it, JW rate row static vs SO wizard);
bunx tsc --noEmit adds no error in the touched files. Suite / build / deploy
left to the watcher. PLANNING §531.
C254 (owner 2026-10-19: "switch the positions of job rate per pick and rate per
mtr in the form"). The background Price section of OrderEntryForm.svelte now
reads rate ₹/metre → picks → job rate ₹/pick, matching the ORDER_FIELD_SPEC
summary order; only those two blocks swapped, Picks stayed between them. The JW
rate stays the C253 derived read-only value; POST names, validation and the
append-only save path are unchanged; the wizard is unchanged. No new
field/column/migration/screen/theme. Proved non-UI with a one-off bun script
asserting the Price-section order (rate < picks < jobRate), a single jobRate
input, and the same order from orderFieldSectionsFor('JW'); svelte-syntax
compiled clean and bunx tsc --noEmit adds no error in the touched file. Suite /
build / deploy left to the watcher. PLANNING §532.
C255 (owner 2026-10-19: "take "Beams:Whose?" field at the end in terms").
beamsSource moved out of the Order & source block to the last slot of the
Terms section (ORDER_FIELD_SPEC order 150, after the yarn cost basis) in
OrderEntryForm.svelte; FIELD_SECTION follows, so the summary row and the
scroll target moved too. Same POST name, options, validation and append-only
save path; the wizard never asked it. Shared form means OUTSOURCE reads the same
order. No new field/column/migration/screen/theme. Proved non-UI by importing
orderLogic.ts (JW terms ends with beamsSource, kind no longer contains it) and
grepping the single name="beamsSource" inside #sec-terms; svelte-syntax
compiled clean and bunx tsc --noEmit adds no error in the touched files (the
orderLogic date-formatter error is pre-existing). Suite / build / deploy left to
the watcher. PLANNING §533.
C256 (owner 2026-10-19: "show booking date, Then order no., Quality palias,
Party alias, Mtr/beam, job rate, Status in this order").
The compact JW table on /orders?type=JW now renders booking date, order no,
quality alias, party alias, mtr/beam, job rate (₹/pick from order.jobRate) and
status, in that order, with the C250 Ops marker kept last. load in
orders/+page.server.ts projects jobRate; other tabs unchanged. No schema,
migration or write path. Proved non-UI by importing the route load() for
?type=JW (rows carry orderDate/orderNo/aliases/qty+unit/jobRate/status; live
row jobRate 13, qty 30000 mtrs) and bunx tsc --noEmit adds no error in the
touched files. Suite / build / deploy left to the watcher. PLANNING §534.
C257 (owner 2026-10-19: "enable chat with party and share order button in job
work order"). The C153 chat/share links in the orders row popup were disabled
whenever the job-work party had no mobile (most live JW parties), so
src/routes/orders/+page.svelte and src/routes/orders/[id]/+page.svelte now
always render both actions; a known mobile still deep-links to that party and a
missing number opens WhatsApp's contact picker (wa.me/?text=…) with the same
prefilled text, plus the existing "add one on the party master" hint. The href
rule moved to the pure src/lib/waLink.ts; WhatsAppLink.svelte takes an
optional fallbackToPicker (default off), so other callers are unchanged. No
new field/column/migration/screen/theme. Proved non-UI with a one-off bun
script (waHref 5/5) and Svelte compilation of the three touched files;
bunx tsc --noEmit adds no error in the new helper. Suite / build / deploy left
to the watcher. PLANNING §535.
C258 (owner 2026-10-19: "remove book yarn button for job work order"). The
orders row popup offered Book yarn on every kind, including job work, which
runs on the buyer's yarn and already omitted that step on its detail page
(C161). src/routes/orders/+page.svelte now includes the action only when the
tapped row is not JW; Sale/Outsource/Stock and every other popup action are
unchanged, and the All tab hides it on JW rows too (per-row decision). No new
field/column/migration/screen/theme. Proved non-UI with bun run check (0
errors, pre-existing warnings only) and bunx tsc --noEmit showing no error in
the touched file. Suite / build / deploy left to the watcher. PLANNING §536.
C259 (owner 2026-10-19: "remove new tippan button for job work order"). The
orders row popup offered New tippan on every kind, including job work, whose
detail-page next steps already stop at beam-out and yarn-inward (C161).
src/routes/orders/+page.svelte now includes the action only when the tapped
row is not JW; Sale/Outsource/Stock and every other popup action are
unchanged, and the All tab hides it on JW rows too (per-row decision). The JW
detail page needed no edit. No new field/column/migration/screen/theme. Proved
non-UI with bun run check (0 errors, pre-existing warnings only) and
bunx tsc --noEmit showing no error in the touched file. Suite / build /
deploy left to the watcher. PLANNING §537.
C260 (owner 2026-10-19: "picks field give +/- button with change of 1. Show
suggestions based on picks of the same quality last used"). The job-work picks
field was a plain input in both the price/terms popup and the wizard step,
while sale/production already had the C142 ±1 stepper and quality coins.
src/lib/components/orders/OrderEntryForm.svelte now renders the same stepper
(clamped at 0, step 1) and the same picksSuggestions coins for job work — the
most recent distinct stored picks of the same quality, up to four, none when
that quality has no history. Taps still recompute the JW rate = job rate ×
picks. No new field/column/migration/screen/theme. Proved non-UI with a one-off
bun script asserting the quality-only suggestion ladder (deduped, newest
first) and last-picks read, plus bun run check (0 errors, pre-existing
warnings only) and bunx tsc --noEmit showing no error in the touched file.
Suite / build / deploy left to the watcher. PLANNING §538.
C261 (owner 2026-10-19: "Job work form is showing trading draft. Correct
that"). /orders rendered all three per-form draft offers (C157) on every kind
tab, so ?type=JW showed a half-filled Trading SO draft above the job-work
table. src/routes/orders/+page.svelte now derives draftOffers filtered by
data.fType: a kind tab shows only its own form's draft; the All tab keeps all
three as before. DraftOffer and the sessionStorage keys are untouched. No new
field/column/migration/screen/theme. Proved non-UI with bun run check
(0 errors, pre-existing warnings only) and bunx tsc --noEmit showing no error
in the touched file. Suite / build / deploy left to the watcher. PLANNING §539.
C262 (owner 2026-10-19: "job rate per pick suggestions from previous used for
same quality. give +/- button with change of 0.25"). The job rate was a plain
input in the job-work form's terms popup and wizard step, beside a picks field
that already had C260's stepper and quality coins. OrderEntryForm.svelte now
renders a ±₹0.25 join stepper (clamped at 0, 2dp) and quality-history coins
from the new pure jobRateSuggestions helper (most recent distinct stored job
rates of the same quality, up to four). orders/new/+page.server.ts supplies a
bounded jobRateHistory read (200 rows, like picksHistory). Taps still
recompute the JW rate = job rate × picks ÷ 100. The list itself stays
read-only. No new field/column/migration/screen/theme. Proved non-UI with a
one-off bun script asserting the quality-only ladder (deduped, newest first,
cap 4) plus bun run check (0 errors, pre-existing warnings only) and
bunx tsc --noEmit showing no new error in the touched files. Suite / build /
deploy left to the watcher. PLANNING §540.
C263 (owner 2026-10-19: "mtr/beam field make the active scroll as editable.
Right now a seperate editable text box is shown for manual entry. correct it").
The job-work wizard's quantity step showed the C136 slot wheel (already
typeable) AND a separate w-32 quantity input beside it. The separate box is
removed from src/lib/components/orders/OrderEntryForm.svelte; the wheel's own
active slot is now the manual-entry surface (tap centre to type, drag to spin,
coins still fill), with the hint copy updated; NumberWheel gains an optional
ariaLabel prop so that input keeps the removed box's "Quantity" name. The
step 3 name="qty" input
still posts the bound value, so the POST is unchanged. No new
field/column/migration/screen/theme. Proved non-UI with bun run check
(0 errors, pre-existing warnings only); bunx tsc --noEmit still reports only
the repo's pre-existing errors (the .svelte file is covered by
svelte-fast-check, not tsc). Suite / build / deploy left to the watcher.
PLANNING §541.
C264 (owner 2026-10-19: "All 4 order forms should show only their draft entries
respectively. not each other's"). The four order forms share one draft mechanism
keyed order-new-draft[-TYPE], but /orders derived draftOffers from only
three entries (SO/JW/PROD); the generic OUTSOURCE form (/orders/new?type=OUTSOURCE,
null fixedType) keys order-new-draft and had no offer, so its draft was never
offered and the OUTSOURCE tab matched no offer either.
src/routes/orders/+page.svelte adds the fourth entry
({ key: 'order-new-draft', type: 'OUTSOURCE', label: 'Outsource Order', href: '/orders/new?type=OUTSOURCE' }); the existing !data.fType || data.fType === d.type filter then gives each kind tab only its own draft and the
All tab all four. DraftOffer and the sessionStorage keys are untouched. No new
field/column/migration/screen/theme/write path. Proved non-UI with a one-off
bun script asserting every tab maps to its own kind only and the OUTSOURCE
form's key matches the new offer, plus bunx tsc --noEmit showing no error in
the touched +page.svelte. Suite / build / deploy left to the watcher.
PLANNING §542.
C265 (owner 2026-10-19: "remove efficiency linked rate, additional
unconditional rate, weaver liability clause, delivery gatepass leg" on
/orders/new?type=OUTSOURCE). The four OUTSOURCE-only entries (tcsEffRate,
tcsUncondRate, tcsLiability, deliveryGpId) are removed from the ONE
ORDER_FIELD_SPEC in src/routes/orders/new/orderLogic.ts (so they leave the
summary and both form faces at once) and recorded in
ORDER_INTENTIONALLY_HIDDEN with the C265 reason; the hardcoded "Outsource
terms + delivery leg" block in src/lib/components/orders/OrderEntryForm.svelte
loses its four inputs and the matching stepLessControl pop-ups, $state,
FIELD_SECTION, roleHiddenCarried, fieldVal, reset entries and the unused
gpMap, and its title becomes "Outsource terms". No schema/migration/POST
name/action/validation change — a hand-rolled POST still validates and a past
order keeps its booked T&C/gatepass (append-only); the operations/production
face and order-record page are untouched (Q199/A199, --assume). No new field,
so no new validation. Proved non-UI with a one-off bun script asserting the
OUTSOURCE form no longer lists/has the four and that all four are declared
intentionally hidden (all PASS, terms/qty/rate/partyId still present),
plus bun run check via scripts/quiet.sh+heavy.sh (0 errors, pre-existing
warnings only). Suite / build / deploy left to the watcher. PLANNING §543.
C266 (owner 2026-10-19: "shift Beam:Whose? to the end of terms" on
/orders/new?type=OUTSOURCE). Already satisfied by C255: beamsSource is the
last entry of the shared ORDER_FIELD_SPEC terms section (order 150) and its
single select renders after the "Outsource terms" block in
OrderEntryForm.svelte, which the legacy OUTSOURCE route uses; FIELD_SECTION
follows, so the summary tap scrolls to Terms. No code change — same POST name,
options and optional validation, append-only save untouched. No new field, so no
new validation. Proved non-UI by importing orderFieldSectionsFor({type: 'OUTSOURCE', role:'sales'}) (terms list ends with remark > beamsSource) and
checking the one name="beamsSource" input sits inside #sec-terms. No UI test
was run. Suite / build / deploy left to the watcher. PLANNING §544.
C267 (owner 2026-10-19: "make outsourced form UI same as job work order form.
Except instead of buyer's order no. it will be weaver's order no.").
orderLogic.ts now makes wizardStepsFor treat JW and OUTSOURCE identically for
the OCR source, loom, job-rate and derived-rate steps, and removes the
OUTSOURCE-only loomType/contractFile/terms entries (recorded in
ORDER_INTENTIONALLY_HIDDEN); lotSizeMtrPerRoll/rolls include OUTSOURCE.
OrderEntryForm.svelte locks the OUTSOURCE kind by URL (lockedType, no kind
picker, hidden type, same summary rows; draft key still order-new-draft for
C264), derives the rate for both job kinds, says "Read the contract copy" on the
OCR step, and labels partyContractNo "Weaver's order no" via the new
orderFieldLabel; server validation uses the same label. No new field/column/
migration/screen/theme; append-only save untouched; brokerage left for C268.
Proved non-UI with a one-off bun script asserting wizard/field parity for
sales+owner, the three hidden fields, and the OUTSOURCE vs JW validation label
(all PASS), plus bun run check (0 errors, pre-existing warnings) and
bunx tsc --noEmit showing no new error in the touched files. Suite / build /
deploy left to the watcher. PLANNING §545.
C268 (owner 2026-10-19: "remove brokerage" on
/orders/new?type=OUTSOURCE). orderLogic.ts narrows the shared
brokeragePct spec from COMMERCIAL_KINDS to ['SO', 'JW', 'PROD'], so the
outsource form no longer shows a brokerage row on either face (the sale / job
work / production forms keep it); the job-work-style wizard already skipped the
brokerage popup. OrderEntryForm.svelte's C138 1% prefill effect now fires only
for t === 'JW'. +page.server.ts writes brokeragePct: null for OUTSOURCE
(the direct-order fallback never lands there). No new field/column/migration/
screen/theme; append-only save untouched, a past outsource order keeps its booked
value. Proved non-UI with a one-off bun script asserting orderFormHas /
orderFieldsFor / wizard steps for OUTSOURCE vs JW/SO/PROD (all PASS), and
bunx tsc --noEmit (no error in the touched files; repo-wide red as usual).
Suite / build / deploy left to the watcher. PLANNING §546.
C269 (owner 2026-10-19: "remove buyer, rate, send to city, all terms and papers
fields (except yarn allocation and remarks)" on /orders/new/production-order).
orderLogic.ts narrows the shared ORDER_FIELD_SPEC for PROD: partyId/rate
lose PROD, dueDays/paymentDhara/paymentCombo become ['SO','JW','OUTSOURCE'],
brokeragePct['SO','JW'], passOnPct['SO'], and PROD-only dispatchCity
(Send to city) + promisedDays (Sales promise) leave the spec and are recorded in
ORDER_INTENTIONALLY_HIDDEN; costBookingId (yarn allocation) and remark stay.
wizardStepsFor drops the terms popups for PROD (noTerms = job || PROD).
OrderEntryForm.svelte gates the buyer picker with {#if shows('partyId')},
labels the section "Quality" for PROD, treats production as buyer-less in
partySummary/partyDone, and adds a PROD_DROPPED set so a carried rate /
payment / brokerage / city / promise cannot ride back as a hidden POST.
STOCK_SALES_REQUIRED trims to ['orderDate','brokerName','designId','picks'].
No new field, so no new validation; append-only (past orders keep their booked
values), no edit/delete surface. Proved non-UI with a one-off bun script
(/tmp/opencode/c269-proof.ts) asserting the field/wizard/required/validation
outcomes for sales+production+owner (all PASS), plus bun run check via
scripts/quiet.sh (0 errors, pre-existing warnings) and bunx tsc --noEmit
(no error in the touched files; repo-wide red as usual). Suite / build / deploy
left to the watcher. PLANNING §547.
C270 (owner 2026-10-19: "remove quote, lead, broker, buyer order no., booking
date" on /orders/new/production-order). orderLogic.ts narrows
ORDER_FIELD_SPEC for PROD: partyContractNo/orderDate (COMMERCIAL_KINDS)
and brokerName (BUYER_KINDS) lose PROD, quoteId/leadId become ['SO'];
none leaves the spec, so ORDER_INTENTIONALLY_HIDDEN is unchanged.
wizardStepsFor drops the source-link step for PROD (linksSource = type === 'SO'), leaving source > quality > picks > qty. OrderEntryForm.svelte adds
the five names to PROD_DROPPED and gates the always-posted quote/lead hidden
inputs with {#if shows(...)}. STOCK_SALES_REQUIRED trims to
['designId','picks']; a new STOCK_PRODUCTION_REQUIRED
(deliveryForm,lotSizeMtrPerRoll,rolls) lets a production-role PROD booking
skip the absent buyer's order no, while the order-record completion keeps
requiring all four via the untouched TRADING_PRODUCTION_REQUIRED. No new field,
so no new validation; append-only (past orders keep their booked values), no
edit/delete surface. Proved non-UI with /tmp/opencode/c270-proof.ts (field
visibility per role, wizard = source>quality>picks>qty, required sets; all PASS),
plus bun run check via scripts/quiet.sh (0 errors, pre-existing warnings) and
bunx tsc --noEmit (no error in the touched files; repo-wide red as usual).
Suite / build / deploy left to the watcher. PLANNING §548.
C271 (owner 2026-10-19: "remove panna from wizard" on
/orders/new/job-work). orderLogic.ts narrows the loom wizard step to
fields: ['loomWidth'] (title "Loom width?") and adds panna to
STEP_LESS_ENTERABLE, so it stays a summary row that opens its own single-field
box (summaryRowPopup('panna','JW') === 'field') instead of owning a wizard
step (tapOpensPopup('panna','JW') === -1). OrderEntryForm.svelte drops the
panna control from the loom step and adds the stepLessControl branch (whole
number, min 1, 1/2/3 coins, hint, server error); the hidden form keeps
name="panna", the existing server validation and the stored column untouched.
panna stays optional and history-prefilled, so Save does not gate on it. No
new field, so no new validation; append-only, no edit/delete surface. Proved
non-UI with a one-off bun script over orderLogic.ts (loom fields,
step index, summary popup, field spec; all PASS), plus bun run check via
scripts/quiet.sh (0 errors, pre-existing warnings) and bunx tsc --noEmit
(no error in the touched files; repo-wide red as usual). Suite / build / deploy
left to the watcher. PLANNING §549.
C272 (owner 2026-10-19: "remove loom class master. only one master of loom type
is enough. In loom type width (in cm) has to be typable and not drop down" on
/masters?tab=loom). +page.svelte drops the Loom class tab/section and its
LoomClassForm import, and an old ?tab=loom-class link now falls back to
Parties; LoomClassForm.svelte is deleted. +page.server.ts removes the
createLoomClass action and loomClassErrors, and masters.ts drops
buildLoomClassValues/LoomClassValues; the master-name route drops its
loomClass entry. LoomTypeForm.svelte turns the width select into a typed
numeric input (2-3 digits, cm suffix stripped) and loomTypeErrors accepts any
such width instead of requiring a class row, while the (width, type) duplicate
guard stays. The loom_class table, loom.loomClass column and stored values
are untouched (beams/board/print read them); the width list for legacy rows is
now defaults + loom_type.width + widths on looms. No new field, no schema
change, append-only, no edit/delete surface. Proved non-UI with bun run check
via scripts/quiet.sh (0 errors, pre-existing warnings) and bunx tsc --noEmit (no error in the touched files; repo-wide red as usual). Suite /
build / deploy left to the watcher. PLANNING §550.
C273 (owner 2026-10-19: "OCR and dictation field add in the wizard first pop up"
on /bookings/new). The first wizard popup (trader's booking no.) now renders
BookingCapture.svelte: a speech/typed textarea + Web Speech mic and a
photo/PDF button; OCR reuses /api/capture-ocr (slip kept as a capture draft)
and both paths post to the new /api/booking-understand, whose
bookingUnderstand.ts reads the note/slip against the yarn/supplier/broker/lot/
own-firm masters and returns the booking fields it understood. BookingWizard
applies them only as touched prefills (booking no/date, supplier, broker, firm,
product id, warp/weft, bags, cones, bag wt, rate, lot, payment term, remark);
the existing schema, check-and-save gate and save path are unchanged, no column
or migration was added, and nothing is auto-committed. Values are normalised to
the form's own rules server-side and re-bounded in the wizard. Proved non-UI
with a one-off bun script importing normalizeBookingFields/bookingSystem
(33/33 valid + refusal checks, PASS), bun run check via scripts/quiet.sh
(0 errors, 329 pre-existing warnings) and bunx tsc --noEmit (no error in the
touched files; repo-wide red as usual). No UI test was run. Suite / build /
deploy left to the watcher. PLANNING §551.
C274 (owner 2026-10-19: "put start wizard button" on /bookings/new). The
header now carries a Start the wizard primary button whenever the focus
wizard is closed; tapping it reopens the wizard at step 0 (the C273 trader-slip
capture popup). The wizard still opens by default, and no field, POST name,
validation, schema, migration or save path changed. Proved non-UI with
bunx tsc --noEmit (no error in the touched files; repo-wide red as usual) and
bun run check via scripts/quiet.sh (0 errors, 329 pre-existing warnings). No
UI test was run. Suite / build / deploy left to the watcher. PLANNING §552.
C275 (owner 2026-10-19: delivery start/end, interval and bags per interval in
one focus-entry popup, on /bookings/new). The three delivery wizard steps
fold into one Delivery period? popup: start date, new end date, interval
(coins 3/5/7/10, − / + one day, 1–365) and bags per interval (− / + one bag);
the untouched per-interval count divides the total bags over the inclusive
delivery count (floor((end−start)/interval)+1) and then yields to edits. Adds
the optional booking.delivery_end column (drizzle 0044), server + client
validation (real date, on/after start, start required for a period, bags ≤
total) and the summary/step-index updates; existing blanks and save path
unchanged. Proved non-UI with a bun script against schema.ts (5/5: valid
period, reversed end, end without start, rolled-over date, period without end),
bun run check via scripts/quiet.sh (0 errors, 329 pre-existing warnings) and
bunx tsc --noEmit (no error in the touched files; repo-wide red as usual). No
UI test was run. Suite / build / deploy left to the watcher. PLANNING §553.
C276 (owner 2026-10-19: trader/supplier from the Supplier party master, with an
add-new option in the dropdown, on /bookings/new). The wizard's supplier step
is a daisyUI select fed by party.category = 'Supplier', keeping historic
booking supplier names selectable so old prefills still work; the last option
opens the shared PartyForm (category Supplier) in a popup, and on save the new
name is added to the picker and set on the booking. No new booking field,
column, POST name or save-path change; the booking's supplier stays optional
(max 120) and the party form keeps its existing client + server validation.
Proved non-UI with bun run check via scripts/quiet.sh (0 errors, 329
pre-existing warnings) and bunx tsc --noEmit (no error in the touched files;
repo-wide red as usual). No UI test was run. Suite / build / deploy left to the
watcher. PLANNING §554.
C277 (owner 2026-10-19: a new-broker option in the broker picker that opens the
party master in a popup, on /bookings/new). The wizard's broker list is fed by
party.category = 'Broker' plus brokers created in this picker, and a
+ Add new broker… button opens the shared PartyForm (category Broker) in a
popup; on save the new name is added to the list, set on the booking and the
wizard advances. No new booking field, column, POST name or save-path change;
the booking's broker stays optional (max 120), the server rule requiring a
Broker-category master is unchanged, and the party form keeps its existing
client + server validation. Proved non-UI with bun run check via
scripts/quiet.sh (0 errors, 329 pre-existing warnings) and bunx tsc --noEmit
(no error in the touched files; repo-wide red as usual). No UI test was run.
Suite / build / deploy left to the watcher. PLANNING §555.
C278 (owner 2026-10-19: an add-yarn option in the yarn picker that opens the
yarn master in a popup for new yarn creation, on /bookings/new). The wizard's
product step gains + Add new yarn… below the search box; it layers the shared
YarnForm over the field popup, posting to /masters?/createYarn, and on save
refreshes the page data, selects the new yarn and advances. YarnForm now
accepts the same optional actionBase/refresh/onCancel/showServerError
props as PartyForm and returns the new name with the id; masters usage is
unchanged. No new booking field, column, POST name or save-path change; the
shared form keeps its client + server validation. Proved non-UI with bun run check via scripts/quiet.sh (0 errors, 330 pre-existing warnings, none in the
touched files) and bunx tsc --noEmit (no error in the touched files;
repo-wide red as usual). No UI test was run. Suite / build / deploy left to the
watcher. PLANNING §556.
Form-reader correction memory (owner 2026-10-19: the OCR should learn from its
own missed recognitions, for the sale order, job-work order, outsource contract
and booking). form_memory is fed only from a confirmed save: the form posts
readText + readFields, the save action learns only the fields the user
changed, and the three readers merge the best word hit before resolving names.
The production contract sides read as job-work but key their memory as
outsource; tesseract/the model are untouched and no read ever writes a
business row. Proved non-UI: bun run check 0 errors (336 pre-existing
warnings) and bunx tsc --noEmit adds no error in the touched files. No UI test
was run. Suite / build / deploy left to the watcher. PLANNING §557.
Booking yarn split (owner 2026-10-19): /bookings/new step 5 now asks Yarn
count & type (distinct count+type from the yarn master) and Mill / ticket
(from the mill master), each with a + Add new… option that opens the shared
YarnForm / MillForm in a nested popup. MillForm gained the reusable
actionBase/refresh/onCancel/showServerError props and returns its name.
The pick is stored on the new optional booking.mill_ticket (migration 0045);
old bookings fall back to yarn.ticket, and a legacy yarn seeds the field
untouched-only. Product labels, pack hints, order-yarn grid, booking list,
saved popup and gatepass handoff prefer the booking's mill. Validation both
surfaces: optional zod format 1–40 allowed characters plus a server mill-master
membership check (legacy yarn ticket accepted). Proved non-UI: schema proof 5/5,
bun run check 0 errors and bunx tsc --noEmit adds no error in the touched
files. No UI test was run. Suite / build / deploy left to the watcher.
PLANNING §558.
Outsource price-field order (owner 2026-10-19: "Switch rate per mtr and job rate
fields position in the form"). On /orders/new?type=OUTSOURCE the background
Price section now reads job rate ₹/pick → picks → derived rate ₹/m, and
orderFieldsFor overrides the spec order for OUTSOURCE only so the
check-and-save summary matches. JW keeps C254's rate → picks → job rate and SO
is unchanged; the wizard is untouched (no rate step). The rate stays read-only
and derived from job rate × picks; POST names, validation and the append-only
save path are unchanged. No new field/column/migration/screen/theme. Proved
non-UI: a one-off bun import asserted the three per-kind party orders, a
markup check asserted the OUTSOURCE render order (job rate before picks, rate
after, one copy of each) and the else-branch order, and svelte-syntax
compiled clean; bunx tsc --noEmit adds no error in the touched files. No UI
test was run. Suite / build / deploy left to the watcher. PLANNING §559.
Booking picker tabs (owner 2026-10-19): on /gatepasses/new?kind=YARN_IN (and
the shared DIRECT booking field) the pending booking table now opens in its own
popup from the booking row, with Job work / Yarn booking tabs — the SO
lead/quote picker shape. Reuses OrderTablePicker (top-layer dialog, search,
two tabs with counts, Done footer); the pending set is split on the existing C113
rule (no purchase rate = job work). FocusEntrytable mode gained an optional
groups body (the old inline table stays for any other caller) and
OrderTablePicker prefers a row's explicit label. Selecting still writes the
same bookingPick, carries the same booking fields and advances/closes; no new
field, column, migration or validation change. Proved non-UI: svelte-syntax
clean on the three changed files, bun run check with no error in them,
bunx tsc --noEmit adds no error in the touched files, and a libSQL query showed
both tabs populated (1 job work / 23 yarn bookings of 24 pending). No UI test
was run. Suite / build / deploy left to the watcher. PLANNING §560.
Yarn-inward intimation header (owner 2026-10-19: "Yarn inward intimation number
and system date to be shown above the form. Ui like as in booking form"). On
/gatepasses/new?kind=YARN_IN a booking-style C209 top card now sits between
the page header and the focus form — intimation no. (GP-YYMMDD-XXXX,
"Generated on save") on the left, server system date on the right. The two
static rows that carried the same values inside "Which movement" are removed,
so nothing is duplicated; other kinds and the wizard are untouched. Display
only — no field, column, migration, validation or POST-name change. Proved
non-UI: bun scripts/svelte-syntax.ts clean, bun run check with no error in
the file, bunx tsc --noEmit adds no error there. No UI test was run.
Suite / build / deploy left to the watcher. PLANNING §561.
Yarn-inward GP no. field (owner 2026-10-19: "Add gp no. Field"). On
/gatepasses/new?kind=YARN_IN the focus form now asks an optional GP no
text row after the booking/yarn identity, capped at 64 characters and validated
server-side as GP-style letters/digits/space/-//. It posts the existing
partnerGpNo name into gatepass.partner_gp_no (no migration); the auto
GP-YYMMDD-XXXX intimation number in the C282 top card is unchanged and never
overridden. Proved non-UI: the real zod schema accepts/rejects the 8 cases
(absent, typical, slash form, 64, 65, leading space, internal separators, junk),
svelte-syntax compiled the page clean, bunx tsc --noEmit adds no error in
the touched files. No UI test was run. Suite / build / deploy left to the
watcher. PLANNING §562.
Job-work beam quantity wheel starts on 1 (owner C284: "Beam scroll start from
Right now it is starting from 999"). On /orders/new/job-work the quantity
step keeps the unit and quantity coherent: switching metres↔beams now clears
the prefilled quantity via qtyForUnitSwitch, so the beam wheel opens on its
first slot (1 beam) instead of clamping to the bottom of its 1,000-slot range;
switching back opens metres on 1,000. qtySlotFloor is also clamped to the
wheel top for restored drafts. Proved non-UI: /tmp/opencode/c284-proof.ts
runs the real helpers (5,000 mtr → beams opens on 1, beams → metres on 1,000,
same-unit keeps the value; 8/8 PASS), svelte-syntax compiled the changed page
clean, bunx tsc --noEmit adds no error there. No UI test was run. Suite /
build / deploy left to the watcher. PLANNING §563.
Job-work orders table fields made legible (owner C285: "The table fields are
cramped. Make them visible. Horizontal scroll allowed"). On /orders?type=JW
the tab drops the C140 phone-compact table-fixed layout and renders the
standard wide SmartTable, so booking date, order, quality, party, mtr/beam,
job rate, status and Ops each keep their natural width; the card scrolls
sideways when the row is wider than the screen. Column set, row popup, Ops
marker and Fill details are unchanged; no field, validation or migration.
Proved non-UI: svelte-syntax compiled the page clean, bunx tsc --noEmit
adds no error there. No UI test was run. Suite / build / deploy left to the
watcher. PLANNING §564.
Every screen opens at its own top after navigation (owner C287: "improve UI
ergonomics", no target named). The shell scrolls <main>, but SvelteKit resets
the window, so the previous screen's scroll offset survived route changes. The
layout now binds <main> and, in afterNavigate, scrolls it to the top on a
normal navigation; a URL hash still scrolls its named element into view and
back/forward is untouched. No field, validation, migration, action or theme
changed. Proved non-UI: svelte-syntax compiled the layout clean,
bunx tsc --noEmit adds no error there. No UI test was run. Suite / build /
deploy left to the watcher. PLANNING §565.
C288 — the ERP's colour code is readable now, without breaking the monochrome
decision (owner C288: "color code of the complete erp is not ergonomic", no
target named). Cause measured in the tokens: daisyUI's pale status hues read
at 1.8–2.9:1 on white, body text held the /60 muted tier at 4.1:1, and the
table header strip was 1.04:1 against the white rows — invisible. One CSS
file (src/app.css): body ink to zinc-900, status pinned to the 700 tones
with white content (5.0–6.7:1 as text and on the chip), neutral pinned to
primary, header strip to zinc-100. No markup, field, action, route or theme
changed. Proved non-UI: new bun scripts/color-contrast.ts passes all 14
WCAG checks (reads the shipped tokens and header-strip rule, exits 1 on a
regression), lightningcss compiles the stylesheet with
the new tokens, tsc --noEmit adds no error. No UI test
was run. Suite / build / deploy left to the watcher. PLANNING §566.
C288 loop (7 sessions in 18 minutes) — the row line was refusing its own live
host. scripts/deploy.sh gated deploys to the Pi/VPS checkouts only, so on the
phone (/home/userland/trading, the host now serving trading.tynolabs.com via
tunnel phone-tyno) every watcher deploy exited 3, the row stayed claimed, the
next tick un-stuck it, and the identical session ran again. Fixed in two files:
the phone joins the deploy-host list plus a --can-deploy probe (one source of
truth for "may this checkout serve?"), and scripts/row-watch.sh now (a) asks
that probe and, off a deploy host, commits + pushes and leaves the row
committed instead of trying to serve, (b) counts consecutive failed
integrations in an untracked .row-watch-fails ledger and holds the row
blocked-owner at ROW_WATCH_FAIL_MAX (2) rather than re-running it, and
(c) fixes the idle branch reading $? after an if, which made the catch-up
deploy unreachable. No app code, field, route or data change. Proved non-UI:
deploy.sh --can-deploy = 0 here / 3 from a scratch checkout, deploy.sh ""
now stops at the usage check, the ledger exercised from the shipped functions
(bump, independence, clear, absent-file), bash -n clean, watcher status
runs, phone build + live-check 81/81 green. No UI test was run. PLANNING §567.
Watcher: the stale row-watch lock starved the board (2026-10-23)
Symptom: C289 was never started. svc status swarm said active, but every
~3 s the log got another row-watch is active — exiting with no watcher alive.
Cause: scripts/row-watch.sh held its single-watcher flock on hardcoded fd 9,
which it passed to every child. work_one runs deploy.sh, and that restarts
project@trading and queue-watch — both inherited fd 9. flock lives on the open
file, not on the process, so the restarted services kept the lock after the watcher
that took it had gone; the next tick's flock -n 9 failed and the line stopped.
Three processes were holding it: svc __supervise trading, the app process under
it, and svc __supervise swarm.
Fix (script only, no app code): take_lock in scripts/row-watch.sh —
the lock file records the owner pid, the fd is opened append (>>, so reading it
cannot wipe the owner), a lock whose recorded owner is not a live row-watch.sh is
reclaimed on a fresh inode (the stale fd keeps its dead inode), and every
long-lived child (opencode run, both deploy.sh calls) is spawned 9>&-.
row-watch.sh lock takes/reports the lock; status prints its state.
Proof: scripts/row-watch-lock-check.sh reproduces the leak (unrelated
long-lived fd holder, no watcher) — fixed code reclaims and marks it, pre-fix code
prints another row-watch is active — exiting (verified against git show HEAD:scripts/row-watch.sh lock block). Live: the running watcher logged
stale row-watch lock ... reclaiming + row-watch lock reclaimed and started
C289 at 03:11:05; the row session's timeout child was checked with readlink /proc/<pid>/fd/9 = none, so the fd no longer reaches services.
C289 (2026-10-23): the dashboard scrollbar becomes a map
Owner: "we need a smartly designed scrollbar to make user instantly understand
layout of dashboard". Read as both halves (Q216/A216, --assume recorded on the
row): one monochrome scroll skin app-wide + a dashboard rail whose ticks are the
board's nine sections.
src/app.css: every scroll region gets a transparent track and a 10px
rounded zinc-400 thumb (2px transparent border), zinc-600 on hover, zinc-900
while dragging; scrollbar-width: thin + scrollbar-color for Firefox. No
token, theme or layout change.
src/lib/scrollMap.ts (new, pure): railThumb / railTicks / activeTick
place the thumb and the section ticks on one shared lane, so a tick sits exactly
where the thumb lands when that section tops out.
src/lib/components/ScrollMap.svelte (new): 56vh rail at the right gutter,
one 1px tick per section, the reader's section inked zinc-900, name on hover,
click scrolls there. Measures the shell's main scroller; desktop only; hidden
while the page fits.
src/routes/+page.svelte: dash-sec-<n> ids on the section headings,
lg:pr-8 lane, rail mounted outside the fade-in wrapper (a transform ancestor
captures fixed).
Proof: bun scripts/scroll-map.ts — 18 geometry/wiring checks, ALL PASS (its
first run caught the bottom-lane clamp). app.css block parses clean under
lightningcss (0 warnings); tsc --noEmit and bun run check add no error in the
touched files. No UI test; suite/build/deploy left to the watcher.
PLANNING §568.
E1 (2026-10-23): a row closes at commit time, live: records the build that serves it
Model change (owner ruling this session): C131 made done mean "served by
the live build", so every batched row sat in committed until a full deploy.
Now the row closes done as soon as its commit is pushed, and a separate
live: <sha> field on the row records the build that serves it.
queue-next.ts: live field (parse/render), --set --live with sha
validation, --stamp-live, and the C131 downgrade removed.
row-watch.sh: done-at-exit + --live in the same call when the deploy that
just ran is the one that built it; sweep_sessions moved from the dead
OpenCode-1 HTTP API (/session now answers HTML — 14 "session sweep skipped")
to opencode session list/delete, which is what left "row C289" listed after
the row read done.
deploy.sh calls --stamp-live in step 4c; /queue renders live <sha> /
awaiting build beside a done row.
Proved non-UI: bun run check clean (rc 0), --set C289 --live zzz rejected
rc 1, --set C289 --status done stays done, --stamp-live stamped 159/159
rows, and the board parser read C289 status=done live=3fe635a3. No UI test.
C290 (2026-10-23): the phone gets a scrollbar it can actually grab
Owner: "draggable scrollbar on phone?". The C289 rail was desktop-only
(hidden … lg:block) and Android draws an overlay bar that cannot be touched, so
a phone had no bar at all — read as "one rail at every width, and it drags"
(Q217/A217, --assume on the row).
src/lib/scrollMap.ts: railScrollTop — the pointer→scrollTop map the
rail needs: finger position on the track, thumb centred under it, clamped to both
ends; returns 0 when the page fits or the track has no height.
src/lib/components/ScrollMap.svelte: the rail no longer hides below lg
(right-1 on phones, sm:right-4); pointerdown captures the pointer and
pointermove sets main.scrollTop, pointerup/cancel releases; touch-none
keeps the gesture ours; ticks stay click-to-jump (pointerdown on a tick is
ignored so the button still fires). Track/thumb look unchanged.
Proof: bun scripts/scroll-map.ts — 9 new drag cases (top / middle /
bottom / both clamps / page-that-fits / zero-height track / thumb centre) plus
four wiring assertions (no lg:block, pointer handlers, touch-none,
railScrollTop), ALL PASS. bun scripts/svelte-syntax.ts compiles the
component clean; bunx tsc --noEmit adds no error in the touched files. No UI
test; suite/build/deploy left to the watcher.
PLANNING §570.
C291 (2026-10-23): the dashboard rail takes a finger, its ink matches the system bar
Owner: "current draggable scrollbar in dashboard can hardly be grabbed by a
finger. visually it must be very subtle. but, it should match system's scroll".
The C290 rail was an 8px track with a grey bg-base-300/70 slab — a mouse
handle that is neither subtle nor like the phone's own bar. Read (Q218/A218,
--assume on the row) as: grow the lane, shrink the ink.
src/lib/components/ScrollMap.svelte: the nav sits right-0 on a
w-11 sm:w-6 lane (44px on phones, 24px from sm), so the whole strip takes
the pointer; the bar is a 6px pointer-events-none element pinned right-0.5
in zinc-400, zinc-600 under the pointer, zinc-900 while dragging, on a
transparent track — the same palette as the src/app.css scrollbar skin. Tick
pips moved from -left-* to right-2 sm:right-1.5 so their 24px tap targets
sit clear of the bar; the drag map and click-to-jump are untouched.
scripts/scroll-map.ts: 8 new guards (lane width, no grey slab, 6px ink,
edge placement, zinc-400, hover/drag darkening, pip clearance) + 2 cross-checks
that src/app.css uses the same #a1a1aa on a transparent track.
Proof: bun scripts/scroll-map.ts ALL PASS (44 checks); on the pre-fix
component the same script fails 8, so the guards bite.
bun scripts/svelte-syntax.ts compiles the component clean; bunx tsc --noEmit adds no error in the touched files. No UI test; suite/build/deploy
left to the watcher.
PLANNING §571.
C292 (2026-10-25): daisyUI is the only kit, and the audit says so
Owner: "strictly use daisyUI throughout https://trading.tynolabs.com/. no
other UI libraries should be used unless daisyUI can't do it". The tree was
already daisyUI-native, so the row lands as a rule plus a guard plus the drift
it exposed. Read (Q219–A221, --assume on the row) as: icons stay (daisyUI
ships none), paper sheets keep their .tbl grid, layout buttons keep
utilities but nothing is left unstyled, and daisyUI-less controls sit in one
exemption list that fails when stale.
scripts/daisy-audit.ts (new): scans with a real tag tokenizer
(=> puts > inside attributes, which fools a regex) and checks five
surfaces — libraries (package.json + bare src/ imports), stylesheets
(app.css = tailwind + daisyUI, one light theme), html (no external UI
asset), controls (every visible input/select/textarea/table/progress has
its daisyUI class, excusing <label class="input"> wrappers and hidden
inputs), buttons (none unstyled outside daisyUI's modal-backdrop close).
15 fixtures run before the real scan; the paper/exemption lists fail when
stale.
src/routes/+layout.svelte: the header search — the last hand-rolled
text box — becomes <label class="input input-bordered input-sm"> + bare
<input class="grow">, matching ListToolbar, plus an aria-label.
src/routes/+page.svelte: dashboard grade-mix bars <div style="width:…%"> → <progress class="progress progress-…">, same tone map.
10 success hint strips (BookingWizard, OrderEntryForm, NewWizard,
leads/new): rounded-box border border-success/40 bg-success/10 →
alert alert-success alert-soft, same padding/label/clear button.
Proof: bun scripts/daisy-audit.ts ALL PASS; adding bootstrap to
package.json exits 1, and reverting the header search to raw utilities exits
1 (both restored). bun scripts/svelte-syntax.ts compiles the six touched
.svelte files clean; bunx tsc --noEmit adds no error in them. No UI
test; suite/build/deploy left to the watcher.
PLANNING §572; rule written into docs/UIPATTERN.md (hard rules).
C293 (2026-10-25): the dashboard's phone face is a row of cards you swipe
Owner: "dashboard can be card swipes left and right on phones? current look
seems to be too chaotic. decide and implement the best UI for the dashboard".
Read (Q222/A222, --assume on the row) as the cards inside a section sliding
sideways, not the sections becoming swipe pages — the C289/C290 rail already
maps the sections and the at-a-glance board must stay one page.
src/routes/+page.svelte: the KPI card body is now
{#snippet dashCard(ca: BoardCard)} (one definition, both surfaces). On
phones each section is carousel w-full gap-3 sm:hidden with
carousel-item w-[86%] — 29 cards became 9 swipeable rows instead of a
29-card stack, next card's edge peeking as the swipe cue; sm and up keeps
the same hidden gap-4 sm:grid sm:grid-cols-2 lg:grid-cols-3 grid. Section
head gained a phone-only N cards · swipe → hint; each carousel is a
labelled role="group".
docs/UIPATTERN.md: the dashboard pattern records the phone carousel and
the shared card snippet.
Proof: real render, no browser — a script signed in on a dev server and
read /: 9/9 checks PASS (9 sections, 9 carousels, 9 grids, 29
carousel-items, labelled groups, sm:hidden hint, 58 card anchors = 2 per
card, no phone stack left); 6 of the 9 fail on HEAD's page. bun scripts/svelte-syntax.ts clean, bun scripts/daisy-audit.ts ALL PASS,
bunx tsc --noEmit adds no error in the touched file. No UI test; suite /
build / deploy left to the watcher.
PLANNING §573.
C294 (2026-10-26): the dashboard shows one scrollbar, not two
Owner: "remove the extra scrollbar in dashboard". The dashboard is the only
screen that mounts the C289 rail, and the rail's ink sits 2px off the right edge
— directly over the shell's native main scrollbar that C289 also skinned. Two
bars moved together there. Read (Q223/A223, --assume on the row) as: the rail
is the dashboard's scrollbar (owner vocabulary since C289/C291, the only bar a
finger can grab on a phone — C290 — and it carries the section map), so the plain
bar under it is the extra one.
src/lib/components/ScrollMap.svelte: the rail's nav carries
data-scroll-rail, so the sheet knows the screen has a rail without naming
the route.
src/app.css: main:has(nav[data-scroll-rail]) → scrollbar-width: none
and ::-webkit-scrollbar → display: none. The app-wide 10px zinc thumb skin
is untouched on every other screen; the rail only renders while the page
scrolls, so a page that fits still shows no bar.
docs/UIPATTERN.md: the dashboard pattern records the one-bar rule.
Proof: bun scripts/scroll-map.ts ALL PASS (51 checks, 9 new: the marker,
the native bar stepping aside in both engines, the skin elsewhere, only the
dashboard mounting the rail, no second vertical scroller, and a specificity
calculation showing (0,1,2) beats *(0,0,0) and ::-webkit-scrollbar(0,0,1)); deleting the two rules fails 2 of them. lightningcss emits the
rule for both engines with no new warning, bun scripts/svelte-syntax.ts
compiles the component clean, bun scripts/daisy-audit.ts ALL PASS, bunx tsc --noEmit adds no error in the touched files. No UI test; suite / build /
deploy left to the watcher.
PLANNING §574.
C295 — dashboard keeps the og scrollbar. Owner follow-up to C294: "keep
only the og scrollbar in dashboard". C294 had hidden the shell's native bar
under the rail's ink; now the ink goes instead and the app-wide skinned bar is
back, so the dashboard draws one bar (the same 10px zinc thumb as every other
screen) and the rail is a pure section map + invisible 44px drag lane.
src/app.css loses the main:has(nav[data-scroll-rail]) pair,
ScrollMap.svelte loses data-scroll-rail and the ink div (drag, ticks, jump
and role="scrollbar" kept), docs/UIPATTERN.md records the rule. Proof:
bun scripts/scroll-map.ts ALL PASS (46 checks; 5 of them fail on the pre-fix
sources), svelte-syntax + daisy-audit clean, bunx tsc --noEmit adds no error
in touched files. PLANNING §575, Q224/A224.
C296 — the sidebar shows one work group at a time, and every screen has one
home. Owner: "we need strict organizational segregation for the UI, and
have minimal options in the sidebar. maintain mobile and desktop first." The
nav map moves into src/lib/nav.ts (NAV / UTILITY / HIDDEN + the pure
isActive / sectionFor / visibleRows), so +layout.svelte holds no hrefs
(50 inline literals → 0) and draws one accordion: Dashboard, Masters and the
eight work groups, with only the group that owns the current screen open
(/orders/new → Orders, /gatepasses/abc → Gatepasses) or the one last
tapped; headers are aria-expanded buttons, links carry aria-current. 45
rows always → 10 at rest, 22 with Production open, same accordion on the
phone drawer and the desk sidebar; the collapsed desktop rail still shows every
icon. A screen can no longer float or appear twice: nine flow screens
(sizing, winding, beam-end, inward, fabric-in, pending, queue, search, signin)
are declared HIDDEN with a reason, and the utility "Reports" (an upload bin)
is renamed "Report files" so nothing shares a name in the sidebar.
scripts/nav-audit.ts (new, 16 checks) guards all of it and exits 1 on the
pre-fix shell (4 FAILs). docs/UIPATTERN.md records the standard. PLANNING
§576, Q225/A225, Q226/A226.
C297 — the shared surfaces read as a picture plus a word (owner: "readibility
and UI ergonomics needs an udpate throughout the website, maybe use cards more
so that website can be used by less literate people as well and use icons
wherever it feels necessary", no target named). Rather than restyle 40 pages,
the four surfaces every screen shares were fixed. ListToolbar (19 list
screens) announces its filter group with a Funnel badge, names every filter
select for screen readers, and replaces the raw ⬇ CSV / × clear / ‹ prev /
next › glyph labels with lucide icon + word, reading "Showing 1–50 of 320" /
"No matches" and "page n of m". RecordDialog (the row popup behind ~20 list
screens) renders each field as a card in a two-column chip grid (11px uppercase
caption over a semibold value), gives the action row a hierarchy — first action
primary, the rest outline, a worded X Close last — and lifts phone taps to
44px (min-h-11 sm:min-h-0). SmartTable's default row action becomes a
btn btn-outline btn-xs with a forward ChevronRight instead of a bare text
link, and /pending — the last action-family screen still drawing two zebra
tables — now uses the guard/transporter queue-card pattern: one card per parked
lot or booking (id + status badge, yarn, supplier/mill, "N bags · kg", the same
form beneath). No route, field, validation, query, action, schema, theme or role
rule changed; every POST name, hidden id, contract picker and use:enhance is
as it was; dense multi-column tables stay where the job is comparing numbers
(C285). Proven non-UI: new scripts/readability-audit.ts ALL PASS (24 checks)
and 16 FAILs on the pre-fix tree, svelte-syntax compiles the four files clean,
daisy-audit ALL PASS, bun run check exits 0 with no new diagnostic. No UI
test was run. Suite / build / deploy left to the watcher. PLANNING §577.
C298 — "boost lighthouse scores", no category or page named, and no Chrome on
this host to run Lighthouse itself, so the row took the half of the score a
server-rendered app decides in its head and headers. Measured on the live
server first: /signin had no <title> at all (only the print and docs
routes ever set one), no meta description, no theme-color, no color-scheme, a
render-blocking third-party Inter stylesheet, and static/ served with no
cache-control at all. src/lib/nav.ts gains titleFor(pathname, search) — the
sidebar row names the screen (/orders?type=SO → "Sale"), the owning row names
a deep screen, a declared SCREEN_TITLES map names the engines with no row
(sign-in, search, pending, queue, sizing, winding, beam-end, inward, fabric-in,
docs, imports, upload, backup, report files) and the path is the last resort,
everything branded · Trading; the root layout renders it in one
<svelte:head><title>, and Svelte's head dedupe keeps a print/docs page's own
title. src/app.html gains a 102-char description, theme-color #1d3a5f (same
as the manifest), color-scheme: light and Inter off the critical path
(media="print" + onload swap, <noscript> fallback; same URL, still
display=swap). New src/lib/staticCache.ts + a hooks.server.ts pass give
static assets public, max-age=86400only when the response has no policy
— hashed _app/immutable/** keeps the adapter's immutable year, page loads stay
uncached and /upload-file is user data. No route, field, validation, query,
action, schema, theme or role rule changed; light mode still pinned. Proven
non-UI: new scripts/lighthouse-audit.tsALL PASS (22 checks) and 18 FAILs
with exit 1 on the pre-fix tree; dev-server curls showed /signin named "Sign
in · Trading" with the description and theme-color, /docs/APP still using its
own title, and dev's cache-control: no-cache untouched; live pre-fix had 0 of
those markers and no cache-control on /favicon.svg. svelte-syntax clean,
bunx tsc --noEmit adds no diagnostic. No UI test was run. Suite / build /
deploy left to the watcher. PLANNING §578, Q228/A228.
C299 — dashboard is chaotic. The home board drew all nine sections and 29
cards at once (nine stacked phone carousels, create shortcuts at the bottom of
a ~4,350px equal-weight stack, attention strip under the period filter). Now:
one top-to-bottom order (header with the single typed New dropdown → needs
action → dates → 14-day pulse → Boards — tap one to open it), nine native
<details class="collapse collapse-arrow" name="dash-sec"> rows with exactly
one open — the row with the most bad(×2)/warn KPIs — and {n} red /
{n} watch / {n} cards on every closed summary, so collapsing loses no
signal; the open row keeps C293's phone carousel and the sm+ grid. No route,
action, field, validation, query, schema, theme or role rule touched; cards
still server-render inside every row (works with JS off). Proven non-UI: new
scripts/dashboard-audit.tsALL PASS (23 checks), 10 FAILs on the
pre-fix file; dev / curled with a signed-in cookie returned 9 dash-sec rows
with exactly 1 open (Warp prep, 1 red · 3 watch), the 8 icon+word New
entries, no Quick add, 29 cards × 2 surfaces and the documented order;
bun run check green, bunx tsc --noEmit adds no diagnostic. No UI test run.
Suite / build / deploy left to the watcher. PLANNING §579, Q229/A229.
C300 — show wizards or wizard options only if necessary for form filling.
The shared entry shell src/lib/components/focus/FocusEntry.svelte (gatepass,
inward, movement, sizing, winding, stocktake, sample, beam-card, beam-receive)
opened its one-field wizard at step 0 every time, so steps a carry had
already answered (winding's gate filling winder/set no./expected kg, a
movement's reason, a booking's picks) were asked again and a fully answered
form still popped a wizard with nothing to ask. Now the wizard opens on the
first step that still needs filling and does not open at all when every
step is answered; Next walks forward over carried steps. New pure
src/lib/focusWizard.ts (firstNeededStep, wizardNeeded,
focusStepFilled) holds the rule; every step stays reachable (and confirmable)
from its summary row, so nothing became un-editable. Signed booking / tippan /
order / quote step lists untouched; no route, action, field, validation,
schema, query, theme or role change; light mode pinned. Proven non-UI: new
bun scripts/focus-wizard-audit.tsALL PASS (15 checks), and 3 FAIL
(exit 1) against the pre-fix shell copied from HEAD into a temp root;
svelte/compiler compiles the shell with 0 errors; bunx tsc --noEmit adds
no diagnostic in the two files and bun run check reports 0 errors. No UI
test was run. Suite / build / deploy left to the watcher. PLANNING §580.
C301 — New gatepass opens the kind of the screen, no type dropdown./gatepasses header: the New gatepass dropdown menu of the eight movements
(NEW_KINDS, <details class="dropdown">) is replaced by one plain link to
/gatepasses/new?kind=<the screen's kind filter> (plain /gatepasses/new on
the unfiltered All screen); the empty-state button carries the same href. The
eight kinds stay reachable from the sidebar kind rows (src/lib/nav.ts), so no
movement is stranded; list body, tabs, filters, totals, dialog, status actions
and all server logic untouched; no field added → no new validation. Proven
non-UI with a signed-in dev-server fetch on 127.0.0.1:5199 (header anchor href
per screen, form badge reads the carried kind), scripts/svelte-syntax.ts
clean and bunx tsc --noEmit adding no error under gatepasses. PLANNING §581.
C302 — yarn-in says warp or weft, as a tag that may change later./gatepasses/new?kind=YARN_IN gains one optional Yarn use (warp / weft) row
(focus-shell choice mode: Warp / Weft / Both — the booking wizard's own
vocabulary) that a picked booking prefills from its warp_weft_tag and the user
can retap. It posts warpWeft, zod-validated (z.enum(['warp','weft','both']),
skipped row → unset, junk → visible field error), rides the gatepass creation
event (no gatepass column, no migration) and createLotsFromGatepass stamps it
onto the delivered lot.warp_weft, which /lots and the lot record already
show; the record page reads it back as Yarn use (warp / weft). Nothing enforces
the tag — a warp lot can run as weft later, corrections are appendages, and no
edit screen was added; the other seven kinds are untouched. Proven non-UI:
bunx tsc --noEmit adds no error under gatepasses/lots.ts, and a throwaway
script on a copy of data.db parsed the real form schema (accepts
warp/weft/both, '' and absent unset, junk rejected) and ran the real
createLotsFromGatepass for payload warp / weft / none → lot warp /
weft / default weft: 9/9 PASS. No UI test was run. Suite / build / deploy
left to the watcher. PLANNING §582.
C303 — a yarn inward is picked from three reference sources, each showing kg in / kg to be received./gatepasses/new?kind=YARN_IN's picker (C281) now opens three tabs in the
owner's order — SO / production order, yarn booking, job work — all
three always present. Every kg figure comes from the quote form's own engine
(computeDesignWeights, docs/COSTING.md §5): order mtrs × the quality's kg/m,
one line per warp row and per weft row (multi-warp / multi-weft never merged,
warp at the costing weight, weft with waste). The SO / production tab lists every
open SO and open PROD order with received / required kg, a floored pending
(never negative) and its per-yarn requirement; the yarn-booking tab shows
received / booked kg per booking; the job-work tab adds the order's weft
requirement. Received is this form's own receipt side — a YARN_IN/DIRECT
gatepass carrying bookingId counts against that booking and one carrying
source_order_id against that order, each at its own net kg or, when G0 recorded
only bags, its bags × the booking's per-bag weight. Picking still carries a
booking id and posts exactly the fields it posts today; an order row carries the
open booking against it (or the order alone). No schema change, no migration, no
edit/delete screen, and no other kind's form touched. Proven non-UI: bunx tsc --noEmit adds no error under gatepasses, bun run check shows the same 342
pre-existing warnings, and a throwaway script on a copy of data.db ran the
real load with fixtures (an open 500-mtr SO, a beams JW, warp/both bookings,
four receipts) and read back 43.266 kg warp-only, 104.772 kg warp+weft,
Warp 40 18.5 · Warp 20 24.7 · Weft 20 61.5 kg, 61 kg received, 43.8 kg pending,
— on beams and received / booked on both booking tabs; the two shipped
helpers executed from source: 27/27 PASS. No UI test was run. Suite / build /
deploy left to the watcher. PLANNING §583.
C304 (/gatepasses/new?kind=YARN_IN, owner): the yarn identity is AUTO (count + type + mill name, static) only when the picked reference is a rated yarn booking; an SO / production order row, a rate-less job-work booking and an order carried with ?orderId= now render Yarn count / Yarn type / Mill name (countYarn / yarnTypeCode / ticket) as required user picks, join the wizard only when they render, and the order pick clears a stale booking yarn. Server: yarnTypeCode is now a zod optional enum over the yarn-master vocabulary, and a job-work YARN_IN must post count + type + mill (visible 400 field errors, booking's own yarn master still fills count/ticket); the three names join BOOKING_HIDDEN.YARN_IN. No new column/migration/screen. Proved non-UI on a data.db copy by POSTing the real create action + schema: job-work without yarn 400 with the three field errors, with 40/CW/MILL-A saves and stores them, rated booking saves and inherits 61/Sanathan, forged type 400 — 11/11 PASS; tsc adds no diagnostic in the touched files, svelte-syntax clean. PLANNING §584. Suite / build / deploy left to the watcher.
C305 (/gatepasses/new?kind=YARN_IN, owner): the "Gatepass uploads & dictation (you can skip)" section is now just Gatepass photo (you can skip) with the one image 【PH】 row (Add / Replace + file name). Removed the ocrDoc, document and dictateText rows and all client code only they used (ocrGatepass(), fillFromDictation(), their four states, the uploadPhoto/onfield id branches, the values entries, the three BOOKING_HIDDEN.YARN_IN names and the ocrMsg line). No schema change, no migration, no edit/delete screen; the zod fields stay optional and /gatepasses/[id]'s read-only card still shows legacy uploads, so old records read exactly as before and no other kind changed. Proved non-UI: bunx tsc --noEmit adds no error under gatepasses/new, bun run check same 342 pre-existing warnings, and a signed-in curl of a throwaway dev server (cookie minted from the session row + HMAC) rendered ?kind=YARN_IN with "Gatepass photo" ×2 and no "/uploads/", "OCR the gatepass received", "Gatepass document (saved)" or "Dictation", while ?kind=YARN_OUT still showed "Photo & vehicle" / "Vehicle no" / "Transport" — PASS. No UI test run. Suite / build / deploy left to the watcher. PLANNING §585.
C306 (/gatepasses/new?kind=YARN_IN, owner): the OCR detection is now a wizard step inside the pop-up, the New Orders screen's shape, instead of a row in a section. FocusEntry gains mode: 'ocr' + an ocrPanel snippet (it renders the caller's panel as that step; the shell stays generic), the new GatepassOcrRead card posts the trader's gatepass to the existing /api/capture-ocr (capture-first, never commits) and hands back the count, yarn type, mill name and bags (+ vehicle/transporter, still-mounted POST names on G0), and #lib/gatepassOcr.ts maps the reader's "40 CW" onto the form's own count + type rows and sanitises to the server's zod rules (digits for bags, the yarn-master type vocabulary, the plate's character set and 24-char cap). YARN_IN's wizard now starts on gatepassRead while the form is fresh (C300's carry rule kept; a carried form still reaches it from the summary row), the C305 image 【PH】 row and every other kind are untouched, and there is no new POST name, column, migration or edit/delete screen. Proved non-UI: tsc adds no diagnostic in the touched files (same 139 pre-existing), bun run check same 342 pre-existing warnings, bun scripts/gatepass-ocr-audit.ts12/12 PASS on the mapping, and a signed-in curl of a throwaway dev server rendered ?kind=YARN_IN 200 with Step 1 of 4 + the data-gatepass-ocr card + the Read the gatepass row while ?kind=YARN_OUT 200 has zero data-gatepass-ocr and its own photo/vehicle section. No UI test run. Suite / build / deploy left to the watcher. PLANNING §586.
C307 (/gatepasses/new?kind=YARN_IN, owner): the wizard now walks the owner's seven points in order — 1 the OCR read card, 2 warp/weft, 3 the no. of bags, 4 the SO / production order / yarn booking / job-work pick, 5 the yarn (count + type + mill name), 6 the pickup location, 7 the trader's GP no. WIZARD_IDS.YARN_IN is reordered to that list (the shell already walks it in array order; WIZARD still drops ids with no rendered row, so a yarn booking's static yarn row is never asked and an SO order / job work asks its three picks at point 5); a field the OCR read filled is not asked again (C300's step rule: points 3, 5, 6 and 7 skip while answered, points 1, 2 and 4 always show, since nothing fills them), and because bags and warp/weft are now asked before the reference pick, pickBooking fills them only when the form has none yet instead of overwriting what the operator typed or the read filled. The partnerGpNo row is relabelled Trader's GP no (POST name, zod rule and event ride unchanged). No field added, no POST name, column, migration, edit/delete screen or other kind touched; the C113 reference gate is unchanged. Proved non-UI: bunx tsc --noEmit adds no diagnostic in the touched files (same 139 pre-existing), bun scripts/svelte-syntax.ts clean, and the new bun scripts/gatepass-wizard-audit.ts — which reads the form's real wizard list and runs the real firstNeededStep/focusStepFilled rule over it — 28/28 PASS (order 1–7, every step has a row, cold form opens on the read card, a read's bags/yarn are skipped, location then GP no close the flow, no clobber on a pick). No UI test run. Suite / build / deploy left to the watcher. PLANNING §587.
C308 (/gatepasses/new?kind=YARN_IN, owner): a Verify fields button at the top of the yarn-in form pins the uploaded gatepass in a frozen pane on the top third of the screen while the rows are checked against it — sticky top-0 z-30 block, h-[33vh] inner box, − / + zoom (50–400% width in 25% steps) with the photo area panning inside the pane, a small ✕ that closes it and leaves the form exactly as it was, and ?verify=1 to reopen it after a reload. The pane shows the image row's upload (/upload-file/<name>?bin=screenshots) when there is one, else the file the read step saved (/capture-img/<name>), a PDF in an iframe, and a "no gatepass yet" line when there is neither. Both the button and the pane are gated on YARN_IN, so the other seven movements are unchanged; no new POST name, column, migration, route, server action or edit/delete screen. Proved non-UI: svelte-syntax compiles the page clean, tsc adds no diagnostic under gatepasses/new, and a signed-in throwaway dev server on a data.db snapshot rendered ?kind=YARN_IN 200 with the toggle and no pane, ?kind=YARN_IN&verify=1 200 with the pane + zoom + close + no-photo note, and ?kind=YARN_OUT 200 with neither — PASS; the two photo URLs were exercised on the same server (upload → /upload-file/<name>?bin=screenshots = 200 image/png). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §588.
C309 (/gatepasses/new?kind=YARN_IN, owner): the reference table now shows oneKgs pending / required column per tab — count-wise (Warp 20 18 kg · Weft 20 61 kg · pend 43 / 104 kg) when a quality has several warp/weft yarns, — when a figure cannot be computed — and the inward takes several references at once: PickerGroup.exclusive + multi rows in OrderTablePicker make the SO / production order and yarn booking tabs additive and mixed while the job-work tab is picked alone and clears the others (DIRECT and the orders screens keep their single pick). The first pick stays the form's one reference (yarn identity, trader, posted bookingId), the picks live in bookingRefs and the row is renamed References (SO / production order + yarn booking + job work). A live box split ({#snippet refSplit} → FocusEntry.pickerExtra) divides the gatepass's bags equally across the picks up to each row's pending, with a −/1 bag/+ stepper, the kg apportioned and the further pending per row; the pure rules live in src/lib/yarnSplit.ts (splitBoxes / parseAllocation / allocationError). Validation both surfaces: the box input is numeric and clamped to its cap client-side, and the new allocation POST name is a length-checked zod field the create action checks against real open refs (unknown id, duplicate, job work mixed, more boxes than the gatepass — each a visible 400), stored append-only on the creation event. No new column, migration, route or edit/delete screen; the other seven kinds unchanged. Proved non-UI: bun run check exit 0 (same 345 pre-existing warnings), tsc adds no diagnostic in the touched files (same 139 pre-existing), a throwaway dev server on a copy of data.db rendered ?kind=YARN_IN 200 with the new row and POSTed the real ?/create — over-box 400, job-work-mixed 400, unknown-ref 400, valid mixed 303 with allocation on the event and bookingId unchanged — and the pure rules ran 15/15 PASS. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §589.
C310 (/masters, owner): the yarn master shows its Mill / ticket again, as a picker of the mill/ticket master instead of the hidden input C172 left — YarnForm takes mills (the rate-board tickets + mill rows from load) and renders a daisyUI select name="ticket" whose options are those values plus the yarn's own current mill (backend spelling kept), blank allowed so the booking wizard's quick-add yarn still works before its step-5 mill pick; the masters record modal lists Mill / ticket, and the yarn name is count + type + mill again (buildYarnValues unchanged, now fed). Validation both surfaces: client MILL_RE check with a visible error, server yarnErrors() same rule on create and update (trim + re-set, plain-word 400, nothing stored); membership deliberately not enforced so an old yarn's delisted mill stays saveable. No new column, migration, route or edit/delete screen, other tabs untouched. Proved non-UI: bun run check exit 0 with the same 345 pre-existing warnings, bunx tsc --noEmit same 139 pre-existing errors (none under masters), and the new bun scripts/yarn-mill-audit.ts drives the real createYarn/updateYarn/load on a throwaway data.db copy — create with a master mill stores + names 61 CW Sanathan, blank saves 62 CW, BAD(MILL) 400 on create and edit, valid edit renames 61 CW NITIN, load offers the master mills — 7/7 PASS. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §590.
C311 (/masters, owner): a count master — new count_master table (drizzle/0046: id, value real, active, timestamps) whose list is the distinct yarn.count values plus hand-added rows (de-duped by value, ascending), so a count already on a yarn appears by itself; a Counts tab beside Yarns (/masters?tab=count, badges + Add count dialog via the new CountForm.svelte, modal/toast/highlight like the mill and weave tabs, no edit/delete screen) and buildCountValues beside the other builders. Linked to the yarn master's Count field as an offer, not a gate (Q238): YarnForm takes counts: number[] and the required Count input carries a <datalist> of the master (current value included) while a typed number still saves — a strict picker would block the first yarn on a fresh DB and the booking wizard's quick-add yarn, and a typed count joins the master through its own yarn (C310's reading for the mill). Design warp/weft counts are not projected (Q239). Validation both surfaces, one rule (COUNT_RE: > 0, ≤ 4 digits, ≤ 2 decimals): the count form and countMasterErrors reject blank / abc / 0 / -5 / 61.555 / 12345 with a visible error and store nothing, a duplicate is a 409 (inline warning before submit), and the yarn count — previously only "> 0" — now applies the same rule on create and edit. No column added to existing tables, no route. Proved non-UI: bunx tsc --noEmit same 139 pre-existing errors (none under masters/CountForm/the schema addition), svelte-syntax 3/3 clean, and bun scripts/count-master-audit.ts runs the real createYarn/updateYarn/createCount/load on a throwaway data.db copy — projection, hand-add, duplicates (47 vs 47.0, and a yarn's 61), six bad values on both masters, an edit to 47.5 joining the master — 19/19 PASS. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §591.
C312 (/masters, owner): a type master — new type_master table (drizzle/0047: id, code, label, active, timestamps) whose list is the distinct yarn.type_code values plus hand-added rows (de-duped case-insensitively), so a type already on a yarn appears by itself; a Types tab beside Counts (/masters?tab=type, badges CCW · CCW combed compact + Add type dialog via the new TypeForm.svelte, modal/toast/highlight like the count and mill tabs, no edit/delete screen) and buildTypeValues beside the other builders. Linked to the yarn master's Type field: the eight built-ins + the master's codes (with labels) are the select's options and a yarn's own dropped code stays an option, so no existing yarn becomes unsaveable; the vocabulary, the code rule and yarnTypeOptions live once in src/lib/yarnTypes.ts. Validation both surfaces, one rule (YARN_TYPE_CODE_RE: 1-8 letters/digits starting with a letter; label 1-40 name characters): blank / 9CW / CW- / C W / 9+ chars and a bad label are visible field errors on the type form and typeMasterErrors, a duplicate (from a yarn or the master, CPT vs cpt) is a 409 warned inline, and yarnErrors refuses blank / ZZZ / CW- / 9CW on create and edit while resolving cw → CW. The gatepass yarn inward's frozen C304 type enum now carries the same shared rule, so a type the master was just taught is accepted (CPT) and junk still is not — no POST name changed. No column added to existing tables, no route. Proved non-UI: bunx tsc --noEmit same 139 pre-existing errors (none in the touched files), svelte-syntax 3/3 clean, the gatepass field schema run directly (accepts CCW/CPT/Dn/blank, rejects 9CW/CW-/CCWWWWWWW), and bun scripts/type-master-audit.ts runs the real createYarn/updateYarn/createType/load on a throwaway data.db copy — projection, hand-add with label, duplicates, bad codes/labels, a master-added type on a yarn, cw→CW, an edit to CCW — 22/22 PASS. apply-db.sh ran locally (additive table, evt index + WAL restored). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §594.
C313 (/gatepasses/new?kind=YARN_IN, owner): the yarn identity of an SO trading order / production order / job work is now ONE field picked from the yarn master — Yarn (count + type + mill), each option reading 40 KW · mill IMP — replacing the three rows Yarn count / Yarn type / Mill name in the form and in the wizard (WIZARD_IDS YARN_IN point 5 and DIRECT), and the same one row replaces the direct-delivery job-work block; one tap fills countYarn / yarnTypeCode / ticket, so the POST names, columns and event payload are unchanged. "Only from the yarn master" is enforced: the pick posts a new optional yarnId, the create action resolves it and the picked row is the authority for count + type + mill, a stale id is a visible yarnId 400 that saves nothing, and the three values are required only when a picked reference has no yarn to inherit — a rated yarn booking still inherits its yarn and asks nothing new (the inline YARN_TYPES list went with the row it served). No column, migration, route or edit/delete screen; other kinds untouched. Proved non-UI on a data.db copy by POSTing the real create action: yarnId alone stored count 40 + mill IMP + event type KW, an open SO order with no yarn 400'd on the yarnId row, a forged id 400'd with no row, and the field schema accepted the id / rejected an 80-char one / kept blank optional — 6/6 PASS; tsc adds no diagnostic in the touched files, bun run check exit 0 (same 347 pre-existing warnings). No UI test run. Suite / build / deploy left to the watcher. PLANNING §595.
C314 (/gatepasses/new?kind=YARN_IN, owner): the yarn inward now asks cones and bag weight with the bags — Bags to be received → Cones in this delivery → Bag weight (kg per bag) in the Goods block and in the same order in WIZARD_IDS.YARN_IN (a booking-filled figure skips its step, C300). Cones store in the gatepass's existing cones column; the bag weight is a new nullable gatepass.bag_wt real (drizzle/0048, additive) that the create action writes and the reference's received kg uses when the receipt has no net (receivedKg(net, bags, bagWt ?? booking's per-bag)), so a bags-only inward still counts toward its SO / booking. Both rows suggest this yarn's own previous figures — past YARN_IN / DIRECT receipts with the same count, de-duped chips via receiptHistory.cones / .bagWt, blank when there is no history — and the booking pick prefills bagWt from its per-bag weight without overwriting a typed figure. Validation both surfaces, same commit: cones optIntMin0 (whole, ≥ 0, the bags rule), bag weight optNumPos (blank valid, 0 / abc a visible field error that saves nothing), the input a decimal-keypad number row — plus the one shared addition C314 made to FocusEntry: a number-mode row renders chips (no other caller passes chips there). The gatepass record prints Bag weight (kg / bag) beside the cones. No route, no edit/delete screen, other kinds untouched. Proved non-UI: the form schema run directly — valid 10 bags / 600 cones / 50.5 kg parses (bagWt 50.5), bag weight 0 / abc rejected, blank + absent valid, cones −5 / abc / 12.5 rejected, blank valid — 9/9 PASS; bun scripts/gatepass-wizard-audit.ts rewritten to the current flow (8 FAIL at HEAD) is 36/36 PASS — wizard OCR → warp/weft → bags → cones → bag weight → reference pick → yarn → pickup → GP no, every step drawn, cones then bag weight the two steps right after the bags, a carried bag weight skipping its step, and cones + bagWt mounted on the yarn-in hidden form; bunx tsc --noEmit adds no diagnostic under gatepasses/ or FocusEntry; bun run check exit 0 (none of the warnings under this page); apply-db.sh's push + evt index + WAL ran locally. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §596.
C315 (/gatepasses/new?kind=YARN_IN, owner): the gatepass read now takes the trader's printed Booking No. ("Booking No. : 1128") and the form pre-selects the open reference it names. intake.ts gains bookingNo (printed label required — booking no|number|nos|# — letters/digits plus /-_, ≤ 24 chars, bookingNoSane; heuristics and the model JSON both), DOC_SYSTEM / DOC_FIELD_KEYS ask for and merge it (the model fills a gap, never overrides), captureRouting.prefillParams('gatepass', …) sends bookingNo and asks for kind=YARN_IN (a read destination still wins → DIRECT), and /gatepasses/new validates the carry (/^[A-Za-z0-9][A-Za-z0-9/_-]{0,23}$/, over-long cut to 24, junk empty), resolves it against the open/partial rows letters+digits only (normBookingNo) and hands the match in as prefillBookingId, so the trader, yarn (count + type + mill) and rate arrive from the booking — an unmatched number picks nothing and shows as a chip to search with under the reference row, with OrderTablePicker's new optional search seeding the table's filter on open (threaded via FocusEntry's table-mode search prop). Nothing is posted by the read; no new column, migration, route, POST name or edit/delete screen. Proved non-UI (/tmp script over the real modules, 26/26 PASS): the printed wording reads, prose/bare numbers do not, model JSON keeps/drops/fills, routing carries bookingNo + YARN_IN (and DIRECT with a destination), capturePrefill (executed from source) takes 1128 and refuses junk, and 1128 resolves to its own row while 9999 and BK/1128 resolve to none. bunx tsc --noEmit adds no diagnostic in the touched files (the one captureOcr.ts cast error is pre-existing at HEAD); bun run check exit 0, no new warning class. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §597.
C316 (/gatepasses/new?kind=YARN_IN, owner): the gatepass read now reads the goods line as count + TYPE, from the words as well as the code — "61s Combed Compact - Classic Yarn" is count 61 + type CCW with no literal "CW" on the paper, and "40 CW" / "30s KW" / "150 Dn" keep reading. The type master's one vocabulary (src/lib/yarnTypes.ts, C312) gains yarnTypeFromText / yarnTypeWordSource / canonicalYarnTypeCode: a code wins over a word and the longest phrase over its head word, so combed compact → CCW, combed → CW, karded → KW, karded compact → KCW, denier → Dn, lycra → Ly, slub → Sl, hank → Hk (a new built-in default, Q244 — additive, reversible), and dn/cw land in the master's spelling instead of being dropped. parseCaptureText reads the count off the goods line (NNs, NN CW, or a number before a type word) and the type off the same words; the dead letters-only dN branch is gone. DOC_SYSTEM / LLM_SYSTEM ask the gateway for the same mapping by example, and both merge paths canonicalise the model's type code. /gatepasses/new splits a carried read (?countYarn=61+CCW, or the in-page OCR's count + type) through gatepassReadFields — count row takes 61, type rides its hidden value, the code list comes from the shared vocabulary, not a copy — and resolves one yarn-master entry by count + type + mill (a link-carried read resolves once the master is on the page); a read matching no entry stays a visible chip "Yarn read: 61 CCW · mill … — matches no yarn-master entry" and the pick is the operator's, nothing created (the tap-to-create chain is C319, Q245). No column, migration, route, POST name or edit/delete screen. Proved non-UI (bun script over the shipped modules, no browser): the five acceptance reads + 2/60 Hank + 40 combed + two Sl. No. serial columns + prose-with-nothing, seven gatepass-read splits (incl. a hand-added CPT), the model's dn → Dn, and the word map (Combed Compact / karded compact / Hank / denier / Sl. No.) — 26/26 PASS, and the shipped hand-off prefillParams('gatepass', …) → gatepassReadFields takes a real read (GATEPASS / 61s Combed Compact / Booking No. 1128 / 30 Bags) to kind=YARN_IN, countYarn=61 CCW, form count 61 + type CCW + bags 30 (ROUTE PASS); bunx tsc --noEmit adds no diagnostic under the touched files (the captureOcr.ts cast error is pre-existing at HEAD); bun run check exit 0, no new warning in this page. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §598.
C317 (/gatepasses/new?kind=YARN_IN, owner): the gatepass read now takes the mill off the spinner line resolved against the mill/ticket master, never off a word in an address. intake.ts gains readMill / millMasterName / millVisibleText: a line that is (or sits beside) an address — chambers, corner, road, plot, nagar, pin/GSTIN, a 6-digit pin code — is struck out of every mill read, a master entry reads in the master's own spelling either whole or by its leading word ("GVG" and the printed "Gvg Elite Yarns And Fabrics (tex Div Of Gvg Paper Mills) Gpm" both → GVG Classic), the built-in vocabulary is kept but only for a spinner line, and an unknown spinner keeps ticket empty and rides as a new millHint (offered, never invented). parseCaptureText(text, masters) takes the list, DOC_SYSTEM / LLM_SYSTEM say the mill is the spinner's own line and never an address word, and captureOcr's merge gate (applyMill) keeps a model ticket only when its words appear on the paper outside an address — the VISHNU-out-of-"Vishnu Chambers, Shahu Corner" back door is shut. formMemory.millMasterNames() is the one master list (mill table + the yarn master's tickets, de-duped), prefillParams hands an unknown mill as millHint= and never ticket=, and the read card shows it as "Mill read: … — not in the mill master" (its chip/tap-to-create is C319). No schema change, no migration, no route, no POST name, no edit/delete screen, nothing committed by the read. Proved non-UI: bun scripts/mill-read-audit.ts26/26 PASS (spinner line → GVG Classic, the whole paper → GVG Classic, the bare/split/adjacent address → no mill, unknown spinner → hint only, vocabulary spinner → SANATHAN, master in prose → Sanathan, a spinner-line mill beats a same-word master in the goods text, the goods line alone names no mill, model VISHNU dropped, model GVG resolved, prefill → read card both ways) plus the real millMasterNames() list (8 entries → GVG Classic, address → none), bun scripts/gatepass-ocr-audit.ts12/12 and gatepass-wizard-audit.ts 36/36 PASS; bunx tsc --noEmit adds no diagnostic in the touched files (same 139 pre-existing, the one captureOcr.ts cast error is HEAD's). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §599.
C318 (/gatepasses/new?kind=YARN_IN, owner): the gatepass read now learns from its own corrections — form memory (§397) gains the gatepass form and its kinds count / type / ticket (mill) / godown / transporter beside the existing yarn, so a spinner name, count, type, godown or transporter the user confirms becomes the answer for the transcript words of that gatepass. The form posts the draftId it was opened with (one hidden input; no zod field, no column), and the create action reads that draft's transcript (rawText) and read fields (parsedJson) and links the words to the confirmed values via changedFromRead — only a field the user changed or supplied, best-effort in the existing try/catch contract so a memory write can never fail the save. On the next readcaptureOcr.ingestCaptureDoc resolves those words (lookupFormMemory → resolveFormMemory, most hits wins) and applyGatepassMemory puts them in before the model's answer is used, so the gateway only fills what memory did not: a remembered mill reads in the mill/ticket master's own spelling (a confirmed name the master still lacks is kept verbatim — never invented, never a master written), a remembered type is canonicalised through the shared type vocabulary, a remembered godown only when it is one of the read's three words (godown / premises / sizer; the form's our-godown maps to godown at learn time), count and transporter straight. Nothing creates a master and nothing commits from the read (capture-first §15; the create-a-master chain is §592/C319). No migration, no route, no POST name, no edit/delete screen. Proved non-UI (bun script against a throwaway DB, real modules: learn → lookup → resolve over a second slip) 11/11 PASS — the long spinner line's words resolve GVG Classic with no millHint, count 61 / type CCW / godown / transporter come back on a bare slip that prints none of them, the most-corrected count wins, an empty memory changes nothing, a confirmed non-master mill is kept, a junk godown is dropped, and the order form never sees the gatepass words. bunx tsc --noEmit adds no diagnostic in the touched files (same 139 pre-existing; the one captureOcr.ts cast error is HEAD's). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §600.
C319 (/gatepasses/new?kind=YARN_IN + /masters, owner): an unmatched read value is now a + chip into its own master instead of prose. /gatepasses/new replaces C316's warning badge with a + 61 · CCW · mill IMP chip whose link opens the yarn master prefilled (?tab=yarn&new=61 CCW&mill=IMP&returnTo=<this form>), and FocusEntry gained FocusField.create = { value, href } — the + <value> row under a pick (real matches above it), used by the yarn row and the ticket row; the OCR card's unmatched mill (millHint) rides the same chip via a new oncreate. /masters now opens the yarn and mill/ticket add dialogs prefilled on ?tab=&new=&mill=&returnTo= (yarn: count + type, the type added as an option when the type master lacks it; mill: its name), and the yarn form's mill field shows + <mill> when the mill master lacks it — that chip opens the mill/ticket master and returns to the yarn link with the mill preselected. Saving the yarn returns to the inward as ?yarnId=<id>&ticket=<mill> and the inward shows that yarn row selected (the master row is the authority for count/type/mill; a stale id selects nothing; ?yarnId= alone renders the row before a reference pick). One new helper src/lib/masterCreate.ts builds and validates every link (safeReturnTo refuses //host and absolute URLs), so there is no open redirect and no second copy of the chain. Nothing is created without the user's Add tap — no POST name, column, migration, route, edit/delete screen or server-validation change. Proved non-UI: bun scripts/master-create-audit.ts23/23 PASS (the yarn and mill links and their params, the mill → yarn → form round trip carrying ticket then yarnId, "61 CCW"/"61s Combed Compact"/bare count/bare code/blank splits, the refused return targets, the 64-char cap); bun run check exit 0 with the same 349 warnings as HEAD (none new — the three initial-value captures are untracked), bunx tsc --noEmit adds no diagnostic in the touched files. No UI test was run (the row's own verify is browser; the watcher deploys). PLANNING §601.
C320 (/gatepasses/new?kind=YARN_IN + /masters, owner): C319's create-on-the-go chain now covers every other master-backed field on the form — the pickup godown/location, the party/trader and the transporter. All three are party rows on /masters?tab=party, told apart by category (a location is a Godown or Sizing row, a trader a plain Party, a transporter a Transport): src/lib/masterCreate.ts gains PARTY_MASTER / GODOWN_CATEGORY / SIZING_CATEGORY / TRANSPORT_CATEGORY / TRADER_CATEGORY, partyMasterPath(value, category, returnTo, field), masterField() (transport|trader|location) and unmatchedMasterValue() (the one rule: a value the master already carries, case/space-insensitive, makes no chip; blank makes none; the value is capped at 64). Each chip stands on the value the form carries — the read's transport, the picked reference's printed supplier when it matched no party (C40's "pick the party by hand", the chip rides under the reference picker because the booking-driven kinds ask no trader row of their own), and a named pickup place that is not a location-master row. A save returns ?field=&partyId=&party= and the form selects it (location as destPlace=sizer + its party id — enum and POST names unchanged; trader into sourcePartyId, now mounted on the yarn-in and direct-delivery hidden forms so the server's existing str(d.sourcePartyId) || the booking's makes it the record's trader; transporter into transport), and the return link carries what was already on the form (count/type/ticket, transport, place, parties, picked booking) with every carry validated (junk lands empty). destOptions / locationOptions now offer the Godown + Sizing parties (the beam legs keep Sizing only, and a Godown is no longer listed as a trader); FocusEntry's create chip now renders under a text row and a table picker as well as a pick. /masters opens the party add dialog on ?tab=party&new=&category=&field=&returnTo= with PartyForminitialName= + defaultCategory= and returns on save. Nothing is created without the user's Add tap — no new column, migration, POST name or zod field, no edit/delete screen, the gatepass still saves as its own record. Proved non-UI: bun scripts/master-create-audit.ts extended and 39/39 PASS (the three party links + their category/field params, the save back to the field with partyId, the three accepted return fields and the refused made-up ones, the unmatched-value rule incl. case/space and the 64 cap, plus every C319 case); bun run check exit 0 with the same 349 warnings as HEAD (none in the touched files), bunx tsc --noEmit adds no diagnostic under gatepasses/masters/FocusEntry/masterCreate (the masters +page.server.ts errors are HEAD's), gatepass-wizard-audit.ts 36/36 and gatepass-ocr-audit.ts 12/12 still PASS. No UI test was run (the row's own verify is browser; the watcher deploys). PLANNING §602.
C321 (/masters + every master-backed focus field, owner): the create-on-the-go chain now covers the typed value, not just the read one. FocusEntry gains FocusField.master (beside C320's masterCategory/masterMill) and draws the pick list's last row as + <typed value> — a ghost button inside the results box, below every real match — whenever the typed text matches no master value (case/space-insensitive, unmatchedMasterValue); a value the master carries shows no row, and a field whose values are not a master declares no master so it can never invent one. The row is declared on the masters FOCUS-ENTRY §20 names: gatepass yarn (master: 'yarn', mill/ticket, countYarn/count), ticket/mill, pickup location (Godown), transporter (Transport), sizers (Sizing), winders (Winder), plus sizing, winding, samples, movements and stocktake party/yarn picks. /masters now prefills yarn, mill-ticket, party, count and type on ?tab=&new=&mill=&category=&field=&returnTo= (CountForm/TypeForm gain prefill, mirroring YarnForm/MillForm/PartyForm), and a save returns ?field=&value=&label=&master= beside the C319/C320 yarnId/ticket/partyId/party names — FocusEntry applies it once (a pick takes the option id, a typed row takes the master's name), so the entry lands selected in the field it left on every focus form. The URL is validated server-side in load({ url }) — five known tabs, control characters stripped, new/mill capped at 64, category a real category, field a safe id (masterField now accepts any lower-camel focus field id, the three C320 names unchanged), returnTo a same-app path — and a value the master already carries opens no dialog and writes nothing (match back), so a stale link can never add a duplicate. Nothing auto-creates: no new column, migration, route, POST name, zod field or edit/delete screen. Proved non-UI: bun scripts/master-create-audit.ts extended to 45/45 PASS (the count/type links and their field, the count save back onto countYarn, focus field ids accepted and junk refused, plus all C319/C320 cases) and a bun script calling the real load() — nine URLs: count 61.5 prefill clean, new=40 and new=ccw/new=kw come back as matches → no dialog, mill THENPANDIAN matches Thenpandian, %00/control characters stripped, junk category lands Party, returnTo=https://evil.example refused, unknown tab ignored; bunx tsc --noEmit adds no diagnostic under the touched files (the masters/+page.server.ts 742/1156+ errors are HEAD's), bun run check exit 0 with the same 349 warnings as HEAD. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §603.
C322 (/capture → src/lib/server/intake.ts, owner): a gatepass header printed "GATE PASS" with a space now reads the read as a gatepass — the intent scan's seam pattern was [/gatepass/i] (glued only), so the spaced header fell through to \b(booking|book)\b and the printed "Booking No. : 1128" opened the yarn booking form (two live uploads of the same photo). The one regex is now [/gate\s*pass/i, 'gatepass'], still ahead of the booking pattern and still matching the glued GATEPASS / BEAMIGATEPASS OCR seam (§136) with no word boundary; the booking number still rides as bookingNo (C315), nothing else changes — no new intent, field, column, migration, route, POST name or edit/delete screen, and a booking with no gatepass header still reads booking. Validation untouched (no field added). Proved non-UI: a bun script over the shipped parseCaptureText — pre-fix GATE PASS / Booking No. : 1128 / 61s Combed Compact / 3 Bags → intent=booking (FAIL), glued GATEPASS + BEAMIGATEPASS PASS, plain booking PASS; post-fix 4/4 PASS, exit 0 — and the regression now lives in scripts/mill-read-audit.ts (named for the spaced header, plus the glued case and the plain-booking guard): 29/29 PASS, the 26 C317 cases still green. bunx tsc --noEmit: same 139 pre-existing errors, none under intake.ts/mill-read-audit.ts. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §604.
C323 (/tippans/new + /samples + /quote/new, owner): the OCR now adapts in every form — the tippan, sample and quote reads learn from their own corrections and resolve them on the next read, the same loop the orders and booking already had (§397/C318). formMemory.ts gains the tippan / sample / quote forms and four shared helpers: draftFormMemory (the capture draft a form was opened with → its rawText + this form's resolved kinds), transcriptMemory (words → best meaning, most hits wins, never throws), memoryId/memoryValue (a remembered id is used only when the form's own master list carries it), and learnFormMemorySafely (a memory write can never fail the save). Each form's load resolves its memory ahead of the read's own answer (sample/quote: party + quality; tippan: sizer + quality + count) and each save posts a hidden readText/readFields so the action links only the fields the user changed or supplied (changedFromRead) to that read's transcript words; the quote wizard's dictate step asks a same-route ?/memory action first and the fuzzy matcher fills what memory did not. Nothing is committed by a read, no master is written, no edit/delete screen; no new intent, endpoint, column, migration, route or form POST name outside the three hidden read fields, and a form opened without a draft behaves exactly as before. Proved non-UI: bun scripts/form-memory-audit.ts17/17 PASS (throwaway DB, shipped modules: sample learn→resolve, per-form isolation, quote learn→resolve, tippan count + repeat confirmation, unknown-id→empty, save-time diff only on a change, draft text+memory, missing draft/junk read/broken table → no memory and no throw, failed write → save still fine); bun run check exit 0 with the same 352 warnings as HEAD. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §605.
C317 reopen (/gatepasses/new?kind=YARN_IN → src/lib/server/intake.ts, owner): the mill is read off the spinner line again on the real Goenka paper. addressContext() widened an address onto both neighbours, and ADDRESS_RE counted any \b\d{6}\b (a PIN) as an address — so the goods line 61s Combed Compact - Classic Yarn 520524 3 ... (HSN 520524) marked the spinner line directly under it (Gvg Elite Yarns And Fabrics (tex Div Of Gvg Paper Mils) Gpm) as an address, skipped it, and readMill() returned {} (live draft probe: ticket undefined, millHint undefined); the widening cascaded until the visible text was one vehicle line. The regex now splits into ADDRESS_WORD_RE + ADDRESS_PIN_RE: a line is an address by its own words, or by a bare 6-digit number unless that line names a mill/spinner, and only the address WORDS spread to neighbours — never over a mill/spinner line, so a spinner printed above a two-line address reads too. The Vishnu-in-the-address cases are unchanged (Vishnu Chambers, Shahu Corner, Vishnu alone under it → no mill); an unknown spinner is still only a millHint, never ticket. No new field (no validation change), no master/column/migration/route/POST name, no edit/delete screen; captureOcr.ts and formMemory.ts need no change — they consume readMill/millVisibleText/millHint and pick the fix up through them. Proved non-UI: bun scripts/mill-read-audit.ts gains six checks named for the case (the HSN-above-spinner paper, the same paper without the master → printed hint, a bare 6-digit line above the spinner, a spinner above a two-line address, the full paper through detectCaptureIntent → prefillParams → ticket: GVG Classic, and the paper's gatepass/count/CCW read) — 31/36 with those five FAILs pre-fix, 36/36 PASS post-fix; scripts/gatepass-ocr-audit.ts 12/12 and scripts/form-memory-audit.ts 17/17 still PASS; bunx tsc --noEmit adds no diagnostic under intake.ts/mill-read-audit.ts. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §606.
C324 (scripts/deploy.sh, vite.config.ts + new scripts/build-swap.ts, owner): a deploy no longer builds into the directory the app serves. vite.config.ts passes adapter({ out: process.env.ADAPTER_OUT ?? 'build' }), deploy.sh step 2 runs ADAPTER_OUT=build.next bash scripts/heavy.sh bun run build, and step 2b — immediately before the restart — runs bun scripts/build-swap.ts "$ROOT", which carries the served build's client/_app/immutable files the new build lacks into it (mtimes preserved), registers each one in the new build's generated asset table with the adapter's own meta (blake2b256 sliced to 16 hex + mtime — adapter-bun only serves what it listed at build time, so a copied file alone would still 404), then swaps with two renames: build → build.prev (kept for rollback) and build.next → build. A missing build.next/index.js aborts before anything served changes; /build.next + /build.prev are gitignored; a plain bun run build still writes build/, and docs/DEPLOY.md's manual-build note now shows the side-directory form. No app route, column, migration, POST name or edit/delete screen. Proved non-UI: bash -n scripts/deploy.sh clean; a /tmp fixture run of the swap script 16/16 PASS (new build served, old kept as build.prev, old-only chunk carried and table-registered with a hash matching its bytes, new entries untouched, mtimes preserved, an open handle on the pre-swap index.js still reads, patched adapter-bun.js still bundles, first ship with no previous build, missing build.next aborts with the served build intact), the table parser run against the real build/adapter-bun.js (187 assets, head/tail found), and live curl baseline /queue/__data.json 200 / /queue 303→/signin; bunx tsc --noEmit adds no diagnostic in vite.config.ts. No UI test and no production build were run; the build-during-serve proof comes with the next full deploy. Suite / build / deploy left to the watcher. PLANNING §607.
C325 (/gatepasses/new?kind=YARN_IN, owner): the file a gatepass read saved now IS the form's Gatepass photo by default, not just the verify pane's file. captureOcr gains attachSavedAsUpload (copies the saved read into the screenshots upload drop under the same name, so the existing image field and /upload-file/<name>?bin=screenshots resolve it — a copy, never a move: the draft's /capture-img/<name> copy stays attached to the draft and the two files are independent, and a file it cannot place only leaves the field empty) and formImageFor (only a gatepass read, only an image), /api/capture-ocr returns that formImage, and the form's fillFromGatepass sets the Gatepass photo from it via the new readPhotoName rule in #lib/gatepassOcr.ts (image names only — a PDF read leaves the row empty; upload-name charset, ≤ 180 chars) — only when the row is empty or still holds the previous read's file, so the row's Replace Gatepass photo button and a snap taken before the read are never overwritten. No new field, POST name, zod rule, column, migration, route or edit/delete screen; the other five intents write nothing new. Proved non-UI (bun /tmp/c325-proof.ts over the shipped modules on a temp capture dir + upload bin, 28/28 PASS): same-name file in the drop with matching bytes, draft copy intact and independent (twice-attach included), null on a missing source, nothing placed for order/booking/quote/unknown, readPhotoName accepting an image name (incl. uppercase/jpg/webp/gif) and refusing PDF/bare word/blank/path/spaces/over-long, the exact /upload-file/<name>?bin=screenshots URL answering 200 image/png with the read's bytes, and the create action's field schema accepting the name as image while still refusing junk; bunx tsc --noEmit adds no diagnostic in the touched files (same 139 pre-existing; the captureOcr.ts cast error is HEAD's), svelte-syntax compiles the page clean. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §608.
C326 (/gatepasses/new?kind=YARN_IN, owner): the verify pane's gatepass now pinches and pans like a phone photo viewer. C308's image box was a plain scroll box with −/+ only, so a pinch zoomed the page instead of the photo. The box (data-verify-photo, unchanged selector) is touch-none, role="application" and carries pointerdown/move/up/cancel/lostpointercapture with per-pointer capture: two fingers scale verifyZoom by the span ratio around the fingers' midpoint (the box's scroll is re-seated (scroll + mid) × ratio − mid), clamped 50%–400% by the one setVerifyZoom the buttons now share; one finger drags the photo on both axes; closeVerify drops held pointers and resets the zoom; the +/−/percentage/✕ and ?verify=1 are unchanged and the PDF branch keeps its own iframe viewer (no pointer layer over a PDF). One component's own state — no new field, POST name, zod rule, column, migration, route, component or edit/delete screen. Proved non-UI: the shipped gesture block extracted verbatim, transpiled and run against a stub scroll box (/tmp/opencode/prove-c326-gestures.ts, 8/8 PASS: 100→200px pinch doubles, clamps at 400%/50%, midpoint held, one-finger pan by its own delta) and a svelte/compiler client compile of the page (/tmp/opencode/prove-c326.ts, 10/10 PASS: five handlers on the photo box, touch-none, non-draggable image, buttons intact); bun run check352 warnings, same as HEAD (none new) and --no-svelte-warnings "No problems found". No UI test was run (the row's own verify is browser; the watcher deploys). PLANNING §609.
C327 (/gatepasses/new?kind=YARN_IN, owner): the yarn-in screen now has Start wizard and Start OCR at the top. The two btn-xs buttons sit in the page header beside the existing C308 Verify fields toggle, inside the {#if kind === 'YARN_IN'} cluster, so every other movement's header is unchanged; they are always rendered (the wizard overlay covers the screen while it is open, so they are visible exactly when tappable) instead of being gated on a wizOpen the page does not own. FocusEntry gains an instance export openWizard(id?) — typed by the new FocusEntryHandle, driven from the page with bind:this={entry} — which reuses the component's private openWiz / openPop: with no id it opens the first still-needed step (the same firstNeededStep walk the C141/C300 first open uses), and with gatepassRead it opens the wizard's own OCR step, or that field's own popup when a carried yarn-in has dropped it from the walk — both render the same GatepassOcrRead card, so nothing new stands behind either button. No new field, POST name, zod rule, column, migration, route, component, wizard step or edit/delete screen; the auto-open lifecycle, the summary rows and the ?verify=1 pane are untouched. Proved non-UI: a svelte/compiler client compile of both files (/tmp/opencode/c327-check.ts) 9/9 PASS — both buttons in the markup and compiled template, handlers compiled to openWizard() / openWizard(READ_STEP_ID), a bind:this on the page, $$exports = { openWizard } on the component, one openWiz only, the firstNeededStep fallback and the openPop branch — plus bun run check0 errors, 352 warnings, same as HEAD (none on any added line) and bunx tsc --noEmit adding no diagnostic under gatepasses/new/focus/FocusEntry (same 139 pre-existing). No UI test was run (the row's own verify is browser; the watcher deploys). PLANNING §610.
C328 (scripts/deploy.sh, scripts/row-watch.sh + new scripts/check-conflict-markers.sh, owner): a conflicting pull now stops a deploy, and no commit can carry conflict markers. At 15:22 on 2026-10-29 the deploy's pull --rebase --autostash conflicted (C315-C323 against three rows added to the board at the same time), it carried on, git add -A staged docs/CORRECTIONS.md with the raw conflict block as ordinary content, and the push committed those markers plus duplicate C315-C317 ids. The pull is now its own checked step: a non-zero pull that left a conflicted tree (unmerged paths, rebase in progress, or a marker line) prints what git did and how to finish it by hand, writes the runtime mark .deploy-conflicted and exits 4 — no abort, no autostash pop, no build, the conflict stays in the tree for a human; a non-conflict pull failure exits 5 with no mark. The new scripts/check-conflict-markers.sh reads the index (git grep --cached, so a marker line git add -A just staged is seen even though git calls the path resolved) or the working tree (--tree), is line-anchored (<<<<<<< / seven = / >>>>>>> ), names every file:line and fails closed (0 clean, 1 refuse, 2 cannot read) — no tracked file in the repo trips it today and a marker named mid-sentence does not; step 5 runs git add -A as a checked step then the guard, and a refusal stops the deploy with exit 4 before any commit or push (a successful push clears the mark). Its --self-test replays the incident in a throwaway repo (6/6 PASS: staged block refused and named, divider/close named, resolved file passes, unstaged conflicted file fails --tree, resolved tree passes, prose does not trip) and deploy.sh runs it as step 0a so the regression keeps running without resurrecting the removed tests/ suite (A251's reading). row-watch.sh refuses to start a row while the tree still holds that conflict, re-checks the mark against live evidence and clears it itself once a human has resolved the tree, and holds a row blocked-owner immediately when a deploy exits 4 instead of spending FAIL_MAX sessions; .deploy-conflicted is gitignored. No app route, field, POST name, column, migration or edit/delete screen. Proved non-UI: bash scripts/check-conflict-markers.sh --self-test6/6 PASS, --staged / --tree clean on this checkout, bash -n clean on all three scripts, and a throwaway-repo harness running the real deploy.sh --commit-only end to end 23/23 PASS (conflicting pull → exit 4, mark written, markers and rebase-merge kept, nothing committed or pushed; marker-laden staged tree → exit 4, guard refusal, markers never in a commit; clean change → exit 0, committed + pushed, stale mark cleared; row-watch with a conflicted tree → rc 1, no session launched, log says why; resolved tree → mark cleared, log records it). No UI test was run; no test suite, build or deploy. PLANNING §611.
C329 (/gatepasses/new?kind=YARN_IN, owner): the gatepass read now takes the trader's printed Gate Pass No. ("Gate Pass No. : GSL/5546") and fills the yarn-in's Trader's GP no row (partnerGpNo). intake.ts gains partnerGpNo (printed label required — gate|gatepass|gp + no|number|nos|# — letters/digits plus the job-book separators, ≤ 24 chars, partnerGpNoSane beside bookingNoSane; heuristics, DOC_SYSTEM/DOC_FIELD_KEYS and LLM_SYSTEM/extractCaptureJson all ask for and sanitise it), captureRouting.prefillParams('gatepass', …) sends it, /gatepasses/new validates the carry (/^[A-Za-z0-9][A-Za-z0-9\s/-]*$/, over-long cut to 24, junk empty) instead of echoing it, and #lib/gatepassOcr.ts gains the field, its own sanitiser (24 cap) and the Trader GP … badge so the read card lists it and the in-page OCR sets the row. The wizard's read-step hint names it. The field's own rule (64 max, letters/digits/space/hyphen/slash) is untouched — only the read is capped at the row's 24. No new field, POST name, zod rule, column, migration, route or edit/delete screen. Proved non-UI: bun scripts/gatepass-ocr-audit.ts extended and 22/22 PASS (the printed wording reads GSL/5546, a bare number and a following word never do, the sanitiser keeps the slash, drops @#, caps 24 and refuses a leading /, the read list names Trader GP GSL/5546, and the shipped parseCaptureText → prefillParams('gatepass', …) → gatepassReadFields hand-off carries it to the row and the badge), gatepass-wizard-audit.ts 36/36 and mill-read-audit.ts 36/36 still PASS, bunx tsc --noEmit adds no diagnostic in the touched files (same pre-existing repo-wide errors). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §612.
C330 (/gatepasses/new?kind=YARN_IN, owner): the gatepass read card now lists every value the read took, not six of its rows. #lib/gatepassOcr.ts's GatepassReadFields gains bookingNo, gross, net, expectedDate — values captureRouting.prefillParams('gatepass', …) had been sending all along (C315's booking no, the reader's gross/net/expectedDate) that only the badge row dropped, so a booking number that WAS read looked unread. gatepassReadFields carries each on the field's own rule in the same commit: the booking no ^[A-Za-z0-9][A-Za-z0-9/_-]{0,23}$ (the page's carry rule, 24 max), a weight a number ≥ 0 (6,010 kg → 6010, abc / blank → empty, never 0/NaN, 12 chars), the expected date a real YYYY-MM-DD; junk lands empty and is never listed. gatepassReadLabels reads Booking · Count · Type · Mill · Bags · Gross kg · Net kg · Vehicle · Transport · Trader GP · Expected — still only what was filled. fillFromGatepass applies the same values, and only where the kind has the row (weights on WINDING, the expected date on DIRECT); a read booking no that names no open reference becomes C315's "matches nothing open" chip and seeds the reference table's search (prefillBookingNo is now $state), while a number that does match picks nothing — the operator still picks, as the server-side carry does. No new field, POST name, zod rule, column, migration, route or edit/delete screen; nothing is committed by a read. Proved non-UI: bun scripts/gatepass-ocr-audit.ts extended to 36/36 PASS (the whole Goenka paper → Booking BK/1128 · Count 61 · Type CCW · Bags 30 · Vehicle MH 12 AB 1234 · Transport Kailash · Trader GP GSL/5546 by exact array, each new badge named, 6,010 kg → 6010, junk/blank weights empty, BK 1128 / @@ refused, the 24-char cap, 12/10/2026 and 2026-13-40 refused, a real date kept, the empty-read shape); gatepass-wizard-audit.ts 36/36, mill-read-audit.ts 36/36 and master-create-audit.ts 45/45 still PASS; bun run check exit 0, 339 warnings with none on the touched files; bunx tsc --noEmit same 139 pre-existing errors, none in gatepassOcr.ts / the page / the audit. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §613.
C331 (/gatepasses/new?kind=YARN_IN + read card, owner): the gatepass read's yarn now resolves structurally against the yarn master, and an unmatched read is a chip inside the read card. The 19:09 Goenka upload read count 61 + type CCW + mill and left the yarn row empty with nothing to tap, because the page's resolver compared the read's count to yarn.count and then required the mill to equal yarn.ticket — while a master row's identity is its name ("61 CCW Sanathan"; the imported legacy rows "61/1 CBD" / "20 OE" carry a myTextile count column stuck at 40). One new pure module src/lib/yarnMatch.ts (yarnRowCount / yarnRowType / yarnRowParts / yarnEntryFor): a row's count is the number its name leads with (column fallback), its type the type its name names (30/1-SLUB → Sl, via the C312 yarnTypeFromText, column fallback), mill its ticket; the read's count is the identity and type/mill only narrow (a mill spelled differently never discards a count+type match), more than one distinct yarn left resolves to none, and duplicate rows of one identity are one yarn. Used on both surfaces: the pick's label is the row's name (+ mill … when the name does not print it — no more 96 identical 40 CW · mill IMP options), a pick fills countYarn/yarnTypeCode/ticket from the parts (61/1 CBD → 61, not the stale 40), the master-return selects the same way, and the create action resolves the picked id through yarnRowParts so the record is never written with 40 over the read's 61. The read card (GatepassOcrRead) gains an optional yarnSuggestion and draws the C319 + 61 · CCW · mill Goenka chip (opens /masters?tab=yarn&new=…&mill=… prefilled, nothing created without the tap) inside the wizard, where the page's own chip was hidden; and a read carrying a yarn now shows the Yarn (count + type + mill) row before a reference pick (optional until a reference makes it the receipt's yarn). No new field, POST name, zod rule, column, migration, route or edit/delete screen; a read still commits nothing. Proved non-UI: bun scripts/gatepass-ocr-audit.ts extended by twelve named checks — 48/48 PASS (legacy name count, name type, duplicate-identity merge, unknown mill keeps count+type, ambiguous read → none, type narrows by name not column, the Goenka paper's chip case and its spinner-mill resolution), the 36 C330 cases still green; the same rule run against live data.db resolves 61 CCW Sanathan → p9ivbu7cac, 60 KCW VISHNU → y-60s-kcw, leaves 61 CCW / 61 CCW Goenka for the chip and reads 61/1 CBD as count 61; bun run check exit 0 with the same 352 warnings as HEAD, bunx tsc --noEmit adds no diagnostic under the touched files. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §614.
C332 (/gatepasses/new?kind=YARN_IN, owner): the yarn-in's packing row now reads Cones per bag, not Cones in this delivery. The row (id conesPerBag, unit /bag) keeps C314's place after Bags to be received — Goods block and WIZARD_IDS.YARN_IN alike — and the delivery total becomes arithmetic: yarnConesTotal = bags × cones per bag, posted in the existing cones name/column so lots.ts, the per-bag grain, the reports and the prints all keep reading one total, with the per-bag figure in the existing cone_per_bag column (now mounted on the yarn-in hidden form). A read-only Total cones in this delivery row and the row's own popup sub show the working figure (18 bags × 60 = 1080 cones in this delivery); the booking's conesPerBag only prefills when the form has none and is never carried as a 0 (C307's rule, already applied to the bag weight; a booking's 0 means "no packing recorded"); the yarn's previous receipts now offer their figures read back per bag (cones ÷ bags) instead of as delivery totals. Validation on both surfaces (schema optIntPosCones/bag must be greater than 0, the qty numeric row, blank still valid — C333 owns requiredness), and the create action derives a total that arrives missing (YARN_IN only), so a gatepass can never store 0 total cones beside a filled per-bag count. Other kinds' Total cones (all bags) row untouched; no new field, POST name, column, migration, route or edit/delete screen. Proved non-UI: bun scripts/gatepass-wizard-audit.ts extended and 46/46 PASS, a svelte/compiler client compile of the page 5/5 PASS (compiles, the new label and the total row in the compiled markup, the old label gone), the form schema run directly 13/13 PASS (60/1080 parse, 0/−5/sixty/12.5 refused by their own messages, blank/absent valid), the OCR/mill-read/master-create audits still 48/48 and 36/36 and 45/45, bun run check exit 0 with HEAD's 352 warnings (none on this page), bunx tsc --noEmit no diagnostic under gatepasses/new/. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §615.
C333 (/gatepasses/new?kind=YARN_IN, owner): every row the yarn-in wizard shows is compulsory except the remark. The reference pick, the yarn row (when it renders), the trader's GP no, the pickup location, the bags, the cones per bag, the bag weight, the yarn use and the gatepass photo now carry required on the page, so the shared FocusEntry prints the *, marks each empty one Tap to add, counts N required to fill and keeps the Save disabled — no new form code — and the photo section drops its (you can skip) on this kind only. The schema the create action validates with gains a superRefine for YARN_IN: an empty bags / conesPerBag / bagWt / warpWeft / destPlace / partnerGpNo / image is its own issue at its own path with its own message, so the action's fail(400, { form }) paints it beside that row and nothing is saved or silently defaulted; destPlace also joins the emptyToUndef enum rows so a blank location reports the row's message instead of Invalid option. The yarn identity check in the action was gated on a picked booking / carried order and now runs on the kind alone (YARN_IN with no booking-carried yarn), so a receipt can never save without a yarn. Compulsory = filled (a posted 0 counts; the bags ≥ 0 and cones/bag, bag weight > 0 rules are untouched); the Read the gatepass card stays optional so manual entry survives (Q266), and the reference is enforced client-side plus the action's existing ref checks (Q267). No new field, POST name, column, migration, route or edit/delete screen; other kinds untouched. Proved non-UI: bun scripts/gatepass-wizard-audit.ts extended by twenty-one C333 checks and 67/67 PASS (complete yarn-in parses; each of the seven fields blanked gives its own path + message; remark and every other kind still parse blank; the page marks all nine rows compulsory; the read card carries no required; the action's yarn gate is the kind alone), svelte-syntax compiles the page clean, the OCR/mill-read/master-create audits still 48/48 and 36/36 and 45/45, bunx tsc --noEmit adds no diagnostic under gatepasses/new/ or the audit. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §616.
C334 (/gatepasses/new?kind=YARN_IN, owner): the long one-line explanations come off the gatepass page, the field pop-ups and the read wizard, leaving the short label plus the field. KindCfg drops its hint (all eight movement descriptions) with the header <p> that printed it, and the saved summary's Route fallback becomes routeParties || '—' (a value, never a sentence — C197). FocusEntry stops printing the sub / staticNote line under the wizard step and the single-field pop-up headings and drops the money wheel's "Scroll ₹… a slot…" line; no behaviour moved (required count, the empty gate, wizard order, the block list, the pop-ups and the static rows' tappability are untouched), and the callers' sub: strings stay inert in source, so no screen's configuration changed. GatepassOcrRead loses its hint prop and the two chip help paragraphs, keeping its chips, badges, busy line and messages. The page's own read chips lose their instruction spans, the read step loses its sub, the vehicle row loses its plate sub. Other screens' own hint components (orders/new, BookingCapture) are a later sweep. No new field, POST name, zod rule, column, migration, route or edit/delete screen; a read still fills and commits nothing. Proved non-UI: svelte/compiler client compile of the three targets PASS (each compiles; Pick the yarn booking, Snap or upload, the AI reads it, Camera reads the number plate, The reference table opens searched, Tap to fill and Scroll ₹ absent from compiled code and source, while data-gatepass-ocr-file, data-start-wizard, the read heading and the step counter are still there); bunx tsc --noEmit 139 diagnostics, HEAD's, none in the touched files. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §617. Q268.
C335 (/gatepasses/new?kind=YARN_IN → /bookings/new, owner): a read booking number that matches no open reference is now an + Add Booking No. <no> chip under the reference picker (C319's chip shape, C315's guard: only while nothing is picked), and the chip opens the yarn booking form prefilled with that number and the read's count, type, mill and bags. bookings/new's load gains the read's hand-off beside the procure board's — ?traderBookingNo=&count=&type=&mill=&bags=&returnTo=, each on the form's own rule (number ^[A-Za-z0-9][A-Za-z0-9 /._-]*$ ≤ 60, mill ≤ 40 on the schema's character set, the type canonicalised and capped at 8, bags whole-positive; junk lands empty, never echoed). The count + type + mill resolve the yarn-master row through yarnEntryFor (C331's parts rule), so the booking opens on the yarn the paper names; when the master lacks it the two parts ride BookingWizard's own Add-yarn dialog (prefill / millPrefill, props YarnForm already had) — nothing is created by arriving. The form's hidden returnTo is validated with safeReturnTo (a same-app path only, no open redirect) and the save redirects to ?bookingId=<id>&created=<id> on it instead of /bookings?created=, so the yarn-in — whose load already resolves ?bookingId= — shows the new booking selected (count, type, mill, trader, rate, bags from the booking) with the saved-popup confirmation intact; a plain booking still lands on the list. The read's values carry the C14 gatepass read source badge (traderBookingNo, yarnId, millTicket, bags) and the procure-board alert no longer fires for them. Nothing is created without the tap — no new endpoint, column, migration, zod field or edit/delete screen. Proved non-UI: bun /tmp/c335-proof.ts37/37 PASS (the chip link, the return link, safeReturnTo refusals, and the load's own prefill block extracted from the shipped source and executed with a fake db: a real read, junk refused, an unmatched count kept for the Add-yarn dialog, a bare request unchanged); bunx tsc --noEmit adds no diagnostic under the touched files, bun run check exit 0 with HEAD's warning class and none in the changed regions. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §618. Q269.
C336 (/gatepasses/new?kind=YARN_IN, owner): every gatepass read now carries the identity of the FORMAT that printed it. intake.ts's CaptureResult gains docIssuer, read by readDocIssuer() off the top block before any label (first six lines; stops at the first labelled field, skips address lines and GATE PASS-style headings) and only from a line that reads like a firm — a company suffix (Private Limited / Pvt Ltd / Ltd / LLP / Syndicate / Industries / Mills / Textiles / Traders / & Sons) — through docIssuerSane() (letters, digits and a firm's punctuation & . , ' ( ) -, collapsed spaces, 3–80 chars; a bare GOENKA is not an issuer). The model may fill a gap only: DOC_SYSTEM asks for docIssuer and forbids the buyer/mill, the key joins DOC_FIELD_KEYS, and mergeDoc sanitises it on the same rule. captureRouting.prefillParams('gatepass', …) sends it, so the draft's parsedJson carries it and the link prefill does too, and /gatepasses/new's capturePrefill validates the carry (^[A-Za-z0-9][A-Za-z0-9&.,'()\-\s]{2,79}$, 80 cap, junk empty). #lib/gatepassOcr.ts gains the field, its client-side sanitiser and the first read badge Format <issuer>, and fillFromGatepass keeps it in page state for the header's Format … chip. No schema, column, migration, route, POST name or edit/delete screen; a read still commits nothing. Proved non-UI: bun scripts/gatepass-ocr-audit.ts extended by nine named checks — 57/57 PASS (letterhead → Goenka Syndicate Private Limited, buyer under To: never the issuer, two traders → two different issuers, no letterhead → none, an address line → none, issuer rides prefill and shows first on the read card, model fills a gap, junk empty / 80-char cap); mill-read-audit.ts 36/36, form-memory-audit.ts 17/17 and gatepass-wizard-audit.ts 66/67 (its one FAIL is the pre-existing C333 source-grep at HEAD, verified by stash) still PASS; bun run check exit 0, no diagnostic on the touched files. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §619. Q270.
C337 (/gatepasses/new?kind=YARN_IN + the capture read, owner): a gatepass FORMAT is now learned per issuer — the anchor that led the user to a value the read missed, plus the value's own shape — and the next paper of the same format is filled by it with no manual step. New src/lib/server/docFormatMemory.ts: anchorForValue finds the saved value on its transcript line and keeps the label in front of it (Booking No., Gate Pass No. — ≥3 letters) or, for an unlabelled box, the position hint"lineFraction|column" (0.80|0.00, read back on the nearest line ±1 because a re-read never has the same line count); valueShape collapses the value to its character classes (1128 → 9, GSL/5546 → A/9) and the candidate the anchor yields must match that shape and the field's own rule, so a shape is never a wildcard. Table doc_format_memory (issuer, field, anchor_kind, anchor_text, position, shape, hits — migration 0049_classy_meteorite, applied locally by apply-db.sh), one row per identity with hits counting re-confirmations, bounded at 2000 and pruned weakest first. Written only by the gatepass create save (beside learnFormMemory, only where changedFromRead says the read missed the field): the picked booking's number, the posted trader GP no, the shipped mill — issuer from the draft's letterhead, no save → no row. The pre-pass runs inside detectCaptureIntent: regex parse → learned anchors over the transcript → the model's answer merged into what is still empty → the mill resolved, so an anchor beats the model but never the reader, and only a gatepass read is pre-filled; the values ride the existing bookingNo / partnerGpNo / ticket prefill and the user still confirms — a read commits nothing. No new POST name, no route change, no edit/delete screen. Proved non-UI: bun scripts/doc-format-audit.ts18/18 PASS (shapes, label vs position anchor, absent value learns nothing, nothing known before a save, one row per field, keyed on the issuer, re-confirmation counts a hit, the next paper fills 1231 / GSL/5812 / GVG Elite Yarns while the model's 9999 loses, a read value 777 is never overridden, a non-gatepass read untouched, no rows changes nothing); bunx tsc --noEmit adds no diagnostic under the touched files (the captureOcr cast diagnostic is HEAD's, verified by stash). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §620. Q271, Q272.
C338 (/gatepasses/new?kind=YARN_IN, owner): the learned gatepass format now teaches the model too and shows on the read card. docFormatMemory.ts gains formatHint(issuer, rows) — one sentence per learned field in the issuer's own words ("the booking number prints on the line "Booking No", a value of digits; the mill name is unlabelled — about 60% down the paper and 0% across its line, a value of letters"), empty for no rows, a junk issuer or a non-gatepass read — plus formatKnownFields (booking no / GP no / mill, in the fields' own order) and formatFieldValue (the field rule, one place: junk never rides a hint). captureOcr.gatepassFormat runs after the read's own first pass named the letterhead: when the intent is a gatepass and that issuer has learned rows, it asks the gateway once more over the same transcript, text only (no image re-sent) with DOC_SYSTEM + the layout sentence, and merges the answer into the gaps of bookingNo / partnerGpNo / ticket only — a value the read already had is never overridden, a first read of a new trader spends nothing. ingestCaptureDoc makes that one call and hands the same rows to detectCaptureIntent (§620's deterministic pre-pass untouched), so the table is read once per upload. /api/capture-ocr answers formatKnown: { issuer, fields }; GatepassOcrRead.svelte draws the issuer beside format known: booking no, GP no, mill, validated by the new gatepassOcr.gatepassFormatKnown (letterhead rule, short word-pair names, de-duped, capped) so junk draws no line. No new field, POST name, column, migration, route or edit/delete screen; a read still learns nothing and commits nothing. Proved non-UI: bun scripts/doc-format-audit.ts extended by twenty C338 checks — 38/38 PASS (the hint names the issuer, the learned booking-no and GP-no lines, the unlabelled mill with its position; empty for nothing learned and a junk issuer; the card words; the field rule; and against a stub gateway: exactly one call, no image, the prompt carrying the layout sentence and the paper's own text, the missed booking no filled 7777, the read's own GSL/5812 kept, the second field filled, the card naming the issuer + fields; a stranger issuer and a non-gatepass read spend no call; the card guard drops junk and de-dupes) — plus the eight §620 checks it still runs; svelte/compiler compiles GatepassOcrRead.svelte with the format line 1/1 PASS; bunx tsc --noEmit adds no diagnostic under the touched files (the one captureOcr cast diagnostic is HEAD's). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §621. Q273, Q274.
C339 (scripts/row-watch.sh, scripts/queue-next.ts + new scripts/row-watch-lanes-check.sh, owner): the row line now runs two sessions at once, guarded by their target files — it was network-bound (each turn a remote model call, ~15 s, 30-60 turns a row) while the box sat at load 0.00. queue-next.ts gains --select --avoid "<t1,t2>" (one group per in-flight lane; same runnable set and sheet order as --first, first row whose targets clash with none) over a shared lanesClash — same path or parent/child directory, two hot files never together, docs-only targets never a conflict — plus --json (the row with its targets), and --unstick --keep C1,C3 so a blanket release can never rob the lane that is mid-row. row-watch.sh keeps the single-watcher lock and is the only dispatcher: each tick it syncs, prunes claims whose pid is gone, releases doing rows except the live lanes' (--unstick --keep), then fills every free lane from --select; default ROW_LANES=2. A lane is row-watch.sh lane <n> <row> — session, integrate, drop its claim in .row-claims/<n> (pid, row, targets) — so a row that dies (restart, timeout) leaves a claim no pid owns, the claim is pruned, its row released and the free lane takes the next non-overlapping one; a lane's session sweep now skips the other lane's live session, and status prints both lanes, rate [h] prints rows/hour from the log. Integration is serialised on .row-integrate.lock (deploy.sh pulls + git add -A + pushes, so two at once would sweep one lane's half-written files into the other's commit) and board writes on .row-board.lock; a full build first waits for the other lane to close (shipping marker parks new dispatches) and stays serial on heavy.sh. once mode is unchanged (one row, synchronous) for tests; .row-claims/, .row-integrate.lock, .row-board.lock are gitignored. No app route, field, validation, POST name, column, migration or edit/delete screen. Proved non-UI: bash scripts/row-watch-lanes-check.sh11/11 PASS on a sandbox board (its own docs/scripts, a fake opencode, a stub deploy): --select skips the same-file row and offers the next, a docs-only opposite never blocks, two hot-file rows are kept apart, two avoided groups honoured; --unstick --keep C1 holds C1 and releases C3; run puts a second session on a disjoint row (max 2 at once, no clashing pair over the whole run), never starts the same-file row beside the live one, and after that lane is killed its claim is gone, the row is re-picked and finished while the other lane's session is neither killed nor restarted; the board drains 6/6. bash scripts/row-watch-lock-check.sh still OK, bash -n clean on both scripts, bunx tsc --noEmit adds no diagnostic in queue-next.ts. Before measure from the log: 4.25 rows/h (last 12 h, 51 rows; 24 h: 2.12), after read with bash scripts/row-watch.sh rate. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §622. Q275, Q276.
C340 (scripts/row-watch.sh, owner): the row prompt now quotes its own row, so a session stops spending its first turns re-finding what it was told. row_brief reads the row's block from docs/QUEUE.md (target, verify, priority, acceptance bullets) and its text from the docs/CORRECTIONS.md Pending table in one Python pass, and row_prompt embeds that block; the orientation reads (docs/STATUS.md, docs/BUILD-LOG.md, grep docs/PLANNING.md) are now explicitly conditional — only when the row changes a flow or a decision, never for a copy/label/badge row. A missing row still gets an id + empty brief, so nothing silently loses its wording. row-watch.sh brief <row> prints exactly the prompt a session receives. No app route, field, validation, POST name, column, migration or edit/delete screen; the board is only read, never written. Proved non-UI: bash -n scripts/row-watch.sh clean and bash scripts/row-watch.sh brief C340 shows C340's own text ("Cut the per-row preamble…"), Targets: scripts/row-watch.sh, Verify: script and its three acceptance lines, while brief C999 prints the not-found fallback and - fields. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §623.
C341 (runtime, owner): the Bun version is pinned in the repo and one command moves any host to it. New .bun-version = 1.4.4 (newest stable, bun-v1.4.4 2026-10-22 — this host was already on it; the repo just never said so, the only requirement being adapter-bun's throw below 1.4 in the build output) and new scripts/bun-upgrade.sh: --check prints bun <v> — at the pinned version (<pin>) and exits 1 only when the host is behind (at or above passes, so it is a floor, not a lock), no flag upgrades via the official installer into BUN_INSTALL (default ~/.bun, where the proot ~/bin/bun wrapper points) then re-verifies, --install is the same path for a host with no Bun. It never downgrades a host ahead of the pin, and copies the current binary to bun.<oldversion> first and restores it when the new one does not answer bun --version — a failed download leaves the host exactly as it was, and the running app keeps its mapped binary until the restart deploy.sh already does. docs/DEPLOY.md's first-time setup gains step 4 (Bun) with the three commands. No app code, route, field, validation, POST name, column, migration or edit/delete screen; the suite and build are untouched. Proved non-UI: bash scripts/bun-upgrade.sh --check → bun 1.4.4 — at the pinned version (1.4.4) exit 0 and a bare run the same idempotent no-op; the script run from a scratch copy against fake pins → 1.3.0newer than the pin, left alone exit 0, 1.5.0 / 2.0.0behind the pin exit 1, missing .bun-version exit 2, PATH without Bun → not installed — run --install exit 1, bad flag → usage exit 2; bash -n clean. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §624. Q277.
C342 (schema, owner): the cost foundation every C343–C357 row stands on is one additive migration — drizzle/0050_sticky_mojo.sql, four CREATEs and twelve ALTERs, nothing readable yet. src/lib/server/schema-foundation.ts adds yarn.grade (A+/A/B/C, null = blank), order.role (so|po|fabric_purchase — type keeps its SO|JW|OUTSOURCE|PROD words), planned_mtrs, trigger (so|stock), sale_kind (normal|damaged), parent_so_id, frozen_at, unit_cost and legacy_cost (boolean default 0 = pre-WAC order the costing engine must never re-cost), lot.quality_key + lot.pick, dispatch_slip.apportionment_id, and four new tables: pool_entry (track yarn|produced|purchased|salvage, in|out, its key columns count+type+grade or quality+pick, qty/unit_cost/value, source lot/order/ref — no stored average: WAC is SUM(value)÷SUM(qty), so a later purchase cannot rewrite a locked cost), cost_apportionment (SO qty locked onto a pool entry / a PO's output / market / a new PO shell, unit_cost frozen, released_qty for amendment and cancellation at that same cost), cost_leg (kind preserves C353's warping/sizing/weaving/checking/mending/packing/loading and C356's exceptional; level po = capitalised into WAC vs so = direct P&L; status provisional→actual with bill no/date/vendor/ref/reason) and substitution (lot swap, quantity only, cost basis untouched). parent_so_id is a plain id, not an FK: SQLite cannot ALTER-add an FK to the reserved-word table order — the generated REFERENCES order(id) is a syntax error, proved by the first push. Every new column on an existing table is nullable or defaulted, so no reader, form, POST name, route or edit/delete screen changed. Proved non-UI: bunx drizzle-kit generate → the one migration; push --force on a VACUUM copy of data.db (/tmp/opencode/c342-final.db) → Changes applied, 1899 orders preserved; then evt_client_event_id_idx re-created and journal_mode=wal re-asserted on the copy (the copy's sqlite_autoindex_evt_1 integrity warning and one NULL-keyed duplicate evt group are HEAD's, identical in the source data.db); a 20/20 proof script compared the declared columns of yarn/order/lot/dispatch_slip/pool_entry/cost_leg/cost_apportionment/substitution against PRAGMA table_info (8/8 exact) and round-tripped a PO/SO pair, a yarn pool entry, a locked apportionment, a leg, a substitution, a graded yarn, a lot key and a dispatch link through the shipped drizzle schema; bunx tsc --noEmit stays at HEAD's 139 diagnostics, none on the added lines. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §625. Q278, A278.
C343 (src/lib/server/costing/, owner): the costing engine is now pure functions over C342's tables — no route, no UI, no DB handle, no stored average. pool.ts keys a yarn pool on count+type+grade and a fabric pool on quality+pick, keeps produced/purchased/salvage as separate tracks, and reports a balance (in-qty/value, out-qty/value, working qty/value, average); the WAC counts inward entries only, so an issue at the average of its own moment never moves it, and a zero-cost entry is excluded from the average while its quantity stays in stock — a free opening lot or a salvage roll cannot drag the produced average, so a later purchase changes only future issues. The salvage track is worth 0 whatever the row says and its sale proceeds roll into the parent SO's P&L (salvageRollup → soPnl.salvageValue), a damaged SO's own sale being its revenue rather than a second roll-up. legs.ts prefers a stated amount, else qty × rate (with weavingAmount = picks × ₹/pick ÷ 100 on good mtrs), freezes a leg without mutating it, bills it to actual with its own no/date, and keeps the unbilled commitment as an accrual (committed/billed/provisional/accrued/frozen); capitalised vs direct split PO-level from SO-level and a legacyCost leg is never re-costed. apportion.ts locks qty + unit cost and releases at that cost (so a later pool change cannot restate a released metre), walking locked → part_released → released, with shortfallReprice shrinking the assignment to the good metres and pricing them at incurred ÷ good mtrs (zero good metres → zero unit cost, never a division). pnl.ts costs an SO from the outstanding value of its locked apportionments plus its direct legs, prices a PO's good metres at incurred ÷ good mtrs, and gives variance/legVariance a quantity/rate split that adds back to the total (ready for C354). substitute.ts moves kg between lots, capped by the from-lot, and returns the legs unchanged — a shade swap can never re-price committed fabric. Nothing is rounded inside the engine; money rounds where it is displayed. No route, form, field, validation, migration or edit/delete screen; nothing imports the module yet. Proved non-UI: the co-located runner bun run src/lib/server/costing/costing.test.ts → 12/12 PASS (WAC in and out, key/track separation, apportionment at pool cost, release at locked cost, shortfall re-price, salvage roll-up, zero-cost isolation, PO incurred ÷ good mtrs, quantity/rate variance, leg freeze and accrual, substitution quantity-only, and the full spine: yarn 200 kg @ ₹100 → PO ₹25,600 on 1,000 good m → ₹25.60/m produced → SO 1,000 m @ ₹40 + 20 salvage m @ ₹12 → ₹14,640 margin, 36.6%); tests live beside the engine because this checkout has no tests/ root for bun test (Q279/A279, Q280/A280); bunx tsc --noEmit stays at HEAD's 139 diagnostics, none in src/lib/server/costing/. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §626.
C344 (/lots + /masters?tab=yarn + /weft-issue, owner): the yarn pool is wired end to end. Every delivered yarn-in (createLotsFromGatepass — the one hook /inward, /gatepasses and /guard share) now appends one pool_entryin: track = yarn, key copied from the yarn master at receipt time (count + typeCode + grade, an ungraded yarn its own pool), qty = net kg, unit_cost = the gatepass booking's ratePerKg, value = kg × rate, plus the lotId, the refNo (GP no) and the orderId — set = apportioned to that PO, null = free stock. One inward row per lot, so a re-delivered tap adds nothing, and a receipt with no priced booking is a zero-cost entry whose kg is stock but never drags the average (Q281/A281: no price is invented, the booking is what the inward already carries). An issue to a production order (/weft-issue, when the loom's beam resolves the order) appends one out at issueAtWac — the pool's average of that moment — plus that order's kind = yarn, level = pocost_leg at the same unit cost, amount kg × WAC, rate_unit = kg, provisional, frozenAt stamped, issue no as doc_ref; idempotent per issue document, and an issue with no order (an internal move) writes no cost. A later purchase therefore moves only future issues, the earlier leg keeps its locked ₹/kg. yarn.grade gets its form and reader: the yarn master's Add/Edit gains a Grade select (blank, A+, A, B, C) with one shared rule yarnGradeError in src/lib/yarnGrade.ts applied on the server (yarnErrors folds the code to upper case and refuses anything else before create/update) and on the form (submit cancels with its own error line), and the yarn table shows it. Yarn stock shows grade: /lots gains Grade and Pool columns (free / apportioned + the receipt's ₹/kg), its row dialog, the lot page and the board's ys table all read it. No migration, no new POST name, no route, no edit/delete screen — C342's tables and C343's engine are only written to and read. Proved non-UI: bun /tmp/opencode/c344-proof.ts against a copy of data.db pushed to the shipped schema (/tmp/opencode/c344.db) 30/30 PASS — receipt in at the booking rate ₹100, key 61/CCW/A, re-delivery idempotent, free vs apportioned to PO-1, issue 60 kg at the moment's WAC ₹106.67 with the pool out and the cost leg at that same rate, leg frozen and keyed to the PO, same document twice writes nothing, a later purchase moving the next issue to ₹120 while the first leg stays ₹106.67, an unpriced 500 kg receipt kept out of the average, a no-order issue writing nothing, an ungraded yarn keying its own pool, and the grade rule (blank/A+/A/B/C, a+→A+, D refused); the deploy's own non-forced drizzle-kit push (stdin closed) applies C342's tables to a fresh copy of the live data.db (Changes applied, pool_entry + cost_leg present) and the proof passes on that copy too; bunx tsc --noEmit stays at HEAD's 29 diagnostics, none in the touched files; bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §627. Q281, A281.
C345 (/orders/new + /orders/[id] + src/lib/server/poLifecycle.ts, owner): the production order now has a life. A PROD order is stamped role = po, trigger = so|stock, planned_mtrs = the sale order's remaining metres rounded up to 100 (own qty when no SO is carried or nothing is left), and the one rate the form knows — picks × ₹/pick ÷ 100 — is committed as a cost_legweaving, level = po, provisional and frozen; yarn legs stay C344's issue path. No column was added for the SO link: it rides the po:planned event (Q282/A282). Completion prices the good metres at incurred ÷ good mtrs (leg accrual, frozen onto the order with unit_cost/frozen_at), writes the unassigned output to the produced pool free at that unit cost, keeps min(assigned, good) as a locked apportionment re-priced to the new unit cost, and shrinks the assignment for a shortfall (the difference released at its locked cost) while opening po:shortfall; queue.ts turns every open shortfall into a dashboard todo (PO … short by n m — apportion the gap) through the pure shortfallTodos, cleared by a po:shortfall:apportioned event. Cancellation releases tagged yarn as a zero-value out+in pair (kg and WAC untouched, tag nets to zero), leaves produced fabric free, accrues no conversion (conversionAccrued: 0) and records its reason as an amendment; the plan is editable only with a confirm prompt (blank/junk/zero refused, rounds to 100, before→after amendment, locked once completed or cancelled). ProductionOrderRecord.svelte on /orders/[id] shows badge, trigger, planned/apportioned/free metres, incurred, unit cost and legs, with complete / cancel / change-the-plan forms validated the same on both surfaces, and the SO record gains Raise production order (?soId=). No migration, no edit/delete screen. Proved non-UI: bun run src/lib/server/poLifecycle.test.ts12/12 PASS and bun /tmp/opencode/c345-proof.ts on a pushed copy of data.db38/38 PASS (800 m plan from a 4,250/5,000 m SO, ₹10.24/m frozen weave, ₹54.56/m on 700 good m, 100 m shortfall released and re-priced, board todo, stock PO 1,200 m all free, 100 kg released with WAC unchanged and no conversion, confirm gate); bunx tsc --noEmit adds no diagnostic in the touched files (HEAD's 141), bun run check clean on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §628. Q282, A282, Q283, A283.
C346 (/orders/new + src/lib/server/soCoverage.ts, owner): the sale order is now covered when it is booked. New soCoverage.ts holds the pure plan (parseCoverage, planSoCoverage, coverageBalanceError, saleKindError, soMargin) and the writer writeSoCoverage, which writes the SO, its cost_apportionment lines and any auto production order inside one transaction — a failed line rolls the sale order back with it, so nothing is ever saved pending or half-covered. The four sources are the row's: the produced pool at the WAC of its moment (locked onto the line), an existing production order's unassigned output at its own cost (frozen unit_cost, else committed legs ÷ plan; provisional while unfrozen), market = the purchased track at its own WAC (never blended with produced, and what C349 will feed), and new_po = the rest, a role po/PROD/trigger so shell whose planned_mtrs is the gap rounded up to 100 while the apportionment it carries is the exact gap (a 1,234 m sale plans 1,300 and covers 1,234). The balance must equal the sale qty exactly — short and over are both refused, availability and cost are re-read at save (a posted cost is never trusted), and the payload is parsed strictly (unknown source, missing key/PO, qty ≤ 0, duplicate source, junk JSON). sale_kind (normal|damaged, blank → normal) and parent_so_id (damaged only, must name a live SO, refused on a normal sale) are validated on both surfaces. The wizard gains the SO-only coverage step after qty (FOCUS-ENTRY popup: sale kind, parent select, one line per source with free metres and ₹/m, Cover the rest with a new production order, live value · covered cost · margin), posting hidden saleKind/parentSoId/coverageJson declared in ORDER_INTENTIONALLY_HIDDEN, and the check-and-save sheet gains a Cost coverage group. Coverage is optional (Q284/A284): no plan posts nothing, so every existing SO books as before. No migration, no edit/delete screen. Proved non-UI: bun run src/lib/server/soCoverage.test.ts9/9 PASS and bun /tmp/opencode/c346-proof.ts on a copy of data.db5/5 PASS (separate produced/market WACs, PO at its cost, one tx writing SO + two apportionments summing exactly to 5,000 + the 2,000 m auto PO, exact gap against a rounded-up plan, short/over refused, rollback leaving no SO and no apportionment); bunx tsc --noEmit adds no diagnostic in the touched files (HEAD's 141), bun run check clean on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §629. Q284, A284, Q285, A285.
C347 (src/routes/orders/[id] + new src/lib/server/soAmendment.ts + src/lib/server/soCoverage.ts + poLifecycle.ts + ops.ts, owner): the sale order now changes by releasing what it held at the cost it held it at, and covers again through one rebalance action. soCoverage.ts gains the pure plan — ApportionLine/outstandingOf/coveredMtrs, releasePlan (newest hold first, each metre at its locked unitCost, locked → part_released → released), soQuantityPlan (a cut is a release with no gap, a rise is a gap with nothing released, a cut deeper than what is apportioned refused with the numbers), soReassignmentPlan/soCancelPlan (release everything at locked cost; a cancel names the production orders whose hold came back) — plus the four input rules on both surfaces (soQtyError metres to 2dp > 0, soRateError money ≥ 0, soPickError whole > 0, soTermsError ≤ 280, quality must be a quality-master row, reason required ≤ 280) and a coverageOf that now returns each hold's id, source, released/outstanding metres, status, quality and pick. New soAmendment.ts is the applier: amendSaleOrder for qty/rate/terms/quality/pick (rate and terms move no apportionment; a quality or pick change releases every hold so the next cover is written from scratch, never mixed), rebalanceSaleOrder the single pricing action (gap recomputed from the sale, sources re-read live, exact balance or refused, lines written through C346's writeSoCoverage, so:rebalanced event), cancelSaleOrder (release all, flag cancelled, delete amendment, so:cancelled event listing the surviving POs) and releaseSoHolds for the edit path — every verb writes its rows in a transaction. ops.applyOrderChange sends a C152 Delete through cancelSaleOrder and releases the cut/rebind after an approved edit (dynamic import, no import cycle), so /api/amend enforces the same invariant. poLifecycle.poCancelError + cancelProductionOrder refuse an already-cancelled order, a complete one (cost frozen) and one a live sale still holds (Sale order SO-x still holds n m of this order — release that hold first). The SO record gains a Cost coverage card (sale qty · covered · uncovered · locked cost · value · margin, one row per hold with ₹/m, value and locked/part-released/released, Fill the gap with one line per live source and Save enabled only at an exact balance, Cancel sale order with a reason saying the holds release at locked cost and the POs survive) and the Amend modal gains Terms/Quality/Picks for an SO, with Quantity and Rate routed through the sale applier. No migration, no edit/delete screen. Proved non-UI: bun run src/lib/server/soCoverage.test.ts16/16 PASS, bun run src/lib/server/poLifecycle.test.ts12/12 PASS and bun /tmp/opencode/c347-proof.ts on a copy of data.db8/8 PASS (cut releases market ₹61 + PO ₹48.25 at their locked costs, held PO refuses to cancel, rise → 2,000 m gap → rebalance opens a new PO for exactly the gap, quality change releases all 6,000 m and re-covers under the new key, rate/terms move no hold, cancel releases all 6,000 m and frees the PO to cancel); bunx tsc --noEmit adds no diagnostic in the touched files (HEAD's 141), bun run check clean on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §630. Q286, A286, Q287, A287.
C348 (/checking, owner): R now ships and X is salvage. Checking grades are A|B|C|R|X through one rule on both surfaces — new src/lib/server/rollGrade.ts (normalizeGrade, gradeError = Grade must be A, B, C, R or X, gradeStatus, isSalvageGrade, salvageGradeSaleError, salvageEntry) — so the five tap buttons and the route's server check can never drift. R grades to FG (sellable, cost-bearing like A/B/C) instead of reject; X (absolutely non-usable) grades to reject and the checking save appends one zero-cost pool_entrytrack = salvage, source = salvage, unit_cost = 0, value = 0 keyed by the design's quality + pick plus a roll:salvage event, so an X roll can never move the produced WAC (costing/pool.ts already keeps the salvage track and zero-cost entries out of the average) — no produced-track out is written, because the produced pool is keyed by quality+pick and cannot be attributed back to one roll. X is gated by the damaged-SO marking: naming a normal sale order is refused (Grade X is salvage only — it can be sold on a damaged sale order…) on the server and in the form (xOrderInvalid blocks the review sheet), an X roll with no order stays salvage stock, and the order picker now marks · damaged lot. costing/pnl.ts gains salvageOwner (a damaged SO's proceeds roll into its parentSoId, else stay in its own P&L) and salvageFor (filter + the existing salvageRollup). UI: five grade buttons, an R/X hint line, badge colours (X error, R info) — light mode, daisyUI only. No migration, no new POST name, no edit/delete screen. Proved non-UI: bun run src/lib/server/rollGrade.test.ts5/5 PASS and bun /tmp/opencode/c348-proof.ts on a pushed scratch DB 15/15 PASS (the salvage row read back through the shipped schema, produced WAC ₹70/m unmoved, salvage priced at 0); bunx tsc --noEmit adds no diagnostic in the touched files (HEAD's 141). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §631. Q288, A288.
C349 (/fabric-purchase + new src/lib/server/fabricPurchase.ts, owner): market fabric can now be bought into the cost model. One screen — eight fields (supplier, quality, pick, mtrs, rate ₹/m, freight, GST %, date) plus "assign to a sale order or leave it in the pool" — writes, in one transaction, an order (type = PURCHASE, role = fabric_purchase, rate = the invoiced ₹/m), its onecost_leg (kind = purchase, level = po, rate = the landed ₹/m, gst_pct, provisional, frozen) and the pool_entry on the purchased track (quality_key + pick, value = qty × landed), with a marketcost_apportionment (locked at the landed cost) only when a live SO is picked; the fabric:purchase event is logged after the commit. Freight is capitalised (landedValue = mtrs × rate + freight), GST is input credit and stays on the leg — so the pool and the leg agree to the paisa (Q289/A289). The purchased average is its own track keyed quality + pick: the panel shows purchased metres/₹·m beside produced metres/₹·m and the live "current → landed → new purchased average, produced unchanged" line, which is exactly the sum the pool will hold (purchasePreview). Assignment refuses over-coverage (Only n m of SO-… is still uncovered — this purchase is m m) and writes no pool out entry, the same rule C346's market source already follows (Q290/A290). Every field validates on both surfaces (supplier = party row, quality = design-master quality so a typo cannot open a second pool, picks whole > 0, metres/rate/freight 2dp with metres and rate > 0, GST 0–28, date real and not future), with the per-field map returned by the route. No migration — C342's columns already carry it, and type = PURCHASE is a new word precisely so C346's PROD source never picks it up; no new owner gate (Q291/A291). Sidebar: "Fabric purchase" in Orders (owner/planner/accounts), +layout.server.ts gate added, nav-audit ALL PASS. Proved non-UI: bun run src/lib/server/fabricPurchase.test.ts 8/8 PASS, /tmp/opencode/c349-proof.ts on a VACUUM copy of data.db 21/21 PASS (produced WAC ₹70/m → ₹70/m across a purchase, plus the route's own load and create action), tsc adds no diagnostic in the touched files.
C350 (/reports + /lots + src/lib/server/reports.ts, owner): the stock report now reads the cost pools back. /reports replaces the lot-status / latest-rate table with two: yarn by count + type + grade — total = the pool balance (Σ in − Σ out, C343's poolBalance, the same average issues are priced at), own/party and in-house/sizer/weaver/transit split by the lot each entry names (per-lot netting, so both splits sum to the total), free + apportioned = total with apportioned = the kg a still-live production order still holds, and WIP beside the balance (yarn already issued to a live order has left the pool), valued at WAC; and fabric by quality + pick (+ the grade a hold or a roll names) on the produced and purchased tracks, each at its own WAC — the produced pool only holds a PO's unassigned output so free + apportioned is all of it, while a market hold is subtracted from the purchased free metres rather than double counted (Q290/A290), and a WIP roll shows uncosted. Live lots that predate the pool wiring (2,052 rows, pool_entry empty) are not blanked: a lot with no pool receipt becomes its own opening line at blank WAC — kg is stock, no price is invented (Q281/A281) — and source prints pool/opening. Three filters on the existing screen, the row's own — track / grade / pick — as GET params with one rule each on both surfaces (parseStockFilters: pick whole > 0, grade A+/A/B/C case-folded, track yarn|produced|purchased, junk dropped and never queried; type=number min=1 step=1 max=9999 on the input). /lots' Pool column gains the wip state (an out entry to a live order) beside free / apportioned, and the dump edge gains the fabricstock slug (stock keeps its name and now streams the pool lines). No migration, no new screen, no write path, no edit/delete UI. Proved non-UI: bun run src/lib/server/stockReport.test.ts14/14 PASS and bun /tmp/opencode/c350-proof.ts on a copy of data.db31/31 PASS (240 kg pool, own 40 / party 200, godown 40 / sizer 200, apportioned 100 / free 140 / WIP 60, WAC 106.67 and ₹25,400, produced 100 m at ₹70 + a ₹54.56 hold, market hold 100 of 300 m subtracted not doubled, WIP roll 30 m uncosted, opening lot 500 kg at blank WAC, and the filters through the real builders). bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §633. Q292–Q294, A292–A294.
C351 (src/routes/gatepasses/new + src/lib/server/lots.ts + the GP record, owner): a direct delivery may now unload at a weaver. The DIRECT location picker lists the Weaving-category parties beside the godowns and sizing units ((weaver) on the label) and posts its own word — destPlace gains weaver beside our-godown|premises|sizer, so the creation event and the GP record say where the yarn actually is ("Named weaver") instead of calling a weaver's shed a sizer's (Q295/A295). lots.ts gains the pure directLotLocation(category) — Sizing → sizer, Weaver → weaver, anything else (our premises, a godown, no party) → godown — and createLotsFromGatepass uses it, so a DIRECT to a weaver creates the lot at the weaver's shed; weaver is a word the lot table and the stock report already count, so /reports shows that kg under weaver with no change there. No WAC change: the receipt's own ₹/kg still sets the pool entry and no pool code was touched. Both surfaces validate: the picker sets destPlace from the picked party's category and offers only place rows, and the route re-reads the party and applies the same pure rule directDestinationError (a weaver location needs a Weaver party, a sizer location a Sizing unit or godown, the premises none) as a visible error on targetPartyId, so a hand-forged POST cannot misplace the stock; the carried ?destPlace= round-trip accepts weaver. The weaver rows are additive to the DIRECT picker only (yarn-in pickup places and the beams' sizer list unchanged). No migration, no new screen, no edit/delete UI. Proved non-UI: bun run src/lib/server/directDelivery.test.ts6/6 PASS and bun /tmp/opencode/c351-proof.ts on a copy of data.db6/6 PASS (DIRECT to a Weaver party → lot location = weaver, parked, unpriced receipt entry at no invented cost; DIRECT to a Sizing party still sizer), plus a schema parse that accepts weaver and refuses junk. bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §634. Q295, A295.
C352 (/bookings/new + /inward + src/lib/server/landedCost.ts, owner): yarn landed cost, and a short receipt is absorbed by the kg that arrived. booking gains two nullable columns, freight_amt and brokerage_amt (drizzle/0051_bent_fenris.sql), and the booking wizard's rate step gains the two rupee inputs beside the rate and the GST basis (digits + one dot, ≤ 2 decimals, ≤ 7 integer digits client-side; the zod schema refuses negatives, three decimals, junk and ≥ ₹1,00,00,000 with its own message, and both ride the booking:created event). They sit beside the yarn amount — basic/gst/total are unchanged, because GST is input credit (Q296/A296). New pure landedCost.ts: landedCost({billedKg, ratePerKg, basicAmt, freightAmt, brokerageAmt, receivedKg}) → billed/arrived/short kg, the basic amount (stored basicAmt else rate × kg), freight, brokerage, landed amount and landed ₹/kg = landed amount ÷ arrived kg (full precision, so the pool value is the billed amount to the paisa), plus shortReceipt() for its own check line; with nothing extra on a full receipt it is exactly the booked ₹/kg, so every existing booking lands where it always did. lots.ts lands the yarn pool in entry at that rate — yarnInLanded = unit_cost the landed ₹/kg, value the landed amount — so 1,000 kg at ₹250 + ₹5,000 freight + ₹2,500 brokerage over 950 kg arrives at ₹271.0526/kg, and because the pool is keyed count+type+grade and an issue prices at its WAC, the landed rate reaches the yarn WAC and the PO's yarn cost leg with no change to costing/. /inward gains three automatic rows in the receipt summary — Booked kg (with the booking no.), Billed vs arrived ("n kg short — absorbed" / "Full") and Landed ₹/kg — recomputed live from the typed arrived weight, and markDelivered logs billedKg/shortKg/landedRate on the gatepass:delivered event; receiptChecks() reads the basis for the whole board in two queries. No edit/delete screen, one additive migration, no new route or POST name. Proved non-UI: bun test src/lib/server/landedCost.test.ts8/8 PASS (scratch cwd, bunfig root → src/, no tests/ root in this checkout), bun /tmp/opencode/proof-landed.ts on a scratch DB pushed to the shipped schema (billed 1,000 vs arrived 950, short 50, landed ₹271.0526; pool in at ₹271.0526/kg value ₹2,57,500 on key 40|CW|A; issueYarnToOrder freezing the PO yarn leg at that same WAC; the same gatepass at 1,000 kg → ₹257.5/kg), and bun /tmp/opencode/proof-booking-schema.ts8/8 (blank/2-decimal/zero accepted; negative, 3-decimal, junk and over-large refused). bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §635. Q296, A296.
C352 re-check (row lane, bookings/new/schema.ts): the freight / purchase-brokerage cap was inclusive, so ₹1,00,00,000 passed while the entry input stops at seven integer digits — now .lt(10_000_000, '… must be below ₹1,00,00,000') on both fields, matching PLANNING §635. Re-proved non-UI: landedCost.test.ts 8/8 PASS, schema parse refuses negative / three-decimal / junk / ≥ ₹1,00,00,000 and accepts blank, 0 and 2-decimal, bun run check exit 0, bunx tsc --noEmit adds no diagnostic in the touched files. No UI test; suite / build / deploy left to the watcher.
C353 (src/lib/server/costing + src/lib/legBasis.ts + /orders/[id], owner): a cost leg is now charged on its own base. One pure table (src/lib/legBasis.ts, re-exported by costing/index.ts, and LegKind/LegLevel/LegRateUnit now alias its unions) prices every kind the same way on both surfaces: warping ₹/kg of warp; sizing ₹/kg of warp sent, where the bill's own billing kg (typed, or reversed from the counts as kg sent × count sent ÷ count received) becomes the leg's quantity with the gap kept as the quantity variance in reason; weaving picks/m × ₹/pick ÷ 100 on the good mtrs, the rate booked per order (no master — the market moves and the rate is width-sensitive) with an internal rate for in-house looms, stored as ₹/m like C345's commitment; checking ₹ per metre actually checked including X, at the quality's rate (checkingRateFor: exact year → newest year before it → the quality's yearly default → the global default → nothing); mending never charged (refused on both surfaces); packing, loading, freight and exceptional as direct ₹ at PO level (Q298/A298). bookCostLeg (poLifecycle.ts) writes one append-only cost_leg + a po:leg event, actual when a bill no is given; the running production order's new Book a cost card posts ?/poLeg, switches its fields to the chosen kind's base, previews the amount with the same legDraft the action runs, and validates every field on both surfaces (2-decimal positive quantity/rate/picks/good mtrs, a count pair 0–400, the internal rate when in-house, a junk billing kg and a zero direct amount refused). poRecord also carries picks/jobRate for the prefill. No migration, no column, no new route, no edit/delete. Proved non-UI: costing.test.ts19/19, /tmp/opencode/c353-proof.ts on a data.db copy 27/27 (all seven legs book, refusals write nothing, freeze and SO refused, accrual ₹30,993.33), bunx tsc --noEmit adds no diagnostic in the touched files, bun run check exit 0. No UI test.
C354 (src/routes/bills + new src/lib/bills.ts + src/lib/server/bills.ts, owner): a bill now settles the committed cost and restates everything built on it. A bill — vendor, bill no, bill date, kind, amount ₹, GST %, optional billed quantity — is allocated by allocateBill over the open (provisional, non-legacy, level = po) legs of its kind, pro rata to the quantity each committed, last line absorbing the rounding so the lines add back to the bill to the paisa; a bill with no stated quantity counts as the committed quantity, so it is all rate variance and no phantom quantity gap (Q299/A299). Per line the gap splits into quantity(billed − committed qty) × committed rate and rate as the remainder, so qty + rate = total always holds. The settled leg becomes actual with the billed amount/qty/rate, bill no/date/party and GST; the new order cost (Σ non-legacy legs ÷ produced mtrs) is written to the order, to every produced pool_entry of it and to every cost_apportionment that drew from it, so each sale order's locked cost moves by exactly the variance — the in-place move is kept on the ledger as an amendment plus a po:restated event and the bill itself is one bill:recorded event with its allocations (Q300/A300). A sale order's pnlStatus is derived: no so:pnl:closed event = provisional, one = closed; the last bill closes it by itself, and a missing bill can be closed by hand only with a reason — that reason is the flag on the close (Q301/A301). Bills are typed on the new screen or pasted (bill no, date, vendor, kind, amount, GST %, qty per line, parseBillImport reports a bad line by number and never guesses). No migration, no new table, no edit/delete UI; a repeated bill number from the same vendor and a never-billed kind (mending) are refused. Every field validates on both surfaces with the same predicates (bill no ≤ 40, vendor a party row, kind a charged kind, amount > 0 ≤ 2dp, GST 0–28 ≤ 2dp, quantity > 0 ≤ 2dp, date real, in the trading window, not future). Sidebar "Bills" in Orders (owner/planner/accounts) with the +layout.server.ts gate and nav-audit ALL PASS; billsPending() adds the bills-pending row to the queue board (Q302/A302). Proved non-UI: bun run src/lib/server/bills.test.ts6/6 PASS, bun /tmp/opencode/c354-proof.ts on a data.db copy 30/30 PASS (₹6,600/95 kg against a ₹7,000/100 kg leg → −₹350 quantity, −₹50 rate; order ₹33.75 → ₹33.25; pool entry and sale-order hold restated to ₹26,600; last bill auto-closed the P&L; duplicate bill and mending refused; a manual close carries its flag) plus bun /tmp/opencode/c354-route.ts12/12 PASS through the real route module (the /billsload returns vendors, open legs, billable rows, bills and pending sale orders; the create action refuses an empty bill naming each field and saves a ₹240 packing bill that restates its order; the pasted list records its bill). bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §637. Q300–Q303, A300–A303.
C354 re-check (row lane): the bills work is re-proved end to end and its four decisions are on record. Re-ran the row's own proofs, all green: bun run src/lib/server/bills.test.ts6/6 PASS, /tmp/opencode/c354-proof.ts on a copy of data.db30/30 PASS (₹6,600/95 kg bill against a ₹7,000/100 kg leg → −₹350 quantity, −₹50 rate, split adding back to −₹400; leg actual with bill no/GST/amount/95 kg; order ₹33.75 → ₹33.25; produced pool entry and sale-order hold restated to ₹33.25/₹26,600; last bill auto-closed the P&L; duplicate bill number and mending refused; a manual close carrying its flag), /tmp/opencode/c354-route.ts12/12 PASS through the real route module, and a fresh board proof /tmp/opencode/c354-board2.tsPASS — with one provisional packing leg outstanding the owner board's exceptions section reads BOARDPO-… · packing unbilled ₹2,400 → enter the bill, so the bills-pending row is live on the board and not only in billsPending(). bunx tsc --noEmit adds no diagnostic in src/lib/bills.ts, src/lib/server/bills.ts, src/lib/server/bills.test.ts, src/lib/legBasis.ts or src/routes/bills. The four Pareto answers the row was carrying as assumptions are now asked and answered rather than merely asserted — Q300/A300 (pro rata by committed quantity, last line absorbing the rounding, no stated quantity = all rate variance), Q301/A301 (pool entry and holds restated in place, amendment + po:restated), Q302/A302 (P&L status derived from so:pnl:closed, last bill auto-closes, a manual close needs its reason as the flag), Q303/A303 (no bill table — the header is the bill:recorded event, the bills-pending row is a board exception, an import reads one bill per line and reports a bad line by number) — and PLANNING §637 now cites those numbers instead of the earlier phantom Q299–Q302, which in docs/QUESTIONS.md belongs to C353's retry. No code changed in this pass. No UI test was run. Suite / build / deploy left to the watcher.
C355 (/orders/[id] + src/lib/server/costing/pnl.ts + yarnMath.ts + the design master default, owner): the sale order now has a P&L on the metres actually delivered, and every charge in it is a share of the selling price. New pure soPnlLines (+ the one primitive it shares with the quote engine, grossUpPrice/grossUpCharge) in costing/pnl.ts: revenue = delivered metres × rate; cost = those metres at the locked cost of the hold (coverage.cost ÷ covered, else the locked yarn basis ÷ ordered metres, and the card says "no locked cost yet" when neither); then CD availed (the slab this order took), credit cost = the dhara master's max CD − availed CD, brokerage, overhead 1%, freight-out, insurance, octroi & statutory, sample — each exactly p% of the revenue, with the margin as the remainder and its % on the price. The record gains a P & L card (one row a line, the credit cost naming max − availed, the dhara on the subtitle) and a Change the shares form posting the five percentages; the save is an append-only so:pnl:lines event, newest read by the load (Q305/A305), and the company-wide default is the design master's Other expense % — now 1 instead of 6.5 (schema default 1, drizzle/0052_eminent_jack_murdock.sql, quote new/edit fall back to 1). computeCosting is corrected to the same gross-up: overhead, dhara and brokerage come off one price (subtotal × 100/(100 − Σp)), so each is exactly its share instead of a % of cost, with no dhara/brokerage a quote is now subtotal × 1/99. Same rule on both surfaces: 0–100, ≤ 2 decimals, the five together below 100 (min/max/step + a disabled Save on the client, the same regex and sum check in the action, the field named on refusal). No column, no new route, no edit/delete screen. Proved non-UI: bun run src/lib/server/costing/costing.test.ts22/22 PASS (the new split, gross-up and quote-parity cases) and /tmp/opencode/c355-proof.ts on a copy of data.db through the real route module 25/25 PASS (750 m delivered → ₹75,000 revenue, ₹45,000 cost at the locked ₹60/m, dhara 4% max vs 2% availed → ₹1,500 credit, ₹1,500 CD, ₹1,125 brokerage, ₹750 overhead, ₹25,125 margin; three decimals and 60+50% refused, a clean reading saved and read back, a sale with no dhara at zero credit cost, nothing delivered at zero revenue); the schema push on a data.db copy Changes applied with 627 designs preserved, evt index and WAL re-asserted; bunx tsc --noEmit stays at HEAD's 29 with none in the touched files, bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §638. Q305, A305.
C355 re-check (row lane, /quote/new + /quote/edit + the demo seeds): the P&L work was re-proved end to end — costing.test.ts22/22, /tmp/opencode/c355-proof.ts on a data.db copy through the real /orders/[id] route module 25/25 PASS (750 m delivered → ₹75,000 revenue, ₹45,000 cost at the locked ₹60/m, dhara max 4% vs availed 2% → ₹1,500 CD + ₹1,500 credit + ₹1,125 brokerage + ₹750 overhead, ₹25,125 margin; three decimals and 60+50% refused naming the field; a clean reading saved and read back; no dhara = no credit cost; nothing delivered = no revenue) — and one gap was closed: every one of the 627 designs still stores the sheet's 6.5 and both quote screens took the master as a fallback, so quotes were still priced at 6.5% overhead despite the engine default being 1. Per owner §252 (Other % quote-only, never the master) the master is no longer a source on either surface: /quote/new's designData prefills otherExpensePct: null and /quote/edit's resolveQuoteInput passes null as the design value, so both fall to the 1% company overhead while a quote's own saved reading still wins. No data rewritten (reversible with git; old designs keep their history) and scripts/seed-demo.ts / scripts/seed-e2e.ts now write 1 instead of 6.5. Proved non-UI: /tmp/opencode/c355-quote-proof.ts on a copy of data.db through the real /quote/new and /quote/editload functions 9/9 PASS (a 6.5 design hands the wizard null while the rest of it still prefills, the edit form reads 1, the engine overhead is the 1% share of the price, the price is the gross-up ₹56.31 vs ₹59.73 with 6.5). bunx tsc --noEmit adds no diagnostic at the touched lines; no UI test was run. Suite / build / deploy left to the watcher. PLANNING §638 (re-check). Q305, A305.
C357 (/lots/opening + new src/lib/openingStock.ts + src/lib/server/openingStock.ts, owner): the cost pools now start from real cutover stock. One line per pool key — yarn by count + type + grade (kg, ₹), fabric by quality + pick on the produced or purchased track (mtrs, ₹) — appended to pool_entry as source='opening', direction='in', with the sheet's ₹ as value and value ÷ qty as unit_cost, so every later receipt, issue and apportionment averages over the stock that was actually on the floor instead of an empty pool. No new table and no migration: C342 already reserved the opening source. A key that already has an opening line is refused and the paste import is all-or-nothing — the write is one transaction that rolls back when any line is refused, so nothing lands half-imported (an unreadable or repeated line writes nothing and names its line number), so re-pasting the same cutover sheet can never double the stock; corrections stay append-only. Validation is one pure rule (openingLineError) applied in the action and on the form: count 0–500 (2 dp), type in the master's code format, grade blank/A+/A/B/C, quality the design master's own name (1–60 characters, read from the master so commas and inch marks land as they are), pick a whole 1–400, qty > 0 ≤ 10,00,000 (2 dp), value optional where blank books zero-cost stock exactly like an unpriced receipt. The screen is C349's /fabric-purchase shape (daisyUI card + ReviewSheet, live pool key and ₹/unit) with a paste-import card (yarn, 61, CCW, A, 1200, 300000 / produced, 55/5*65, 64, 800, 96000, tab-separated when a quality name carries a comma), with a fabric line's quality resolved against the design master and rewritten to its spelling, and the opening lines listed; it is linked beside Cone stocktake on /lots. Proved non-UI: bun src/lib/openingStock.test.ts10/10 PASS (keys match yarnKey/fabricKey, every field rule, paste parsing with header/# comments and line-numbered refusals — tab-separated when a master quality name carries a comma — opening + a ₹275/kg receipt = ₹260/kg, unpriced line never moves the average, and 626/627 design-master quality names accepted as they are) and a data.db-copy proof through the real recordOpeningStock/openingStockRows10/10 PASS (two lines written, a duplicate refused, a mixed batch writing nothing, right labels/values, ₹250/kg and ₹120/m, one stock:opening event per written row); bunx tsc --noEmit adds no diagnostic in the touched files and bun run check reports no error in them. No UI test. PLANNING §639. Q308/A308, Q309/A309.
C356 (src/routes/expenses + #lib/exceptional + costing/pnl, owner): an exceptional charge is one append-only cost_leg on one document. The screen /expenses (nav: Orders → Expenses, next to Bills) opens on Direct to P&L (default) and lists live sale orders, or switches to Capitalised (moves WAC) and lists live producing orders with what each already carries and what it made, so the form shows the ₹/m the charge will move; it is the C349/C357 daisyUI card + ReviewSheet shape (a one-time office entry, not the FocusEntry wizard), with the booked charges and both running totals underneath. docRef (1–40) and reason (1–280) are required — the row says the charge comes with its paper, and an append-only ledger has no later step to add one (Q310/A310); the amount is the standard rupee rule (2 dp, > 0, under ₹1 crore) in one pure table (src/lib/exceptional.ts, exceptionalLevel/Amount/DocRef/ReasonError + exceptionalDraft) that the form's derived errors and the action both call. Level po writes the leg and runs the same restateOrder cascade a bill runs, in the same transaction (Q311/A311): the order's unit_cost, its produced pool entries and every hold that drew from it move together; a frozen producing order refuses; the leg is actual (document in hand), so no bill list ever re-settles it. Level so is a direct cost of that one sale: soPnlLines takes exceptional and the order page's P&L card gains one line, the margin falling by it, with no metre cost touched. The producing order's “Book a cost” form no longer offers exceptional and links here instead. No migration, no new column, no edit/delete. Proved non-UI (no UI test): bun src/lib/server/exceptional.test.ts5/5 PASS (level default/refusal, every field rule, the draft, a so leg off the margin and out of the cost, a po leg ₹25 → ₹30/m restating a 400 m hold to ₹12,000) and /tmp/opencode/c356-proof.ts on a data.db copy through the real route module 34/34 PASS (a ₹2,500 sale charge booked actual with ref+reason and read back in the order's P&L margin; a ₹4,000 producing charge moving the order ₹25 → ₹30/m, one sale restated, pool ₹24,000, hold ₹12,000, the event on the history; six refusals writing nothing; the screen's rows/totals/lists/prefill). bunx tsc --noEmit adds no diagnostic in the touched files (147 repo-wide, 151 before), bun run check exit 0 with none in them, bun scripts/nav-audit.ts ALL PASS. PLANNING §640. Q310/A310, Q311/A311.
C358 (/orders/[id] + new src/lib/server/soFulfilment.ts + ops.ts + /approvals, owner): a sale order can now be marked fulfilled by hand, inside its tolerance only. Delivered metres are the order's dispatch slips in loaded|delivered (dispatch's own auto-delivery measure); the card shows ordered / delivered / tolerance / difference and, outside ordered ± diff_mtr, blocks with "Amend the sale order quantity first" and writes nothing. Inside it: a shortfall releases the remainder through C347's release plan at each hold's locked cost; an excess must be apportioned from free stock or an existing open production order at current cost (the same sources the coverage card offers, a new production order refused), the lines adding exactly to the excess; an exact delivery just closes. Marking writes the existing delivered status, one amendment and a so:fulfilled event carrying delivered/ordered/tolerance, short, excess, released and covered numbers. Reversal rides C152's order_change rail as a new fulfil-reverse action — an owner/admin applies it at once (status back to in-prod, reverse amendment, so:fulfilment-reversed event); anyone else leaves a pending item on /approvals (label "Fulfilment reversal (recorded)") and the order stays delivered until the owner decides; the releases stay released, append-only. Note optional ≤ 280 (maxlength + fulfilmentNoteError), coverage quantities positive 2 dp within each source's free metres with Save disabled until balanced, reversal reason required ≤ 280 — the action names each refusal. No migration, no new table, no edit/delete. Proved non-UI (no UI test): bun run src/lib/server/soFulfilment.test.ts8/8 PASS; a data.db-copy service proof 13/13 PASS (985 m of 1000 m at ±25 marks, releases 15 m at locked ₹50/₹750, amendment + event; 950 m blocked, order stays in-prod; non-owner reversal pending, owner decision returns in-prod, non-owner decision refused); and two proofs through the real route module — the record's load + actions 13/13 PASS (delivered/short/within read on the page, mark releases 10 m, blocked 50 m refused with the order untouched, reversal pending for a non-owner, blank reason refused) and the long path 12/12 PASS (10 m excess inside ±25 refuses an empty pick and a new_po pick by name, then marks once 10 m are taken from the purchased pool, the extra landing as a locked ₹60/m hold). bunx tsc --noEmit adds no diagnostic in the touched files, bun run check exit 0 with none. Suite / build / deploy left to the watcher. PLANNING §641. Q312–Q314, A312–A314.
C357 re-verify (later lane, no new decision): the opening-stock screen and its pool_entry write re-proved end to end with no code change beyond one UI line. bun src/lib/openingStock.test.ts10/10 PASS; a copy-of-data.db proof through recordOpeningStock/openingStockRows11/11 PASS (two lines written, labels ₹250/kg and ₹120/m, source='opening'/direction='in'/note/by on the row, a repeat and a mixed batch writing nothing, one stock:opening event per line, opening 1200 kg @ ₹250 + 800 kg @ ₹275 = ₹260/kg over 2000 kg, a blank value adding 100 kg without moving the average, produced ≠ purchased for the same quality+pick, and /reports stock-by-pool listing the opening pool) and a proof through the real route module16/16 PASS (load's tracks/designs/grades/types, yarn and design-master-resolved fabric lines, four field refusals each naming its field and writing nothing, a repeated key refused, an unreadable/unknown-quality/duplicate paste writing nothing and echoing its text, a clean paste landing both tracks, pool_entry matching the screen). Clear now resets the stock type to Yarn while a saved line keeps it for the next line. bunx tsc --noEmit adds no diagnostic in src/routes/lots/opening or src/lib/openingStock.ts. No UI test. PLANNING §639 (re-verified).
C359 (src/lib/server/captureRouting.ts + src/lib/server/intake.ts + src/lib/server/captureOcr.ts + src/routes/gatepasses/new, owner): a read gatepass no longer opens as a DIRECT delivery just because the paper prints a godown address. The reader's destination is an intent word again — new pure destinationSane keeps godown / premises / sizer (a leading "our" stripped) or the paper's own "direct delivery" and drops anything else, so the Goenka paper's printed address ("KRISHNANUNJ GODOWN - 1, Shahapur, Ichalkaranji 416115") is no longer a destination; both model prompts now say the same. captureRouting gains the pure gatepassKindFromRead, the one order the kind is decided in: a read booking number wins as YARN_IN, else an exact godown / premises / sizer destination (or our godown) is DIRECT, else the words "direct delivery" are DIRECT, else no kind rides the link and the form keeps its own YARN_OUT default — the address is the receipt's location, never the movement, and it never fills destPlace either (Q317/A317, Q318/A318). The yarn the API already resolved now rides the prefill as ?yarnId=, so the yarn-in's one yarn field arrives selected and its count / type / mill come from the master row (C331), kept only when the yarn master knows the id (carriedYarnId, a phantom id lands empty like every other carry) and kept across a master round-trip (selfUrl). No new field, no new POST name, no migration, no edit/delete screen. Proved non-UI: bun run src/lib/server/captureRouting.test.ts7/7 PASS (the Goenka address read carries no kind; a booking number beside an address → YARN_IN; godown / our godown / OUR-GODOWN / premises / sizer and the words "direct delivery" → DIRECT; the yarn master rides the prefill; a phantom yarn id lands empty; the address is no destination for the reader) and bun /tmp/opencode/c359-proof.ts on a scratch copy of data.db12/12 PASS — the real extractCaptureJson read of the Goenka fields now yields ?draftId=…&kind=YARN_IN&bookingNo=1128&yarnId=y-mt-1&partnerGpNo=GSL%2F5546&countYarn=61+CCW&ticket=Sanathan&bags=40&gross=1020&net=1000 with no kind=DIRECT, the address-only read carries no kind, and an explicit direct-delivery word still opens DIRECT. bunx tsc --noEmit adds no diagnostic in the touched files (the one in captureOcr.ts is HEAD's own mergeDoc cast, only shifted a line). No UI test was run. Suite / build / deploy left to the watcher. PLANNING §642. Q317/A317, Q318/A318.
C360 (src/routes/gatepasses/new + src/lib/components/focus/FocusEntry.svelte + src/lib/components/gate/GatepassOcrRead.svelte + src/routes/masters + src/routes/bookings/new, owner): creating a master or a reference from inside a form no longer costs the form. One protocol in new src/lib/createModal.ts — createModalHref builds the child URL as the modal opens it (drops the returnTo= trip, moves the field= it carried onto the URL itself, sets modal=1), postCreated/postCancelled hand the answer to the parent window and are no-ops outside a modal — plus src/lib/components/focus/CreateModal.svelte: a full-height daisyUI sheet (z-80, over the wizard's z-40 and the popup's z-60) with ← Back to the form, ✕ and Escape, holding the child in a same-origin iframe (Q319/A319 — no second copy of the master forms or the booking wizard). FocusEntry gains an optional oncreate(href, fieldId) — with it every + <value> row is a button that opens the caller's modal, without it the row stays the same link, so no other form changes — and applyCreated(fieldId, value, label) joins its handle, which the old ?field=&value=&label= mount now runs through (GatepassOcrRead gains onyarncreate beside C319's oncreate, so the read card's yarn chip is the same modal). /masters in modal=1 posts {field,value,label,master} on save (and a cancel when its add dialog is closed unsaved) instead of goto(returnTo); /bookings/new in modal=1 posts {field:bookingRefs, value:id, label:bookingNo, master:'booking'} instead of the 5-second pop-up and goto('/bookings?created=…'). The gatepass keeps every value: a party saved there joins the lists it picks from (so its chip is answered), a yarn or a mill joins that field's option list (the summary reads its name, never the id), the trader lands on sourcePartyId, and the read's + Add Booking No. booking joins the reference table and is picked — showing its number and the read's yarn with — remainders (Q320/A320), the server resolving the real row from the DB on save. The yarn pick also stops wiping count/type/mill for an id outside this page's master read. No migration, no new POST name, no new route, no edit/delete. Proved non-UI: bun run src/lib/createModal.test.ts6/6 PASS (modal link drops returnTo and keeps its field, including the party masters' field carried in the return link; an own field= wins; the booking link keeps field=bookingRefs and the read number; blank opens nothing; flag/field read back; a save outside a modal posts nothing); bun run check exit 0 with no warning on any touched file, bunx tsc --noEmit adds no diagnostic in them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §643. Q319, A319, Q320, A320.
C359 re-check (row lane, no new decision, no code change): the shipped C359 pieces were re-proved on the current tree. bun run src/lib/server/captureRouting.test.ts7/7 PASS; /tmp/opencode/c359-proof.ts on a copy of data.db12/12 PASS; new /tmp/opencode/c359-route-proof.ts runs the REAL /gatepasses/new server pieces (capturePrefill + resolveKind, extracted from the shipped source as in C315) over the link the routing builds for the Goenka read — 12/12 PASS: the link is ?draftId=…&kind=YARN_IN&bookingNo=1128&yarnId=<master>&partnerGpNo=GSL%2F5546&countYarn=61+CCW&ticket=Sanathan&bags=40&gross=1020&net=1000 with no kind=DIRECT, the form opens YARN_IN and carries count/type/mill/bags/trader GP no/booking no, the resolved yarn master rides and a phantom id lands empty; an address-only read carries no kind and the form keeps its own YARN_OUT default; an explicit our godown still opens DIRECT. bunx tsc --noEmit adds no diagnostic in captureRouting.ts / intake.ts / gatepasses/new. No UI test was run (C65/C66). Suite / build / deploy left to the watcher.
C361 (/gatepasses/new?kind=YARN_IN, owner): the yarn-in form shows no total cones. C314/C332's derived figure stays derived but is no longer displayed — the read-only Total cones in this delivery row and the 18 bags × 60 = 1080 cones in this delivery working in the cones-per-bag subtitle are both gone, so the pack block is just Cones per bag and Bag weight (kg per bag). yarnConesTotal (bags × cones/bag) still posts in the existing cones field/column and the server still derives it when only the per-bag count arrives, so the lot's bag grain, the reports and the prints keep one total; the other kinds' Total cones (all bags) row and the gatepass record's own read-only line are untouched (Q322/A322). No new field, POST name, column, migration, route or edit/delete screen. Proved non-UI: /tmp/opencode/c361-proof.ts8/8 PASS (no conesTotalStatic, no Total cones in this delivery label, no total in the per-bag sub, cones-per-bag and bag-weight rows still there, still derived and posted, still hidden as a typed row), bun scripts/gatepass-wizard-audit.ts67/68 with the C332 sub check restated as C361's rule (the one remaining FAIL — the read card stays optional — fails at HEAD too: its regex reaches the photo row's required), and bunx tsc --noEmit adds no diagnostic in the touched files (147 repo-wide). No UI test was run (C65/C66). Suite / build / deploy left to the watcher. PLANNING §644. Q322, A322.
C360 re-pass (row lane, src/routes/gatepasses/new + src/lib/components/focus/FocusEntry.svelte + src/lib/components/focus/CreateModal.svelte + src/lib/createModal.ts + src/routes/masters + src/routes/bookings/new + new src/lib/poolKey.ts, owner): the modal-over-the-form work was already in the tree from the earlier lane (§643); this pass re-proved it, closed one landing gap, and cleared the build blocker that had failed every deploy since C356. The gap: the yarn-in pickup row's + chip carries field=location (that word served the old URL trip, where location meant "the place"); in the modal the row that asked is destPlace, so masterCreated now lands a location on destPlace on every kind except DIRECT — before this, a godown saved from the pickup row joined the lists and left the row itself untouched, which is not the row's acceptance. The blocker: C357's browser module src/lib/openingStock.ts imported #lib/server/costing/pool.ts, refused by SvelteKit's server_only_import guard (lots/opening/+page.svelte imports #lib/openingStock.ts imports #lib/server/costing/pool.ts), so C357/C358 were held and C359/C360 logged "deploy failed (1/2)" on that same error. The two key builders (yarnKey, fabricKey) now live in client-safe src/lib/poolKey.ts; costing/pool.ts imports and re-exports them (so costing/index.ts, lots.ts, reports.ts, fabricPurchase.ts are untouched) and PoolTrack is now KeyTrack's alias — one rule for the key, byte-identical strings, no behaviour change, reversible. Proved non-UI: bun run buildexit 0 (the gate that was red; 4,134 modules, adapter-bun server built), bun run src/lib/createModal.test.ts6/6 PASS, bun src/lib/openingStock.test.ts10/10 PASS (the keys it now imports are the engine's), bunx tsc --noEmit at HEAD's 29 diagnostics with none in any touched file, and the live app restarted onto the new build — /signin 200 and its _app/immutable assets 200 (a stale-hash 500 before the restart). No UI test was run (C65/C66). PLANNING §645.
C362 (/gatepasses/new?kind=YARN_IN + new src/lib/gatepassDocuments.ts + new src/lib/components/gate/GatepassDocuments.svelte, owner): the yarn-in's documents field now lists what was uploaded or read, downloads any of it, and takes new papers several at a time. A new card under the intimation-number block shows the GP photo, the OCR read's file and every paper added here — each with a Download (/upload-file/<name>?bin=screenshots, the drop's own attachment; the capture file via /capture-img/<name>) — and one multiple file input posts every picked file in a single request to the same /upload-file?bin=screenshots the photo row uses, appending the returned names. The list rides a new hidden POST name documents (one saved name per line) and the gatepass:<kind> creation event payload, so no column and no migration; /gatepasses/<id>'s existing "Gatepass uploads & dictation" card gains a Documents list with one download per name. One pure rule on both surfaces (gatepassDocuments / gatepassDocumentsError): safe upload names ([A-Za-z0-9._-]), png/jpg/webp/gif/pdf, ≤ 200 chars each, at most 10 per gatepass; the zod schema refuses with the exact message on documents, the card refuses before posting, the input carries accept="image/*,application/pdf". The GP photo, the OCR read and C214's legacy single document keep their own meanings (Q324/A324, Q325/A325). Proved non-UI (no UI test): bun test src/lib/gatepassDocuments.test.ts6/6 PASS and /tmp/opencode/c362-proof.ts on the real route modules 16/16 PASS (two files in one multipart POST land as two saved names, both download with content-disposition: attachment and their bytes, the list parses through the shipped schema, blanks accepted, traversal / .xlsx / spaced name / 11th file / over-long name each refused on documents); bunx tsc --noEmit stays at HEAD's 29 with none in the touched files, bun run check exit 0 with no warning in them. Suite / build / deploy left to the watcher. PLANNING §646. Q324, A324, Q325, A325.
C358 re-pass (row lane, src/routes/orders/[id] + src/lib/server/soFulfilment.ts + src/lib/server/ops.ts, owner): the fulfilment card was already on the tree from the earlier lane (§641) — this pass re-proved every branch of it on the current tree and closed the one type error sitting in the row's own route. A sale order marks fulfilled on the metres its dispatch slips (loaded | delivered) delivered, inside ordered ± diff_mtr only: outside it the card blocks with "Amend the sale order quantity first" and the action refuses with the same words, writing nothing; a shortfall inside releases the remainder at each hold's locked cost; an excess inside must be apportioned from free stock or an existing open production order at that source's current cost (a new_po pick refused by name) with the lines equal to the excess before Save enables; an exact delivery just closes; reversal is a fulfil-reverse order change — owner/admin applies at once, anyone else leaves a pending /approvals item — writing a reverse amendment and a so:fulfilment-reversed event, releases staying released, append-only. The gap closed: orders/[id]/+page.server.ts writes an ops-complete amendment on the C154 operations completion but appendAmendment's kind union never admitted the word (a live TS2322 in the row's target); the union now carries 'ops-complete' — the value already in that text column, a widening only, no write or reader changed, no migration. Validation re-checked on both surfaces, nothing added: note ≤ 280 (maxlength + fulfilmentNoteError), coverage quantities min 0 / max free m / step 0.01 with Save disabled until balanced and excessCoverageError naming blank / inexact / new_po picks, reversal reason required ≤ 280. Proved non-UI (no UI test, C65/C66): bun run src/lib/server/soFulfilment.test.ts8/8 PASS; /tmp/opencode/c358-route.ts through the real route module13/13 PASS (985 m of 1000 m at ±25 marks and releases 10 m, 950 m blocked with the order untouched, non-owner reversal pending, blank reason refused); /tmp/opencode/c358-excess.ts12/12 PASS (10 m excess inside ±25 refuses a blank and a new_po pick, then marks from the purchased pool at ₹60/m); /tmp/opencode/fulfil-proof.ts13/13 PASS (locked ₹50/m release, amendment + so:fulfilled numbers, blocked order staying in-prod, owner approval returning in-prod, non-owner decision refused) — 46/46, all on a data.db copy. bunx tsc --noEmit147 → 146, none left in soFulfilment.ts, ops.ts or orders/[id]. Suite / build / deploy left to the watcher. PLANNING §647.
C357 re-pass (row lane, src/routes/lots/opening + src/lib/openingStock.ts + its test, owner): an opening line can only land on the pool key a receipt will hit. The shipped screen (§639) already wrote one pool_entry per pool key with source='opening' and the WAC started from it; the hole closed here is the key's spelling. yarnKey is case-sensitive on purpose (the type master keeps Dn/CW/CCW), so a cutover sheet typing ccw opened a second bucket beside CCW and the real stock never reached the average — the exact failure the row exists to prevent. A yarn type now resolves against the yarn type master case-insensitively and is stored in the master's spelling, and a code the master does not carry is refused with its own line ("ZZX is not in the yarn type master — add the type first"); the single entry form resolves the posted code the same way, so a hand POST cannot bypass the select, and this matches the rule the fabric track already followed (quality resolved against the design master). The paste's duplicate check runs after resolution, so ccw and CCW are one key and the second line is refused as a repeat — no half-imported pair; the resolver is an optional argument to parseOpeningImport, so the function stays pure and the preview checks against data.types. No field added, no new rule owed; the quantity and value inputs now also carry the server's ceilings (max 1000000 / max 1000000000). No schema, no write, no reader changed. Proved non-UI: bun src/lib/openingStock.test.ts12/12 PASS (two new cases: ccw → yarnKey(61,'CCW','A') with the Ccw repeat refused; unknown ZZX refused by line while the no-resolver call keeps the raw code, so the old behaviour is reversible) and /tmp/opencode/c357-lane3.ts on a VACUUM INTO copy of data.db through the real route module 24/24 PASS (load's tracks/types/designs; ccw paste → one line, typeCode='CCW', ₹250/kg; a canonical repeat and an unknown type each refused writing nothing; the form refuses ZZX on field='typeCode' and takes lowercase cw to the CW pool; a produced line against its design-master name at ₹120/m; the balance over those rows starts at the entered quantity). bunx tsc --noEmit adds no diagnostic in the touched files, bun run check exit 0 with none in them. No UI test. PLANNING §648. Q326/A326.
C359 re-pass (row lane, src/lib/server/captureRouting.ts + src/lib/server/intake.ts + src/routes/gatepasses/new, owner): the kind-from-intent fix was already on the tree (§642, carried in by another lane's git add -A); this pass re-proved it on the current tree and changed no code. bun run src/lib/server/captureRouting.test.ts7/7 PASS (the Goenka address read carries no kind, a booking number beside it → YARN_IN, godown / our godown / premises / sizer and "direct delivery" → DIRECT, the yarn master rides the prefill, a phantom id lands empty, the reader refuses an address as a destination); /tmp/opencode/c359-proof.ts12/12 PASS (the real extractCaptureJson read of the Goenka text carries no destination, the link is ?draftId=d1&kind=YARN_IN&bookingNo=1128&yarnId=…&partnerGpNo=GSL%2F5546&countYarn=61+CCW&ticket=Sanathan&bags=40&gross=1020&net=1000, an address-only read carries no kind, an explicit godown still opens DIRECT); /tmp/opencode/c359-route-proof.ts over the shipped /gatepasses/newcapturePrefill / resolveKind on a scratch copy of data.db12/12 PASS (the form opens YARN_IN with count / type / mill / bags / trader GP no / booking no carried and the resolved yarn master selected, a phantom yarn id lands empty, the address-only read keeps the form's own YARN_OUT default, an explicit godown word still opens DIRECT). bunx tsc --noEmit adds no diagnostic in captureRouting.ts / intake.ts / gatepasses/new (the repo-wide run stays red elsewhere, same as HEAD). No UI test was run (C65/C66). Suite / build / deploy left to the watcher.
C363 (/gatepasses/new?kind=YARN_IN, owner): the verify pane zooms past 100% again. C308/C326's zoom math was fine (verifyZoom 0.5–4, photo width verifyZoom × 100%), but Tailwind preflight ships img, video { max-width: 100%; height: auto } and a width: 200% does not beat a max-width — so every step above 100% drew the same pane-wide photo while the label read 125%, 200% … The pane's <img> now carries max-w-none (max-width: none, already in the app CSS), so the inline width: <zoom>% is the only width rule again; overflow-auto lets the oversized photo overflow and pan, mx-auto still centres it while it fits (auto margins go to zero once it overflows, so every pixel is reachable). Pinch and the +/− buttons share that one rule; the gesture code, the clamp and the 400% cap are unchanged, the PDF iframe branch and every other screen's images are untouched. No field, POST name, column, migration, route or edit/delete screen. Proved non-UI: /tmp/opencode/c363-proof.ts12/12 PASS (real img tag + shipped preflight + built .max-w-none{max-width:none}, cascade resolved class-beats-type, then 360px pane → 100%=360 / 125%=450 / 200%=720 / 400%=1440 with overflow-auto + mx-auto), and 7/12 with the class stripped (--pre-fix), failing exactly the past-100% cases — fails pre-fix, passes post-fix. bunx tsc --noEmit at HEAD's 29 with none in the touched file. No UI test was run (C65/C66). Suite / build / deploy left to the watcher. PLANNING §649. Q327, A327.
C366 (src/lib/server/schema-foundation.ts + src/lib/server/poLifecycle.ts + src/routes/orders/[id] + ProductionOrderRecord.svelte + drizzle/0053_wide_sue_storm.sql, owner): a production order is a program — it now records its segments. New additive po_segment table (po_order_id, seq, quality_key, pick, weaver_id, tippan_id, planned_mtrs, weaving_rate ₹/pick, in_house); the order's own picks/jobRate/plannedMtrs stay the default first segment, read synthetically (no backfill, no stored row) and added segments start at seq 2 (Q328/A328). addPoSegment writes one provisional cost_leg per segment at pick × ₹/pick ÷ 100 over that segment's planned mtrs; an out-house leg carries the weaver as bill_party_id, an in-house leg books the typed internal ₹/pick with bill_party_id null and a reason, so no vendor bill is implied (Q329/A329). Adding is append-only — row + leg + update amendment + po:segment event, no edit/delete. The add is refused by name once any fabric_in names the PO (and on a completed/cancelled PO); validation is pick 1–200, rate > 0, planned mtrs > 0, on both the action and the client form. poRecord carries segments + segmentsLocked; the Production order card lists #/quality/pick/weaver/tippan/planned/₹-per-pick/₹-per-metre/commitment with default and in-house badges and shows Add a segment only while running and unlocked, weavers from the party master and tippans from the PO's own recent tippans. No column on order changed, no edit/delete screen. Proved non-UI (C65/C66): bun run src/lib/server/poLifecycle.test.ts14/14 PASS (two new cases — two segments at different picks price differently on one PO; the field rule refuses pick 0/201, ₹0/negative rate, blank/zero plan) and /tmp/opencode/c366-proof.ts on a VACUUM INTO copy of data.db18/18 PASS (default segment ₹10,240; 120-pick/₹20 = ₹24/m and ₹12,000 over 500 m; in-house ₹4,200 with no bill party; one provisional weaving leg per segment; record commits only stored legs ₹16,200; a fabric_in row locks the add and writes nothing). bun run check exit 0 with no warning in the touched files; bunx tsc --noEmit adds no diagnostic in them. Suite / build / deploy left to the watcher. PLANNING §651. Q328, A328, Q329, A329.
C364 (src/lib/server/costing/pool.ts + src/lib/server/lots.ts + src/lib/server/reports.ts + src/lib/yarnSplit.ts, owner): a yarn pool now keeps two ledgers — the free stock at its own WAC, and each production order's earmark at the booking landed rate (ARCHITECTURE §9.3). poolBalance/poolWac/trackedWac/apportionAtWac read only entries with no order tag, so an earmarked purchase never drags the free average: isEarmarked/orderTag are a yarn entry's orderId, and the new earmarkLedgers/earmarkFor give each order its own qty/value/locked landed rate (tagged-in value ÷ tagged-in qty, never a pool average). recordYarnPoolIn splits one receipt into one pool_entry per earmarked order plus the free remainder — the booking's orderSplit (C195, the split fixed at booking) first, else the receipt's order-tagged boxes (C309), else the single carried order — pro rata to the received kg via the new splitKg in src/lib/yarnSplit.ts, so the tags add back to the receipt; idempotence moved to per-lot because a receipt now writes several rows. issueYarnToOrder uses the new issueFromEarmark (own earmark first at its locked rate, then free stock at the free WAC) and appends one out entry per part plus one yarn cost leg at the blended rate, so the free pool only loses the free kg. /reports yarn stock carries Free kg and Earmarked kg as separate columns (renamed from apportioned), Free WAC ₹/kg = the free pool WAC, value = free stock + earmarked kg at their locked rates, and WIP beside the balance; a pre-C364 tagged issue with no earmark folds back into free so free + earmarked = total always. Validation is one pure rule, earmarkError: earmarked kg ≤ received kg (and a receipt with no net kg cannot be earmarked), applied on the yarn-in form (boxes capped by the gatepass's own boxes, refusal printed under the split table) and in the action (the posted split, on the allocation field). No field added, no column, no migration, no route, no edit/delete. Proved non-UI (C65/C66), no UI test: costing.test.ts24/24 PASS (a 100 kg ₹200 receipt leaves a 100 kg free pool at ₹150 and the PO ledger reads ₹200; an issue takes the earmark first at ₹200 then free at ₹150), stockReport.test.ts15/15 PASS (Free 100 + Earmarked 100 = 200, value ₹15,000 + ₹20,000; the closed-order/release and re-averaging cases restated), new yarnSplit.test.ts3/3 PASS, /tmp/opencode/c364-proof.ts on a copy of data.db through the real writers 19/19 PASS (a 100 kg ₹150 free receipt + a 100 kg ₹200 booking split 75/25 with ₹1,000 freight and ₹500 brokerage: free WAC stays ₹150, PO-A/PO-B each read the landed ₹215, a 90 kg issue writes a tagged 75 kg out at ₹215 and an untagged 15 kg out at ₹150, the free pool falls to 85 kg at ₹150 and one yarn leg lands ₹18,375 at ₹204.17), and stockTable() on the local data.db printing Free kg | Earmarked kg | WIP kg | Free WAC ₹/kg. bunx tsc --noEmit adds no diagnostic in the touched files (146 repo-wide, same as HEAD); bun run check exit 0 with none on them. Suite / build / deploy left to the watcher. PLANNING §652. Q330/A330, Q331/A331.
C367 (src/routes/fabric-in + src/routes/checking + src/lib/server/poLifecycle.ts + src/lib/server/schema-foundation.ts + drizzle/0054_open_green_goblin.sql, owner): cost is per lot, not per PO.fabric_in now carries the po_segment it came from, its own quality_key and the actual pick of the delivered fabric (never design.pick); every roll it creates carries fabric_in_id, so a lot's grades, X salvage and checked mtrs stay together. A production-order receipt needs a pick 1–200 and a segment (the order's default segment counts), validated on the action and the client input; a stock/SO receipt may leave them blank. New pure lotCost/planLotCompletions/proRata price a lot from its segment's yarn-warping-sizing share (pro rata over delivered mtrs), weaving at the segment ₹/pick × the lot's actual pick ÷ 100, and checking on its checked mtrs, divided by its sellable A/B/C/R metres — X metres carry zero and their share stays in the numerator, so 10% X lifts the sellable ₹/m by exactly the X-borne cost. completeProductionOrder refuses unless each lot's graded mtrs equal its net and the typed good mtrs equal the graded sellable, then writes one produced pool_entry per lot at that lot's unit cost with its quality and actual pick, and one cost_apportionment per lot at lot cost for an SO-linked PO (assignment spread by sellable mtrs); a lot with no sellable metres is refused, and pre-C367 lots with no pick fall back to the old one-row completion. checkRoll takes the lot's quality/pick for the X salvage row and refuses grading over the lot net. Proved non-UI (C65/C66): bun run src/lib/server/poLifecycle.test.ts18/18 PASS (two picks → two lot ₹/m on one PO; 10% X → xCost pushed onto the sellable ₹/m; one plan per lot at its own cost; pick/segment/grade-total rules) and /tmp/opencode/c367-proof.ts on a fresh schema DB 10/10 PASS (60- and 90-pick lots → ₹49/m and ₹100/m pool rows, 550 sellable + 450 X, one produced row per lot, mismatched good refused). Migration drizzle/0054_open_green_goblin.sql (four additive columns). bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §653. Q332/A332, Q333/A333.
C365 (src/lib/server/lots.ts + src/routes/bookings/new/+page.server.ts + src/routes/orders/[id]/+page.server.ts + orders/ProductionOrderRecord.svelte + lots.test.ts, owner): yarn cost is a manual earmark — a gatepass moves kg and prices nothing (ARCHITECTURE §9.1/§9.4). issueYarnToOrder now writes the pool out entries only and no yarn cost leg; the leg comes from earmarkBookingToOrder (a booking or part of it, at that booking's landed rate — rate × kg + freight + brokerage, C352) and from drawFreeYarnToOrder (free stock at the free pool's WAC that moment, for the count + type + grade picked). The booking form's Against split is the earmark: each part's kg is its bags × bag weight and only a production order (role='po'/PROD) takes a leg — an SO / JW target keeps its orderCost basis and a JW order never takes one (Q334/A334, Q335/A335); a free draw writes the leg only, the kg still leave on the gatepass / weft issue (Q336/A336). One validation rule on both surfaces: kg > 0 and ≤ the booking's still-un-earmarked kg (net minus the legs already written against that booking no) or ≤ the key's free balance, the landed/WAC rate read-only and never posted. The Production order card carries a Yarn bucket — source (booking no or Free stock), kg, ₹/kg, value, billed/unbilled, with the unbilled total — plus the two forms (Earmark a booking / Draw free stock) while the PO is running. Append-only: inserts + po:yarn:earmark / po:yarn:free-draw events, no schema, no migration, no edit/delete. Proved non-UI (C65/C66): bun run src/lib/server/lots.test.ts5/5 PASS (a PO with only a weaving leg reads yarn cost 0 after an issue; a 200 kg ₹210 earmark = ₹42,000 unbilled; the bucket lists a booking row and a Free stock row with their sources and totals ₹51,920; over-earmark / 0 kg / negative / no-rate all refused; the Against split earmarks only the production part its own 750 kg while an SO target takes none) and /tmp/opencode/c365-proof.ts on a copy of data.db through the real writers 13/13 PASS — a 200 kg earmark on BK-E2E-3001 landed ₹252 → one ₹50,400 leg, the over-earmark refused against the 2,500 kg left, a 50 kg free draw at ₹198.40 → ₹9,920, and a 40 kg weft issue writing zero cost legs (bucket still 0). bunx tsc --noEmit adds no diagnostic in the touched files (29 repo-wide, same as HEAD); bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §654. Q334/A334, Q335/A335, Q336/A336.
C368 (src/lib/server/costing/pnl.ts + src/routes/checking/+page.server.ts + src/lib/server/poLifecycle.ts, owner): inside a lot cost is grade-blind — A/B/C/R divide one sellable ₹/m, X is zero and its share rides the sellable metres (ARCHITECTURE §9.7). The divisor is now a named engine rule: lotUnitCost({incurred, sellableMtrs}) (0 when nothing is sellable), gradeUnitCost(grade, …)/isSellableGrade (A, B, C and R read the same ₹/m — the grade difference is the selling price — X reads 0) and lotSellableError (a lot with no sellable m refuses completion, same wording, now the gate the completion calls); lotCost and completeProductionOrder use lotUnitCost, identical arithmetic, no behaviour change. 1000 m at ₹80,000 with 50 m X is ₹84.21/m for A and B alike and ₹0/m for X. The checking screen could not grade a lot at all: a fabric-in lot's rolls carry no beam and checkRoll demanded one, so every lot roll 400'd Beam is required before C367's flow could run — a roll with a lot now grades without a beam (the lot is its traceability) and keeps its WIP location, while a loom roll still needs its beam. checkRoll also writes the lot's checked mtrs (graded rolls, X included, plus the roll being saved) on its roll:checked event and returns them, so the checking leg — ₹/m × checked m (C353) — is charged on 1000 m and not on the 950 sellable; X stays reject with a ₹0 salvage row, and grading X still needs a damaged sale order (C348, unchanged). Proved non-UI (C65/C66): bun run src/lib/server/costing/costing.test.ts25/25 PASS (₹84.21/m for A/B/C/R, 0 for X, ₹/m × 950 = ₹80,000, zero-sellable refused) and bun run src/lib/server/poLifecycle.test.ts19/19 PASS (₹12 × 1000 checked = ₹12,000 over 950 sellable), plus /tmp/opencode/c368-proof.ts on a VACUUM INTO copy of data.db11/11 PASS (lot roll grades with no beam; X roll → checked mtrs 1000, reject, 50 m salvage at ₹0; only 950 m sellable; an all-X 200 m lot refuses completion and stays open). bunx tsc --noEmit adds no diagnostic in the touched files. No schema change, no migration. Suite / build / deploy left to the watcher. PLANNING §655. Q337/A337, Q338/A338.
C369 (src/routes/beams/[id] + src/lib/server/beamTransfer.ts + src/lib/beamTransfer.ts + src/lib/server/schema-foundation.ts + drizzle/0055_chief_scourge.sql, owner): a beam or part of one can move to another PO — even of another quality — and the move now carries money (ARCHITECTURE §9.6). The receiving PO books the moved kg at the market value the user types leg by leg — warp kg × yarn rate + warp kg × warping rate + sizing kg × sizing rate — written as three cost_leg rows (yarn/warping/sizing, level po, status actual, no vendor bill). The gap against the beam's carried cost (the transferring PO's own yarn + warping + sizing ₹/kg × its net kg) is one signed exceptional leg on the transferring PO — carried − market, positive a loss, negative a gain, under half a paisa no leg — and the SO apportioned to either order is re-priced (restateOrder for both POs). A part splits pro rata by kg on both sides: 200 of a 500 kg beam is carried ₹12,000, market ₹11,000, variance ₹1,000, the beam stays on the transferring PO and no BEAM_IN is raised; only a whole beam changes hands. The form shows the three hints with their sources — the latest rate-board row for the beam's warp count + type, the latest warping bill leg (the rate board carries yarn ₹/kg only, Q341/A341) and the yarn master's siz_rate — and the typed figures are the authority, stored on the new beam_transfer row with the hints snapshot. Validation on both surfaces: kg > 0 and ≤ the beam net, all three rates > 0, and no transfer of a beam whose PO has no sizing bill (the from-PO's sizing cost leg is the bill, Q339/A339); a same-contract move stays the C6b intimation with no cost (Q340/A340). Append-only: movement + transfer row + legs + amendment + beam:transferred event, no edit/delete. Proved non-UI (C65/C66): beamTransfer.test.ts5/5 PASS (₹30,000 vs ₹27,500 → ₹2,500 loss; 200/500 kg → 12,000/11,000/1,000; gain negative; nil on paise noise; kg and rate rules) and /tmp/opencode/c369-proof.ts on a VACUUM INTO copy of data.db31/31 PASS (three hints; whole-beam ₹27,500 on the receiving PO with the ₹2,500 loss leg and the GP; a 200 kg part; the no-sizing-bill refusal; five field refusals; same-contract books nothing). bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none on them. Suite / build / deploy left to the watcher. PLANNING §656. Q339/A339, Q340/A340, Q341/A341.
C370 (src/lib/server/poLifecycle.ts + src/routes/orders/new/+page.server.ts + orders/ProductionOrderRecord.svelte + src/lib/server/poLifecycle.test.ts, owner): an SO-linked PO is born with a budget, and the record shows budget vs actual per leg (ARCHITECTURE §9.2). quoteLegRates reads the accepted quote's engine snapshot as planned ₹/m per leg (yarn = warp + weft, warping, sizing, weaving); poBudget multiplies each leg by the PO's OWN plannedMtrs and returns null without a parent SO, without a quote, or when the quote planned nothing — a stock PO carries no budget. The snapshot is written once into the existing po:planned event payload (stampProductionOrder resolves the SO's quote_id and reads the quote once), so no table, column or migration was added and a later quote edit/re-quote can never move a budget already taken; the same snapshot rides the po:planned event of a PO that an SO's coverage opens automatically (orders/new). poRecord reads it back (plannedPayloadOf) with budget, budgetDrift and budgetTotal, old POs reading null. budgetDrift totals the PO's own cost_leg rows by kind (level po, legacy excluded) and compares with variance() from costing/pnl, the app's one drift arithmetic: ₹12/m planned vs ₹13.20/m actual over 1000 m is ₹12,000 vs ₹13,200 = +₹1,200 / +10 %, direction over. The Production order card carries a Budget — QT-xx block inside the PO record component (the route page stays a thin shell): leg, budget ₹/m, planned m, budget, actual, drift badge, and a total row; a stock PO renders no budget block at all. Validation is quoteRevisionError — a quote whose SO exists is frozen, enforced where a second SO would re-link it (the order action refuses a quote a live SO already carries; a cancelled SO does not freeze it, and Reuse/Re-quote already save a NEW quote). No edit/delete, no schema change. Proved non-UI (C65/C66): bun run src/lib/server/poLifecycle.test.ts22/22 PASS (three new cases: +10 % drift and the snapshot round-trip; a stock PO / SO-without-quote has no budget and no rows, four legs with yarn = warp + weft; the frozen-quote refusal), and a cp data.db copy through the real writers printed a ₹12,000 budget over the 1000 m plan, a ₹13,200 actual leg, +₹1,200 / +10 %, and budget = null with zero rows for a stock PO. bunx tsc --noEmit adds no diagnostic in the touched files (146 repo-wide, same as HEAD); bun run check exit 0 with none on them. No UI test was run. Suite / build / deploy left to the watcher. PLANNING §657. Q342/A342, Q343/A343.
C371 (src/lib/server/soCoverage.ts + src/lib/server/costing/pnl.ts + src/routes/orders/[id] + costing.test.ts + soCoverage.test.ts, owner): the sale order costs per leg at the rates of the order it was apportioned against, over the fabric actually delivered (ARCHITECTURE §9.5). soDelivery({legs, deliveredMtrs, pick, lockedUnitCost}) prices a pick leg as ₹/pick × delivered picks/m ÷ 100 and every other leg as its committed ₹/m over the same metres; ₹8.75/pick against a 72-pick lot is ₹6.30/m and a second order's ₹7/pick prices differently. The apportionment reading now carries the delivering order's committed leg rates — read live from its own cost_leg rows (weaving at order.job_rate, ₹/pick), grouped per metre over its produced metres, a produced-pool/market lot keeping its locked ₹/m as one leg (9.5 gives it no weaving) — and until a leg is committed the order's C370 quote budget stands in, provisional (Q344/A344). The delivered pick is the order's fabric_in lots' pick weighted by metres, else its planned pick, else the line's (Q345/A345); a dispatch slip's apportionment_id attributes the delivered metres to their line. The delivering order's own cost against the line's locked value is variance() — over when the delivered lot is dearer, under when finer — and the sale's committed rate and locked value never move: the sale P&L reads the delivered per-leg cost (so a higher-pick lot shows its extra cost there) and the coverage card carries the order's over/under badge. Validation: a lot with no pick cannot be apportioned — apportionPickError refuses the …|0 pool key in planSoCoverage, the one gate every writer (order, fulfilment excess, rebalance) goes through; a production-order line may still be apportioned before its lot is in. No schema change, no migration, no edit/delete. Proved non-UI (C65/C66): bun run src/lib/server/costing/costing.test.ts28/28 PASS (₹6.30/m and not ₹7/pick; +₹70 over / −₹35 under with ₹560 locked; budget leg provisional) and bun run src/lib/server/soCoverage.test.ts17/17 PASS (a no-pick lot refused, the key's pick read back), plus /tmp/opencode/c371-proof.ts on a VACUUM INTO copy of data.db through coverageOf13/13 PASS (₹5.60/m locked vs ₹6.30/m delivered = over ₹280 on 400 m; a 250 m dispatch → variance ₹175; the option's pick; no-pick refusal; legs stripped → locked value stands, provisional). bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none on them. Suite / build / deploy left to the watcher. PLANNING §658. Q344/A344, Q345/A345.
C372 (src/lib/server/poLifecycle.ts + poLifecycle.test.ts, owner): cancelling a production order returns its yarn to free stock at the cost it was carried at, never at zero. The release pair used to write value: 0 on both sides — the order's earmark emptied and free stock gained 200 kg worth nothing, deflating the free pool and its WAC with every cancel. poCancellation now derives the release value as qty × unit cost from the yarn the order still holds, and the new yarnReleaseEntries writes the out/in pair from that one figure (out tagged to the order, in untagged), so the value that leaves the order and the value free stock gains are the same number and neither side can be a zero: 200 kg at ₹210 puts 200 kg at ₹210 into free stock and lifts the pool value by ₹42,000. Produced fabric is untouched and stays free at its lot cost (completion's produced rows are untagged; the cancelled PO's produced figure is now read by its doc number, as poRecord did, instead of by a tag those rows never carry). conversionAccrued on po:cancelled was hardcoded 0 and is now poCancelledConversion over the order's own legs — actual, po-level, non-legacy, yarn excluded — so a beam transfer's variance leg (C369) stays posted and reported on the cancelled PO while a provisional sizing/weaving commitment is not charged; nothing is deleted, reversed or re-priced (Q346/A346, Q347/A347). Proved non-UI (C65/C66), no UI test: bun run src/lib/server/poLifecycle.test.ts24/24 PASS (two new cases — 200 kg at ₹210 → ₹42,000 out and in with the free pool value up by exactly that and the earmark at 0; a ₹2,500 transfer variance reported while ₹10,000 weaving / ₹5,000 sizing commitments are not charged, no leg removed) and /tmp/opencode/c372-proof.ts on a VACUUM INTO copy of data.db through the real cancelProductionOrder19/19 PASS (two ₹42,000 release rows, free pool +200 kg / +₹42,000 at ₹210, variance still posted, order cancelled; the same pair at value 0 leaves the pool ₹42,000 short — fails pre-fix). bunx tsc --noEmit adds no diagnostic in the touched files (146 repo-wide, same as HEAD); bun run check exit 0 with none on them. No schema change, no migration. Suite / build / deploy left to the watcher. PLANNING §659. Q346/A346, Q347/A347.
C373 (src/lib/bills.ts + src/lib/server/bills.ts + src/routes/bills + bills.test.ts, owner): every bill is apportioned to its PO by the user at entry — the order and its share are named, nothing is spread silently (ARCHITECTURE §9.1). The bill screen was the last auto-attacher: recordBill ran allocateBill across every open leg of the kind and spread the whole amount by committed quantity, so a ₹50,000 sizing bill settled whatever orders happened to be open in proportions nobody chose. planBillAllocation now takes the named orders with their amounts and returns exactly those legs with the same quantity/rate variance arithmetic; a bill with no allocation is refused (Apportion the bill to its orders — nothing is auto-allocated) and writes nothing. allocateBill survives only as the form's suggestion — the amount by open committed quantity, one tap per row (fill) or one tap for all (Suggest by open quantity) — and the user confirms every field. Validation is one pure function on both surfaces (billShareError + planBillAllocation, called by the load-time form check and by the action): a share must be money above zero with two decimals, the order must carry an open leg, no order twice, a share cannot exceed that leg's open committed amount (₹30,000 is more than PO-B's open committed ₹20,000) and the shares must add back to the bill (₹10,000 of this bill is not apportioned yet / The shares are ₹10,000 more than the bill). The form's allocation card shows each order's open quantity and committed ₹, its own input (alloc_<legId>, so the action reads it without JS) and the live remainder beside the bill amount; the review sheet names the apportioned total. Paste-a-bill-list carries the shares too (…, 50000, 5, 300, PO-1:30000, PO-2:20000) and a line without one is reported, not spread. Allocation is keyed by leg — the row's own "open committed leg" — so two open legs of one kind on one PO are two rows. Append-only: the legs move provisional → actual with an amendment each, no schema change, no migration, no edit/delete. Proved non-UI (C65/C66), no UI test: bun run src/lib/server/bills.test.ts7/7 PASS (the new C373 case: ₹50,000 60/40 over two POs = ₹30,000 + ₹20,000, remainder 0; a partly allocated bill reports the ₹20,000 left; over the bill, no share, a share above the cap and a zero share all refused; 300 kg splits 180/120) and /tmp/opencode/c373-proof.ts on a VACUUM INTO copy of data.db through the real recordBill16/16 PASS (two POs carrying ₹30,000 / ₹20,000 sizing commitments: the unallocated ₹50,000 bill refused with no leg written, the 60/40 bill writing two actual legs of ₹30,000 and ₹20,000 adding back to ₹50,000 with both POs restated; the cap and the short bill refused). bunx tsc --noEmit adds no diagnostic in the touched files (146 repo-wide, same as HEAD); bun run check exit 0 with none on them. PLANNING §660. Q348/A348, Q349/A349, Q350/A350.
C373 close-out (src/lib/server/bills.test.ts, owner): the manual bill apportionment of §660 is now proved at the write, not just the plan. The test's new case runs the real recordBill against a temp libSQL file replayed from drizzle/ (the legacy unquoted REFERENCES order( in 0016 repaired on replay, no dev data.db needed): two POs carrying ₹30,000 / ₹20,000 of sizing commitment — an unallocated ₹50,000 bill is refused (Apportion the bill to its orders — nothing is auto-allocated) and writes no leg, then the 60/40 bill writes two actual legs of ₹30,000 and ₹20,000 on PO-1 / PO-2 that add back to ₹50,000 with both POs restated; the leg table is read back (status actual, order, amount, total) so the assertion is the row, not the return value. Fails on pre-C373 code (which spread the amount silently). bun run src/lib/server/bills.test.ts8/8 PASS; bunx tsc --noEmit 146 repo-wide, none in the touched files. No app code, schema or migration change this turn (the feature itself is in 4dfba036); form already shows each leg's open quantity, committed ₹ and the live remainder. PLANNING §661. Q351/A351.
C375 (src/routes/orders/new/+page.server.ts + orderLogic.ts + a new orderLogic.test.ts, owner): a normal SO is created from a quote, and a sales-booked SO needs no approval (ORDER-FORMS D23/D24). validateOrderFields now refuses a SO with no quoteId (field error; the field spec marks it required) and a SO rate that is not above zero; the damaged (X) sale stays exempt (D21). quoteCarry reads the quote's own facts — buyer, broker (broker_id), quality, picks (inputs_json.pick) and the delivery window (columns, else the costing inputs) — and the create action writes them over the posted values before validation, so buyer / quality / broker can no longer be keyed on this form; the window prefills dueDays. A posted rate always wins (the final negotiated figure), a blank rate takes the quote's final_rate, and the quote is only accepted, never rewritten. One SO per quote is checked before the quote's status, so even an already-accepted quote reports the frozen message. needsOwnerApproval is false for every SO role: the order is saved approved with the creator stamped and no order:approved event is written; the other kinds keep the C111 gate. No schema change, no migration, no edit/delete. Proved non-UI (C65/C66), no UI test: /tmp/opencode/c375-check.ts drives the real create action on a copy of data.db16/16 PASS (no-quote refused; buyer/quality/broker/picks/20-day window inherited; ₹110 stored on the SO while the quote kept ₹101.5 and went accepted; status=approved with only order:created + the amendment in evt; second SO from the same quote refused; ₹0 refused; damaged sale accepted with no quote; blank rate took ₹77.25) and bun test orderLogic.test.ts (run from its own directory) 6/6 PASS. bunx tsc --noEmit adds no diagnostic in the touched files (repo-wide red unchanged). Suite / build / deploy left to the watcher. PLANNING §663. Board readings: picks from the quote's inputs_json.pick; an unresolvable broker_id reads as DIRECT; the window prefills dueDays only until C378.
C374 (src/lib/server/schema-foundation.ts + src/routes/quote + src/routes/orders + drizzle/0056_living_scream.sql, owner): the quote settles the commercial terms and the SO inherits them read-only (ORDER-FORMS D17/D18). Four additive columns on quote — brokerage_pct, pass_on_pct, delivery_term, freight_recovery (default on-top) — plus delivery_term/freight_recovery on order. Both percentages are 0–100 or blank (quotePctReading), the delivery term is a list ex-mill | fob | cif | ddp, the recovery basis a list on-top (excluded, billed on top, buyer bears — the default) | in-rate; DELIVERY_TERMS/FREIGHT_RECOVERIES in quote/quoteLogic.ts are the one source the popup buttons, the server whitelist and the SO's read-only line all read. The quote form asks them in a new Commercial terms block on the calc and check screens (one-field popup: numeric 0–100 for the percentages, buttons for the two lists), the popup refuses an out-of-band percentage before it closes, and the create action 400s the same rules and persists the four into inputs_json + the new columns (guarded by quoteCols, so a pre-migration DB still saves). quoteCarry carries them to the SO; the order create takes them from the quote (a posted value cannot override it) and stores the two new order columns. A quote-linked SO shows brokerage/pass-on read-only with a From the quote — the sale order never asks again note plus read-only delivery-term/freight-recovery lines, the order record lists both beside brokerage/pass-on, and validateOrderFields no longer demands brokerage/pass-on from a quote-carried sale order (quoteCarried) — a quote stating none leaves them blank, while a direct / job-work order keeps today's editable form and 1 %/1 % convention. Append-only: quote rows are only ever added, no edit/delete. Proved non-UI (C65/C66), no UI test: bun test src/routes/orders/new/orderLogic.test.ts12/12 PASS (6 new) and /tmp/opencode/c374-action-check.ts driving the real create action18/18 PASS (1 % / 0.5 % / ex-mill / on-top stored on the quote and read back by quoteCarry; 101 % / 150 % / an unknown term / an unknown recovery refused with no row written; a quote posting no recovery still saves on-top and no percentages), plus /tmp/opencode/c374-check.ts17/17 PASS on the pure readings and the persisted row. bunx tsc --noEmit adds no diagnostic in the touched files. Suite / build / deploy left to the watcher. PLANNING §664. Q352/A352, Q353/A353.
C379 (src/routes/dispatch/+page.server.ts + src/lib/server/soCoverage.ts + schema-foundation.ts + soCoverage.test.ts + drizzle/0057_sharp_loa.sql, owner): a dispatch is checked against the sale quantity's own tolerance band, cumulatively, and grade X never moves the figure (ORDER-FORMS D2/D4/D5/D6). One additive column, order.tolerance_pct (percent of the sale quantity; a blank reads 5 %, a stored figure outside 0–20 % falls back to 5). dispatchBand reads qty ± pct over the whole window — the slip being created plus every loaded/delivered slip — and dispatchBandError refuses past the ceiling with "… against 500 m ±5 % (525 m) — 75 m past the band. Amend the sale order quantity and get owner approval before dispatching"; the create action then fails 400 and writes no slip, while a dispatch inside the band (a part lot, or exactly on the ceiling) saves as before and the apportionment gate stays D4's (a coverage gap never blocks). countsTowardSale counts A/B/C/R and excludes X, so dispatchRollMtrs and cumulativeDispatchMtrs (each slip read from its own rolls_json) leave X metres out — an X-only slip moves nothing. Nothing enforces the buyer's dispatch-lot size: a 300 m or 120 m lot against a 5,000 m hint saves, and no lot-size field was added because there is no master for the hint yet. The gate keys on type='SO', so job-work keeps its absolute diff_mtr/diff_beam reading at load and a damaged sale is exempt (C390/D21). soTolerancePctError (up to 2 decimals, > 0, ≤ 20 %) is exported for the SO form. Proved non-UI (C65/C66), no UI test: bun run src/lib/server/soCoverage.test.ts22/22 PASS (5 new) and /tmp/opencode/c379-proof.ts driving the real create action on a copy of data.db10/10 PASS (300 m under a 5,000 m hint saves; 300 loaded + 300 more refused at 600 m against 525 m with nothing written; an X-only 500 m slip saves at that moment; a 200 m lot inside the band saves). bunx tsc --noEmit 146 repo-wide, same as HEAD, none in the touched files. Suite / build / deploy left to the watcher. PLANNING §665.
C376 (src/routes/orders/new/trading/+page.svelte + src/lib/components/orders/OrderEntryForm.svelte + orderLogic.ts/orderLogic.test.ts, owner): the new-sale-order screen opens on the pending-quotes table, and a change of quote starts the sales fields clean (ORDER-FORMS D24). /orders/new/trading now renders the open quotes as a searchable table — party · quality + alias (design master) · quantity · rate · valid till · days open (from quote.created_at, now carried on openQuotes, one additive read line in the shared order load) — and one tap on a row navigates to ?quoteId=…, where the C375 carry opens the SO already filled. Back (the SO header arrow) returns to that table with the choice cleared; the Change action on the quote chip does the same, and picking another quote re-inherits everything from it, so nothing typed against the old quote crosses over. A change made inside the form (the lead/quote picker) runs salesResetForQuote: quantity from the new quote's estimate, rate from it (rateTouched cleared), tolerance back to the 5 % default, remark cleared, and the delivery tolerance typed against the old quote (tolKind/diffMtr/diffBeam) dropped; re-picking the linked quote is not a change and keeps the values. A Create SO shortcut stays on the quote screen (D24) and every existing carry — ?new=1 for the exempt damaged sale (D21), ?draftId=/?partyId=/?rate=/?designId=/?qty=/?leadId= from capture and leads — still opens the form, so no read off a document is dropped. No field added (the sale-tolerance input and its validation are C378's; this turn resets the state), no schema change, no migration, no edit/delete. Proved non-UI (C65/C66), no UI test: bun test orderLogic.test.ts15/15 PASS (3 new — quantity + rate re-inherit; tolerance 5 and remark cleared; a quote with no estimate / a zero rate leaves both blank, never a silent 0), bunx tsc --noEmit 146 repo-wide, same as HEAD, none in the touched files, and bun run check exit 0 with no diagnostic on them. Suite / build / deploy left to the watcher. PLANNING §666. Board reading: days open needed createdAt on openQuotes, added additively to the shared load.
C374 close-out (no code change this turn — the work landed in e254bd25 under a lane-2 label, and the board row stayed open): re-verified on the tree that serves 79bcd063 that the quote's commercial terms and the SO's read-only carry are all in place (schema columns + drizzle/0056_living_scream.sql, quote/quoteLogic.ts lists and quotePctReading, the quote action's 400s, the SO carry and its read-only lines, PLANNING §664, Q352/A352, Q353/A353). Proof, non-UI (C65/C66), no UI test: bun /tmp/opencode/c374-check.ts17/17 PASS and bun /tmp/opencode/c374-action-check.ts driving the real quote create action18/18 PASS on this tree — a quote of 1 % brokerage / 0.5 % pass-on / ex-mill / on-top stores all four and the SO reads them back, 101 % / 150 % / an unknown term / an unknown recovery are refused with no quote row written, and a quote that posts no recovery still saves the excluded, buyer-borne default; bun test orderLogic.test.ts (from its own directory) 15/15 PASS. bunx tsc --noEmit adds no diagnostic (no source touched). The row is closed on this evidence; deploy/commit left to the watcher.
C380 (src/lib/server/soCoverage.ts + src/routes/orders/[id]/+page.server.ts + +page.svelte + soFulfilment.ts + queue.ts + the two tests, owner): cost cover is the PPC stage — inside the band is complete, a dispatch past the cover leaves a board item and releases nothing, a dispatch under it releases the difference at its locked cost (ORDER-FORMS D3/D4/D20). coverageBand reads the sale's own tolerance (blank = 5 %) as floor/ceiling/gap; coverageBalanceError is banded rather than exact, so a plan 3 % under the quantity passes while 4,600 m of 5,000 m still fails with "Coverage is 150 m short of the 5 % band". soReadyToProduce is sales-then-PPC — pending_approval is "Sales is not done", no cover is never ready, a partial cover carries the shortfall/excess — and rides the order load as soReadiness, badged on the coverage card. dispatchCoverSettlement splits the D3 gap: over = pending item + zero releases (never held, D4), written by the new settleCover action as so:cost-not-covered and read back onto the pending board by coverTodos (deduped per order, cleared by so:cover-settled); under = the excess released through releaseSoHolds at each line's locked cost. markSoFulfilled now releases apportioned − delivered, not the ordered shortfall (which would over-release up to the band's width now that cover may sit below the quantity) and reports costPendingMtrs. The rebalance form balances against the same ±tolerance, and a Settle cover action sits on the coverage card. Proved non-UI (C65/C66), no UI test: bun run src/lib/server/soCoverage.test.ts26/26 PASS (4 new), bun run src/lib/server/soFulfilment.test.ts8/8, bun run src/lib/server/poLifecycle.test.ts25/25 (1 new board case), and /tmp/opencode/c380-proof.ts driving the real load + settleCover action on a copy of data.db16/16 PASS (3 % under passes and reads ready; no cover reads not-ready; a 5,200 m dispatch against 4,500 m apportioned answers ok, writes the so:cost-not-covered event and leaves every line at released_qty 0; a 4,300 m completion releases 200 m at the line's locked ₹61/m off the newest line, older line untouched). bunx tsc --noEmit adds no diagnostic in the touched files (repo-wide red unchanged), bun run check exit 0 with no diagnostic on them. The soCoverage.ts half was snapshotted early by the watcher in 44fe59c6 (labelled C374, lane 1); the rest is this turn's tree. Suite / build / deploy left to the watcher. PLANNING §667. Q356/A356.
C377 (src/routes/orders/new/orderLogic.ts + OrderEntryForm.svelte + orders/new/+page.server.ts + orderLogic.test.ts, owner): the sale order is four focus steps with the quote pinned above them (ORDER-FORMS D25). wizardStepsFor('SO') now returns its own four steps — quantity → rate → tolerance → remark — instead of the shared filter that had grown the sale's walk to fourteen (booking date, slip read, link, booking no., broker, buyer, quality, rate, picks, quantity, cost cover, payment, brokerage, pass-on); job work, outsource and the stock (production) order keep their step sets untouched. wizardSkipsCarried('SO') is false, so a quote-carried value is confirmed on every booking and the sale opens on quantity with that step's field focused (the keyboard comes up and Next keeps it up); the other kinds keep C300's skip and C144's no-autofocus. quoteStripFacts is the one six-fact reading pinned on every step and on the screen behind the card — quote no · buyer · quality · delivery window · payment · rate (the live rate the step under it edits; a fact the quote lacks reads —). Each step keeps its own gesture and context coins: a 1,000 m NumberWheel with ±100/±1,000 steppers and coins from the quote estimate / this buyer / this quality (salesQtyCoins); a keypad with a ₹0.25 stepper and coins from the quote (from quote) / this quality / this buyer (salesRateCoins); chips 3/5/7/10 with this buyer's last two (salesToleranceCoins); a mic that appends plus this buyer's past remarks (salesRemarkCoins). The tolerance became a real posted field: the step writes tolerancePct, the route validates it with C379's own rule in the same words (tolerancePctError: a percentage above 0, at most 20 %, up to 2 decimals) and the action now stores order.tolerance_pct (blank = the 5 % default soCoverage reads); the field spec carries it (sale only) so the summary row reads 5% and taps back into the step. The dead hidden-section markup is deleted — the four "Step N of 4" cards, the section menu, the sticky section bar, goSec/secOpen/secIndex and FIELD_SECTION — leaving the hidden form a pure POST host (with them go the wizard branches only the sale ever reached: the lead/quote link step, payment terms, brokerage, pass-on, so no step of the old fourteen is left anywhere). Nothing D25 does not cover was lost: the C346 cost cover plan moved out of the carousel into its own card on the SO screen (same lines, live gap, exact-balance refusal) and payment terms ride the strip's payment cell (it opens the existing dhara/slab pickers). Every summary label is max-w-[45%] truncate, so a label can never own more than half the row. Board: Q357/A357 (four steps exactly — the sale's slip-read/link steps retire, cover to a screen card, payment via the strip); assumed keyboard-on-open for the sale only. Proved non-UI (C65/C66), no UI test: bun test src/routes/orders/new/orderLogic.test.ts25/25 PASS (10 new cases), bunx tsc --noEmit 146 repo-wide — same as HEAD, the one hit in the touched files pre-existing — and bun run check exit 0 with no diagnostic on the component. Suite / build / deploy left to the watcher. PLANNING §668.
C383 (src/routes/orders/new/+page.server.ts + src/routes/orders/[id]/+page.server.ts + src/routes/production/productionOps.ts + src/routes/orders/+page.server.ts + src/routes/orders/[id]/+page.svelte, owner): the chosen payment slab's own two numbers are stored on the order at booking, so operations confirms them instead of re-keying and the pending-ops board stops flagging a booked order (ORDER-FORMS D9). The create action now writes cd_pct = slab.discount and payment_days = slab.days beside the payment_term_id / payment_dhara link it already stored — a 0 % / 30 days slab stores 0 (a real term, not null) and 30, and an order booked with no dhara stores neither (no invented term). The slab is still resolved against that dhara's slabs_json first, so a stale or tampered paymentCombo is a 400 field error and no row. pendingOpsCondition's payment leg was isNull(cdPct) OR isNull(dueDays) — a delivery deadline (D13), not the credit terms; it is now isNull(cdPct) OR isNull(paymentDays), and src/routes/orders/+page.server.ts (which kept a hand-copy of the condition for its ?operations=1 filter) now calls pendingOpsCondition() itself so the two lists cannot drift. The record page's completion form already posted and rendered both fields; the load now also resolves them for legacy rows that carry the dhara but no figures — the row's own pair wins, else the slab on the linked dhara master matching the CD availed, else the dhara label's own "0% – 30 days" — and the sale P&L's dharaAvailed reads that same figure. Nothing is written by the load (append-only): a legacy row is stamped by the desk's first completion through the existing ops-complete amendment, and posting the prefilled pair back is no change at all, so confirming a booking writes no fake diff. Validation on both surfaces: the CD % input carries 0–100 with 2 decimals and payment days whole 0–365 (pattern/min/max/step, mirroring the ops spec's stepper cap) against the readNum rules validateOpsFields already enforced. No new column, no migration, no edit/delete. Proved non-UI (C65/C66), no UI test: /tmp/opencode/c383-proof.ts driving the real create action and the real record load + complete action on a VACUUM INTO copy of data.db15/15 PASS (0 % / 30 days stores 0 / 30 with the link; the condition flags it not at all; nulling the pair puts it back on the board — the pre-fix reading; a legacy row off the dhara, and one off its label alone, prefills 0 / 30; the completion posting the prefilled pair answers ok with no cdPct/paymentDays amendment and the row still 0 / 30; a slab not on the dhara refused 400; no dhara stores neither number). bunx tsc --noEmit146 repo-wide, the same as HEAD, none added in the touched files. The row's "test" half is this action-level proof: the Bun suite was removed in 7668c583 and bunfig.toml still roots at the absent tests/, so no tests/ file could run. Suite / build / deploy left to the watcher. PLANNING §668. Q358/A358, Q359/A359.
C387 (src/routes/orders/[id]/+page.svelte + +page.server.ts + src/lib/soPlanning.ts + src/lib/server/soPlanning.ts + src/lib/server/queue.ts, owner): planning works the sale order's card — the buyer's order image pinned above the fields it verifies, and saving the stage clears the planning board. A new Planning card sits under Next steps on every SO: the newest kept buyer-order image is pinned, collapsible and zoomable, with the five D16 fields below it prefilled and editable in place (buyer PO no, packing rolls/lumps, cut size, rolls computed from quantity ÷ cut when blank, consignee); nothing an input does hides the photo. The image is optional at booking and planning attaches it when sales did not — Attach the buyer's order / Add another page, a photo or PDF up to 12 MB through the existing saveDoc capture store and order_attachment row (no staging, no new storage). Field rules, both surfaces: buyer PO no required ≤ 60 with the reference shape (letters, digits, spaces and / . _ - #), packing one of the two stored words, cut > 0 and ≤ 1000 m/roll with ≤ 2 decimals, rolls whole 1–100000 (blank computes), consignee ≤ 80 (blank = decide at fulfilment). Saving writes the changed fields (and the image) as an update amendment planning stage saved, recomputes lots_computed, and logs so:planning-complete; the planning board is derived from that ledger (open SOs minus completed ids) and rides pendingBoard as a planning section for C386's per-role landing. The consignee rides dispatch_city until C381 (Q360/A360). Proved non-UI (C65/C66), no UI test: /tmp/opencode/c387-proof.ts driving the real action + real board on a VACUUM INTO copy of data.db31/31 PASS (both orders on the board before; a PNG save stores PO/cut/50 computed rolls/consignee, attaches the image, writes the amendment rolls null→50 and the event, and leaves the board; a no-image save writes no attachment and leaves it too; 13 field refusals 400 with their own field and no attachment; markup order image→form, sticky while the fields scroll, with collapse/zoom and no input hiding it). bunx tsc --noEmit adds no diagnostic in the touched files (same three pre-existing queueBoard.ts lines as HEAD). Suite / build / deploy left to the watcher. PLANNING §669. Q360/A360.
C378 (src/lib/server/schema-foundation.ts + src/routes/orders/new/orderLogic.ts + orderLogic.test.ts + OrderEntryForm.svelte + orders/new/+page.server.ts + orders/[id] record/print + soCoverage.ts, owner): the sale order's tolerance is sales' number (0–25%) and its delivery promise is two dates — the start date for the first delivery, the end date for completion — never a day count (ORDER-FORMS D2/D6/D13/D20). Two additive nullable columns on order, delivery_start and delivery_end (timestamp_ms), carry the promise; the form's C175 window control (start/end date, ±1-day nudge, the today/+2/+7/+15/+30/+40 chips) now posts them, a linked quote's window prefills both, and the record shows Delivery start / Delivery end (and the print sheet a Delivery window) instead of day counts. due_days / promised_days are retired from the SO form and from the record's operations completion (?/complete no longer renders or writes either; it shows the window read-only) and stay in the table for history; the four job-work production forms keep their own day count and validateOpsFields still parses it for them. The tolerance reuses the existing order.tolerance_pct column (C379) and is now 0–25 inclusive, typed in the tolerance step beside the 3/5/7/10 chips, 5 the default and a blank still 5; tolerancePctError and the dispatch band's soTolerancePct/soTolerancePctError were moved to the same 0–25 ceiling together, so a stored 25 is never silently read as 5. Validation is one rule on both surfaces: deliveryWindowError refuses a start before the booking date ("Delivery start cannot be before the booking date") and an end before the start ("Delivery end cannot be before the start date"), junk dates on their own field, blank allowed; validateOrderFields returns the two stamps and the form shows the same message live with matching min attributes. Assumptions: Q361/A361 (the completion shows the window, never re-asks it — one source of truth, D13, corrections via the append-only amend path), no duplicate tol_pct column, dates optional at booking, only the SO retires the day counts. Proved non-UI (C65/C66), no UI test: bun test orderLogic.test.ts (from its own directory) 32/32 PASS (7 new C378 cases) and bun test soCoverage.test.ts26/26 PASS (the 0–25 bounds case), plus /tmp/opencode/c378-verify.ts reading 7% → 7, 30 → refused, end < start → refused, start < booking → refused, the stored 2026-11-05 → 2026-11-20, and the SO form answering deliveryStart/End = true, dueDays/promisedDays = false. bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none. No migration generated — apply-db.sh pushes the two additive columns at deploy. Suite / build / deploy left to the watcher. PLANNING §670. Q361/A361.
C389 (src/lib/server/reports.ts + src/lib/server/costBasis.ts (deleted) + orders/new + OrderEntryForm.svelte + bookings routes + orders/[id] + orderProfit.test.ts, owner): the legacy yarn cost-basis path is retired and order profitability is the cost engine's own answer (ORDER-FORMS D26). recordCostBasis and its whole module are deleted, with all three writes — the order form's costBookingId (spec entry, entry-form details block, single-field box, dirty/review maps, label helper, route load/validation), the booking form's Against picker and the bookings list's booked-first attach (which still reassigns the booking and appends its amendment — the physical link never was the cost). Nothing in src/ reads order_cost; the record page's Locked yarn cost basis reconciliation card goes with it, and the table stays in the schema for history only. Reports → Order profitability (costBasisTable, CSV slug costbasis kept) now feeds the live sale orders, their cost_apportionment rows and their cost_leg rows into the engine's soPnl: revenue = ordered mtrs × rate, cost = outstanding apportioned value (released metres off), direct cost = level-so legs only (a capitalised po leg stays out), margin = revenue − cost − direct, plus covered mtrs, rate and provisional/actual; new pure orderProfitLines (DB read after, like the stock tables) keeps it testable. Assumptions: Q362/A362 (sale-order rows only — a PROD order is a poPnl, not a sale, and its qty × rate is not revenue; salvage stays out for now so a damaged child is not double-counted) and an --assume that the record page card had to go with the ledger. Proved non-UI (C65/C66), no UI test: bun run src/lib/server/orderProfit.test.ts7/7 PASS (covered numbers, part-released hold, PO leg excluded, provisional, no invented cost, cancelled/PROD dropped, two holds add up) and /tmp/opencode/c389-proof.ts on a pushed copy of data.db13/13 PASS (fixture 1,000 m @ ₹40 with 500 m @ ₹25 + 400 m @ ₹30 outstanding and a ₹500 direct leg → ₹40,000 revenue, 900 m covered, ₹24,500 cost, ₹500 direct, ₹15,000 margin, equal to soPnl, with a stale ₹9,99,999 order_cost row invisible; all 553 live SOs match the engine; order_cost count unchanged; no costBookingId in the field spec; table still present). bunx tsc --noEmit adds no diagnostic in the touched files (146 repo-wide, none in them); bun run check exit 0, no new warning. No migration — the table is kept. Suite / build / deploy left to the watcher. PLANNING §671. Q362/A362.
C381 (src/lib/server/schema-foundation.ts + src/lib/consignee.ts + src/lib/consignee.test.ts + dispatch/+page.server.ts + dispatch/+page.svelte + orders/new/orderLogic.ts + orders/new/+page.server.ts + OrderEntryForm.svelte + orders/[id] record card/load/planning action + masters party categories, owner): the consignee is a party-master link with an explicit decide-at-fulfilment state, and a dispatch slip cannot save without one (ORDER-FORMS D7). Additive nullable order.consignee_party_id and dispatch_slip.consignee_party_id (FK party) plus the new Others party category (masters list, PartyForm, server message). One blank link is the state — consigneeState/consigneeLabel read at-fulfilment and print Decide at fulfilment until a party is picked, so no second flag can drift. Pure rules in src/lib/consignee.ts: the consignee must be an existing, active party in a receiving category (Others + Godown + Transport — D7's depot / broker godown / transporter hub), buyers, brokers and inactive rows refused in plain words, pickers offer only active candidates. Booking is optional: the SO terms block gets a Consignee select defaulting to Decide at fulfilment (with a same-form “add consignee party (Others)” through the existing PartyForm) and the route validates existence/activity/category; the field spec carries consigneePartyId (sale only, optional) so the summary row prints the decision or the party. A consignee named at booking is read-only for planning — the card prints it and the planning action refuses a different value; an open one is resolved on the card (city then follows the party's city). Dispatch gets a required Consignee select prefilled and locked from the order, and ?/create refuses blank/invalid/unknown/inactive or a party other than the order's, storing the slip's own consignee; when the SO left it open the chosen party is written back as an append-only update amendment (consignee decided at dispatch) + order:consignee-decided event. No edit/delete UI; light mode unchanged. Assumptions Q363/A363 (one nullable link, receiving categories Others+Godown+Transport, a booked consignee fixed for planning and dispatch — corrections via the amend path). Proved non-UI (C65/C66), no UI test: bun src/lib/consignee.test.ts7/7 PASS, bun test orderLogic.test.ts (from its own directory) 32/32 PASS, and /tmp/opencode/c381-proof.ts driving the real create/planning actions on a VACUUM INTO copy of data.db23/23 PASS (SO saves with no consignee; dispatch refuses blank/buyer/inactive/unknown with no slip; an active Others consignee saves, resolves the order, writes amendment+event, and a second different consignee is refused; planning refuses to change a booked one, resolves an open one, refuses inactive). bunx tsc --noEmit 146 repo-wide, the same as HEAD, none added in the touched files; bun run check exit 0, no new diagnostic. Migration drizzle/0058_next_shadowcat.sql additive (also carries C378's missing delivery columns). Suite / build / deploy left to the watcher. PLANNING §672. Q363/A363.
C384 (src/routes/orders/new/orderLogic.ts + orderLogic.test.ts + orders/new/+page.server.ts + OrderEntryForm.svelte + orders/[id]/+page.svelte + orders/[id]/+page.server.ts, owner): the order form's numbers are rules on both surfaces — rate above zero, picks per inch 1–200, quantity in 1,000 m steps, one buyer PO per buyer (ORDER-FORMS D10/D15). validateOrderFields reads the rate with exclusiveMin: 0, so ₹0 is refused on every kind and for every role (the sale order's own message unchanged), and picks through the new picksRuleError: whole, 1 to 200, Picks must be between 1 and 200 — 0, 201 and 0.5 all refused, 1 / 64 / 200 accepted. qtyStepError refuses a metre quantity that is not a multiple of 1,000 (Quantity must be in multiples of 1,000 metres): 5,500 m refused, 5,000 m saves 5,000; a beam count (qtyUnit: 'beams') and a damaged (X) sale are exempt (Q364/A364). buyerPoDuplicateError compares one buyer's order numbers case- and space-insensitively and is called by the create action (over the buyer's live orders, a cancelled one releasing its number) and by the planning card action (excluding the order being planned); the field error renders on both form faces. Every rule is mirrored in OrderEntryForm.svelte — rateIssue / picksIssue / qtyIssue derived checks with the server's own words, min/max/step/maxlength on the inputs, the ±1 picks stepper clamped — and the picks wording moves to picks per inch with PICKS_PLACEHOLDER = 'e.g. 64' replacing the old e.g. 4000 on the sale/job-work form, its wizard step and the order record's ops card. No schema change, no migration, no edit/delete. Proved non-UI (C65/C66), no UI test: /tmp/opencode/c384-check.ts21/21 PASS (three roles' rate 0 refused, ₹0.01 accepted; picks 0/201/0.5 refused, 1/64/200 accepted; 5,500 refused, 5,000 accepted; beams and damaged exempt; duplicate PO refused, other buyer / new / blank allowed; placeholder is 64). The Bun suite was removed in 7668c583 and bunfig.toml still roots at the absent tests/, so the six regression cases live in src/routes/orders/new/orderLogic.test.ts. bunx tsc --noEmit adds no diagnostic in the touched files; bun run check exit 0 with none. Suite / build / deploy left to the watcher. PLANNING §673. Q364/A364.
C381 close-out (docs only + src/lib/consignee.ts / consignee.test.ts / dispatch/+page.server.ts re-proved, no code change): the consignee row is closed — its build shipped in 9abcc4cf (committed under the C389 label, so the board line stayed doing) and every acceptance line is re-proved on a VACUUM INTO copy of data.db.bun src/lib/consignee.test.ts7/7 PASS; the real create/planning/dispatch actions 23/23 PASS (SO saves with no consignee storing null; dispatch refuses blank / buyer / inactive / unknown with no slip; an active Others consignee saves, resolves the order with the amendment + order:consignee-decided, a second different consignee is refused; planning refuses to change a booked consignee, resolves an open one, city follows the party); and new /tmp/opencode/c381-recheck.ts11/11 PASS through the masters createParty action — an Others party saves active, lands in consigneeCandidates, an unlisted category is refused with no row written. Reading recorded (A365): a create POST that omits active stores the party inactive, and that is left as is — PartyForm always posts the toggle and every consignee picker goes through it. Noted: C384's buyer-PO duplicate rule now refuses a planning post re-sending another order's PO before the consignee rule is reached; the proof fixture reused PO-C381-3 and was given its own number, the consignee cases pass unchanged. No code, schema or migration change in this session; PLANNING §674. A365.
C384 second pass (src/routes/orders/new/orderLogic.ts + orderLogic.test.ts + OrderEntryForm.svelte, owner): the number rules reach the prefill too (§673 built D10/D15; this pass closes the two places the built surface still disagreed with them). picksInRange now gates every picks figure history hands back — lastPicksFor and picksSuggestions only offer a whole 1–200 value, so a pre-rule picks-per-metre 4,000 is never prefilled or coined and the field stays blank instead (A366) — and the form's live qtyIssue exempts a damaged (X) sale exactly as the route does, so a 5,500 m cut no longer shows an error the save would not raise. The record page's ops completion is left lenient on purpose (Q365/A365): a rate above zero and whole positive picks still pass there, so an order booked before the rule can be completed; D10/D15 bite at booking and corrections ride the amend path. Proved non-UI (C65/C66), no UI test: bun test orderLogic.test.ts (from its own directory) 39/39 PASS (1 new case) and /tmp/opencode/c384-check.ts26/26 PASS (the five acceptance lines — rate 0 refused on SO / JW / OUTSOURCE production, ₹0.01 accepted; picks 0 / 201 / 0.5 refused, 1 / 64 / 200 accepted; 5,500 refused, 5,000 accepted; same-buyer PO twice refused while another buyer / new / blank pass; placeholder e.g. 64 — plus the 4,000-not-offered bounds). bunx tsc --noEmit 146 repo-wide, the same as HEAD, none added in the touched files; bun run check exit 0 with no diagnostic on them. Suite / build / deploy left to the watcher. PLANNING §675. Q365/A365, A366.
C382 (src/lib/server/schema-foundation.ts + drizzle/0059_premium_quasar.sql + scripts/backfill-broker-id.ts + scripts/apply-db.sh + orders/new/+page.server.ts + orderLogic.ts + brokerPick.test.ts + OrderEntryForm.svelte + orders/[id]/+page.server.ts, owner): the broker is stored as a party link, so a rename never orphans a past order (ORDER-FORMS D8). Additive nullable order.broker_id (FK party); broker_name stays as the name the booking recorded — history only, never re-written — and DIRECT keeps its sentinel with no link. brokerPickError (orderLogic) is the one rule both surfaces read: a master's id wins, a blank id falls back to a name a Broker master carries (a pre-id booking / older client still saves), blank and DIRECT mean "no broker", anything else is refused with Broker must be picked from masters. The picker lists Broker masters only and is valued by id (a hidden brokerId posts beside the name; the quick-add, the wizard coins, the lead/history prefill and the draft snapshot all carry the id), the form shows the rule's own words under it, and the create action refuses an unknown id, a non-broker party and an unknown name with no row written. An SO inherits the quote's broker_id and records that master's name; the order record resolves its broker by id first (name only for a row the migration could not link), so a renamed broker's name and mobile still land on the old order. Migration 0059_premium_quasar.sql adds the column and links broker_name → party.id where the name matches a Broker master (case/space-insensitive, DIRECT skipped); apply-db.sh runs the same fill via scripts/backfill-broker-id.ts (idempotent — only null links are touched). Live data: 878 of 913 pre-id orders linked, the 35 left being QA-BROKER (no party) and ARROW WEAVING PVT LTD (a buyer, not a broker) — both keep their name. Proved non-UI (C65/C66), no UI test: bun test brokerPick.test.ts (from its own directory) 5/5 PASS and /tmp/opencode/c382-proof.ts on a VACUUM INTO copy of data.db19/19 PASS (the migration links the pre-id row and keeps its name, DIRECT and a buyer's name stay unlinked; the record resolves the broker's mobile by id, still resolves it after a rename and prints the new name while the row keeps the booked one; an unknown id / a buyer id / an unknown name are all refused 400 with no order written; a real broker id saves as the link + the master name; an SO from a quote inherits the quote's broker_id). bunx tsc --noEmit 146 repo-wide, the same as HEAD, none in the touched files; bun run check exit 0 with none on them. No edit/delete UI, light mode unchanged. PLANNING §676. A367.
Gate change (owner, AGENTS.md + docs/OWNERSHIP.md + docs/STATUS.md + scripts/row-run.sh + scripts/row-watch.sh + src/lib/components/orders/OrderEntryForm.svelte, 2026-10-31): the row gate is now bun run check + bunx tsc --noEmit. The line stalled from 06:00 because C378 left OrderEntryForm.svelte importing DELIVERY_CHIP_DAYS while orderLogic.ts exports DELIVER_CHIP_DAYS — tsc does not read .svelte, so it gated green and died in vite build, re-running C374/C381/C384 and holding the last two. One-token fix; bun run check 0 errors, bun run build green. PLANNING §677.
C392 (src/lib/soPlanning.ts + soPlanning.test.ts + orders/[id]/+page.server.ts + orders/[id]/+page.svelte, owner): the ops completion no longer edits what sales and planning own (ORDER-FORMS D16; C375/C383/C381). The rate and the cash discount show read-only on the card (printed from the order, (sales) on the label; the focus-entry step 2 is a confirmation, no number wheel) and the hidden completion form carries neither input, so neither is posted — a sale's send-to city is likewise gone and replaced by planning's consignee (the party, or Decide at fulfilment). Server-side keepOpsOwnedFields(post, row) (src/lib/soPlanning.ts) is the one rule: every owned field is replaced by the order's own value and dropped when the order's value is blank, so an old client or stale tab can neither overwrite sales' rate/discount nor planning's city, nor invent one; the action validates that copy, which makes the surviving put(...) calls no-ops. A non-SO order keeps its city on this card (Q368/A368 — D7's consignee and the planning card are sale-order only). Also recorded (A368): the commercial set stays a booking gate — validateOpsFields' sales-role requirement is not applied on this card, since the two fields it demanded are gone and 549/553 sale orders carry no cash discount. No schema change, no migration, no edit/delete UI, light mode unchanged. Proved non-UI (C65/C66), no UI test: bun src/lib/soPlanning.test.ts6/6 PASS; bunx tsc --noEmit 146 repo-wide, the same as HEAD, none in the touched files; bun run check exit 0 with none. Suite / build / deploy left to the watcher. PLANNING §678. Q368/A368.
Row line (scripts/row-watch.sh + board, owner, 2026-10-31): row sessions stop exploring — the prompt now says work from the row's file:line anchors, grep for the symbol, never read a 1,000-line file or the 3,000-line component whole; do not re-read STATUS/BUILD-LOG/PLANNING when the row text carries the decision; run bun run check + tsc ONCE after the last edit. Evidence: rows were 12 min (29th) → 80 min (SO block) while a trivial model turn is 9 s and the gates cost ~1 min. C391 withdrawn into C393 (same file, one session). PLANNING §678.
Row line (scripts/row-watch.sh, owner, 2026-11-02): the row line was dead for two days and nobody could see it — row_prompt's unquoted heredoc left two backticks unescaped on the gate line, so every dispatch ran bun run check + bunx tsc --noEmit at prompt-build time and spliced ~2k lines of output into the prompt argument; execve then failed E2BIG (/usr/bin/timeout: Argument list too long, rc=126) and the row never started. First seen 2026-10-31 09:17, 58 failed sessions, C385/C386 stuck at fail 9/2, zero commits since 10-31 16:43 — and the fail cap only guarded integration, so the loop retried forever. Fixed: backticks escaped (prompt is 3.6 KB, brief answers in 0.17 s instead of running svelte-check) and a row whose session fails ROW_WATCH_FAIL_MAX times in a row is now held blocked-owner like an integration failure, so a bad prompt can never spin silently again.
C385 (src/lib/components/orders/OrderEntryForm.svelte + src/routes/orders/new/orderLogic.ts + orderLogic.test.ts, owner): a rejected save is no longer silent — the error banner and every fieldErrors line were rendered inside the display:none POST host, so a failed save looked like a dead button. The banner now sits in the visible summary (route form.error, else Could not save — fix the field(s) below, one tappable label: message line per refused field, Dismiss kept); fieldErrorJump (orderLogic) reads the server's own order and returns the first refused field that owns a surface — its wizard step, else its single-field box — so the form opens that step/box and prints the message under the field (data-step-error / data-field-error), falling back to the banner alone only when no refused field can be opened. The duplicate per-control fe(...) lines and the invisible rateIssue copy are removed from the hidden host; no field, name, rule or save path changed, so a success still redirects to /orders?created=…. No schema change, no migration, no edit/delete surface, light mode unchanged. Proved non-UI (C65/C66), no UI test: bun test orderLogic.test.ts45/45 PASS (6 new C385 cases) and /tmp/opencode/c385-proof.ts13/13 PASS (banner visible, hidden host clean, step + box messages, first-openable jump, success redirect unchanged). bunx tsc --noEmit 146 repo-wide, the same as HEAD, none in the touched files; bun run check exit 0 with none on them. Suite / build / deploy left to the watcher. PLANNING §679.
Row line watchfulness (scripts/row-watch-health.sh + ~/bin/svchealth unit + ~/services/row-watch-health.sh, owner, 2026-11-02): the line can now say it is stuck — a loop cannot report its own death, and the two dead days proved it (every session dying at exec with rc=126, fail counters at 9/2, nothing said a word). The check runs from outside the loop and answers three questions: is the watcher alive and writing (log quiet > ROW_WATCH_QUIET_SECS, 30 min, while rows wait), is a session failing to start right now (rc=126/Argument list too long inside ROW_WATCH_START_FAIL_WINDOW, 30 min), and is work landing (nothing closed inside ROW_WATCH_STALE_HOURS, 6 h, measured from the later of the last closed row and the last row-watch up, so a fresh restart is not accused). Exit 0 ok / 1 stalled / 2 a human must act; it also flags rows past ROW_WATCH_FAIL_MAX and blocked-owner rows, and ignores fail counters left behind by rows that have since closed (C352–C355 sat at 2/2 after closing). --quiet for a loop. On the phone the health unit polls it every 10 minutes and leaves ~/services/row-watch.UNHEALTHY with the reason while it is not 0. Proved without a UI: fixtures for healthy/stale/start-failure (ok 0, STALLED 1, NEEDS A HUMAN 2), then live — the running check caught 2026-11-02 11:12:44 rc=126 on its first pass and dropped the flag; bash -n clean, whole check ~0.9 s on the phone. No app route, field, migration or screen.
C386 (src/routes/+page.svelte + src/lib/server/queue.ts + src/lib/server/queue.test.ts, owner): one order record with three stages and a board per role that is the landing page — sales, planning and PPC share one sale order and one trail; the record's rail states each stage (done/open/blocked) and the two gates read separately: ready to produce is sales + PPC (cover inside the sale's ±band), weaving is that plus planning saved (so:planning-complete), so an order with sales and cover complete but no planning is ready to produce and still not weavable. Each stage opens its own fields for its role and reads back a summary for the others (planning card form vs read-only list; PPC gap/rebalance controls only for production/planner/owner/admin). The three pending sections (sales/planning/ppc, each row labelled, with what it wants and the age it has sat open) are now drawn on the landing page — they were computed but never rendered, so a role lands on its own board after sign-in (stageSectionsFor: sales→sales, planner→planning, production→PPC, owner→all three, others none). No new field, no schema change. Proved non-UI (C65/C66): src/lib/server/queue.test.ts10/10 PASS (two gates, three states, cancelled blocked, band short/over names the metres, each board's label+age, a saved planning stage leaves the board) and /tmp/opencode/c386-proof.ts on the live data.db prints sales 5 / planning 20 / PPC 6 rows with real ages for the owner and none for accounts. bun run check + bunx tsc --noEmit clean on the touched files. Suite / build / deploy left to the watcher. PLANNING §680.
C386 re-verify (src/lib/server/queue.ts + src/routes/orders/[id]/+page.svelte + src/routes/+page.svelte, owner): the three-stage rail and the per-role landing boards were already at HEAD (C385's deploy swept them in; git log --grep C386 only shows the docs commit), so the row was re-proved rather than rewritten. On a VACUUM INTO copy of the live data.db plus one synthetic covered SO added to the copy (the live 29 open SOs all carry zero cover, so the covered branch cannot be shown on today's data): rail reads sales:done, planning:open, ppc:done with ready-to-produce true and weaving false; the planning board lists it with age 3 days and what it still wants; the owner's landing row carries that age; writing so:planning-complete clears the row and unlocks weaving; sales 5 / planning 20 / PPC 6 all carry ages; stageSectionsFor maps sales/planner/production/owner as D27 says. /tmp/opencode/c386-recheck.ts 19/19 PASS, src/lib/server/queue.test.ts 10/10 PASS. No field, schema or reading changed. PLANNING §681.
C386 (src/lib/server/queue.ts + src/lib/server/queue.test.ts + src/routes/orders/[id]/+page.svelte, owner): the per-role boards and the record's role gate now agree about an admin, and the rail says whose stage each one is (ORDER-FORMS D16/D27). stageSectionsFor maps admin exactly like owner (sales,planning,ppc) and admin joins the Role union, so an admin no longer lands on a page with no stage board while the record opens every stage's fields for them (the same owner-or-admin reading ops.ts and stageRoleOpen already use). Each rail cell carries data-stage-mine and Yours to work / Read-only for you, read from the existing stageRoleOpen, so the three stages say which side of D16 the reader is on without adding a control or an input. The rest of the row was already at HEAD (C385's deploy, C386 re-verify) and is re-proved rather than rewritten. Proved non-UI (C65/C66), no UI test: /tmp/opencode/c386-final.ts22/22 PASS (sales+cover complete with no planning is ready to produce and not weavable; cover inside the band counts as PPC done; sales not done blocks; cancelled blocks all three; the planning row lists with age 3 days and its missing fields and a saved stage clears it; sales/PPC ages; each role's landing list incl. admin; live pendingBoard sales 5 / planning 20 / PPC 6, every row aged) and bun run src/lib/server/queue.test.ts10/10 PASS (one new admin case). No field, no schema change, no migration, no edit/delete. PLANNING §682. A(C386) admin owns every stage like the owner.
C388 (src/lib/components/orders/OrderEntryForm.svelte + src/routes/orders/new/orderLogic.ts + src/routes/orders/new/orderLogic.test.ts, owner): the picked quality now records its own alias on the order (ORDER-FORMS D14). The picker already searched name or alias (filterDesigns, qualityOptionLabel), but order.alias was only ever written from a lead's free-text quality/design alias, so a direct / stock / sample-carried order saved with no alias while the master held a short name. onDesign() — the single path the quality combobox, the wizard picker and the quote/lead/draft carries all use — resolves through the new aliasForQuality(picked, current): the master alias wins over one inherited from a lead or quote, a quality with no alias leaves the carried value alone, whitespace trimmed; the existing hidden alias input and the route's str(fd,'alias') read are unchanged. Pareto reading: no second free-text alias box (C134 retired the nickname), so a typed alias = the alias typed into the picker search, and a quality picked after a lead replaces the lead's carried alias. Proved non-UI (C65/C66): src/routes/orders/new/orderLogic.test.ts49/49 PASS (four new C388 cases — pick records it, merc/MERCER and twill find the quality, picked beats inherited, no-alias keeps the carried value). No field, no schema change, no migration. PLANNING §683.
C390 (src/routes/orders/new/damagedSale.ts + damagedSale.test.ts + orders/new/+page.server.ts + orders/new/damaged-sale/+page.server.ts + +page.svelte + OrderEntryForm.svelte + orders/new/trading/+page.svelte, owner): the damaged (X) sale is its own small form, exempt from the quote (ORDER-FORMS D21/D22). /orders/new/damaged-sale asks six facts — buyer, the damaged lot, metres, scrap rate, consignee, remark — and posts them to its own createDamagedSale action: no quote id, no coverage plan, no approval; the order is written sale_kind = damaged, qty_unit = mtrs, parent_so_id = the posted source SO else the one its lot's metres came from. OrderEntryForm gained an opt-in damaged surface (compact card; wizard, quote strip and coverage untouched). The lot is the salvage pool key (quality · pick — the only place X metres live, C348) with its live balance, and the sale writes the pool's zero-cost out entry, so the metre cap is what is really left. Rules live once in damagedSale.ts and both surfaces read them: metres > 0 and ≤ the lot balance, rate > 0 (2 decimals each), buyer required, lot on the list and non-empty, remark ≤ 500. Proved non-UI (C65/C66), no UI test: src/routes/orders/new/damagedSale.test.ts8/8 PASS and /tmp/opencode/c390-proof.ts on a copy of live data.db13/13 PASS (lot 55 m with source SO-C390-P; save with quote_id null, damaged, link stored, 20 m out, 35 m left; 40.01 m / zero rate / produced-track lot / blank buyer all 400 with nothing written; blank link still stores the lot's SO); the damaged route's load returns type SO + its lots on live data. No schema change, no migration, no edit/delete, light mode unchanged. PLANNING §684. A(C390): the lot is the salvage pool key, not a fabric-in row.
C394 (docs/contracts/so-form.json + docs/contracts/README.md + src/lib/contracts.ts + src/lib/contracts.test.ts + src/routes/orders/new/trading/+page.server.ts, owner): an entry form's field list now lives in its contract and nowhere else (ORDER-FORMS D29/D30). so-form.json was data nothing read; now one schema covers every entry form — form (the route), kind, role, decision, the read-only strip (fact + where it is read from), fields (name, label, order, owner role, required, unit, rule, prefill — quote.estQty-style source, a literal, or null), the actions a screen may offer (save, back-to-quotes), and an absent list naming every omitted field and the stage that owns it (quote/planning/ppc/production/dispatch/costing/master/route/never), plus optional noSections/afterCreation. One strict loader (src/lib/contracts.ts) reads docs/contracts off the checkout: parseContract rejects a field without a unique name/order, an owner, a rule or a stated prefill, an action no screen can offer, an absent entry that is also a field, an unknown where, and a strip key that collides with a field — and readContract/allContracts/contractForForm/soFormContract throw ContractError whose message starts with the contract's path and the JSON pointer it broke (missing file, invalid JSON, two contracts on one form, wrong kind: all the same). /orders/new/trading now resolves its contract by path + kind in its load (contractForForm(SO_FORM, 'SO')) and hands it to the screen, so the six fields, order, labels, rules, prefill sources, quote strip and actions come from the contract; its own files name no field. Proved non-UI (C65/C66), no UI test: src/lib/contracts.test.ts9/9 PASS (typed SO contract — six fields ordered 1–6, five required, owner sales, prefills quote.estQty/quote.deliveryStart/quote.deliveryEnd/quote.finalRate/5/null, six-key quote strip, two actions, ten absent entries with coverage → ppc; route lookup + existing route file; wrong kind / unknown form / missing file / path outside the dir each naming their path; thirteen malformed documents each naming their path and pointer /fields/1/order, /fields/0/rule, /fields/0/prefill, /fields/0/required, /fields/0/owner, /actions/1, /absent/0/name, /absent/0/where, /form, /kind; every contract on disk parses and claims one form; the SO screen's files list no field and read the loader). No schema change, no migration, no edit/delete surface, light mode unchanged. PLANNING §685. A(C394): ORDER_FIELD_SPEC stays for the other kinds so the sale form is rebuilt by C395 from this contract, not here.
C395 (src/lib/components/orders/SoOrderForm.svelte (new) + src/routes/orders/new/trading/+page.svelte + src/routes/orders/new/orderLogic.ts + orderLogic.test.ts + orders/new/+page.server.ts + OrderEntryForm.svelte, owner): the sale order is one flat six-field form under a read-only quote strip (ORDER-FORMS D29/D30). /orders/new/trading now renders the contract itself: the six-fact strip (quote · buyer · quality · quoted delivery · payment · quoted rate, from quoteStripFacts + the quote prefill) over Quantity · Delivery start · Delivery end · Rate · Tolerance · Remark in so-form.json order, labels, required marks and units — quantity in whole 1,000 m from the quote estimate, both promise dates prefilled from the quote and required at booking (D30), the rate from the quote's final figure (above zero), tolerance 0–25 with 3/5/7/10 chips plus this buyer's last two, remark ≤ 500 with the buyer's past remarks offered. Gone from the SO: the four section blocks, the coverage/apportionment step and its hidden saleKind/parentSoId/coverageJson (deleted from OrderEntryForm; PPC owns cover), the production-role fields (buyer order no, cut size, rolls), the consignee field, the hidden duplicate sections and the Create SO button on the pending-quotes table (a sale starts from a quote); the SO kind is also out of the combined /orders/new picker (that route already redirects type=SO here). Saving is unchanged end to end: the form posts the six fields plus only type/quoteId/booking day and the quote's own paymentTermId/paymentCombo (the load now hands that pair over from the quote's costing snapshot, so payment rides the strip read-only), and the create action still carries buyer, broker, quality, picks, rate and the commercial terms from the quote. New rules on both surfaces in orderLogic.ts: salesPromiseError (Delivery start/end is required, then every C378 rule), a required SO tolerance and remarkError (500 max); roleRequiredFields no longer demands production fields from an SO booking (planning owns them, the record's ops completion still checks them). The draft is salesDraftPayload/salesDraftRestore — exactly the contract's field names, only for the quote it was typed against (withdrawn C391/C393). Proved non-UI (C65/C66), no UI test: an SSR render of the component (Vite ssrLoadModule + svelte/server, /tmp/opencode/ssr-check.ts) shows the strip keys in contract order and the form controls exactly qty, deliveryStart, deliveryEnd, rate, tolerancePct, remark with prefills 3000 / 2026-11-05 / 2026-11-20 / 98.50 / 5 and nopartyContractNo, lotSizeMtrPerRoll, rolls, consigneePartyId, coverage, saleKind, qtyUnit or "Create SO"; bun test src/routes/orders/new/orderLogic.test.ts55/55 PASS (6 new) proves the two-date rule, the required tolerance, production fields no longer demanded, the 500-char cap and a draft with exactly the contract fields + its own quote; bun run check exit 0, bunx tsc --noEmit adds no error in the touched files (repo-wide red unchanged). No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING §686. --assume: ?new=1/capture/lead/bare carries no longer open a blank SO form (the table is the only way in; the draft restores once its quote is picked), and a quote with no dhara books no payment term.
C396 (src/lib/contracts.ts + src/routes/orders/new/orderLogic.test.ts, owner): the sales form cannot drift from its contract (ORDER-FORMS D29/D30). C394/C395 moved the SO's field list into docs/contracts/so-form.json and made SoOrderForm.svelte render it, but nothing failed when a later row grew the screen a control of its own or edited the contract alone. contracts.ts now exports the check every form runs against its own spec: formFieldSpec(contract) (name, label, order, owner — the contract's fields, in contract order), assertFormSpec(contract, form, names) (refuses a control the contract does not list, a field drawn out of contract order, a field whose owner is not the contract's role, and a contract field with no control) and assertAbsent(contract, form, names) (refuses any name the absent list sends to another stage) — each a ContractError naming the contract file and its JSON pointer (docs/contracts/so-form.json#/absent/2, #/fields/1, #/fields/5/owner). orderLogic.test.ts reads the contract through soFormContract() and the screen's spec off SoOrderForm.svelte (the controls it branches on a field name, in draw order): the six fields, in contract order, all owned by sales, and no absent name with a control. Proved non-UI (C65/C66), no UI test: bun test src/routes/orders/new/orderLogic.test.ts60/60 PASS (5 new — green on the shipped list; rolls re-added fails naming /absent/2; a contract with tolerancePct deleted fails on the orphan control; rate drawn first fails #/fields/1 out of contract order; a remark re-owned to production fails #/fields/5/owner) and src/lib/contracts.test.ts9/9 PASS; bun run check exit 0, bunx tsc --noEmit adds no error in the touched files (repo-wide red unchanged). No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING §687. --assume: the sales form is SoOrderForm.svelte (C395 moved it there), so OrderEntryForm stays as it is; a field's control is an f.name === 'x' branch in draw order, never a hidden input or a helper-only name. Re-verified 2026-11-03 (re-run: the work was already in the tree, the earlier failure was the integration): bun test ./src/routes/orders/new/orderLogic.test.ts61/61 PASS (the 5 C396 cases plus C400's), bun src/lib/contracts.test.ts12/12 PASS, and both negative cases proved against the real tree, not just in memory — emptying remark out of docs/contracts/so-form.json fails 58/3 naming the orphan control; one {:else if f.name === 'rolls'} branch added to SoOrderForm.svelte fails naming docs/contracts/so-form.json#/absent/2: the sales form asks "rolls", which this contract puts in planning. Both restored (git diff clean for those two files); bun run check exit 0, bunx tsc --noEmit 148 repo-wide errors, none in contracts.ts / orderLogic.test.ts / contracts.test.ts. Note: this checkout has no tests/ directory (and so no tests/setup.ts), so the file was run against a throwaway preload symlink removed afterwards.
C397 (scripts + contracts, owner): the served screen is read back and held to its contract.scripts/form-contract-check.ts signs in against the running app on 127.0.0.1:4102 through its own /api/auth/sign-in/email (the app's ORIGIN as Origin/Host, the fresh@example.com session by default, --base/--only/--quote/--self-test flags, DATABASE_PATH used only to find a quote id so the form renders), fetches the contract's route with the cookie and hands the HTML to pure assertions in src/lib/contracts.ts — no browser: one sign-in plus one fetch per screen, and the parse itself is ~60 ms per 700 KB page (the rest is the app's own SSR time). assertRenderedForm requires the one POST ?/create form's controls, in document order, to be exactly the contract's fields in contract order (each once), exactly one submit button, no create shortcut beside it, and no control or error line inside a hidden container (class="hidden", display:none, visibility:hidden; hidden type="hidden" inputs stay legal) — role="alert", text-error, data-issue, data-form-error are the error markup it looks for. assertQuoteTable reads the screen's first table: a quote-no column, table-fixed w-full, 48 px per column inside 356 px, and no create link that bypasses the row tap; failures print the offending element or the needed width (needs 384 px at 356 px (8 columns)). /orders/new/trading's pending-quotes table gains the Quote column first (its rows already carried quoteNo), seven columns at 336/356 px. Proved non-UI (C65/C66), no UI test: bun scripts/form-contract-check.ts --self-test4/4 PASS (contract-true form passes; a deliberately hidden error div fails printing <p class="mt-1 text-xs text-error" data-issue="qty"> inside <div class="hidden">; a six-column table fails naming the missing quote-no column; seven columns pass at 336/356 px and an eighth reports 384 px), src/lib/contracts.test.ts12/12 PASS (3 new C397 cases), and the script against the still-pre-C395 live build exits 1 naming both real defects (table "has no quote-no column (columns: Party, Quality, Qty, Rate, Valid till, Days open)"; form asks partyContractNo, planning's) while the old page's hidden POST host yields 16 controls inside <form ... class="hidden"> — green once the watcher deploys C395+C397. bun run check exit 0, bunx tsc --noEmit adds no error in the touched files. No schema change, no migration, no edit/delete, light mode unchanged. PLANNING §688. Q372/A372; --assume: the contract's six fields (not the row's pre-C395 "four"), "the quote table" = the form route's pending-quotes table, 48 px per column against 356 px, and OrderEntryForm untouched (C396's reading). Re-verified 2026-11-02 20:30: the build still served on 127.0.0.1:4102 predates C395 (built 17:25, C395 committed fe9e3379), so --self-test 4/4 PASS again, the live run exits 1 in 0.97 s for 2 screens (~0.5 s a form) naming the same two defects (no quote-no column; partyContractNo), and bun run check exit 0 with bunx tsc --noEmit adding no error in the touched files — the first green run of the check is therefore the watcher's post-C397 deploy, and the script stays off deploy.sh (its gate sign-in user must exist in every host's DB, and the row asked for the check, not the wiring).
C398 (src/lib/components/focus/focusSpec.ts + FocusLayer.svelte + focusSpec.test.ts + src/lib/components/ScreenShell.svelte + src/routes/quote/new/+page.svelte + src/lib/components/quote/{NewWizard,TermsPopup,DharaPopup,YarnPopup,AdjustPopup,QualityPopup,NewSavedPopup}.svelte + src/lib/components/orders/OrderEntryForm.svelte, owner): one shell, one layer — the quote screen and the sale screen stop reading as different apps. Every class string now lives once in focusSpec.ts (SHELL_PAGE/WRAP/HEAD/BODY for a page, LAYER_FRAME/CARD/WIDTH/BACKDROP/HEAD/BODY/FOOT + LAYER_DEPTH + the keyboard-open docks for a layer): one width for every layer (w-full max-w-md, replacing the quote popups' max-w-xs, the SO's max-w-md/max-w-xs/max-w-sm and NewSavedPopup's max-w-sm), one backdrop (bg-black/15 backdrop-blur-[2px], bg-black/70 only while the keyboard is open on a stepping layer — C144 kept; was bg-black/30 flat or bg-black/15 blurred depending on the file), one header, one footer. ScreenShell.svelte is the page frame (background, pinned header with back control + title + actions snippet, max-w-2xl, body spacing) and focus/FocusLayer.svelte the over-the-form card (frame starting at top-14 so the page header stays reachable, backdrop, card, optional header/footer snippets, depth = z-40/z-[60]/z-[70], kbOpen, fill, center, bindable element for the sale wizard's [data-step-focus] ref). Adopted by /quote/new and the sale form (OrderEntryForm's non-damaged branch renders ScreenShell, its badges + "Start the wizard" in the actions slot) and by all nine of their popups/wizards (NewWizard, TermsPopup, DharaPopup, YarnPopup, AdjustPopup, QualityPopup, NewSavedPopup, and the SO wizard + payment-dhara + payment-slab + single-field popups) — none writes a width, a backdrop or a fixed inset-0 frame any more; popup headers/footers moved into head/foot snippets and the sale wizard's step errors moved into the shared footer (now flex-wrap) so they stay pinned above Back/Next. The damaged (X) sale form keeps C390's small card, and SavedPopup/QuotePreview/booking/production keep their own chrome (other screens, not named by the row). Proved non-UI (C65/C66), no UI test: src/lib/components/focus/focusSpec.test.ts4/4 PASS (one width and one blurred backdrop, card/frame/footer carrying none of their own; ScreenShell/FocusLayer restate no max-w-/bg-black/min-h-dvh and render a labelled back control under a pinned header; every adopted popup renders FocusLayer with no fixed inset-0 and no bg-black; both screens render <ScreenShell and the quote screen keeps no page height or width of its own); bun run check exit 0, bunx tsc --noEmit adds no error in the touched files (repo-wide red unchanged). No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING §689. A(C398): one layer width for every popup (a max-w-xs one-field popup widens only on a desk screen), the backdrop keeps its blur + keyboard variant inside the spec, and the shell header pins while the layer starts at top-14.
C399 (src/lib/historyLayer.ts (new) + src/lib/historyLayer.test.ts (new) + src/lib/components/focus/FocusLayer.svelte + src/routes/quote/new/+page.svelte + scripts/layer-audit.ts (new) + scripts/layer-audit.baseline.json (new), owner): Android back unwinds one layer, never the form under it. A layer now sits on its own history entry: historyLayer.ts keeps the layer stack, openLayer(dismiss) pushes exactly one entry (pushState merged over the router's own state plus __layer: depth, so the router still recognises the entry), and the entry is popped whether the layer was closed by backdrop, by a button or by back — a real back press runs the top layer's dismiss and leaves the page where it is, a self-close calls history.back() and swallows the popstate it caused, so the entry never lingers and a second back is the page's own; closing after a route change skips the back() so a save's navigation is not undone; nested layers unwind top first. Wired once, in focus/FocusLayer.svelte (the shared layer behind the SO focus wizard and every quote popup — TermsPopup, YarnPopup, DharaPopup, AdjustPopup, QualityPopup and the SO's dhara/slab/field popups), so the row's two targets get back through the component they both render and no caller can double-push; /quote/new's beforeNavigate cancels a popstate while openLayerCount() > 0 instead of unwinding the calc phase under an open layer. scripts/layer-audit.ts fails on a role="dialog" or fixed inset-0 layer whose file neither imports the helper nor renders FocusLayer, on one more layer than a baselined file already had, and on FocusLayer losing the helper — the 21 files that still draw their own layer are recorded in layer-audit.baseline.json with today's counts, so the audit is clean now and only shrinks. Proved non-UI (C65/C66), no UI test: bun test src/lib/historyLayer.test.ts9/9 PASS against a fake history (one entry per layer, 1→2→3; back closes the layer with the form entry still in place; a second back reaches the page below; nested layers unwind one at a time; a button close pops its own entry and the swallowed popstate does not re-dismiss; the pushed state keeps the router's historyIndex; a close after a route change leaves the new page alone) plus the audit run for real (ALL PASS, exit 0) and against a fixture layer that skips the helper (exit 1, naming NewPopup.svelte); bun scripts/layer-audit.tsALL PASS on the tree; bun run check exit 0, bunx tsc --noEmit adds no error in the touched files (repo-wide red unchanged). No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING §690. --assume: the helper lands in FocusLayer.svelte rather than in OrderEntryForm.svelte/TermsPopup.svelte (both already render it — C398), a layer with no dismiss (the saved sheet) pushes nothing so back cannot be silently swallowed, and the layers on other screens (booking, leads, production, tippan, FocusEntry) are baselined, not migrated, so this row does not rewrite screens it was not asked to touch.
C400 (AGENTS.md + scripts/row-watch.sh + src/routes/orders/new/orderLogic.test.ts, process): a moved or removed field is superseded where it used to be, in the same commit. A row could add a field's new placement and leave the old control behind because nothing in the process said the old markup must go. The rule is now in both places a row session reads: AGENTS.md (Domain rules, after Add a field ⇒ add its validation) and the row prompt in scripts/row-watch.sh — the contract's fields lose the field, its absent list gains the name with the stage that now owns it (where) and the reason (why), the old markup is deleted, and the contract test carries the negative assertion (assertAbsent in src/lib/contracts.ts, as the SO contract test does). The prompt is an unquoted heredoc, so the added line escapes its backticks — unescaped, building a prompt would execute bun run check and bunx tsc --noEmit as command substitutions. A row that adds the new placement and leaves the old one fails that test. orderLogic.test.ts gains a C400 case: the SO contract with remark moved to dispatch (removed from fields, added to absent) still drawn by the sales form fails assertAbsent, naming docs/contracts/so-form.json#/absent/... and dispatch. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts61/61 PASS (60 existing + the C400 case); bun run check exit 0, bunx tsc --noEmit adds no error in the touched files. assertAbsent already existed from C396, so no second helper. No schema change, no migration, no edit/delete, light mode unchanged. PLANNING §691. --assume: moved and removed are one assertion (both mean the old screen must stop drawing the name); the rule lives in AGENTS.md + the row prompt only, not in a new checker. Suite / build / deploy left to the watcher. Note: tests/setup.ts is absent from this checkout, so the one file was run against a throwaway preload file that was removed afterwards.
C401 (scripts/queue-next.ts + AGENTS.md + docs/QUEUE-AUTO.md, process): one owner-row per screen until that screen is stable. Two rows on one component in one block meant the same screen designed twice from two readings — planGroups unioned them into one lane, a silent serialisation where a worker meets both texts at once and the later row's wording was written against markup the first had already replaced. The rule is now stated in both places a session reads (AGENTS.md, "The corrections board / row line"; docs/QUEUE-AUTO.md, "One row per screen") and enforced by the engine: screenHolds(items) holds a pending row whose target overlaps an earlier open row (pending/triage/doing/verify/reopen/committed) with held: same screen as C4nn — runs after it closes; planBatches, planGroups, firstRunnable and --select drop held rows; --add, --set, --first, --next and --list report them, and renderQueue writes the reason on the row's notes line so the board warns instead of serialising quietly. The hold is derived on every read, never a status or stored flag, so it lifts by itself when the open row closes — a clash that was already serialised still runs after the first closes, and no row can be held immortal by a stale marker. blocked-owner/withdrawn rows hold nothing; doc-only targets (docs/*.md, AGENTS.md) are exempt exactly as targetsOverlap already treats them for lanes, so C402 is not held by C401. stripScreenHold re-derives the note each render so it never accumulates. Proved non-UI (C65/C66), no UI test: a 20-case script against the exported engine (/tmp/opencode/c401-check.ts: held naming the open row, first row free, plan/lane/--first drop it, it joins the plan once the blocker is done, doing blocks, folder↔file is one screen, docs exempt, disjoint both plan, blocked-owner/withdrawn hold nothing, a reopen row is never held, board note present and stale note replaced) 20/20 PASS, plus --list/--first/--next clean on the real board; bun run check exit 0, bunx tsc --noEmit adds no error in the touched files. No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING §692. --assume: "block" = the rows open on the board at the same time (the line is serial, so an earlier open row is the one that runs first); a reopen/verify row is never held (it is the live signal), only a pending row is; the clash comparison is targetsOverlap without doc-only targets, so folder↔file counts as one screen; blocked-owner and withdrawn rows hold no screen.
C402 (AGENTS.md + scripts/queue-next.ts + docs/contracts/README.md + docs/OWNERSHIP.md, process): no form block is queued before its contract exists and the owner has approved it. C394-C396 put the sales order's field inventory in docs/contracts/so-form.json, but nothing stopped the next block from being queued first and inventoried while it was built — the block's rows would each read the form differently and the contract would land after the markup it governs. The rule is now at the artefact and in both places a session reads: docs/contracts/<form>.json is the artefact a block is built from, every row of the block names it (docs/contracts/so-form.json for the SO block), AGENTS.md (Domain rules, after Move or remove a field) states it, docs/contracts/README.md repeats it, docs/OWNERSHIP.md rule 4 carries it, and queue-next.ts --precheck refuses a block row without it: formBlockRow (a row carrying a contract, or targeting a /new screen or a *Form.svelte), contractRefs (paths named in text/targets/acceptance/notes) and missingContractRef (the reason, or null) — refused when none is named and when the named file is not on disk, with the check's own --note allowed to carry the reference so a legacy row is recorded against the contract instead of parked; non-form rows untouched. Proved non-UI (C65/C66), no UI test: a 12-case script against the exported engine 12/12 PASS (a /new row with no contract refused; with so-form.json allowed; *Form.svelte detected; a missing jw-form.json refused naming the file; the note accepted; /orders, docs/*, AGENTS.md untouched; a contract target allowed; the three real SO block rows all name so-form.json; 0 open rows refused on the live board) plus an end-to-end CLI run in a throwaway copy of docs/ + queue-next.ts: refused exit 1 (names docs/contracts/<form>.json), recorded exit 0 with the reference, refused naming jw-form.json, and a doc-only row recorded. bun run check exit 0, bunx tsc --noEmit adds no error in the touched files (repo-wide red unchanged). No schema change, no migration, no edit/delete, light mode unchanged. PLANNING §693. --assume: a block row is detected from its targets rather than a new block: ledger column (fewest files, no board change, reversible, covers the SO block exactly), and the pre-check note may satisfy the reference because a legacy row's text is the owner's. The refusal also reaches the plan: needsPrecheck now gates a block row with no contract even when it already carries a pre-check record (row-watch records with || true, so the CLI refusal alone was bypassed) and precheckNeeds reports the contract reason, so such a row never enters planBatches/planWorkstreams/--first and rejoins the moment a pre-check names an existing contract. Regression check in the repo: bun scripts/contract-gate-check.ts — 15/15 PASS (refusal on a /new row, a *Form.svelte, a missing jw-form.json, the note satisfying the reference, plain screens and docs untouched; C394–C396 name docs/contracts/so-form.json; an un-contracted block row is not offered to a worker and is offered once its note names the contract; no runnable block row on the board lacks its contract). Suite / build / deploy left to the watcher.
C403 (scripts/queue-next.ts + scripts/row-watch.sh + scripts/row-watch-health.sh + AGENTS.md + docs/QUEUE-AUTO.md, process): no question waits for the owner — the board answers its own needs-you list by Pareto. On 2026-11-02 seven rows (C396–C402) sat blocked-owner behind retry questions the watcher itself had written, each costing a person's answer before the line moved. paretoAnswer(text) takes the safe 80/20 reading (a retry question → re-run unchanged, the failure was the integration; money/rate/price/cost/approval/payment/invoice/data → leave the committed value and the current behaviour exactly as they are; a scope question → the narrowest reading of the row as written; otherwise the row as written), autoAnswers(questionsMd, items, reopens, today) writes each open question as - R<n> … auto (Pareto) (a recorded assumption, which parseOpenQuestions already settles), answers rows ownerNeeds reports with no question of their own the same way, notes auto-answered (Pareto): … on the row, archives the double-fail reopen line so the hold it was released for cannot re-hold it, and re-queues the row. bun scripts/queue-next.ts --auto-answer [--dry-run]; row-watch.sh calls it at the top of every tick (run + once) and clears the released row's .row-watch-fails counter, so FAIL_MAX means "attempts since the last answer" instead of a permanent hold, and a repeated answer carries (attempt N) so a systematic failure stays visible; row-watch-health.sh reads that number and exits 2 once a row has been answered 3+ times (the fail counter can no longer see it). Proved non-UI on the real board: --dry-run read the seven open retry questions (Q371, Q373, Q375–Q377, Q379, Q381), the run wrote seven R lines, re-queued C396–C402 and left docs/NEEDS-OWNER.md at "Nothing needs you right now"; the owner's own scope answers (A374, A378, A380) and rows with no question/hold were untouched; --auto-answer on a clean board prints "nothing waits" and writes nothing. No schema change, no migration, no edit/delete, no UI change, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING §694. --assume: auto-answer belongs to the engine (a session keeps its own --ask/--assume path), the record is R<n> because it is an assumption and not the owner's word, and a released row's fail counter is cleared so the cap cannot re-hold it on the same broken deploy before the retry it was released for.
Model switch (owner): the row line runs on meta/muse-spark-1.3-contributor.scripts/row-watch.sh + scripts/row-run.sh default MODEL to it (ROW_MODEL overrides per run) and ~/services/swarm.env sets the same, so the supervised watcher picks it up on restart. opencode models lists it active (Meta). Capture gateway untouched. PLANNING §695.
C396 re-run (src/routes/orders/new/orderLogic.test.ts + src/lib/contracts.ts, docs only): the contract test stands as written — no code changed. The tree already holds the enforcement (assertFormSpec/assertAbsent/formFieldSpec, the salesFormSpec reader over SoOrderForm.svelte's f.name === chain, four C396 cases + the C400 case); the row's "four fields" and OrderEntryForm.svelte target are pre-C395 wording, superseded by the six-field contract and SoOrderForm.svelte (standing --assume, kept). Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts ./src/lib/contracts.test.ts61 pass, 0 fail (throwaway tests/setup.ts preload, removed afterwards). No schema change, no migration, no edit/delete, light mode unchanged. PLANNING §696. Suite / build / deploy left to the watcher.
Cloudflare Pages port (cf-pages branch, infra): the same tree now builds for @sveltejs/adapter-cloudflare (ADAPTER=cloudflare bun run build → .svelte-kit/cloudflare + wrangler.jsonc) while the default Bun build is untouched. The blockers were the four places the app read the repo at request time: docs/contracts/*.json and docs/*.md (loader + /docs), the upload drops (node:fs in /upload-file, the [name] download and the guard's driver photo) and the docs bundle size. Contracts keep readFileSync as the source of truth and fall back to a new BUNDLED_CONTRACTS map (the bundled set is asserted against docs/contracts in contracts.test.ts, so a contract added without bundling fails); /docs and [slug] read import.meta.glob('/docs/*.md', { query: '?raw' }) instead of readdir; uploads go through a new src/lib/server/upload-store.ts that writes R2 (UPLOADS binding via $app/env/private) when the host binds one and the local directories otherwise — /upload-file POST/GET and the [name] download all use it, and guard's driver photo moved to it. vite.config.ts picks the adapter from ADAPTER; wrangler.jsonc carries compatibility_flags: ["nodejs_compat"], the R2 binding and the pages.dev vars; .dev.vars is gitignored. Proved on workerd (wrangler pages dev) against the production dataset in Turso aws-ap-south-1: live-check86/86 PASS (all 81 screens, docs included) and an upload round-trip (POST /upload-file → 200 {saved}, GET → listed, R2-backed). Type check clean. Remaining on that host: OCR/PDF capture (tesseract/magick/mutool are CLIs — not available on Workers) and a deploy still needs the Cloudflare token. No schema change, no migration, no UI change, light mode unchanged.
Workers CPU budget (/hishob, /weft-issue, infra): both screens were over the 10 ms CPU ceiling because of N+1 reads, not because of SSR./hishob's H1 legs preview called legsForContract once per open contract (up to 100 contracts × 2 sequential Turso queries) inside the load; it now fetches every open contract's lots and dispatch slips in two inArray queries and groups them in a Map, so computeLegs runs synchronously with no awaits. /weft-issue did select() on the whole beam (6,720 rows) and order (1,899) tables and then beams.find(...) inside the tile loop (O(beams × issued-beams)); it now selects only the eight columns the screen reads and looks the beam up through a Map. Warm page time 5.9 s → 0.26 s locally. Proved on the real edge, not a local workerd: trading-tyno.pages.dev went from error code: 1102 on five screens to 86/86 live-check PASS (/hishob 1.05 s, /weft-issue 1.75 s, /reports 2.50 s, /tippans 0.39 s, /tippans/new 0.76 s), with no plan change and no limits.cpu_ms tuning. bun run check exit 0. No schema change, no migration, no UI change.
Nightly DB backup (scripts/db-snapshot.ts, scripts/db-restore.ts, .github/workflows/db-backup.yml, infra): the only durable copy of production is Turso itself, so it now has a daily restore point. First attempt was a Cloudflare Worker cron dumping into R2 — abandoned, because serialising 33k rows blows the free plan's 10 ms CPU budget (error code: 1102, confirmed in wrangler tail), and a Pages project has no cron triggers at all. The workflow instead runs on GitHub Actions at 20:30 UTC (02:00 IST): bun scripts/db-snapshot.ts reads each table through the libSQL client and writes one gzipped JSON per table plus a manifest (blobs base64-tagged, integers intact), uploaded as a 14-day artifact. Restore is bun scripts/db-restore.ts <snapshot>: wipe, resolve the FK graph from the target's own pragma foreign_key_list, insert parents-first, then re-create the evt dedupe index — orphan rows are reported and skipped rather than silently dropped. Proved end-to-end: workflow_dispatch run success, artifact turso-snapshot-416157052531.25 MB, log 58 tables, 33172 rows; the same dump ran locally in 4.4 s. Repo secrets TURSO_DATABASE_URL + TURSO_AUTH_TOKEN set. No schema change, no migration, no UI change.
Uploads move into the database (src/lib/server/upload-store.ts, src/lib/imageShrink.ts, src/lib/server/schema.ts, infra): owner decision — Turso is the only datastore, no object store. The UPLOADS R2 binding and both buckets are gone; files are rows in the new upload_file table (bin, name, mime, size, base64 bytes, unique on bin+name) so /upload-file POST/GET, the [name] download and the guard's driver photo all work with no filesystem and no second service. Two consequences shaped the change: (a) the Worker must base64-encode the body inside its 10 ms CPU budget, so images are downscaled in the browser to ~1600 px / q0.75 before upload (shrinkImage, wired into UploadDrop and the guard's photo picker via a DataTransfer) and every bin is capped at 2 MB server-side — the old 25 MB / 100 MB caps are impossible without a CPU budget; (b) the migration-only backups drop now accepts 2 MB like the rest, because a 100 MB legacy backup cannot be a row. Blob values are base64 text rather than a BLOB so the same code runs under Bun and workerd (no Buffer) and the nightly snapshot already serialises them. Proved on the deployed edge: POST /upload-file → 200 {ok, saved}, GET lists it, GET /upload-file/<name> returns the exact bytes with the stored mime, and the row is visible in Turso (bin=screenshots size=1 mime=image/png); live-check86/86 against trading-tyno.pages.dev. Table created on Turso directly (create table if not exists), matching the drizzle definition for future pushes. No UI redesign, no schema change to business tables.
Row line autostart (.opencode/plugins/row-line.ts, .opencode/agent/row.md, opencode.json, infra): opening OpenCode on this checkout now brings up the dev server (Vite + HMR on 127.0.0.1:5173) and the row watcher (ROW_MODEL=opencode-go/deepseek-v4.1-flash). Both are detached and self-guarding (Vite cannot rebind a taken port; row-watch.sh takes .row-watch.lock.d), so a config reload or a second window never double-starts them; worktrees are skipped. 7668c583 had deleted the row agent, opencode.json and tests/, so --agent row could not start a session — both files restored from 7668c583^ and corrected (no tests/ step, no heavy.sh test routing, model pins moved off the dead opencode-go/space-bunny-free to opencode-go/deepseek-v4.1-flash). @opencode/plugin@2.2.5 added as a dev dependency. Board: 272 rows, done:263 withdrawn:2 doing:1 pending:6; C400 first, C401/C402 after; C397–C399 stay blocked (same screen as C396 + no contract). No schema change, no migration, no UI change. PLANNING §697.
C400 re-run (AGENTS.md + scripts/row-watch.sh + src/routes/orders/new/orderLogic.test.ts, process): the rule and its proof stand as shipped — no code changed. C400's three acceptance points were already in the tree: the rule is in AGENTS.md (Domain rules, after Add a field ⇒ add its validation) and in the per-session row prompt at scripts/row-watch.sh:488 (backticks escaped because the heredoc is unquoted); assertAbsent(contract, form, names) is exported from src/lib/contracts.ts:327 and used by assertFormSpec, the SO contract test and scripts/form-contract-check.ts; and orderLogic.test.ts carries the negative assertion (C400 — a moved field is superseded where it used to be): the SO contract with remark moved to dispatch and still drawn by the sales form fails assertAbsent, naming docs/contracts/so-form.json#/absent/… and dispatch. The prior attempt's failure was integration (commit), not the row's work, so the re-run only re-proved it. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts61 pass, 0 fail, 258 expect() calls (the C400 case included; tests/setup.ts now present, clearing TURSO_*). bun run check exit 0, bunx tsc --noEmit adds no error in the touched files (repo-wide red unchanged). No schema change, no migration, no edit/delete, light mode unchanged. PLANNING §698. --assume: none new — the standing reading (moved and removed are one assertion; the rule lives in AGENTS.md + the row-watch prompt, not a second checker) stays. Suite / build / deploy left to the watcher.
C396 attempt 3 (src/routes/orders/new/orderLogic.test.ts + src/lib/contracts.ts + tests/setup.ts, docs + test preload): the contract test stands as written; the repeat was the integration, not the check. The tree already holds C396 in full (formFieldSpec / assertFormSpec / assertAbsent in src/lib/contracts.ts; the four-case C396 describe plus the C400 supersede case in orderLogic.test.ts, reading docs/contracts/so-form.json through soFormContract() and the sales screen control chain off SoOrderForm.svelte in draw order), and the earlier re-run entry above still describes it. Re-proved the acceptance directly with the narrowest non-UI check (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts61 pass, 0 fail - green on the shipped six-field contract; rolls re-added to the sales spec fails naming docs/contracts/so-form.json#/absent/2 and planning; a contract with tolerancePct deleted fails naming "tolerancePct"; a draw out of contract order fails at #/fields/1; a remark re-owned to production fails at #/fields/5/owner. No code edit, no schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING section 699. --assume: none new - the two stale row readings (four fields, OrderEntryForm.svelte) stay superseded by C395 as already recorded; the one non-doc change is the preload bunfig.toml names, tests/setup.ts (the TURSO_* scrub, restored from 7668c583^), because without it any bun test <file> in this checkout dies on preload not found and a row has no narrow way to prove anything.
Row line repaired (scripts/deploy.sh, scripts/row-watch.sh, scripts/row-run.sh, src/lib/server/watcher.ts, infra): no row could close on this checkout. Every session finished, then integration logged commit failed (1/2); left claimed for the next pass and the row re-ran forever (C396 reached attempt 3, C400 had failed before). row-watch.sh degrades a non-deploy host to --commit-only, but deploy.sh's stale-serve guard promoted every commit-only call to a FULL deploy when no .deployed-head marker exists — and that clause ignored ROW_SHIP_EVERY. This Windows checkout has no marker, so every row ran bun run build, which dies on @libsql/client's native sqlite3 binding (win32-arm64) → vite.exe exited with code 1. The promotion is now gated on on_deploy_host (and ROW_SHIP_EVERY > 0; 0 = never promote, which is what row-watch passes now), so a checkout that cannot serve commits and pushes without building, and a real deploy host still promotes on a missing marker. Also fixed: the default model in row-watch.sh/row-run.sh/startWatcher() was meta/muse-spark-1.3-contributor, gone from the catalog — any start path without ROW_MODEL (the /queue Start button, a bare bash scripts/row-watch.sh run) launched sessions on a dead model; all three now default to opencode-go/deepseek-v4.1-flash. row-watch.sh now resolves ROW_SERVER from opencode service status (the desktop service runs on a random loopback port) instead of assuming :4096, so row sessions attach and show under Sessions instead of running headless. Proved non-UI: bash -n on all three scripts, the promotion decision simulated on this host (stays commit-only for SHIP_EVERY 0/3/999999), bun -e importing watcher.ts (state reads: running, pid 1177, lane 1 = C396). PLANNING §700.
Row line attach (scripts/row-watch.sh, infra): the desktop service needs OPENCODE_PASSWORD, or every row session dies at start. §700's URL discovery attached the line to http://127.0.0.1:28894, and the CLI refused each session with requires a password; set OPENCODE_PASSWORD (C397/C401 failed rc=1 in ~2 s, C400 hit its fail cap and was held). The watcher now reads "password" from ~/.config/opencode/service.json and exports OPENCODE_PASSWORD before attaching; without that file it keeps the :4096 default and runs headless rather than failing. Verified with the CLI: session list --server http://127.0.0.1:28894 --format json returns the row C396 session. Also recorded as an operational rule: bash reads a script incrementally, so editing scripts/row-watch.sh under a live watcher corrupts the running copy (seen twice today: unexpected EOF while looking for matching " at 19:44, the loop stopping at 19:50) — edit, commit, then restart. Leftover tests/setup.ts scratch from a failed row attempt removed. PLANNING §701.
C400 close-out (.opencode/agent/row.md + docs/PLANNING.md, process): the supersede rule now reaches the row agent file, so no session can be told to move a field and find nothing about the old placement in its own steps.AGENTS.md (Domain rules) and the watcher's prompt (scripts/row-watch.sh:511) already carried it, and assertAbsent/assertFormSpec already fail a stale control (C396/C397); the gap was .opencode/agent/row.md step 6 — the file every --agent row session (the watcher's attach path, row-run.sh) reads — which said nothing about a moved field. Step 6 now says: the contract's fields lose it, absent gains it with where and why, the old screen's markup is deleted rather than left beside the new placement, and the negative assertion is added (assertAbsent, src/lib/contracts.ts). Acceptance re-proved with the narrowest non-UI checks (C65/C66), no UI test: the C400 case of bun test ./src/routes/orders/new/orderLogic.test.ts -t C400 green (remark moved to dispatch, still drawn → fails naming docs/contracts/so-form.json#/absent/… and dispatch), and the rule's presence in all three files confirmed by grep. No code change beyond the agent prompt, no schema change, no migration, no edit/delete, light mode unchanged. PLANNING §702. --assume: "the row prompt" means both surfaces a session reads — the watcher's per-row brief and the row agent file — so the rule belongs in both, not in a new checker. Suite / build / deploy left to the watcher.
C396 attempt 4 (tests/setup.ts restored; src/lib/contracts.ts + src/routes/orders/new/orderLogic.test.ts unchanged, test + preload): the contract test is now runnable in this checkout and its teeth are shown by mutating the screen, not by reading it. C396's enforcement has been at HEAD since §687 (formFieldSpec / assertFormSpec / assertAbsent, the five C396 cases + the C400 supersede case), so no assertion was added; what was missing was proof — bunfig.toml preloads ./tests/setup.ts and 7668c583 deleted tests/, so every bun test died at error: preload not found "./tests/setup.ts" and the last three attempts each rebuilt a throwaway preload and deleted it. tests/setup.ts is restored verbatim from 7668c583^ (the TURSO_* scrub plus the C118 queue/row cap scrub) and kept, so the file bunfig.toml names exists and the narrow proof works for the next row too; bunfig.toml and its root = "./tests" are untouched. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts61 pass, 0 fail, 258 expect() calls green on the shipped six-field contract, and a deliberate mutation — rolls re-added to SoOrderForm.svelte as a real control branch inside the {#each contract.fields} chain — fails 3 of the 5 C396 cases (Expected to not contain: "rolls", received the drawn list with rolls in it; assertFormSpec names docs/contracts/so-form.json#/absent/2 and planning), the screen then restored byte-identically (git status clean) and green again; a contract with tolerancePct deleted fails naming "tolerancePct", out-of-contract order fails at #/fields/1, remark re-owned fails at #/fields/5/owner. bun run check exit 0 (389 pre-existing warnings, 0 errors); bunx tsc --noEmit adds no error in the touched files (154 repo-wide pre-existing errors unchanged). No field added, no schema change, no migration, no edit/delete, light mode unchanged. PLANNING §704. --assume: restoring tests/setup.ts is the infra the row is proved with, not the row's subject — the file bunfig.toml already names, restored verbatim and tracked rather than scratch; the two stale row readings (four fields, OrderEntryForm.svelte) stay superseded by C395 as recorded in §696/§699. Suite / build / deploy left to the watcher.
C400 re-verified (AGENTS.md + scripts/row-watch.sh + src/lib/contracts.ts, docs only): the rule and its proof stand as shipped; no code changed. The three acceptance points are at HEAD - the rule in AGENTS.md (Domain rules, after Add a field => add its validation) and in the per-session prompt at scripts/row-watch.sh:511 plus .opencode/agent/row.md; ssertAbsent(contract, form, names) exported at src/lib/contracts.ts:327; and the C400 case in orderLogic.test.ts (
emark moved to dispatch, still drawn by the sales form -> fails naming docs/contracts/so-form.json#/absent/... and dispatch). The enforcement is wider than asked: ssertAbsent is also called from ssertFormSpec (line 365) and from ssertRenderedForm (line 620), so a stale control fails on both the screen's source spec and the served create-form bytes; scripts/form-contract-check.ts reaches it through ssertRenderedForm. Proved non-UI (C65/C66), no UI test: un test ./src/routes/orders/new/orderLogic.test.ts -t C400 1 pass, 0 fail, 4 expect() calls. No schema change, no migration, no edit/delete, light mode unchanged. PLANNING section 704. --assume: none new - the standing readings hold (moved and removed are one assertion; prose plus the shared helper, not a second checker). Suite / build / deploy left to the watcher.- Row line autostart is now a loop (.opencode/plugins/row-line.ts, infra): opening OpenCode starts the dev server + watcher, and keeps them up while it is open. Before, setup() ran once per load, so a watcher killed by a crash, a deploy or the board's Stop button stayed dead until the next reload. The plugin now ticks every minute: restarts Vite when 127.0.0.1:5173 stops answering, starts row-watch.sh run whenever the lock pid is gone, logs each decision to screenshots/row-line.log, and clears its timer on unload. Liveness goes through Git Bash (kill -0) because the watcher's pid is an MSYS pid. Proved live: reload → tick started the dead dev server (Vite ready, HTTP 303 on /); kill -KILL 3056 → next tick logged watcher pid 3056 is gone - starting it and the lock returned as pid 5113 (watcher: running (pid 5113)). PLANNING §702.
C396 attempt 5 (src/routes/orders/new/orderLogic.test.ts + src/lib/contracts.ts + docs/contracts/so-form.json, docs + test): the enforcement stands as shipped and the narrow check is green on this tree - no code changed. The row's four acceptance points are at HEAD since 687: formFieldSpec / assertFormSpec / assertAbsent in src/lib/contracts.ts (each a ContractError naming the contract file and its JSON pointer), and the four C396 cases plus the C400 supersede case in orderLogic.test.ts, reading docs/contracts/so-form.json through soFormContract() and the sales screen's spec off SoOrderForm.svelte in draw order. The row's "four fields" and OrderEntryForm.svelte target are pre-C395 wording: the contract has six fields and OrderEntryForm.svelte serves the legacy combined, job-work, production and damaged forms, so it is not the contract's form (standing --assume). Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts61 pass, 0 fail, 258 expect() calls - green on the shipped list; rolls re-added to the spec fails naming docs/contracts/so-form.json#/absent/2 and planning; a contract with tolerancePct deleted fails naming "tolerancePct"; a draw out of contract order fails at #/fields/1; a remark re-owned to production fails at #/fields/5/owner. bun run check exit 0 (389 pre-existing warnings, 0 errors); bun x tsc --noEmit adds no error in the touched files (154 repo-wide pre-existing errors unchanged). Note for the next session: bunfig.toml sets root = "./tests", so a default bun test never discovers this file - it runs by explicit path. No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 705. --assume: none new.
C400 re-run (.opencode/agent/row.md, docs + row prompt): the supersede rule and its teeth stand at HEAD; the one gap was the row prompt telling sessions not to run the test that catches a stale placement.AGENTS.md (Domain rules) and the watcher's per-row brief (scripts/row-watch.sh:511) carry the rule, assertAbsent(contract, form, names) is exported at src/lib/contracts.ts:327 and called by assertFormSpec/assertRenderedForm and the SO contract test, and the C400 case in orderLogic.test.ts moves remark to dispatch and fails on the still-drawn control, naming docs/contracts/so-form.json#/absent/<i> and dispatch. .opencode/agent/row.md step 7 still said the checkout has no tests/ directory and no test suite (removed 2026-10-05) and never to run bun test here - so a session would skip the contract test. Step 7 now says the suite is back (bunfig.toml preloads tests/setup.ts, which clears TURSO_* for local data.db) and that a moved or removed field is proved by bun test ./src/routes/orders/new/orderLogic.test.ts, failing through assertAbsent on #/absent/<i>. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts -t C4001 pass, 0 fail, 4 expect() calls; the rule re-checked by grep in all three files; bun run check exit 0, bunx tsc --noEmit adds no error in the touched files. No schema change, no migration, no edit/delete, light mode unchanged. PLANNING section 706. --assume: the row prompt is both surfaces a session reads (watcher brief + row agent file), and the proof stays the existing assertAbsent + SO contract test, not a second checker. Suite / build / deploy left to the watcher.
C396 attempt 6 (src/routes/orders/new/orderLogic.test.ts, test): the absent check now also reads the controls the screen names itself, so a literal <input name="rolls"> fails instead of passing unseen.salesFormSpec() followed only the form's f.name === 'x' branch chain, so a control written outside {#each contract.fields} (literal name="rolls") was not in the drawn list and assertAbsent was never handed it. The C396 absent case now unions that chain with salesLiteralControls() (the component's name="x" / name='x' attributes), runs assertAbsent first so the failure is the contract's own message, and derives the refused entry's index from the contract instead of hardcoding it. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts61 pass, 0 fail, 260 expect() calls; one name="rolls" attribute added to the remark textarea fails with docs/contracts/so-form.json#/absent/2: the sales form asks "rolls", which this contract puts in planning: rolls follow from the cut size while the branch reader still sees only the six fields; deleting tolerancePct from the contract fails the shipped-list case; both mutations reverted (git status clean but for the test file). bun run check exit 0 (389 pre-existing warnings, 0 errors); bunx tsc --noEmit keeps the repo's 154 pre-existing errors, none in the touched files. No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. PLANNING 707. --assume: a literal name attribute is a control the form draws and belongs in the absent check; contract, component and src/lib/contracts.ts untouched. Suite / build / deploy left to the watcher.
C400 (src/lib/contracts.test.ts, process + test): the SO contract test now calls assertAbsent itself, so a moved field's old placement fails in the file that owns the contract's shape. The rule already stands in AGENTS.md (Domain rules, Move or remove a field => supersede it in the same commit) and in the watcher's per-row brief (scripts/row-watch.sh:511) plus .opencode/agent/row.md; assertAbsent(contract, form, names) has been exported at src/lib/contracts.ts:327 since C396 and is called by assertFormSpec and assertRenderedForm. The gap was that src/lib/contracts.test.ts reached it only through assertRenderedForm; it now imports it and carries C400: a field the contract moves to another stage fails where it used to be - the contract's six own names pass, rolls (absent at planning) fails naming the contract file, /absent/2, "rolls" and planning, and a contract with remark moved out of fields onto absent at dispatch fails on the still-drawn remark, naming /absent/10 and dispatch. Proved non-UI (C65/C66), no UI test: bun test ./src/lib/contracts.test.ts14/14 pass; bun run check exit 0; bunx tsc --noEmit keeps the repo's 154 pre-existing errors, none in the touched file. No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. PLANNING 707. --assume: the SO contract test is both files that assert over the SO contract (the screen's spec in orderLogic.test.ts and the contract itself here), so the direct use lands in both, not in a third checker. Suite / build / deploy left to the watcher.
/queue + /signin 500s (src/routes/+layout.svelte, src/routes/+layout.server.ts, vite.config.ts, infra): two causes, both fixed. (1) The shell read the route from $app/state (const pathname = $derived(page.url.pathname)); SvelteKit 3 binds page.url to the render context, so resolving <svelte:head><title>{docTitle}</title> on the server threw state_read_outside_render → 500. +layout.server.ts now returns { session, path, search } and the layout derives from data.path/data.search, so no page state is read outside render (the $app/state import is gone). (2) Vite watches the repo root, and the row line writes runtime state there every tick (.row-board.lock.d/pid → page reload every ~2 s, lane claims, queue stamps, screenshots/*.log, plus .fast-check/tsconfig.json and .svelte-kit/ rewritten by every bun run check); each write forced a full SSR program reload, and a request landing mid-reload hit the Svelte dev race null is not an object (evaluating 'context.function') at +layout.svelte:215/235. server.watch.ignored now covers all of it, so only real source edits reload. Proved: signed in as the seeded owner, /queue → 200 with <title>Queue · Trading</title> and its columns (3.5 s cold, 0.14 s warm), signed out → 303 (the gate); state_read_outside_render count since the fix 0; bun run check exit 0. Remaining, not a page bug: a reload landing mid-render while rows edit src/** can still throw once, and a cold render after a reload is slow on this box (8-30 s while tsc/check run) — a refresh fixes both, and both stop when the line is idle. PLANNING §704.
C396 attempt 7 (src/routes/orders/new/orderLogic.test.ts, test): the field-chain reader now sees a branch however it is written, so a re-added control can no longer be drawn yet stay invisible to the contract check. The enforcement itself (formFieldSpec / assertFormSpec / assertAbsent, the C396 cases + C400) stands at HEAD since 687; the gap was salesFormSpec() scanning whole lines for {#if f.name === 'x' with single quotes only, so f.name === "rolls" - or a header reflowed onto two lines - never reached assertFormSpec and the form passed while drawing the field. The reader now takes the screen's field chain as a brace-balanced block (fieldChainOf, keeping the tolerance chips' nested {#each} inside it) and collects names from its {#if} / {:else if} headers (formSpecOf) on both quote styles and across a line break; an attribute comparison (min={f.name === 'deliveryStart' ? ...}) is not a control and stays out, so the drawn list is still the six contract fields in contract order, and the commit handler's tolerancePct stays out too. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts64 pass, 0 fail, 269 expect() calls - green on the shipped list; a double-quoted {:else if f.name === "rolls"} branch added to the real SoOrderForm.svelte fails 4 cases naming docs/contracts/so-form.json#/absent/2: the sales form asks "rolls", which this contract puts in planning, the screen then restored byte-identically (git status clean); tolerancePct deleted from the contract fails naming "tolerancePct"; out-of-contract order fails at #/fields/1; remark re-owned to production fails at #/fields/5/owner. No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 710. --assume: none new - the row's "four fields" and OrderEntryForm.svelte target stay superseded by C395 as recorded in 696/699/705; a field's control is a branch in the contract's chain, which is where a re-added control is written.
C400 (AGENTS.md + scripts/row-watch.sh + .opencode/agent/row.md, process): the supersede rule now names the file that carries its proof, so the commit that closes the row is the one that holds the assertion. The row kept returning because its commits changed none of its targets - the rule, the helper and its test were at HEAD, but the proof landed only in orderLogic.test.ts and the docs, and the board now refuses that (AGENTS.md, A row closes only when one of its own targets changed in the commit). So the rule gained one clause on every surface a session reads: the contract test that holds the negative assertion goes on the row's targets line before the commit, because a proof in a file the row does not name moves nothing. That clause is in AGENTS.md (Domain rules, Move or remove a field => supersede it in the same commit), in the watcher's per-row brief at scripts/row-watch.sh:511 (unquoted heredoc, so the added text carries no $ or backtick) and in .opencode/agent/row.md step 6. The standing acceptance re-proved as well: assertAbsent(contract, form, names) is exported at src/lib/contracts.ts:327, called by assertFormSpec (line 365), assertRenderedForm (line 620) and directly by both SO contract tests (src/routes/orders/new/orderLogic.test.ts for the screen spec, src/lib/contracts.test.ts for the contract itself), and the C400 case moves remark onto absent at dispatch then feeds it the sales form's real spec, so the still-drawn control fails naming docs/contracts/so-form.json#/absent/<i>, "remark" and dispatch; the C396 case also refuses a literal name="x" outside the {#each contract.fields} chain. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts -t C4001 pass, 0 fail, 4 expect() calls; bun test ./src/lib/contracts.test.ts14/14 pass; the clause re-grepped in all three surfaces. No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. PLANNING 708. --assume: the clause is the row prompt's business, not a new checker - it keeps the standing reading that moved and removed are one assertion and that the enforcement is assertAbsent, and it makes the proof count at the close gate instead of adding a second gate. Note for the next session: bunfig.toml sets root = "./tests" and tests/ holds only setup.ts, so the contract test is run by explicit path as .opencode/agent/row.md step 7 says - a bare bun test discovers nothing here. Suite / build / deploy left to the watcher.
C397 (scripts/form-contract-check.ts, owner): the contract check reads the page again before it judges, so a dev server's first-request 500 no longer reads as a contract failure. C397's assertions and script were at HEAD since §688 and --self-test was green, but the live run was not: the app 500s the first SSR request after every (ssr) program reload, and parallel sessions reload it constantly, so the check's first fetch almost always hit it (/orders/new/trading returned 500) and exited 1. page() now refetches the same URL once when the first response is 5xx, then applies the same verdict — a screen that 500s twice still fails, naming the route and status. What it asserts is unchanged: the contract's fields in its order (six after C395), exactly one create action, no control or error line in a hidden container, and on the table screen a quote-no column, seven columns inside 356 px, no second create action. Proved non-UI (C65/C66), no UI test: bun scripts/form-contract-check.ts --base http://127.0.0.1:51732/2 PASS, exit 0 (Quote · Party · Quality · Qty · Rate · Valid till · Days open — 336/356 px; 6 fields in contract order (qty, deliveryStart, deliveryEnd, rate, tolerancePct, remark), one create action, no hidden inputs), the retry exercised in that run; --self-test4/4 PASS (hidden error div printed with its container, six-column table names the missing quote-no column, eight columns report needs 384 px at 356 px); parse cost 3 ms on the served 823 KB page, warm fetch 583 ms, so about a second per form on a warm app. No schema change, no migration, no field moved, no edit/delete, light mode unchanged. PLANNING §712. --assume: a 500 before the page is served is the server's warm-up, not the screen's contract, so one retry is the safe reading.- C396 attempt 9 (src/routes/orders/new/orderLogic.test.ts, test): a re-added control is caught however its name is written, so name={"rolls"} can no longer sit on the sales form unseen. The enforcement (formFieldSpec / assertFormSpec / assertAbsent) and both readers were at HEAD (687/710/711) and the suite was green, but the two readers together still missed one spelling: the branch reader follows only the {#each contract.fields}{#if} headers, and salesLiteralControls() matched only a quote straight after name=, so a control written as a braced string literal - name={"rolls"} - was drawn, posted, and invisible to assertAbsent; the row's own "re-adding rolls fails it" had a hole. salesLiteralControls() now runs through literalControlsOf(source), reading name="x", name='x', name={"x"}, name={'x'} and the backtick form alike while leaving name={f.name} out (a template expression is not a control name), and withLiteralControl(name) builds that screen in memory so the component on disk is never edited to prove it. A new C396 case pins it: the braced control is absent from the chain spec, present in the literal controls, and assertAbsent refuses it naming docs/contracts/so-form.json#/absent/<i> (the index read from the contract), "rolls" and planning. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts67 pass, 0 fail, 279 expect() calls - green on the shipped six-field contract with every earlier case still failing as it should (rolls branch at #/absent/2, tolerancePct deleted naming it, out-of-contract order at #/fields/1, remark re-owned at #/fields/5/owner); the teeth shown on the real tree, not only in memory - one <input name={"rolls"} /> inserted into SoOrderForm.svelte fails the absent case with docs/contracts/so-form.json#/absent/2: the sales form asks "rolls", which this contract puts in planning: rolls follow from the cut size, then the screen restored byte-identically (git status clean for it). No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 713. --assume: a braced string literal is the same control as a quoted one (its value is fixed, not computed), so it belongs in the absent check; the row's "four fields" / OrderEntryForm.svelte stay pre-C395 wording as recorded in 696/699/705.
C396 attempt 8 (src/routes/orders/new/orderLogic.test.ts, test): the contract test now holds in both directions, so adding a field the right way - contract edit plus the screen - is green instead of failing. The enforcement (formFieldSpec / assertFormSpec / assertAbsent, the C396 cases + C400) and the field-chain reader landed in 687/710; the shipped-list case still compared the screen against a hardcoded second copy of the six names, so a field added to docs/contracts/so-form.json and drawn by SoOrderForm.svelte failed the suite - the opposite of the row rule that only an edit without the contract fails. The case now reads the contract document on disk (contractDoc()), orders it by its own order, checks every entry is owned by the contract's role, and holds the screen spec equal to it, so there is no third list to edit; two new cases pin the rule from both sides - brokerageNote added to the contract at order 7 (owner sales) and drawn by the screen passes, discountPct drawn by the screen alone fails naming docs/contracts/so-form.json#/fields and "discountPct". Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts66 pass, 0 fail, 273 expect() calls (green on the shipped six-field contract; rolls re-added still fails at #/absent/2 naming planning; deleting tolerancePct still fails naming it; out-of-contract order still fails at #/fields/1; remark re-owned still fails at #/fields/5/owner). bun run check exit 0 (389 pre-existing warnings, 0 errors); bunx tsc --noEmit keeps the repo 154 pre-existing errors, none in the touched files. No schema change, no migration, no field added or moved on any screen, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 711. --assume: the "shipped field list" is the contract's own list read from disk, not a second copy in the test - a hardcoded copy makes a legitimate contract edit red; the row's "four fields" and OrderEntryForm.svelte target stay pre-C395 wording (six fields, SoOrderForm.svelte is the SO form).
C400 (AGENTS.md + scripts/row-watch.sh, process): the six C400 re-runs were the close gate, not the rule - a row whose proof lives in a test file closes only if that file is on its targets line. The three acceptance points were already at HEAD and green (assertAbsent at src/lib/contracts.ts:327, called by assertFormSpec/assertRenderedForm and by both SO contract tests; the C400 case in orderLogic.test.ts moves remark to dispatch and fails on the still-drawn control, naming docs/contracts/so-form.json#/absent/<i>). scripts/row-watch.sh releases a row to pending when none of its targets appears in the commit, and every C400 commit so far changed only orderLogic.test.ts, contracts.test.ts and the docs - so the row never closed however right its proof was. Both prompt surfaces now say it: AGENTS.md (row line) and the watcher's per-row brief step 5 (backticks escaped for the unquoted heredoc). A proof that lands in a file the row does not name goes on the row's targets line first (bun scripts/queue-next.ts --set <id> --target ...), and the move/remove rule names the contract test as that file. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts66 pass, 0 fail, -t C4001 pass, 0 fail, 4 expect() calls, src/lib/contracts.test.ts14/14 pass, bun scripts/form-contract-check.ts --self-test4/4 pass (the served-bytes path through assertAbsent); the per-row prompt built from the heredoc under bash with a stub brief renders the rule with literal backticks, rc 0, empty stderr (no command substitution); bash -n scripts/row-watch.sh clean. bun run check exit 0 (389 pre-existing warnings, 0 errors); bunx tsc --noEmit keeps the repo's 154 pre-existing errors, none in the touched files. No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. PLANNING section 709. --assume: none new - moved and removed stay one assertion, and the close-gate fact belongs on both prompt surfaces rather than in the board tooling. Suite / build / deploy left to the watcher.
Console windows on Windows (src/lib/server/watcher.ts, src/routes/queue/+page.server.ts, .opencode/plugins/row-line.ts, infra): they were the liveness probes. The watcher's pid is an MSYS pid, so liveness is asked of Git Bash (kill -0), and execFileSync opens a console window for the child unless windowsHide is set — so one window per plugin tick (60 s), one per /api/watcher poll (the /queue card polls it), one per Board action (queue-next.ts) and one on Stop. All five call sites now pass windowsHide: true; only the row sessions' own tool shells remain, and those run inside the OpenCode process the plugin starts hidden. Proved: bun run check exit 0, plugin reloaded, /queue 200, and every execFileSync/spawn in those files carries the flag. PLANNING §705.
No console window, properly this time (.opencode/plugins/row-line.ts, infra): windowsHide is not enough on Windows 11. The dev server and the watcher were spawned with windowsHide: true + detached: true and still opened a visible Windows Terminal window titled C:\Users\parag\.bun\bin\bun.exe — the default terminal host opens a tab for any console process it is handed and ignores the hide flag, and detached asks Windows for a console of the child's own. Both units are now launched by a generated .vbs (WScript is GUI-subsystem, sh.Run "…", 0, False = hidden, no wait), with the command embedded in the file because passing a command line as an argument loses its quoting — the first attempt produced four hung Windows Script Host windows (a quoted path needs """…""" in a VBS literal). screenshots/row-line-{dev,watch}.sh carry the bash side and its log redirection; both files are regenerated per launch and gitignored. Proved: reload → tick started both hidden, 303 on 5173, watcher pid alive, visible-window list showed no Windows Terminal and no bun console. PLANNING §706.
C396 attempt 10 (src/lib/contracts.ts + src/routes/orders/new/orderLogic.test.ts, docs + test): the failure now names the contract line that actually broke, so a contract listed out of order cannot point the message at the wrong entry.parseContract sorts fields by order, and assertFormSpec / assertRenderedForm built every pointer from that sorted index - so a re-owned qty in a document that lists remark first reported #/fields/0/owner, naming an entry that never broke. ContractField now carries docIndex (the entry own line in the document fields array, captured before the sort), FormFieldSpec passes it through, and all five pointers (owner, out-of-order, no-control in assertFormSpec; no-control, out-of-order in assertRenderedForm) read it. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts ./src/lib/contracts.test.ts70 + 14 pass, 0 fail - the shipped contract with its fields reversed still draws contract order and passes, re-owning that qty (document line 5) fails naming docs/contracts/so-form.json#/fields/5/owner and not #/fields/0/owner, and drawing no qty control fails naming #/fields/5; both negatives are the pre-fix message, so the case fails before the change, and the standing teeth stay green (rolls re-added fails at #/absent/2 naming planning; tolerancePct deleted fails naming it; out-of-contract order fails at #/fields/1; remark re-owned fails at #/fields/5/owner). bun run check exit 0, bunx tsc --noEmit keeps the repo pre-existing errors, none in the touched files. No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 714. --assume: a contract may list its entries in any document order (order alone decides the draw order), so a pointer must read the document line; the row four fields / OrderEntryForm.svelte readings stay superseded by C395 as recorded in 696/699/705.
C396 attempt 11 (src/routes/orders/new/orderLogic.test.ts, test): a control the contract cannot name fails however it is written, so a literal-name field can no longer ride the sales form unseen. The screen's spec has two readers — the {#each contract.fields} branch chain (salesFormSpec) and the controls that write their own name= attribute (salesLiteralControls) — and only the union went to assertAbsent; assertFormSpec (exactly the contract's fields, in contract order, owned by sales) saw the chain alone, so <input name={"discountPct"} /> was drawn on the form while the contract neither listed nor placed it and both checks passed. The union is now the spec fed to both assertions, so such a control fails naming docs/contracts/so-form.json#/fields and "discountPct". The literal reader also drops <input type="hidden" …> tags before matching, mirroring isControl in src/lib/contracts.ts, so the form's own envelope (type, quoteId, orderDate, paymentTermId, paymentCombo) is never read as a field while a visible name="type" still is; name={'x'}, name={"x"} and the backtick form are all the same control. Both sides are proved on a screen built in memory, so the component on disk is never edited. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts ./src/lib/contracts.test.ts70 + 14 pass, 0 fail (-t C396 alone 13 pass; green on the shipped six-field contract, rolls re-added still fails at #/absent/2 naming planning, a deleted tolerancePct still fails naming it, out-of-order still fails at #/fields/1, remark re-owned still fails at #/fields/5/owner). No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING §715. --assume: a hidden input is the envelope, not a field, so only visible controls are checked - the same rule assertRenderedForm already applies. The row's four fields / OrderEntryForm.svelte readings stay superseded by C395 as recorded in 696/699/705.
C401 re-run (tests/queueScreenHold.test.ts + docs/QUEUE-AUTO.md, process docs): the one-owner-row-per-screen hold is now pinned by a test in the repo, not by a proof in /tmp. The rule shipped in f2ed3993 (swept into a C396 watcher commit, so C401 never closed): screenHolds holds a pending row whose non-doc target overlaps an earlier open row and renderQueue writes held: same screen as C4nn - runs after it closes on its notes line, derived on every read so it lifts when the earlier row closes. What was missing was a regression test: tests/queueScreenHold.test.ts now covers the three acceptance readings plus the edges - the held row is named and the first row is free, the board writes the reason once (a stale note is replaced, never accumulated), planBatches/planGroups/firstRunnable all drop the held row, it joins the plan the moment the blocker is done, a doing row owns its screen, a reopen row is never held, a folder and a file inside it are one screen, doc targets are exempt, and blocked-owner/withdrawn rows hold nothing. Proved non-UI (C65/C66), no UI test: bun test tests/queueScreenHold.test.ts10 pass, 0 fail (23 expects). No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 716. --assume: the block is the rows open at the same time; only a pending row is held; the comparison is targetsOverlap minus doc-only targets.
/ and /signin answering 500 in 0.04 s (vite.config.ts, .opencode/plugins/row-line.ts, infra): a wedged Vite module runner, invisible to a port probe. The log had transport invoke timed out after 60000ms … vite:invoke fetchModule /node_modules/lucide-svelte/…svelte — the runner gave up fetching a module under load and every render failed instantly while the socket stayed open, so "is 5173 listening?" said healthy and the tick never restarted it. Fixes: (1) the tick now fetches /signin and restarts the listener only when it answers 5xx twice in a row (3 s apart) — one 500 is normal mid-edit; (2) the dev launcher is idempotent, so two plugin instances no longer start a second Vite on 5174 against the same tree; (3) server.watch.ignored now also covers docs/**, AGENTS.md and **/*.test.ts — rows rewrite the ledgers and their test file every minute and each write was a full program reload, which is what wedged the runner. Proved: /signin 200 (0.03 s warm), exactly one dev server, no restart logged on a healthy server; kill-both-and-wait brought both units back in 16 s / 52 s, the same path a Windows restart + OpenCode open takes. PLANNING §707.
C396 attempt 12 (src/routes/orders/new/orderLogic.test.ts, test): the literal-control reader's two blind spots now have cases, so the absent check cannot be narrowed back in silence. The sales form's spec is read twice - the {#each contract.fields} branch chain and the controls that write their own name= outside it - and the second reader had been widened to walk input, select and textarea and to read type in any attribute order, but no case held either half: a hidden envelope input with name before type, or a re-added field drawn as a <select>, could be lost to a later edit with the suite still green. Two cases now pin the rule: the tag decides what is a control (input/select/textarea count, button/div do not, type="hidden" is the envelope in either attribute order and either quote style, submit is not a field), and a re-added <select name="rolls"> fails exactly as an <input> does. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts73 pass, 0 fail, 310 expect() calls green on the shipped six-field contract, and the teeth shown on the real tree, not only in memory - one {:else if f.name === 'rolls'} branch added to SoOrderForm.svelte fails 10 C396 cases naming docs/contracts/so-form.json#/absent/2 and planning, and tolerancePct deleted from docs/contracts/so-form.json fails 11 naming "tolerancePct"; both restored (git status clean for both files). bun run check exit 0, bunx tsc --noEmit adds no error in the touched files. No schema change, no migration, no field added or moved on any screen, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 718. --assume: a select/textarea is a control and a button/div is not, the rule assertRenderedForm already applies through isControl in src/lib/contracts.ts; a hidden input is the envelope, never a field; the row's "four fields" and OrderEntryForm.svelte target stay pre-C395 wording as recorded in 696/699/705 (six fields, SoOrderForm.svelte, reached from the contract's own form key by 717).- C396 attempt 13 (src/routes/orders/new/orderLogic.test.ts, test): the screen's spec is now the control each branch draws, and the chain's own {:else} draws every contract field that has no branch - so a contract-only field add is green and a deleted control fails. The reader had taken its names from the {#if} / {:else if} headers alone: a branch that named a field but rendered no control for it counted as drawn, and the {:else} fallback - the . name={f.name} input every field without a branch falls through to - counted as nothing, so the grow case passed only by appending the name by hand. chainBranchesOf now splits the chain with a depth counter (nested {#each}/{#if} stay inside their branch), namesInHeader reads every f.name === 'x' on a header (the delivery pair shares one), drawsField accepts a branch only when it draws a control bound to f.name (or writes the name itself), and a fallback that draws name={f.name} contributes the contract fields no branch names, in contract order; salesFormSpec() defaults its declared list to formFieldSpec(soFormContract()). Three cases pin it (grow, fallback removed, silent branch). Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts75 pass, 0 fail, 319 expect() and src/lib/contracts.test.ts14 pass; the grow case fails before the change (fallback field never in the spec), and on the real tree a contract-only edit - packingNote at order 8, no branch, drawn by the fallback - stays 75 pass, then the fallback input's name={f.name} stripped in SoOrderForm.svelte fails 4 cases with docs/contracts/so-form.json#/fields/7: the sales form draws no control for "packingNote"; both mutations restored (git status clean for both files). bun run check exit 0, bunx tsc --noEmit adds no error in the touched file. No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING �719. --assume: a field with no branch is drawn by the chain's {:else} when it writes name={f.name}, so one contract edit is a complete field change on this form; a header that names a field without a control is not a drawn field. The row's "four fields" / OrderEntryForm.svelte readings stay pre-C395 wording (six fields, SoOrderForm.svelte).
Row line (scripts/row-watch.sh + src/lib/server/watcher.ts + scripts/row-watch-lanes-check.sh, process): the correction board runs ONE lane. Six sessions were live on C397/C401/C402 twice each, because five run loops were alive: the single-watcher lock named a dead owner, a later start reclaimed it legitimately, and the old loop kept dispatching without re-checking ownership. LANES=1 is now unconditional (ROW_LANES is read only to log that it was capped), owns_lock() re-reads the marker every tick and dispatch_lanes refuses to start a lane from a loop that lost the lock (a loser exits; its Windows EXIT trap releases the marker only when it still owns it), and watcher.ts reports lanes: 1 instead of echoing the env. The C339 check is rewritten for the cap: ROW_LANES=6 still starts one session and one claim, one session at a time for the whole run, the dead-lane release/re-pick still works, 6/6 rows drain; it now copies scripts/lib/portable.sh into its sandbox (the old check could not start the loop at all) and its stub deploy commits while the fake session edits the row's target, so the target-changed close gate is exercised. Proved non-UI (C65/C66), no UI test: bash -n clean, bash scripts/row-watch-lanes-check.sh12/12 PASS, exit 0 (7 sessions, max 1 at once, 6/6 done). No app route, field, validation, column, migration or edit/delete screen. PLANNING 721. --assume: the cap is a limit, not a dial — a value above 1 is clamped and reported so an inherited env can never start a second session on the same checkout; the lane machinery stays because it keeps the one lane safe across a killed session.
C397 re-run (scripts/form-contract-check.ts + src/lib/contracts.ts, owner): the table screen answers to the hidden-markup rule, the hidden-hit message no longer prints null, and the check can reach the address the row names. The script, assertions and --self-test were at HEAD and the live run was green, so the re-run hunted the gaps in the row's own clauses: assertQuoteTable never applied hiddenMarkupHits, so the quote list - the screen that once carried the display:none POST host - could render an error line or control in class="hidden" and pass; it now fails on the first hit before counting create actions. hiddenMarkupHits reports container: null for an element that hides itself, and the failure interpolated null; the message is now one exported helper, hiddenMarkupMessage(form, hit), used by both assertions (sits inside a hidden container <div class="hidden"> / is hidden itself). And the row's default base 127.0.0.1:4102 could not be reached on this host (the Windows dev server binds ::1 only), so every default run died at sign-in with ECONNREFUSED; one request() helper now retries a refused 127.0.0.1 request against localhost once and keeps that base, and a host that answers neither still fails naming the route and status. Proved non-UI (C65/C66), no UI test: bun scripts/form-contract-check.ts --self-test8/8 pass (the two new cases print the hidden table error line and is hidden itself), live bun scripts/form-contract-check.ts on the default base 2/2 PASS, exit 0 (Quote | Party | Quality | Qty | Rate | Valid till | Days open - 336/356 px; 6 fields in contract order (qty, deliveryStart, deliveryEnd, rate, tolerancePct, remark), one create action, no hidden inputs), 3.5 s including the refused first request and 2.4 s warm - one sign-in, two fetches per form, about a second per screen; bun test ./src/lib/contracts.test.ts14/14 pass. bun run check exit 0, bunx tsc --noEmit adds no error in the touched files. No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 720. --assume: the hidden-markup clause is a rule about the rendered screen, not only the create form, so the table screen carries it too; a refused 127.0.0.1 is the host binding, not a missing app, so one localhost retry is the safe reading.
C398 re-run (src/lib/components/orders/OrderEntryForm.svelte + src/lib/components/focus/focusSpec.test.ts, UI shell): the sale screen's damaged (X) branch stops drawing its own page, so both branches of the screen render the same shell as the quote screen. The one-shell-one-layer spec (focus/focusSpec.ts), ScreenShell.svelte, FocusLayer.svelte and the seven quote-new popups were already at HEAD from f2ed3993 (swept into a C396 watcher commit, which is why C398 never closed), so this run re-read the spec against the tree and closed the branch that still contradicted it: the damaged branch of OrderEntryForm — the same screen, reached from /orders/new/damaged-sale — opened with min-h-screen bg-base-200 p-4 sm:p-6, mx-auto max-w-xl and its own header row (second back link, h3, badge), i.e. a different page width, background and header from the branch beside it. It now renders ScreenShell title="Damaged (X) sale" back="/orders/new/trading" with the "no quote needed" badge in actions, the old frame and header row deleted rather than left beside the new placement, and the body dedented to the shell's level; the form, fields, validation and actions are unchanged. A guard case was added to focusSpec.test.ts — "every branch of the sale screen takes its frame from the shell": no min-h-screen, no mx-auto max-w-, no bg-base-200 p-4, exactly two <ScreenShell renders — and it fails before the fix. Proved non-UI (C65/C66), no UI test: bun src/lib/components/focus/focusSpec.test.ts5/5 PASS, exit 0, and on the real file the old frame line put back at the damaged branch fails it 1 failed, "no page height of its own" (restored, byte-identical). bun run check exit 0, bunx tsc --noEmit adds no error in the touched files. No schema change, no migration, no field added, moved or removed, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 722. --assume: "both screens render the same shell classes" covers every branch of those screens, not only the branch the row text quoted — the narrower reading leaves one screen with two page widths; the spec, the two components and the quote popups stay exactly as committed in f2ed3993.
C399 re-run (src/lib/historyLayer.ts + src/lib/historyLayer.test.ts, back gesture): the back entry belongs to its own layer, so closing one can no longer eat another's history. The row's work (helper, FocusLayer wiring, audit + baseline, tests) was already at HEAD, swept into f2ed3993 (a C396 watcher commit), which is why C399 kept coming back — its own targets never moved. Re-reading the helper against the acceptance found one real hole: release() checked only the URL before calling history.back() and swallowing the next popstate, so closing a layer that was not the top one popped the top layer's entry and left it on screen with nothing behind it — the next back left the page. Each layer now writes a unique id into the state it pushes (__layer: <id>), release() pops only when the current entry still carries that layer's id (and the URL is unchanged), and popstate unwinds by the state it landed on instead of popping one layer blindly, so a back that skips entries closes exactly the layers above the landing point. FocusLayer still carries the wiring for both of the row's component targets (SO focus wizard, quote popups via TermsPopup), which need no markup change. Proved non-UI (C65/C66), no UI test: bun test ./src/lib/historyLayer.test.ts10/10 pass against a fake history — one entry per layer (1→2→3), back closes only the layer with the form staying, a second back reaches the page below, nested layers unwind top first, button close pops its own entry without re-dismissing, the router's state is kept, a route change is left alone, and the new case (closing a layer under the top one does not pop the top entry) fails before the fix; bun scripts/layer-audit.tsALL PASS, and the audit test proves it fails a new layer that skips the helper. bun run check exit 0, bunx tsc --noEmit adds no error in the touched files. No schema change, no migration, no field added, moved or removed, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 723. --assume: the row's two component targets are already applied through the shared FocusLayer, so the sound fix for a back defect lives in the helper rather than in markup that already renders it.
C401 re-run (scripts/queue-next.ts + tests/queueScreenHold.test.ts + docs/QUEUE-AUTO.md, process docs): the one-owner-row-per-screen hold is derived from the whole board, so a deferred doing row no longer loses its screen to the next row in the same block. The rule shipped in f2ed3993 (swept into a C396 watcher commit, so C401 never closed) and its test with it; re-reading the acceptance against the planner found the one path that could still put two texts on one component: planBatches computed screenHolds from the list it was handed, and --has-work, --next and --next --json hand it a deferred-filtered list — deferredSet includes doing, so a doing row that recorded an open question disappeared from that list and its screen stopped being held while its session was still editing it, and the next row on that screen joined the plan. planBatches now takes the hold map from the board (planBatches(items, max, holds), default screenHolds(items) unchanged for other callers), and those three call sites pass screenHolds(items); the hold still lifts by itself when the blocker closes. Proved non-UI (C65/C66), no UI test: bun test tests/queueScreenHold.test.ts13 pass, 0 fail (40 expect) — the new case plans C11 from the filtered list alone and plans nothing once the board's holds are passed (it fails before the change), and the standing teeth stay green (held row named and first row free, note written once with a stale note replaced, planBatches/planGroups/firstRunnable drop the held row, the clash runs the moment the blocker is done, doing owns its screen, folder-and-file are one screen, docs exempt, blocked-owner/withdrawn hold nothing, a held row's pre-check says it waits). No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 724. --assume: the block is the rows open at the same time; only a pending row is held; the comparison is targetsOverlap minus doc-only targets; a deferred doing row still owns its screen because its session is still running.
Row line resume (scripts/row-watch.sh + scripts/row-watch-lanes-check.sh + docs, process): an interrupted row continues its own session, it is not restarted from scratch. A reboot, killed lane or timeout used to throw away 30-60 remote turns and brief a fresh session on the same row. The watcher now records the row a dead claim held (or a doing row no live lane holds) on .row-resume with the newest session id titled row <id> in this checkout, and work_row continues it with opencode run --session <id> plus a short "continue, do not start over" message; a fresh row keeps --title + the full brief. capture_resumes runs before prune_claims and before sweep_sessions, and the sweep keeps live/resume/doing sessions so the startup sweep cannot delete the one about to be resumed; once captures before its blanket --unstick. .row-resume is gitignored runtime state. Proved non-UI (C65/C66), no UI test: bash -n clean, bash scripts/row-watch-lanes-check.sh14/14 PASS, exit 0 (the killed-lane case now asserts resuming session ses_fake and --session ses_fake, alongside one session/one claim/6-of-6 done/max concurrency 1), and the live line, restarted with C402 stranded doing, logged found the interrupted session (ses_e5d8767a…) then resuming session ses_e5d8767a… (one session continued, not a new one). No app route, field, validation, column, migration or edit/delete screen. PLANNING 725. --assume: the row named by a dead claim is the interrupted row; the newest matching session in this checkout is the one to continue; "do not start over" on top of the session's own history is enough.
C402 re-run (scripts/contract-gate-check.ts + AGENTS.md + docs/QUEUE.md, process): the inventory gate was red on the real board and never checked its own rule, so it now re-proves both. The rule and the script were already at HEAD from f2ed3993/dd6beb61 (swept into a C396 watcher commit, which is why the row kept returning with its own targets untouched), but re-reading the row against the tree found C397, C398 and C399 - all rows of the SO block, the rows that built the SO entry form - naming no contract, so "every row in the block names docs/contracts/so-form.json" was false and the gate exited 3 FAIL; and nothing asserted acceptance #1 at all, so a doc edit could have deleted the rule in silence. The gate now checks AGENTS.md for it (the phrase "Inventory before queue", the docs/contracts/<form>.json artefact, "every row in the block names it"), and a failing SO-block row prints its own remedy (queue-next.ts --set <id> --assume "built from docs/contracts/so-form.json"). The three rows now carry the contract they were built from on their notes, written through the sanctioned writer, never a hand edit of docs/QUEUE.md. Proved non-UI (C65/C66), no UI test: bun scripts/contract-gate-check.ts21 PASS, 0 FAIL, "contract gate clean", exit 0 (was 3 FAIL); the pre-check refusal proved on the command line in a throwaway checkout (copy of docs/ + scripts/queue-next.ts, one synthetic block row C900 in the Pending table) - --precheck C900 --note "read the row" printed C900 refused: builds an entry form but names no contract … and exited 1, the same row with --note "read docs/contracts/so-form.json and the screen" printed pre-checked C900 - recorded, the row joins the plan and exited 0. bun run check exit 0, bunx tsc --noEmit adds no error in the touched file. No route, form, field, schema, migration or edit/delete screen; light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 726. --assume: the SO block is the gate own C394-C399 list; a closed row still carries the reference, because the board records what the block was built from, not only what is queued.
C403 (docs/contracts/so-form.json + src/lib/components/orders/SoOrderForm.svelte + src/lib/contracts.test.ts + src/routes/orders/new/orderLogic.test.ts, SO entry form): the sale order now asks rate, qty, tolerance, delivery start, delivery end. The four order numbers and the array order in the SO contract move rate to 1, qty to 2, tolerancePct to 3, deliveryStart to 4 and deliveryEnd to 5 (remark stays 6), and the form's field chain moves the same branches, so the rendered list is rate first and the C396 spec still matches the contract - no second field list, no added or removed field, same validation, prefills and draft. The contract test's expected order, the orderLogic contract fixture, the C396 silent-branch case (now tolerancePct, since rate opens the chain) and the out-of-order pointer (#/fields/0) move with it. Proved non-UI (C65/C66), no UI test: bun --smol test --timeout 30000 --isolate ./src/lib/contracts.test.ts ./src/routes/orders/new/orderLogic.test.ts75 pass, 0 fail, 319 expect() calls (contract check 14/14). No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 727. --assume: the row's five named fields leave remark in its optional last place; "tolerance" is tolerancePct.
C403 re-run (src/routes/orders/new/orderLogic.test.ts + docs, SO entry form): the sequence was already on the screen - the row now carries its own named proof. The first commit failed the watcher's target gate (not a code defect): docs/contracts/so-form.json and SoOrderForm.svelte at HEAD already ask rate, qty, tolerancePct, deliveryStart, deliveryEnd (remark 6), and the C396 case already compares the drawn chain to the contract, but no test was named after C403. orderLogic.test.ts gains a C403 case asserting salesFormSpec() is exactly rate, qty, tolerancePct, deliveryStart, deliveryEnd, remark and the contract's order numbers are 1-6, so a later reorder fails a test that names this row. Entry 727 was also written through a shell that ate its backslash escapes (11 control characters inside words: rate, tolerancePct, remark, bun); the text is repaired, content unchanged. Proved non-UI (C65/C66), no UI test: bun --smol test --timeout 30000 --isolate ./src/lib/contracts.test.ts ./src/routes/orders/new/orderLogic.test.ts76 pass, 0 fail, 321 expect() calls, contract check 14/14 ("the served form draws the contract fields, in contract order, once"). No schema change, no migration, no field added or moved, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 728. --assume: unchanged from 727 - remark keeps its optional last place and "tolerance" is tolerancePct; a re-run that finds the work at HEAD adds the missing named proof instead of re-editing the screen.
C404 (src/lib/components/orders/SoOrderForm.svelte, SO entry form): tapping the sale order's rate or quantity expands the previous entries under it, and tapping one closes the portion. The form already showed context coins for tolerance and remark, and salesQtyCoins / salesRateCoins (D25) already produced the figures for a quote, but this screen never drew them. openCoins now opens one portion at a time under the focused field - rate or qty - listing those coins with their tags (quote, this quality, this buyer, recent), the current value's chip active; a chip writes the figure into the field and closes the portion, and the portion carries a Close. Nothing is suggested when there is no history or estimate, so a fresh quote behaves as before; focusing the other field moves the portion instead of stacking a second one. No field added, moved or removed - the contract is unchanged and the portion posts nothing; a chip's figure goes through the same client (qtyStepError, rateRuleError) and server rules as typing. Proved on the row's own address (C65/C66), no UI test: a signed-in fetch of /orders/new/trading?quoteId=c68-quote-01 answers 200 with the quote strip and both so-rate / so-qty fields and no portion at rest, and with openCoins seeded to rate or qty for one render the served markup carries data-suggestions="rate" / "qty", the "Previous entries" header, its Close and the chips (₹38.50 and 12000 m tagged from quote, the current value active), then both seedings were restored; bun run screens -- --only orders8/8 screens, 13/13 live-check. bun run check leaves no new diagnostic on the touched file (389 warnings before and after; the one line naming it is the pre-existing state_referenced_locally at the values initialiser) and bunx tsc --noEmit adds no error there (154 pre-existing repo errors, none in this file). The coin data is covered by the standing D25 cases in src/routes/orders/new/orderLogic.test.ts. No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 729. --assume: "rate or quantity" is those two number fields (tolerance and remark keep their inline chips); the portion opens on a tap of the field (focus or click, so a second tap reopens it) and closes on a suggestion or Close, never on blur, because blur fires before a chip's tap on a phone; the suggestions are the D25 coins already on this screen's data.
C405 (src/lib/components/orders/SoOrderForm.svelte + src/routes/orders/new/orderLogic.ts + docs/contracts/so-form.json, SO entry form): the quote's brokerage, pass-on, delivery term and freight recovery are now pinned on the sale strip. They were carried onto the SO read-only (C374 / D17 / D18) but drawn nowhere, so a seller could not read the terms the booking carried. The contract's strip gains four entries after the quoted rate (brokerage, passOn, deliveryTerm, freightRecovery), quoteStripFacts formats them (percentages 0-100, the two term ids through deliveryTermLabel / freightRecoveryLabel, a dash when the quote states none), and the component's stripValue maps the four keys. No field added, moved or removed: the four names stay absent (the SO never asks them) with their why now naming the strip fact that shows them, and the six controls, their order, rules, prefills and draft are untouched. Proved non-UI (C65/C66), no UI test: bun --smol test --timeout 30000 --isolate ./src/lib/contracts.test.ts ./src/routes/orders/new/orderLogic.test.ts77 pass, 0 fail, 330 expect() calls - the contract case now pins the ten strip keys, and a new C405 case pins 1% / 1.5% / the two labels and the dash fallback (including 101% as junk). The same run repairs the C396 fallback reader whose lastIndexOf('{:else}') C404's nested coins {:else} had captured, so the "screen stopped drawing a field" case reads the outer fallback again (it now counts {#if} depth). No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 731. --assume: "terms like brokerage" is the four D17/D18 commercial terms the quote carries, shown on the read-only strip, never asked on the form; a quote with no brokerage shows a dash, not a default.
Row line close gate (scripts/row-watch.sh + scripts/row-watch-lanes-check.sh, process): a row that widens its own targets now closes on its first attempt. C403 ran twice: the gate compared the commit against .row-claims/<n> (the targets at dispatch time) while the session had added the files it actually edited to the board, so the commit matched the board and not the claim, and a finished row was released (closed with none of its targets in the commit, fail 1/2) and re-run. The gate now reads the row's - target: line from docs/QUEUE.md (row_targets), falling back to the claim only when the board has no line. The check pins it: C5 edits a file it did not name at dispatch time and records it on the board, and the check asserts C5 started exactly once — pre-fix it fails with C5 started 2 times, post-fix 15/15 PASS, exit 0. Live: the line was restarted on the fixed script and resumed C404 in its existing session, one lane/one claim. No app route, field, validation, column, migration or edit/delete screen. PLANNING 730. --assume: the board's target line is the row's truth at close time; the claim is a dispatch-time snapshot and stays only as a fallback.
C406 (src/lib/components/orders/SoOrderForm.svelte + src/routes/orders/new/orderLogic.ts + src/routes/orders/new/orderLogic.test.ts + src/routes/orders/new/+page.server.ts, SO entry form): the sale's delivery start and end are one two-column row, the window states its period in days, and period chips move only the end. The two promise dates were stacked full-width inputs and the operator counted the span themselves; they now sit side by side (start left, end right) under the same two contract labels. deliveryPeriodLabel puts the period on the screen as whole days ("15 days", "1 day", "Same day", "Pick both dates" while a side is blank), and salesPeriodCoins offers the spans already in use as chips, most specific first — the quote's own window (from quote), this buyer's last window, this quality's, the last three orders' — then the usual 7/15/30/40 ladder when history is thin, duplicates collapsed so a span is never offered twice. A tap rewrites exactly one value, deliveryEnd = start + the chip's days via addDaysIso (base = the form's start, else the booking day); the start never moves and the matching chip reads active. No field added, moved or removed — the contract's six fields, their order, rules, prefills and draft are untouched, both dates still go through deliveryWindowError on both surfaces, and the end input still carries min = the start. To make the spans history rather than invention the load's recent rows now also carry each past order's deliveryStart / deliveryEnd (additive optional on RecentOrderRow); nothing new posts. Proved non-UI (C65/C66), no UI test: bun --smol test --timeout 30000 --isolate ./src/routes/orders/new/orderLogic.test.ts -t "C406"2 pass, 0 fail, 15 expect() calls — one case pins the period label (including blank sides) and addDaysIso moving the end by the chip's days, the other pins the chip order and the no-repeat rule plus the bare ladder with no history. bun run check exits 0 with the same 389 warnings C405 recorded and no new diagnostic; bun x tsc --noEmit reports 153 repo-wide errors (one fewer than C405's 154 — the pre-existing v === '' comparison in orderLogic's date reader was dead for its number | Date parameter and is gone) and none in the files this row touched. No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 732. --assume: the suggested periods are the spans already in use (quote, this buyer, this quality, last three orders) then the usual ladder, and a tap rewrites only delivery end (start plus N days), never the start.
C407 (src/lib/components/orders/SoOrderForm.svelte + src/routes/orders/new/orderLogic.ts + src/routes/orders/new/orderLogic.test.ts, SO entry form): a tap on any sale field opens its own previous entries, and a tap on another field closes the one that was open. C404 opened the portion on rate and quantity only, so moving to tolerance or a promise date left the old portion open and offered nothing new; openCoins now holds the open field's name (not a two-name union) and every control sets it on focus and click, the two promise dates through their side loop included, so the second tap replaces the first portion in the same render. Every field now has entries: a new salesDateCoins in orderLogic.ts answers the promise dates from the same ladder the period chips use (quote, this buyer, this quality, last three orders, duplicates collapsed, stored timestamps read through quoteDay), dropping a start before the booking day and an end before the form's start so a suggested day never fails the two-surface rule; tolerance offers this buyer's past tolerances, remark its earlier remarks (replacing the inline past-remark chips), rate and qty keep their D25 coins, and the fixed 3/5/7/10 tolerance presets stay visible as presets. One snippet draws all portions, a chip writes the value into its own field and closes it, and the Close stays. No field added, moved or removed - contract, order, rules, prefills, draft unchanged; nothing new posts. Proved non-UI (C65/C66), no UI test: bun --smol test --timeout 30000 --isolate ./src/routes/orders/new/orderLogic.test.ts -t "C407"1 pass, 0 fail, 4 expect() calls (date ladder, no-repeat, timestamp, empty history). bun run check exits 0 with the same 389 warnings and no new diagnostic on the touched files; bun x tsc --noEmit adds no error there (repo-wide errors are the pre-existing +page.server.ts route-type ones). No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 733. --assume: "all the fields" is the six contract fields, so the promise dates get their own history; tolerance presets stay visible and only history moves into the portion; a tap on another field closes the open portion, never blur (blur fires before a chip's tap on a phone).
2026-11-04 C408: a saved order now confirms with the app-wide popup toast (notify + the root-layout <Toasts/>, C25) instead of the inline ✓ Added banner that pushed the table down; the text is unchanged (✓ Added — order SO-261104-U7IO), the new row still flashes row-highlight for 4.2 s, orders list only, no field/contract/schema/server change, light mode unchanged. Proved non-UI (C65/C66), no UI test: the dev server's own transform of src/routes/orders/+page.svelte → 200 with the toast import and notify( present and the old alert-success mb-3 banner gone, GET /orders 200; bun run check exit 0 / 389 warnings (no new), bun x tsc --noEmit no error in the file. PLANNING 734.
C409 (src/lib/components/SuggestionChips.svelte + src/lib/components/quote/TermEditor.svelte (both new) + src/lib/components/orders/SoOrderForm.svelte + src/lib/components/quote/NewSummary.svelte + src/lib/components/quote/NewCalc.svelte + src/routes/quote/new/+page.svelte + src/routes/quote/new/quoteNewState.svelte.ts + src/lib/components/quote/TermsPopup.svelte (deleted) + src/lib/components/focus/focusSpec.test.ts, owner): the quote terms now open their suggestions inline under the row - the sale form's tap flow - instead of in a popup. C404/C407 made every sale field open its previous entries as chips in the page; the quote screen still opened the TermsPopup modal for all nine of its terms, which is the popup whose body is the suggestion list. SuggestionChips.svelte is now the one component (the sale form's data-suggestions portion: "Previous entries", the active chip, the optional tag, Close) - SoOrderForm draws it instead of its own copy, and the new TermEditor.svelte draws the suggestions, date coins, delivery coins, estimated quantities, selling margins and both choice lists through it. NewSummary and NewCalc render the editor directly under the tapped term row, TermsPopup.svelte and the page-level {#if s.termPop} render are deleted, and quoteNewState's chromeHidden no longer counts a term, so the page header/summary stay reachable while a term is open. No field, value, saveTerm write, hidden POST or server rule changed; the editor keeps the percentage range, date input, wheel, stepper, error line and Done/Cancel. Proved non-UI (C65/C66), no UI test: bun src/lib/components/focus/focusSpec.test.ts6/6 passed (new case: the quote screen renders no TermsPopup, both quote lists draw <TermEditor> only on s.termPop === row[0], the editor is no layer/dialog/frame, it draws SuggestionChips, and the sale form keeps no copy of the chip markup) and bun scripts/svelte-syntax.ts6 .svelte files compiled clean. bun run check exit 0 / 389 warnings (no new), bun x tsc --noEmit 153 pre-existing errors, none in the changed files. No schema change, no migration, no edit/delete, light mode unchanged. Suite / build / deploy left to the watcher. PLANNING 736. --assume: "throughout the website" starts with the quote term popup (the popup whose purpose is the suggestion list); the signed FOCUS-ENTRY wizards/field popups are the entry pattern itself and keep their popups; two new files, the old popup deleted with its only caller, reversible.
owner (2026-11-04, src/lib/nav.ts + src/routes/orders/{+page.svelte,+page.server.ts,ordersLoad.ts (new),[id]/+page.server.ts,new/+page.server.ts} + src/lib/components/orders/OrdersList.svelte (new) + src/routes/sale/** (new) + src/routes/prod/** (new) + src/routes/+layout.server.ts + src/routes/signin/+page.server.ts + src/routes/signin/+page.svelte + scripts/nav-audit.ts + scripts/lighthouse-audit.ts): Sale and Stock own /sale and /prod, and a production order's record is the order itself. The orders list became one component + one loader (ordersLoad(event, lockedType)), so /orders keeps All / Job-work / Outsource while /sale (SO) and /prod (PROD) are thin shells over the same rows; the nav's Sale/Stock rows point at them and /orders?type=SO|PROD 308-redirect with q/status/page carried, so old links (share messages, docs, bookmarks) keep working. /prod/<id> draws exactly the eleven values the list popup reads — Order, Kind, Party, Quality, Qty, Rate, Value, Cover, Progress, Status, Booked — and no stage rail, next steps, contract, price & terms, P&L, coverage, operations wizard, attachments or history (a stock order has none of those surfaces); /orders/<id> for a PROD order 307s there, and the production list drops the Cover and Operations columns, its popup offering only what that kind has (C161: yarn booking, sizing tippan). A saved order now lands on its own kind's list (/sale?created=, /prod?created=, /orders?type=JW|OUTSOURCE&created=), and the app gate carries /signin?redirectTo=<path> so signing in returns to the screen that was asked for (same-origin only, never /signin itself), with a hard-navigation fallback for a tab whose client route manifest predates a route — the /sale-shows-"Open the app" symptom. No table, column or migration changed (ARCHITECTURE §9 intact: a PROD order is still the cost bucket). The owner then had the 216 bare seeded stock orders deleted: snapshot first (snapshot/2026-11-04/), then the 216 rows with their own history (683 evt, 234 amendment), keeping the two live orders and their 2 apportionments; the delete refused had any other table pointed at those ids. 2 production orders remain (1,688 orders in all). Proved non-UI (C65/C66), no UI test: a signed-in probe 19/19 (redirects carry the query, three lists 200, all eleven labels and none of the removed cards on the production record, /orders/<prod> 307s to /prod/<id>); bun run screens -- --only orders|sale|prod opens them on Turso data; bun scripts/nav-audit.ts ALL PASS; bun scripts/lighthouse-audit.ts PASS; bun run check 390 warnings (none new), bun x tsc --noEmit no error in the touched files; the sign-in path re-walked on a clean dev server to the sale list. PLANNING 746. --assume: the production record is the owner's list and nothing else; the sale record keeps its tabs (owner's answer); one list, three addresses, permanent redirects; restart the dev server once when routes are added, since its client manifest can predate them.
owner (2026-11-04, src/routes/queue/** (deleted) + src/routes/api/watcher/** (deleted) + src/lib/server/watcher.ts + src/lib/server/queueBoard.ts + src/lib/server/board.ts + src/lib/nav.ts + src/routes/docs/+page.svelte + src/routes/+layout.svelte + scripts/row-watch*.sh/queue-*.sh/queue-next.ts/row-run.sh/status.sh/line-status.sh/contract-gate-check.ts/.plan-summary.py/lib/portable.sh (deleted) + scripts/deploy.sh + tests/queueScreenHold.test.ts (deleted) + .opencode/plugins/dev-server.ts (was row-line.ts) + docs/QUEUE-AUTO.md/HELD.md/NEEDS-OWNER.md (deleted) + AGENTS.md + STATUS.md + OWNERSHIP.md + COMPONENTS.md + FOCUS-ENTRY.md): the corrections board, the row line and /queue are removed completely. Owner: "purge watcher and http://127.0.0.1:5173/queue completely". The watcher was retrying a catch-up deploy it could never perform every 30 s (~1,100 attempts, a 3.3 MB log, docs/QUEUE.md rewritten each tick), kept Vite alive at 1.8 GB, and could start an autonomous session that committed to the same branch the owner was editing. /queue is gone with its loader (queueBoard.ts), its watcher API (/api/watcher + start/stop + watcher.ts), its nav/title entries, the docs-page link and the dashboard's "Corrections need you" todo; the row scripts, agent row.md, queue-watch service and queue-next.ts are deleted; deploy.sh lost its watcher restart, row promotion and board stamping; the OpenCode plugin is now dev-server.ts and only keeps the dev server up. docs/QUEUE.md and docs/QUESTIONS.md stay as history (nothing reads them); queue.ts (/pending) and the database are untouched. Proved non-UI (C65/C66), no UI test: bun run check exit 0, bun x tsc --noEmit adds no error in the touched files, /queue 404 and / + /signin 200 on the dev server, and bun scripts/live-check.ts opens every remaining screen. No route lost a field, contract, schema or migration; light mode unchanged. PLANNING 747. --assume: "purge completely" is the automation and its screen, not the history; the dev server stays; /pending is a different feature.
owner (2026-11-04, src/routes/orders/new/orderLogic.ts + src/routes/orders/new/orderLogic.test.ts + src/routes/orders/new/+page.server.ts + src/routes/orders/new/trading/+page.svelte + src/routes/quote/+page.server.ts + src/routes/quote/edit/+page.server.ts + src/lib/components/quote/EditSavedQuotes.svelte + src/lib/components/quote/QuotePreview.svelte, SO entry form): a quote that already booked a sale order opens that order, never a form whose Save must fail. The reported URL (/orders/new/trading?quoteId=5aincgag3zxm&type=SO) was reached from the quote list's "Create Trading SO", which was drawn for a quote already booked that morning as SO-261104-4IFQ; the form rendered, took the values and the save answered 400 with the one-SO-per-quote refusal, so the screen offered a Save that was known to fail before the user typed. The load now decides one of three doors through orderLogic.quoteGate - used (names the sale order and links /orders/<id>), closed (the quote's status cannot become an order; offers a re-quote), form - and the trading page draws that instead of the form; QUOTE_LINKABLE_STATUSES moved into orderLogic.ts and gained accepted, because the quote screen's own Accept moves a quote there and then says "Next: create the order" (the one-SO-per-quote check, not the status, stops a duplicate). Both quote lists now carry soOrderId/soOrderNo per row and draw "Open SO-..." instead of the create button, and the links point at the real route rather than the legacy redirect. No schema change, no migration, no field added, moved or removed, no edit/delete, light mode unchanged. Proved non-UI (C65/C66), no UI test: bun test ./src/routes/orders/new/orderLogic.test.ts76 pass (three new cases: used/closed/form and accepted still linkable; the 12 C396 form-reader failures are pre-existing on this host, identical with the change stashed), and on the served bytes a signed-in GET of the reported URL answers 200 with data-quote-used naming SO-261104-4IFQ and no action="?/create", an unused quote still renders the prefilled form, an expired quote renders data-quote-closed, the pending table still hides the used quote, and /quote + /quote/edit serve soOrderNo and "Open SO-261104-4IFQ". bun run check exit 0, bun x tsc --noEmit adds no error in the touched files (136 repo-wide, all pre-existing). PLANNING 748. --assume: a cancelled sale order releases the quote (the same query the save reads); a non-linkable quote with no order says it is closed rather than rendering a form; accepted is linkable since the quote screen's Accept sends the user here.