@dailephd/my-dev-kit-lab 0.8.1 → 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.
Files changed (127) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +20 -4
  3. package/benchmarks/contracts/agent-success-rate-project-profiles.json +33 -0
  4. package/benchmarks/contracts/agent-success-rate-tasks.json +671 -0
  5. package/benchmarks/projects/agent-success-inventory-node/.gitattributes +2 -0
  6. package/benchmarks/projects/agent-success-inventory-node/README.md +74 -0
  7. package/benchmarks/projects/agent-success-inventory-node/package.json +14 -0
  8. package/benchmarks/projects/agent-success-inventory-node/src/fulfillmentReport.js +38 -0
  9. package/benchmarks/projects/agent-success-inventory-node/src/index.js +26 -0
  10. package/benchmarks/projects/agent-success-inventory-node/src/inventoryStore.js +114 -0
  11. package/benchmarks/projects/agent-success-inventory-node/src/quantity.js +19 -0
  12. package/benchmarks/projects/agent-success-inventory-node/src/reservationService.js +45 -0
  13. package/benchmarks/projects/agent-success-inventory-node/tests/quantity-edge.check.mjs +51 -0
  14. package/benchmarks/projects/agent-success-inventory-node/tests/quantity-primary.check.mjs +46 -0
  15. package/benchmarks/projects/agent-success-inventory-node/tests/regression.check.mjs +115 -0
  16. package/benchmarks/projects/agent-success-inventory-node/tests/report-edge.check.mjs +109 -0
  17. package/benchmarks/projects/agent-success-inventory-node/tests/report-primary.check.mjs +47 -0
  18. package/benchmarks/projects/agent-success-inventory-node/tests/reservation-edge.check.mjs +63 -0
  19. package/benchmarks/projects/agent-success-inventory-node/tests/reservation-primary.check.mjs +44 -0
  20. package/benchmarks/projects/agent-success-task-board-node/.gitattributes +2 -0
  21. package/benchmarks/projects/agent-success-task-board-node/README.md +77 -0
  22. package/benchmarks/projects/agent-success-task-board-node/package.json +14 -0
  23. package/benchmarks/projects/agent-success-task-board-node/src/index.js +24 -0
  24. package/benchmarks/projects/agent-success-task-board-node/src/projectSummary.js +15 -0
  25. package/benchmarks/projects/agent-success-task-board-node/src/taskService.js +58 -0
  26. package/benchmarks/projects/agent-success-task-board-node/src/taskStore.js +58 -0
  27. package/benchmarks/projects/agent-success-task-board-node/src/validation.js +31 -0
  28. package/benchmarks/projects/agent-success-task-board-node/tests/import-edge.check.mjs +65 -0
  29. package/benchmarks/projects/agent-success-task-board-node/tests/import-primary.check.mjs +31 -0
  30. package/benchmarks/projects/agent-success-task-board-node/tests/regression.check.mjs +107 -0
  31. package/benchmarks/projects/agent-success-task-board-node/tests/summary-edge.check.mjs +67 -0
  32. package/benchmarks/projects/agent-success-task-board-node/tests/summary-primary.check.mjs +54 -0
  33. package/benchmarks/projects/agent-success-task-board-node/tests/title-edge.check.mjs +39 -0
  34. package/benchmarks/projects/agent-success-task-board-node/tests/title-primary.check.mjs +29 -0
  35. package/dist/src/cli/help.js +54 -2
  36. package/dist/src/commands/runExperimentDescribeCommand.js +60 -2
  37. package/dist/src/commands/runExperimentRunCommand.js +208 -5
  38. package/dist/src/core/processTree.js +55 -0
  39. package/dist/src/core/runMeasuredCommand.js +104 -22
  40. package/dist/src/evaluation/agentSuccess/applyPatchToSandbox.js +37 -0
  41. package/dist/src/evaluation/agentSuccess/assessBaseline.js +23 -0
  42. package/dist/src/evaluation/agentSuccess/corpusTypes.js +74 -0
  43. package/dist/src/evaluation/agentSuccess/extractPatchCandidate.js +46 -0
  44. package/dist/src/evaluation/agentSuccess/index.js +14 -0
  45. package/dist/src/evaluation/agentSuccess/parseUnifiedDiff.js +210 -0
  46. package/dist/src/evaluation/agentSuccess/patchTypes.js +5 -0
  47. package/dist/src/evaluation/agentSuccess/readAgentSuccessCorpus.js +183 -0
  48. package/dist/src/evaluation/agentSuccess/runVerificationCheck.js +100 -0
  49. package/dist/src/evaluation/agentSuccess/taskPaths.js +34 -0
  50. package/dist/src/evaluation/agentSuccess/taskTypes.js +14 -0
  51. package/dist/src/evaluation/agentSuccess/validateAgentSuccessCorpus.js +307 -0
  52. package/dist/src/evaluation/agentSuccess/validateAgentSuccessTask.js +297 -0
  53. package/dist/src/evaluation/agentSuccess/validatePatchPolicy.js +66 -0
  54. package/dist/src/evaluation/agentSuccess/verificationTypes.js +1 -0
  55. package/dist/src/evaluation/benchmarkSandbox/createBenchmarkSandbox.js +127 -0
  56. package/dist/src/evaluation/benchmarkSandbox/errors.js +11 -0
  57. package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +60 -0
  58. package/dist/src/evaluation/benchmarkSandbox/index.js +8 -0
  59. package/dist/src/evaluation/benchmarkSandbox/minimalHostEnv.js +37 -0
  60. package/dist/src/evaluation/benchmarkSandbox/pathPolicy.js +49 -0
  61. package/dist/src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.js +74 -0
  62. package/dist/src/evaluation/benchmarkSandbox/treeSnapshot.js +57 -0
  63. package/dist/src/evaluation/benchmarkSandbox/types.js +3 -0
  64. package/dist/src/evaluation/changeSet/captureChangeSet.js +98 -0
  65. package/dist/src/evaluation/changeSet/index.js +2 -0
  66. package/dist/src/evaluation/changeSet/types.js +9 -0
  67. package/dist/src/evaluation/runMyDevKitRetrieval.js +1 -0
  68. package/dist/src/experiments/defaultRegistry.js +4 -0
  69. package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
  70. package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
  71. package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
  72. package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
  73. package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
  74. package/dist/src/experiments/plugins/agentSuccessRate/config.js +123 -0
  75. package/dist/src/experiments/plugins/agentSuccessRate/contextGeneration.js +497 -0
  76. package/dist/src/experiments/plugins/agentSuccessRate/execution.js +441 -0
  77. package/dist/src/experiments/plugins/agentSuccessRate/executionArtifact.js +126 -0
  78. package/dist/src/experiments/plugins/agentSuccessRate/executionTypes.js +2 -0
  79. package/dist/src/experiments/plugins/agentSuccessRate/index.js +20 -0
  80. package/dist/src/experiments/plugins/agentSuccessRate/metadata.js +28 -0
  81. package/dist/src/experiments/plugins/agentSuccessRate/metrics.js +76 -0
  82. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +507 -0
  83. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +250 -0
  84. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +43 -0
  85. package/dist/src/experiments/plugins/agentSuccessRate/repairAnalysis.js +148 -0
  86. package/dist/src/experiments/plugins/agentSuccessRate/repairExecution.js +103 -0
  87. package/dist/src/experiments/plugins/agentSuccessRate/repairFeedback.js +91 -0
  88. package/dist/src/experiments/plugins/agentSuccessRate/repairPolicy.js +88 -0
  89. package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
  90. package/dist/src/experiments/plugins/contextPackGeneration/analysis.js +157 -0
  91. package/dist/src/experiments/plugins/contextPackGeneration/analysisArtifact.js +22 -0
  92. package/dist/src/experiments/plugins/contextPackGeneration/analysisTypes.js +1 -0
  93. package/dist/src/experiments/plugins/contextPackGeneration/buildContextPack.js +84 -0
  94. package/dist/src/experiments/plugins/contextPackGeneration/config.js +39 -0
  95. package/dist/src/experiments/plugins/contextPackGeneration/execution.js +361 -0
  96. package/dist/src/experiments/plugins/contextPackGeneration/executionArtifact.js +17 -0
  97. package/dist/src/experiments/plugins/contextPackGeneration/executionTypes.js +11 -0
  98. package/dist/src/experiments/plugins/contextPackGeneration/identityEvidence.js +71 -0
  99. package/dist/src/experiments/plugins/contextPackGeneration/index.js +19 -0
  100. package/dist/src/experiments/plugins/contextPackGeneration/localSubjectExecution.js +146 -0
  101. package/dist/src/experiments/plugins/contextPackGeneration/localSubjectPrivacy.js +276 -0
  102. package/dist/src/experiments/plugins/contextPackGeneration/metadata.js +24 -0
  103. package/dist/src/experiments/plugins/contextPackGeneration/metrics.js +46 -0
  104. package/dist/src/experiments/plugins/contextPackGeneration/packArtifact.js +17 -0
  105. package/dist/src/experiments/plugins/contextPackGeneration/packEvidence.js +152 -0
  106. package/dist/src/experiments/plugins/contextPackGeneration/packSelectionPolicy.js +305 -0
  107. package/dist/src/experiments/plugins/contextPackGeneration/plugin.js +355 -0
  108. package/dist/src/experiments/plugins/contextPackGeneration/renderContextPack.js +61 -0
  109. package/dist/src/experiments/plugins/contextPackGeneration/types.js +29 -0
  110. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
  111. package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
  112. package/dist/src/report/experiments/buildAgentSuccessRateReport.js +255 -0
  113. package/dist/src/report/experiments/buildContextPackGenerationReport.js +171 -0
  114. package/dist/src/report/experiments/buildPluginExperimentReport.js +49 -2
  115. package/dist/src/report/experiments/contextPackGenerationReportModel.js +9 -0
  116. package/dist/src/report/experiments/index.js +9 -0
  117. package/dist/src/report/experiments/loadContextPackArtifacts.js +43 -0
  118. package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +169 -0
  119. package/dist/src/report/experiments/renderAgentSuccessRateText.js +139 -0
  120. package/dist/src/report/experiments/renderContextPackGenerationHtml.js +122 -0
  121. package/dist/src/report/experiments/renderContextPackGenerationText.js +101 -0
  122. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +6 -0
  123. package/dist/src/report/experiments/renderPluginExperimentReportText.js +12 -0
  124. package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +15 -15
  125. package/dist/src/report/experiments/writePluginExperimentReports.js +2 -0
  126. package/docs/METRICS.md +98 -1
  127. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createInventorySystem } from "../src/index.js";
4
+
5
+ test("the report distinguishes active, fulfilled and cancelled reservations", () => {
6
+ const { store, service, report } = createInventorySystem();
7
+ store.addStock("widget", 10);
8
+ service.reserve({ reservationId: "r-1", sku: "widget", quantity: 3 });
9
+ service.reserve({ reservationId: "r-2", sku: "widget", quantity: 2 });
10
+ service.reserve({ reservationId: "r-3", sku: "widget", quantity: 1 });
11
+ service.cancel("r-1");
12
+ service.fulfill("r-2");
13
+ const [row] = report().skus;
14
+ assert.equal(row.sku, "widget");
15
+ assert.deepEqual(row.active, { count: 1, quantity: 1 });
16
+ assert.deepEqual(row.fulfilled, { count: 1, quantity: 2 });
17
+ assert.deepEqual(row.cancelled, { count: 1, quantity: 3 });
18
+ });
19
+
20
+ test("cancellation restores exactly the reserved quantity", () => {
21
+ const { store, service } = createInventorySystem();
22
+ store.addStock("widget", 10);
23
+ service.reserve({ reservationId: "r-1", sku: "widget", quantity: 4 });
24
+ assert.equal(store.getItem("widget").available, 6);
25
+ const cancelled = service.cancel("r-1");
26
+ assert.equal(cancelled.status, "cancelled");
27
+ assert.equal(store.getItem("widget").available, 10);
28
+ });
29
+
30
+ test("reported totals agree with the underlying store", () => {
31
+ const { store, service, report } = createInventorySystem();
32
+ store.addStock("widget", 10);
33
+ store.addStock("gadget", 6);
34
+ service.reserve({ reservationId: "r-1", sku: "widget", quantity: 3 });
35
+ service.reserve({ reservationId: "r-2", sku: "gadget", quantity: 2 });
36
+ service.reserve({ reservationId: "r-3", sku: "gadget", quantity: 1 });
37
+ service.cancel("r-1");
38
+ service.fulfill("r-2");
39
+ const result = report();
40
+ for (const row of result.skus) {
41
+ const item = store.getItem(row.sku);
42
+ assert.equal(row.available, item.available);
43
+ assert.equal(row.received, item.received);
44
+ assert.equal(row.received, row.available + row.active.quantity + row.fulfilled.quantity);
45
+ }
46
+ assert.deepEqual(result.totals, { received: 16, available: 13, active: 1, fulfilled: 2, cancelled: 3 });
47
+ });
@@ -0,0 +1,63 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createInventorySystem } from "../src/index.js";
4
+
5
+ test("a duplicate reservation id does not consume stock twice", () => {
6
+ const { store, service } = createInventorySystem();
7
+ store.addStock("widget", 5);
8
+ service.reserve({ reservationId: "r-1", sku: "widget", quantity: 2 });
9
+ assert.throws(() => service.reserve({ reservationId: "r-1", sku: "widget", quantity: 2 }), {
10
+ code: "DUPLICATE_RESERVATION"
11
+ });
12
+ assert.equal(store.getItem("widget").available, 3);
13
+ assert.equal(store.listReservations().length, 1);
14
+ });
15
+
16
+ test("a duplicate id for another sku changes neither sku nor the original record", () => {
17
+ const { store, service } = createInventorySystem();
18
+ store.addStock("widget", 5);
19
+ store.addStock("gadget", 5);
20
+ service.reserve({ reservationId: "r-1", sku: "widget", quantity: 2 });
21
+ assert.throws(() => service.reserve({ reservationId: "r-1", sku: "gadget", quantity: 4 }), {
22
+ code: "DUPLICATE_RESERVATION"
23
+ });
24
+ assert.equal(store.getItem("gadget").available, 5);
25
+ assert.equal(store.getItem("widget").available, 3);
26
+ assert.deepEqual(store.getReservation("r-1"), { id: "r-1", sku: "widget", quantity: 2, status: "active" });
27
+ });
28
+
29
+ test("a failed reservation id can be used again by a later valid reservation", () => {
30
+ const { store, service } = createInventorySystem();
31
+ store.addStock("widget", 3);
32
+ assert.throws(() => service.reserve({ reservationId: "r-9", sku: "widget", quantity: 99 }), {
33
+ code: "INSUFFICIENT_STOCK"
34
+ });
35
+ service.reserve({ reservationId: "r-9", sku: "widget", quantity: 1 });
36
+ assert.equal(store.getItem("widget").available, 2);
37
+ assert.equal(store.listReservations().length, 1);
38
+ });
39
+
40
+ test("unrelated inventory items stay unchanged through failures and successes", () => {
41
+ const { store, service } = createInventorySystem();
42
+ store.addStock("widget", 4);
43
+ store.addStock("gadget", 7);
44
+ const gadgetBefore = store.getItem("gadget");
45
+ assert.throws(() => service.reserve({ reservationId: "r-1", sku: "widget", quantity: 5 }), {
46
+ code: "INSUFFICIENT_STOCK"
47
+ });
48
+ service.reserve({ reservationId: "r-2", sku: "widget", quantity: 4 });
49
+ assert.deepEqual(store.getItem("gadget"), gadgetBefore);
50
+ assert.equal(store.getItem("widget").available, 0);
51
+ });
52
+
53
+ test("stock never goes negative after repeated failing attempts", () => {
54
+ const { store, service } = createInventorySystem();
55
+ store.addStock("widget", 1);
56
+ for (let attempt = 0; attempt < 3; attempt += 1) {
57
+ assert.throws(() => service.reserve({ reservationId: `r-${attempt}`, sku: "widget", quantity: 2 }), {
58
+ code: "INSUFFICIENT_STOCK"
59
+ });
60
+ }
61
+ assert.equal(store.getItem("widget").available, 1);
62
+ assert.deepEqual(store.listReservations(), []);
63
+ });
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createInventorySystem } from "../src/index.js";
4
+
5
+ test("a reservation that exceeds stock fails without consuming any stock", () => {
6
+ const { store, service } = createInventorySystem();
7
+ store.addStock("widget", 5);
8
+ assert.throws(() => service.reserve({ reservationId: "r-1", sku: "widget", quantity: 8 }), {
9
+ code: "INSUFFICIENT_STOCK"
10
+ });
11
+ assert.equal(store.getItem("widget").available, 5);
12
+ });
13
+
14
+ test("a failed reservation leaves no reservation record", () => {
15
+ const { store, service } = createInventorySystem();
16
+ store.addStock("widget", 2);
17
+ assert.throws(() => service.reserve({ reservationId: "r-1", sku: "widget", quantity: 3 }), {
18
+ code: "INSUFFICIENT_STOCK"
19
+ });
20
+ assert.throws(() => service.reserve({ reservationId: "r-2", sku: "missing", quantity: 1 }), {
21
+ code: "UNKNOWN_SKU"
22
+ });
23
+ assert.deepEqual(store.listReservations(), []);
24
+ });
25
+
26
+ test("a successful reservation decrements stock exactly once", () => {
27
+ const { store, service } = createInventorySystem();
28
+ store.addStock("widget", 5);
29
+ const reservation = service.reserve({ reservationId: "r-1", sku: "widget", quantity: 3 });
30
+ assert.deepEqual(reservation, { id: "r-1", sku: "widget", quantity: 3, status: "active" });
31
+ assert.equal(store.getItem("widget").available, 2);
32
+ assert.equal(store.listReservations().length, 1);
33
+ });
34
+
35
+ test("reserving exactly the available stock succeeds and leaves none", () => {
36
+ const { store, service } = createInventorySystem();
37
+ store.addStock("widget", 5);
38
+ service.reserve({ reservationId: "r-1", sku: "widget", quantity: 5 });
39
+ assert.equal(store.getItem("widget").available, 0);
40
+ assert.throws(() => service.reserve({ reservationId: "r-2", sku: "widget", quantity: 1 }), {
41
+ code: "INSUFFICIENT_STOCK"
42
+ });
43
+ assert.equal(store.getItem("widget").available, 0);
44
+ });
@@ -0,0 +1,2 @@
1
+ # Keep checked-out bytes identical on every platform so reference patches apply deterministically.
2
+ * text eol=lf
@@ -0,0 +1,77 @@
1
+ # agent-success-task-board-node
2
+
3
+ A small, dependency-free, in-memory task board written as JavaScript ES modules for Node.js `>=24`.
4
+
5
+ This project is an **implementation benchmark** for the `agent-success-rate` experiment of
6
+ `my-dev-kit-lab`. It is a controlled fixture: the Lab copies it into a disposable sandbox, asks an
7
+ implementation to change code under `src/`, and judges the result with trusted checks that the
8
+ implementation does not receive. The project is not a product and has no persistence, network access,
9
+ environment dependence, randomness or timestamps.
10
+
11
+ ## Behavior
12
+
13
+ The board models task creation, storage, external-task import, project ownership, completion and
14
+ per-project summaries. All state lives in memory inside one board instance; two boards never share state.
15
+
16
+ - A task has `id`, `title`, `projectId`, `externalId` (`null` for manually created tasks) and `completed`.
17
+ - Task ids are sequential per board: `task-1`, `task-2`, ... An operation that is rejected does not
18
+ consume an id and does not change the store.
19
+ - A **title** is a non-empty string once surrounding whitespace is removed. Surrounding whitespace is not
20
+ part of the stored title; interior whitespace is kept. Invalid titles fail with a `ValidationError`
21
+ whose `code` is `INVALID_TITLE`.
22
+ - A **project identity** is the project id trimmed and lower-cased. Tasks created without a project belong
23
+ to `inbox`. A blank project id fails with a `ValidationError` whose `code` is `INVALID_PROJECT`.
24
+ A project's tasks are exactly the tasks with that identity; other projects, including projects whose
25
+ names merely start with the same text, are unrelated.
26
+ - An **external id** identifies a task in an external system. It is compared after trimming and
27
+ lower-casing, and the normalized form is what the board stores. An import without a usable external id
28
+ fails with a `ValidationError` whose `code` is `INVALID_EXTERNAL_ID`.
29
+ - **Import** processes items in input order. An item whose external id is already known, from an earlier
30
+ call or earlier in the same call, is skipped instead of creating a duplicate.
31
+ - Tasks that were created manually are never removed by imports.
32
+ - A **summary** reports one project's `total`, `completed` and `open` counts and the ids of its tasks in
33
+ creation order. `completed` can never exceed `total`, and tasks of other projects never change a
34
+ project's summary.
35
+
36
+ ## Public API
37
+
38
+ Everything is exported from `src/index.js`.
39
+
40
+ | Export | Purpose |
41
+ | --- | --- |
42
+ | `createTaskBoard()` | Returns `{ store, service, summarize }` for a fresh, independent board. |
43
+ | `TaskService` | `createTask({ title, projectId? })`, `completeTask(id)`, `importTasks(items)`. |
44
+ | `TaskStore` | `nextId()`, `add(task)`, `get(id)`, `list()`, `update(id, changes)`, `findByExternalId(key)`. |
45
+ | `summarizeProject(store, projectId)` | Returns `{ projectId, total, completed, open, taskIds }`. |
46
+ | `validateTitle(title)` | Returns the normalized title or throws `ValidationError`. |
47
+ | `normalizeProjectId(projectId)` | Returns the normalized project identity or throws `ValidationError`. |
48
+ | `normalizeExternalId(externalId)` | Returns the identity key used to compare external ids. |
49
+ | `ValidationError`, `TaskNotFoundError` | Error classes with a stable `code` property. |
50
+ | `INVALID_TITLE`, `INVALID_PROJECT`, `INVALID_EXTERNAL_ID` | Validation error codes. |
51
+
52
+ `importTasks(items)` takes `{ externalId, title, projectId? }` items and returns
53
+ `{ imported, skipped }`: the created tasks, and the normalized external ids that were skipped.
54
+
55
+ Stores return copies of tasks, so changing a returned object never changes the board.
56
+
57
+ ## Layout
58
+
59
+ ```
60
+ src/validation.js title, project and error contracts
61
+ src/taskStore.js in-memory storage and external-id identity
62
+ src/taskService.js task creation, completion and import
63
+ src/projectSummary.js per-project summaries
64
+ src/index.js public exports and createTaskBoard()
65
+ tests/*.check.mjs trusted verification checks (owned by the Lab verifier)
66
+ ```
67
+
68
+ ## Running checks
69
+
70
+ ```
71
+ npm test
72
+ node --test tests/regression.check.mjs
73
+ ```
74
+
75
+ The files under `tests/` are named `*.check.mjs` on purpose and are run explicitly through Node's built-in
76
+ test runner. They are verification inputs owned by the Lab, not part of the implementation to change.
77
+ No installation step is needed.
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "agent-success-task-board-node",
3
+ "version": "0.0.0",
4
+ "description": "Dependency-free JavaScript implementation benchmark: an in-memory task board.",
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,24 @@
1
+ import { summarizeProject } from "./projectSummary.js";
2
+ import { TaskService } from "./taskService.js";
3
+ import { TaskStore } from "./taskStore.js";
4
+
5
+ export { summarizeProject } from "./projectSummary.js";
6
+ export { TaskService } from "./taskService.js";
7
+ export { TaskNotFoundError, TaskStore, normalizeExternalId } from "./taskStore.js";
8
+ export {
9
+ INVALID_EXTERNAL_ID,
10
+ INVALID_PROJECT,
11
+ INVALID_TITLE,
12
+ ValidationError,
13
+ normalizeProjectId,
14
+ validateTitle
15
+ } from "./validation.js";
16
+
17
+ /**
18
+ * Creates an independent in-memory board: a store, a service over it, and a summary helper.
19
+ */
20
+ export function createTaskBoard() {
21
+ const store = new TaskStore();
22
+ const service = new TaskService(store);
23
+ return { store, service, summarize: (projectId) => summarizeProject(store, projectId) };
24
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Summarizes the tasks of one project. Task ids are listed in creation order.
3
+ */
4
+ export function summarizeProject(store, projectId) {
5
+ const all = store.list();
6
+ const tasks = all.filter((task) => task.projectId.startsWith(projectId));
7
+ const completed = all.filter((task) => task.completed).length;
8
+ return {
9
+ projectId,
10
+ total: tasks.length,
11
+ completed,
12
+ open: tasks.length - completed,
13
+ taskIds: tasks.map((task) => task.id)
14
+ };
15
+ }
@@ -0,0 +1,58 @@
1
+ import { TaskNotFoundError, normalizeExternalId } from "./taskStore.js";
2
+ import { INVALID_EXTERNAL_ID, ValidationError, validateTitle } from "./validation.js";
3
+
4
+ const DEFAULT_PROJECT = "inbox";
5
+
6
+ export class TaskService {
7
+ constructor(store) {
8
+ this.store = store;
9
+ }
10
+
11
+ createTask({ title, projectId = DEFAULT_PROJECT } = {}) {
12
+ const normalizedTitle = validateTitle(title);
13
+ return this.store.add({
14
+ id: this.store.nextId(),
15
+ title: normalizedTitle,
16
+ projectId,
17
+ externalId: null,
18
+ completed: false
19
+ });
20
+ }
21
+
22
+ completeTask(id) {
23
+ if (!this.store.get(id)) {
24
+ throw new TaskNotFoundError(id);
25
+ }
26
+ return this.store.update(id, { completed: true });
27
+ }
28
+
29
+ /**
30
+ * Imports external tasks in input order. Returns the tasks that were created and the
31
+ * external identifiers that were skipped because the task was already imported.
32
+ */
33
+ importTasks(items) {
34
+ const imported = [];
35
+ const skipped = [];
36
+ for (const item of items) {
37
+ if (typeof item?.externalId !== "string" || item.externalId.trim() === "") {
38
+ throw new ValidationError(INVALID_EXTERNAL_ID, "external id must be a non-empty string");
39
+ }
40
+ const externalId = normalizeExternalId(item.externalId);
41
+ if (this.store.findByExternalId(item.externalId)) {
42
+ skipped.push(externalId);
43
+ continue;
44
+ }
45
+ const title = validateTitle(item.title);
46
+ imported.push(
47
+ this.store.add({
48
+ id: this.store.nextId(),
49
+ title,
50
+ projectId: item.projectId ?? DEFAULT_PROJECT,
51
+ externalId,
52
+ completed: false
53
+ })
54
+ );
55
+ }
56
+ return { imported, skipped };
57
+ }
58
+ }
@@ -0,0 +1,58 @@
1
+ export class TaskNotFoundError extends Error {
2
+ constructor(id) {
3
+ super(`task not found: ${id}`);
4
+ this.name = "TaskNotFoundError";
5
+ this.code = "TASK_NOT_FOUND";
6
+ }
7
+ }
8
+
9
+ /**
10
+ * Returns the identity key used to compare external task identifiers.
11
+ */
12
+ export function normalizeExternalId(externalId) {
13
+ return String(externalId).trim();
14
+ }
15
+
16
+ /**
17
+ * In-memory task storage. Tasks are returned as copies, in insertion order.
18
+ */
19
+ export class TaskStore {
20
+ #tasks = [];
21
+ #sequence = 0;
22
+
23
+ nextId() {
24
+ this.#sequence += 1;
25
+ return `task-${this.#sequence}`;
26
+ }
27
+
28
+ add(task) {
29
+ this.#tasks.push({ ...task });
30
+ return { ...task };
31
+ }
32
+
33
+ get(id) {
34
+ const task = this.#tasks.find((candidate) => candidate.id === id);
35
+ return task ? { ...task } : null;
36
+ }
37
+
38
+ list() {
39
+ return this.#tasks.map((task) => ({ ...task }));
40
+ }
41
+
42
+ update(id, changes) {
43
+ const task = this.#tasks.find((candidate) => candidate.id === id);
44
+ if (!task) {
45
+ throw new TaskNotFoundError(id);
46
+ }
47
+ Object.assign(task, changes);
48
+ return { ...task };
49
+ }
50
+
51
+ /**
52
+ * Finds a task by an already-normalized external identifier key.
53
+ */
54
+ findByExternalId(externalId) {
55
+ const task = this.#tasks.find((candidate) => candidate.externalId === externalId);
56
+ return task ? { ...task } : null;
57
+ }
58
+ }
@@ -0,0 +1,31 @@
1
+ export const INVALID_TITLE = "INVALID_TITLE";
2
+ export const INVALID_PROJECT = "INVALID_PROJECT";
3
+ export const INVALID_EXTERNAL_ID = "INVALID_EXTERNAL_ID";
4
+
5
+ export class ValidationError extends Error {
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.name = "ValidationError";
9
+ this.code = code;
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Returns the normalized title for a task, or throws ValidationError(INVALID_TITLE).
15
+ */
16
+ export function validateTitle(title) {
17
+ if (typeof title !== "string" || title.length === 0) {
18
+ throw new ValidationError(INVALID_TITLE, "title must be a non-empty string");
19
+ }
20
+ return title;
21
+ }
22
+
23
+ /**
24
+ * Returns the normalized project identity, or throws ValidationError(INVALID_PROJECT).
25
+ */
26
+ export function normalizeProjectId(projectId) {
27
+ if (typeof projectId !== "string") {
28
+ throw new ValidationError(INVALID_PROJECT, "project id must be a string");
29
+ }
30
+ return projectId.trim();
31
+ }
@@ -0,0 +1,65 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createTaskBoard } from "../src/index.js";
4
+
5
+ test("duplicates inside a single import call are collapsed to the first occurrence", () => {
6
+ const { service, store } = createTaskBoard();
7
+ const result = service.importTasks([
8
+ { externalId: "A-1", title: "First", projectId: "ops" },
9
+ { externalId: " a-1 ", title: "Second", projectId: "ops" },
10
+ { externalId: "b-2", title: "Third", projectId: "ops" }
11
+ ]);
12
+ assert.deepEqual(
13
+ result.imported.map((task) => task.title),
14
+ ["First", "Third"]
15
+ );
16
+ assert.deepEqual(result.skipped, ["a-1"]);
17
+ assert.equal(store.list().length, 2);
18
+ });
19
+
20
+ test("stored external identifiers use one normalized form", () => {
21
+ const { service, store } = createTaskBoard();
22
+ service.importTasks([
23
+ { externalId: " MiXeD-9 ", title: "Mixed", projectId: "ops" },
24
+ { externalId: "UPPER-1", title: "Upper", projectId: "ops" }
25
+ ]);
26
+ assert.deepEqual(
27
+ store.list().map((task) => task.externalId),
28
+ ["mixed-9", "upper-1"]
29
+ );
30
+ });
31
+
32
+ test("import order is deterministic and ids follow input order", () => {
33
+ const run = () => {
34
+ const { service } = createTaskBoard();
35
+ return service
36
+ .importTasks([
37
+ { externalId: "z-1", title: "Z", projectId: "ops" },
38
+ { externalId: "a-1", title: "A", projectId: "ops" },
39
+ { externalId: "Z-1", title: "Z duplicate", projectId: "ops" }
40
+ ])
41
+ .imported.map((task) => [task.id, task.title, task.externalId]);
42
+ };
43
+ assert.deepEqual(run(), [
44
+ ["task-1", "Z", "z-1"],
45
+ ["task-2", "A", "a-1"]
46
+ ]);
47
+ assert.deepEqual(run(), run());
48
+ });
49
+
50
+ test("manually created tasks are kept and do not block imports", () => {
51
+ const { service, store } = createTaskBoard();
52
+ const manual = service.createTask({ title: "Manual task", projectId: "ops" });
53
+ service.importTasks([{ externalId: "ext-1", title: "Imported", projectId: "ops" }]);
54
+ service.importTasks([{ externalId: "EXT-1", title: "Imported", projectId: "ops" }]);
55
+ const titles = store.list().map((task) => task.title);
56
+ assert.deepEqual(titles, ["Manual task", "Imported"]);
57
+ assert.equal(store.get(manual.id).externalId, null);
58
+ });
59
+
60
+ test("skipped identifiers are reported in normalized form", () => {
61
+ const { service } = createTaskBoard();
62
+ service.importTasks([{ externalId: "ext-5", title: "Five", projectId: "ops" }]);
63
+ const second = service.importTasks([{ externalId: " EXT-5", title: "Five", projectId: "ops" }]);
64
+ assert.deepEqual(second.skipped, ["ext-5"]);
65
+ });
@@ -0,0 +1,31 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createTaskBoard } from "../src/index.js";
4
+
5
+ test("reimporting the same external task does not create a duplicate", () => {
6
+ const { service, store } = createTaskBoard();
7
+ service.importTasks([{ externalId: "ext-1", title: "Alpha", projectId: "ops" }]);
8
+ const second = service.importTasks([{ externalId: "ext-1", title: "Alpha", projectId: "ops" }]);
9
+ assert.equal(second.imported.length, 0);
10
+ assert.equal(store.list().length, 1);
11
+ });
12
+
13
+ test("external identifiers are compared after normalization across import calls", () => {
14
+ const { service, store } = createTaskBoard();
15
+ service.importTasks([{ externalId: "EXT-7", title: "Alpha", projectId: "ops" }]);
16
+ const second = service.importTasks([
17
+ { externalId: " ext-7 ", title: "Alpha again", projectId: "ops" },
18
+ { externalId: "Ext-7", title: "Alpha once more", projectId: "ops" }
19
+ ]);
20
+ assert.equal(second.imported.length, 0);
21
+ assert.equal(second.skipped.length, 2);
22
+ assert.equal(store.list().length, 1);
23
+ });
24
+
25
+ test("new distinct external identifiers remain importable", () => {
26
+ const { service, store } = createTaskBoard();
27
+ service.importTasks([{ externalId: "ext-1", title: "Alpha", projectId: "ops" }]);
28
+ const second = service.importTasks([{ externalId: "EXT-2", title: "Beta", projectId: "ops" }]);
29
+ assert.equal(second.imported.length, 1);
30
+ assert.equal(store.list().length, 2);
31
+ });
@@ -0,0 +1,107 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ INVALID_EXTERNAL_ID,
5
+ INVALID_TITLE,
6
+ TaskNotFoundError,
7
+ createTaskBoard
8
+ } from "../src/index.js";
9
+
10
+ test("createTask stores an open task with a sequential id and a default project", () => {
11
+ const { service, store } = createTaskBoard();
12
+ const first = service.createTask({ title: "First" });
13
+ const second = service.createTask({ title: "Second", projectId: "ops" });
14
+ assert.deepEqual(first, { id: "task-1", title: "First", projectId: "inbox", externalId: null, completed: false });
15
+ assert.equal(second.id, "task-2");
16
+ assert.equal(second.projectId, "ops");
17
+ assert.deepEqual(
18
+ store.list().map((task) => task.id),
19
+ ["task-1", "task-2"]
20
+ );
21
+ });
22
+
23
+ test("missing, non-string and empty titles are rejected with the invalid-title error", () => {
24
+ const { service, store } = createTaskBoard();
25
+ for (const title of [undefined, null, 42, {}, ""]) {
26
+ assert.throws(() => service.createTask({ title }), { code: INVALID_TITLE });
27
+ }
28
+ assert.equal(store.list().length, 0);
29
+ });
30
+
31
+ test("already-trimmed titles are stored unchanged", () => {
32
+ const { service } = createTaskBoard();
33
+ assert.equal(service.createTask({ title: "Plain title" }).title, "Plain title");
34
+ });
35
+
36
+ test("completeTask marks a task completed, is idempotent and rejects unknown ids", () => {
37
+ const { service, store } = createTaskBoard();
38
+ const task = service.createTask({ title: "Finish me" });
39
+ assert.equal(service.completeTask(task.id).completed, true);
40
+ assert.equal(service.completeTask(task.id).completed, true);
41
+ assert.equal(store.get(task.id).completed, true);
42
+ assert.throws(() => service.completeTask("task-99"), (error) => error instanceof TaskNotFoundError);
43
+ });
44
+
45
+ test("returned tasks are copies and cannot mutate the store", () => {
46
+ const { service, store } = createTaskBoard();
47
+ const task = service.createTask({ title: "Immutable" });
48
+ task.title = "Changed";
49
+ store.list()[0].title = "Changed too";
50
+ assert.equal(store.get(task.id).title, "Immutable");
51
+ });
52
+
53
+ test("boards are independent of each other", () => {
54
+ const one = createTaskBoard();
55
+ const two = createTaskBoard();
56
+ one.service.createTask({ title: "Only in one" });
57
+ assert.equal(two.store.list().length, 0);
58
+ assert.equal(two.service.createTask({ title: "Fresh" }).id, "task-1");
59
+ });
60
+
61
+ test("importing the same external identifier twice does not duplicate it", () => {
62
+ const { service, store } = createTaskBoard();
63
+ const first = service.importTasks([{ externalId: "ext-1", title: "Imported", projectId: "ops" }]);
64
+ const second = service.importTasks([{ externalId: "ext-1", title: "Imported", projectId: "ops" }]);
65
+ assert.equal(first.imported.length, 1);
66
+ assert.deepEqual(second, { imported: [], skipped: ["ext-1"] });
67
+ assert.equal(store.list().length, 1);
68
+ });
69
+
70
+ test("distinct external identifiers import in input order and keep manual tasks", () => {
71
+ const { service, store } = createTaskBoard();
72
+ service.createTask({ title: "Manual", projectId: "ops" });
73
+ const { imported } = service.importTasks([
74
+ { externalId: "ext-2", title: "Two", projectId: "ops" },
75
+ { externalId: "ext-1", title: "One", projectId: "ops" }
76
+ ]);
77
+ assert.deepEqual(
78
+ imported.map((task) => task.externalId),
79
+ ["ext-2", "ext-1"]
80
+ );
81
+ assert.deepEqual(
82
+ store.list().map((task) => task.title),
83
+ ["Manual", "Two", "One"]
84
+ );
85
+ });
86
+
87
+ test("imports without a usable external identifier are rejected", () => {
88
+ const { service } = createTaskBoard();
89
+ for (const externalId of [undefined, null, "", " ", 7]) {
90
+ assert.throws(() => service.importTasks([{ externalId, title: "No id" }]), { code: INVALID_EXTERNAL_ID });
91
+ }
92
+ });
93
+
94
+ test("a single-project summary counts total, completed and open tasks", () => {
95
+ const { service, summarize } = createTaskBoard();
96
+ const a = service.createTask({ title: "A", projectId: "web" });
97
+ service.createTask({ title: "B", projectId: "web" });
98
+ service.createTask({ title: "C", projectId: "web" });
99
+ service.completeTask(a.id);
100
+ assert.deepEqual(summarize("web"), {
101
+ projectId: "web",
102
+ total: 3,
103
+ completed: 1,
104
+ open: 2,
105
+ taskIds: ["task-1", "task-2", "task-3"]
106
+ });
107
+ });