@dailephd/my-dev-kit-lab 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +18 -3
- package/benchmarks/contracts/agent-success-rate-project-profiles.json +33 -0
- package/benchmarks/contracts/agent-success-rate-tasks.json +671 -0
- package/benchmarks/projects/agent-success-inventory-node/.gitattributes +2 -0
- package/benchmarks/projects/agent-success-inventory-node/README.md +74 -0
- package/benchmarks/projects/agent-success-inventory-node/package.json +14 -0
- package/benchmarks/projects/agent-success-inventory-node/src/fulfillmentReport.js +38 -0
- package/benchmarks/projects/agent-success-inventory-node/src/index.js +26 -0
- package/benchmarks/projects/agent-success-inventory-node/src/inventoryStore.js +114 -0
- package/benchmarks/projects/agent-success-inventory-node/src/quantity.js +19 -0
- package/benchmarks/projects/agent-success-inventory-node/src/reservationService.js +45 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/quantity-edge.check.mjs +51 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/quantity-primary.check.mjs +46 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/regression.check.mjs +115 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/report-edge.check.mjs +109 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/report-primary.check.mjs +47 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/reservation-edge.check.mjs +63 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/reservation-primary.check.mjs +44 -0
- package/benchmarks/projects/agent-success-task-board-node/.gitattributes +2 -0
- package/benchmarks/projects/agent-success-task-board-node/README.md +77 -0
- package/benchmarks/projects/agent-success-task-board-node/package.json +14 -0
- package/benchmarks/projects/agent-success-task-board-node/src/index.js +24 -0
- package/benchmarks/projects/agent-success-task-board-node/src/projectSummary.js +15 -0
- package/benchmarks/projects/agent-success-task-board-node/src/taskService.js +58 -0
- package/benchmarks/projects/agent-success-task-board-node/src/taskStore.js +58 -0
- package/benchmarks/projects/agent-success-task-board-node/src/validation.js +31 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/import-edge.check.mjs +65 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/import-primary.check.mjs +31 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/regression.check.mjs +107 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/summary-edge.check.mjs +67 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/summary-primary.check.mjs +54 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/title-edge.check.mjs +39 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/title-primary.check.mjs +29 -0
- package/dist/src/cli/help.js +38 -1
- package/dist/src/commands/runExperimentDescribeCommand.js +52 -1
- package/dist/src/commands/runExperimentRunCommand.js +118 -2
- package/dist/src/core/processTree.js +55 -0
- package/dist/src/core/runMeasuredCommand.js +104 -22
- package/dist/src/evaluation/agentSuccess/applyPatchToSandbox.js +37 -0
- package/dist/src/evaluation/agentSuccess/assessBaseline.js +23 -0
- package/dist/src/evaluation/agentSuccess/corpusTypes.js +74 -0
- package/dist/src/evaluation/agentSuccess/extractPatchCandidate.js +46 -0
- package/dist/src/evaluation/agentSuccess/index.js +14 -0
- package/dist/src/evaluation/agentSuccess/parseUnifiedDiff.js +210 -0
- package/dist/src/evaluation/agentSuccess/patchTypes.js +5 -0
- package/dist/src/evaluation/agentSuccess/readAgentSuccessCorpus.js +183 -0
- package/dist/src/evaluation/agentSuccess/runVerificationCheck.js +100 -0
- package/dist/src/evaluation/agentSuccess/taskPaths.js +34 -0
- package/dist/src/evaluation/agentSuccess/taskTypes.js +14 -0
- package/dist/src/evaluation/agentSuccess/validateAgentSuccessCorpus.js +307 -0
- package/dist/src/evaluation/agentSuccess/validateAgentSuccessTask.js +297 -0
- package/dist/src/evaluation/agentSuccess/validatePatchPolicy.js +66 -0
- package/dist/src/evaluation/agentSuccess/verificationTypes.js +1 -0
- package/dist/src/evaluation/benchmarkSandbox/createBenchmarkSandbox.js +127 -0
- package/dist/src/evaluation/benchmarkSandbox/errors.js +11 -0
- package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +60 -0
- package/dist/src/evaluation/benchmarkSandbox/index.js +8 -0
- package/dist/src/evaluation/benchmarkSandbox/minimalHostEnv.js +37 -0
- package/dist/src/evaluation/benchmarkSandbox/pathPolicy.js +49 -0
- package/dist/src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.js +74 -0
- package/dist/src/evaluation/benchmarkSandbox/treeSnapshot.js +57 -0
- package/dist/src/evaluation/benchmarkSandbox/types.js +3 -0
- package/dist/src/evaluation/changeSet/captureChangeSet.js +98 -0
- package/dist/src/evaluation/changeSet/index.js +2 -0
- package/dist/src/evaluation/changeSet/types.js +9 -0
- package/dist/src/experiments/defaultRegistry.js +2 -0
- package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
- package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
- package/dist/src/experiments/plugins/agentSuccessRate/config.js +123 -0
- package/dist/src/experiments/plugins/agentSuccessRate/contextGeneration.js +497 -0
- package/dist/src/experiments/plugins/agentSuccessRate/execution.js +441 -0
- package/dist/src/experiments/plugins/agentSuccessRate/executionArtifact.js +126 -0
- package/dist/src/experiments/plugins/agentSuccessRate/executionTypes.js +2 -0
- package/dist/src/experiments/plugins/agentSuccessRate/index.js +20 -0
- package/dist/src/experiments/plugins/agentSuccessRate/metadata.js +28 -0
- package/dist/src/experiments/plugins/agentSuccessRate/metrics.js +76 -0
- package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +507 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +250 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +43 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairAnalysis.js +148 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairExecution.js +103 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairFeedback.js +91 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairPolicy.js +88 -0
- package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
- package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
- package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
- package/dist/src/report/experiments/buildAgentSuccessRateReport.js +255 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +27 -2
- package/dist/src/report/experiments/index.js +4 -0
- package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +169 -0
- package/dist/src/report/experiments/renderAgentSuccessRateText.js +139 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +5 -0
- package/docs/METRICS.md +70 -0
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# agent-success-inventory-node
|
|
2
|
+
|
|
3
|
+
A small, dependency-free, in-memory inventory and reservation system written as JavaScript ES modules for
|
|
4
|
+
Node.js `>=24`.
|
|
5
|
+
|
|
6
|
+
This project is an **implementation benchmark** for the `agent-success-rate` experiment of
|
|
7
|
+
`my-dev-kit-lab`. It is a controlled fixture: the Lab copies it into a disposable sandbox, asks an
|
|
8
|
+
implementation to change code under `src/`, and judges the result with trusted checks that the
|
|
9
|
+
implementation does not receive. The project is not a product and has no database, network access,
|
|
10
|
+
environment dependence, randomness or timestamps.
|
|
11
|
+
|
|
12
|
+
## Behavior
|
|
13
|
+
|
|
14
|
+
The system models stock, reservations against that stock, and a fulfillment report. All state lives in
|
|
15
|
+
memory inside one system instance; two systems never share state.
|
|
16
|
+
|
|
17
|
+
- An item is identified by its `sku` and tracks `received` (all stock ever added) and `available` (stock not
|
|
18
|
+
held by a reservation or consumed by a fulfilled reservation).
|
|
19
|
+
- A **quantity** for stock or for a reservation must be a positive integer. Zero, negative numbers,
|
|
20
|
+
fractions, `NaN`, infinities and non-number values are rejected with a `QuantityError` whose `code` is
|
|
21
|
+
`INVALID_QUANTITY`. Rejection happens before any stock or reservation changes.
|
|
22
|
+
- A **reservation** has an `id` supplied by the caller, a `sku`, a `quantity` and a `status`, which is one of
|
|
23
|
+
`active`, `fulfilled` or `cancelled`.
|
|
24
|
+
- Reserving moves the quantity out of `available` exactly once and records an `active` reservation.
|
|
25
|
+
- A reservation that cannot be completed (unknown sku, insufficient stock, duplicate id) fails with a
|
|
26
|
+
stable error `code` (`UNKNOWN_SKU`, `INSUFFICIENT_STOCK`, `DUPLICATE_RESERVATION`) and leaves stock and
|
|
27
|
+
reservation records exactly as they were. A reservation id that never produced a record may be used
|
|
28
|
+
again.
|
|
29
|
+
- Cancelling an `active` reservation returns its quantity to `available` and marks it `cancelled`. Cancelling
|
|
30
|
+
an already cancelled reservation changes nothing. A reservation in another terminal state is not
|
|
31
|
+
restored (`INVALID_TRANSITION`).
|
|
32
|
+
- Fulfilling an `active` reservation marks it `fulfilled`; its stock stays consumed.
|
|
33
|
+
- The **fulfillment report** has one row per sku, sorted by sku, plus totals. Each row reports `received`,
|
|
34
|
+
`available` and `active`, `fulfilled` and `cancelled` buckets of `{ count, quantity }`. Reported figures
|
|
35
|
+
agree with the store, and `received = available + active.quantity + fulfilled.quantity` holds for every
|
|
36
|
+
row. The report is deterministic.
|
|
37
|
+
|
|
38
|
+
## Public API
|
|
39
|
+
|
|
40
|
+
Everything is exported from `src/index.js`.
|
|
41
|
+
|
|
42
|
+
| Export | Purpose |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `createInventorySystem()` | Returns `{ store, service, report }` for a fresh, independent system. |
|
|
45
|
+
| `InventoryStore` | `addStock`, `getItem`, `listItems`, `takeStock`, `returnStock`, `addReservation`, `getReservation`, `listReservations`, `setReservationStatus`. |
|
|
46
|
+
| `ReservationService` | `reserve({ reservationId, sku, quantity })`, `cancel(id)`, `fulfill(id)`. |
|
|
47
|
+
| `buildFulfillmentReport(store)` | Returns `{ skus, totals }`. |
|
|
48
|
+
| `assertPositiveInteger(value)` | Returns the quantity or throws `QuantityError`. |
|
|
49
|
+
| `RESERVATION_ACTIVE`, `RESERVATION_FULFILLED`, `RESERVATION_CANCELLED` | Reservation status values. |
|
|
50
|
+
| `QuantityError`, `UnknownSkuError`, `InsufficientStockError`, `UnknownReservationError`, `DuplicateReservationError` | Error classes with a stable `code` property. |
|
|
51
|
+
|
|
52
|
+
Stores return copies of items and reservations, so changing a returned object never changes the system.
|
|
53
|
+
|
|
54
|
+
## Layout
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
src/quantity.js quantity contract
|
|
58
|
+
src/inventoryStore.js in-memory stock and reservation storage
|
|
59
|
+
src/reservationService.js reserve, cancel and fulfill
|
|
60
|
+
src/fulfillmentReport.js deterministic fulfillment report
|
|
61
|
+
src/index.js public exports and createInventorySystem()
|
|
62
|
+
tests/*.check.mjs trusted verification checks (owned by the Lab verifier)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Running checks
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
npm test
|
|
69
|
+
node --test tests/regression.check.mjs
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The files under `tests/` are named `*.check.mjs` on purpose and are run explicitly through Node's built-in
|
|
73
|
+
test runner. They are verification inputs owned by the Lab, not part of the implementation to change.
|
|
74
|
+
No installation step is needed.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-success-inventory-node",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Dependency-free JavaScript implementation benchmark: an in-memory inventory and reservation system.",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test \"tests/*.check.mjs\""
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RESERVATION_FULFILLED } from "./inventoryStore.js";
|
|
2
|
+
|
|
3
|
+
function bucket(reservations) {
|
|
4
|
+
return {
|
|
5
|
+
count: reservations.length,
|
|
6
|
+
quantity: reservations.reduce((sum, reservation) => sum + reservation.quantity, 0)
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds a deterministic fulfillment report: one row per sku (sorted by sku) and totals across rows.
|
|
12
|
+
* For every row, received = available + active.quantity + fulfilled.quantity.
|
|
13
|
+
*/
|
|
14
|
+
export function buildFulfillmentReport(store) {
|
|
15
|
+
const reservations = store.listReservations();
|
|
16
|
+
const rows = store.listItems().map((item) => {
|
|
17
|
+
const mine = reservations.filter((reservation) => reservation.sku === item.sku);
|
|
18
|
+
return {
|
|
19
|
+
sku: item.sku,
|
|
20
|
+
received: item.received,
|
|
21
|
+
available: item.available,
|
|
22
|
+
active: bucket(mine.filter((reservation) => reservation.status !== RESERVATION_FULFILLED)),
|
|
23
|
+
fulfilled: bucket(mine.filter((reservation) => reservation.status === RESERVATION_FULFILLED)),
|
|
24
|
+
cancelled: bucket([])
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
const sum = (selector) => rows.reduce((total, row) => total + selector(row), 0);
|
|
28
|
+
return {
|
|
29
|
+
skus: rows,
|
|
30
|
+
totals: {
|
|
31
|
+
received: sum((row) => row.received),
|
|
32
|
+
available: sum((row) => row.available),
|
|
33
|
+
active: sum((row) => row.active.quantity),
|
|
34
|
+
fulfilled: sum((row) => row.fulfilled.quantity),
|
|
35
|
+
cancelled: sum((row) => row.cancelled.quantity)
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buildFulfillmentReport } from "./fulfillmentReport.js";
|
|
2
|
+
import { InventoryStore } from "./inventoryStore.js";
|
|
3
|
+
import { ReservationService } from "./reservationService.js";
|
|
4
|
+
|
|
5
|
+
export { buildFulfillmentReport } from "./fulfillmentReport.js";
|
|
6
|
+
export {
|
|
7
|
+
DuplicateReservationError,
|
|
8
|
+
InsufficientStockError,
|
|
9
|
+
InventoryStore,
|
|
10
|
+
RESERVATION_ACTIVE,
|
|
11
|
+
RESERVATION_CANCELLED,
|
|
12
|
+
RESERVATION_FULFILLED,
|
|
13
|
+
UnknownReservationError,
|
|
14
|
+
UnknownSkuError
|
|
15
|
+
} from "./inventoryStore.js";
|
|
16
|
+
export { INVALID_QUANTITY, QuantityError, assertPositiveInteger } from "./quantity.js";
|
|
17
|
+
export { ReservationService } from "./reservationService.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates an independent in-memory inventory: a store, a reservation service over it, and a report helper.
|
|
21
|
+
*/
|
|
22
|
+
export function createInventorySystem() {
|
|
23
|
+
const store = new InventoryStore();
|
|
24
|
+
const service = new ReservationService(store);
|
|
25
|
+
return { store, service, report: () => buildFulfillmentReport(store) };
|
|
26
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { assertPositiveInteger } from "./quantity.js";
|
|
2
|
+
|
|
3
|
+
export const RESERVATION_ACTIVE = "active";
|
|
4
|
+
export const RESERVATION_FULFILLED = "fulfilled";
|
|
5
|
+
export const RESERVATION_CANCELLED = "cancelled";
|
|
6
|
+
|
|
7
|
+
export class UnknownSkuError extends Error {
|
|
8
|
+
constructor(sku) {
|
|
9
|
+
super(`unknown sku: ${sku}`);
|
|
10
|
+
this.name = "UnknownSkuError";
|
|
11
|
+
this.code = "UNKNOWN_SKU";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class InsufficientStockError extends Error {
|
|
16
|
+
constructor(sku) {
|
|
17
|
+
super(`insufficient stock for sku: ${sku}`);
|
|
18
|
+
this.name = "InsufficientStockError";
|
|
19
|
+
this.code = "INSUFFICIENT_STOCK";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class UnknownReservationError extends Error {
|
|
24
|
+
constructor(id) {
|
|
25
|
+
super(`unknown reservation: ${id}`);
|
|
26
|
+
this.name = "UnknownReservationError";
|
|
27
|
+
this.code = "UNKNOWN_RESERVATION";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class DuplicateReservationError extends Error {
|
|
32
|
+
constructor(id) {
|
|
33
|
+
super(`duplicate reservation: ${id}`);
|
|
34
|
+
this.name = "DuplicateReservationError";
|
|
35
|
+
this.code = "DUPLICATE_RESERVATION";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* In-memory inventory and reservation storage. Items and reservations are returned as copies.
|
|
41
|
+
* Each item tracks `received` (all stock ever added) and `available` (stock not held by a reservation).
|
|
42
|
+
*/
|
|
43
|
+
export class InventoryStore {
|
|
44
|
+
#items = new Map();
|
|
45
|
+
#reservations = new Map();
|
|
46
|
+
|
|
47
|
+
addStock(sku, quantity) {
|
|
48
|
+
assertPositiveInteger(quantity);
|
|
49
|
+
const item = this.#items.get(sku) ?? { sku, available: 0, received: 0 };
|
|
50
|
+
item.available += quantity;
|
|
51
|
+
item.received += quantity;
|
|
52
|
+
this.#items.set(sku, item);
|
|
53
|
+
return { ...item };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getItem(sku) {
|
|
57
|
+
const item = this.#items.get(sku);
|
|
58
|
+
return item ? { ...item } : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Items sorted by sku. */
|
|
62
|
+
listItems() {
|
|
63
|
+
return [...this.#items.values()]
|
|
64
|
+
.sort((a, b) => (a.sku < b.sku ? -1 : a.sku > b.sku ? 1 : 0))
|
|
65
|
+
.map((item) => ({ ...item }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Removes stock from `available` for a reservation. */
|
|
69
|
+
takeStock(sku, quantity) {
|
|
70
|
+
const item = this.#items.get(sku);
|
|
71
|
+
if (!item) {
|
|
72
|
+
throw new UnknownSkuError(sku);
|
|
73
|
+
}
|
|
74
|
+
item.available -= quantity;
|
|
75
|
+
if (item.available < 0) {
|
|
76
|
+
throw new InsufficientStockError(sku);
|
|
77
|
+
}
|
|
78
|
+
return { ...item };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Returns stock to `available`. */
|
|
82
|
+
returnStock(sku, quantity) {
|
|
83
|
+
const item = this.#items.get(sku);
|
|
84
|
+
if (!item) {
|
|
85
|
+
throw new UnknownSkuError(sku);
|
|
86
|
+
}
|
|
87
|
+
item.available += quantity;
|
|
88
|
+
return { ...item };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
addReservation(record) {
|
|
92
|
+
this.#reservations.set(record.id, { ...record });
|
|
93
|
+
return { ...record };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getReservation(id) {
|
|
97
|
+
const record = this.#reservations.get(id);
|
|
98
|
+
return record ? { ...record } : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Reservations in the order they were recorded. */
|
|
102
|
+
listReservations() {
|
|
103
|
+
return [...this.#reservations.values()].map((record) => ({ ...record }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setReservationStatus(id, status) {
|
|
107
|
+
const record = this.#reservations.get(id);
|
|
108
|
+
if (!record) {
|
|
109
|
+
throw new UnknownReservationError(id);
|
|
110
|
+
}
|
|
111
|
+
record.status = status;
|
|
112
|
+
return { ...record };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const INVALID_QUANTITY = "INVALID_QUANTITY";
|
|
2
|
+
|
|
3
|
+
export class QuantityError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "QuantityError";
|
|
7
|
+
this.code = INVALID_QUANTITY;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns the quantity when it is a valid stock or reservation quantity, or throws QuantityError.
|
|
13
|
+
*/
|
|
14
|
+
export function assertPositiveInteger(value) {
|
|
15
|
+
if (typeof value !== "number" || value < 0) {
|
|
16
|
+
throw new QuantityError("quantity must be a positive integer");
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RESERVATION_ACTIVE,
|
|
3
|
+
RESERVATION_FULFILLED,
|
|
4
|
+
UnknownReservationError
|
|
5
|
+
} from "./inventoryStore.js";
|
|
6
|
+
import { assertPositiveInteger } from "./quantity.js";
|
|
7
|
+
|
|
8
|
+
export class ReservationService {
|
|
9
|
+
constructor(store) {
|
|
10
|
+
this.store = store;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reserves `quantity` units of `sku` under a caller-supplied reservation id.
|
|
15
|
+
*/
|
|
16
|
+
reserve({ reservationId, sku, quantity }) {
|
|
17
|
+
assertPositiveInteger(quantity);
|
|
18
|
+
this.store.addReservation({ id: reservationId, sku, quantity, status: RESERVATION_ACTIVE });
|
|
19
|
+
this.store.takeStock(sku, quantity);
|
|
20
|
+
return this.store.getReservation(reservationId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cancels a reservation and returns its quantity to available stock.
|
|
25
|
+
*/
|
|
26
|
+
cancel(reservationId) {
|
|
27
|
+
const record = this.store.getReservation(reservationId);
|
|
28
|
+
if (!record) {
|
|
29
|
+
throw new UnknownReservationError(reservationId);
|
|
30
|
+
}
|
|
31
|
+
this.store.returnStock(record.sku, record.quantity);
|
|
32
|
+
return this.store.setReservationStatus(reservationId, "canceled");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Marks a reservation as fulfilled. Fulfilled stock stays consumed.
|
|
37
|
+
*/
|
|
38
|
+
fulfill(reservationId) {
|
|
39
|
+
const record = this.store.getReservation(reservationId);
|
|
40
|
+
if (!record) {
|
|
41
|
+
throw new UnknownReservationError(reservationId);
|
|
42
|
+
}
|
|
43
|
+
return this.store.setReservationStatus(reservationId, RESERVATION_FULFILLED);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { INVALID_QUANTITY, QuantityError, assertPositiveInteger, createInventorySystem } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
function isQuantityError(error) {
|
|
6
|
+
return error instanceof QuantityError && error.code === INVALID_QUANTITY;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
test("non-finite and not-a-number values are rejected", () => {
|
|
10
|
+
for (const value of [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) {
|
|
11
|
+
assert.throws(() => assertPositiveInteger(value), isQuantityError);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("values that only look like zero or a whole number are rejected", () => {
|
|
16
|
+
for (const value of [-0, 0.1, 1e-9, Number.EPSILON, 0.999999, 2.000001]) {
|
|
17
|
+
assert.throws(() => assertPositiveInteger(value), isQuantityError);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("nonnumeric values are rejected", () => {
|
|
22
|
+
for (const value of ["3", "", " ", null, undefined, {}, [], [3], true, 3n, () => 3]) {
|
|
23
|
+
assert.throws(() => assertPositiveInteger(value), isQuantityError);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("positive integers are accepted and returned unchanged", () => {
|
|
28
|
+
for (const value of [1, 2, 10, 1000, Number.MAX_SAFE_INTEGER]) {
|
|
29
|
+
assert.equal(assertPositiveInteger(value), value);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("rejected stock additions never mutate inventory", () => {
|
|
34
|
+
const { store } = createInventorySystem();
|
|
35
|
+
store.addStock("gadget", 2);
|
|
36
|
+
for (const quantity of [Number.NaN, Number.POSITIVE_INFINITY, -0, "4", null]) {
|
|
37
|
+
assert.throws(() => store.addStock("gadget", quantity), isQuantityError);
|
|
38
|
+
assert.throws(() => store.addStock("fresh", quantity), isQuantityError);
|
|
39
|
+
}
|
|
40
|
+
assert.deepEqual(store.listItems(), [{ sku: "gadget", available: 2, received: 2 }]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("rejected reservations never mutate inventory or reservation records", () => {
|
|
44
|
+
const { store, service } = createInventorySystem();
|
|
45
|
+
store.addStock("gadget", 4);
|
|
46
|
+
for (const quantity of [Number.NaN, Number.POSITIVE_INFINITY, -0, 0.25, "2"]) {
|
|
47
|
+
assert.throws(() => service.reserve({ reservationId: "r-edge", sku: "gadget", quantity }), isQuantityError);
|
|
48
|
+
}
|
|
49
|
+
assert.equal(store.getItem("gadget").available, 4);
|
|
50
|
+
assert.equal(store.getReservation("r-edge"), null);
|
|
51
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { INVALID_QUANTITY, QuantityError, createInventorySystem } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
function isQuantityError(error) {
|
|
6
|
+
return error instanceof QuantityError && error.code === INVALID_QUANTITY;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
test("zero is not a valid stock quantity and creates no item", () => {
|
|
10
|
+
const { store } = createInventorySystem();
|
|
11
|
+
assert.throws(() => store.addStock("widget", 0), isQuantityError);
|
|
12
|
+
assert.equal(store.getItem("widget"), null);
|
|
13
|
+
assert.deepEqual(store.listItems(), []);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("negative and fractional quantities are rejected before stock changes", () => {
|
|
17
|
+
const { store } = createInventorySystem();
|
|
18
|
+
store.addStock("widget", 5);
|
|
19
|
+
for (const quantity of [-1, -5, 1.5, 0.5]) {
|
|
20
|
+
assert.throws(() => store.addStock("widget", quantity), isQuantityError);
|
|
21
|
+
}
|
|
22
|
+
assert.deepEqual(store.getItem("widget"), { sku: "widget", available: 5, received: 5 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("invalid reservation quantities are rejected before inventory or reservations change", () => {
|
|
26
|
+
const { store, service } = createInventorySystem();
|
|
27
|
+
store.addStock("widget", 5);
|
|
28
|
+
for (const quantity of [0, -1, 2.5]) {
|
|
29
|
+
assert.throws(
|
|
30
|
+
() => service.reserve({ reservationId: `r-${String(quantity)}`, sku: "widget", quantity }),
|
|
31
|
+
isQuantityError
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
assert.deepEqual(store.getItem("widget"), { sku: "widget", available: 5, received: 5 });
|
|
35
|
+
assert.deepEqual(store.listReservations(), []);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("valid positive quantities keep working", () => {
|
|
39
|
+
const { store, service } = createInventorySystem();
|
|
40
|
+
store.addStock("widget", 3);
|
|
41
|
+
store.addStock("widget", 2);
|
|
42
|
+
assert.equal(store.getItem("widget").available, 5);
|
|
43
|
+
const reservation = service.reserve({ reservationId: "r-1", sku: "widget", quantity: 4 });
|
|
44
|
+
assert.equal(reservation.quantity, 4);
|
|
45
|
+
assert.equal(store.getItem("widget").available, 1);
|
|
46
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
InventoryStore,
|
|
5
|
+
QuantityError,
|
|
6
|
+
RESERVATION_ACTIVE,
|
|
7
|
+
RESERVATION_FULFILLED,
|
|
8
|
+
createInventorySystem
|
|
9
|
+
} from "../src/index.js";
|
|
10
|
+
|
|
11
|
+
test("addStock accumulates per sku and tracks received stock", () => {
|
|
12
|
+
const { store } = createInventorySystem();
|
|
13
|
+
store.addStock("widget", 3);
|
|
14
|
+
store.addStock("widget", 4);
|
|
15
|
+
store.addStock("gadget", 1);
|
|
16
|
+
assert.deepEqual(store.getItem("widget"), { sku: "widget", available: 7, received: 7 });
|
|
17
|
+
assert.deepEqual(
|
|
18
|
+
store.listItems().map((item) => item.sku),
|
|
19
|
+
["gadget", "widget"]
|
|
20
|
+
);
|
|
21
|
+
assert.equal(store.getItem("missing"), null);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("a valid reservation holds stock and records an active reservation", () => {
|
|
25
|
+
const { store, service } = createInventorySystem();
|
|
26
|
+
store.addStock("widget", 5);
|
|
27
|
+
const reservation = service.reserve({ reservationId: "r-1", sku: "widget", quantity: 2 });
|
|
28
|
+
assert.deepEqual(reservation, { id: "r-1", sku: "widget", quantity: 2, status: RESERVATION_ACTIVE });
|
|
29
|
+
assert.deepEqual(store.getItem("widget"), { sku: "widget", available: 3, received: 5 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("quantities given as strings or other non-numbers are rejected", () => {
|
|
33
|
+
const { store, service } = createInventorySystem();
|
|
34
|
+
store.addStock("widget", 5);
|
|
35
|
+
assert.throws(() => store.addStock("widget", "2"), QuantityError);
|
|
36
|
+
assert.throws(() => service.reserve({ reservationId: "r-1", sku: "widget", quantity: "2" }), QuantityError);
|
|
37
|
+
assert.throws(() => service.reserve({ reservationId: "r-1", sku: "widget", quantity: undefined }), QuantityError);
|
|
38
|
+
assert.equal(store.getItem("widget").available, 5);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("negative quantities are rejected", () => {
|
|
42
|
+
const { store } = createInventorySystem();
|
|
43
|
+
assert.throws(() => store.addStock("widget", -3), { code: "INVALID_QUANTITY" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("reserving an unknown sku fails with the unknown-sku error", () => {
|
|
47
|
+
const { service } = createInventorySystem();
|
|
48
|
+
assert.throws(() => service.reserve({ reservationId: "r-1", sku: "missing", quantity: 1 }), {
|
|
49
|
+
code: "UNKNOWN_SKU"
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("fulfilling an active reservation keeps its stock consumed", () => {
|
|
54
|
+
const { store, service } = createInventorySystem();
|
|
55
|
+
store.addStock("widget", 5);
|
|
56
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 2 });
|
|
57
|
+
const fulfilled = service.fulfill("r-1");
|
|
58
|
+
assert.equal(fulfilled.status, RESERVATION_FULFILLED);
|
|
59
|
+
assert.equal(store.getItem("widget").available, 3);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("cancelling an active reservation restores its stock", () => {
|
|
63
|
+
const { store, service } = createInventorySystem();
|
|
64
|
+
store.addStock("widget", 5);
|
|
65
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 2 });
|
|
66
|
+
service.cancel("r-1");
|
|
67
|
+
assert.equal(store.getItem("widget").available, 5);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("the report of an untouched inventory mirrors the stock", () => {
|
|
71
|
+
const { store, report } = createInventorySystem();
|
|
72
|
+
store.addStock("b-item", 2);
|
|
73
|
+
store.addStock("a-item", 4);
|
|
74
|
+
assert.deepEqual(report(), {
|
|
75
|
+
skus: [
|
|
76
|
+
{
|
|
77
|
+
sku: "a-item",
|
|
78
|
+
received: 4,
|
|
79
|
+
available: 4,
|
|
80
|
+
active: { count: 0, quantity: 0 },
|
|
81
|
+
fulfilled: { count: 0, quantity: 0 },
|
|
82
|
+
cancelled: { count: 0, quantity: 0 }
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
sku: "b-item",
|
|
86
|
+
received: 2,
|
|
87
|
+
available: 2,
|
|
88
|
+
active: { count: 0, quantity: 0 },
|
|
89
|
+
fulfilled: { count: 0, quantity: 0 },
|
|
90
|
+
cancelled: { count: 0, quantity: 0 }
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
totals: { received: 6, available: 6, active: 0, fulfilled: 0, cancelled: 0 }
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("the report counts active and fulfilled reservations", () => {
|
|
98
|
+
const { store, service, report } = createInventorySystem();
|
|
99
|
+
store.addStock("widget", 10);
|
|
100
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 3 });
|
|
101
|
+
service.reserve({ reservationId: "r-2", sku: "widget", quantity: 2 });
|
|
102
|
+
service.fulfill("r-2");
|
|
103
|
+
const [row] = report().skus;
|
|
104
|
+
assert.deepEqual(row.active, { count: 1, quantity: 3 });
|
|
105
|
+
assert.deepEqual(row.fulfilled, { count: 1, quantity: 2 });
|
|
106
|
+
assert.equal(row.available, 5);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("inventories are independent of each other", () => {
|
|
110
|
+
const one = createInventorySystem();
|
|
111
|
+
const two = createInventorySystem();
|
|
112
|
+
one.store.addStock("widget", 2);
|
|
113
|
+
assert.deepEqual(two.store.listItems(), []);
|
|
114
|
+
assert.ok(new InventoryStore().listReservations().length === 0);
|
|
115
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { RESERVATION_ACTIVE, RESERVATION_CANCELLED, RESERVATION_FULFILLED, createInventorySystem } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
test("repeated cancellation does not restore stock again", () => {
|
|
6
|
+
const { store, service } = createInventorySystem();
|
|
7
|
+
store.addStock("widget", 10);
|
|
8
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 4 });
|
|
9
|
+
service.cancel("r-1");
|
|
10
|
+
service.cancel("r-1");
|
|
11
|
+
service.cancel("r-1");
|
|
12
|
+
assert.equal(store.getItem("widget").available, 10);
|
|
13
|
+
assert.equal(store.getReservation("r-1").status, RESERVATION_CANCELLED);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("a fulfilled reservation cannot be cancelled back into stock", () => {
|
|
17
|
+
const { store, service } = createInventorySystem();
|
|
18
|
+
store.addStock("widget", 10);
|
|
19
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 4 });
|
|
20
|
+
service.fulfill("r-1");
|
|
21
|
+
try {
|
|
22
|
+
service.cancel("r-1");
|
|
23
|
+
} catch {
|
|
24
|
+
// Rejecting the transition is acceptable; restoring stock is not.
|
|
25
|
+
}
|
|
26
|
+
assert.equal(store.getItem("widget").available, 6);
|
|
27
|
+
assert.equal(store.getReservation("r-1").status, RESERVATION_FULFILLED);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a cancelled reservation cannot be fulfilled", () => {
|
|
31
|
+
const { store, service, report } = createInventorySystem();
|
|
32
|
+
store.addStock("widget", 10);
|
|
33
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 4 });
|
|
34
|
+
service.cancel("r-1");
|
|
35
|
+
try {
|
|
36
|
+
service.fulfill("r-1");
|
|
37
|
+
} catch {
|
|
38
|
+
// Rejecting the transition is acceptable; reporting it as fulfilled is not.
|
|
39
|
+
}
|
|
40
|
+
assert.equal(store.getReservation("r-1").status, RESERVATION_CANCELLED);
|
|
41
|
+
assert.deepEqual(report().skus[0].fulfilled, { count: 0, quantity: 0 });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("unknown reservations are rejected and change nothing", () => {
|
|
45
|
+
const { store, service } = createInventorySystem();
|
|
46
|
+
store.addStock("widget", 3);
|
|
47
|
+
assert.throws(() => service.cancel("nope"), { code: "UNKNOWN_RESERVATION" });
|
|
48
|
+
assert.throws(() => service.fulfill("nope"), { code: "UNKNOWN_RESERVATION" });
|
|
49
|
+
assert.equal(store.getItem("widget").available, 3);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("only the documented lifecycle states can be stored", () => {
|
|
53
|
+
const { store, service } = createInventorySystem();
|
|
54
|
+
store.addStock("widget", 3);
|
|
55
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 1 });
|
|
56
|
+
for (const status of ["canceled", "complete", "", undefined, "ACTIVE"]) {
|
|
57
|
+
assert.throws(() => store.setReservationStatus("r-1", status));
|
|
58
|
+
}
|
|
59
|
+
assert.equal(store.getReservation("r-1").status, RESERVATION_ACTIVE);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("unrelated skus are unaffected by cancellation and fulfillment", () => {
|
|
63
|
+
const { store, service, report } = createInventorySystem();
|
|
64
|
+
store.addStock("widget", 10);
|
|
65
|
+
store.addStock("gadget", 6);
|
|
66
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 4 });
|
|
67
|
+
service.reserve({ reservationId: "r-2", sku: "gadget", quantity: 2 });
|
|
68
|
+
const gadgetBefore = report().skus.find((row) => row.sku === "gadget");
|
|
69
|
+
service.cancel("r-1");
|
|
70
|
+
service.cancel("r-1");
|
|
71
|
+
const gadgetAfter = report().skus.find((row) => row.sku === "gadget");
|
|
72
|
+
assert.deepEqual(gadgetAfter, gadgetBefore);
|
|
73
|
+
assert.deepEqual(gadgetAfter.active, { count: 1, quantity: 2 });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("the report is deterministic and sorted by sku regardless of insertion order", () => {
|
|
77
|
+
const build = (order) => {
|
|
78
|
+
const { store, service, report } = createInventorySystem();
|
|
79
|
+
for (const sku of order) {
|
|
80
|
+
store.addStock(sku, 5);
|
|
81
|
+
}
|
|
82
|
+
service.reserve({ reservationId: "r-1", sku: "b-item", quantity: 2 });
|
|
83
|
+
service.reserve({ reservationId: "r-2", sku: "a-item", quantity: 1 });
|
|
84
|
+
service.cancel("r-2");
|
|
85
|
+
return report();
|
|
86
|
+
};
|
|
87
|
+
const first = build(["b-item", "a-item", "c-item"]);
|
|
88
|
+
const second = build(["c-item", "a-item", "b-item"]);
|
|
89
|
+
assert.deepEqual(first, second);
|
|
90
|
+
assert.deepEqual(
|
|
91
|
+
first.skus.map((row) => row.sku),
|
|
92
|
+
["a-item", "b-item", "c-item"]
|
|
93
|
+
);
|
|
94
|
+
assert.deepEqual(first, build(["b-item", "a-item", "c-item"]));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("every row keeps received equal to available plus active plus fulfilled quantity", () => {
|
|
98
|
+
const { store, service, report } = createInventorySystem();
|
|
99
|
+
store.addStock("widget", 9);
|
|
100
|
+
service.reserve({ reservationId: "r-1", sku: "widget", quantity: 2 });
|
|
101
|
+
service.reserve({ reservationId: "r-2", sku: "widget", quantity: 3 });
|
|
102
|
+
service.reserve({ reservationId: "r-3", sku: "widget", quantity: 1 });
|
|
103
|
+
service.cancel("r-1");
|
|
104
|
+
service.cancel("r-1");
|
|
105
|
+
service.fulfill("r-2");
|
|
106
|
+
for (const row of report().skus) {
|
|
107
|
+
assert.equal(row.received, row.available + row.active.quantity + row.fulfilled.quantity);
|
|
108
|
+
}
|
|
109
|
+
});
|