الأبحاث · جولةُ البرمجة الأولى

CB-2-07 — إعادة هيكلة · JavaScript/TypeScript · عالٍ

جولةُ البرمجة الأولى · 2026-10-12 · GAIBRA v2.30.04 · واجبُ التشغيل

نتيجةُ الجولة

الترتيبالمتنافسالرمز أمام المحكّمالكلماتالاختباراتOpusGrokالمجموعالصدق (Opus · Grok)
1GAIBRAب2,26515/1593.096.094.511 · 12
2Claude Sonnetهـ1,63415/1594.093.093.510 · 11
3DeepSeekأ1,75414/1574.779.777.26 · 8
4ChatGPTج1,01714/1566.775.771.27 · 9
5Geminiد6832/1526.746.736.74 · 7

اختلف المحكّمان على المركز الأوّل: Opus اختار Claude Sonnet، وGrok اختار GAIBRA.

نصُّ السيناريو كما كتبه المولّد

Our restaurant POS order module grew by accretion and every new rule ("a served order can no longer be cancelled", "only new orders can add items", "payment requires a total > 0") was bolted on as another nested if. We keep shipping regressions when we touch it. Current code (runs on Node 20, single file): JavaScript "use strict"; class OrderManager { &#x20; constructor() { &#x20; this.orders = new Map(); &#x20; this.nextId = 1; &#x20; } &#x20; createOrder(tableNo) { &#x20; const id = this.nextId++; &#x20; this.orders.set(id, { id, tableNo, status: "new", items: \[], paid: false, fired: false }); &#x20; return id; &#x20; } &#x20; addItem(id, name, price) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; if (o.status !== "new") throw new Error("cannot add items to order in status " + o.status); &#x20; if (typeof price !== "number" || price <= 0) throw new Error("price must be positive"); &#x20; o.items.push({ name, price }); &#x20; return o.items.length; &#x20; } &#x20; fire(id) { // send to kitchen &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; if (o.status === "cancelled") throw new Error("order is cancelled"); &#x20; if (o.status !== "new") throw new Error("order already sent to kitchen"); &#x20; if (o.items.length === 0) throw new Error("cannot fire an empty order"); &#x20; o.status = "preparing"; o.fired = true; &#x20; } &#x20; markReady(id) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; if (o.status === "cancelled") throw new Error("order is cancelled"); &#x20; if (o.status !== "preparing") throw new Error("order is not being prepared"); &#x20; o.status = "ready"; &#x20; } &#x20; serve(id) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; if (o.status === "cancelled") throw new Error("order is cancelled"); &#x20; if (o.status !== "ready") throw new Error("order is not ready"); &#x20; o.status = "served"; &#x20; } &#x20; pay(id, amount) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; if (o.status !== "served") throw new Error("only served orders can be paid"); &#x20; const total = o.items.reduce((s, i) => s + i.price, 0); &#x20; if (amount < total) throw new Error("insufficient amount"); &#x20; o.status = "paid"; o.paid = true; &#x20; return amount - total; &#x20; } &#x20; cancel(id) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; if (o.status === "paid") throw new Error("paid orders cannot be cancelled"); &#x20; if (o.status === "served") throw new Error("served orders cannot be cancelled"); &#x20; if (o.status === "cancelled") throw new Error("order already cancelled"); &#x20; o.status = "cancelled"; &#x20; } &#x20; getStatus(id) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; return { ...o }; &#x20; } } module.exports = { OrderManager }; What I want: restructure this into an explicit finite-state machine — a single transition table (state × event → guard → new state) instead of scattered conditionals, while keeping the public API (class name, method names and signatures, and every exact error message string) unchanged, so the rest of the POS and our existing integration tests don't notice. The redundant paid/fired flags should disappear in favor of deriving whatever is needed from status, without changing what getStatus returns (keep the flags in its output for compatibility, computed from status). Note createOrder and addItem's price validation and the insufficient-amount change calculation must behave identically. Constraints: single file, Node 20, standard library only, under 300 lines. Add a short \_\_main\_\_-style demo or comment showing the transition table you ended up with.

الردودُ الخمسة كما وصلت المحكّمين

الرمزُ هو ما رآه المحكّمان؛ والاسمُ كُشف بعد التحكيم.

بGAIBRA94.5 · 2,265 كلمة
"use strict"; /\* &#x20;\* Transition table: state × event → guard → next state &#x20;\* &#x20;\* state | addItem | fire | markReady | serve | pay | cancel &#x20;\* NEW | \[pricePositive] NEW | \[hasItems] PREPARING | ✗ | ✗ | ✗ | CANCELLED\_UNFIRED &#x20;\* PREPARING | ✗ | ✗ | READY | ✗ | ✗ | CANCELLED\_FIRED &#x20;\* READY | ✗ | ✗ | ✗ | SERVED | ✗ | CANCELLED\_FIRED &#x20;\* SERVED | ✗ | ✗ | ✗ | ✗ | \[amountCoversTotal] PAID | ✗ &#x20;\* PAID | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ &#x20;\* CANCELLED\_UNFIRED | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ &#x20;\* CANCELLED\_FIRED | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ &#x20;\* &#x20;\* ✗ = rejected with the exact legacy message for that (state, event) cell. &#x20;\* Both CANCELLED\_\* states are exposed as status "cancelled"; they differ only in the derived `fired` flag. &#x20;\* Run `node <this file>` to print the live table and execute the self-checks. &#x20;\*/ /\*\* Internal FSM states. The public status string is a projection of these (see STATE\_VIEW). \*/ const OrderState = Object.freeze({ &#x20; NEW: "NEW", &#x20; PREPARING: "PREPARING", &#x20; READY: "READY", &#x20; SERVED: "SERVED", &#x20; PAID: "PAID", &#x20; CANCELLED\_UNFIRED: "CANCELLED\_UNFIRED", &#x20; CANCELLED\_FIRED: "CANCELLED\_FIRED", }); /\*\* Events, named after the public methods that raise them. \*/ const OrderEvent = Object.freeze({ &#x20; ADD\_ITEM: "addItem", &#x20; FIRE: "fire", &#x20; MARK\_READY: "markReady", &#x20; SERVE: "serve", &#x20; PAY: "pay", &#x20; CANCEL: "cancel", }); /\*\* Public projection of each state: legacy status string plus compatibility flags derived from the state. \*/ const STATE\_VIEW = Object.freeze({ &#x20; \[OrderState.NEW]: Object.freeze({ status: "new", paid: false, fired: false }), &#x20; \[OrderState.PREPARING]: Object.freeze({ status: "preparing", paid: false, fired: true }), &#x20; \[OrderState.READY]: Object.freeze({ status: "ready", paid: false, fired: true }), &#x20; \[OrderState.SERVED]: Object.freeze({ status: "served", paid: false, fired: true }), &#x20; \[OrderState.PAID]: Object.freeze({ status: "paid", paid: true, fired: true }), &#x20; \[OrderState.CANCELLED\_UNFIRED]: Object.freeze({ status: "cancelled", paid: false, fired: false }), &#x20; \[OrderState.CANCELLED\_FIRED]: Object.freeze({ status: "cancelled", paid: false, fired: true }), }); /\*\* Every error message string, byte-for-byte identical to the legacy implementation. \*/ const Message = Object.freeze({ &#x20; UNKNOWN\_ORDER: "unknown order", &#x20; CANNOT\_ADD\_ITEMS\_PREFIX: "cannot add items to order in status ", &#x20; PRICE\_NOT\_POSITIVE: "price must be positive", &#x20; ORDER\_CANCELLED: "order is cancelled", &#x20; ALREADY\_SENT: "order already sent to kitchen", &#x20; EMPTY\_ORDER: "cannot fire an empty order", &#x20; NOT\_PREPARING: "order is not being prepared", &#x20; NOT\_READY: "order is not ready", &#x20; ONLY\_SERVED\_PAYABLE: "only served orders can be paid", &#x20; INSUFFICIENT\_AMOUNT: "insufficient amount", &#x20; PAID\_NOT\_CANCELLABLE: "paid orders cannot be cancelled", &#x20; SERVED\_NOT\_CANCELLABLE: "served orders cannot be cancelled", &#x20; ALREADY\_CANCELLED: "order already cancelled", }); /\*\* Sum of item prices, computed exactly as the legacy code did (same reduce, same order). \*/ function orderTotal(order) { &#x20; return order.items.reduce((sum, item) => sum + item.price, 0); } /\*\* Guards return null when the transition may proceed, or the exact error message when it may not. \*/ const Guard = Object.freeze({ &#x20; pricePositive: (order, { price }) => &#x20; typeof price !== "number" || price <= 0 ? Message.PRICE\_NOT\_POSITIVE : null, &#x20; hasItems: (order) => (order.items.length === 0 ? Message.EMPTY\_ORDER : null), &#x20; amountCoversTotal: (order, { amount }) => &#x20; amount < orderTotal(order) ? Message.INSUFFICIENT\_AMOUNT : null, }); /\*\* Effects run after all guards pass; their return value becomes the public method's return value. \*/ const Effect = Object.freeze({ &#x20; appendItem: (order, { name, price }) => { &#x20; order.items.push({ name, price }); &#x20; return order.items.length; &#x20; }, &#x20; returnChange: (order, { amount }) => amount - orderTotal(order), }); function allow(next, guards = \[], effect = null) { &#x20; return Object.freeze({ next, guards: Object.freeze(guards), effect }); } function reject(message) { &#x20; return Object.freeze({ rejection: message }); } function rejectAddItem(state) { &#x20; return reject(Message.CANNOT\_ADD\_ITEMS\_PREFIX + STATE\_VIEW\[state].status); } function cancelledRow(state) { &#x20; return Object.freeze({ &#x20; \[OrderEvent.ADD\_ITEM]: rejectAddItem(state), &#x20; \[OrderEvent.FIRE]: reject(Message.ORDER\_CANCELLED), &#x20; \[OrderEvent.MARK\_READY]: reject(Message.ORDER\_CANCELLED), &#x20; \[OrderEvent.SERVE]: reject(Message.ORDER\_CANCELLED), &#x20; \[OrderEvent.PAY]: reject(Message.ONLY\_SERVED\_PAYABLE), &#x20; \[OrderEvent.CANCEL]: reject(Message.ALREADY\_CANCELLED), &#x20; }); } /\*\* The single source of truth for order lifecycle rules. \*/ const TRANSITIONS = Object.freeze({ &#x20; \[OrderState.NEW]: Object.freeze({ &#x20; \[OrderEvent.ADD\_ITEM]: allow(OrderState.NEW, \[Guard.pricePositive], Effect.appendItem), &#x20; \[OrderEvent.FIRE]: allow(OrderState.PREPARING, \[Guard.hasItems]), &#x20; \[OrderEvent.MARK\_READY]: reject(Message.NOT\_PREPARING), &#x20; \[OrderEvent.SERVE]: reject(Message.NOT\_READY), &#x20; \[OrderEvent.PAY]: reject(Message.ONLY\_SERVED\_PAYABLE), &#x20; \[OrderEvent.CANCEL]: allow(OrderState.CANCELLED\_UNFIRED), &#x20; }), &#x20; \[OrderState.PREPARING]: Object.freeze({ &#x20; \[OrderEvent.ADD\_ITEM]: rejectAddItem(OrderState.PREPARING), &#x20; \[OrderEvent.FIRE]: reject(Message.ALREADY\_SENT), &#x20; \[OrderEvent.MARK\_READY]: allow(OrderState.READY), &#x20; \[OrderEvent.SERVE]: reject(Message.NOT\_READY), &#x20; \[OrderEvent.PAY]: reject(Message.ONLY\_SERVED\_PAYABLE), &#x20; \[OrderEvent.CANCEL]: allow(OrderState.CANCELLED\_FIRED), &#x20; }), &#x20; \[OrderState.READY]: Object.freeze({ &#x20; \[OrderEvent.ADD\_ITEM]: rejectAddItem(OrderState.READY), &#x20; \[OrderEvent.FIRE]: reject(Message.ALREADY\_SENT), &#x20; \[OrderEvent.MARK\_READY]: reject(Message.NOT\_PREPARING), &#x20; \[OrderEvent.SERVE]: allow(OrderState.SERVED), &#x20; \[OrderEvent.PAY]: reject(Message.ONLY\_SERVED\_PAYABLE), &#x20; \[OrderEvent.CANCEL]: allow(OrderState.CANCELLED\_FIRED), &#x20; }), &#x20; \[OrderState.SERVED]: Object.freeze({ &#x20; \[OrderEvent.ADD\_ITEM]: rejectAddItem(OrderState.SERVED), &#x20; \[OrderEvent.FIRE]: reject(Message.ALREADY\_SENT), &#x20; \[OrderEvent.MARK\_READY]: reject(Message.NOT\_PREPARING), &#x20; \[OrderEvent.SERVE]: reject(Message.NOT\_READY), &#x20; \[OrderEvent.PAY]: allow(OrderState.PAID, \[Guard.amountCoversTotal], Effect.returnChange), &#x20; \[OrderEvent.CANCEL]: reject(Message.SERVED\_NOT\_CANCELLABLE), &#x20; }), &#x20; \[OrderState.PAID]: Object.freeze({ &#x20; \[OrderEvent.ADD\_ITEM]: rejectAddItem(OrderState.PAID), &#x20; \[OrderEvent.FIRE]: reject(Message.ALREADY\_SENT), &#x20; \[OrderEvent.MARK\_READY]: reject(Message.NOT\_PREPARING), &#x20; \[OrderEvent.SERVE]: reject(Message.NOT\_READY), &#x20; \[OrderEvent.PAY]: reject(Message.ONLY\_SERVED\_PAYABLE), &#x20; \[OrderEvent.CANCEL]: reject(Message.PAID\_NOT\_CANCELLABLE), &#x20; }), &#x20; \[OrderState.CANCELLED\_UNFIRED]: cancelledRow(OrderState.CANCELLED\_UNFIRED), &#x20; \[OrderState.CANCELLED\_FIRED]: cancelledRow(OrderState.CANCELLED\_FIRED), }); /\*\* Fails at load time if any state × event cell is missing, so an incomplete edit never reaches runtime. \*/ function assertTableComplete() { &#x20; for (const state of Object.values(OrderState)) { &#x20; if (!STATE\_VIEW\[state] || !TRANSITIONS\[state]) throw new Error(`FSM: state ${state} lacks a view or row`); &#x20; for (const event of Object.values(OrderEvent)) { &#x20; if (!TRANSITIONS\[state]\[event]) throw new Error(`FSM: missing cell ${state} × ${event}`); &#x20; } &#x20; } } assertTableComplete(); function requireOrder(orders, id) { &#x20; const order = orders.get(id); &#x20; if (!order) throw new Error(Message.UNKNOWN\_ORDER); &#x20; return order; } /\*\* Order of checks: unknown order → cell rejection → guards in order → effect → state change. \*/ function dispatch(orders, id, event, payload) { &#x20; const order = requireOrder(orders, id); &#x20; const cell = TRANSITIONS\[order.state]\[event]; &#x20; if (cell.rejection !== undefined) throw new Error(cell.rejection); &#x20; for (const guard of cell.guards) { &#x20; const failure = guard(order, payload); &#x20; if (failure !== null) throw new Error(failure); &#x20; } &#x20; const result = cell.effect === null ? undefined : cell.effect(order, payload); &#x20; order.state = cell.next; &#x20; return result; } /\*\* Restaurant order lifecycle manager driven by an explicit transition table. \*/ class OrderManager { &#x20; constructor() { &#x20; this.orders = new Map(); &#x20; this.nextId = 1; &#x20; } &#x20; /\*\* Creates an order in status "new" and returns its numeric id. \*/ &#x20; createOrder(tableNo) { &#x20; const id = this.nextId++; &#x20; this.orders.set(id, { id, tableNo, state: OrderState.NEW, items: \[] }); &#x20; return id; &#x20; } &#x20; /\*\* Adds an item to a "new" order; returns the item count. Throws on unknown order, wrong status or non-positive price. \*/ &#x20; addItem(id, name, price) { &#x20; return dispatch(this.orders, id, OrderEvent.ADD\_ITEM, { name, price }); &#x20; } &#x20; /\*\* Sends a non-empty "new" order to the kitchen. \*/ &#x20; fire(id) { &#x20; dispatch(this.orders, id, OrderEvent.FIRE, {}); &#x20; } &#x20; /\*\* Marks a "preparing" order as ready. \*/ &#x20; markReady(id) { &#x20; dispatch(this.orders, id, OrderEvent.MARK\_READY, {}); &#x20; } &#x20; /\*\* Marks a "ready" order as served. \*/ &#x20; serve(id) { &#x20; dispatch(this.orders, id, OrderEvent.SERVE, {}); &#x20; } &#x20; /\*\* Pays a "served" order; returns the change (amount - total). \*/ &#x20; pay(id, amount) { &#x20; return dispatch(this.orders, id, OrderEvent.PAY, { amount }); &#x20; } &#x20; /\*\* Cancels an order that is not yet served, paid or cancelled. \*/ &#x20; cancel(id) { &#x20; dispatch(this.orders, id, OrderEvent.CANCEL, {}); &#x20; } &#x20; /\*\* Returns { id, tableNo, status, items, paid, fired } with flags derived from the FSM state. \*/ &#x20; getStatus(id) { &#x20; const order = requireOrder(this.orders, id); &#x20; const view = STATE\_VIEW\[order.state]; &#x20; return { id: order.id, tableNo: order.tableNo, status: view.status, items: order.items, paid: view.paid, fired: view.fired }; &#x20; } } function describeCell(cell) { &#x20; if (cell.rejection !== undefined) return `✗ ${cell.rejection}`; &#x20; const guards = cell.guards.map((guard) => guard.name).join(", "); &#x20; return guards ? `\[${guards}] → ${cell.next}` : `→ ${cell.next}`; } function runDemo() { &#x20; const assert = require("node:assert/strict"); &#x20; const table = {}; &#x20; for (const state of Object.values(OrderState)) { &#x20; table\[state] = {}; &#x20; for (const event of Object.values(OrderEvent)) table\[state]\[event] = describeCell(TRANSITIONS\[state]\[event]); &#x20; } &#x20; console.table(table); &#x20; const expectError = (fn, message) => assert.throws(fn, { name: "Error", message }); &#x20; const pos = new OrderManager(); &#x20; const dinner = pos.createOrder(4); &#x20; assert.equal(pos.addItem(dinner, "Burger", 12.5), 1); &#x20; assert.equal(pos.addItem(dinner, "Fries", 4.5), 2); &#x20; pos.fire(dinner); &#x20; assert.deepEqual(\[pos.getStatus(dinner).status, pos.getStatus(dinner).fired], \["preparing", true]); &#x20; expectError(() => pos.addItem(dinner, "Cola", 3), "cannot add items to order in status preparing"); &#x20; expectError(() => pos.fire(dinner), "order already sent to kitchen"); &#x20; pos.markReady(dinner); &#x20; pos.serve(dinner); &#x20; expectError(() => pos.pay(dinner, 10), "insufficient amount"); &#x20; assert.equal(pos.pay(dinner, 20), 3); &#x20; const paidView = pos.getStatus(dinner); &#x20; assert.deepEqual(Object.keys(paidView), \["id", "tableNo", "status", "items", "paid", "fired"]); &#x20; assert.deepEqual(paidView, { &#x20; id: 1, tableNo: 4, status: "paid", &#x20; items: \[{ name: "Burger", price: 12.5 }, { name: "Fries", price: 4.5 }], paid: true, fired: true, &#x20; }); &#x20; expectError(() => pos.cancel(dinner), "paid orders cannot be cancelled"); &#x20; const walkout = pos.createOrder(7); &#x20; pos.cancel(walkout); &#x20; assert.deepEqual(\[pos.getStatus(walkout).status, pos.getStatus(walkout).fired], \["cancelled", false]); &#x20; expectError(() => pos.fire(walkout), "order is cancelled"); &#x20; expectError(() => pos.addItem(walkout, "Tea", 2), "cannot add items to order in status cancelled"); &#x20; expectError(() => pos.pay(walkout, 5), "only served orders can be paid"); &#x20; expectError(() => pos.cancel(walkout), "order already cancelled"); &#x20; const sentBack = pos.createOrder(9); &#x20; assert.equal(pos.addItem(sentBack, "Soup", 6), 1); &#x20; pos.fire(sentBack); &#x20; pos.cancel(sentBack); &#x20; assert.deepEqual(\[pos.getStatus(sentBack).status, pos.getStatus(sentBack).fired], \["cancelled", true]); &#x20; const empty = pos.createOrder(2); &#x20; expectError(() => pos.fire(empty), "cannot fire an empty order"); &#x20; expectError(() => pos.addItem(empty, "Water", 0), "price must be positive"); &#x20; expectError(() => pos.markReady(empty), "order is not being prepared"); &#x20; expectError(() => pos.serve(empty), "order is not ready"); &#x20; expectError(() => pos.getStatus(99), "unknown order"); &#x20; console.log("All demo checks passed."); } if (require.main === module) runDemo(); module.exports = { OrderManager }; Explanation The flags and the status do not carry the same information, and that is the real finding of this refactor. In your original code, cancel never resets fired, so an order cancelled while "new" reports fired: false, while one cancelled from "preparing" or "ready" reports fired: true. Both report status: "cancelled". The fired flag therefore cannot be computed from the public status string alone. Your requirement to derive the flags from status and your requirement to keep getStatus unchanged only seemed to conflict. The fix is to make the machine's state richer than the string it exposes. The table has seven internal states, and CANCELLED\_UNFIRED and CANCELLED\_FIRED both project to "cancelled" through STATE\_VIEW. Every flag is now a pure function of one field, and nothing in the record can drift out of step with it. The dispatcher preserves your error precedence exactly. It looks up the order first ("unknown order"). Next it checks the cell's rejection, then runs the guards in order, then the effect, and only then changes the state. This reproduces the subtle orderings in the old code. A cancelled order asked to fire still says "order is cancelled" instead of "order already sent to kitchen". pay on a cancelled order still says "only served orders can be paid", because the old pay never checked for cancellation. addItem on a non-new order still reports the status error before it looks at the price. No guard mutates anything, so a rejected call leaves the order untouched, as it did before. assertTableComplete runs when the module is loaded. Each regression you described came from a rule added in one method and forgotten in another. With this check, adding a state or an event without deciding every cell fails at require time, before any order is affected. I used module-level functions instead of #private methods on purpose. A private method would add a brand check that throws a different TypeError when someone calls a method with .call on another object. Here is every behaviour difference from the original. First, records inside the public manager.orders map now have the shape { id, tableNo, state, items } instead of status/paid/fired. Code that reads those records directly, or writes status onto them, will see the change. getStatus also no longer echoes extra properties someone attached to a record. Second, running the file directly with node now prints the table and runs the assertions. Third, loading the module now performs the completeness check. Error classes (plain Error), message strings, return values, getStatus key order, and the floating-point order of the total are all unchanged. Your rule that "payment requires a total > 0" has no explicit guard. That matches the original, where it holds only indirectly: firing needs at least one item, and each item needs a price that passes price <= 0. That indirect protection has holes the refactor does not close. Important Three defects are kept deliberately because you required that "the insufficient-amount change calculation must behave identically", that "addItem's price validation" be preserved, and that getStatus not change what it returns. First, pay(id) with a missing or non-numeric amount marks the order paid and returns NaN, because undefined < total is false. Second, addItem accepts NaN and Infinity, and one NaN price lets any later payment succeed. Third, getStatus returns the live items array, so a caller pushing into it bypasses the "only new orders can add items" rule. I did not run this code. The demo's expected values come from tracing your original code by hand on the same inputs. Usage Save the file under its current name and run node order-manager.js once. It prints the live state × event matrix and either passes every assertion or stops at the first mismatch. Then run your existing integration suite against it unchanged. After the suite passes, close the three defects as a separate, versioned change. Each one is a single line in the table: an amountIsFinite guard ahead of amountCoversTotal, a Number.isFinite(price) condition in pricePositive, and items: order.items.map(item => ({ ...item })) in getStatus. Because each fix lives in the table, the rest of the code stays as it is.
هـClaude Sonnet93.5 · 1,634 كلمة
I’ll restructure it into a table-driven state machine, then fuzz it against your original to confirm identical behavior. Now a differential fuzz test against the original. order-manager.js is the state-machine version. It is 224 lines, uses only the standard library, and keeps the same class, method signatures and error strings. How it’s structured TRANSITIONS is the single table of state × event → guard → new state (+ effect). There are 8 rows. REJECTIONS gives the exact error message for each state/event pair that has no transition. GUARDS holds the checks that run after a transition is found: price, non-empty, and sufficient amount. \_dispatch is now the only place where status changes. At load time the module checks that every state × event pair has either a transition or a rejection message. If you add a state or event and forget a rule, it fails on startup instead of silently letting the order through. Running node order-manager.js prints both tables and a sample order lifecycle. The transition table is also in the header comment. Verified: I ran a differential fuzz test against your original code: 500,000 random operations, including odd prices and amounts like NaN, "5", undefined and Infinity, plus unknown order IDs. Return values, error messages, which check fires first, and getStatus output (including key order) all matched. The sandbox has Node 22, but nothing in the file needs anything newer than Node 20. Things to know fired can’t be worked out from status alone. A cancelled order may or may not have reached the kitchen, and the old flag kept that distinction. So cancel now records which state it came from in cancelledFrom. That’s a fact about the transition, not a duplicate flag, and getStatus().fired comes out the same as before. The records inside this.orders no longer have paid/fired. If any POS code reads manager.orders directly instead of calling getStatus, it will see that change. Existing quirks are kept on purpose: pay(id, undefined) or pay(id, NaN) still succeeds and returns NaN change. NaN and Infinity prices still pass validation. getStatus().items is still the same array the order uses, not a copy. These look like bugs. If you want them fixed, it’s a one-line guard change each, but it would be a deliberate change in behavior. “Payment requires total > 0” has no explicit check, same as your current code. It already holds because fire rejects empty orders and prices must be positive. Order manager JS  "use strict"; &#x20; /\* &#x20;\* Restaurant order lifecycle as an explicit finite-state machine. &#x20;\* &#x20;\* Every rule lives in one of three tables: &#x20;\* TRANSITIONS state × event → guard → new state (+ effect producing the return value) &#x20;\* REJECTIONS the exact error for an event fired in a state with no transition &#x20;\* GUARDS conditions checked only after a transition is found &#x20;\* &#x20;\* from event guard to effect &#x20;\* ---------- ---------- ------------ ---------- ------------- &#x20;\* new addItem validPrice new appendItem &#x20;\* new fire hasItems preparing &#x20;\* preparing markReady ready &#x20;\* ready serve served &#x20;\* served pay coversTotal paid makeChange &#x20;\* new cancel cancelled rememberOrigin &#x20;\* preparing cancel cancelled rememberOrigin &#x20;\* ready cancel cancelled rememberOrigin &#x20;\* &#x20;\* Any (state, event) pair not listed is rejected with its REJECTIONS message. &#x20;\* The module checks at load time that every pair is covered by exactly one of &#x20;\* the two tables, so a new state or event cannot silently fall through. &#x20;\* &#x20;\* Run `node order-manager.js` to print the table and a sample run. &#x20;\*/ &#x20; const State = Object.freeze({ &#x20; NEW: "new", &#x20; PREPARING: "preparing", &#x20; READY: "ready", &#x20; SERVED: "served", &#x20; PAID: "paid", &#x20; CANCELLED: "cancelled", }); &#x20; const EVENTS = Object.freeze(\["addItem", "fire", "markReady", "serve", "pay", "cancel"]); &#x20; // States in which the order has been sent to the kitchen. const FIRED\_STATES = new Set(\[State.PREPARING, State.READY, State.SERVED, State.PAID]); &#x20; const orderTotal = (o) => o.items.reduce((s, i) => s + i.price, 0); &#x20; // Guards return an error message to reject the event, or null to allow it. // They must not mutate the order. const GUARDS = Object.freeze({ &#x20; validPrice: (o, { price }) => &#x20; typeof price !== "number" || price <= 0 ? "price must be positive" : null, &#x20; hasItems: (o) => (o.items.length === 0 ? "cannot fire an empty order" : null), &#x20; coversTotal: (o, { amount }) => (amount < orderTotal(o) ? "insufficient amount" : null), }); &#x20; // Effects run after the guard passes; their return value is the method's return value. const EFFECTS = Object.freeze({ &#x20; appendItem: (o, { name, price }) => { &#x20; o.items.push({ name, price }); &#x20; return o.items.length; &#x20; }, &#x20; makeChange: (o, { amount }) => amount - orderTotal(o), &#x20; // "cancelled" alone cannot tell whether the kitchen had the order, so remember where we came from. &#x20; rememberOrigin: (o, \_payload, from) => { &#x20; o.cancelledFrom = from; &#x20; }, }); &#x20; const TRANSITIONS = Object.freeze(\[ &#x20; // from event guard to effect &#x20; \[State.NEW, "addItem", "validPrice", State.NEW, "appendItem"], &#x20; \[State.NEW, "fire", "hasItems", State.PREPARING, null], &#x20; \[State.PREPARING, "markReady", null, State.READY, null], &#x20; \[State.READY, "serve", null, State.SERVED, null], &#x20; \[State.SERVED, "pay", "coversTotal", State.PAID, "makeChange"], &#x20; \[State.NEW, "cancel", null, State.CANCELLED, "rememberOrigin"], &#x20; \[State.PREPARING, "cancel", null, State.CANCELLED, "rememberOrigin"], &#x20; \[State.READY, "cancel", null, State.CANCELLED, "rememberOrigin"], ].map((\[from, event, guard, to, effect]) => Object.freeze({ from, event, guard, to, effect }))); &#x20; // Error for an event in a state with no transition. "\*" covers every other state; // a function receives the current state. const REJECTIONS = Object.freeze({ &#x20; addItem: { "\*": (s) => "cannot add items to order in status " + s }, &#x20; fire: { \[State.CANCELLED]: "order is cancelled", "\*": "order already sent to kitchen" }, &#x20; markReady: { \[State.CANCELLED]: "order is cancelled", "\*": "order is not being prepared" }, &#x20; serve: { \[State.CANCELLED]: "order is cancelled", "\*": "order is not ready" }, &#x20; pay: { "\*": "only served orders can be paid" }, &#x20; cancel: { &#x20; \[State.PAID]: "paid orders cannot be cancelled", &#x20; \[State.SERVED]: "served orders cannot be cancelled", &#x20; \[State.CANCELLED]: "order already cancelled", &#x20; }, }); &#x20; const keyOf = (state, event) => state + "|" + event; &#x20; function rejectionFor(state, event) { &#x20; const rules = REJECTIONS\[event] || {}; &#x20; const msg = Object.prototype.hasOwnProperty.call(rules, state) ? rules\[state] : rules\["\*"]; &#x20; return typeof msg === "function" ? msg(state) : msg; } &#x20; // Build the lookup index and verify the tables are complete and consistent. const INDEX = (() => { &#x20; const index = new Map(); &#x20; const states = new Set(Object.values(State)); &#x20; for (const t of TRANSITIONS) { &#x20; if (!states.has(t.from) || !states.has(t.to)) throw new Error("FSM: unknown state in " + keyOf(t.from, t.event)); &#x20; if (!EVENTS.includes(t.event)) throw new Error("FSM: unknown event " + t.event); &#x20; if (t.guard \&\& !GUARDS\[t.guard]) throw new Error("FSM: unknown guard " + t.guard); &#x20; if (t.effect \&\& !EFFECTS\[t.effect]) throw new Error("FSM: unknown effect " + t.effect); &#x20; const k = keyOf(t.from, t.event); &#x20; if (index.has(k)) throw new Error("FSM: duplicate transition " + k); &#x20; index.set(k, t); &#x20; } &#x20; for (const s of states) { &#x20; for (const e of EVENTS) { &#x20; if (!index.has(keyOf(s, e)) \&\& typeof rejectionFor(s, e) !== "string") { &#x20; throw new Error("FSM: no transition or rejection for " + keyOf(s, e)); &#x20; } &#x20; } &#x20; } &#x20; return index; })(); &#x20; class OrderManager { &#x20; constructor() { &#x20; this.orders = new Map(); &#x20; this.nextId = 1; &#x20; } &#x20; &#x20; createOrder(tableNo) { &#x20; const id = this.nextId++; &#x20; this.orders.set(id, { id, tableNo, status: State.NEW, items: \[], cancelledFrom: null }); &#x20; return id; &#x20; } &#x20; &#x20; addItem(id, name, price) { &#x20; return this.\_dispatch(id, "addItem", { name, price }); &#x20; } &#x20; &#x20; fire(id) { // send to kitchen &#x20; this.\_dispatch(id, "fire"); &#x20; } &#x20; &#x20; markReady(id) { &#x20; this.\_dispatch(id, "markReady"); &#x20; } &#x20; &#x20; serve(id) { &#x20; this.\_dispatch(id, "serve"); &#x20; } &#x20; &#x20; pay(id, amount) { &#x20; return this.\_dispatch(id, "pay", { amount }); &#x20; } &#x20; &#x20; cancel(id) { &#x20; this.\_dispatch(id, "cancel"); &#x20; } &#x20; &#x20; getStatus(id) { &#x20; const o = this.\_get(id); &#x20; // Same keys, order and shallow-copy semantics as before; flags are derived. &#x20; return { &#x20; id: o.id, &#x20; tableNo: o.tableNo, &#x20; status: o.status, &#x20; items: o.items, &#x20; paid: o.status === State.PAID, &#x20; fired: FIRED\_STATES.has(o.status) || &#x20; (o.status === State.CANCELLED \&\& FIRED\_STATES.has(o.cancelledFrom)), &#x20; }; &#x20; } &#x20; &#x20; \_get(id) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; return o; &#x20; } &#x20; &#x20; // The only place an order's status changes. &#x20; \_dispatch(id, event, payload = {}) { &#x20; const o = this.\_get(id); &#x20; const from = o.status; &#x20; const t = INDEX.get(keyOf(from, event)); &#x20; if (!t) throw new Error(rejectionFor(from, event)); &#x20; if (t.guard) { &#x20; const err = GUARDS\[t.guard](o, payload); &#x20; if (err) throw new Error(err); &#x20; } &#x20; const result = t.effect ? EFFECTS\[t.effect](o, payload, from) : undefined; &#x20; o.status = t.to; &#x20; return result; &#x20; } } &#x20; module.exports = { OrderManager, State, TRANSITIONS, REJECTIONS }; &#x20; if (require.main === module) { &#x20; console.log("Transitions (state × event → guard → new state):"); &#x20; console.table(TRANSITIONS.map((t) => ({ &#x20; from: t.from, event: t.event, guard: t.guard || "", to: t.to, effect: t.effect || "", &#x20; }))); &#x20; &#x20; console.log("Rejections (no transition for state × event):"); &#x20; const rows = \[]; &#x20; for (const s of Object.values(State)) { &#x20; const row = { state: s }; &#x20; for (const e of EVENTS) row\[e] = INDEX.has(keyOf(s, e)) ? "→ " + INDEX.get(keyOf(s, e)).to : rejectionFor(s, e); &#x20; rows.push(row); &#x20; } &#x20; console.table(rows); &#x20; &#x20; const m = new OrderManager(); &#x20; const id = m.createOrder(7); &#x20; m.addItem(id, "Shawarma", 18); &#x20; m.addItem(id, "Mint lemonade", 12); &#x20; m.fire(id); &#x20; m.markReady(id); &#x20; m.serve(id); &#x20; console.log("change from 50:", m.pay(id, 50)); &#x20; console.log(m.getStatus(id)); &#x20; try { m.cancel(id); } catch (e) { console.log("cancel after pay →", e.message); } } &#x20;
جChatGPT71.2 · 1,017 كلمة
The key is to separate transition rules from action-specific validation. A single transition table determines which states accept each event, evaluates the guard, and supplies the next state. The public methods remain unchanged, and getStatus() derives paid and fired from status. One compatibility detail matters: the original fire() checks for "cancelled" before checking whether the order is "new". The table-driven implementation below preserves that error precedence, along with all existing error strings. Refactored implementation — order-manager.js "use strict"; class OrderManager { &#x20; constructor() { &#x20; this.orders = new Map(); &#x20; this.nextId = 1; &#x20; } &#x20; /\* &#x20; \* Transition table: current state -> event -> guard / next state. &#x20; \* &#x20; \* State Event Guard Next state &#x20; \* ---------------------------------------------------------------- &#x20; \* new addItem valid price new (no transition) &#x20; \* new fire at least one item preparing &#x20; \* new cancel always cancelled &#x20; \* preparing markReady always ready &#x20; \* preparing cancel always cancelled &#x20; \* ready serve always served &#x20; \* ready cancel always cancelled &#x20; \* served pay amount >= total paid &#x20; \* paid cancel forbidden - &#x20; \* cancelled fire forbidden - &#x20; \* cancelled markReady forbidden - &#x20; \* cancelled serve forbidden - &#x20; \* cancelled cancel forbidden - &#x20; \* &#x20; \* Events without a transition are rejected by their method-specific &#x20; \* error handler, preserving the original public error messages. &#x20; \*/ &#x20; static TRANSITIONS = Object.freeze({ &#x20; new: Object.freeze({ &#x20; fire: { &#x20; guard: (o) => o.items.length > 0, &#x20; next: "preparing", &#x20; error: "cannot fire an empty order" &#x20; }, &#x20; cancel: { next: "cancelled" } &#x20; }), &#x20; preparing: Object.freeze({ &#x20; markReady: { next: "ready" }, &#x20; cancel: { next: "cancelled" } &#x20; }), &#x20; ready: Object.freeze({ &#x20; serve: { next: "served" }, &#x20; cancel: { next: "cancelled" } &#x20; }), &#x20; served: Object.freeze({ &#x20; pay: { &#x20; guard: (o, amount) => amount >= OrderManager.total(o), &#x20; next: "paid", &#x20; error: "insufficient amount" &#x20; } &#x20; }), &#x20; paid: Object.freeze({}), &#x20; cancelled: Object.freeze({}) &#x20; }); &#x20; static total(o) { &#x20; return o.items.reduce((sum, item) => sum + item.price, 0); &#x20; } &#x20; \_getOrder(id) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; return o; &#x20; } &#x20; \_transition(o, event, ...args) { &#x20; const rule = OrderManager.TRANSITIONS\[o.status]?.\[event]; &#x20; if (!rule) return false; &#x20; if (rule.guard \&\& !rule.guard(o, ...args)) { &#x20; throw new Error(rule.error); &#x20; } &#x20; o.status = rule.next; &#x20; return true; &#x20; } &#x20; createOrder(tableNo) { &#x20; const id = this.nextId++; &#x20; this.orders.set(id, { &#x20; id, tableNo, status: "new", items: \[] &#x20; }); &#x20; return id; &#x20; } &#x20; addItem(id, name, price) { &#x20; const o = this.\_getOrder(id); &#x20; if (o.status !== "new") { &#x20; throw new Error( &#x20; "cannot add items to order in status " + o.status &#x20; ); &#x20; } &#x20; // Keep the original validation semantics, including NaN behavior. &#x20; if (typeof price !== "number" || price <= 0) { &#x20; throw new Error("price must be positive"); &#x20; } &#x20; o.items.push({ name, price }); &#x20; return o.items.length; &#x20; } &#x20; fire(id) { &#x20; const o = this.\_getOrder(id); &#x20; // Preserve original error precedence. &#x20; if (o.status === "cancelled") { &#x20; throw new Error("order is cancelled"); &#x20; } &#x20; if (o.status !== "new") { &#x20; throw new Error("order already sent to kitchen"); &#x20; } &#x20; this.\_transition(o, "fire"); &#x20; // "fired" is derived from status; no stored flag is needed. &#x20; } &#x20; markReady(id) { &#x20; const o = this.\_getOrder(id); &#x20; if (o.status === "cancelled") { &#x20; throw new Error("order is cancelled"); &#x20; } &#x20; if (o.status !== "preparing") { &#x20; throw new Error("order is not being prepared"); &#x20; } &#x20; this.\_transition(o, "markReady"); &#x20; } &#x20; serve(id) { &#x20; const o = this.\_getOrder(id); &#x20; if (o.status === "cancelled") { &#x20; throw new Error("order is cancelled"); &#x20; } &#x20; if (o.status !== "ready") { &#x20; throw new Error("order is not ready"); &#x20; } &#x20; this.\_transition(o, "serve"); &#x20; } &#x20; pay(id, amount) { &#x20; const o = this.\_getOrder(id); &#x20; if (o.status !== "served") { &#x20; throw new Error("only served orders can be paid"); &#x20; } &#x20; const total = OrderManager.total(o); &#x20; if (amount < total) { &#x20; throw new Error("insufficient amount"); &#x20; } &#x20; this.\_transition(o, "pay", amount); &#x20; return amount - total; &#x20; } &#x20; cancel(id) { &#x20; const o = this.\_getOrder(id); &#x20; if (o.status === "paid") { &#x20; throw new Error("paid orders cannot be cancelled"); &#x20; } &#x20; if (o.status === "served") { &#x20; throw new Error("served orders cannot be cancelled"); &#x20; } &#x20; if (o.status === "cancelled") { &#x20; throw new Error("order already cancelled"); &#x20; } &#x20; this.\_transition(o, "cancel"); &#x20; } &#x20; getStatus(id) { &#x20; const o = this.\_getOrder(id); &#x20; return { &#x20; ...o, &#x20; paid: o.status === "paid", &#x20; fired: \[ &#x20; "preparing", "ready", "served", "paid" &#x20; ].includes(o.status) &#x20; }; &#x20; } } module.exports = { OrderManager }; Compatibility notes Public API: The class name, constructor, method names, parameters, and return values remain unchanged. Exact errors: All original error strings are preserved, including the order of checks where that affects which error is thrown. Derived flags: paid is true only in the paid state. fired is true in preparing, ready, served, and paid. Price validation: The original check is retained exactly. In particular, NaN is still accepted by addItem() because NaN <= 0 is false. Payment calculation: The total is still calculated using the same JavaScript number addition, and change is still amount - total. No new rounding is introduced. No dependencies: Uses only standard JavaScript features available in Node 20. Important design distinction The transition table is the source of truth for allowed state changes, but the public methods still contain a few checks to preserve legacy error messages and precedence. This is intentional: a generic transition rejection alone cannot always reproduce the original method-specific errors. One limitation of the exact compatibility requirement is that the original getStatus() returns a shallow copy of the order. The refactored version keeps that behavior, including the fact that the items array remains shared with the internal order. Changing it to a deep copy would be a separate behavioral change.
أDeepSeek77.2 · 1,754 كلمة
Here's a restructured version using an explicit finite-state machine. The public API and every error message are preserved exactly. javascript "use strict"; // ───────────────────────────────────────────────────────────────────────────── // Finite State Machine definition for Order lifecycle. // // States: new → preparing → ready → served → paid // ↘ cancelled (from new/preparing/ready) // // TRANSITION TABLE // ┌────────────┬───────────────┬──────────────────────────────────────────────┬───────────────┐ // │ From state │ Event │ Guard (returns error string, or null to pass)│ To state │ // ├────────────┼───────────────┼──────────────────────────────────────────────┼───────────────┤ // │ new │ addItem │ price must be a positive number │ new │ // │ new │ fire │ items.length > 0 │ preparing │ // │ preparing │ markReady │ — │ ready │ // │ ready │ serve │ — │ served │ // │ served │ pay │ amount >= total │ paid │ // │ new │ cancel │ — │ cancelled │ // │ preparing │ cancel │ — │ cancelled │ // │ ready │ cancel │ — │ cancelled │ // └────────────┴───────────────┴──────────────────────────────────────────────┴───────────────┘ // // Transitions that must fail (and their exact error messages) are encoded as // "reject" rows in the table, so no conditional lives outside the FSM. // ───────────────────────────────────────────────────────────────────────────── const S = Object.freeze({ &#x20; NEW: "new", &#x20; PREPARING: "preparing", &#x20; READY: "ready", &#x20; SERVED: "served", &#x20; PAID: "paid", &#x20; CANCELLED: "cancelled", }); // Derive compatibility flags from status (so they disappear from stored state). function flagsFor(status) { &#x20; return { &#x20; paid: status === S.PAID, &#x20; fired: status === S.PREPARING || status === S.READY || status === S.SERVED || status === S.PAID, &#x20; }; } // A transition entry: { from, event, guard?, to } or { from, event, reject: msg }. // `guard` throws the exact legacy error string when it fails. const TRANSITIONS = \[ &#x20; // addItem: only allowed from "new"; price must be positive. &#x20; { &#x20; from: S.NEW, &#x20; event: "addItem", &#x20; guard: (o, args) => &#x20; typeof args.price !== "number" || args.price <= 0 &#x20; ? "price must be positive" &#x20; : null, &#x20; to: S.NEW, &#x20; }, &#x20; { &#x20; from: S.PREPARING, &#x20; event: "addItem", &#x20; reject: (o) => "cannot add items to order in status " + o.status, &#x20; }, &#x20; { &#x20; from: S.READY, &#x20; event: "addItem", &#x20; reject: (o) => "cannot add items to order in status " + o.status, &#x20; }, &#x20; { &#x20; from: S.SERVED, &#x20; event: "addItem", &#x20; reject: (o) => "cannot add items to order in status " + o.status, &#x20; }, &#x20; { &#x20; from: S.PAID, &#x20; event: "addItem", &#x20; reject: (o) => "cannot add items to order in status " + o.status, &#x20; }, &#x20; { &#x20; from: S.CANCELLED, &#x20; event: "addItem", &#x20; reject: (o) => "cannot add items to order in status " + o.status, &#x20; }, &#x20; // fire: only from "new"; must have items. &#x20; { &#x20; from: S.NEW, &#x20; event: "fire", &#x20; guard: (o) => &#x20; o.items.length === 0 ? "cannot fire an empty order" : null, &#x20; to: S.PREPARING, &#x20; }, &#x20; { &#x20; from: S.CANCELLED, &#x20; event: "fire", &#x20; reject: () => "order is cancelled", &#x20; }, &#x20; { &#x20; from: S.PREPARING, &#x20; event: "fire", &#x20; reject: () => "order already sent to kitchen", &#x20; }, &#x20; { &#x20; from: S.READY, &#x20; event: "fire", &#x20; reject: () => "order already sent to kitchen", &#x20; }, &#x20; { &#x20; from: S.SERVED, &#x20; event: "fire", &#x20; reject: () => "order already sent to kitchen", &#x20; }, &#x20; { &#x20; from: S.PAID, &#x20; event: "fire", &#x20; reject: () => "order already sent to kitchen", &#x20; }, &#x20; // markReady: only from "preparing". &#x20; { &#x20; from: S.PREPARING, &#x20; event: "markReady", &#x20; to: S.READY, &#x20; }, &#x20; { &#x20; from: S.CANCELLED, &#x20; event: "markReady", &#x20; reject: () => "order is cancelled", &#x20; }, &#x20; { &#x20; from: S.NEW, &#x20; event: "markReady", &#x20; reject: () => "order is not being prepared", &#x20; }, &#x20; { &#x20; from: S.READY, &#x20; event: "markReady", &#x20; reject: () => "order is not being prepared", &#x20; }, &#x20; { &#x20; from: S.SERVED, &#x20; event: "markReady", &#x20; reject: () => "order is not being prepared", &#x20; }, &#x20; { &#x20; from: S.PAID, &#x20; event: "markReady", &#x20; reject: () => "order is not being prepared", &#x20; }, &#x20; // serve: only from "ready". &#x20; { &#x20; from: S.READY, &#x20; event: "serve", &#x20; to: S.SERVED, &#x20; }, &#x20; { &#x20; from: S.CANCELLED, &#x20; event: "serve", &#x20; reject: () => "order is cancelled", &#x20; }, &#x20; { &#x20; from: S.NEW, &#x20; event: "serve", &#x20; reject: () => "order is not ready", &#x20; }, &#x20; { &#x20; from: S.PREPARING, &#x20; event: "serve", &#x20; reject: () => "order is not ready", &#x20; }, &#x20; { &#x20; from: S.SERVED, &#x20; event: "serve", &#x20; reject: () => "order is not ready", &#x20; }, &#x20; { &#x20; from: S.PAID, &#x20; event: "serve", &#x20; reject: () => "order is not ready", &#x20; }, &#x20; // pay: only from "served"; amount must cover total. &#x20; { &#x20; from: S.SERVED, &#x20; event: "pay", &#x20; guard: (o, args) => { &#x20; const total = o.items.reduce((s, i) => s + i.price, 0); &#x20; return args.amount < total ? "insufficient amount" : null; &#x20; }, &#x20; to: S.PAID, &#x20; }, &#x20; { &#x20; from: S.NEW, &#x20; event: "pay", &#x20; reject: () => "only served orders can be paid", &#x20; }, &#x20; { &#x20; from: S.PREPARING, &#x20; event: "pay", &#x20; reject: () => "only served orders can be paid", &#x20; }, &#x20; { &#x20; from: S.READY, &#x20; event: "pay", &#x20; reject: () => "only served orders can be paid", &#x20; }, &#x20; { &#x20; from: S.PAID, &#x20; event: "pay", &#x20; reject: () => "only served orders can be paid", &#x20; }, &#x20; { &#x20; from: S.CANCELLED, &#x20; event: "pay", &#x20; reject: () => "only served orders can be paid", &#x20; }, &#x20; // cancel: from new/preparing/ready; not from served/paid/cancelled. &#x20; { from: S.NEW, event: "cancel", to: S.CANCELLED }, &#x20; { from: S.PREPARING, event: "cancel", to: S.CANCELLED }, &#x20; { from: S.READY, event: "cancel", to: S.CANCELLED }, &#x20; { &#x20; from: S.PAID, &#x20; event: "cancel", &#x20; reject: () => "paid orders cannot be cancelled", &#x20; }, &#x20; { &#x20; from: S.SERVED, &#x20; event: "cancel", &#x20; reject: () => "served orders cannot be cancelled", &#x20; }, &#x20; { &#x20; from: S.CANCELLED, &#x20; event: "cancel", &#x20; reject: () => "order already cancelled", &#x20; }, ]; // Index the table for O(1) lookup. const TABLE = new Map(); for (const t of TRANSITIONS) { &#x20; const key = t.from + "\\u0000" + t.event; &#x20; if (TABLE.has(key)) throw new Error("duplicate transition: " + key); &#x20; TABLE.set(key, t); } class OrderManager { &#x20; constructor() { &#x20; this.orders = new Map(); &#x20; this.nextId = 1; &#x20; } &#x20; // Internal: fetch order or throw the legacy "unknown order" message. &#x20; \_get(id) { &#x20; const o = this.orders.get(id); &#x20; if (!o) throw new Error("unknown order"); &#x20; return o; &#x20; } &#x20; // Internal: run the FSM. `args` carries event payload (price/amount, etc.). &#x20; // Returns the transition entry so callers can compute side-effect results. &#x20; \_transition(o, event, args = {}) { &#x20; const t = TABLE.get(o.status + "\\u0000" + event); &#x20; if (!t) throw new Error("unknown order"); // unreachable with full table &#x20; if (t.reject) throw new Error(t.reject(o)); &#x20; if (t.guard) { &#x20; const err = t.guard(o, args); &#x20; if (err) throw new Error(err); &#x20; } &#x20; o.status = t.to; &#x20; return t; &#x20; } &#x20; createOrder(tableNo) { &#x20; const id = this.nextId++; &#x20; this.orders.set(id, { id, tableNo, status: S.NEW, items: \[] }); &#x20; return id; &#x20; } &#x20; addItem(id, name, price) { &#x20; const o = this.\_get(id); &#x20; this.\_transition(o, "addItem", { name, price }); &#x20; o.items.push({ name, price }); &#x20; return o.items.length; &#x20; } &#x20; fire(id) { &#x20; const o = this.\_get(id); &#x20; this.\_transition(o, "fire"); &#x20; } &#x20; markReady(id) { &#x20; const o = this.\_get(id); &#x20; this.\_transition(o, "markReady"); &#x20; } &#x20; serve(id) { &#x20; const o = this.\_get(id); &#x20; this.\_transition(o, "serve"); &#x20; } &#x20; pay(id, amount) { &#x20; const o = this.\_get(id); &#x20; this.\_transition(o, "pay", { amount }); &#x20; const total = o.items.reduce((s, i) => s + i.price, 0); &#x20; return amount - total; &#x20; } &#x20; cancel(id) { &#x20; const o = this.\_get(id); &#x20; this.\_transition(o, "cancel"); &#x20; } &#x20; getStatus(id) { &#x20; const o = this.\_get(id); &#x20; // Keep paid/fired in the output for compatibility, computed from status. &#x20; return { ...o, ...flagsFor(o.status) }; &#x20; } } module.exports = { OrderManager }; // ───────────────────────────────────────────────────────────────────────────── // \_\_main\_\_-style demo — exercises the FSM and prints the transition table. // ───────────────────────────────────────────────────────────────────────────── if (require.main === module) { &#x20; const om = new OrderManager(); &#x20; const id = om.createOrder(5); &#x20; console.log("created:", om.getStatus(id)); &#x20; om.addItem(id, "Soup", 6.5); &#x20; om.addItem(id, "Steak", 22); &#x20; console.log("after addItem:", om.getStatus(id)); &#x20; om.fire(id); &#x20; console.log("after fire:", om.getStatus(id)); &#x20; om.markReady(id); &#x20; om.serve(id); &#x20; console.log("after serve:", om.getStatus(id)); &#x20; const change = om.pay(id, 30); &#x20; console.log("change:", change, "status:", om.getStatus(id)); &#x20; // Show the table we ended up with. &#x20; console.log("\\nTransition table:"); &#x20; for (const t of TRANSITIONS) { &#x20; if (t.reject) { &#x20; console.log(` ${t.from.padEnd(10)} --${t.event.padEnd(10)}--> REJECT`); &#x20; } else { &#x20; console.log(` ${t.from.padEnd(10)} --${t.event.padEnd(10)}--> ${t.to}`); &#x20; } &#x20; } } What changed Single transition table. All rules live in one TRANSITIONS array (state × event → guard/reject → new state). Method bodies are now three lines: fetch order, look up transition, apply side-effect. paid / fired removed from stored state. They're derived from status inside getStatus via flagsFor, so the returned shape is unchanged. Exact error strings preserved. Rejects and guards return the same message strings the original code threw — including the interpolated "cannot add items to order in status " + o.status. Behavioral parity: addItem still validates typeof price === "number" \&\& price > 0, still returns items.length. pay still returns amount - total and only succeeds on served. createOrder produces the same object shape minus the redundant flags. Under 300 lines, single file, Node 20 stdlib only. The if (require.main === module) block prints the table so you can eyeball it. Notes on the design Rows are grouped by event for readability; the table is indexed into a Map at load time so lookups are O(1) and duplicate (state, event) pairs fail fast at startup. "Reject" rows make the failure modes explicit data rather than if-chains, which is what you asked for — but they do inflate the table. If you later add states, the TABLE map + the if (!t) throw "unknown order" fallback keeps things safe. The FSM is data, so you can serialize it, test it exhaustively, or render it as a diagram without touching OrderManager.
دGemini36.7 · 683 كلمة
Here is the refactored code using a clean, explicit Finite-State Machine pattern driven by a transition table.Transition Matrix DesignCurrent StateEventTarget StateGuardActionsnewaddItemnewprice > 0Append itemnewfirepreparingitems.length > 0Set status to preparingpreparingmarkReadyready—Set status to readyreadyserveserved—Set status to servedservedpaypaidamount >= totalCompute change, set status to paidnew / preparing / readycancelcancelled—Set status to cancelledImplementationJavaScript"use strict"; /\*\* &#x20;\* State Transition Table &#x20;\* Format: { \[currentState]: { \[event]: { target, guard?, action? } } } &#x20;\*/ const TRANSITIONS = { &#x20; new: { &#x20; addItem: { &#x20; target: "new", &#x20; guard: ({ price }) => { &#x20; if (typeof price !== "number" || price <= 0) { &#x20; throw new Error("price must be positive"); &#x20; } &#x20; }, &#x20; action: (order, { name, price }) => { &#x20; order.items.push({ name, price }); &#x20; return order.items.length; &#x20; }, &#x20; }, &#x20; fire: { &#x20; target: "preparing", &#x20; guard: (order) => { &#x20; if (order.items.length === 0) throw new Error("cannot fire an empty order"); &#x20; }, &#x20; }, &#x20; cancel: { target: "cancelled" }, &#x20; }, &#x20; preparing: { &#x20; markReady: { target: "ready" }, &#x20; cancel: { target: "cancelled" }, &#x20; }, &#x20; ready: { &#x20; serve: { target: "served" }, &#x20; cancel: { target: "cancelled" }, &#x20; }, &#x20; served: { &#x20; pay: { &#x20; target: "paid", &#x20; action: (order, { amount }) => { &#x20; const total = order.items.reduce((sum, item) => sum + item.price, 0); &#x20; if (amount < total) throw new Error("insufficient amount"); &#x20; return amount - total; &#x20; }, &#x20; }, &#x20; }, &#x20; paid: {}, &#x20; cancelled: {}, }; /\*\* &#x20;\* Specific error mapping when events are triggered on invalid states &#x20;\*/ const INVALID\_STATE\_ERRORS = { &#x20; addItem: (status) => `cannot add items to order in status ${status}`, &#x20; fire: (status) => &#x20; status === "cancelled" ? "order is cancelled" : "order already sent to kitchen", &#x20; markReady: (status) => &#x20; status === "cancelled" ? "order is cancelled" : "order is not being prepared", &#x20; serve: (status) => &#x20; status === "cancelled" ? "order is cancelled" : "order is not ready", &#x20; pay: (status) => "only served orders can be paid", &#x20; cancel: (status) => &#x20; status === "paid" &#x20; ? "paid orders cannot be cancelled" &#x20; : status === "served" &#x20; ? "served orders cannot be cancelled" &#x20; : "order already cancelled", }; class OrderManager { &#x20; constructor() { &#x20; this.orders = new Map(); &#x20; this.nextId = 1; &#x20; } &#x20; createOrder(tableNo) { &#x20; const id = this.nextId++; &#x20; // Internal state omits paid/fired flags &#x20; this.orders.set(id, { id, tableNo, status: "new", items: \[] }); &#x20; return id; &#x20; } &#x20; \_dispatch(id, event, payload = {}) { &#x20; const order = this.orders.get(id); &#x20; if (!order) throw new Error("unknown order"); &#x20; const stateTransitions = TRANSITIONS\[order.status]; &#x20; const transition = stateTransitions ? stateTransitions\[event] : null; &#x20; if (!transition) { &#x20; const errorFormatter = INVALID\_STATE\_ERRORS\[event]; &#x20; const message = errorFormatter &#x20; ? errorFormatter(order.status) &#x20; : `Invalid event ${event} for status ${order.status}`; &#x20; throw new Error(message); &#x20; } &#x20; if (transition.guard) { &#x20; transition.guard(order, payload); &#x20; } &#x20; order.status = transition.target; &#x20; if (transition.action) { &#x20; return transition.action(order, payload); &#x20; } &#x20; } &#x20; addItem(id, name, price) { &#x20; return this.\_dispatch(id, "addItem", { name, price }); &#x20; } &#x20; fire(id) { &#x20; this.\_dispatch(id, "fire"); &#x20; } &#x20; markReady(id) { &#x20; this.\_dispatch(id, "markReady"); &#x20; } &#x20; serve(id) { &#x20; this.\_dispatch(id, "serve"); &#x20; } &#x20; pay(id, amount) { &#x20; return this.\_dispatch(id, "pay", { amount }); &#x20; } &#x20; cancel(id) { &#x20; this.\_dispatch(id, "cancel"); &#x20; } &#x20; getStatus(id) { &#x20; const order = this.orders.get(id); &#x20; if (!order) throw new Error("unknown order"); &#x20; // Reconstruct paid/fired flags dynamically for backward compatibility &#x20; return { &#x20; ...order, &#x20; fired: \["preparing", "ready", "served", "paid"].includes(order.status), &#x20; paid: order.status === "paid", &#x20; }; &#x20; } } module.exports = { OrderManager }; // --- Demo Run --- if (require.main === module) { &#x20; const manager = new OrderManager(); &#x20; const id = manager.createOrder(5); &#x20; manager.addItem(id, "Burger", 12.5); &#x20; manager.addItem(id, "Fries", 4.0); &#x20; console.log("Status after items:", manager.getStatus(id)); &#x20; manager.fire(id); &#x20; manager.markReady(id); &#x20; manager.serve(id); &#x20; const change = manager.pay(id, 20.0); &#x20; console.log(`Paid $20, Change: $${change}`); &#x20; console.log("Final Status:", manager.getStatus(id)); }