Repository navigation
Expand file tree
/
Copy pathdeckbook.html
More file actions
1156 lines (1122 loc) · 53.6 KB
/
Copy pathdeckbook.html
File metadata and controls
1156 lines (1122 loc) · 53.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Deckbook — the Deck protocol, taught from the running code</title>
<style>
/* Boxel light surface tier — the same tokens the workspace chrome uses:
one stage colour, white panels, hairline borders, a teal interactive,
and shadows that stay under the content. */
/* CONTRAST IS MEASURED, NOT JUDGED BY EYE.
Every colour used for text meets WCAG AA (4.5:1) against both --bg and
--panel. Two did not and were darkened: --faint was 3.18 and --accent
was 3.22, which is the band that looks fine to whoever chose it and is
unreadable to a fair number of people who did not.
Measured with the ratios in docs/deck-hub-typography.md §6. */
:root {
--bg: #f7f8fa;
--panel: #ffffff;
--panel-2: #f6f7f9;
--line: #e2e8f0;
--hairline: #eceef1;
--text: #272330;
--muted: #5a586a;
--faint: #6f6f78;
--accent: #097a60;
--accent-2: #0069f9;
--warn: #b45309;
--danger: #d92d20;
--shadow: 0 2px 8px rgba(28, 28, 50, 0.05);
--mono: "IBM Plex Mono", "SF Mono", Menlo, ui-monospace, monospace;
--sans: "IBM Plex Serif", "Iowan Old Style", "Palatino Linotype", Georgia, serif;
--ui: "IBM Plex Sans", "Avenir Next", "Segoe UI", system-ui, sans-serif;
}
/* The link back to /start. Set in the mono, in capitals, the same wordmark
treatment the Hub nav uses — one name, one face, wherever it appears. */
.up {
display: inline-block; margin-bottom: 1rem;
font-family: var(--mono); font-size: 0.78rem; font-weight: 700;
letter-spacing: 0.12em; color: var(--text); text-decoration: none;
}
.up:hover { color: var(--accent-2); text-decoration: none; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background:
radial-gradient(900px 420px at 0% -10%, #eef8f3 0%, transparent 55%),
radial-gradient(800px 380px at 100% 0%, #eef2f9 0%, transparent 50%),
var(--bg);
color: var(--text);
font-family: var(--sans);
line-height: 1.55;
}
a { color: var(--accent-2); text-decoration: none; }
a:hover { text-decoration: underline; }
code, kbd, pre { font-family: var(--mono); }
code, kbd {
font-size: 0.86em;
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 4px;
padding: 0.05em 0.35em;
}
pre {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 0.9rem 1rem;
overflow: auto;
font-size: 0.82rem;
line-height: 1.45;
}
.wrap {
display: grid;
grid-template-columns: 240px minmax(0, 48rem);
gap: 2rem;
max-width: 72rem;
margin: 0 auto;
padding: 1.5rem 1.25rem 4rem;
}
@media (max-width: 860px) {
.wrap { grid-template-columns: 1fr; }
nav.toc { position: static !important; }
}
nav.toc {
position: sticky;
top: 1rem;
align-self: start;
max-height: calc(100vh - 2rem);
overflow: auto;
font-family: var(--ui);
font-size: 0.82rem;
}
nav.toc h1 {
margin: 0 0 0.35rem;
font-size: 1rem;
font-family: var(--ui);
font-weight: 650;
letter-spacing: 0.02em;
}
nav.toc h1 span { color: var(--accent); }
nav.toc .blurb {
color: var(--muted);
margin: 0 0 1rem;
font-size: 0.75rem;
line-height: 1.35;
}
nav.toc ol {
list-style: none;
margin: 0;
padding: 0;
}
nav.toc a {
display: block;
color: var(--faint);
padding: 0.22rem 0;
text-decoration: none;
}
nav.toc a:hover { color: var(--text); }
article header.hero {
margin-bottom: 2rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--line);
}
article header.hero h1 {
margin: 0 0 0.4rem;
font-size: clamp(2rem, 4vw, 2.75rem);
font-weight: 650;
letter-spacing: -0.02em;
line-height: 1.15;
}
article header.hero h1 em {
font-style: normal;
color: var(--accent);
}
article header.hero p {
margin: 0.5rem 0 0;
color: var(--muted);
font-family: var(--ui);
font-size: 1.02rem;
max-width: 40rem;
}
.badges {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 1rem;
font-family: var(--ui);
}
.badge {
display: inline-block;
padding: 0.18rem 0.5rem;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 0.72rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.badge.ok { color: var(--accent); border-color: #cfeee2; background: #f0fbf7; }
.badge.warn { color: var(--warn); border-color: #f4e6d4; background: #fff9f2; }
.badge.dim { color: var(--faint); }
section {
margin: 2.4rem 0;
}
section > h2 {
margin: 0 0 0.75rem;
font-size: 1.55rem;
font-weight: 650;
letter-spacing: -0.015em;
}
section > h3 {
margin: 1.6rem 0 0.5rem;
font-size: 1.15rem;
font-family: var(--ui);
font-weight: 600;
}
p, li { font-size: 1.02rem; }
p { margin: 0.7rem 0; }
ul, ol { padding-left: 1.2rem; }
li { margin: 0.35rem 0; }
.callout {
margin: 1rem 0;
padding: 0.85rem 1rem;
border: 1px solid var(--line);
border-left: 3px solid var(--accent);
background: var(--panel);
border-radius: 0 8px 8px 0;
box-shadow: var(--shadow);
font-family: var(--ui);
font-size: 0.92rem;
}
.callout.warn { border-left-color: var(--warn); }
.callout strong { color: var(--text); }
table {
width: 100%;
border-collapse: collapse;
font-family: var(--ui);
font-size: 0.9rem;
margin: 1rem 0;
}
th, td {
text-align: left;
padding: 0.5rem 0.6rem;
border-bottom: 1px solid var(--hairline);
vertical-align: top;
}
th { color: var(--faint); font-weight: 560; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }
td code { font-size: 0.8em; }
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
@media (max-width: 700px) { .grid-2 { grid-template-columns: 1fr; } }
.card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 10px;
box-shadow: var(--shadow);
padding: 0.9rem 1rem;
font-family: var(--ui);
}
.card h4 {
margin: 0 0 0.35rem;
font-size: 0.95rem;
}
.card p { margin: 0; color: var(--faint); font-size: 0.88rem; }
.cmd {
display: block;
margin: 0.35rem 0;
}
/* Transcript: what the command actually printed, distinct from what you
type. Every one of these was captured from a real run. */
pre.out {
background: var(--panel-2);
border-style: dashed;
box-shadow: none;
color: var(--muted);
margin-top: -0.6rem;
}
pre.out b { color: var(--text); font-weight: 600; }
pre.out i { color: var(--accent); font-style: normal; }
.walk {
counter-reset: step;
}
.walk > h4 {
counter-increment: step;
margin: 1.5rem 0 0.4rem;
font-family: var(--ui);
font-size: 0.98rem;
font-weight: 600;
}
.walk > h4::before {
content: counter(step);
display: inline-grid;
place-items: center;
width: 1.45em;
height: 1.45em;
margin-right: 0.5rem;
border-radius: 999px;
background: var(--accent);
color: #fff;
font-size: 0.78em;
vertical-align: 0.1em;
}
footer.book {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid var(--line);
color: var(--faint);
font-family: var(--ui);
font-size: 0.85rem;
}
</style>
</head>
<body>
<div class="wrap">
<nav class="toc" aria-label="Deckbook contents">
<!-- The way back. This page is reached from /start's contents rail, and a
document you can enter and not leave is a trap. -->
<a class="up" href="/start">← DECK · start</a>
<h1><span>Deck</span>book</h1>
<p class="blurb">Taught from the reference implementation — what runs, what it means, what to try next.</p>
<ol>
<li><a href="#promise">1. The promise</a></li>
<li><a href="#vocabulary">2. Vocabulary</a></li>
<li><a href="#address">3. Layout is the address</a></li>
<li><a href="#volatility">4. Three volatilities</a></li>
<li><a href="#loop">5. The live loop</a></li>
<li><a href="#store">6. Store & packs</a></li>
<li><a href="#history">7. History: every save, or Versions only</a></li>
<li><a href="#vendor">8. Safe vendor</a></li>
<li><a href="#fork">9. Fork, lock, offers</a></li>
<li><a href="#http">10. HTTP surface</a></li>
<li><a href="#cli">11. CLI map</a></li>
<li><a href="#usecases">12. Use cases, at the CLI</a></li>
<li><a href="#coding">13. Coding on Deck</a></li>
<li><a href="#try">14. Try it here</a></li>
<li><a href="#status">15. Honest status</a></li>
</ol>
</nav>
<article>
<header class="hero">
<h1>The <em>Deck</em>book</h1>
<p>
<strong>Deck is version management without the commit, package management
without the install, and cloud delivery without the deploy.</strong>
</p>
<p>
One thing makes all three true: there is only one kind of object here — a
file, named by the hash of its own bytes, reachable at a URL. A tree is a
list of those names. A Version, a Step, a dependency, a fork and a release
are all pointers to trees. Commit, install and deploy all do the same job:
they move a copy to a second place that names things differently. Here
there is no second place. This book teaches the running reference
implementation: the CLI, the store, the server, the History, and the
workflows they open up.
</p>
<div class="badges">
<span class="badge ok">Reference impl built</span>
<span class="badge ok">Versions always on</span>
<span class="badge ok">Every-save History optional</span>
<span class="badge dim">No framework required</span>
</div>
</header>
<section id="promise">
<h2>1. The promise</h2>
<p>
<strong>No npm. No GitHub. No deploy. No PR</strong> — for the loop that
matters while you are building. Edit a file, see it live, keep every
settled state, ship a name when you mean it, share a zip anyone can open,
pull any ESM library in-house, fork without consequences.
</p>
<table>
<thead><tr><th>Promise</th><th>Mechanism (implemented)</th></tr></thead>
<tbody>
<tr><td>Instant feedback</td><td>Version-less URL → working tree, <code>Cache-Control: no-store</code></td></tr>
<tr><td>Never lose work</td><td>Auto-publish <code><base>-dev.N</code> + optional per-save seals</td></tr>
<tr><td>Revert anytime</td><td><code>deck unpack</code> a pack, or <code>deck restore <changeId></code> (with every-save History)</td></tr>
<tr><td>Ship any version</td><td><code>deck release --from dev</code> — pointer, zero bytes copied</td></tr>
<tr><td>Share a milestone</td><td><code>deck pack</code> → a Deck Pack (<code>.pack.zip</code>, a standard zip)</td></tr>
<tr><td>Use any library</td><td><code>deck vendor</code> hermetic CDN graph → your store</td></tr>
<tr><td>Fork freely</td><td><code>deck fork</code> into your publisher scope</td></tr>
<tr><td>Stability on demand</td><td><code>deck lock</code> turns ranges into exact pins</td></tr>
<tr><td>Whole web platform</td><td>Plain ESM + import maps; no bundler required</td></tr>
</tbody>
</table>
</section>
<section id="vocabulary">
<h2>2. Vocabulary</h2>
<p>Borrowed lightly from trading-card games — because a Deck package is an <em>arrangement</em>, not a sealed npm box.</p>
<table>
<thead><tr><th>Term</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><strong>depot</strong></td><td>A directory that is also a URL space. Self-contained.</td></tr>
<tr><td><strong>deck</strong></td><td>A package: <code><publisher>/<name></code> with an <code>importmap.json</code>.</td></tr>
<tr><td><strong>decklist</strong></td><td>The manifest entry: versions, entry, exports, declared deps.</td></tr>
<tr><td><strong>live tree</strong></td><td>Bytes on disk right now. Editing surface.</td></tr>
<tr><td><strong>Version</strong></td><td>A published tree that does not change. A semver number addresses it. (Was “sealed deck”.)</td></tr>
<tr><td><strong>Deck Pack</strong></td><td><code>.pack.zip</code> — one tree and its packlist in one file. Any zip tool opens it. (Was “cardpack”.)</td></tr>
<tr><td><strong>treeHash</strong></td><td>Content identity of a whole tree (<code>tree-hash-v1</code>).</td></tr>
<tr><td><strong>Step / changeId</strong></td><td>One capture of the working tree, one for each save. The Steps in order are History. Needs a history backend (deckd).</td></tr>
<tr><td><strong>offer</strong></td><td>A fork that records its upstream base — a proposal you can run.</td></tr>
</tbody>
</table>
</section>
<section id="address">
<h2>3. The layout is the address</h2>
<p>
What <code>ls</code> shows and what the URL says are the same shape.
A depot carries its machinery inside itself — <code>.deck/store/</code>
for sealed versions, <code>.jj/</code> for seals — both invisible to the
tree hash, so copying the directory takes live files, published versions,
and history with it.
</p>
<pre><code><root>/site/acme/palette/palette.js on disk
/site/acme/palette/palette.js live — every save, no-store
/site/acme/palette@1.1.0/palette.js
sealed copy, immutable forever
/site/_history/<changeId>/acme/palette/palette.js
any Step it was ever in</code></pre>
<div class="callout">
<strong>Publisher segment = fork boundary.</strong>
<code>acme/palette</code> and <code>you/palette</code> are different
decks with different version lines in the same depot. Undiverged forks
cost no extra pack bytes — the store is content-addressed.
</div>
</section>
<section id="volatility">
<h2>4. Three levels of volatility</h2>
<p>All just import-map values / URL forms. Opting into churn is always explicit.</p>
<table>
<thead><tr><th>You want</th><th>You write</th><th>You get</th></tr></thead>
<tbody>
<tr><td>Every save, instantly</td><td><code>"live"</code> / version-less URL</td><td>Working tree, <code>no-store</code></td></tr>
<tr><td>Freshest auto-seal</td><td><code>"dev"</code> or <code>@dev</code></td><td>Short-TTL redirect to newest <code>-dev.N</code></td></tr>
<tr><td>Something dependable</td><td><code>"^1.0.0"</code> / exact</td><td>Stable resolution; ranges ignore prereleases</td></tr>
</tbody>
</table>
<p>
<code>latest</code> is a dist-tag on a stable release.
<strong><code>latest</code> is not live</strong> — live is the bytes on disk with no version segment.
</p>
</section>
<section id="loop">
<h2>5. The live loop</h2>
<p>The core vibe-coding workflow, as implemented:</p>
<pre><code>deck hello ./work # scaffold + serve (or: deck serve --root ./work)
# edit anything under a deck directory
# → debounce (~500ms quiet)
# → treeHash unchanged? stop
# → packFromDir → publish <base>-dev.N, tag:dev
# → [with every-save History] the save is recorded as a Step too
deck release --depot ./work/site --publisher acme --name palette \
--version 1.1.0 --from dev --tag latest</code></pre>
<div class="grid-2">
<div class="card">
<h4>HMR-lite</h4>
<p><code>/_live/events</code> (SSE) + drop-in <code>/_live/hmr.js</code> reloads pages that depend on changed live decks.</p>
</div>
<div class="card">
<h4>Import-map injection</h4>
<p>HTML responses get a real inline import map (browsers ignore external map scripts). Sealed pack bytes stay untouched.</p>
</div>
</div>
</section>
<section id="store">
<h2>6. Store & packs</h2>
<p>At rest, each package lives under the depot store:</p>
<pre><code><depot>/.deck/store/<publisher>/<name>/
meta.json # versions + dist-tags (pointers)
packs/<treeHash>.pack.zip # bytes once per content hash</code></pre>
<ul>
<li><strong>Publish</strong> verifies a pack, writes bytes only if the treeHash is new, then adds a version pointer.</li>
<li><strong>Release</strong> names an existing sealed state — never copies bytes.</li>
<li><strong>Prune</strong> drops old <code>-dev.N</code> records; deletes pack files only when nothing still points at them.</li>
<li><strong>Pack laws</strong>: treeHash survives pack→unpack; pack∘unpack is byte-identical for canonical packs; stock <code>unzip</code> works.</li>
</ul>
</section>
<section id="history">
<h2>7. History: every save, or Versions only</h2>
<p>A depot has one of two answers, and the server reports which. These are
not numbered levels — each one has a name that says what you get.</p>
<table>
<thead><tr><th>Answer</th><th>What you get</th><th>Status</th></tr></thead>
<tbody>
<tr>
<td><strong>Versions only</strong></td>
<td>Every settled save becomes an immutable <code>-dev.N</code> Version. The Versions are the history. Restore by unpacking one.</td>
<td><span class="badge ok">Always on</span></td>
</tr>
<tr>
<td><strong>Every-save History</strong></td>
<td>Every save also becomes a Step at <code>/<depot>/_history/<changeId>/…</code>, with <code>deck history</code> and <code>deck restore</code>. A packlist can name the Step it was cut from.</td>
<td><span class="badge ok">Implemented</span> when <code>--history auto|jj|historyd</code> attaches</td>
</tr>
</tbody>
</table>
<div class="callout warn">
<strong>Capability is reported, not pretended.</strong>
<code>GET /_packages</code> includes a history block (<code>tier</code>, <code>jj</code>, <code>historyd</code>).
Missing jj or a dead daemon leaves the depot with Versions only, and prints the reason — the live/auto-publish loop keeps working.
</div>
<pre><code>deck serve --root ./work --history auto
deck history ./work/site
deck restore ./work/site <changeId> # leaves current state sealed first</code></pre>
</section>
<section id="vendor">
<h2>8. Safe vendor — your own unpkg</h2>
<p>
Pull a CDN module graph once, rewrite references to stay inside the pack,
seal it. Runtime imports hit your Deck server only. Hermetic by default;
<code>--allow-external</code> / <code>--allow-host</code> are explicit opt-outs.
Identity travels as <code>vendoredFrom</code>.
</p>
<pre><code>deck vendor https://esm.sh/simplex-noise@4.0.1 \
--name simplex-noise --version 4.0.1 \
--depot ./work/site --publisher acme --tag latest</code></pre>
<pre><code><script type="importmap">
{ "imports": { "simplex-noise": "/site/acme/simplex-noise@4.0.1" } }
</script></code></pre>
</section>
<section id="fork">
<h2>9. Fork, lock, offers, rebase</h2>
<p>Contribution is a <strong>version you can run</strong>, not a diff waiting for merge theater.</p>
<h3>Fork</h3>
<pre><code>deck fork --depot ./work/site \
--from acme/palette --as you/palette --use-in you/app</code></pre>
<p>Copies into your publisher scope; optional <code>--use-in</code> repoints a consumer at the fork live. Records base (package, version, treeHash) so the fork <em>is</em> an offer.</p>
<h3>Lock</h3>
<pre><code>deck lock --depot ./work/site --publisher you --name app
deck lock --depot ./work/site --publisher you --name app --check</code></pre>
<p>Resolves declared ranges to exact pins in the import map — users never inherit someone else's working tree.</p>
<h3>Try by URL</h3>
<pre><code>/site/you/app/index.html?with=acme/palette=you/palette
/site/you/app/index.html?with=acme/palette@1.1.0</code></pre>
<p>Server rewrites resolution for that request only. No checkout, no build, not cached as sealed content.</p>
<h3>Offers & rebase</h3>
<pre><code>deck offers --depot ./work/site
deck rebase --depot ./work/site # or: deck serve --rebase
GET /site/_offers</code></pre>
<ul>
<li>Clean merges write into the proposal tree (auto-publish + seal).</li>
<li>Conflicts are <strong>reported and left untouched</strong> — machines never damage contributor work.</li>
<li>JSON merges as data (key-level), not as lines — so adding adjacent keys is not a fake conflict.</li>
<li>Accepting a proposal is a pointer move to a treeHash already held. Zero bytes copied.</li>
</ul>
</section>
<section id="http">
<h2>10. HTTP surface (read-only server)</h2>
<p>Writes go through CLI / watcher. The server distributes.</p>
<table>
<thead><tr><th>Request</th><th>Result</th></tr></thead>
<tbody>
<tr><td><code>GET /<depot>/<pub>/<pkg>/…</code></td><td>Live tree, <code>no-store</code></td></tr>
<tr><td><code>GET /<depot>/<pub>/<pkg>@<exact>/…</code></td><td>Immutable + <code>Content-Digest</code></td></tr>
<tr><td><code>GET …@<tag|range>/…</code></td><td>One 302 hop to exact (short TTL)</td></tr>
<tr><td><code>GET …@<exact></code> (bare)</td><td>301 to entry module</td></tr>
<tr><td><code>?meta</code></td><td>Machine-readable listing (incl. seal when present)</td></tr>
<tr><td><code>?format=pack</code></td><td>Faithful stored pack bytes</td></tr>
<tr><td><code>…/vendor.pack.zip!/inner.js</code></td><td>Mount into a nested pack (depth-capped)</td></tr>
<tr><td><code>GET /<depot>/_history</code></td><td>Every-save History, when a history backend is attached</td></tr>
<tr><td><code>GET /<depot>/_history/<id>/…</code></td><td>File at any seal</td></tr>
<tr><td><code>GET /_packages</code></td><td>Catalog + capability truth</td></tr>
<tr><td><code>GET /_browse/…</code></td><td>Directory / file JSON for the store browser</td></tr>
<tr><td><code>GET /<depot>/_offers</code></td><td>Open proposals + merge status</td></tr>
<tr><td><code>GET /_live/events</code></td><td>SSE change bus (HMR-lite)</td></tr>
<tr><td><code>/</code> or <code>/_browser</code></td><td>Store browser UI</td></tr>
</tbody>
</table>
</section>
<section id="cli">
<h2>11. CLI map</h2>
<p>
Twenty-seven commands in eight groups, ordered the way the work happens.
<code>deck help</code> prints this map from the command records themselves;
<code>deck help <command></code> explains any one of them in full.
</p>
<table>
<thead><tr><th>Group</th><th>What it is for</th><th>Commands</th></tr></thead>
<tbody>
<tr><td><strong>start</strong></td><td>stand a depot up and serve it live</td><td><code>hello</code> <code>serve</code></td></tr>
<tr><td><strong>seal</strong></td><td>a directory in, a verified pack out</td><td><code>hash</code> <code>pack</code> <code>unpack</code> <code>list</code> <code>verify</code></td></tr>
<tr><td><strong>publish</strong></td><td>name sealed states in a depot's store</td><td><code>publish</code> <code>tag</code> <code>release</code> <code>prune</code> <code>store</code> <code>derive</code> <code>verify-derivation</code></td></tr>
<tr><td><strong>depend</strong></td><td>what a deck uses, and which bytes answer to it</td><td><code>vendor</code> <code>vendor-tree</code> <code>lock</code> <code>verify-links</code></td></tr>
<tr><td><strong>propose</strong></td><td>change someone else's deck without a branch</td><td><code>fork</code> <code>offers</code> <code>rebase</code></td></tr>
<tr><td><strong>endorse</strong></td><td>who stands behind these bytes</td><td><code>keygen</code> <code>sign</code> <code>verify-sig</code></td></tr>
<tr><td><strong>recall</strong></td><td>every state the depot has been in</td><td><code>history</code> <code>restore</code></td></tr>
<tr><td><strong>prove</strong></td><td>the executable contract</td><td><code>conform</code></td></tr>
</tbody>
</table>
<p>From this repo without a global install:</p>
<pre><code>node packages/cli/bin/deck.js hello ./playground
node packages/cli/bin/deck.js serve --root ./playground --history auto --rebase</code></pre>
<div class="callout warn">
<strong>Not on npm.</strong> <code>npx deck</code> would run an unrelated
package that already owns that name on the public registry. Install from
the repo — see <code>INSTALL.md</code>.
</div>
</section>
<section id="usecases">
<h2>12. Use cases, at the command line</h2>
<p>
From <code>docs/deck-use-cases.md</code> — shapes of work that <em>need</em>
this, not merely benefit. Below, each one is worked as a sequence of real
commands. Every transcript on this page was captured from an actual run
against this implementation; nothing is illustrative.
</p>
<div class="callout">
<strong>One local depot. Libraries come from npm, vendored in.</strong>
There is no remote Deck in any of these. A Deck server runs on your
machine, owns your files, watches them, serves your app, and seals every
save. Third-party code is pulled from npm or a CDN <em>once</em>, sealed
into your own store, and served from your own server from then on. Nothing
your app loads at runtime comes from a host you do not control.
</div>
<h3>12.0 The setup all four share</h3>
<p>
A depot is a directory of decks. Make two — a component and an app that
uses it — and serve the lot:
</p>
<pre><code>work/tool/ui/table/ importmap.json table.js
work/tool/app/dashboard/ importmap.json app.js index.html</code></pre>
<pre><code>deck serve --root ./work --history auto</code></pre>
<pre class="out"><code> tool: <i>history: jj 0.43.0 (seal-grained)</i>
watching tool/app/dashboard
watching tool/ui/table
deck server on http://localhost:8788 (read-only)
browse: http://localhost:8788/ — depots, packages, versions
hmr: http://localhost:8788/_live/hmr.js → SSE /_live/events
live: http://localhost:8788/tool/ui/table/ — auto-publishes @<base>-dev.N (tag: dev)
seals: http://localhost:8788/tool/_history — every save, published or not
<b>auto-published tool/ui/table@0.1.0-dev.1 (d42dbe7c47d5) sealed pwwxoqtvxssy</b></code></pre>
<p>
That last line is the whole loop, and it happened without you asking. Now
edit <code>table.js</code> and save it — twice:
</p>
<pre class="out"><code><b>auto-published tool/ui/table@0.1.0-dev.2 (9f4f2d43a2d2) sealed wtmrttzrvvwp</b>
<b>auto-published tool/ui/table@0.1.0-dev.3 (b90501707b14) sealed zkzwlotsqyzm</b></code></pre>
<p>
Each save is debounced (~500ms of quiet), hashed, and — only if the tree
hash <em>changed</em> — published as an immutable version and sealed.
Touching a file publishes nothing. Two identifiers appear on every line
and they are different things: the <strong>tree hash</strong> is what the
bytes are, and the <strong>changeId</strong> is which save it was.
</p>
<pre><code>deck history ./work/tool --limit 3</code></pre>
<pre class="out"><code>zkzwlotsqyzm 2026-08-08T19:59:32-0400 save: ui/table/table.js
M ui/table/table.js
wtmrttzrvvwp 2026-08-08T19:59:29-0400 save: ui/table/table.js
M ui/table/table.js
pwwxoqtvxssy 2026-08-08T19:59:27-0400 save
A app/dashboard/app.js
A ui/table/table.js</code></pre>
<p>
Meanwhile the version-less URL is the working tree, and says so in a
header the browser is obliged to honour:
</p>
<pre><code>curl -si http://localhost:8788/tool/ui/table/table.js</code></pre>
<pre class="out"><code>HTTP/1.1 200 OK
content-type: text/javascript; charset=utf-8
<b>cache-control: no-store</b>
repr-digest: sha-256=:LCVAhA3V5YjeY287uQSy1dLTffMDbV7pvtIxdhQlxNA=:
x-deck-depot: tool</code></pre>
<div class="callout warn">
<strong>Without <code>jj</code></strong> the first line reads Versions only
instead, and everything else here still works. You keep every settled save
as an immutable version; what you lose is the saves <em>between</em>
publishes, and with them <code>deck history</code> and
<code>deck restore</code>.
</div>
<h3>12.1 The swarm-built internal tool</h3>
<p>
Eight agents, one component each, one human steering. The app composes
them through its import map, so the assembled thing exists continuously —
there is no integration step, because there was never a disintegration.
</p>
<div class="walk">
<h4>Declare what the app uses, at the volatility you want</h4>
<p>
The key is the import specifier your source already writes. The value is
how much churn you are opting into, chosen <em>per component</em>:
</p>
<pre><code>{
"deck": {
"dependencies": {
"ui/table": "live", // you are editing this one
"ui/filter-bar": "live",
"charts/revenue": "dev", // freshest seal, someone else's
"charts/funnel": "^1.2.0", // chart 3 went bad — frozen
"data/warehouse": "^2.0.0" // must not move under you
}
}
}</code></pre>
<h4>Resolve the ranges into pins</h4>
<pre><code>deck lock --depot ./work/tool --publisher app --name dashboard</code></pre>
<pre class="out"><code>ui/table live → /tool/ui/table/table.js
nanoid lib/nanoid@^5.0.0 → /tool/lib/nanoid@5.0.9/index.browser.js
locked app/dashboard</code></pre>
<p>
The pins are written <em>into</em> <code>importmap.json</code>, so
<code>cat</code> is how you read the lock, the lock travels inside the
pack, and moving a dependency forward is an ordinary save that
auto-publishes and seals like any other.
</p>
<h4>Quarantine one agent without stopping the others</h4>
<p>
Chart 3 is producing nonsense. Change one line from <code>live</code> to
<code>^1.2.0</code> and re-lock. That panel freezes at its last good
release; the other seven keep moving under you. No branch, no revert, no
conversation.
</p>
<h4>Check the pins are not lying</h4>
<pre><code>deck verify-links --depot ./work/tool --publisher app --name dashboard</code></pre>
<pre class="out"><code> ok app/dashboard 3 link(s)
1 deck(s) verified</code></pre>
<p>
Offline, on purpose. It checks that every pin resolves to what its
dependency record implies, that every pin has a record and every record
a pin, and that the files named actually exist. <code>deck lock
--check</code> runs the same gate and refuses to write — that is the CI
shape.
</p>
</div>
<h3>12.2 Codelists and reference data</h3>
<p>
A shared table of facts — currencies, tax rates, a model-and-pricing
registry — amended constantly by whoever noticed a fact was wrong. This is
the worst case for git+npm and the best case for Deck.
</p>
<div class="walk">
<h4>Ship a release — and meet the one gate that is not obvious</h4>
<p>
A version is a pointer, so releasing copies nothing. But the sealed
manifest has to <em>agree</em> that it is the version being released:
</p>
<pre><code>deck release --depot ./work/tool --publisher ui --name table --version 1.0.0 --from dev</code></pre>
<pre class="out"><code>error: importmap.json declares ui/table@0.1.0, but the request is publishing ui/table@1.0.0</code></pre>
<p>
So bump <code>deck.packages.<name>.version</code> to
<code>1.0.0</code>. That is an ordinary save, which means the watcher
publishes and seals it for you:
</p>
<pre class="out"><code><b>auto-published tool/ui/table@1.0.0-dev.1 (9c1fb4417167) sealed kppyzoyuwutt</b></code></pre>
<pre><code>deck release --depot ./work/tool --publisher ui --name table \
--version 1.0.0 --from dev --tag latest</code></pre>
<pre class="out"><code>released ui/table@1.0.0 from dev: same sealed state (treeHash 9c1fb4417167c9ee…), <b>zero bytes copied</b>, tag latest</code></pre>
<p>
An amendment publishes to the <code>dev</code> line the instant it is
saved, and no <code>^1.0.0</code> consumer sees it — ranges never match
prereleases. A curator names a batch when it is right, and that naming
is instant no matter how large the list.
</p>
<h4>Keep your patched copy, and stay current anyway</h4>
<p>
Your fork has three internal currency codes in it. A fork records its
base, so it <em>is</em> a proposal — nothing registers it:
</p>
<pre><code>deck fork --depot ./work/tool --from ui/table@1.0.0 --as you/table --use-in app/dashboard</code></pre>
<pre class="out"><code>forked ui/table@1.0.0 → you/table (2 files) at …/work/tool/you/table
it publishes under you/table; ui/table's versions never move
app/dashboard now gets you/table live wherever it imports ui/table — every save, no publish</code></pre>
<pre><code>deck offers --depot ./work/tool</code></pre>
<pre class="out"><code>current you/table ← ui/table@1.0.0
1 open, 0 need a person</code></pre>
<h4>Nobody rebases</h4>
<p>
Upstream releases 1.1.0. The base is exact and both sides are
content-addressed, so re-merging is a pure function a machine runs:
</p>
<pre><code>deck rebase --depot ./work/tool</code></pre>
<pre class="out"><code>rebased you/table ← ui/table@1.1.0 (written)
1 rebased, 0 need a person</code></pre>
<p>
The merge landed in the fork's own tree — which auto-published and
sealed like any other save — and the base pointer moved to what it
absorbed. JSON merges <strong>by key</strong>, not by line, so two people
adding two different currency codes both land and neither is a conflict.
</p>
<h4>What happens when it really is a conflict</h4>
<p>
Both sides change the same key differently. The machine reports it and
writes <em>nothing</em> — not even the part that merged:
</p>
<pre class="out"><code>conflicted you/table ← ui/table@1.2.0 conflicts: table.js
0 rebased, 1 need a person
fork tree before: tree-hash-v1:58519146809b424024290db3579bd1e4b9d860353c099c4d96026f12d710d7d3
fork tree after: tree-hash-v1:58519146809b424024290db3579bd1e4b9d860353c099c4d96026f12d710d7d3</code></pre>
<p>
Byte-identical. A machine never damages a contributor's work. Run it
continuously with <code>deck serve --rebase</code>, and a maintainer with
200 open proposals sees the three that stopped merging rather than the
197 that someone should rebase.
</p>
</div>
<h3>12.3 Adapters, patterns, and the automation you wrote for yourself</h3>
<p>
"Slack → Google Doc" is four things with different physics: a stable
<strong>adapter</strong>, your <strong>bespoke pattern</strong>, the
<strong>generic pattern</strong> it could become, and an
<strong>instance</strong> that is pure config. Today each is exiled to a
different system, so the promotion from bespoke to generic is a rewrite
and mostly nobody does it.
</p>
<div class="walk">
<h4>Bring the adapter in-house, once</h4>
<pre><code>deck vendor npm:nanoid@5.0.9 --depot ./work/tool --publisher lib --tag latest</code></pre>
<pre class="out"><code>vendored 2 modules from nanoid@5.0.9 (entry index.browser.js, <b>0 rewritten — byte-identical to what npm published</b>)
treeHash 81c7824d64e5070895728b60233e96466afdad649d188fe225df0210f76cad36
published lib/nanoid@5.0.9 tag latest — import it as /tool/lib/nanoid@5.0.9/</code></pre>
<p>
For something with dependencies of its own, <code>deck vendor-tree</code>
does the whole graph — one deck per package, wired by aliased dependency
records — so a library two of your decks share is stored and loaded once.
A version published in the last 30 days is refused by default; that is
the shape most supply-chain attacks arrive in.
</p>
<h4>Promote bespoke → generic without migrating anything</h4>
<pre><code>deck fork --depot ./hub --from me/ship-notes --as team/message-to-doc</code></pre>
<p>
Pull the hardcoded IDs out into <code>config.schema.json</code> and
release <code>1.0.0</code>. Your private one keeps working, untouched,
pinned to whatever it was pinned to. The generic one has its own version
line from birth. Nothing migrated, and the extraction pays twice: a
pattern that declares a config schema is configurable without any UI
work, and an instance is a JSON document validated against it.
</p>
<h4>Three strata, three volatilities, one address space</h4>
<pre><code>"lib/slack": "^2.0.0" // adapter — changes when Slack changes
"team/message-to-doc": "dev" // pattern — weekly
"me/ship-notes": "live" // instance — whenever you tweak it</code></pre>
</div>
<h3>12.4 Skills and agent capabilities</h3>
<p>
A skill is a deck of markdown and JSON. Prompt work is the
highest-frequency, lowest-ceremony editing there is — a good skill is 200
tiny edits — and the only real evaluation is running the harness.
</p>
<div class="walk">
<h4>Keep all 200 iterations without authoring a single commit message</h4>
<p>
Nothing to do. The watcher already sealed every one of them, and
published the ones that changed the tree.
</p>
<h4>A/B two skills by opening two links</h4>
<pre><code>curl -si "http://localhost:8788/tool/app/dashboard/index.html?with=ui/table=you/table"</code></pre>
<pre class="out"><code>HTTP/1.1 200 OK
<b>cache-control: no-store</b>
x-deck-importmap: injected
<b>x-deck-with: ui/table, ui/table/</b>
<script type="importmap">{"imports":{"ui/table/":"/tool/you/table/","ui/table":"/tool/you/table/table.js"}}</script>
<script type="module" src="./app.js"></script></code></pre>
<p>
The server resolved that one request's import map differently, using the
same rules <code>deck lock</code> uses. No checkout, no build, no branch,
and the app's own tree is untouched. Repeat <code>with</code> to swap
several at once. A trial is <code>no-store</code> and carries
<code>x-deck-with</code>, so it can never be cached as if it were the
sealed thing.
</p>
<h4>Decide the version from 40 minutes ago was better</h4>
<pre><code>deck history ./work/tool --limit 40
deck restore ./work/tool zkzwlotsqyzm</code></pre>
<pre class="out"><code>restored ./work/tool to zkzwlotsqyzm…: 2 file(s) written, 0 removed
app/dashboard/importmap.json
ui/table/table.js
<b>the state you left is sealed as zmmqpxpvkvww — restore it back any time</b></code></pre>
<p>
The hour you are abandoning was sealed before the restore ran, so this
is undoable. Nothing is rewound: a restore replays forward as ordinary
writes, which auto-publish as the next dev version. History only grows.
</p>
</div>
<h3>What the four have in common</h3>
<table>
<thead><tr><th>Move</th><th>Command</th><th>Why it is not available elsewhere</th></tr></thead>
<tbody>
<tr><td>Compose work in progress</td><td><code>serve</code> + a decklist</td><td>Composition happens at load time through the import map, so "run these eight in-progress things together" is the default state rather than a merge</td></tr>
<tr><td>Dial churn per dependency</td><td><code>lock</code></td><td>live, dev, and semver are three different <em>addresses</em> of one deck</td></tr>
<tr><td>Keep every intermediate state</td><td><code>serve --history auto</code></td><td>No commit step, so the states you actually want back are the ones that survive</td></tr>
<tr><td>Review by running</td><td><code>?with=</code></td><td>The proposal is already published at a URL; trying it is a query parameter</td></tr>
<tr><td>Fork and stay current</td><td><code>fork</code> + <code>rebase</code></td><td>An exact base plus content addressing makes re-merging a pure function</td></tr>
<tr><td>Ship instantly at any size</td><td><code>release --from dev</code></td><td>A version is a pointer; naming a sealed state copies zero bytes</td></tr>
</tbody>
</table>
</section>
<section id="coding">
<h2>13. Coding on Deck</h2>
<p>
The four workloads above are shapes of software. This one is the act of
writing it — vibe coding with agents, and using Deck as the revision
system while you do. It is the workload every other workload sits inside,
and the one where the protocol's odder decisions stop looking odd.
</p>
<h3>Deck as your revision system</h3>
<p>
Not a replacement for git in general — a replacement for what git is doing
on the twenty tabs of half-finished work you have open right now. Those
two jobs have almost nothing in common, and git is only good at one.
</p>
<table>
<thead><tr><th>You want</th><th>git</th><th>Deck</th></tr></thead>
<tbody>
<tr>
<td>keep this state</td>
<td><code>add</code>, <code>commit -m "…"</code>, think of a message</td>
<td>save the file. It is sealed and, if the tree changed, published</td>
</tr>
<tr>
<td>see it running</td>
<td>build, serve, reload</td>
<td>it is already at a URL, <code>no-store</code>, this instant</td>
</tr>
<tr>
<td>go back an hour</td>
<td><code>reflog</code>, <code>reset</code>, hope</td>
<td><code>deck restore <changeId></code> — and that is itself undoable</td>
</tr>
<tr>
<td>two things at once</td>
<td>two branches, one worktree, or a stash</td>
<td>two decks. There was never one tree to share</td>
</tr>
<tr>
<td>ship it</td>
<td>tag, build, publish, wait</td>
<td><code>release --from dev</code> — a pointer, zero bytes</td>
</tr>
<tr>
<td>someone else's change</td>
<td>a diff to read</td>
<td>a URL to run</td>
</tr>
</tbody>
</table>
<p>
The load-bearing difference is <strong>ceremony per state</strong>. Git
charges you a commit message for every state you want to keep, so you keep
the states you can name — and the ones worth going back to are almost
never those. They are the accidental ones: the version from forty minutes
ago, before the refactor that seemed obviously right. Deck charges
nothing, so it keeps them all.
</p>
<div class="callout">
<strong>Going back always goes forward.</strong> <code>deck restore</code>
seals the state you are leaving <em>first</em>, then writes the old one as
ordinary saves, which auto-publish as the next dev version. Nothing is
rewound and nothing is rewritten. That is why there is no reset in this
CLI and no <code>--force</code> anywhere near history: an operation that
can destroy work is a bad operation to have available at 2am.
</div>
<h3>Two agents at once</h3>
<p>
The thing that actually breaks when you point several agents at one
codebase is not merge quality. It is that they share a working tree, so
one of them is always waiting or clobbering. Deck's answer is structural:
<strong>one deck per component</strong>, so there is nothing to share.
</p>
<pre class="out"><code><b>auto-published tool/ui/table@0.1.0-dev.1 (a4e81bdb2b71) sealed zyrspmpksmzk</b>
<b>auto-published tool/charts/spark@0.1.0-dev.1 (7cfa1b609b9c) sealed zyrspmpksmzk</b>
two saves, two version lines, zero merges — different decks.</code></pre>
<p>
Both saves landed inside one seal window, so they share a changeId and
still have separate version lines. Nobody waited, and no merge ran,
because a merge only exists where two writers touched one name.
</p>
<p>
They eventually will. When both amend the same shared deck — a token set,
a codelist — the three-way merge runs against the base each fork recorded,
and most of the time there is nothing to argue about:
</p>
<pre class="out"><code>agent-a/tokens rebased 1.1.0 (written)
agent-b/tokens rebased 1.1.0 (written)
agent-a: ink, surface, muted, accent
agent-b: ink, surface, muted, warning</code></pre>
<p>
Three different keys under one JSON object. Both forks absorbed
upstream's and kept their own. Nobody reviewed anything, because a line
merge's idea of "adjacent lines changed" was never a real disagreement.
</p>
<h3>The conflict that is real, and resolving it deterministically</h3>
<p>
Then they collide for real: the same key, two answers. The generic merge
refuses — and refuses <em>completely</em>:
</p>
<pre class="out"><code>agent-a/tokens conflicted 1.2.0 ← tokens.json
agent-a's tree: 66febf4edc8b… before, 66febf4edc8b… after — <b>byte for byte</b></code></pre>
<p>
It has to refuse, because it does not know what the key <em>means</em>.
<code>scale</code> is a set, so two people extending it are agreeing.
<code>radius</code> is a scalar, so two people setting it are
disagreeing — but there may still be a house rule. Nothing generic can
tell those apart, and a merge tool that guessed would be worse than one
that stops.
</p>
<p>
So the rules are <strong>published data</strong>, in upstream's own sealed
tree, versioned like everything else:
</p>
<pre><code>// std/tokens/merge-policy.json
{ "rules": { "scale": "union", "radius": "max" } }</code></pre>
<p>
A resolver reads them and settles what they cover:
</p>
<pre class="out"><code>radius base 4 upstream 8 fork 12
→ rule "max" → <b>12</b>
scale base [4,8,16,24] upstream [4,8,16,24,40] fork [4,8,16,24,48]
→ rule "union" → <b>[4,8,16,24,40,48]</b>