@dev.fast/whiteboard 0.5.0 → 0.5.2-preview.20261104.146

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 (117) hide show
  1. package/dist/{account-alias-Bw3PT3Qo.js → account-alias-BlYamXAG.js} +5 -5
  2. package/dist/{agent-cli-kHRrwO4n.js → agent-cli-D7fF5xmm.js} +9 -9
  3. package/dist/agent-cli-DLibAL4u.js +2 -0
  4. package/dist/{agent-client-CMacGbMp.js → agent-client-BCEbSPWU.js} +4 -4
  5. package/dist/{authoring-tools-CiLy46kh.js → authoring-tools-CkfxTVJm.js} +12 -8
  6. package/dist/{background-server-eZWXoVcI.js → background-server-BraP8pJ9.js} +4 -4
  7. package/dist/{background-server-BLNHbksB.js → background-server-CwToVN1g.js} +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-Bxmiqutp.js → cli-BpadBaGU.js} +1 -1
  10. package/dist/{cli-runner-DQPuu7kJ.js → cli-runner-BebYF50a.js} +22 -22
  11. package/dist/cli.js +2 -2
  12. package/dist/{fuzzy-match-DZjeOaPY.js → fuzzy-match-CxcsLQGx.js} +3 -3
  13. package/dist/{headless-host-Dl2KZ9IT.js → headless-host-FAlbekFD.js} +561 -415
  14. package/dist/{headless-host-B4r0s1ks.js → headless-host-cLipIufH.js} +1 -1
  15. package/dist/{import-YAIU6rB0.js → import-CZ1958L4.js} +234 -62
  16. package/dist/{local-data-C58vzLZR.js → local-data-DdFkxFK_.js} +41 -16
  17. package/dist/{mcp-COYWiGVn.js → mcp-Claiy9xE.js} +9 -6
  18. package/dist/{package-paths-Cg6fnaGp.js → package-paths-Dk7MOXxN.js} +1 -1
  19. package/dist/{profile--BYIjPkc.js → profile-Bsgzi9PA.js} +2 -2
  20. package/dist/profile-BxdzBxHW.js +2 -0
  21. package/dist/{remote-attach-C1prLjmj.js → remote-attach-LGTpwpUv.js} +4 -4
  22. package/dist/{remote-extensions-Bw029iIS.js → remote-extensions-BvV29_gk.js} +1 -1
  23. package/dist/{remote-extensions-MUUgbLJn.js → remote-extensions-L95HNwIH.js} +2 -2
  24. package/dist/{remote-language-server-z3rdHcLL.js → remote-language-server-BAznkpeu.js} +4 -4
  25. package/dist/{remote-uninstall-B3Slwor1.js → remote-uninstall-CLP79Cuk.js} +5 -5
  26. package/dist/{request-origin-B3_XI2VA.js → request-origin-D6B-zrL8.js} +27 -5
  27. package/dist/{review-app-launcher-BZEbDKuK.js → review-app-launcher-tTe8bTwg.js} +2 -2
  28. package/dist/{review-home-paths-LdGS4SpR.js → review-home-paths-BmZX2qiz.js} +1 -1
  29. package/dist/{review-telemetry-x8u-9EZb.js → review-telemetry-DACDMfqq.js} +4 -4
  30. package/dist/server/desktop-host.js +26 -38
  31. package/dist/{server-discovery-WmuFl2S3.js → server-discovery-CV5qf1J8.js} +2 -2
  32. package/dist/sharing/index.d.ts +137 -14
  33. package/dist/sharing/index.js +1 -1
  34. package/dist/{src-B2b8cCoI.js → src-BQDlJXkG.js} +44 -5
  35. package/dist/{src-DLWol7GZ.js → src-CrrW7lcY.js} +16 -1
  36. package/dist/{stored-document-migration-iwIQ4l5G.js → stored-document-migration-D1wK4yID.js} +3 -3
  37. package/dist/{thread-state-B7L1mpN_.js → thread-state-WTqSborE.js} +1 -1
  38. package/dist/{tool-failure-RLR8m9hq.js → tool-failure-YOFGHJCZ.js} +319 -55
  39. package/instructions/animation.md +69 -5
  40. package/instructions/animations/before-after-structure/README.md +27 -0
  41. package/instructions/animations/before-after-structure/animation.html +89 -0
  42. package/instructions/animations/before-after-structure/block.json +10 -0
  43. package/instructions/animations/before-after-structure/frames.json +313 -0
  44. package/instructions/animations/cache-fill-evict/README.md +24 -0
  45. package/instructions/animations/cache-fill-evict/animation.html +81 -0
  46. package/instructions/animations/cache-fill-evict/block.json +13 -0
  47. package/instructions/animations/cache-fill-evict/frames.json +79 -0
  48. package/instructions/animations/cache-fill-evict/scenarios.json +17 -0
  49. package/instructions/animations/log-replay/README.md +25 -0
  50. package/instructions/animations/log-replay/animation.html +86 -0
  51. package/instructions/animations/log-replay/block.json +10 -0
  52. package/instructions/animations/log-replay/frames.json +143 -0
  53. package/instructions/animations/log-replay/scenarios.json +18 -0
  54. package/instructions/animations/queue-workers/README.md +23 -0
  55. package/instructions/animations/queue-workers/animation.html +107 -0
  56. package/instructions/animations/queue-workers/block.json +13 -0
  57. package/instructions/animations/queue-workers/frames.json +119 -0
  58. package/instructions/animations/queue-workers/scenarios.json +18 -0
  59. package/instructions/animations/race-two-lanes/README.md +22 -0
  60. package/instructions/animations/race-two-lanes/animation.html +97 -0
  61. package/instructions/animations/race-two-lanes/block.json +1 -0
  62. package/instructions/animations/race-two-lanes/frames.json +234 -0
  63. package/instructions/animations/race-two-lanes/scenarios.json +19 -0
  64. package/instructions/animations/request-across-layers/README.md +24 -0
  65. package/instructions/animations/request-across-layers/animation.html +85 -0
  66. package/instructions/animations/request-across-layers/block.json +17 -0
  67. package/instructions/animations/request-across-layers/frames.json +58 -0
  68. package/instructions/animations/request-across-layers/scenarios.json +12 -0
  69. package/instructions/animations/request-timing/README.md +22 -0
  70. package/instructions/animations/request-timing/animation.html +120 -0
  71. package/instructions/animations/request-timing/block.json +21 -0
  72. package/instructions/animations/request-timing/markers.json +32 -0
  73. package/instructions/animations/request-timing/scenarios.json +4 -0
  74. package/instructions/animations/retry-backoff/README.md +22 -0
  75. package/instructions/animations/retry-backoff/animation.html +122 -0
  76. package/instructions/animations/retry-backoff/block.json +10 -0
  77. package/instructions/animations/retry-backoff/frames.json +65 -0
  78. package/instructions/animations/retry-backoff/scenarios.json +12 -0
  79. package/instructions/animations/state-machine/README.md +24 -0
  80. package/instructions/animations/state-machine/animation.html +95 -0
  81. package/instructions/animations/state-machine/block.json +9 -0
  82. package/instructions/animations/state-machine/frames.json +79 -0
  83. package/instructions/animations/state-machine/scenarios.json +12 -0
  84. package/instructions/animations/video-platform-architecture/README.md +24 -0
  85. package/instructions/animations/video-platform-architecture/animation.html +76 -0
  86. package/instructions/animations/video-platform-architecture/block.json +17 -0
  87. package/instructions/animations/video-platform-architecture/frames.json +97 -0
  88. package/instructions/animations/video-platform-architecture/scenarios.json +28 -0
  89. package/instructions/whiteboard.d.ts +168 -14
  90. package/package.json +6 -6
  91. package/src/animation-stage-kit.ts +150 -10
  92. package/src/cli-runner.ts +4 -1
  93. package/src/fixtures/blocks/clip.json +19 -0
  94. package/src/review-api/README.md +1 -0
  95. package/src/review-api/agent-cli.ts +3 -2
  96. package/src/review-api/animation-drafts.ts +151 -47
  97. package/src/review-api/animation-references.ts +92 -0
  98. package/src/review-api/animation-render.ts +106 -0
  99. package/src/review-api/authoring-tools.ts +10 -3
  100. package/src/review-api/document-text.ts +27 -12
  101. package/src/review-api/http.ts +52 -6
  102. package/src/review-api/instructions.ts +1 -1
  103. package/src/review-api/mcp.ts +10 -5
  104. package/src/review-api/public-tools.ts +71 -2
  105. package/src/review-api/store.ts +89 -12
  106. package/src/server/desktop-server.ts +17 -5
  107. package/src/server/headless-host.ts +4 -0
  108. package/src/server/review-gateway.ts +1 -0
  109. package/src/server/review-server-core.ts +71 -12
  110. package/src/sharing/client.ts +129 -32
  111. package/src/sharing/export.ts +42 -5
  112. package/src/sharing/file.ts +8 -0
  113. package/src/sharing/host.ts +202 -73
  114. package/src/sharing/import.ts +125 -33
  115. package/src/ui-telemetry-events.ts +25 -1
  116. package/dist/agent-cli-B8KBHuh_.js +0 -2
  117. package/dist/profile-BjcXi-oH.js +0 -2
@@ -0,0 +1,119 @@
1
+ [
2
+ {
3
+ "key": "steady-put",
4
+ "title": "The producer enqueues two jobs",
5
+ "label": "PUT",
6
+ "caption": "j1 and j2 wait in the queue; both workers are idle.",
7
+ "data": {
8
+ "producer": { "next": "j3", "state": "ready" },
9
+ "queue": ["j1", "j2"],
10
+ "bound": 4,
11
+ "workers": [null, null],
12
+ "done": []
13
+ }
14
+ },
15
+ {
16
+ "key": "steady-take",
17
+ "title": "Each worker takes one",
18
+ "label": "TAKE",
19
+ "caption": "Workers take from the head of the queue, oldest first, one job each.",
20
+ "data": {
21
+ "producer": { "next": "j3", "state": "ready" },
22
+ "queue": [],
23
+ "bound": 4,
24
+ "workers": ["j1", "j2"],
25
+ "done": []
26
+ }
27
+ },
28
+ {
29
+ "key": "steady-refill",
30
+ "title": "More jobs arrive",
31
+ "label": "PUT",
32
+ "caption": "The producer keeps putting while the workers run. Two of four slots are used.",
33
+ "data": {
34
+ "producer": { "next": "j5", "state": "ready" },
35
+ "queue": ["j3", "j4"],
36
+ "bound": 4,
37
+ "workers": ["j1", "j2"],
38
+ "done": []
39
+ }
40
+ },
41
+ {
42
+ "key": "steady-done",
43
+ "title": "A worker finishes and takes the next",
44
+ "label": "DONE",
45
+ "caption": "Worker 1 completes j1 and takes j3. Under steady load the queue stays short and never reaches its bound.",
46
+ "data": {
47
+ "producer": { "next": "j5", "state": "ready" },
48
+ "queue": ["j4"],
49
+ "bound": 4,
50
+ "workers": ["j3", "j2"],
51
+ "done": ["j1"]
52
+ }
53
+ },
54
+ {
55
+ "key": "burst-busy",
56
+ "title": "Both workers are busy",
57
+ "label": "BUSY",
58
+ "caption": "j1 and j2 are running and j3 waits. The producer is about to send a burst.",
59
+ "data": {
60
+ "producer": { "next": "j4", "state": "ready" },
61
+ "queue": ["j3"],
62
+ "bound": 4,
63
+ "workers": ["j1", "j2"],
64
+ "done": []
65
+ }
66
+ },
67
+ {
68
+ "key": "burst-fill",
69
+ "title": "The burst fills the queue",
70
+ "label": "FILL",
71
+ "caption": "j4 to j6 arrive faster than the workers finish. The queue holds four jobs: its bound.",
72
+ "data": {
73
+ "producer": { "next": "j7", "state": "ready" },
74
+ "queue": ["j3", "j4", "j5", "j6"],
75
+ "bound": 4,
76
+ "workers": ["j1", "j2"],
77
+ "done": []
78
+ }
79
+ },
80
+ {
81
+ "key": "burst-wait",
82
+ "title": "put() blocks the producer",
83
+ "label": "BACKPRESSURE",
84
+ "caption": "The queue is full, so put(j7) waits instead of growing the queue. The producer slows to the workers' pace.",
85
+ "data": {
86
+ "producer": { "next": "j7", "state": "blocked" },
87
+ "queue": ["j3", "j4", "j5", "j6"],
88
+ "bound": 4,
89
+ "workers": ["j1", "j2"],
90
+ "done": []
91
+ }
92
+ },
93
+ {
94
+ "key": "burst-drain",
95
+ "title": "A worker frees a slot",
96
+ "label": "TAKE",
97
+ "caption": "Worker 1 completes j1 and takes j3. That frees a slot and wakes the blocked put.",
98
+ "data": {
99
+ "producer": { "next": "j7", "state": "woken" },
100
+ "queue": ["j4", "j5", "j6"],
101
+ "bound": 4,
102
+ "workers": ["j3", "j2"],
103
+ "done": ["j1"]
104
+ }
105
+ },
106
+ {
107
+ "key": "burst-resume",
108
+ "title": "The waiting put goes through",
109
+ "label": "RESUME",
110
+ "caption": "j7 takes the free slot and the queue is full again, so put(j8) blocks until the next completion.",
111
+ "data": {
112
+ "producer": { "next": "j8", "state": "blocked" },
113
+ "queue": ["j4", "j5", "j6", "j7"],
114
+ "bound": 4,
115
+ "workers": ["j3", "j2"],
116
+ "done": ["j1"]
117
+ }
118
+ }
119
+ ]
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "key": "steady",
4
+ "label": "Steady load",
5
+ "frames": ["steady-put", "steady-take", "steady-refill", "steady-done"]
6
+ },
7
+ {
8
+ "key": "burst",
9
+ "label": "Burst hits the bound",
10
+ "frames": [
11
+ "burst-busy",
12
+ "burst-fill",
13
+ "burst-wait",
14
+ "burst-drain",
15
+ "burst-resume"
16
+ ]
17
+ }
18
+ ]
@@ -0,0 +1,22 @@
1
+ # Lost update between two writers
2
+
3
+ Two actors read, modify and write one shared row in interleaved steps on two lanes, losing an update without a lock and serializing behind it with one.
4
+
5
+ ## Use when
6
+
7
+ A change touches a read-modify-write on shared state, such as a balance, a counter or a JSON blob saved back whole, or adds or removes a lock, a transaction or a version check around it, and the reviewer has to judge whether two concurrent callers can clobber each other.
8
+
9
+ ## Shows
10
+
11
+ Each actor's steps appear on its own lane in time columns, so the interleaving reads left to right, and its card shows the value it holds locally. The row below changes as writes land. Without a lock, both read 100 and B's write of 120 erases A's 150. With a lock, B's blocked bar grows until A writes and releases, then B reads 150 and writes 170.
12
+
13
+ ## Technique
14
+
15
+ Data-driven: each frame's `data` holds the full state (the column count, every step so far with its lane, column, span and tone, each actor's local value, and the row), and one `whiteboard.on("*", ...)` handler redraws it all. It compares with `previous.data` to animate only what changed: a new step slides in, a wait that lasted another step grows from its old width, a step whose tone changed fades up, and a changed cell slides in, all through `whiteboard.animate`, so jumps and reduced motion draw the end state.
16
+
17
+ ## Adapt
18
+
19
+ - Rename the actors in the two cards and rewrite the step `text` for your operation: an increment, a cache refill, a config file saved back.
20
+ - Change the row: its columns are the keys of `record.row`, so add a `version` column to show optimistic concurrency, and add a scenario where B's write is rejected and retried.
21
+ - Keep `columns` the same across a scenario's frames so the lanes do not reflow as steps arrive.
22
+ - A new frame or scenario is only new entries in frames.json and scenarios.json; animation.html stays as it is.
@@ -0,0 +1,97 @@
1
+ <div class="race">
2
+ <div class="wb-card" data-actor="A">
3
+ <span class="wb-card-title">A · deposit +50</span>
4
+ <span class="wb-card-note"></span>
5
+ </div>
6
+ <div class="wb-lane steps" data-actor="A"></div>
7
+ <div class="wb-card" data-actor="B">
8
+ <span class="wb-card-title">B · deposit +20</span>
9
+ <span class="wb-card-note"></span>
10
+ </div>
11
+ <div class="wb-lane steps" data-actor="B"></div>
12
+ <div class="record">
13
+ <span class="wb-caps">accounts</span>
14
+ <table class="wb-table"><thead><tr></tr></thead><tbody><tr></tr></tbody></table>
15
+ </div>
16
+ </div>
17
+
18
+ <style>
19
+ .race {
20
+ display: grid;
21
+ grid-template-columns: max-content minmax(0, 1fr);
22
+ align-items: center;
23
+ gap: var(--wb-space-16);
24
+ padding: var(--wb-space-20) var(--wb-space-24);
25
+ }
26
+ .steps {
27
+ display: grid;
28
+ grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
29
+ align-items: center;
30
+ gap: var(--wb-space-4);
31
+ }
32
+ .steps .wb-chip { position: relative; justify-self: center; max-width: 100%; text-align: center; }
33
+ .steps .wait { justify-self: stretch; transform-origin: left; }
34
+ .record {
35
+ grid-column: 1 / -1;
36
+ justify-self: center;
37
+ width: min(100%, calc(var(--wb-space-160) * 2));
38
+ margin-top: var(--wb-space-12);
39
+ }
40
+ </style>
41
+
42
+ <script>
43
+ const lanes = document.querySelectorAll(".steps");
44
+ const head = document.querySelector(".record thead tr");
45
+ const row = document.querySelector(".record tbody tr");
46
+
47
+ const cell = (tag, text) => {
48
+ const element = document.createElement(tag);
49
+ element.textContent = text;
50
+ return element;
51
+ };
52
+
53
+ const slideIn = (element) =>
54
+ whiteboard.animate(element, [{ opacity: 0, transform: "translateY(-4px)" }, { opacity: 1, transform: "none" }], 300);
55
+
56
+ whiteboard.on("*", ({ frame, previous }) => {
57
+ const { columns, steps, local, record } = frame.data;
58
+ const before = previous?.data ?? { steps: [], record: { row: {} } };
59
+ const animations = [];
60
+
61
+ for (const lane of lanes) {
62
+ lane.style.setProperty("--columns", columns);
63
+ lane.replaceChildren();
64
+ }
65
+
66
+ steps.forEach((step, index) => {
67
+ const chip = cell("span", step.text);
68
+ chip.className = step.wait ? "wb-chip wait" : "wb-chip";
69
+ chip.style.gridColumn = `${step.at} / span ${step.span ?? 1}`;
70
+ if (step.tone) chip.dataset.tone = step.tone;
71
+ document.querySelector(`.steps[data-actor="${step.actor}"]`).append(chip);
72
+
73
+ const old = before.steps[index];
74
+ if (!old) animations.push(slideIn(chip));
75
+ // A wait that lasts into this frame grows from where it ended.
76
+ else if ((old.span ?? 1) < (step.span ?? 1))
77
+ animations.push(whiteboard.animate(chip, [{ transform: `scaleX(${(old.span ?? 1) / step.span})` }, { transform: "none" }], 400));
78
+ else if (old.tone !== step.tone) animations.push(whiteboard.animate(chip, [{ opacity: 0.3 }, { opacity: 1 }], 400));
79
+ });
80
+
81
+ for (const [actor, value] of Object.entries(local))
82
+ document.querySelector(`.wb-card[data-actor="${actor}"] .wb-card-note`).textContent = `local ${value}`;
83
+
84
+ head.replaceChildren(...Object.keys(record.row).map((key) => cell("th", key)));
85
+ row.replaceChildren(
86
+ ...Object.entries(record.row).map(([key, value]) => {
87
+ const td = cell("td", value);
88
+ if (before.record.row[key] !== undefined && before.record.row[key] !== value) animations.push(slideIn(td));
89
+ return td;
90
+ }),
91
+ );
92
+ if (record.tone) row.dataset.tone = record.tone;
93
+ else delete row.dataset.tone;
94
+
95
+ return animations;
96
+ });
97
+ </script>
@@ -0,0 +1 @@
1
+ { "title": "Lost update between two writers" }
@@ -0,0 +1,234 @@
1
+ [
2
+ {
3
+ "key": "n-start",
4
+ "title": "Two deposits, one row",
5
+ "label": "START",
6
+ "caption": "Account 7 holds 100. A deposits 50 and B deposits 20, each by reading the balance, adding, and writing it back, with no lock.",
7
+ "data": {
8
+ "columns": 6,
9
+ "steps": [],
10
+ "local": { "A": "—", "B": "—" },
11
+ "record": { "row": { "id": "acct 7", "balance": "100" } }
12
+ }
13
+ },
14
+ {
15
+ "key": "n-read",
16
+ "title": "Both read 100",
17
+ "label": "BOTH READ",
18
+ "caption": "A reads 100, then B reads 100 before A has written. Each now holds its own copy.",
19
+ "data": {
20
+ "columns": 6,
21
+ "steps": [
22
+ { "actor": "A", "at": 1, "text": "read 100" },
23
+ { "actor": "B", "at": 2, "text": "read 100" }
24
+ ],
25
+ "local": { "A": "100", "B": "100" },
26
+ "record": { "row": { "id": "acct 7", "balance": "100" } }
27
+ }
28
+ },
29
+ {
30
+ "key": "n-modify",
31
+ "title": "Both add locally",
32
+ "label": "BOTH ADD",
33
+ "caption": "A computes 150 and B computes 120, each from the 100 it read. The row has not changed yet.",
34
+ "data": {
35
+ "columns": 6,
36
+ "steps": [
37
+ { "actor": "A", "at": 1, "text": "read 100" },
38
+ { "actor": "B", "at": 2, "text": "read 100" },
39
+ { "actor": "A", "at": 3, "text": "+50 → 150" },
40
+ { "actor": "B", "at": 4, "text": "+20 → 120" }
41
+ ],
42
+ "local": { "A": "150", "B": "120" },
43
+ "record": { "row": { "id": "acct 7", "balance": "100" } }
44
+ }
45
+ },
46
+ {
47
+ "key": "n-write-a",
48
+ "title": "A writes 150",
49
+ "label": "A WRITES",
50
+ "caption": "The row now holds 150, A's deposit included.",
51
+ "data": {
52
+ "columns": 6,
53
+ "steps": [
54
+ { "actor": "A", "at": 1, "text": "read 100" },
55
+ { "actor": "B", "at": 2, "text": "read 100" },
56
+ { "actor": "A", "at": 3, "text": "+50 → 150" },
57
+ { "actor": "B", "at": 4, "text": "+20 → 120" },
58
+ { "actor": "A", "at": 5, "text": "write 150", "tone": "modified" }
59
+ ],
60
+ "local": { "A": "150", "B": "120" },
61
+ "record": {
62
+ "row": { "id": "acct 7", "balance": "150" },
63
+ "tone": "modified"
64
+ }
65
+ }
66
+ },
67
+ {
68
+ "key": "n-write-b",
69
+ "title": "B overwrites with 120",
70
+ "label": "LOST UPDATE",
71
+ "caption": "B writes the 120 it computed from a stale read and clobbers A's write. The row should hold 170; A's 50 is lost.",
72
+ "data": {
73
+ "columns": 6,
74
+ "steps": [
75
+ { "actor": "A", "at": 1, "text": "read 100" },
76
+ { "actor": "B", "at": 2, "text": "read 100" },
77
+ { "actor": "A", "at": 3, "text": "+50 → 150" },
78
+ { "actor": "B", "at": 4, "text": "+20 → 120" },
79
+ { "actor": "A", "at": 5, "text": "write 150", "tone": "removed" },
80
+ { "actor": "B", "at": 6, "text": "write 120", "tone": "modified" }
81
+ ],
82
+ "local": { "A": "150", "B": "120" },
83
+ "record": {
84
+ "row": { "id": "acct 7", "balance": "120" },
85
+ "tone": "removed"
86
+ }
87
+ }
88
+ },
89
+ {
90
+ "key": "l-start",
91
+ "title": "Two deposits, one locked row",
92
+ "label": "START",
93
+ "caption": "Account 7 holds 100. This time each actor locks the row before it reads, and releases the lock when it writes.",
94
+ "data": {
95
+ "columns": 7,
96
+ "steps": [],
97
+ "local": { "A": "—", "B": "—" },
98
+ "record": { "row": { "id": "acct 7", "balance": "100", "lock": "free" } }
99
+ }
100
+ },
101
+ {
102
+ "key": "l-lock",
103
+ "title": "A locks and reads 100",
104
+ "label": "A LOCKS",
105
+ "caption": "A takes the row lock and reads 100. B asks for the same lock and blocks.",
106
+ "data": {
107
+ "columns": 7,
108
+ "steps": [
109
+ { "actor": "A", "at": 1, "text": "read 100" },
110
+ {
111
+ "actor": "B",
112
+ "at": 2,
113
+ "text": "blocked",
114
+ "span": 1,
115
+ "wait": true,
116
+ "tone": "muted"
117
+ }
118
+ ],
119
+ "local": { "A": "100", "B": "—" },
120
+ "record": {
121
+ "row": { "id": "acct 7", "balance": "100", "lock": "held by A" }
122
+ }
123
+ }
124
+ },
125
+ {
126
+ "key": "l-modify",
127
+ "title": "A adds while B waits",
128
+ "label": "A ADDS",
129
+ "caption": "A computes 150. B is still blocked, so it cannot read the old balance.",
130
+ "data": {
131
+ "columns": 7,
132
+ "steps": [
133
+ { "actor": "A", "at": 1, "text": "read 100" },
134
+ {
135
+ "actor": "B",
136
+ "at": 2,
137
+ "text": "blocked",
138
+ "span": 2,
139
+ "wait": true,
140
+ "tone": "muted"
141
+ },
142
+ { "actor": "A", "at": 3, "text": "+50 → 150" }
143
+ ],
144
+ "local": { "A": "150", "B": "—" },
145
+ "record": {
146
+ "row": { "id": "acct 7", "balance": "100", "lock": "held by A" }
147
+ }
148
+ }
149
+ },
150
+ {
151
+ "key": "l-write-a",
152
+ "title": "A writes and unlocks",
153
+ "label": "A WRITES",
154
+ "caption": "A writes 150 and releases the lock, which wakes B.",
155
+ "data": {
156
+ "columns": 7,
157
+ "steps": [
158
+ { "actor": "A", "at": 1, "text": "read 100" },
159
+ {
160
+ "actor": "B",
161
+ "at": 2,
162
+ "text": "blocked",
163
+ "span": 3,
164
+ "wait": true,
165
+ "tone": "muted"
166
+ },
167
+ { "actor": "A", "at": 3, "text": "+50 → 150" },
168
+ { "actor": "A", "at": 4, "text": "write 150", "tone": "modified" }
169
+ ],
170
+ "local": { "A": "150", "B": "—" },
171
+ "record": {
172
+ "row": { "id": "acct 7", "balance": "150", "lock": "free" },
173
+ "tone": "modified"
174
+ }
175
+ }
176
+ },
177
+ {
178
+ "key": "l-read-b",
179
+ "title": "B reads A's write",
180
+ "label": "B READS",
181
+ "caption": "B takes the lock and reads 150: it sees A's deposit because it had to wait for it.",
182
+ "data": {
183
+ "columns": 7,
184
+ "steps": [
185
+ { "actor": "A", "at": 1, "text": "read 100" },
186
+ {
187
+ "actor": "B",
188
+ "at": 2,
189
+ "text": "blocked",
190
+ "span": 3,
191
+ "wait": true,
192
+ "tone": "muted"
193
+ },
194
+ { "actor": "A", "at": 3, "text": "+50 → 150" },
195
+ { "actor": "A", "at": 4, "text": "write 150" },
196
+ { "actor": "B", "at": 5, "text": "read 150" }
197
+ ],
198
+ "local": { "A": "150", "B": "150" },
199
+ "record": {
200
+ "row": { "id": "acct 7", "balance": "150", "lock": "held by B" }
201
+ }
202
+ }
203
+ },
204
+ {
205
+ "key": "l-write-b",
206
+ "title": "B writes 170",
207
+ "label": "B WRITES",
208
+ "caption": "B writes 170 and releases the lock. Both deposits are in the row: the lock turned the race into a queue.",
209
+ "data": {
210
+ "columns": 7,
211
+ "steps": [
212
+ { "actor": "A", "at": 1, "text": "read 100" },
213
+ {
214
+ "actor": "B",
215
+ "at": 2,
216
+ "text": "blocked",
217
+ "span": 3,
218
+ "wait": true,
219
+ "tone": "muted"
220
+ },
221
+ { "actor": "A", "at": 3, "text": "+50 → 150" },
222
+ { "actor": "A", "at": 4, "text": "write 150" },
223
+ { "actor": "B", "at": 5, "text": "read 150" },
224
+ { "actor": "B", "at": 6, "text": "+20 → 170" },
225
+ { "actor": "B", "at": 7, "text": "write 170", "tone": "modified" }
226
+ ],
227
+ "local": { "A": "150", "B": "170" },
228
+ "record": {
229
+ "row": { "id": "acct 7", "balance": "170", "lock": "free" },
230
+ "tone": "modified"
231
+ }
232
+ }
233
+ }
234
+ ]
@@ -0,0 +1,19 @@
1
+ [
2
+ {
3
+ "key": "no-lock",
4
+ "label": "Without a lock",
5
+ "frames": ["n-start", "n-read", "n-modify", "n-write-a", "n-write-b"]
6
+ },
7
+ {
8
+ "key": "lock",
9
+ "label": "With a row lock",
10
+ "frames": [
11
+ "l-start",
12
+ "l-lock",
13
+ "l-modify",
14
+ "l-write-a",
15
+ "l-read-b",
16
+ "l-write-b"
17
+ ]
18
+ }
19
+ ]
@@ -0,0 +1,24 @@
1
+ # Request across layers
2
+
3
+ One request crosses client, API, service and database and back, and each card shows the payload in the shape that layer holds, with a branch where validation stops it at the API.
4
+
5
+ ## Use when
6
+
7
+ A change touches how a request is parsed, validated, mapped or persisted, and the reviewer needs to see which layer owns which shape and where a bad input is turned away.
8
+
9
+ ## Shows
10
+
11
+ - Valid order: a JSON body with a string `qty` becomes a typed DTO, a domain Order without an id, a row with one, an Order again, and a 201 body.
12
+ - Validation fails: the API answers 422 naming the field; the service and the database are marked not called.
13
+ - The card doing the work is ringed in each frame; its note keeps the shape it produced, so earlier shapes stay readable.
14
+
15
+ ## Technique
16
+
17
+ A scene: `whiteboard.scene({ columns: 4 })` with four `actor`s, solid `link`s for requests and dashed ones for replies, one `send` per hop per frame, and `on(frame, card, { text, tone })` for the payload shape. Each card wears its layer's `form`: a `window` for the client, `bars` for the API and the service, a `cylinder` in `teal` for the database. A `group` boxes the API and the service as the one server, `change: "modified"` marks the API as what the review touched, and a `legend` decodes the three marks. `highlight` rings the active card; `code` on a card and each frame's `code` link the ranges.
18
+
19
+ ## Adapt
20
+
21
+ - Rename the actors and code ranges to your layers; keep four or fewer in a row. A third-party API in the chain gets `outline: "dashed"`.
22
+ - Write each note as the payload's real type at that layer, short enough for two lines.
23
+ - Keep one `send` per frame: sends on different edges in one frame start together.
24
+ - Branch with new frame keys (a different body) and a scenario, not by drawing buttons.
@@ -0,0 +1,85 @@
1
+ <script>
2
+ const s = whiteboard.scene({ columns: 4 });
3
+
4
+ // Ring the card that reshapes the payload in each frame.
5
+ // Each card wears the shape of its layer: a window for the client, bars
6
+ // for the services this codebase runs, a cylinder for the database.
7
+ const client = s.actor({
8
+ id: "client",
9
+ at: [1, 1],
10
+ title: "Client",
11
+ form: "window",
12
+ highlight: ["request", "respond", "bad-request"],
13
+ });
14
+ const api = s.actor({
15
+ id: "api",
16
+ at: [2, 1],
17
+ title: "API",
18
+ form: "bars",
19
+ change: "modified",
20
+ code: "validate",
21
+ highlight: ["validate", "rejected"],
22
+ });
23
+ const service = s.actor({
24
+ id: "service",
25
+ at: [3, 1],
26
+ title: "OrderService",
27
+ form: "bars",
28
+ code: "service",
29
+ highlight: ["persist", "map"],
30
+ });
31
+ const db = s.actor({
32
+ id: "db",
33
+ at: [4, 1],
34
+ title: "Postgres",
35
+ form: "cylinder",
36
+ color: "teal",
37
+ code: "repository",
38
+ highlight: ["stored"],
39
+ });
40
+
41
+ // The API and the service are the one codebase; the review changed the API.
42
+ s.group({ id: "server", at: [2, 1, 1, 2], title: "Order server" });
43
+ s.legend([
44
+ { form: "bars", label: "This codebase" },
45
+ { form: "cylinder", color: "teal", label: "Database" },
46
+ { change: "modified", label: "Changed in this review" },
47
+ ]);
48
+
49
+ // Requests travel solid edges; replies ride dashed ones beside them.
50
+ s.link(client, api);
51
+ s.link(api, service);
52
+ s.link(service, db);
53
+ s.link(db, service, { dashed: true });
54
+ s.link(service, api, { dashed: true });
55
+ s.link(api, client, { dashed: true });
56
+
57
+ // Success: each layer holds the payload in its own shape.
58
+ s.on("request", client, { text: '{ sku, qty: "2" }' });
59
+ s.send(client, api, "POST /orders", "request");
60
+
61
+ s.on("validate", api, { text: "CreateOrderDto { sku, qty: 2 }" });
62
+ s.send(api, service, "dto", "validate");
63
+
64
+ s.on("persist", service, { text: "Order { id: null }" });
65
+ s.send(service, db, "INSERT", "persist");
66
+
67
+ s.on("stored", db, { text: "orders row id=812" });
68
+ s.send(db, service, "row 812", "stored");
69
+
70
+ s.on("map", service, { text: "Order { id: 812 }" });
71
+ s.send(service, api, "Order", "map");
72
+
73
+ s.on("respond", client, { text: '201 { id: "ord_812" }', tone: "added" });
74
+ s.send(api, client, "201", "respond");
75
+
76
+ // Validation failure: the API answers and the request stops there.
77
+ s.on("bad-request", client, { text: '{ sku, qty: "-1" }' });
78
+ s.send(client, api, "POST /orders", "bad-request");
79
+
80
+ s.on("rejected", api, { text: "qty: must be ≥ 1", tone: "removed" });
81
+ s.on("rejected", client, { text: "422 { field: qty }", tone: "removed" });
82
+ s.on("rejected", service, { text: "not called", tone: "muted" });
83
+ s.on("rejected", db, { text: "not called", tone: "muted" });
84
+ s.send(api, client, "422", "rejected", { tone: "removed" });
85
+ </script>
@@ -0,0 +1,17 @@
1
+ {
2
+ "title": "An order request across the layers",
3
+ "code": {
4
+ "validate": {
5
+ "source": "head/src/api/orders/validate.ts#L12-L40",
6
+ "label": "validateCreateOrder"
7
+ },
8
+ "service": {
9
+ "source": "head/src/orders/service.ts#L20-L58",
10
+ "label": "OrderService.create"
11
+ },
12
+ "repository": {
13
+ "source": "head/src/orders/repository.ts#L8-L31",
14
+ "label": "insertOrder"
15
+ }
16
+ }
17
+ }