Compare commits
664
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8abd0bdb55 | ||
|
|
ba91206c6b | ||
|
|
23356b2c88 | ||
|
|
441296f37a | ||
|
|
e1764b4892 | ||
|
|
eb061c7603 | ||
|
|
c1d331da40 | ||
|
|
a062015501 | ||
|
|
712f2cc60f | ||
|
|
fe5275b470 | ||
|
|
fdaff7b780 | ||
|
|
93985958bc | ||
|
|
5b54cfdad1 | ||
|
|
ac217628a1 | ||
|
|
927ff51842 | ||
|
|
7ee9271a3b | ||
|
|
85b1a9f317 | ||
|
|
5216099fe4 | ||
|
|
7e9ed3abf3 | ||
|
|
a8c596c270 | ||
|
|
da5c034928 | ||
|
|
d50a593d68 | ||
|
|
6818f61ce0 | ||
|
|
315cd7ec24 | ||
|
|
e1819e0a0d | ||
|
|
d9a0e19b20 | ||
|
|
2d5f6ac949 | ||
|
|
fd04333fa5 | ||
|
|
feebb8cb20 | ||
|
|
867229003b | ||
|
|
c5bdcbf0c5 | ||
|
|
efec941fa7 | ||
|
|
528a7ee41c | ||
|
|
9d57887357 | ||
|
|
2b731e1141 | ||
|
|
d0a7565088 | ||
|
|
d60f14c56c | ||
|
|
2103f4479b | ||
|
|
a1c77688ab | ||
|
|
dca4bc973e | ||
|
|
201e998418 | ||
|
|
597d097982 | ||
|
|
a1efa45250 | ||
|
|
c0409d137a | ||
|
|
0bcfff8e11 | ||
|
|
9e3b63a6db | ||
|
|
cab8c547f1 | ||
|
|
d0597bd2e5 | ||
|
|
21fa15d96d | ||
|
|
1ac0f4e311 | ||
|
|
4d729a009a | ||
|
|
c3de071fc8 | ||
|
|
b057329e65 | ||
|
|
99d9721ff3 | ||
|
|
c58a00b3a7 | ||
|
|
7c476bfb4a | ||
|
|
7dc0707008 | ||
|
|
269d226a8d | ||
|
|
b6556a4bcf | ||
|
|
0e1a742b10 | ||
|
|
cfb6666711 | ||
|
|
51a9919c56 | ||
|
|
3e7befcb0b | ||
|
|
e95b7f8ea0 | ||
|
|
7f094b8278 | ||
|
|
26f4b1c2a5 | ||
|
|
60e99f3875 | ||
|
|
e6c8bb15d1 | ||
|
|
414490fa5a | ||
|
|
ccc8a4c492 | ||
|
|
350d05b2c2 | ||
|
|
51c6dcbe46 | ||
|
|
9381cde1b4 | ||
|
|
d004b90e49 | ||
|
|
1012514fdc | ||
|
|
0cdd66f945 | ||
|
|
108a56fbc7 | ||
|
|
785cd229b3 | ||
|
|
e5d199263b | ||
|
|
8999f8e9d0 | ||
|
|
6280fc3a3e | ||
|
|
b4abb98aaa | ||
|
|
1c6fe9611c | ||
|
|
f65acb98c7 | ||
|
|
51b43dfb10 | ||
|
|
040ad22e69 | ||
|
|
e164a66362 | ||
|
|
caa7a9265c | ||
|
|
0b90251098 | ||
|
|
a3d9975e4b | ||
|
|
cbafc7556a | ||
|
|
6d0506402d | ||
|
|
e504dac534 | ||
|
|
e41dbeef4a | ||
|
|
05b16d63bb | ||
|
|
529cba366b | ||
|
|
298e68c71f | ||
|
|
e9ae4afc12 | ||
|
|
ec6be19b11 | ||
|
|
ae1b5ee957 | ||
|
|
9ca4757357 | ||
|
|
073160ca46 | ||
|
|
a1cbcfb23f | ||
|
|
4976caa124 | ||
|
|
6c1e759e26 | ||
|
|
d0760629e5 | ||
|
|
27ec6f8500 | ||
|
|
c098518cc8 | ||
|
|
f1fe609b3e | ||
|
|
8e9116469a | ||
|
|
28c22c65cb | ||
|
|
fbd7981b7b | ||
|
|
b4a07c1de6 | ||
|
|
30b29a58a4 | ||
|
|
b08147e531 | ||
|
|
7dfb6f0b7b | ||
|
|
95ae1dd6ed | ||
|
|
de62fe5852 | ||
|
|
bddf6ce95a | ||
|
|
636b2be7ac | ||
|
|
a6c9b96be3 | ||
|
|
ca1fbf7867 | ||
|
|
aec610a6f4 | ||
|
|
d758964ca9 | ||
|
|
c232b1fb81 | ||
|
|
024627f658 | ||
|
|
c74546a16f | ||
|
|
87827ebb82 | ||
|
|
bfd5fd2c0b | ||
|
|
9f9cb58f54 | ||
|
|
9bcd0ad197 | ||
|
|
27c9c6f11f | ||
|
|
893d83d23b | ||
|
|
5df2eca59e | ||
|
|
b157283202 | ||
|
|
f215cb9207 | ||
|
|
c46669a17e | ||
|
|
e27fd02269 | ||
|
|
63eeb96ebd | ||
|
|
da83f29f47 | ||
|
|
000a35d5cf | ||
|
|
b58d789e83 | ||
|
|
e65d0f8f2b | ||
|
|
cdfed7593e | ||
|
|
6e90fa443e | ||
|
|
8f9a6d4f7b | ||
|
|
b229f5c4df | ||
|
|
ebf115303a | ||
|
|
3d97fe016c | ||
|
|
b33ac325c0 | ||
|
|
d0d71b5548 | ||
|
|
c814bf2e0f | ||
|
|
2ba88971cf | ||
|
|
38915bbe01 | ||
|
|
78ef41e7bd | ||
|
|
7bbd4a86b4 | ||
|
|
43eac215d2 | ||
|
|
40661df80f | ||
|
|
45cc27cf24 | ||
|
|
a8103cf0b1 | ||
|
|
bce14ad12e | ||
|
|
55ac9f2d8e | ||
|
|
3de6895e32 | ||
|
|
2fa15a1c8d | ||
|
|
471fd8791d | ||
|
|
6611b94e3d | ||
|
|
fc6a30070f | ||
|
|
c9abb1c5f9 | ||
|
|
b3484cdcfc | ||
|
|
76b317ed3a | ||
|
|
205992f15c | ||
|
|
e73a1a7bcf | ||
|
|
64cdada15a | ||
|
|
4f388e8452 | ||
|
|
2fd96e1e4e | ||
|
|
7f2f37d9e6 | ||
|
|
2de011e070 | ||
|
|
d61bed820f | ||
|
|
3cf28e840d | ||
|
|
c57930a759 | ||
|
|
e7f090e333 | ||
|
|
dea6b197c5 | ||
|
|
1fb7943efd | ||
|
|
e01bf97a17 | ||
|
|
b6b657917e | ||
|
|
aaad9daa69 | ||
|
|
ec71485755 | ||
|
|
885099eebb | ||
|
|
d676466578 | ||
|
|
da828c147a | ||
|
|
35b92fde78 | ||
|
|
60369fddee | ||
|
|
12489d580c | ||
|
|
dd55153cf3 | ||
|
|
27d31d5490 | ||
|
|
8a85a2fd54 | ||
|
|
49ecfb6cdf | ||
|
|
25d9f8df61 | ||
|
|
af6695161c | ||
|
|
ce3fe6aa3b | ||
|
|
de7358a2c5 | ||
|
|
8d98d54bd2 | ||
|
|
dbc047bf56 | ||
|
|
5ff1298e21 | ||
|
|
c1cf0149b6 | ||
|
|
5e3b836404 | ||
|
|
cad2693361 | ||
|
|
e99094ab94 | ||
|
|
1ca5a3a133 | ||
|
|
b926db338d | ||
|
|
d61e18de41 | ||
|
|
8b923e6bff | ||
|
|
26b6dcf4f6 | ||
|
|
5688a2efa1 | ||
|
|
7e33e9a15f | ||
|
|
674f88f9ab | ||
|
|
b81c05c36e | ||
|
|
160f384e4f | ||
|
|
e1713bf59a | ||
|
|
cb1b055b80 | ||
|
|
c70b275187 | ||
|
|
b165eb9438 | ||
|
|
7d77102db0 | ||
|
|
d1e2059dab | ||
|
|
304d17b561 | ||
|
|
331e570792 | ||
|
|
749249466f | ||
|
|
c4b98b2a03 | ||
|
|
6b8b6b08e3 | ||
|
|
f91a5a7c75 | ||
|
|
aac8494ca3 | ||
|
|
f0c05d1edb | ||
|
|
b525f1059b | ||
|
|
69f95f551d | ||
|
|
d4ae727823 | ||
|
|
304ce84cac | ||
|
|
e0a70a468b | ||
|
|
571c0a3e68 | ||
|
|
b0a9fa14da | ||
|
|
b3a9970ac7 | ||
|
|
5499c3546b | ||
|
|
c3d8367b33 | ||
|
|
8f75606167 | ||
|
|
901c92a924 | ||
|
|
bdc5ae4fe6 | ||
|
|
aced03a287 | ||
|
|
da1c7f30a9 | ||
|
|
273d608d9f | ||
|
|
cac55dc254 | ||
|
|
01d4f2781e | ||
|
|
48fa0a5a48 | ||
|
|
f47744a8b6 | ||
|
|
6cd965b3cd | ||
|
|
4f6b9f0ca5 | ||
|
|
a838b6b3d3 | ||
|
|
6c6b2c5cc1 | ||
|
|
fb7aa0915f | ||
|
|
74b69857a1 | ||
|
|
0e8a698025 | ||
|
|
01d2ea0855 | ||
|
|
e7ac664aff | ||
|
|
457ac2eabd | ||
|
|
216e0e5102 | ||
|
|
f91f4879fc | ||
|
|
c8e6eb5edf | ||
|
|
a81334b1af | ||
|
|
546a18a20e | ||
|
|
a9745b63cb | ||
|
|
021fca5e6d | ||
|
|
fa2d199b16 | ||
|
|
cdfddff72e | ||
|
|
939d000327 | ||
|
|
e51e6961e3 | ||
|
|
0168a545c5 | ||
|
|
0ae7fc2e57 | ||
|
|
da823becbe | ||
|
|
3b1278b3bc | ||
|
|
4710fd18c6 | ||
|
|
34384b6c9c | ||
|
|
2d48b85c0e | ||
|
|
01a0a3a4d9 | ||
|
|
276cbef0c4 | ||
|
|
5b2cead5e3 | ||
|
|
9845b78e5b | ||
|
|
20157fcafe | ||
|
|
27063a492b | ||
|
|
f6bd4246b0 | ||
|
|
33d25c5074 | ||
|
|
bf5ef24ea3 | ||
|
|
979bf3f19c | ||
|
|
c5644e4a5d | ||
|
|
43d5347258 | ||
|
|
b5347d0294 | ||
|
|
4315bb6307 | ||
|
|
e8c154c3a0 | ||
|
|
666c9271dc | ||
|
|
d829fd6f9b | ||
|
|
10df1c7be5 | ||
|
|
8420c6f236 | ||
|
|
9a2d398070 | ||
|
|
476557deeb | ||
|
|
3d4e361425 | ||
|
|
0cd0cfd778 | ||
|
|
c033592509 | ||
|
|
221165c712 | ||
|
|
ed67d1f84f | ||
|
|
08b968fd80 | ||
|
|
b01f867345 | ||
|
|
92fa66f4e7 | ||
|
|
6bc1f7cc10 | ||
|
|
402ac3bfda | ||
|
|
ae4c90f990 | ||
|
|
4d4524cda3 | ||
|
|
5accd21320 | ||
|
|
3a3501d9f4 | ||
|
|
8402a7d773 | ||
|
|
5d9a654767 | ||
|
|
1a389be090 | ||
|
|
c2b162a9e8 | ||
|
|
3122e8be84 | ||
|
|
36517018d3 | ||
|
|
b8ad9da784 | ||
|
|
eaa98758b9 | ||
|
|
b9a42eaf90 | ||
|
|
6256fc8e3b | ||
|
|
012d276dcc | ||
|
|
5a9a5d8d1d | ||
|
|
2731c796f0 | ||
|
|
7470927a07 | ||
|
|
6c395cc206 | ||
|
|
594a0871dd | ||
|
|
7021d0cf5a | ||
|
|
4037bc1fb0 | ||
|
|
121b582ebc | ||
|
|
fa3d8aaeec | ||
|
|
633c587a1e | ||
|
|
6f2bacf6c4 | ||
|
|
f5624e7d9f | ||
|
|
b861b3ae36 | ||
|
|
0ac621e03f | ||
|
|
f312e7cd0e | ||
|
|
2b9e6b256b | ||
|
|
1747485cb3 | ||
|
|
0c450cad25 | ||
|
|
2e235286b1 | ||
|
|
a35c7ed6d0 | ||
|
|
e65f897a1f | ||
|
|
7fd89de7cb | ||
|
|
f4fdbde558 | ||
|
|
7d022710b3 | ||
|
|
f89b8599c6 | ||
|
|
eec73b8af0 | ||
|
|
1aa887dd28 | ||
|
|
aa4c2fe7ef | ||
|
|
292232dad2 | ||
|
|
a9830f5110 | ||
|
|
02a18c9aa8 | ||
|
|
606956da97 | ||
|
|
d088f17cd2 | ||
|
|
dd03229ea8 | ||
|
|
781392b121 | ||
|
|
c2f4bb2d48 | ||
|
|
ea0c62355c | ||
|
|
8f8393fa93 | ||
|
|
dd36b7557a | ||
|
|
d348256090 | ||
|
|
2ea3368b76 | ||
|
|
3c2d5af436 | ||
|
|
bcad1e363a | ||
|
|
edbb53bb78 | ||
|
|
32da464794 | ||
|
|
10bc8d396c | ||
|
|
8e732e22ae | ||
|
|
6ef40bbcc7 | ||
|
|
7ff4c7ce68 | ||
|
|
0d1aa93e37 | ||
|
|
1afab9739e | ||
|
|
420751b214 | ||
|
|
043f8ecf9c | ||
|
|
132aae8433 | ||
|
|
28d84f31bf | ||
|
|
278ca42b3d | ||
|
|
1dab64134a | ||
|
|
a0e4c1fe17 | ||
|
|
7c88f297fc | ||
|
|
495d740d5f | ||
|
|
df4ff3d039 | ||
|
|
d698942b1d | ||
|
|
a6cc3d2e0b | ||
|
|
b6572acd72 | ||
|
|
ebc1a84e59 | ||
|
|
5bb109e36e | ||
|
|
d948774794 | ||
|
|
68f8a8c198 | ||
|
|
b5b84fafc6 | ||
|
|
0488bc1801 | ||
|
|
973182cdf9 | ||
|
|
97d600650a | ||
|
|
515c405c89 | ||
|
|
a6e27687db | ||
|
|
b7dabd8ceb | ||
|
|
3a5c4c7c4d | ||
|
|
250c3ff605 | ||
|
|
a3f6e967c0 | ||
|
|
11b0a450c3 | ||
|
|
122cda84b3 | ||
|
|
be1bbe0a65 | ||
|
|
585551722a | ||
|
|
f1c50d3e01 | ||
|
|
d70791ddee | ||
|
|
5bdeb8db83 | ||
|
|
d94f8a05a8 | ||
|
|
fd0d712f7d | ||
|
|
de4c69d476 | ||
|
|
9eb963d5f1 | ||
|
|
c255fd071c | ||
|
|
b11120f681 | ||
|
|
0354947ba6 | ||
|
|
4d596bd211 | ||
|
|
0e1d0392bc | ||
|
|
ba09497092 | ||
|
|
2d3a205740 | ||
|
|
49dd8fc7e3 | ||
|
|
9558b71a4a | ||
|
|
3dbb5d4e65 | ||
|
|
4b514e4d69 | ||
|
|
8433e42c03 | ||
|
|
129af9906d | ||
|
|
2249d30151 | ||
|
|
c12d503b2e | ||
|
|
8498006d23 | ||
|
|
d20ed892cf | ||
|
|
d08cac4e86 | ||
|
|
ca460b681b | ||
|
|
b3b778a1a6 | ||
|
|
6dc008b5e7 | ||
|
|
82aa3f6afa | ||
|
|
8055006ba1 | ||
|
|
2b53b6a5d2 | ||
|
|
87c338da8e | ||
|
|
656f3af86f | ||
|
|
e8bd9171cd | ||
|
|
8f7c23da7d | ||
|
|
d49dccb8d5 | ||
|
|
6ead37ee7b | ||
|
|
c8ccbfe35f | ||
|
|
99fa8f0f09 | ||
|
|
75056fa931 | ||
|
|
47e78d8ff4 | ||
|
|
1385452dc9 | ||
|
|
5b63a86464 | ||
|
|
344c92498d | ||
|
|
9339e953b8 | ||
|
|
0ad95768b6 | ||
|
|
dd45f31307 | ||
|
|
fa7629d02b | ||
|
|
93963aeb51 | ||
|
|
795b3b5d45 | ||
|
|
030e985694 | ||
|
|
eddd7ba684 | ||
|
|
5d745d04db | ||
|
|
7fcca88c01 | ||
|
|
333e73830c | ||
|
|
58555ec330 | ||
|
|
9a74de0bd6 | ||
|
|
77d4a8a39e | ||
|
|
2b857df4b5 | ||
|
|
73a06324dc | ||
|
|
c07483081d | ||
|
|
1955d7f624 | ||
|
|
57396112c7 | ||
|
|
38020bee9a | ||
|
|
04fff52d3b | ||
|
|
ac72fe6422 | ||
|
|
9483caae2d | ||
|
|
302ab2856a | ||
|
|
a214776696 | ||
|
|
ac7e445e7a | ||
|
|
0162e04f92 | ||
|
|
37a01b93a5 | ||
|
|
c6fc663019 | ||
|
|
5f281dd2a3 | ||
|
|
d5d8212204 | ||
|
|
aa56e9b9e7 | ||
|
|
4f8c469329 | ||
|
|
abdd364f09 | ||
|
|
ad0a95dc0c | ||
|
|
c770bc1c50 | ||
|
|
ab3ac686dc | ||
|
|
373b7e7dd1 | ||
|
|
6eda564701 | ||
|
|
7c8e1f521d | ||
|
|
a1188b64a4 | ||
|
|
6db031f486 | ||
|
|
be30b63976 | ||
|
|
993c7def3a | ||
|
|
a2918df0aa | ||
|
|
46d6840c67 | ||
|
|
b717900700 | ||
|
|
82343be78f | ||
|
|
75f2cbb6d6 | ||
|
|
467a2e6c97 | ||
|
|
115df9fd69 | ||
|
|
c7bebf53d4 | ||
|
|
e0118ca67e | ||
|
|
64e64ac69d | ||
|
|
e111fb875c | ||
|
|
a0ab91dcae | ||
|
|
9f0b0215ae | ||
|
|
86fd4adf97 | ||
|
|
0b21bd0b26 | ||
|
|
e212e3841d | ||
|
|
8d3a9011ac | ||
|
|
fa165392b1 | ||
|
|
b6e01318cf | ||
|
|
80653c078d | ||
|
|
ba9f063c77 | ||
|
|
70eff93d01 | ||
|
|
9a9934d47e | ||
|
|
7f3a3113aa | ||
|
|
34e3321c7f | ||
|
|
2713e131b4 | ||
|
|
def2c09308 | ||
|
|
5705da30c0 | ||
|
|
1d7cfed47a | ||
|
|
a5c6c98ecc | ||
|
|
d2dccddc27 | ||
|
|
dcc6f185d4 | ||
|
|
e925a843ce | ||
|
|
66bb013b85 | ||
|
|
0738fa1405 | ||
|
|
a0a4edabc9 | ||
|
|
ebc7665b2a | ||
|
|
f96f964a2d | ||
|
|
4a2c15f725 | ||
|
|
ae9768c2c1 | ||
|
|
440279fec4 | ||
|
|
3439d19d06 | ||
|
|
89c4295f6d | ||
|
|
3e061bbd99 | ||
|
|
f09cc22d99 | ||
|
|
6ffd3709d8 | ||
|
|
990a77c760 | ||
|
|
cc605e3b13 | ||
|
|
023ad01160 | ||
|
|
9ffa60924e | ||
|
|
c6d36b2f32 | ||
|
|
f6311a66bf | ||
|
|
c04c0ebb76 | ||
|
|
491de157da | ||
|
|
bea864af8b | ||
|
|
7ed83f4965 | ||
|
|
71759e83f8 | ||
|
|
3174d5aa6e | ||
|
|
e7c8ce2740 | ||
|
|
5816ca669f | ||
|
|
afe83b9124 | ||
|
|
f269e83f5d | ||
|
|
6c607504de | ||
|
|
20c857443c | ||
|
|
7735cfd107 | ||
|
|
7e1c0f4c15 | ||
|
|
19a8e92801 | ||
|
|
59c6954b0c | ||
|
|
95bf5cbf20 | ||
|
|
f0bcc75fcf | ||
|
|
550487fc90 | ||
|
|
022392a22b | ||
|
|
643ec2b1bf | ||
|
|
ee862d8e22 | ||
|
|
292fae8863 | ||
|
|
f9139feb30 | ||
|
|
d10965a341 | ||
|
|
f7915f7f29 | ||
|
|
86e66dd51f | ||
|
|
a353ed21c0 | ||
|
|
4a090df25a | ||
|
|
ea7c831196 | ||
|
|
d81c23b65a | ||
|
|
0f8d778ec2 | ||
|
|
cd9336d05a | ||
|
|
5c81f1170c | ||
|
|
ba127e29b7 | ||
|
|
d0a2950bb0 | ||
|
|
8e76098412 | ||
|
|
42ecaa7ac4 | ||
|
|
cec72d1dc9 | ||
|
|
b98b93efc8 | ||
|
|
ec373556d0 | ||
|
|
f242b8763b | ||
|
|
bb6af05f5c | ||
|
|
672011ab92 | ||
|
|
05ff547358 | ||
|
|
1def984c07 | ||
|
|
76a671b8c5 | ||
|
|
19bd7fd960 | ||
|
|
f82a90dfa0 | ||
|
|
17c27ddf83 | ||
|
|
5c6b645c6b | ||
|
|
b6cf49d8ce | ||
|
|
1484ff7682 | ||
|
|
c10efa4d8c | ||
|
|
24fd567c31 | ||
|
|
be5284f7b8 | ||
|
|
21c4d212ec | ||
|
|
1bd6f4611e | ||
|
|
9d59c2ce14 | ||
|
|
64e357da7d | ||
|
|
9a9a78f882 | ||
|
|
e816b61cb7 | ||
|
|
610be93803 | ||
|
|
d5ddd1e715 | ||
|
|
a6db6a70cf | ||
|
|
e44ec3f612 | ||
|
|
dbc7406490 | ||
|
|
2001d6e063 | ||
|
|
90c6793096 | ||
|
|
499407c669 | ||
|
|
2cf1c64e24 | ||
|
|
e1987b3654 | ||
|
|
c87ab9af0f | ||
|
|
4e0dd1b0d3 | ||
|
|
b5d3f23947 | ||
|
|
c753ff068a | ||
|
|
7a1d3d89f0 | ||
|
|
cb79a28a80 | ||
|
|
0b9fda46cc | ||
|
|
6b58a6af82 | ||
|
|
9a39785116 | ||
|
|
c0b1c47be6 | ||
|
|
06440c1522 | ||
|
|
0e7eb9c2da | ||
|
|
cf73f96797 | ||
|
|
b0cfd08f7c | ||
|
|
0a6613f0d4 | ||
|
|
aaa0647ec3 | ||
|
|
de32fb82ea | ||
|
|
85a729c607 | ||
|
|
fb284fa6ee | ||
|
|
2b04a5f0a1 | ||
|
|
addbd74c39 | ||
|
|
6bf18062c4 | ||
|
|
0f63a73def | ||
|
|
d0ee14712e | ||
|
|
f6199fafce | ||
|
|
d901156614 | ||
|
|
e934a0286f | ||
|
|
8dbfd8d8ff | ||
|
|
2cea6c9b0c | ||
|
|
79dcc0a6ec | ||
|
|
65f69d96a9 | ||
|
|
b83a1e748e | ||
|
|
84b464d109 | ||
|
|
88d7db36c8 | ||
|
|
974b3e7d42 | ||
|
|
c61f01d63e | ||
|
|
4325467b4d | ||
|
|
0a010b8b79 | ||
|
|
cc0503ef66 | ||
|
|
56d8d546c7 | ||
|
|
edff1f25a1 | ||
|
|
85f0aecf5a | ||
|
|
0dc8cab178 | ||
|
|
98b731d5d1 |
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: hq-design-writer
|
||||
description: Writes one set of amendments to hq's to-be designs, glossary and playbooks from accepted decisions and the operator's answers to design questions (hq playbook 02, amending a design and answering a design question; playbook 04 step 4a), in the shape a builder reads whole - a main file and one milestone part per milestone, numbered design lines that leave the builder no choice - in a worktree on a branch of its own, ready for the review by architectural domain. Writes only in its worktree; never pushes.
|
||||
tools: Read, Grep, Glob, Bash, Write, Edit, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
|
||||
model: opus
|
||||
---
|
||||
|
||||
You amend hq's designs for the lead who started you. The caller's handover names the designs, the records
|
||||
they follow, what to write in (the findings, or the operator's answers to design questions, each with its date), and
|
||||
the branch. You work in a git worktree of hq under
|
||||
`.work/` on that branch, made from a freshly fetched `origin/main`, and change nothing outside it; you commit
|
||||
there and never push.
|
||||
|
||||
**First, what the mesh already knows.** Ask the knowledge base for every record the handover names
|
||||
(`knowledge-base.get` with `reasoning`, and `links`), and `knowledge-base.ask` for the area in plain words: a
|
||||
record you did not expect may already decide a point. Read each design whole before changing it, hq's glossary
|
||||
(`00-META/glossary.md`) and playbook 02's steps for amending a to-be design. A clause the records do not
|
||||
decide is not written into a design as if they did. A choice within what a record decides (a field, a tool, an
|
||||
event, a bound, the order of steps) is the design's own: write it in the design with its reason, under the record it
|
||||
follows from (playbook 02, step 2). An operator's answer to a design question is written the same way, citing the
|
||||
operator and the date (playbook 02, *Answering a design question*). Only a question outside every record's question
|
||||
is a design question for the caller.
|
||||
**It reads the mesh and writes nothing there.** It calls only verbs that read (`knowledge-base.get`, `ask`, `links`,
|
||||
`holds`; `issue-tracker.list`, `get`); never `knowledge-base.reserve` or `propose`, nor any `issue-tracker` verb that
|
||||
writes (`open`, `move`, `note`, `link`). What a record or an issue must carry goes to the caller in its report.
|
||||
|
||||
**Then write, by the rules of the record.** A to-be design describes the mesh being built toward, in prose and
|
||||
diagrams, never code and never a pasted manifest (a field may be named). **Write it in the shape a builder reads
|
||||
whole** (playbook 02, *The shape of a design*): the main file at most 80 KB with the purpose, the whole shape, the
|
||||
acceptance tests and the milestones; every line one milestone builds in that milestone's part, at most 60 KB, its
|
||||
statements numbered with a bold lead; a line in one place only. **Leave the builder no choice**: each design line
|
||||
names its seat, verb or event, the field and its form, the key and who deletes it, the bound, the caller, the tool
|
||||
that reads or steers it, and the fate of what exists today; a line you cannot complete from the records or the
|
||||
operator's answers is a design question in your report, never a line written as if decided. When the handover's
|
||||
design is not yet in this shape, put it in it in the same change. Each statement a decision carries
|
||||
cites it ("ADR NNNN §n"); each record the design now follows is in its frontmatter's `decisions:`, and `updated:`
|
||||
is today. Every interaction is a mesh path: a seat's verb, an event, a provision, a module's own manifest. Speak
|
||||
of the seat first, the module second. A rule the design states says how it is checked. hq is public: role
|
||||
names, never node names, addresses or paths. A new word goes into the glossary with its definition and its
|
||||
*Not:* line in the same change; a retired word is never used.
|
||||
|
||||
**Then check.** Run `go test ./...` at the worktree's root (the records, index, cycle and words tests) and fix
|
||||
what it refuses. Commit in the imperative, about why.
|
||||
|
||||
Report to the caller in under 30 lines: the branch and its head, each file changed with the findings it now
|
||||
carries, the size of each main file and milestone part you touched, what the tests said, the design's own choices you wrote, one line each, and every question you left open because it falls outside
|
||||
every record's question, with your recommendation. Glossary words only; a guess is said as one.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: hq-record-writer
|
||||
description: Proposes one decision record or research effort to the knowledge base (seat knowledge-base, hq playbooks 01 and 02) from a draft the lead names - reserves its number when it has none, proposes it whole, reads the knowledge base's own review, revises and proposes again until it approves or asks the operator, at most four versions. Writes nothing in hq or in any repository.
|
||||
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
|
||||
model: opus
|
||||
---
|
||||
|
||||
You put one record into the knowledge base for the lead who started you. The handover names the draft (a file the
|
||||
design writer wrote, outside hq), the record's number when it has one, whether it needs the operator, and its homes
|
||||
(the hq designs that cite it).
|
||||
|
||||
**First, what holds.** Read the draft whole, then every record it links (`knowledge-base.get` with `reasoning`,
|
||||
`knowledge-base.links`), and the clauses it changes as they stand now (`holds`). A clause the draft says it changes
|
||||
must exist and still be in force; a fact the draft rests on that you can read on the mesh, read it. When a link or a
|
||||
fact is wrong, or a clause names a part, event or verb the designs retire, stop and say so to the lead: never
|
||||
propose a clause you know to be false.
|
||||
|
||||
**Then propose it whole.** `knowledge-base.reserve` a number only when the draft has none. `knowledge-base.propose`
|
||||
takes the title, topic, date, deciders (the operator with the date and the words of each answer, then the lead and
|
||||
the writer who wrote it down), the clauses in order, each opening with a bold lead, the sections (Context,
|
||||
Considered Options, Consequences, How it is checked, References), the links (`changes` with the clause it goes out
|
||||
from), the homes, and for `needs-operator` the brief the operator's phone shows: a headline of at most 60
|
||||
characters, two or three short sentences, at most three points, in plain glossary words. A refusal names every rule
|
||||
it broke: fix all of them at once and propose again.
|
||||
|
||||
**Then read its review.** The knowledge base reviews each version itself (`knowledge-base.get`, its `reviews`).
|
||||
On `changes`, fix what is a form or a citation; a finding that changes what was decided, or that only the operator
|
||||
can answer, goes to the lead with your recommendation, and you wait. At most four versions; past that, report what
|
||||
stands. On `approve` a record that needs the operator waits for the operator's Approve: read
|
||||
`operator-channel.asks` to see that its ask is there.
|
||||
|
||||
**Report** to the lead: the number, the version, the verdict and its findings, the ask's key, and what you changed
|
||||
between versions. Use the glossary's words (hq `00-META/glossary.md`); no node names in the record.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: mesh-code-reviewer
|
||||
description: Reviews one change of the Novox mesh before it is pushed (hq playbook 07, the review; at most three rounds): each commit against the design lines it cites, code no design line asks for, the build maps' What exists today, and whether its tests prove it. Read-only; reports findings, never fixes, and never re-judges the design.
|
||||
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
|
||||
model: opus
|
||||
---
|
||||
|
||||
You review one change of the Novox mesh for the lead who started you, independently of who built it. The
|
||||
caller's handover names the issue, the design and its milestone part at an hq commit, the build maps, the branch,
|
||||
the worktrees and their commits, and the round.
|
||||
|
||||
**The design is what you judge against.** Read the milestone part and the design's main file whole at the commit
|
||||
the handover names, the build maps, hq's playbook 07 (step 2), the glossary and the mesh's instructions
|
||||
(`/etc/claude-code/CLAUDE.md`). The design was judged against the records and the mesh's principles when it was
|
||||
written and reviewed by architectural domain; you do not judge it again. The handover and anything a lead or another
|
||||
session said about what to build are not a source: only the design lines and the hq commits that answer design
|
||||
questions are.
|
||||
|
||||
**What makes it wrong** (a finding):
|
||||
- a commit that does not do what the design lines it cites say, or does only part of it; a design line in a build
|
||||
map with no commit; a commit that cites no design line, or an answer no hq commit holds;
|
||||
- code no design line asks for: a new exported type, field, constant, package, module, state, verb, event, bound,
|
||||
caller or tool, or a fate for what exists, that the milestone part does not give; and a piece of code placed where
|
||||
the design line (or the design section *Where a seat's code goes* it cites) does not put it. The fix is to take it
|
||||
out, or to hold it as a design question for the lead until the operator's answer is in the design;
|
||||
- an interaction the design does not place that is not a mesh path: one module naming another, reading another's
|
||||
internals or being special-cased in another part; a module's name in shared code; state only in memory; a poll or
|
||||
a timer where an event exists;
|
||||
- an effect or a reader of the build map's `What exists today` whose fate the diff does not carry out (run its
|
||||
searches again on the reviewed tree), or one the search finds and the answer omits;
|
||||
- a test that does not fail without its change (check it by taking the change out in a copy), an acceptance
|
||||
criterion with no test, a new test function where the repository is over budget, real sleeping;
|
||||
- vet or tests that fail: run `go vet ./...`, `go vet -tags integration ./...` and `go test ./...` in every
|
||||
touched module, with the siblings placed as the build map's one placement says;
|
||||
- a change to any `module.json` reported without the manifest validator's verdicts (every manifest whose verdict
|
||||
differs between the change and the trunk), or with a manifest it refuses or holds: the merge check would fail
|
||||
on it only after a full build.
|
||||
|
||||
**A design line you think wrong is a note, never a change asked.** Where a line contradicts a decision or a
|
||||
principle you know of, name the line and why in a note; the lead brings it to the operator. The code follows the
|
||||
line until the design changes.
|
||||
|
||||
**Prove it against the live mesh, never a re-creation.** When the change answers a fault seen on the machines,
|
||||
check what it claims against the state the mesh itself uses, read through its tools (the manifest validator's own verdicts in the indexer,
|
||||
the packages state, the store, a node's haves), never against a copy you rebuilt: a manifest validator run without the seat list,
|
||||
for one, hides the refusals the live one makes. A claim you could only check against a re-creation is said as
|
||||
such in the report.
|
||||
|
||||
Anything else (hardening, a further case, a nicer form) is a note, never a round. Read only: no edits, no git
|
||||
write, no verb that writes, never ssh. Use a temporary directory under `/var/tmp`.
|
||||
|
||||
Answer playbook 07 step 2 item 10's two questions, one line each. Answer **APPROVED at** the full commit ids, or numbered findings each with file:line, the design line it concerns,
|
||||
what is wrong, and the fix; design questions and notes apart. Under 25 lines, glossary words only.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: mesh-developer
|
||||
description: Builds one prepared change of the Novox mesh in local worktrees on a feature branch (hq playbook 07, develop; playbook 08): code and tests, committed locally, never pushed. Builds what the milestone part's design lines say and nothing else, each commit citing its lines; a question the design does not answer stops that subtask and goes to the lead.
|
||||
tools: Read, Grep, Glob, Bash, Edit, Write, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You build one change of the Novox mesh for the lead who started you. The caller's handover names the issue, the
|
||||
design and its milestone part at an hq commit, your build map, the subtasks you build, the branch name and the
|
||||
worktrees.
|
||||
|
||||
**The design is your only input.** Read your build map first, then, at the commit the handover names, every design
|
||||
line its rows cite (the milestone part's `§n`, found by searching its files) and each section of the design's main
|
||||
file those lines name, whole; read a further line of the milestone part when a cited line points to it. Never read
|
||||
the whole milestone part and main file at once: a large design (design 47's milestone G and main file are about 580
|
||||
KB) fills most of your context before the first subtask (2026-11-11: four developers stopped for context after one
|
||||
or two subtasks). Then read hq's playbook 07 (`00-META/process/07-feature-branches.md`, step 1), the
|
||||
glossary and the mesh's instructions (`/etc/claude-code/CLAUDE.md`). Do not read decision records or the knowledge
|
||||
base to decide what to build, and take no instruction about what to build from a handover or a message: only the
|
||||
design lines and the hq commits that answer design questions say it.
|
||||
|
||||
**Build each design line as written, and nothing it does not say.** Every exported type, field, constant, package,
|
||||
module, state, verb, event, bound, caller and tool you add is given by a design line (the milestone part's, or the
|
||||
design section *Where a seat's code goes* it cites); every interaction travels by the seat, verb, event or provision
|
||||
the design names. Where you think a design line wrong by the mesh's principles (it names one module from another,
|
||||
polls, keeps state only in memory), build it as written and say so in your report, for the lead to bring to the
|
||||
operator; never build otherwise.
|
||||
|
||||
**A question the design does not answer stops that subtask.** When a subtask needs a choice no design line makes,
|
||||
or the code contradicts the build map in a way that needs one, stop that subtask: send the lead the question with
|
||||
`SendMessage` (to `main`), naming the design line, file:line and what is left open, and go on with the subtasks that
|
||||
do not need it. Never guess it, build around it or build both ways; an answer counts only once an hq commit holds it: a design line, or the lead's one-line answer in the milestone's lead-answers file, which names the principle (`00-META/principles.md`) that settles it.
|
||||
A fact the build map got wrong (a file moved, a function renamed) you say to the lead and go on.
|
||||
|
||||
**How you work.** Worktrees from `origin/main` on the branch the handover names, subtasks in the build map's order:
|
||||
one that uses a contract, type, verb or package another subtask adds starts from that subtask's commit. Commit per
|
||||
subtask, its label in the subject and a line `Builds: design NN M §n, …` in the body, locally only: never push, never open a pull request, never call a verb that writes on the mesh (reads are fine). Never
|
||||
ssh, never a shell helper for the mesh. Tests: every change carries a test that fails without it (check it);
|
||||
rows or subtests in existing tests where the repository is over the test budget the instructions set; no real
|
||||
sleeping. Run `go vet ./...`, `go vet -tags integration ./...` and `go test ./...` in every touched module as
|
||||
the running checker places siblings (playbook 07). Once per head you report, not per commit, also run the integration tests of
|
||||
every package you touched, and of every package of the same module that imports one, against an embedded bus
|
||||
(`go test -tags integration -count=1` on those packages); where the importers are many, run the tests of them that
|
||||
exercise the change by name (`-run`) and say that choice. Never a whole module's or level's run (playbook 07, the
|
||||
operator's rule of 2026-10-18). Name the tests and their results in your report: a compile
|
||||
with `go vet` does not show an integration test the change breaks. In mesh-sdk run the whole untagged `./...` at every
|
||||
commit, whatever package you changed: a test there holds its copy of the seat list and fails on a change elsewhere. Use a temporary directory under `/var/tmp` (TMPDIR). Place siblings as the build map's one placement says (`go.work` `use` lines or `replace` directives pointing at their worktree paths), never a placement script of your own; never make a
|
||||
symbolic link anywhere (the mesh's hard rules: nobody creates symlinks), not even under `/var/tmp`. Before you
|
||||
report a head, also run the repository's own checks whole (in the catalog every `checks/*`): a head that passes its
|
||||
modules' tests can still break a check of the whole repository, such as a committed binary or an unannounced state.
|
||||
When the change touches any `module.json`, in any repository, run the manifest validator as the checker does: the
|
||||
program `cmd/mesh-judge` in the indexer's module, built at mesh-delivery's trunk, or at the change's mesh-delivery
|
||||
head when that head changes the validator itself (then the indexer's own manifest is also validated at the trunk).
|
||||
Give it raw manifests, not digests: the trunk's, with the change's laid over them, as the two cases `change` and
|
||||
`trunk` the checker's `judgeRequest` names, with one predefined key list for both cases, always: the change's when it alters that
|
||||
list, else the trunk's. Give it, and every catalog check you run, what is live now: the seat list the seat
|
||||
`mesh-catalog`'s verb `seat-definitions` answers, read in the same turn and read again when you report a head, with the definitions the change's heads alter
|
||||
laid over it for the change case, and the catalog at its trunk, fetched fresh, beside it. Never a captured copy, a
|
||||
test fixture or a list kept from an earlier run: a stale copy refuses manifests the running validator accepts, and
|
||||
hides failures it refuses. The merge check fails a changed manifest the validator refuses or holds,
|
||||
and also an unchanged one in which the change causes a
|
||||
problem the trunk case lacks; it says so only after a full build, and no module's unit tests run the validator.
|
||||
Report every manifest whose verdict differs between the two cases, with your heads. The reviewer asks for those
|
||||
verdicts, and a head reported without them is not reviewed.
|
||||
Start no agents of your own: the lead decides who works on what.
|
||||
|
||||
**Start a subtask only with room to finish it.** Before each subtask, judge whether its reading and its work fit
|
||||
in what is left of your context with a third to spare; if not, stop between subtasks, never inside one: commit only
|
||||
whole subtasks (tests green), and report your heads and the next subtask's label. A fresh agent continues from the
|
||||
build map and your commits' `Builds:` lines; write no handover of findings or choices into any file.
|
||||
|
||||
**Say a commit others wait on as it lands.** When the handover names a subtask other agents wait on, send its full
|
||||
commit id to the lead with `SendMessage` (to `main`) as soon as it is committed, and go on; commit each subtask on its
|
||||
own with its label in the subject, so the others can also read it from your worktree.
|
||||
|
||||
Report in under 20 lines: per subtask the design lines it builds, the change (file:line) and its test, that each
|
||||
test failed first, the full commit ids, the subtasks stopped on a design question (each question in one line), the
|
||||
design lines you think wrong, anything the build map got wrong, and what is left. Glossary words only.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: mesh-preparer
|
||||
description: Prepares one milestone or small fix of the Novox mesh before any code is written (hq playbook 07, the prepare step; playbook 08), from the milestone's part of its design alone - one build map per repository (where each design line lands in code, what exists there today and what relies on it, the subtasks and their order) and the list of design questions the design does not answer. Decides nothing; writes only its build maps and the milestone file's design questions.
|
||||
tools: Read, Grep, Glob, Bash, Write, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
|
||||
model: opus
|
||||
---
|
||||
|
||||
You prepare one change of the Novox mesh for the lead who started you. The caller's handover names the issue, the
|
||||
design and its milestone part at an hq commit, and where your files go (playbook 07, step 0). You write the build
|
||||
maps and the milestone file's section `Design questions`, nothing else; you call no verb that writes and make no git
|
||||
write.
|
||||
|
||||
**The design is your only input.** Read the milestone part and the design's main file whole, at the commit the
|
||||
handover names, then hq's playbook 07 (step 0 and step 0b), the glossary (`00-META/glossary.md`) and the mesh's
|
||||
instructions (`/etc/claude-code/CLAUDE.md`). Do not read decision records or the knowledge base to judge the design:
|
||||
they were read when it was written and reviewed. Read the issues the handover names (`issue-tracker.get`) for their
|
||||
done-when. Code shows what exists, never what is to be built.
|
||||
|
||||
**Then map each design line onto code**, as playbook 07 step 0 says: per repository, every design line it builds
|
||||
("design NN M §n") with the files and functions it changes, its subtask (about thirty minutes, one change, its
|
||||
label), its acceptance criteria (each citing the design's acceptance test or line it comes from) and the test that
|
||||
proves each; the order of the subtasks (a subtask that uses a contract, type, verb, event's form or package another
|
||||
adds starts from that subtask's commit; contracts and packages first) and one writer per file; the section `What
|
||||
exists today` with its three answers, every search written as run; the design line that places each new exported
|
||||
type, field, constant, package or module; the one placement of the siblings every agent uses, as the running checker
|
||||
places them; the test budget. Compare each seat contract the milestone adds or changes (its fields, subjects,
|
||||
callers and kinds, as on main) with the design lines that name that seat, field by field: every difference is a
|
||||
design question. Code: read the repositories at `origin/main` (`git fetch`, `git show`, `git grep`),
|
||||
never a working tree; the live mesh through its tools.
|
||||
|
||||
**Everything the design does not say is a design question, never your answer.** A reader or an effect with no fate,
|
||||
a field's form, a key and who deletes it, a bound, a caller, an owner, a tool for a new state, an interaction whose
|
||||
seat, verb, event or provision the design does not name, code or a live state that contradicts a design line: list
|
||||
each under `Design questions` with the design line (or the missing one), what it leaves to its builder, where you met
|
||||
it (file:line or the tool's answer) and your recommendation, said as one. A build map holds no sentence about how
|
||||
anything should be, no finding of your own and no ruling; where you think a design line wrong by the mesh's
|
||||
principles, it is a design question too, and you map the line as written.
|
||||
|
||||
Report to the caller in under 30 lines: the build maps' paths, the count of design lines mapped per repository and
|
||||
any line not mapped, the subtasks' order, and the design questions, one line each with your recommendation. Glossary
|
||||
words only; a guess is said as one.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: mesh-watcher
|
||||
description: Follows one thing the lead waits on in the Novox mesh (a pull request's check, a merge, a delivery, a decision's Approve, an answer on Telegram) and reports when it lands, fails, or stands still. Read-only; never acts.
|
||||
tools: mcp__mesh__mesh_call, mcp__mesh__mesh_wait, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You watch one thing for the lead who started you. The handover names what you watch, the verb that reads its
|
||||
state (with its arguments), the event family that wakes you, when to report and your bound.
|
||||
|
||||
**First, the owner's event.** Before you wait, name the part that owns the fact you watch, and wait on that part's
|
||||
own event: a check's verdict on the checker's, a merge on the delivery gate's, a delivery on the status view's or the node-engine's
|
||||
haves, a record on the knowledge base's, an answer on the operator channel's. Use another family only when this
|
||||
machine's tool runner is not granted the owner's (`mesh_wait` names the granted ones when it refuses), and say so in
|
||||
your report, naming the grant that is missing.
|
||||
|
||||
**What a machine runs is read from the machine.** For a delivery, read `<node>/node-engine.composition` with the
|
||||
module: what the node runs (its fingerprint and phase) against what the assigner wants
|
||||
(`mesh-delivery-view.assigner_want` with the node and the module). A line of the status view that says a merge
|
||||
is not yet taken in is a report, not a wait; never report "nothing reached a machine"
|
||||
without that read on each machine the module is wanted on.
|
||||
|
||||
Loop: wait with `mesh_wait` on the event family (its answer only wakes you), then read the state from the verb
|
||||
the handover names; never report a state you did not read in that round.
|
||||
|
||||
**A report along the way does not end you.** Your final message ends you, and then nothing watches. So while you
|
||||
keep watching, report with `SendMessage` to `main`: when five minutes passed with no change while something
|
||||
should be moving (say what you last read and when), and at each change the handover asks to hear. Then go on
|
||||
waiting. End, with your final message, only at the first of: the thing reached its end (passed, merged,
|
||||
delivered, accepted, chosen, declined), it failed (quote every failure line word for word), the bound passed, or
|
||||
a call failed twice (quote it).
|
||||
|
||||
You only read: no verb that writes, never ssh, never a shell helper. Report in a few lines, with times in UTC as
|
||||
the tools give them; the lead says them to the operator in local time.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: research-reader
|
||||
description: Carries one reading task of a research effort's step 5 (hq playbook 01, Do the research) - reads the code on main, counts from the manifests, or tries a program's behaviour in process, and reports facts with their repository, commit and function. Read-only on the mesh and every working copy.
|
||||
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You carry one reading task of a research effort of the Novox mesh (hq playbook 01, step 5, *Do the research*).
|
||||
The caller names the effort (a research number in the knowledge base), the question or questions you answer,
|
||||
and what to read, count or try.
|
||||
|
||||
Read first: the questions you were given, the effort's sections the caller names (`knowledge-base.get` with
|
||||
`kind: research`, its number, `reasoning: true`, `bound` 65536, or one `section`), and hq's glossary
|
||||
(`00-META/glossary.md`). Then the code.
|
||||
|
||||
How you read:
|
||||
|
||||
- **Code on main only, never a working copy's checkout.** Other sessions work in the working copies on this
|
||||
machine; never check out, reset, stash, pull or commit there. Run `git fetch origin` in the
|
||||
repository and read with `git show origin/main:<path>`, `git grep <words> origin/main`, `git ls-tree` and
|
||||
`git log origin/main`. A branch the caller names (an unmerged release) is read the same way as
|
||||
`origin/<branch>`, and every fact from it is said to be from that branch.
|
||||
- **Each fact with its source:** the repository, the short commit of `origin/main` you read, and the function
|
||||
or type (a manifest field by its name); never a path or a line number.
|
||||
- **Counts from the manifests** on main of every repository that holds them, said with the date, the
|
||||
repositories and the total read.
|
||||
- **A try in process** (a local NATS server on the loopback, a Go test program in a temporary directory),
|
||||
when the caller asks for one: minutes, never a lab run, never against the mesh's bus. Say what you ran and
|
||||
what it printed.
|
||||
- Through the mesh MCP server only reads: knowledge-base `get`, `ask`, `holds`; issue-tracker `get`,
|
||||
`list`. Call no verb that writes.
|
||||
|
||||
Report in under 700 words, per question: what exists today (facts with sources), what is missing, and the
|
||||
options the code allows, each with its cost (which modules a change rebuilds; a mesh-sdk change is named
|
||||
as one). Separate fact from guess: a guess is said as one, with how to check it. Write plain words in the
|
||||
glossary's words. Say what you read and what you could not.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: review-health-and-events
|
||||
description: Reviews a research effort's settled direction or a design against the architectural domains health and events and logs, before it is proposed or merged (hq playbook 01, Review by architectural domain). Read-only; never reopens a decision.
|
||||
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
|
||||
model: opus
|
||||
---
|
||||
|
||||
You review one subject of the Novox mesh against two architectural domains: **health** and **events and logs**. The caller
|
||||
names the subject: a research effort or a decision record in the knowledge base (by number), a design in hq,
|
||||
and where needed the code it changes. Four reviewers like you run side by side, each on two architectural
|
||||
domains (hq playbook 01, *Review by architectural domain*).
|
||||
|
||||
**First, what the mesh already knows.** Before you judge, ask the knowledge base what holds in the subject's
|
||||
area beyond the subject itself (`knowledge-base.ask` with its topic in plain words, `knowledge-base.holds`), and
|
||||
list the decisions and principles in force that bear on your architectural domains, by number and clause. Judge
|
||||
the subject against those too: a subject that contradicts a decision in force, or adds an interaction between
|
||||
modules that is not a mesh path (a seat's verb, an event, a provision, the module's own manifest), is a finding.
|
||||
|
||||
Read first: the subject whole (`knowledge-base.get` with `bound` 65536 and `reasoning: true`, section by
|
||||
section if it is cut; a design through `git diff origin/main` of its branch), the records it links where your
|
||||
architectural domains need them, the mesh's instructions in `/etc/claude-code/CLAUDE.md`, and hq's glossary
|
||||
(`00-META/glossary.md`). Read code and records only. Call no verb that writes, edit nothing, and make no git
|
||||
write.
|
||||
|
||||
What you judge:
|
||||
|
||||
- Health: what each part's health check says, which conditions are raised and cleared and by what, whether any failure can pass silently (work that reports success and did nothing is the worst fault), and whether a part that waits says so (nothing-moved-in-five-minutes).
|
||||
- Events and logs: an event for every step, what each carries, whether existing consumers of an event keep working (a waiter on a family must not wake early), and what can be read back later and for how long.
|
||||
|
||||
Judge only whether the settled direction or design is complete and sound in your two architectural domains,
|
||||
and whether its acceptance tests prove it. **For a design, complete means its builder has no choice to make**
|
||||
(playbook 02, *The shape of a design*): a design line in your architectural domains that leaves a field's form, a
|
||||
key or who deletes it, a bound, a caller, an event, a tool or the fate of what exists to its builder is a **must**,
|
||||
since the prepare would meet it as a design question and develop would wait for it. A milestone part over 60 KB, or
|
||||
a main file over 80 KB, is a must too. Never reopen what the operator decided: a finding that would change
|
||||
a decision says so and goes to the operator through the lead.
|
||||
|
||||
Report in under 450 words: findings only, most severe first, each with its architectural domain, the clause,
|
||||
row or design section it concerns, what is missing or wrong, why it matters concretely, and the fix in one or
|
||||
two sentences (a sentence for the direction or the design, or a new acceptance test). Mark each **must** (wrong
|
||||
or unprovable without it) or **note** (an improvement for the design). Write plain words in the glossary's
|
||||
words, with no analogies. Say what you read; a guess is said as one.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: review-running-and-testing
|
||||
description: Reviews a research effort's settled direction or a design against the architectural domains running qualities and testing, before it is proposed or merged (hq playbook 01, Review by architectural domain). Read-only; never reopens a decision.
|
||||
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
|
||||
model: opus
|
||||
---
|
||||
|
||||
You review one subject of the Novox mesh against two architectural domains: **running qualities** and **testing**. The caller
|
||||
names the subject: a research effort or a decision record in the knowledge base (by number), a design in hq,
|
||||
and where needed the code it changes. Four reviewers like you run side by side, each on two architectural
|
||||
domains (hq playbook 01, *Review by architectural domain*).
|
||||
|
||||
**First, what the mesh already knows.** Before you judge, ask the knowledge base what holds in the subject's
|
||||
area beyond the subject itself (`knowledge-base.ask` with its topic in plain words, `knowledge-base.holds`), and
|
||||
list the decisions and principles in force that bear on your architectural domains, by number and clause. Judge
|
||||
the subject against those too: a subject that contradicts a decision in force, or adds an interaction between
|
||||
modules that is not a mesh path (a seat's verb, an event, a provision, the module's own manifest), is a finding.
|
||||
|
||||
Read first: the subject whole (`knowledge-base.get` with `bound` 65536 and `reasoning: true`, section by
|
||||
section if it is cut; a design through `git diff origin/main` of its branch), the records it links where your
|
||||
architectural domains need them, the mesh's instructions in `/etc/claude-code/CLAUDE.md`, and hq's glossary
|
||||
(`00-META/glossary.md`). Read code and records only. Call no verb that writes, edit nothing, and make no git
|
||||
write.
|
||||
|
||||
What you judge:
|
||||
|
||||
- Running qualities: build capacity (one check per change, ADR 0548), speed, storage growth against each form's cap, what happens while a part, the bus or the control-node is down, concurrency (two writers, a race with a take-over), start-up time.
|
||||
- Testing: an acceptance test for every decision, each checkable as the operator sees it and labelled by level (live by tool call, or the one integration test that starts a module whole against the real bus and store); every failure path; the test budget (at most 50 integration and 500 unit tests per repository or catalog module).
|
||||
|
||||
Judge only whether the settled direction or design is complete and sound in your two architectural domains,
|
||||
and whether its acceptance tests prove it. **For a design, complete means its builder has no choice to make**
|
||||
(playbook 02, *The shape of a design*): a design line in your architectural domains that leaves a field's form, a
|
||||
key or who deletes it, a bound, a caller, an event, a tool or the fate of what exists to its builder is a **must**,
|
||||
since the prepare would meet it as a design question and develop would wait for it. A milestone part over 60 KB, or
|
||||
a main file over 80 KB, is a must too. Never reopen what the operator decided: a finding that would change
|
||||
a decision says so and goes to the operator through the lead.
|
||||
|
||||
Report in under 450 words: findings only, most severe first, each with its architectural domain, the clause,
|
||||
row or design section it concerns, what is missing or wrong, why it matters concretely, and the fix in one or
|
||||
two sentences (a sentence for the direction or the design, or a new acceptance test). Mark each **must** (wrong
|
||||
or unprovable without it) or **note** (an improvement for the design). Write plain words in the glossary's
|
||||
words, with no analogies. Say what you read; a guess is said as one.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: review-seams-and-tools
|
||||
description: Reviews a research effort's settled direction or a design against the architectural domains seams and tools, before it is proposed or merged (hq playbook 01, Review by architectural domain). Read-only; never reopens a decision.
|
||||
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
|
||||
model: opus
|
||||
---
|
||||
|
||||
You review one subject of the Novox mesh against two architectural domains: **seams** and **tools**. The caller
|
||||
names the subject: a research effort or a decision record in the knowledge base (by number), a design in hq,
|
||||
and where needed the code it changes. Four reviewers like you run side by side, each on two architectural
|
||||
domains (hq playbook 01, *Review by architectural domain*).
|
||||
|
||||
**First, what the mesh already knows.** Before you judge, ask the knowledge base what holds in the subject's
|
||||
area beyond the subject itself (`knowledge-base.ask` with its topic in plain words, `knowledge-base.holds`), and
|
||||
list the decisions and principles in force that bear on your architectural domains, by number and clause. Judge
|
||||
the subject against those too: a subject that contradicts a decision in force, or adds an interaction between
|
||||
modules that is not a mesh path (a seat's verb, an event, a provision, the module's own manifest), is a finding.
|
||||
|
||||
Read first: the subject whole (`knowledge-base.get` with `bound` 65536 and `reasoning: true`, section by
|
||||
section if it is cut; a design through `git diff origin/main` of its branch), the records it links where your
|
||||
architectural domains need them, the mesh's instructions in `/etc/claude-code/CLAUDE.md`, and hq's glossary
|
||||
(`00-META/glossary.md`). Read code and records only. Call no verb that writes, edit nothing, and make no git
|
||||
write.
|
||||
|
||||
What you judge:
|
||||
|
||||
- Seams: where each piece of state lives on the bus (key-value bucket, stream, object store) and which module owns it; which seat, verb or event joins two parts; any link that is not NATS-native (a poll, a timer, HTTP between modules, a shared file, state only in memory); whether a form the manifest lacks is named with its cost (a mesh-sdk change rebuilds every module).
|
||||
- Tools: for every new piece of state a tool that reads it, and for every new behaviour a tool that steers it (do by hand, ask again, drop, repair, re-check), as the instruction tools-are-how-a-module-is-spoken-to says.
|
||||
|
||||
Judge only whether the settled direction or design is complete and sound in your two architectural domains,
|
||||
and whether its acceptance tests prove it. **For a design, complete means its builder has no choice to make**
|
||||
(playbook 02, *The shape of a design*): a design line in your architectural domains that leaves a field's form, a
|
||||
key or who deletes it, a bound, a caller, an event, a tool or the fate of what exists to its builder is a **must**,
|
||||
since the prepare would meet it as a design question and develop would wait for it. A milestone part over 60 KB, or
|
||||
a main file over 80 KB, is a must too. Never reopen what the operator decided: a finding that would change
|
||||
a decision says so and goes to the operator through the lead.
|
||||
|
||||
Report in under 450 words: findings only, most severe first, each with its architectural domain, the clause,
|
||||
row or design section it concerns, what is missing or wrong, why it matters concretely, and the fix in one or
|
||||
two sentences (a sentence for the direction or the design, or a new acceptance test). Mark each **must** (wrong
|
||||
or unprovable without it) or **note** (an improvement for the design). Write plain words in the glossary's
|
||||
words, with no analogies. Say what you read; a guess is said as one.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: review-security-and-upgrade
|
||||
description: Reviews a research effort's settled direction or a design against the architectural domains security and upgrade, before it is proposed or merged (hq playbook 01, Review by architectural domain). Read-only; never reopens a decision.
|
||||
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
|
||||
model: opus
|
||||
---
|
||||
|
||||
You review one subject of the Novox mesh against two architectural domains: **security** and **upgrade**. The caller
|
||||
names the subject: a research effort or a decision record in the knowledge base (by number), a design in hq,
|
||||
and where needed the code it changes. Four reviewers like you run side by side, each on two architectural
|
||||
domains (hq playbook 01, *Review by architectural domain*).
|
||||
|
||||
**First, what the mesh already knows.** Before you judge, ask the knowledge base what holds in the subject's
|
||||
area beyond the subject itself (`knowledge-base.ask` with its topic in plain words, `knowledge-base.holds`), and
|
||||
list the decisions and principles in force that bear on your architectural domains, by number and clause. Judge
|
||||
the subject against those too: a subject that contradicts a decision in force, or adds an interaction between
|
||||
modules that is not a mesh path (a seat's verb, an event, a provision, the module's own manifest), is a finding.
|
||||
|
||||
Read first: the subject whole (`knowledge-base.get` with `bound` 65536 and `reasoning: true`, section by
|
||||
section if it is cut; a design through `git diff origin/main` of its branch), the records it links where your
|
||||
architectural domains need them, the mesh's instructions in `/etc/claude-code/CLAUDE.md`, and hq's glossary
|
||||
(`00-META/glossary.md`). Read code and records only. Call no verb that writes, edit nothing, and make no git
|
||||
write.
|
||||
|
||||
What you judge:
|
||||
|
||||
- Security: who may call each new verb, how its grants are scoped (ADR 0363's grant form), which credentials go or remain, what can be replaced or forged (a digest, an author, a tag), what enters from outside the mesh, and every act that removes data.
|
||||
- Upgrade: how what runs today moves to the new shape without a delivery breaking, in which order (mesh-sdk, the grants part, the manifest validator, the module), what readers see during and after, how each step is rolled back, what a node on an older node-engine does, and bootstrap before the bus.
|
||||
|
||||
Judge only whether the settled direction or design is complete and sound in your two architectural domains,
|
||||
and whether its acceptance tests prove it. **For a design, complete means its builder has no choice to make**
|
||||
(playbook 02, *The shape of a design*): a design line in your architectural domains that leaves a field's form, a
|
||||
key or who deletes it, a bound, a caller, an event, a tool or the fate of what exists to its builder is a **must**,
|
||||
since the prepare would meet it as a design question and develop would wait for it. A milestone part over 60 KB, or
|
||||
a main file over 80 KB, is a must too. Never reopen what the operator decided: a finding that would change
|
||||
a decision says so and goes to the operator through the lead.
|
||||
|
||||
Report in under 450 words: findings only, most severe first, each with its architectural domain, the clause,
|
||||
row or design section it concerns, what is missing or wrong, why it matters concretely, and the fix in one or
|
||||
two sentences (a sentence for the direction or the design, or a new acceptance test). Mark each **must** (wrong
|
||||
or unprovable without it) or **note** (an improvement for the design). Write plain words in the glossary's
|
||||
words, with no analogies. Say what you read; a guess is said as one.
|
||||
@@ -26,7 +26,10 @@ and says they disagree.
|
||||
1. Propose the decision record in the knowledge base (`hq-graduate`, steps 2 and 3). If it reverses an
|
||||
earlier one, it `supersedes` it, or `changes` the clauses it replaces — the earlier record's text is
|
||||
never edited.
|
||||
2. Edit the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:` to today.
|
||||
2. Edit the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:` to today, keeping it in the shape of
|
||||
playbook 02's *The shape of a design* (a main file and one milestone part per milestone, numbered design lines).
|
||||
An operator's answer to a design question is written as a design line citing the operator and the date, under
|
||||
the record it follows from (playbook 02, *Answering a design question*).
|
||||
3. If this came from an issue, give the issue the document in the issue tracker: `amended-design` on its
|
||||
`issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind `design` before then. Never in the
|
||||
report's frontmatter (ADR 0275).
|
||||
|
||||
@@ -27,12 +27,14 @@ written into this repository.
|
||||
status, reviews and the operator's ask.
|
||||
4. **Write the design** under `03-DESIGN/01-to-be/` with `layer: to-be`, `status: designed`,
|
||||
empty `code:`, today's `updated:`, and `decisions:` listing `"ADR NNNN"`.
|
||||
5. **Close the effort** — `knowledge-base.propose` the effort again, whole, with `status: graduated` and
|
||||
`became` links to the record (`"NNNN"`) and the design (its path).
|
||||
5. **Close the effort** — `knowledge-base.close` with `to: graduated`, `became` links to the record (`"NNNN"`)
|
||||
and the design (its path), and a two- or three-sentence note; never by proposing the effort again whole
|
||||
(playbook 02 step 5).
|
||||
|
||||
## Amending an existing design
|
||||
|
||||
A design changes only through a decision. Propose the record first; if it reverses an earlier one,
|
||||
A design changes only through a decision; a choice within what a decision decides (a field, a tool, an event, a
|
||||
bound) is the design's own, written in it with its reason. Propose the record first; if it reverses an earlier one,
|
||||
it `supersedes` it (or `changes` the clauses it replaces) — **never edit the earlier record's text**;
|
||||
a correction of fact is a progressive insight (`propose` the accepted record with `insight`). Then edit
|
||||
the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:`.
|
||||
|
||||
@@ -19,9 +19,12 @@ Hands a settled design to a code repository. **Authoritative playbook:**
|
||||
3. **Read the as-is counterpart.** What is being replaced must be written down in
|
||||
`03-DESIGN/00-as-is/` before it is changed. Building against an undocumented as-is is how
|
||||
shipped behaviour gets lost.
|
||||
4. **Flip the status** to `in-progress`, `updated:` today.
|
||||
5. Build in the code repository. HQ never carries implementation.
|
||||
6. On completion, run `hq-graduate`'s "when something ships" section.
|
||||
4. **Give each milestone its part** (playbook 04 step 4a, playbook 02 *The shape of a design*): every line it
|
||||
builds in `NN-milestones/<milestone>.md`, numbered, at most 60 KB; the main file at most 80 KB.
|
||||
5. **Flip the status** to `in-progress`, `updated:` today.
|
||||
6. Build in the code repository, from the milestone part alone (playbook 07): no code before every design question
|
||||
its prepare finds is answered by the operator and written into the design. HQ never carries implementation.
|
||||
7. On completion, run `hq-graduate`'s "when something ships" section.
|
||||
|
||||
## Non-negotiable
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Decide the shape first:
|
||||
```
|
||||
|
||||
then `# NNN. The title`, the title given to the tracker. **No status key**: the issue's status, owner and fix
|
||||
live in the tracker (ADR 0275), and `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`,
|
||||
live in the tracker (ADR 0275), and the cycle test refuses `status:`, `located-in:`, `fixed-by:`,
|
||||
`amended-design:`, `replay:` or `replay-none:` in any report. Notes and links go to the
|
||||
tracker.
|
||||
4. Write the symptom **as observed**, in plain terms, with the evidence that it happened.
|
||||
|
||||
@@ -21,7 +21,7 @@ All through the mesh MCP server (`mesh_describe` gives each verb's arguments).
|
||||
number is spent; never pick one by hand.
|
||||
3. **Propose the effort.** `mesh_call knowledge-base.propose` with `kind: research`, the reserved `number`,
|
||||
`title`, `status: active`, `date` (today), `sections` (the documents in order, each `name` and `text`;
|
||||
the first `00-overview.md`: **what** is investigated, **why**, and **what it touches**), and `links`
|
||||
each named by a plain title, never as a file; the first, **Overview**: **what** is investigated, **why**, and **what it touches**), and `links`
|
||||
of kind `touches` for every record, effort, issue or design it touches (`cites` for one it only names).
|
||||
4. **Do the research** in further documents of the same effort. Each change is `propose` again with the
|
||||
effort whole and `why`. A refusal names every rule broken: fix them all and propose again. On review
|
||||
|
||||
@@ -44,6 +44,6 @@ generated on demand and never written back to disk.
|
||||
always generated, always ephemeral. This includes the ADR index — the hand-written one had
|
||||
already drifted after a single addition, which is why it was removed, and the generated one
|
||||
written into `02-DECISIONS/README.md` made every two open decisions conflict (issue 297), which
|
||||
is why that went too. `index.py` fails if the list is stored there again.
|
||||
is why that went too. the index test fails if the list is stored there again.
|
||||
- Do not infer status from prose. Trust only a design's frontmatter, the tracker for an issue and the
|
||||
knowledge base for a record; if one is wrong, flag it.
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
__pycache__/
|
||||
.work/
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ works — plus the engineering practice that holds across everything Novox build
|
||||
|
||||
## Rules
|
||||
|
||||
- Markdown only.
|
||||
- Markdown only, but for the checks and one drawn overview of the playbooks (`process/playbooks.html`).
|
||||
- **Stable by nature.** Changes here reflect a genuine shift in intent, not iteration. The one
|
||||
exception is `how-we-build.md`, which changes whenever a rule is earned — and only through
|
||||
its amendment process.
|
||||
|
||||
+22
-16
@@ -1,15 +1,20 @@
|
||||
# Checks
|
||||
|
||||
The four checks are Go tests (ADR 0527), in this folder (`*_test.go`) with `go.mod` at the root: run
|
||||
`go test ./...` from the root. The checker runs the same as `mesh/check` on every pull request. Each check is a
|
||||
function over a file tree that returns its problems, tested on hq's tree and on a deliberately broken tree built
|
||||
in memory. The Python files and `merge-check.sh` are gone (issue 1296).
|
||||
|
||||
```
|
||||
python3 00-META/checks/records.py structure: links, no record written here, citations in the form "ADR NNNN"
|
||||
python3 00-META/checks/index.py the reading order's topics are written, and no list of records is stored
|
||||
python3 00-META/checks/cycle.py the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
|
||||
python3 00-META/checks/words.py the glossary's retired words are not used, no daemon's line is written, and no word is defined twice
|
||||
python3 00-META/checks/words.py --list tools the words the catalogue's copy must list
|
||||
python3 00-META/checks/words.py --list lines the daemons' lines the catalogue's copy must list
|
||||
records_test.go structure: links, no record written here, citations in the form "ADR NNNN"
|
||||
index_test.go the reading order's topics are written, and no list of records is stored
|
||||
cycle_test.go the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
|
||||
words_test.go the glossary's retired words are not used, no daemon's line is written, no word is defined twice,
|
||||
and the glossary names no seat's holder
|
||||
```
|
||||
|
||||
Non-zero exit on any problem, so it can be a gate rather than a report.
|
||||
A failing test fails `go test`, so it can be a gate rather than a report. With `MESH_CATALOG_DIR` set, the words
|
||||
test also compares the catalog's copies of the retired words and the daemons' lines with the glossary.
|
||||
|
||||
**Why this exists.** Until now nothing in this repository was verified by anything but reading,
|
||||
which is how a superseded decision stayed live in the constitution for days and in
|
||||
@@ -26,7 +31,8 @@ indistinguishable from one that cannot.
|
||||
| `no-records` | `02-DECISIONS/` and `01-RESEARCH/` hold their README and nothing else: a decision record or research effort lives in the knowledge base (ADR 0290 rule 9) | written with the change that removed the records (issue 426); it failed on a record file put back in `02-DECISIONS/` before it passed |
|
||||
| `decisions-form` | every entry of a design's `decisions:` is "ADR NNNN", four digits | written with issue 426; it failed on an entry `ADR 290` before it passed |
|
||||
| `mention-form` | every "ADR N" in the text has four digits, and every "research N" three; a code span is an example and is not read | written with issue 426; it failed on `ADR 12` and `research 45` before it passed |
|
||||
| *(index.py)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
|
||||
| *(index test)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
|
||||
| `holders` | the glossary names no module as a seat's holder: no seat "held by" or "claimed by" a named module, no named module "holder of" a seat, no module that "holds the seat"; the holder is read from the mesh's assignments (the operator, 2026-11-09) | **twenty statements** in the glossary, from the registry seats' "held today by the forge module" to the knowledge base's holder |
|
||||
| `status-vs-code` | a to-be document naming specific code is not still `designed` | **ten documents**, several with a *What was built* section, describing lab-proven code |
|
||||
|
||||
## Where the rules on records went
|
||||
@@ -37,7 +43,7 @@ named in its refusal: `knowledge-base.propose` refuses a record that breaks one
|
||||
record, naming every reason at once, and the four rules that judge the whole record set run after every
|
||||
acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, one table-test case per rule).
|
||||
|
||||
| Rule here until issue 426 | Where it lives now |
|
||||
| Rule here until issue 426 (the Python checks, deleted with issue 1296) | Where it lives now |
|
||||
|---|---|
|
||||
| `records.py` `rests-on`, for a record's `extends:` | the knowledge base's `rests-on` at propose, and the whole-set rule `rests-on-live` |
|
||||
| `records.py` `rests-on`, for a design's `decisions:` naming an accepted record | **nowhere yet**: this repository cannot read the knowledge base. The reviewer checks each new entry with `knowledge-base.get`. Designs become the knowledge base's own kind under issue 428 |
|
||||
@@ -47,7 +53,7 @@ acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, on
|
||||
| `records.py` `partial-supersession` | the knowledge base's `changes` link to a clause, `changes-in-force` and `supersedes-accepted`, and the whole-set rule `partial` |
|
||||
| `records.py` `clauses` | the knowledge base's `has-clause`, `clause-numbering`, `clause-lead` and `clause-size` |
|
||||
| `records.py` `numbering` | the knowledge base's `title` and `reserved` |
|
||||
| `records.py` `topics`, `index.py` (a record's topic) | the knowledge base's `topic`, against the reading order `index.py` still checks |
|
||||
| `records.py` `topics`, `index.py` (a record's topic) | the knowledge base's `topic`, against the reading order the index test still checks |
|
||||
| `records.py` `insights` | the knowledge base's `insight-form`; an accepted record changes by a new version marked as a progressive insight |
|
||||
| `index.py --print` | `knowledge-base.holds` with a topic |
|
||||
| `cycle.py` research: a known status, `became:` when graduated | the knowledge base's `research-status`, `overview` and `became` |
|
||||
@@ -72,7 +78,7 @@ acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, on
|
||||
State what incident it would have caught, and make it fail before you make it pass. A check
|
||||
whose failure has never been observed is a guess about its own correctness.
|
||||
|
||||
## cycle.py
|
||||
## The cycle test
|
||||
|
||||
The development cycle, checked (ADR 0080):
|
||||
every design parses, its `layer:` matches its folder and its `status:` is known; a to-be design names a
|
||||
@@ -84,9 +90,9 @@ and no status key — `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
|
||||
that carried them before they were removed. The rules on an issue's status this check held until then — an
|
||||
owner for a located issue, a fix for a resolved one, and a core issue's `replay` or `replay-none` (ADR 0237)
|
||||
— are refused at the write by `issue-tracker.move`. Its rules on research efforts and decision records left
|
||||
with them (the table above). `python3 00-META/checks/cycle.py`
|
||||
with them (the table above).
|
||||
|
||||
## words.py
|
||||
## The words test
|
||||
|
||||
One word per thing, checked (ADR 0244).
|
||||
It reads the glossary's *Not:* lines (the retired words, each with its scope) and *Identifier until
|
||||
@@ -95,15 +101,15 @@ quotations, struck-through text, link targets, comments and frontmatter are take
|
||||
`03-DESIGN/`, `AGENTS.md`, `README.md`, and issues opened from 2026-10-07. Decision records and research
|
||||
efforts live in the knowledge base, which holds a proposed record to the same words (the table above). It
|
||||
also fails when a glossary head word heads two entries or is also retired.
|
||||
[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
|
||||
[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date. With `MESH_CATALOG_DIR` set it compares the catalog's copy
|
||||
of the tools' retired words (`retired-words`) with the glossary.
|
||||
|
||||
It also fails on a **daemon's line** (ADR 0272,
|
||||
its insight of 2026-10-09): the operator types one command, `nox`, and a document names a `nox` line, never the
|
||||
controller's or the node-engine's binary followed by one of its commands. The lines are the check's own list
|
||||
(`DAEMON_LINES`); a command that is a line only with an argument after it is matched as a line is written. Looked
|
||||
(`daemonLines` in the words test); a command that is a line only with an argument after it is matched as a line is written. Looked
|
||||
for in the same documents, from 2026-10-09 for issues, in the document's own words — code blocks and
|
||||
spans included, quotations and block quotes taken out. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
|
||||
spans included, quotations and block quotes taken out. With `MESH_CATALOG_DIR` set it compares the catalog's copy
|
||||
(`retired-lines`) with the list.
|
||||
|
||||
It failed on something real before it passed: 581 uses of retired words in 63 documents on its first run, besides the glossary itself and research 034 —
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The development cycle, checked.
|
||||
|
||||
The knowledge flow (00-META/process/00-overview.md) says work moves idea -> research ->
|
||||
decision -> to-be design -> code, and symptom -> issue -> diagnosis -> fix. Those are rules,
|
||||
and a rule states how it is checked (AGENTS.md) -- this is how. Everything here reads only
|
||||
frontmatter, because a status this repository holds lives in frontmatter and nowhere else.
|
||||
|
||||
What is enforced:
|
||||
|
||||
design every 03-DESIGN doc parses, carries `layer:` matching its directory, and a
|
||||
known `status:`. A TO-BE doc names at least one decision (`decisions:`) -- no
|
||||
design without a decision -- and once `in-progress` or `implemented` it names
|
||||
its owning code (`code:`) -- no development without a design that says where.
|
||||
issues every report has frontmatter, and no two records share a number -- the number is
|
||||
how a record is cited. An issue's status, owner, fix and replay live in the issue
|
||||
tracker since the import of slice 3 (ADR 0275, to-be 53), where `issue-tracker.move`
|
||||
refuses at the write what this check refused at merge until then: a status outside
|
||||
the five, `located` without an owner, `resolved` without a fix, a core issue
|
||||
resolved without its replay (ADR 0237), `wontfix` without a reason. A report
|
||||
carries none of those keys, only the day it was `opened:` -- the tracker holds
|
||||
them, and two places holding one fact drift. The keys the reports opened before
|
||||
the import carried were removed once the import held them; the commit it read
|
||||
is on every imported issue.
|
||||
|
||||
The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
|
||||
rules this check held over them are the knowledge base's now: a research effort's status and what it
|
||||
became, and a decision's duplicate number, are refused by `knowledge-base.propose`; an accepted decision
|
||||
cited by nothing is the knowledge base's whole-set rule `cited`; a deferral that names no issue is its
|
||||
rule `deferral`. README.md beside this file says where each went.
|
||||
|
||||
Deliberately NOT enforced: as-is docs need no decisions (they describe what exists, not what
|
||||
was decided).
|
||||
|
||||
python3 00-META/checks/cycle.py
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
|
||||
|
||||
# The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275,
|
||||
# slice 3 of to-be 53), and `issue-tracker.move` enforces the cycle's rules on them at the write. A report
|
||||
# carrying one of them is a status written in two places, whenever it was opened.
|
||||
TRACKER_KEYS = ("status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved")
|
||||
|
||||
|
||||
def rel(path):
|
||||
return os.path.relpath(path, ROOT)
|
||||
|
||||
|
||||
def frontmatter(path):
|
||||
"""The YAML block between the first two --- lines, as {key: raw-value-string}.
|
||||
|
||||
Minimal on purpose, like records.py: enough for the fields these checks read. A list
|
||||
value (block or inline) is joined into its items; a scalar stays a string.
|
||||
"""
|
||||
text = open(path, encoding="utf-8").read()
|
||||
m = re.match(r"^---\n(.*?)\n---", text, re.S)
|
||||
if not m:
|
||||
return None
|
||||
front, out, key = m.group(1), {}, None
|
||||
for line in front.split("\n"):
|
||||
item = re.match(r"^\s+-\s*(.+?)\s*$", line)
|
||||
if item and key:
|
||||
out[key].append(item.group(1))
|
||||
continue
|
||||
kv = re.match(r"^([A-Za-z-]+):\s*(.*)$", line)
|
||||
if not kv:
|
||||
continue
|
||||
key, value = kv.group(1), kv.group(2).strip()
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
out[key] = [v.strip() for v in value[1:-1].split(",") if v.strip()]
|
||||
elif value == "":
|
||||
out[key] = [] # a block list may follow; stays [] if nothing does
|
||||
else:
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def listy(front, key):
|
||||
v = front.get(key)
|
||||
if v is None:
|
||||
return []
|
||||
return v if isinstance(v, list) else ([v] if str(v).strip() else [])
|
||||
|
||||
|
||||
def main():
|
||||
failures = []
|
||||
|
||||
def bad(path, why):
|
||||
failures.append(" %s: %s" % (rel(path), why))
|
||||
|
||||
# ---- design ------------------------------------------------------------------------
|
||||
for layer, name in (("00-as-is", "as-is"), ("01-to-be", "to-be")):
|
||||
for path in sorted(glob.glob(os.path.join(ROOT, "03-DESIGN", layer, "*.md"))):
|
||||
if os.path.basename(path) == "README.md":
|
||||
continue
|
||||
front = frontmatter(path)
|
||||
if front is None:
|
||||
bad(path, "no frontmatter")
|
||||
continue
|
||||
if front.get("layer") != name:
|
||||
bad(path, "layer is %r; this directory is %s" % (front.get("layer"), name))
|
||||
status = front.get("status")
|
||||
if status not in DESIGN_STATUSES:
|
||||
bad(path, "status %r is not one of %s" % (status, sorted(DESIGN_STATUSES)))
|
||||
if name == "to-be":
|
||||
if not listy(front, "decisions"):
|
||||
bad(path, "names no decisions -- no design without a decision")
|
||||
if status in ("in-progress", "implemented") and not listy(front, "code"):
|
||||
bad(path, "status %s but code: names no owner -- no development "
|
||||
"without a design that says where" % status)
|
||||
|
||||
# ---- issues ------------------------------------------------------------------------
|
||||
# Two records may not share a number. Numbers are taken as "next free after main", and work
|
||||
# sits on unmerged branches for days -- so two people reading the same main allocate the same
|
||||
# number, and nothing said so. It happened twice in one evening between two machines, and the
|
||||
# second collision landed on main with all three checks passing (issue 155). An issue number is
|
||||
# how every other record cites this one; two records answering to it means a pointer that
|
||||
# resolves to whichever the reader happened to open.
|
||||
seen = {}
|
||||
for folder in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", ""))):
|
||||
name = os.path.basename(os.path.normpath(folder))
|
||||
number = name.split("-", 1)[0]
|
||||
if not number.isdigit():
|
||||
continue
|
||||
if number in seen:
|
||||
bad(os.path.join("04-ISSUES", name),
|
||||
"is numbered %s, and so is %s -- an issue number is how it is cited, and two "
|
||||
"records answering to one means a citation that resolves to whichever the reader "
|
||||
"opened. Take the next free number across main AND every open pull request"
|
||||
% (number, seen[number]))
|
||||
else:
|
||||
seen[number] = name
|
||||
|
||||
# Since the import of slice 3 (ADR 0275), an issue's status, owner, fix and replay live in the issue
|
||||
# tracker, and its `move` refuses at the write what this check refused at merge. A report carries none
|
||||
# of those keys.
|
||||
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
|
||||
front = frontmatter(path)
|
||||
if front is None:
|
||||
bad(path, "no frontmatter")
|
||||
continue
|
||||
opened = str(front.get("opened") or "")
|
||||
if not opened:
|
||||
bad(path, "opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
|
||||
continue
|
||||
carried = [key for key in TRACKER_KEYS if key in front]
|
||||
if carried:
|
||||
bad(path, "carries %s, but an issue's status, owner, fix and replay live in the issue tracker "
|
||||
"(ADR 0275): move it with `issue-tracker.move`, and keep no copy here"
|
||||
% ", ".join("`%s:`" % key for key in carried))
|
||||
|
||||
checked = (
|
||||
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
|
||||
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
|
||||
)
|
||||
if failures:
|
||||
print("cycle: %d document(s) break the development cycle:" % len(failures))
|
||||
print("\n".join(failures))
|
||||
return 1
|
||||
print("cycle: %d documents checked, the chain holds" % checked)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,169 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
// The development cycle, checked (cycle.py until ADR 0527; ADR 0080). Everything here reads frontmatter only,
|
||||
// because a status this repository holds lives in frontmatter and nowhere else. The decision records and research
|
||||
// efforts live in the knowledge base (ADR 0290), whose rules judge them.
|
||||
|
||||
var designStatuses = map[string]bool{"proposed": true, "designed": true, "in-progress": true, "implemented": true, "abandoned": true}
|
||||
|
||||
// The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275), and
|
||||
// `issue-tracker.move` enforces the cycle's rules at the write. A report carrying one is a status in two places.
|
||||
var trackerKeys = []string{"status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved"}
|
||||
|
||||
// checkDesigns: every design parses, carries `layer:` matching its folder and a known `status:`; a to-be design
|
||||
// names a decision, and once in-progress or implemented names its owning code. As-is designs need no decisions.
|
||||
func checkDesigns(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, layer := range []struct{ folder, name string }{{"00-as-is", "as-is"}, {"01-to-be", "to-be"}} {
|
||||
entries, _ := fs.ReadDir(fsys, "03-DESIGN/"+layer.folder)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || e.Name() == "README.md" {
|
||||
continue
|
||||
}
|
||||
p := path.Join("03-DESIGN", layer.folder, e.Name())
|
||||
front := frontmatter(readFile(fsys, p))
|
||||
if front == nil {
|
||||
out = append(out, "cycle: "+p+": no frontmatter")
|
||||
continue
|
||||
}
|
||||
if front.scalar("layer") != layer.name {
|
||||
out = append(out, fmt.Sprintf("cycle: %s: layer is %q; this directory is %s", p, front.scalar("layer"), layer.name))
|
||||
}
|
||||
status := front.scalar("status")
|
||||
if !designStatuses[status] {
|
||||
var known []string
|
||||
for s := range designStatuses {
|
||||
known = append(known, s)
|
||||
}
|
||||
sort.Strings(known)
|
||||
out = append(out, fmt.Sprintf("cycle: %s: status %q is not one of %v", p, status, known))
|
||||
}
|
||||
if layer.name == "to-be" {
|
||||
if len(front.list("decisions")) == 0 {
|
||||
out = append(out, "cycle: "+p+": names no decisions -- no design without a decision")
|
||||
}
|
||||
if (status == "in-progress" || status == "implemented") && len(front.list("code")) == 0 {
|
||||
out = append(out, fmt.Sprintf("cycle: %s: status %s but code: names no owner -- no development without a design that says where", p, status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkIssues: no two issue folders share a number (issue 155: the number is how a record is cited), every report
|
||||
// has frontmatter with `opened:`, and none carries a key the tracker holds.
|
||||
func checkIssues(fsys fs.FS) []string {
|
||||
var out []string
|
||||
entries, _ := fs.ReadDir(fsys, "04-ISSUES")
|
||||
seen := map[string]string{}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
number := strings.SplitN(e.Name(), "-", 2)[0]
|
||||
if number == "" || strings.Trim(number, "0123456789") != "" {
|
||||
continue
|
||||
}
|
||||
if first, ok := seen[number]; ok {
|
||||
out = append(out, fmt.Sprintf("cycle: 04-ISSUES/%s: is numbered %s, and so is %s -- an issue number is how it is cited, and two records "+
|
||||
"answering to one means a citation that resolves to whichever the reader opened. Take the next free number across main AND every open pull request", e.Name(), number, first))
|
||||
} else {
|
||||
seen[number] = e.Name()
|
||||
}
|
||||
}
|
||||
for _, e := range entries {
|
||||
p := path.Join("04-ISSUES", e.Name(), "00-report.md")
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, err := fs.Stat(fsys, p); err != nil {
|
||||
continue
|
||||
}
|
||||
front := frontmatter(readFile(fsys, p))
|
||||
if front == nil {
|
||||
out = append(out, "cycle: "+p+": no frontmatter")
|
||||
continue
|
||||
}
|
||||
if front.scalar("opened") == "" {
|
||||
out = append(out, "cycle: "+p+": opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
|
||||
continue
|
||||
}
|
||||
var carried []string
|
||||
for _, k := range trackerKeys {
|
||||
if _, ok := front[k]; ok {
|
||||
carried = append(carried, "`"+k+":`")
|
||||
}
|
||||
}
|
||||
if len(carried) > 0 {
|
||||
out = append(out, fmt.Sprintf("cycle: %s: carries %s, but an issue's status, owner, fix and replay live in the issue tracker (ADR 0275): "+
|
||||
"move it with `issue-tracker.move`, and keep no copy here", p, strings.Join(carried, ", ")))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestCycleDesignsHoldTheChain(t *testing.T) { expectNone(t, checkDesigns(rootFS())) }
|
||||
func TestCycleIssuesHoldTheChain(t *testing.T) { expectNone(t, checkIssues(rootFS())) }
|
||||
|
||||
func design(layer, status, extra string) *fstest.MapFile {
|
||||
return doc("---\nlayer: " + layer + "\nstatus: " + status + "\n" + extra + "---\nbody\n")
|
||||
}
|
||||
|
||||
func TestCycleDesignsCatchesEachBreak(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"03-DESIGN/00-as-is/a-nofront.md": doc("no frontmatter"),
|
||||
"03-DESIGN/00-as-is/b-wronglayer.md": design("to-be", "implemented", ""),
|
||||
"03-DESIGN/00-as-is/c-badstatus.md": design("as-is", "done", ""),
|
||||
"03-DESIGN/01-to-be/d-nodecision.md": design("to-be", "designed", ""),
|
||||
"03-DESIGN/01-to-be/e-nocode.md": design("to-be", "in-progress", "decisions: [ADR 0001]\n"),
|
||||
"03-DESIGN/01-to-be/README.md": doc("not a design"),
|
||||
}
|
||||
got := checkDesigns(broken)
|
||||
expectProblem(t, got, "a-nofront.md: no frontmatter")
|
||||
expectProblem(t, got, `b-wronglayer.md: layer is "to-be"; this directory is as-is`)
|
||||
expectProblem(t, got, `c-badstatus.md: status "done" is not one of`)
|
||||
expectProblem(t, got, "d-nodecision.md: names no decisions")
|
||||
expectProblem(t, got, "e-nocode.md: status in-progress but code: names no owner")
|
||||
if len(got) != 5 {
|
||||
t.Errorf("exactly the five breaks, as-is needs no decisions and a README is no design: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCycleDesignsAcceptsASoundPair(t *testing.T) {
|
||||
sound := fstest.MapFS{
|
||||
"03-DESIGN/00-as-is/a.md": design("as-is", "implemented", ""),
|
||||
"03-DESIGN/01-to-be/b.md": design("to-be", "in-progress", "decisions: [ADR 0001]\ncode: [mesh-sdk]\n"),
|
||||
}
|
||||
expectNone(t, checkDesigns(sound))
|
||||
}
|
||||
|
||||
func TestCycleIssuesCatchesEachBreak(t *testing.T) {
|
||||
report := func(front string) *fstest.MapFile { return doc("---\n" + front + "---\nbody\n") }
|
||||
broken := fstest.MapFS{
|
||||
"04-ISSUES/100-a/00-report.md": report("opened: 2026-10-01\n"),
|
||||
"04-ISSUES/100-b/00-report.md": report("opened: 2026-10-01\n"),
|
||||
"04-ISSUES/101-noopened/00-report.md": report("title: x\n"),
|
||||
"04-ISSUES/102-tracker/00-report.md": report("opened: 2026-10-01\nstatus: open\nreplay: none\n"),
|
||||
"04-ISSUES/103-nofront/00-report.md": doc("plain"),
|
||||
"04-ISSUES/notes/readme.md": doc("not an issue"),
|
||||
}
|
||||
got := checkIssues(broken)
|
||||
expectProblem(t, got, "04-ISSUES/100-b: is numbered 100, and so is 100-a")
|
||||
expectProblem(t, got, "101-noopened/00-report.md: opened: says nothing")
|
||||
expectProblem(t, got, "102-tracker/00-report.md: carries `status:`, `replay:`")
|
||||
expectProblem(t, got, "103-nofront/00-report.md: no frontmatter")
|
||||
if len(got) != 4 {
|
||||
t.Errorf("exactly the four breaks: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The reading order of the decision records: its topics are written here, its records are not.
|
||||
|
||||
A number identifies a record and never changes, so the reading order is not the order of the numbers.
|
||||
It is the six topics `02-DECISIONS/README.md` writes down, in order, and each record's own topic. The
|
||||
records live in the knowledge base (ADR 0290, issue 426), which reads these lines through the records
|
||||
module and refuses a record whose topic is not one of them; the list of records under a topic is its
|
||||
answer to `knowledge-base.holds` with that topic, never stored here (ADR 0248, hq issue 297).
|
||||
|
||||
python3 00-META/checks/index.py check
|
||||
|
||||
It fails when the README's reading order is missing or names a topic twice, and when the README stores
|
||||
a list of records again — the index markers, or a line of a generated list. The last one is what keeps
|
||||
the conflict of issue 297 from coming back. Whether every record's topic is one of the six is the
|
||||
knowledge base's rule `topic`, judged when a record is proposed.
|
||||
"""
|
||||
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
|
||||
README = "02-DECISIONS/README.md"
|
||||
|
||||
# A topic in the README's reading order, e.g.
|
||||
# 1. **What the mesh is** — `topic: the mesh`. What it is for, ...
|
||||
# The knowledge base (mesh-knowledge, reference.go) reads the same line, so the form is a contract.
|
||||
TOPIC_LINE = re.compile(r"^\d+\.\s+\*\*(?P<label>[^*]+)\*\*\s+—\s+`topic:\s*(?P<topic>[^`]+)`", re.M)
|
||||
|
||||
# What a stored list looks like: the old markers, or a line of the list index.py used to print.
|
||||
STORED = [
|
||||
(re.compile(r"<!--\s*index:(start|end)\s*-->"), "an index marker"),
|
||||
(re.compile(r"^- \*\*\d{4}\*\* — \[", re.M), "a line of a generated list"),
|
||||
]
|
||||
|
||||
|
||||
def reading_order(text=None):
|
||||
"""The topics, in order, as (topic, label) pairs, from the README."""
|
||||
if text is None:
|
||||
text = io.open(README, encoding='utf-8').read()
|
||||
return [(m.group("topic").strip(), m.group("label").strip()) for m in TOPIC_LINE.finditer(text)]
|
||||
|
||||
|
||||
def main():
|
||||
if "--print" in sys.argv:
|
||||
print("index: the records live in the knowledge base (ADR 0290). Read a topic's with "
|
||||
"`knowledge-base.holds` and that topic, through the mesh MCP server.")
|
||||
return 2
|
||||
|
||||
text = io.open(README, encoding='utf-8').read()
|
||||
topics = reading_order(text)
|
||||
|
||||
problems = []
|
||||
if not topics:
|
||||
problems.append("%s writes no reading order: no line of the form "
|
||||
"'1. **Label** — `topic: name`. ...'; the knowledge base refuses every "
|
||||
"record without it" % README)
|
||||
names = [t for t, _ in topics]
|
||||
if len(set(names)) != len(names):
|
||||
problems.append("%s names a topic twice in its reading order" % README)
|
||||
for pattern, what in STORED:
|
||||
if pattern.search(text):
|
||||
problems.append("%s stores a list of records (%s). The records live in the knowledge base, "
|
||||
"and a topic's list is `knowledge-base.holds` with that topic" % (README, what))
|
||||
|
||||
for p in problems:
|
||||
print("index: " + p)
|
||||
if problems:
|
||||
return 1
|
||||
print("index: %d topics in the reading order, no stored list" % len(topics))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,93 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
// The reading order of the decision records (index.py until ADR 0527): its topics are written in
|
||||
// 02-DECISIONS/README.md, its records are not. The knowledge base reads these lines and refuses a record whose
|
||||
// topic is not one of them, so the form is a contract; a topic's records are `knowledge-base.holds` with that
|
||||
// topic, never stored here (ADR 0248, issue 297).
|
||||
|
||||
const readmeOfDecisions = "02-DECISIONS/README.md"
|
||||
|
||||
// A topic line: `1. **Label** — `topic: name`. ...`
|
||||
var topicLineRx = regexp.MustCompile(`(?m)^\d+\.\s+\*\*([^*]+)\*\*\s+—\s+` + "`topic:\\s*([^`]+)`")
|
||||
|
||||
// What a stored list looks like: the old markers, or a line of the list index.py used to print.
|
||||
var storedLists = []struct {
|
||||
rx *regexp.Regexp
|
||||
what string
|
||||
}{
|
||||
{regexp.MustCompile(`<!--\s*index:(start|end)\s*-->`), "an index marker"},
|
||||
{regexp.MustCompile(`(?m)^- \*\*\d{4}\*\* — \[`), "a line of a generated list"},
|
||||
}
|
||||
|
||||
func readingOrder(text string) []string {
|
||||
var topics []string
|
||||
for _, m := range topicLineRx.FindAllStringSubmatch(text, -1) {
|
||||
topics = append(topics, strings.TrimSpace(m[2]))
|
||||
}
|
||||
return topics
|
||||
}
|
||||
|
||||
func checkIndex(fsys fs.FS) []string {
|
||||
text := readFile(fsys, readmeOfDecisions)
|
||||
topics := readingOrder(text)
|
||||
var out []string
|
||||
if len(topics) == 0 {
|
||||
out = append(out, "index: "+readmeOfDecisions+" writes no reading order: no line of the form '1. **Label** — `topic: name`. ...'; "+
|
||||
"the knowledge base refuses every record without it")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, topic := range topics {
|
||||
if seen[topic] {
|
||||
out = append(out, fmt.Sprintf("index: %s names a topic twice in its reading order: %s", readmeOfDecisions, topic))
|
||||
break
|
||||
}
|
||||
seen[topic] = true
|
||||
}
|
||||
for _, s := range storedLists {
|
||||
if s.rx.MatchString(text) {
|
||||
out = append(out, fmt.Sprintf("index: %s stores a list of records (%s). The records live in the knowledge base, and a topic's list is "+
|
||||
"`knowledge-base.holds` with that topic", readmeOfDecisions, s.what))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestIndexReadingOrderIsWrittenAndStoresNoList(t *testing.T) {
|
||||
expectNone(t, checkIndex(rootFS()))
|
||||
if n := len(readingOrder(readFile(rootFS(), readmeOfDecisions))); n == 0 {
|
||||
t.Error("the reading order has no topic")
|
||||
}
|
||||
}
|
||||
|
||||
const sound = "1. **One** — `topic: one`. x\n2. **Two** — `topic: two`. y\n"
|
||||
|
||||
func TestIndexCatchesAMissingReadingOrder(t *testing.T) {
|
||||
expectProblem(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc("nothing written here\n")}), "writes no reading order")
|
||||
}
|
||||
|
||||
func TestIndexCatchesADuplicateTopic(t *testing.T) {
|
||||
broken := fstest.MapFS{readmeOfDecisions: doc(sound + "3. **Again** — `topic: one`. z\n")}
|
||||
expectProblem(t, checkIndex(broken), "names a topic twice")
|
||||
}
|
||||
|
||||
func TestIndexCatchesAStoredList(t *testing.T) {
|
||||
for what, stored := range map[string]string{
|
||||
"an index marker": "<!-- index:start -->\n",
|
||||
"a line of a generated list": "- **0001** — [a title](x.md)\n",
|
||||
} {
|
||||
expectProblem(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc(sound + stored)}), what)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexAcceptsASoundReadme(t *testing.T) {
|
||||
expectNone(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc(sound)}))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
These tests read the repository's documents (rootFS), outside this directory: mesh-build-go never keeps this package's verdict (issue 1935).
|
||||
@@ -1,227 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Structural checks over HQ's own documents.
|
||||
|
||||
Every check here exists because the thing it checks for actually happened. See README.md
|
||||
for which incident is behind which check. Run from the repository root:
|
||||
|
||||
python3 00-META/checks/records.py
|
||||
|
||||
Exits non-zero if anything fails, so it can be a gate rather than a report.
|
||||
|
||||
The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
|
||||
rules that judged them here are the knowledge base's now: `propose` refuses a record that breaks one.
|
||||
README.md beside this file says where each rule went. What stays here judges what this repository
|
||||
still holds, and keeps the records from coming back.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
LINK = re.compile(r"\[[^\]]*\]\((?!https?:|mailto:)([^)]+)\)")
|
||||
|
||||
# The two folders the records left. Each keeps a README that points at the knowledge base, and
|
||||
# 02-DECISIONS/README.md also writes the reading order's topics, which the knowledge base reads.
|
||||
RECORD_FOLDERS = ("02-DECISIONS", "01-RESEARCH")
|
||||
POINTERS = {"02-DECISIONS/README.md", "01-RESEARCH/README.md"}
|
||||
|
||||
# A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three.
|
||||
DECISION_ENTRY = re.compile(r"^ADR \d{4}$")
|
||||
ADR_MENTION = re.compile(r"\bADRs?\s+(\d+)\b")
|
||||
RESEARCH_MENTION = re.compile(r"\b[Rr]esearch\s+(\d+)\b")
|
||||
|
||||
|
||||
def markdown_files():
|
||||
for base, dirs, files in os.walk(ROOT):
|
||||
dirs[:] = [d for d in dirs if d not in (".git", ".claude")]
|
||||
for name in sorted(files):
|
||||
if name.endswith(".md"):
|
||||
yield os.path.join(base, name)
|
||||
|
||||
|
||||
def rel(path):
|
||||
return os.path.relpath(path, ROOT)
|
||||
|
||||
|
||||
def read(path):
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
def frontmatter(text):
|
||||
"""Minimal frontmatter reader — enough for the fields these checks use.
|
||||
|
||||
Not a YAML parser on purpose: a dependency in a repository that has none, to read a few
|
||||
scalar fields and one list, would cost more than it returns.
|
||||
"""
|
||||
if not text.startswith("---\n"):
|
||||
return {}
|
||||
end = text.find("\n---", 4)
|
||||
if end == -1:
|
||||
return {}
|
||||
fields, key = {}, None
|
||||
for line in text[4:end].split("\n"):
|
||||
item = re.match(r"^\s+-\s+(.*)$", line)
|
||||
if item and key:
|
||||
fields.setdefault(key, []).append(item.group(1).strip())
|
||||
continue
|
||||
pair = re.match(r"^([A-Za-z_-]+):\s*(.*)$", line)
|
||||
if pair:
|
||||
key = pair.group(1)
|
||||
value = pair.group(2).strip()
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
# inline list: `decisions: [a, b]`, `code: []`
|
||||
inner = value[1:-1].strip()
|
||||
fields[key] = [v.strip() for v in inner.split(",") if v.strip()]
|
||||
elif value:
|
||||
fields[key] = value
|
||||
else:
|
||||
fields[key] = []
|
||||
return fields
|
||||
|
||||
|
||||
def unquote(value):
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
||||
return value[1:-1]
|
||||
return value
|
||||
|
||||
|
||||
class Failures:
|
||||
def __init__(self):
|
||||
self.items = []
|
||||
|
||||
def add(self, check, location, message):
|
||||
self.items.append((check, location, message))
|
||||
|
||||
def report(self):
|
||||
if not self.items:
|
||||
print("records: all checks passed")
|
||||
return 0
|
||||
by_check = {}
|
||||
for check, location, message in self.items:
|
||||
by_check.setdefault(check, []).append((location, message))
|
||||
for check in sorted(by_check):
|
||||
print(f"\n{check} — {len(by_check[check])} problem(s)")
|
||||
for location, message in by_check[check]:
|
||||
print(f" {location}\n {message}")
|
||||
print(f"\nrecords: {len(self.items)} problem(s)")
|
||||
return 1
|
||||
|
||||
|
||||
def check_links(failures):
|
||||
"""Every relative link resolves to something that exists."""
|
||||
for path in markdown_files():
|
||||
folder = os.path.dirname(path)
|
||||
for number, line in enumerate(read(path).split("\n"), 1):
|
||||
for target in LINK.findall(line):
|
||||
target = target.split("#")[0].strip()
|
||||
if not target:
|
||||
continue
|
||||
if not os.path.exists(os.path.normpath(os.path.join(folder, target))):
|
||||
failures.add("links", f"{rel(path)}:{number}", f"link does not resolve: {target}")
|
||||
|
||||
|
||||
def check_no_records(failures):
|
||||
"""No decision record or research effort is written into this repository again (ADR 0290 rule 9).
|
||||
|
||||
The knowledge base is the only place a record is read; a file here would be a second copy that
|
||||
drifts from it, and nothing would read it. The folders keep their pointer READMEs and nothing else.
|
||||
"""
|
||||
for folder in RECORD_FOLDERS:
|
||||
base = os.path.join(ROOT, folder)
|
||||
if not os.path.isdir(base):
|
||||
failures.add("no-records", folder, "the folder is gone; it keeps a README that points at the "
|
||||
"knowledge base (and, for 02-DECISIONS, the reading order the knowledge base reads)")
|
||||
continue
|
||||
for current, dirs, files in os.walk(base):
|
||||
for name in sorted(files + [d + "/" for d in dirs]):
|
||||
path = rel(os.path.join(current, name))
|
||||
if path.rstrip("/") in POINTERS:
|
||||
continue
|
||||
failures.add(
|
||||
"no-records",
|
||||
path,
|
||||
"a decision record or research effort lives in the knowledge base, never here: "
|
||||
"write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)",
|
||||
)
|
||||
|
||||
|
||||
def check_decisions_entries(failures):
|
||||
"""A design's `decisions:` names each record as "ADR NNNN".
|
||||
|
||||
A to-be design rests on the records it names, and the knowledge base answers for a record by its
|
||||
number; an entry in another form is a citation nothing can follow.
|
||||
"""
|
||||
for path in markdown_files():
|
||||
if not rel(path).startswith("03-DESIGN/"):
|
||||
continue
|
||||
for entry in frontmatter(read(path)).get("decisions", []) or []:
|
||||
if not DECISION_ENTRY.match(unquote(entry)):
|
||||
failures.add("decisions-form", rel(path),
|
||||
f"decisions: entry {entry!r} is not 'ADR NNNN' (four digits)")
|
||||
|
||||
|
||||
def check_mentions(failures):
|
||||
"""In running text, a decision record is "ADR NNNN" (four digits) and a research effort "research NNN"
|
||||
(three): the form the knowledge base and the issue tracker read a citation in. A code span is an example,
|
||||
and is not read."""
|
||||
for path in markdown_files():
|
||||
text = read(path)
|
||||
for number, line in enumerate(text.split("\n"), 1):
|
||||
line = re.sub(r"`[^`]*`", "", line)
|
||||
for m in ADR_MENTION.finditer(line):
|
||||
if len(m.group(1)) != 4:
|
||||
failures.add("mention-form", f"{rel(path)}:{number}",
|
||||
f"{m.group(0)!r}: a decision record is named 'ADR NNNN', four digits")
|
||||
for m in RESEARCH_MENTION.finditer(line):
|
||||
if len(m.group(1)) != 3:
|
||||
failures.add("mention-form", f"{rel(path)}:{number}",
|
||||
f"{m.group(0)!r}: a research effort is named 'research NNN', three digits")
|
||||
|
||||
|
||||
def check_status_against_code(failures):
|
||||
"""A design document naming specific code may not still call itself `designed`.
|
||||
|
||||
**Naming a file is a claim that the file implements this**, so the two fields have to agree.
|
||||
They drifted: ten to-be documents named working, lab-proven code — several with a *What was
|
||||
built* or *Raised, and observed* section — while still saying nothing had been built.
|
||||
|
||||
Deliberately weak, and that is the point of it being mechanical. It cannot tell whether the
|
||||
prose is true, only that a document has stopped claiming to be unbuilt once it points at
|
||||
something. `code: [mesh-controller]` — a repository with no path — is a plan and stays
|
||||
`designed`.
|
||||
"""
|
||||
for path in markdown_files():
|
||||
if not rel(path).startswith("03-DESIGN/01-to-be/") or path.endswith("README.md"):
|
||||
continue
|
||||
front = frontmatter(read(path))
|
||||
if front.get("status") != "designed":
|
||||
continue
|
||||
for entry in front.get("code") or []:
|
||||
named = re.sub(r"\s*\(.*\)$", "", entry).strip().split(None, 1)
|
||||
if len(named) > 1:
|
||||
failures.add(
|
||||
"status-vs-code",
|
||||
rel(path),
|
||||
f"`designed`, but names {named[1]!r} in {named[0]}. Naming a file claims "
|
||||
f"it implements this — use `in-progress`, or `implemented` once it is "
|
||||
f"defensible from that repository's main branch.",
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
failures = Failures()
|
||||
check_links(failures)
|
||||
check_no_records(failures)
|
||||
check_decisions_entries(failures)
|
||||
check_mentions(failures)
|
||||
check_status_against_code(failures)
|
||||
return failures.report()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,206 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
// Structural checks over hq's own documents (records.py until ADR 0527). The decision records and research
|
||||
// efforts live in the knowledge base (ADR 0290, issue 426); what stays here judges what this repository still
|
||||
// holds, and keeps the records from coming back.
|
||||
|
||||
var (
|
||||
linkRx = regexp.MustCompile(`\[[^\]]*\]\(([^)]+)\)`)
|
||||
// A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three.
|
||||
decisionEntryRx = regexp.MustCompile(`^ADR \d{4}$`)
|
||||
adrMentionRx = regexp.MustCompile(`\bADRs?\s+(\d+)\b`)
|
||||
researchMentionRx = regexp.MustCompile(`\b[Rr]esearch\s+(\d+)\b`)
|
||||
codeSpanRx = regexp.MustCompile("`[^`]*`")
|
||||
recordFolders = []string{"02-DECISIONS", "01-RESEARCH"}
|
||||
recordFolderPoints = map[string]bool{"02-DECISIONS/README.md": true, "01-RESEARCH/README.md": true}
|
||||
)
|
||||
|
||||
// checkLinks: every relative link resolves to something that exists.
|
||||
func checkLinks(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, p := range markdownFiles(fsys) {
|
||||
for n, line := range strings.Split(readFile(fsys, p), "\n") {
|
||||
for _, m := range linkRx.FindAllStringSubmatch(line, -1) {
|
||||
target := m[1]
|
||||
if strings.HasPrefix(target, "http:") || strings.HasPrefix(target, "https:") || strings.HasPrefix(target, "mailto:") {
|
||||
continue
|
||||
}
|
||||
target = strings.TrimSpace(strings.SplitN(target, "#", 2)[0])
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := fs.Stat(fsys, path.Join(path.Dir(p), target)); err != nil {
|
||||
out = append(out, fmt.Sprintf("links: %s:%d: link does not resolve: %s", p, n+1, target))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkNoRecords: the two folders the records left hold their pointer README and nothing else (ADR 0290 rule 9).
|
||||
func checkNoRecords(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, folder := range recordFolders {
|
||||
if _, err := fs.Stat(fsys, folder); err != nil {
|
||||
out = append(out, "no-records: "+folder+": the folder is gone; it keeps a README that points at the knowledge base")
|
||||
continue
|
||||
}
|
||||
_ = fs.WalkDir(fsys, folder, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil || p == folder || recordFolderPoints[p] {
|
||||
return nil
|
||||
}
|
||||
out = append(out, "no-records: "+p+": a decision record or research effort lives in the knowledge base, never here: "+
|
||||
"write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkDecisionsEntries: a design's `decisions:` names each record as "ADR NNNN".
|
||||
func checkDecisionsEntries(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, p := range markdownFiles(fsys) {
|
||||
if !strings.HasPrefix(p, "03-DESIGN/") {
|
||||
continue
|
||||
}
|
||||
for _, e := range frontmatter(readFile(fsys, p)).list("decisions") {
|
||||
if !decisionEntryRx.MatchString(unquote(e)) {
|
||||
out = append(out, fmt.Sprintf("decisions-form: %s: decisions: entry %q is not 'ADR NNNN' (four digits)", p, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkMentions: in running text a decision record is "ADR NNNN" and a research effort "research NNN"; a code
|
||||
// span is an example and is not read.
|
||||
func checkMentions(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, p := range markdownFiles(fsys) {
|
||||
for n, line := range strings.Split(readFile(fsys, p), "\n") {
|
||||
line = codeSpanRx.ReplaceAllString(line, "")
|
||||
for _, m := range adrMentionRx.FindAllStringSubmatch(line, -1) {
|
||||
if len(m[1]) != 4 {
|
||||
out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a decision record is named 'ADR NNNN', four digits", p, n+1, m[0]))
|
||||
}
|
||||
}
|
||||
for _, m := range researchMentionRx.FindAllStringSubmatch(line, -1) {
|
||||
if len(m[1]) != 3 {
|
||||
out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a research effort is named 'research NNN', three digits", p, n+1, m[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var codeEntryRx = regexp.MustCompile(`\s*\(.*\)$`)
|
||||
|
||||
// checkStatusAgainstCode: a to-be design naming specific code (a path after the repository) may not still be
|
||||
// `designed`. Deliberately weak: it cannot tell whether the prose is true, only that a document stopped claiming
|
||||
// to be unbuilt once it points at something. `code: [mesh-controller]` is a plan and stays `designed`.
|
||||
func checkStatusAgainstCode(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, p := range markdownFiles(fsys) {
|
||||
if !strings.HasPrefix(p, "03-DESIGN/01-to-be/") || strings.HasSuffix(p, "README.md") {
|
||||
continue
|
||||
}
|
||||
front := frontmatter(readFile(fsys, p))
|
||||
if front.scalar("status") != "designed" {
|
||||
continue
|
||||
}
|
||||
for _, e := range front.list("code") {
|
||||
named := strings.SplitN(strings.TrimSpace(codeEntryRx.ReplaceAllString(e, "")), " ", 2)
|
||||
if len(named) > 1 {
|
||||
out = append(out, fmt.Sprintf("status-vs-code: %s: `designed`, but names %q in %s. Naming a file claims it implements this "+
|
||||
"- use `in-progress`, or `implemented` once it is defensible from that repository's main branch.", p, strings.TrimSpace(named[1]), named[0]))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func doc(s string) *fstest.MapFile { return &fstest.MapFile{Data: []byte(s)} }
|
||||
|
||||
// The real tree.
|
||||
|
||||
func TestRecordsLinksResolve(t *testing.T) { expectNone(t, checkLinks(rootFS())) }
|
||||
func TestRecordsFoldersHoldOnlyPointers(t *testing.T) { expectNone(t, checkNoRecords(rootFS())) }
|
||||
func TestRecordsDecisionsEntriesForm(t *testing.T) { expectNone(t, checkDecisionsEntries(rootFS())) }
|
||||
func TestRecordsMentionForm(t *testing.T) { expectNone(t, checkMentions(rootFS())) }
|
||||
func TestRecordsStatusAgainstCode(t *testing.T) { expectNone(t, checkStatusAgainstCode(rootFS())) }
|
||||
|
||||
// The checks themselves, each against a broken fixture, and against a sound one.
|
||||
|
||||
func TestRecordsLinksCatchesABrokenLink(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"a/one.md": doc("see [two](two.md) and [gone](gone.md#x) and [web](https://example.com/x)\n"),
|
||||
"a/two.md": doc("[up](../a/one.md#top) [anchor](#here)\n"),
|
||||
}
|
||||
got := checkLinks(broken)
|
||||
expectProblem(t, got, "a/one.md:1: link does not resolve: gone.md")
|
||||
if len(got) != 1 {
|
||||
t.Errorf("only the broken link is a problem, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordsNoRecordsCatchesARecordPutBack(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"02-DECISIONS/README.md": doc("pointer"),
|
||||
"02-DECISIONS/0001-x.md": doc("a record"),
|
||||
"01-RESEARCH/README.md": doc("pointer"),
|
||||
"01-RESEARCH/045/00-x.md": doc("an effort"),
|
||||
}
|
||||
got := checkNoRecords(broken)
|
||||
expectProblem(t, got, "02-DECISIONS/0001-x.md")
|
||||
expectProblem(t, got, "01-RESEARCH/045")
|
||||
expectProblem(t, checkNoRecords(fstest.MapFS{"02-DECISIONS/README.md": doc("p")}), "01-RESEARCH: the folder is gone")
|
||||
}
|
||||
|
||||
func TestRecordsDecisionsFormCatchesAShortEntry(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"03-DESIGN/01-to-be/1.md": doc("---\nlayer: to-be\ndecisions:\n - ADR 0290\n - ADR 290\n---\n"),
|
||||
"00-META/x.md": doc("---\ndecisions: [ADR 5]\n---\n"),
|
||||
}
|
||||
got := checkDecisionsEntries(broken)
|
||||
expectProblem(t, got, `entry "ADR 290"`)
|
||||
if len(got) != 1 {
|
||||
t.Errorf("only a design's entries are read, and ADR 0290 is sound: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordsMentionFormCatchesShortNumbers(t *testing.T) {
|
||||
broken := fstest.MapFS{"x.md": doc("fine ADR 0290 and research 045\nbad ADR 12 and research 45\nexample `ADR 7` and `research 9`\n")}
|
||||
got := checkMentions(broken)
|
||||
expectProblem(t, got, "x.md:2: \"ADR 12\"")
|
||||
expectProblem(t, got, "x.md:2: \"research 45\"")
|
||||
if len(got) != 2 {
|
||||
t.Errorf("code spans and sound forms are not problems: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordsStatusAgainstCodeCatchesADesignedDesignNamingAFile(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"03-DESIGN/01-to-be/9.md": doc("---\nstatus: designed\ncode:\n - mesh-controller internal/x.go (the thing)\n---\n"),
|
||||
"03-DESIGN/01-to-be/8.md": doc("---\nstatus: designed\ncode: [mesh-controller]\n---\n"),
|
||||
"03-DESIGN/01-to-be/7.md": doc("---\nstatus: in-progress\ncode: [mesh-controller internal/x.go]\n---\n"),
|
||||
}
|
||||
got := checkStatusAgainstCode(broken)
|
||||
expectProblem(t, got, "03-DESIGN/01-to-be/9.md: `designed`, but names \"internal/x.go\" in mesh-controller")
|
||||
if len(got) != 1 {
|
||||
t.Errorf("a plan without a path and an in-progress design are sound: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func TestMarkdownFilesSkipsToolFolders(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"a.md": {Data: []byte("x")},
|
||||
"b/c.md": {Data: []byte("x")},
|
||||
"b/c.txt": {Data: []byte("x")},
|
||||
".git/d.md": {Data: []byte("x")},
|
||||
".claude/e.md": {Data: []byte("x")},
|
||||
".work/f/g.md": {Data: []byte("x")},
|
||||
}
|
||||
got := markdownFiles(fsys)
|
||||
want := []string{"a.md", "b/c.md"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("markdownFiles = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontmatterReadsScalarsAndLists(t *testing.T) {
|
||||
f := frontmatter("---\nlayer: to-be\ndecisions:\n - ADR 0001\n - ADR 0002\ncode: []\ncodes: [a, b]\n---\nbody\n")
|
||||
if f.scalar("layer") != "to-be" {
|
||||
t.Errorf("layer = %q", f.scalar("layer"))
|
||||
}
|
||||
if !reflect.DeepEqual(f.list("decisions"), []string{"ADR 0001", "ADR 0002"}) {
|
||||
t.Errorf("decisions = %q", f.list("decisions"))
|
||||
}
|
||||
if len(f.list("code")) != 0 || !reflect.DeepEqual(f.list("codes"), []string{"a", "b"}) {
|
||||
t.Errorf("code = %q, codes = %q", f.list("code"), f.list("codes"))
|
||||
}
|
||||
if frontmatter("no frontmatter") != nil || frontmatter("---\nunclosed") != nil {
|
||||
t.Error("a document without a closed block has no frontmatter")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Package checks holds hq's four checks as Go tests (ADR 0527): records, index, cycle and words.
|
||||
//
|
||||
// The package has tests only. Each check is a function over an fs.FS that returns its problems, so
|
||||
// the test of the real tree reads the repository's root, and the test of the check itself reads a
|
||||
// deliberately broken tree built in memory and must see the problem named (every check here failed on
|
||||
// a broken fixture before it passed). The shared reading of Markdown and frontmatter is in this file.
|
||||
package checks
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// rootFS is hq's tree: the checks run from 00-META/checks, two folders under the root.
|
||||
func rootFS() fs.FS { return os.DirFS("../..") }
|
||||
|
||||
// skipDirs are folders no check reads: git's, the agent tooling's and a session's scratch folder.
|
||||
var skipDirs = map[string]bool{".git": true, ".claude": true, ".work": true}
|
||||
|
||||
// markdownFiles lists every Markdown file of the tree, sorted, in slash paths from the root.
|
||||
func markdownFiles(fsys fs.FS) []string {
|
||||
var out []string
|
||||
_ = fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
if p != "." && skipDirs[d.Name()] {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(p, ".md") {
|
||||
out = append(out, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func readFile(fsys fs.FS, p string) string {
|
||||
b, err := fs.ReadFile(fsys, p)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Front is a document's frontmatter: a scalar field holds a string, a list field a []string.
|
||||
type Front map[string]any
|
||||
|
||||
func (f Front) scalar(key string) string {
|
||||
s, _ := f[key].(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func (f Front) list(key string) []string {
|
||||
switch v := f[key].(type) {
|
||||
case []string:
|
||||
return v
|
||||
case string:
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return []string{v}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
fmItem = regexp.MustCompile(`^\s+-\s*(.+?)\s*$`)
|
||||
fmPair = regexp.MustCompile(`^([A-Za-z_-]+):\s*(.*)$`)
|
||||
)
|
||||
|
||||
// frontmatter reads the block between the first two --- lines. It is not a YAML parser on purpose: a
|
||||
// dependency to read a few scalar fields and one list would cost more than it returns. It returns nil
|
||||
// when the document has no frontmatter.
|
||||
func frontmatter(text string) Front {
|
||||
if !strings.HasPrefix(text, "---\n") {
|
||||
return nil
|
||||
}
|
||||
end := strings.Index(text[4:], "\n---")
|
||||
if end == -1 {
|
||||
return nil
|
||||
}
|
||||
out := Front{}
|
||||
key := ""
|
||||
for _, line := range strings.Split(text[4:4+end], "\n") {
|
||||
if m := fmItem.FindStringSubmatch(line); m != nil && key != "" {
|
||||
list, _ := out[key].([]string)
|
||||
out[key] = append(list, m[1])
|
||||
continue
|
||||
}
|
||||
m := fmPair.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
key = m[1]
|
||||
value := strings.TrimSpace(m[2])
|
||||
switch {
|
||||
case strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]"):
|
||||
list := []string{}
|
||||
for _, v := range strings.Split(value[1:len(value)-1], ",") {
|
||||
if v = strings.TrimSpace(v); v != "" {
|
||||
list = append(list, v)
|
||||
}
|
||||
}
|
||||
out[key] = list
|
||||
case value == "":
|
||||
out[key] = []string{}
|
||||
default:
|
||||
out[key] = value
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func unquote(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if len(v) >= 2 && v[0] == v[len(v)-1] && (v[0] == '"' || v[0] == '\'') {
|
||||
return v[1 : len(v)-1]
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// lineOf is the 1-based line of byte offset i in text.
|
||||
func lineOf(text string, i int) int { return strings.Count(text[:i], "\n") + 1 }
|
||||
|
||||
// expectNone fails the test with every problem a check found in the real tree.
|
||||
func expectNone(t *testing.T, problems []string) {
|
||||
t.Helper()
|
||||
for _, p := range problems {
|
||||
t.Error(p)
|
||||
}
|
||||
}
|
||||
|
||||
// expectProblem fails unless some problem contains want: the broken fixture must be caught, by name.
|
||||
func expectProblem(t *testing.T, problems []string, want string) {
|
||||
t.Helper()
|
||||
for _, p := range problems {
|
||||
if strings.Contains(p, want) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("no problem mentions %q; got %q", want, problems)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Words allowed — documents `words.py` does not hold to the glossary's retired words
|
||||
# Words allowed — documents the words test does not hold to the glossary's retired words
|
||||
|
||||
Read by [`words.py`](words.py) (ADR 0244).
|
||||
Read by the words test (`words_test.go`) (ADR 0244).
|
||||
One row per document. *Until* is the date by which it is reworded — past it, the row fails like the words
|
||||
themselves. A row whose document no longer uses a retired word fails too, so this list only shrinks. The
|
||||
research efforts this list kept as written (research 034 and 042) live in the knowledge base now (ADR 0290),
|
||||
@@ -8,3 +8,21 @@ which keeps a record's words as it was imported.
|
||||
|
||||
| Document | Until | Why |
|
||||
|---|---|---|
|
||||
| `04-ISSUES/285-the-merge-gate-compared-broken-to-broken-and-passed/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/300-a-new-module-is-not-built-at-its-merge/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/302-the-labs-go-replays-were-never-checked-before-merge/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/305-a-recheck-left-the-old-verdict-standing/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/338-a-module-built-from-a-shared-repository-moves-on-every-merge-to-it/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/341-a-merge-check-judges-the-head-not-what-merging-it-makes/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/353-the-controller-asked-before-the-bus-held-its-grant/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/364-the-gate-refuses-a-module-that-uses-a-seat-another-module-declares/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the words of its day, "merge gate" (issue 1303) and "judge" (the glossary's manifest validator retired the noun); it is not rewritten |
|
||||
| `04-ISSUES/286-a-merge-check-passed-on-an-agents-machine-and-failed-on-the-build-seat/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (the composition check's program built from a change; the glossary's manifest validator retired the noun); it is not rewritten |
|
||||
| `04-ISSUES/306-a-check-on-the-control-node-ran-past-its-timeout/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (the composition check's program built from a change; the glossary's manifest validator retired the noun); it is not rewritten |
|
||||
| `04-ISSUES/310-a-pull-request-ran-its-own-copy-of-its-check/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (the composition check's program built from a change; the glossary's manifest validator retired the noun); it is not rewritten |
|
||||
| `04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (a part of the controller's pull request, named so then; the glossary's manifest validator retired the noun); it is not rewritten |
|
||||
| `04-ISSUES/361-the-control-nodes-agent-account-is-never-judged/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (the self-check's look at what an account can reach; the glossary's manifest validator retired the noun); it is not rewritten |
|
||||
| `04-ISSUES/299-an-optional-verb-makes-a-holder-look-silent/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "seat set" (the seats compiled into the controller then; the glossary's seat list retired it); it is not rewritten |
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One word per thing, checked (ADR 0244).
|
||||
|
||||
The glossary (00-META/glossary.md) is the authority on the mesh's words. It names the words it
|
||||
retired on one fixed kind of line, so a program can read them:
|
||||
|
||||
*Not:* ~~control plane~~, ~~overlay~~ (hq)
|
||||
|
||||
A struck word with no scope is retired everywhere; `(hq)` retires it in this repository's prose only;
|
||||
`(tools)` in the descriptions of the mesh's tools only. And it names code that still carries an old
|
||||
name until that code is renamed:
|
||||
|
||||
*Identifier until renamed:* `mesh-host` — the repository, binary and service unit
|
||||
|
||||
An identifier is allowed inside a code span and nowhere else.
|
||||
|
||||
What is enforced:
|
||||
|
||||
retired no retired word of scope `hq` (or none), and no identifier, in running prose of a
|
||||
checked document. Running prose is what is left once code blocks, code spans, block
|
||||
quotes, text in quotation marks, struck-through text, link targets, HTML comments and
|
||||
frontmatter are taken out: a quotation keeps the words it quotes, a link target is a file
|
||||
name, and an identifier lives in a code span. A word's plural is the word: "alerts" fails
|
||||
as "alert" does.
|
||||
unique no head word (a bolded word opening a glossary entry) heads two entries, and no head
|
||||
word is also struck through on a *Not:* line.
|
||||
tools list with `--list tools` it prints the words retired in the tools' descriptions, which is the
|
||||
copy the catalogue's own check keeps (mesh-catalog `retired-words`). With
|
||||
MESH_CATALOG_DIR set to a checkout of the catalogue it also fails when that copy differs.
|
||||
|
||||
lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09). The operator
|
||||
types one command, `nox` (`mesh-cli`); the node-engine (`nox-mesh-host`) and the
|
||||
controller (`mesh-controller`) are daemons, and a person never types their names — so a
|
||||
document names a `nox` line, never `mesh-controller push` or `mesh-host hand-over`. The
|
||||
lines are DAEMON_LINES below: each daemon's commands, and which of them are a line only
|
||||
with an argument after them ("the mesh-controller module" is prose; `mesh-controller
|
||||
module check …` is a line). A line is looked for in the document's own words — code
|
||||
blocks and code spans included, since that is where a line to type is written — with
|
||||
quotations, block quotes, link targets and comments taken out. `--list lines` prints the
|
||||
list, which the catalogue's own check keeps a copy of (mesh-catalog `retired-lines`); with
|
||||
MESH_CATALOG_DIR set it also fails when that copy differs. The node-engine's install-time
|
||||
subcommands (`nox-mesh-host enrol`, `key`, `reconcile`, …) are not lines here: they are
|
||||
typed at a machine no mesh reaches yet, and the engine's usage says so.
|
||||
|
||||
Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened
|
||||
on or after FROM (on or after LINES_FROM for the lines). The decision records and research efforts live in
|
||||
the knowledge base (ADR 0290, issue 426), which holds a record proposed there to the same words through its
|
||||
rules `retired-word` and `daemon-line`, read from this glossary.
|
||||
|
||||
A document the check fails on that cannot be reworded in the change that found it is named, with a date
|
||||
and a reason, in words-allowed.md beside this file. An entry past its date fails like the word itself, and
|
||||
an entry for a document that no longer needs it fails too.
|
||||
|
||||
python3 00-META/checks/words.py
|
||||
python3 00-META/checks/words.py --list tools
|
||||
python3 00-META/checks/words.py --list lines
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
GLOSSARY = "00-META/glossary.md"
|
||||
ALLOWED = "00-META/checks/words-allowed.md"
|
||||
|
||||
# The day the rule began (ADR 0244): issues opened from it on are held to it.
|
||||
FROM = "2026-10-07"
|
||||
|
||||
ALWAYS = ("00-META/", "03-DESIGN/", "AGENTS.md", "README.md")
|
||||
|
||||
# The daemons' lines (ADR 0272, its insight of 2026-10-09), and the day that rule began. "alone" is a
|
||||
# command that is a line by itself; "with an argument" one that is a line only when written as one.
|
||||
LINES_FROM = "2026-10-09"
|
||||
DAEMON_LINES = {
|
||||
"mesh-controller": {
|
||||
"alone": ("adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act "
|
||||
"hand-acts healers identity kill licence migrate operator pause pin plan prepare push rebuild "
|
||||
"rehearse replay resume retire rollout rotate secret settings status token unassign unpin "
|
||||
"upgrade").split(),
|
||||
"with an argument": ("artifacts ask builds bus collection data delivery images mirrors module node "
|
||||
"overlay plans queue seat seats take version").split(),
|
||||
},
|
||||
"mesh-host": {
|
||||
"alone": "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version".split(),
|
||||
"with an argument": [],
|
||||
},
|
||||
"nox-mesh-host": {"alone": ["hand-over"], "with an argument": []},
|
||||
}
|
||||
|
||||
NOT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Not:\*(.*)$", re.M)
|
||||
STRUCK = re.compile(r"~~([^~]+)~~(?:\s*\((hq|tools)\))?")
|
||||
IDENT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$", re.M)
|
||||
CODE_SPAN = re.compile(r"`([^`]+)`")
|
||||
HEAD = re.compile(r"^\s*-\s+\*\*([^*]+)\*\*", re.M)
|
||||
|
||||
|
||||
def rel(path):
|
||||
return os.path.relpath(path, ROOT)
|
||||
|
||||
|
||||
def read(path):
|
||||
with open(os.path.join(ROOT, path), encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
def frontmatter_field(text, name):
|
||||
if not text.startswith("---\n"):
|
||||
return None
|
||||
end = text.find("\n---", 4)
|
||||
m = re.search(r"^%s:\s*(\S+)" % name, text[4:end], re.M)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def glossary():
|
||||
"""The retired words with their scope, the identifiers, and the head words."""
|
||||
text = read(GLOSSARY)
|
||||
retired = []
|
||||
for line in NOT_LINE.findall(text):
|
||||
for word, scope in STRUCK.findall(line):
|
||||
for one in word.split(" / "):
|
||||
retired.append((one.strip(), scope or "all"))
|
||||
identifiers = []
|
||||
for line in IDENT_LINE.findall(text):
|
||||
identifiers += [i.strip() for i in CODE_SPAN.findall(line)]
|
||||
heads = []
|
||||
for head in HEAD.findall(text):
|
||||
heads += [h.strip() for h in head.split(" / ")]
|
||||
return retired, identifiers, heads
|
||||
|
||||
|
||||
def pattern(word):
|
||||
"""A whole-word, case-insensitive match; a space in the word matches a space, a line break or a hyphen.
|
||||
|
||||
The plural is the word too: its last part may end in `s` or `es` ("alerts", "control planes"), since
|
||||
a retired word does not come back by being counted. Anything else joined on is another word.
|
||||
"""
|
||||
parts = [re.escape(p) for p in word.split()]
|
||||
return re.compile(r"(?i)(?<![\w-])" + r"[\s-]+".join(parts) + r"(?:e?s)?(?![\w-])")
|
||||
|
||||
|
||||
def blank(match):
|
||||
return re.sub(r"[^\n]", " ", match.group(0))
|
||||
|
||||
|
||||
def prose(text, glossary_file=False):
|
||||
"""The text with everything that is not running prose blanked, keeping offsets and line numbers."""
|
||||
if text.startswith("---\n"):
|
||||
end = text.find("\n---", 4)
|
||||
if end != -1:
|
||||
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
|
||||
steps = [
|
||||
r"(?ms)^\s*```.*?^\s*```", # code blocks
|
||||
r"(?s)<!--.*?-->", # comments
|
||||
r"(?m)^\s*>.*$", # block quotes
|
||||
r"`[^`\n]+`", # code spans
|
||||
r"\]\([^)\s]*\)", # link targets
|
||||
r"~~[^~\n]+~~", # struck through: a word named as retired
|
||||
r"\"[^\"\n]*\"", # "quoted"
|
||||
r"\u201c[^\u201d]*\u201d", # curly double quotes
|
||||
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
|
||||
]
|
||||
if glossary_file:
|
||||
steps[:0] = [NOT_LINE.pattern, IDENT_LINE.pattern]
|
||||
for step in steps:
|
||||
text = re.sub(step, blank, text)
|
||||
return text
|
||||
|
||||
|
||||
def issues(since=FROM):
|
||||
"""The issue reports opened on or after `since`, file by file."""
|
||||
out = []
|
||||
base = os.path.join(ROOT, "04-ISSUES")
|
||||
if not os.path.isdir(base):
|
||||
return out
|
||||
for issue in sorted(os.listdir(base)):
|
||||
head = os.path.join(base, issue, "00-report.md")
|
||||
if not os.path.isfile(head):
|
||||
continue
|
||||
date = frontmatter_field(read(rel(head)), "opened")
|
||||
if not date or date < since:
|
||||
continue
|
||||
for name in sorted(os.listdir(os.path.join(base, issue))):
|
||||
if name.endswith(".md"):
|
||||
out.append("04-ISSUES/%s/%s" % (issue, name))
|
||||
return out
|
||||
|
||||
|
||||
def checked_documents(since=FROM):
|
||||
docs = []
|
||||
for entry in ALWAYS:
|
||||
path = os.path.join(ROOT, entry)
|
||||
if os.path.isfile(path):
|
||||
docs.append(entry)
|
||||
continue
|
||||
for base, dirs, files in os.walk(path):
|
||||
dirs.sort()
|
||||
for name in sorted(files):
|
||||
if name.endswith(".md"):
|
||||
docs.append(rel(os.path.join(base, name)))
|
||||
return docs + issues(since)
|
||||
|
||||
|
||||
def allowed():
|
||||
"""words-allowed.md: one row per document — | `document` | until | why |.
|
||||
|
||||
`until` is a date, after which the allowance fails like the words themselves.
|
||||
"""
|
||||
out, problems = {}, []
|
||||
if not os.path.isfile(os.path.join(ROOT, ALLOWED)):
|
||||
return out, problems
|
||||
today = datetime.date.today().isoformat()
|
||||
for line in read(ALLOWED).splitlines():
|
||||
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
||||
if len(cells) != 3 or not cells[0].startswith("`"):
|
||||
continue
|
||||
doc, until, why = cells[0].strip("`"), cells[1], cells[2]
|
||||
if not why:
|
||||
problems.append("%s: the allowance for %s gives no reason" % (ALLOWED, doc))
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", until):
|
||||
problems.append("%s: the allowance for %s has no date" % (ALLOWED, doc))
|
||||
continue
|
||||
elif until < today:
|
||||
problems.append("%s: the allowance for %s ran out on %s — reword it" % (ALLOWED, doc, until))
|
||||
continue
|
||||
out[doc] = until
|
||||
return out, problems
|
||||
|
||||
|
||||
def check_retired(retired, identifiers):
|
||||
problems = []
|
||||
allowance, problems_allowed = allowed()
|
||||
problems += problems_allowed
|
||||
words = [(w, pattern(w), "retired") for w, scope in retired if scope in ("all", "hq")]
|
||||
words += [(i, pattern(i), "an identifier, allowed only in a code span") for i in identifiers]
|
||||
used = set()
|
||||
for doc in checked_documents():
|
||||
text = prose(read(doc), glossary_file=(doc == GLOSSARY))
|
||||
for word, rx, why in words:
|
||||
for m in rx.finditer(text):
|
||||
if doc in allowance:
|
||||
used.add(doc)
|
||||
continue
|
||||
line = text.count("\n", 0, m.start()) + 1
|
||||
problems.append("%s:%d: %r is %s (glossary)" % (doc, line, m.group(0).replace("\n", " "), why))
|
||||
for doc in allowance:
|
||||
if doc not in used:
|
||||
problems.append("%s: %s no longer uses a retired word — remove its allowance" % (ALLOWED, doc))
|
||||
return problems
|
||||
|
||||
|
||||
def lines_list():
|
||||
"""The daemons' lines as the catalogue's copy lists them: `<daemon> <command>`, ` …` when an argument must follow."""
|
||||
out = []
|
||||
for daemon, kinds in DAEMON_LINES.items():
|
||||
out += ["%s %s" % (daemon, c) for c in kinds["alone"]]
|
||||
out += ["%s %s …" % (daemon, c) for c in kinds["with an argument"]]
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def line_pattern(daemon, command, with_argument):
|
||||
"""The line whole: the daemon, white space, the command. One that is a line only with an argument is matched
|
||||
as a line is written — at the start of a line or a code span, a prompt allowed, an argument after it."""
|
||||
d, c = re.escape(daemon), re.escape(command)
|
||||
if with_argument:
|
||||
return re.compile(r"(?m)(?:^|`)[ \t]*(?:\$[ \t]*)?(" + d + r"\s+" + c + r"\s+[^\s`])")
|
||||
return re.compile(r"(?<![\w-])" + d + r"\s+" + c + r"(?![\w-])")
|
||||
|
||||
|
||||
def own_words(text):
|
||||
"""The text with what is not the document's own words blanked: as prose(), but a code block and a code span
|
||||
are kept, since a line to type is written in one."""
|
||||
if text.startswith("---\n"):
|
||||
end = text.find("\n---", 4)
|
||||
if end != -1:
|
||||
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
|
||||
for step in (
|
||||
r"(?s)<!--.*?-->", # comments
|
||||
r"(?m)^\s*>.*$", # block quotes
|
||||
r"\]\([^)\s]*\)", # link targets
|
||||
r"~~[^~\n]+~~", # struck through
|
||||
r"\"[^\"\n]*\"", # "quoted"
|
||||
r"\u201c[^\u201d]*\u201d", # curly double quotes
|
||||
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
|
||||
):
|
||||
text = re.sub(step, blank, text)
|
||||
return text
|
||||
|
||||
|
||||
def check_lines():
|
||||
problems = []
|
||||
patterns = []
|
||||
for daemon, kinds in DAEMON_LINES.items():
|
||||
patterns += [(daemon, c, line_pattern(daemon, c, False)) for c in kinds["alone"]]
|
||||
patterns += [(daemon, c, line_pattern(daemon, c, True)) for c in kinds["with an argument"]]
|
||||
for doc in checked_documents(LINES_FROM):
|
||||
text = own_words(read(doc))
|
||||
for daemon, command, rx in patterns:
|
||||
for m in rx.finditer(text):
|
||||
line = text.count("\n", 0, m.start()) + 1
|
||||
problems.append("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)"
|
||||
% (doc, line, daemon, command))
|
||||
return problems
|
||||
|
||||
|
||||
def check_unique(retired, heads):
|
||||
problems = []
|
||||
seen = {}
|
||||
for head in heads:
|
||||
key = head.lower()
|
||||
if key in seen:
|
||||
problems.append("%s: %r heads two entries" % (GLOSSARY, head))
|
||||
seen[key] = True
|
||||
for word, _ in retired:
|
||||
if word.lower() in seen:
|
||||
problems.append("%s: %r is a head word and also retired" % (GLOSSARY, word))
|
||||
return problems
|
||||
|
||||
|
||||
def tools_list(retired):
|
||||
return sorted({w for w, scope in retired if scope in ("all", "tools")}, key=str.lower)
|
||||
|
||||
|
||||
def check_copy(retired):
|
||||
catalogue = os.environ.get("MESH_CATALOG_DIR")
|
||||
if not catalogue:
|
||||
print("NOT COMPARED: MESH_CATALOG_DIR is not set, so the catalogue's copy of the list was not read")
|
||||
return []
|
||||
path = os.path.join(catalogue, "retired-words")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
|
||||
except OSError as err:
|
||||
return ["the catalogue's copy of the retired words cannot be read: %s" % err]
|
||||
want = tools_list(retired)
|
||||
problems = []
|
||||
if sorted(copy, key=str.lower) != want:
|
||||
problems.append("the catalogue's retired-words differs from the glossary: it should list %s" % ", ".join(want))
|
||||
path = os.path.join(catalogue, "retired-lines")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
|
||||
except OSError as err:
|
||||
return problems + ["the catalogue's copy of the daemons' lines cannot be read: %s" % err]
|
||||
if sorted(copy) != lines_list():
|
||||
problems.append("the catalogue's retired-lines differs from this check's list: regenerate it with "
|
||||
"`python3 00-META/checks/words.py --list lines`")
|
||||
return problems
|
||||
|
||||
|
||||
def main(argv):
|
||||
retired, identifiers, heads = glossary()
|
||||
if argv[1:2] == ["--list"]:
|
||||
scope = argv[2] if len(argv) > 2 else "tools"
|
||||
if scope == "lines":
|
||||
words = lines_list()
|
||||
else:
|
||||
words = tools_list(retired) if scope == "tools" else sorted({w for w, s in retired if s in ("all", scope)})
|
||||
print("\n".join(words))
|
||||
return 0
|
||||
if not retired:
|
||||
print("words: the glossary names no retired word on a *Not:* line — the check would pass on anything")
|
||||
return 1
|
||||
problems = check_unique(retired, heads) + check_retired(retired, identifiers) + check_lines() + check_copy(retired)
|
||||
for p in problems:
|
||||
print(p)
|
||||
if problems:
|
||||
print("words: %d problem(s)" % len(problems))
|
||||
return 1
|
||||
print("words: %d retired words and %d identifiers, none in running prose; %d daemons' lines, none written; "
|
||||
"%d head words, each once" % (len(retired), len(identifiers), len(lines_list()), len(heads)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,862 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// One word per thing, checked (words.py until ADR 0527; ADR 0244). The glossary (00-META/glossary.md) is the
|
||||
// authority on the mesh's words. It names the words it retired on one fixed kind of line, so a program can read them:
|
||||
//
|
||||
// *Not:* ~~control plane~~, ~~overlay~~ (hq)
|
||||
//
|
||||
// A struck word with no scope is retired everywhere; `(hq)` retires it in this repository's prose only; `(tools)`
|
||||
// in the descriptions of the mesh's tools only. And it names code that still carries an old name until it is renamed:
|
||||
//
|
||||
// *Identifier until renamed:* `mesh-host` - the repository, binary and service unit
|
||||
//
|
||||
// An identifier is allowed inside a code span and nowhere else.
|
||||
//
|
||||
// What is enforced:
|
||||
//
|
||||
// retired no retired word of scope hq (or none), and no identifier, in running prose of a checked document.
|
||||
// Running prose is what is left once code blocks, code spans, block quotes, text in quotation marks,
|
||||
// struck-through text, link targets, HTML comments and frontmatter are taken out. A word's plural is the word.
|
||||
// unique no head word heads two entries, and none is also struck through on a *Not:* line.
|
||||
// lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09): the operator types `nox`.
|
||||
// holders the glossary says no seat's holder: no seat "held by" or "claimed by" a named module, no named module
|
||||
// "holder of" a seat, no module that "holds the seat" (the operator, 2026-11-09). Who holds a seat is read
|
||||
// from the mesh's assignments, and the glossary's head says where.
|
||||
// domains no retired domain heads a section of the glossary (ADR 0641 §5: Placement).
|
||||
// copy with MESH_CATALOG_DIR set to a checkout of the catalog, its copies of the retired words and the
|
||||
// daemons' lines equal this glossary's and this list (skipped, with its reason, when unset).
|
||||
//
|
||||
// Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened on or after
|
||||
// wordsFrom (on or after linesFrom for the lines). A document that cannot be reworded in the change that found it is
|
||||
// named, with a date and a reason, in words-allowed.md beside this file; an entry past its date fails like the word
|
||||
// itself, and so does an entry for a document that no longer needs it.
|
||||
|
||||
const (
|
||||
glossaryFile = "00-META/glossary.md"
|
||||
allowedFile = "00-META/checks/words-allowed.md"
|
||||
// The day the rule began (ADR 0244): issues opened from it on are held to it.
|
||||
wordsFrom = "2026-10-07"
|
||||
// The day the daemons' lines rule began.
|
||||
linesFrom = "2026-10-09"
|
||||
)
|
||||
|
||||
var wordsAlways = []string{"00-META/", "03-DESIGN/", ".claude/", "AGENTS.md", "README.md"}
|
||||
|
||||
// daemonLines are the daemons' lines (ADR 0272): each daemon's commands. "alone" is a command that is a line by
|
||||
// itself; "with an argument" one that is a line only when written as one ("the mesh-controller module" is prose;
|
||||
// `mesh-controller module check ...` is a line). The node-engine's install-time subcommands are not lines here.
|
||||
var daemonLines = []struct {
|
||||
daemon, alone, withArgument string
|
||||
}{
|
||||
{"mesh-controller",
|
||||
"adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act hand-acts healers identity kill licence " +
|
||||
"migrate operator pause pin plan prepare push rebuild rehearse replay resume retire rollout rotate secret settings status token " +
|
||||
"unassign unpin upgrade",
|
||||
"artifacts ask builds bus collection data delivery images mirrors module node overlay plans queue seat seats take version"},
|
||||
{"mesh-host", "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version", ""},
|
||||
{"nox-mesh-host", "hand-over", ""},
|
||||
}
|
||||
|
||||
var (
|
||||
notLineRx = regexp.MustCompile(`(?m)^\s*(?:[-*]\s+)?\*Not:\*(.*)$`)
|
||||
struckRx = regexp.MustCompile(`~~([^~]+)~~(?:\s*\((hq|tools)\))?`)
|
||||
identLineRx = regexp.MustCompile(`(?m)^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$`)
|
||||
spanRx = regexp.MustCompile("`([^`]+)`")
|
||||
headRx = regexp.MustCompile(`(?m)^\s*-\s+\*\*([^*]+)\*\*`)
|
||||
openedRx = regexp.MustCompile(`(?m)^opened:\s*(\S+)`)
|
||||
)
|
||||
|
||||
type retiredWord struct{ word, scope string }
|
||||
|
||||
// parseGlossary reads the retired words with their scope, the identifiers and the head words from the glossary.
|
||||
func parseGlossary(text string) (retired []retiredWord, identifiers, heads []string) {
|
||||
for _, m := range notLineRx.FindAllStringSubmatch(text, -1) {
|
||||
for _, s := range struckRx.FindAllStringSubmatch(m[1], -1) {
|
||||
scope := s[2]
|
||||
if scope == "" {
|
||||
scope = "all"
|
||||
}
|
||||
for _, one := range strings.Split(s[1], " / ") {
|
||||
retired = append(retired, retiredWord{strings.TrimSpace(one), scope})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range identLineRx.FindAllStringSubmatch(text, -1) {
|
||||
for _, s := range spanRx.FindAllStringSubmatch(m[1], -1) {
|
||||
identifiers = append(identifiers, strings.TrimSpace(s[1]))
|
||||
}
|
||||
}
|
||||
for _, m := range headRx.FindAllStringSubmatch(text, -1) {
|
||||
for _, h := range strings.Split(m[1], " / ") {
|
||||
heads = append(heads, strings.TrimSpace(h))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isWordRune(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-'
|
||||
}
|
||||
|
||||
// findBounded returns the start offsets of the matches of rx that are whole words: no word character or hyphen
|
||||
// before or after (the lookarounds of the Python check, which RE2 has not).
|
||||
func findBounded(rx *regexp.Regexp, text string) []int {
|
||||
var out []int
|
||||
pos := 0
|
||||
for pos <= len(text) {
|
||||
loc := rx.FindStringIndex(text[pos:])
|
||||
if loc == nil {
|
||||
break
|
||||
}
|
||||
start, end := pos+loc[0], pos+loc[1]
|
||||
before, after := true, true
|
||||
if start > 0 {
|
||||
r := []rune(text[max(0, start-4):start])
|
||||
before = !isWordRune(r[len(r)-1])
|
||||
}
|
||||
if end < len(text) {
|
||||
r := []rune(text[end:min(len(text), end+4)])
|
||||
after = !isWordRune(r[0])
|
||||
}
|
||||
if before && after {
|
||||
out = append(out, start)
|
||||
pos = end
|
||||
if end == start {
|
||||
pos++
|
||||
}
|
||||
} else {
|
||||
pos = start + 1
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// wordRegexp matches a word whole, case-insensitively; a space in the word matches a space, a line break or a hyphen.
|
||||
// The plural is the word too: its last part may end in s or es ("alerts", "control planes").
|
||||
func wordRegexp(word string) *regexp.Regexp {
|
||||
var parts []string
|
||||
for _, p := range strings.Fields(word) {
|
||||
parts = append(parts, regexp.QuoteMeta(p))
|
||||
}
|
||||
return regexp.MustCompile(`(?i)` + strings.Join(parts, `[\s-]+`) + `(?:e?s)?`)
|
||||
}
|
||||
|
||||
// blank replaces every byte of s but the line breaks, so offsets and line numbers hold.
|
||||
func blank(s string) string {
|
||||
b := []byte(s)
|
||||
for i, c := range b {
|
||||
if c != '\n' {
|
||||
b[i] = ' '
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func blankFrontmatter(text string) string {
|
||||
if strings.HasPrefix(text, "---\n") {
|
||||
if end := strings.Index(text[4:], "\n---"); end != -1 {
|
||||
end += 4
|
||||
return blank(text[:end+4]) + text[end+4:]
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func blankAll(text string, steps []*regexp.Regexp) string {
|
||||
for _, rx := range steps {
|
||||
text = rx.ReplaceAllStringFunc(text, blank)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func compileAll(patterns ...string) []*regexp.Regexp {
|
||||
var out []*regexp.Regexp
|
||||
for _, p := range patterns {
|
||||
out = append(out, regexp.MustCompile(p))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// What is not the document's own words, in the order it is taken out.
|
||||
const (
|
||||
stepComment = `(?s)<!--.*?-->`
|
||||
stepBlockQuote = `(?m)^\s*>.*$`
|
||||
stepLinkTarget = `\]\([^)\s]*\)`
|
||||
stepStruck = `~~[^~\n]+~~`
|
||||
stepQuoted = `"[^"\n]*"`
|
||||
stepCurlyDouble = `\x{201c}[^\x{201d}]*\x{201d}`
|
||||
stepCurlySingle = `\x{2018}[^\x{2019}\n]*\x{2019}`
|
||||
)
|
||||
|
||||
var (
|
||||
proseSteps = compileAll("(?ms)^\\s*```.*?^\\s*```", stepComment, stepBlockQuote, "`[^`\\n]+`", stepLinkTarget, stepStruck,
|
||||
stepQuoted, stepCurlyDouble, stepCurlySingle)
|
||||
// In the glossary the *Not:* and *Identifier until renamed:* lines name the retired words, so they are taken out first.
|
||||
glossaryProseSteps = append(compileAll(notLineRx.String(), identLineRx.String()), proseSteps...)
|
||||
// The daemons' lines are looked for in the document's own words: a code block and a code span are kept, since a
|
||||
// line to type is written in one.
|
||||
ownWordsSteps = compileAll(stepComment, stepBlockQuote, stepLinkTarget, stepStruck, stepQuoted, stepCurlyDouble, stepCurlySingle)
|
||||
)
|
||||
|
||||
// prose is the text with everything that is not running prose blanked, keeping offsets and line numbers.
|
||||
func prose(text string, glossaryDoc bool) string {
|
||||
text = blankFrontmatter(text)
|
||||
if glossaryDoc {
|
||||
return blankAll(text, glossaryProseSteps)
|
||||
}
|
||||
return blankAll(text, proseSteps)
|
||||
}
|
||||
|
||||
func ownWords(text string) string { return blankAll(blankFrontmatter(text), ownWordsSteps) }
|
||||
|
||||
// issueDocuments lists the issue reports' files opened on or after since.
|
||||
func issueDocuments(fsys fs.FS, since string) []string {
|
||||
var out []string
|
||||
entries, _ := fs.ReadDir(fsys, "04-ISSUES")
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
head := readFile(fsys, path.Join("04-ISSUES", e.Name(), "00-report.md"))
|
||||
front := ""
|
||||
if strings.HasPrefix(head, "---\n") {
|
||||
if end := strings.Index(head[4:], "\n---"); end != -1 {
|
||||
if m := openedRx.FindStringSubmatch(head[4 : 4+end]); m != nil {
|
||||
front = m[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if front == "" || front < since {
|
||||
continue
|
||||
}
|
||||
files, _ := fs.ReadDir(fsys, path.Join("04-ISSUES", e.Name()))
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f.Name(), ".md") {
|
||||
out = append(out, path.Join("04-ISSUES", e.Name(), f.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// perDocument runs fn over the documents on all processors (the unit level is short) and joins the problems in
|
||||
// the documents' order.
|
||||
func perDocument(docs []string, fn func(doc string) []string) []string {
|
||||
results := make([][]string, len(docs))
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, runtime.NumCPU())
|
||||
for i, d := range docs {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
results[i] = fn(d)
|
||||
<-sem
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
var out []string
|
||||
for _, r := range results {
|
||||
out = append(out, r...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func checkedDocuments(fsys fs.FS, since string) []string {
|
||||
var docs []string
|
||||
for _, entry := range wordsAlways {
|
||||
if strings.HasSuffix(entry, "/") {
|
||||
_ = fs.WalkDir(fsys, strings.TrimSuffix(entry, "/"), func(p string, d fs.DirEntry, err error) error {
|
||||
if err == nil && !d.IsDir() && strings.HasSuffix(p, ".md") {
|
||||
docs = append(docs, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else if _, err := fs.Stat(fsys, entry); err == nil {
|
||||
docs = append(docs, entry)
|
||||
}
|
||||
}
|
||||
return append(docs, issueDocuments(fsys, since)...)
|
||||
}
|
||||
|
||||
var dateRx = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
||||
|
||||
// parseAllowed reads words-allowed.md: one row per document, | `document` | until | why |. Until is a date, after
|
||||
// which the allowance fails like the words themselves. The clock is the caller's: today is YYYY-MM-DD.
|
||||
func parseAllowed(text, today string) (allowed map[string]string, problems []string) {
|
||||
allowed = map[string]string{}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
cells := strings.Split(strings.Trim(strings.TrimSpace(line), "|"), "|")
|
||||
if len(cells) != 3 {
|
||||
continue
|
||||
}
|
||||
for i := range cells {
|
||||
cells[i] = strings.TrimSpace(cells[i])
|
||||
}
|
||||
if !strings.HasPrefix(cells[0], "`") {
|
||||
continue
|
||||
}
|
||||
doc, until, why := strings.Trim(cells[0], "`"), cells[1], cells[2]
|
||||
if why == "" {
|
||||
problems = append(problems, fmt.Sprintf("%s: the allowance for %s gives no reason", allowedFile, doc))
|
||||
}
|
||||
if !dateRx.MatchString(until) {
|
||||
problems = append(problems, fmt.Sprintf("%s: the allowance for %s has no date", allowedFile, doc))
|
||||
continue
|
||||
}
|
||||
if until < today {
|
||||
problems = append(problems, fmt.Sprintf("%s: the allowance for %s ran out on %s — reword it", allowedFile, doc, until))
|
||||
continue
|
||||
}
|
||||
allowed[doc] = until
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkRetired(fsys fs.FS, retired []retiredWord, identifiers []string, today string) []string {
|
||||
allowance, out := parseAllowed(readFile(fsys, allowedFile), today)
|
||||
type matcher struct {
|
||||
word string
|
||||
rx *regexp.Regexp
|
||||
why string
|
||||
}
|
||||
var words []matcher
|
||||
for _, r := range retired {
|
||||
if r.scope == "all" || r.scope == "hq" {
|
||||
words = append(words, matcher{r.word, wordRegexp(r.word), "retired"})
|
||||
}
|
||||
}
|
||||
for _, i := range identifiers {
|
||||
words = append(words, matcher{i, wordRegexp(i), "an identifier, allowed only in a code span"})
|
||||
}
|
||||
var mu sync.Mutex
|
||||
used := map[string]bool{}
|
||||
out = append(out, perDocument(checkedDocuments(fsys, wordsFrom), func(d string) []string {
|
||||
var found []string
|
||||
text := prose(readFile(fsys, d), d == glossaryFile)
|
||||
lower := strings.ToLower(text)
|
||||
for _, w := range words {
|
||||
// Every match holds the word's first part literally, so a text without it is not searched (the unit level is short).
|
||||
if !strings.Contains(lower, strings.ToLower(strings.Fields(w.word)[0])) {
|
||||
continue
|
||||
}
|
||||
for _, start := range findBounded(w.rx, text) {
|
||||
if _, ok := allowance[d]; ok {
|
||||
mu.Lock()
|
||||
used[d] = true
|
||||
mu.Unlock()
|
||||
continue
|
||||
}
|
||||
m := w.rx.FindString(text[start:])
|
||||
found = append(found, fmt.Sprintf("%s:%d: %q is %s (glossary)", d, lineOf(text, start), strings.ReplaceAll(m, "\n", " "), w.why))
|
||||
}
|
||||
}
|
||||
return found
|
||||
})...)
|
||||
var unused []string
|
||||
for d := range allowance {
|
||||
if !used[d] {
|
||||
unused = append(unused, d)
|
||||
}
|
||||
}
|
||||
sort.Strings(unused)
|
||||
for _, d := range unused {
|
||||
out = append(out, fmt.Sprintf("%s: %s no longer uses a retired word — remove its allowance", allowedFile, d))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func checkUnique(retired []retiredWord, heads []string) []string {
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
for _, h := range heads {
|
||||
k := strings.ToLower(h)
|
||||
if seen[k] {
|
||||
out = append(out, fmt.Sprintf("%s: %q heads two entries", glossaryFile, h))
|
||||
}
|
||||
seen[k] = true
|
||||
}
|
||||
for _, r := range retired {
|
||||
if seen[strings.ToLower(r.word)] {
|
||||
out = append(out, fmt.Sprintf("%s: %q is a head word and also retired", glossaryFile, r.word))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// linesList is the daemons' lines as the catalog's copy lists them: "<daemon> <command>", with " …" when an
|
||||
// argument must follow.
|
||||
func linesList() []string {
|
||||
var out []string
|
||||
for _, d := range daemonLines {
|
||||
for _, c := range strings.Fields(d.alone) {
|
||||
out = append(out, d.daemon+" "+c)
|
||||
}
|
||||
for _, c := range strings.Fields(d.withArgument) {
|
||||
out = append(out, d.daemon+" "+c+" …")
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func checkLines(fsys fs.FS) []string {
|
||||
type pat struct {
|
||||
daemon, command string
|
||||
rx *regexp.Regexp
|
||||
bounded bool
|
||||
}
|
||||
var pats []pat
|
||||
for _, d := range daemonLines {
|
||||
dq := regexp.QuoteMeta(d.daemon)
|
||||
for _, c := range strings.Fields(d.alone) {
|
||||
pats = append(pats, pat{d.daemon, c, regexp.MustCompile(dq + `\s+` + regexp.QuoteMeta(c)), true})
|
||||
}
|
||||
for _, c := range strings.Fields(d.withArgument) {
|
||||
// A line only when written as one: at the start of a line or a code span, a prompt allowed, an argument after.
|
||||
pats = append(pats, pat{d.daemon, c, regexp.MustCompile("(?m)(?:^|`)[ \\t]*(?:\\$[ \\t]*)?(" + dq + `\s+` + regexp.QuoteMeta(c) + "\\s+[^\\s`])"), false})
|
||||
}
|
||||
}
|
||||
return perDocument(checkedDocuments(fsys, linesFrom), func(d string) []string {
|
||||
var out []string
|
||||
text := ownWords(readFile(fsys, d))
|
||||
for _, p := range pats {
|
||||
if !strings.Contains(text, p.daemon) {
|
||||
continue
|
||||
}
|
||||
var starts []int
|
||||
if p.bounded {
|
||||
starts = findBounded(p.rx, text)
|
||||
} else {
|
||||
for _, loc := range p.rx.FindAllStringSubmatchIndex(text, -1) {
|
||||
starts = append(starts, loc[2])
|
||||
}
|
||||
}
|
||||
for _, s := range starts {
|
||||
out = append(out, fmt.Sprintf("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)", d, lineOf(text, s), p.daemon, p.command))
|
||||
}
|
||||
}
|
||||
return out
|
||||
})
|
||||
}
|
||||
|
||||
func toolsList(retired []retiredWord) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, r := range retired {
|
||||
if (r.scope == "all" || r.scope == "tools") && !seen[r.word] {
|
||||
seen[r.word] = true
|
||||
out = append(out, r.word)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i]) < strings.ToLower(out[j]) })
|
||||
return out
|
||||
}
|
||||
|
||||
func readList(file string) ([]string, error) {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, l := range strings.Split(string(b), "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// checkCopy compares the catalog's copies of the retired words and the daemons' lines with this glossary's. A
|
||||
// difference names what the copy should list, so regenerating it is copying that from the failure.
|
||||
func checkCopy(catalogue string, retired []retiredWord) []string {
|
||||
var out []string
|
||||
copyWords, err := readList(path.Join(catalogue, "retired-words"))
|
||||
if err != nil {
|
||||
return []string{"the catalog's copy of the retired words cannot be read: " + err.Error()}
|
||||
}
|
||||
sort.Slice(copyWords, func(i, j int) bool { return strings.ToLower(copyWords[i]) < strings.ToLower(copyWords[j]) })
|
||||
want := toolsList(retired)
|
||||
if strings.Join(copyWords, "\n") != strings.Join(want, "\n") {
|
||||
out = append(out, "the catalog's retired-words differs from the glossary: it should list "+strings.Join(want, ", "))
|
||||
}
|
||||
copyLines, err := readList(path.Join(catalogue, "retired-lines"))
|
||||
if err != nil {
|
||||
return append(out, "the catalog's copy of the daemons' lines cannot be read: "+err.Error())
|
||||
}
|
||||
sort.Strings(copyLines)
|
||||
if strings.Join(copyLines, "\n") != strings.Join(linesList(), "\n") {
|
||||
out = append(out, "the catalog's retired-lines differs from this check's list: it should list, one per line:\n"+strings.Join(linesList(), "\n"))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// The real tree.
|
||||
|
||||
func today() string { return time.Now().Format("2006-01-02") }
|
||||
|
||||
func TestWordsGlossaryNamesRetiredWords(t *testing.T) {
|
||||
retired, identifiers, heads := parseGlossary(readFile(rootFS(), glossaryFile))
|
||||
if len(retired) == 0 {
|
||||
t.Fatal("the glossary names no retired word on a *Not:* line — the check would pass on anything")
|
||||
}
|
||||
if len(identifiers) == 0 || len(heads) == 0 {
|
||||
t.Errorf("the glossary names %d identifiers and %d head words; both are expected", len(identifiers), len(heads))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordsEachHeadWordHeadsOneEntry(t *testing.T) {
|
||||
retired, _, heads := parseGlossary(readFile(rootFS(), glossaryFile))
|
||||
expectNone(t, checkUnique(retired, heads))
|
||||
}
|
||||
|
||||
func TestWordsNoRetiredWordInRunningProse(t *testing.T) {
|
||||
retired, identifiers, _ := parseGlossary(readFile(rootFS(), glossaryFile))
|
||||
expectNone(t, checkRetired(rootFS(), retired, identifiers, today()))
|
||||
}
|
||||
|
||||
func TestWordsNoDaemonLineWritten(t *testing.T) { expectNone(t, checkLines(rootFS())) }
|
||||
|
||||
func TestWordsCatalogueCopyAgrees(t *testing.T) {
|
||||
catalogue := os.Getenv("MESH_CATALOG_DIR")
|
||||
if catalogue == "" {
|
||||
t.Skip("NOT COMPARED: MESH_CATALOG_DIR is not set, so the catalog's copy of the list was not read")
|
||||
}
|
||||
retired, _, _ := parseGlossary(readFile(rootFS(), glossaryFile))
|
||||
expectNone(t, checkCopy(catalogue, retired))
|
||||
}
|
||||
|
||||
// The checks themselves, against broken trees (H5a: the glossary).
|
||||
|
||||
const glossaryFixture = `# Glossary
|
||||
|
||||
- **control plane** — the retired one's replacement is below.
|
||||
*Not:* ~~control plane~~, ~~overlay~~ (hq), ~~alert~~ (tools), ~~a / b~~
|
||||
- **controller** — decides.
|
||||
*Not:* ~~control plane~~
|
||||
- **Controller** — again.
|
||||
- *Identifier until renamed:* ` + "`mesh-host`, `nox-old`" + ` — the repository
|
||||
`
|
||||
|
||||
func TestGlossaryReadsNotLinesScopesIdentifiersAndHeads(t *testing.T) {
|
||||
retired, identifiers, heads := parseGlossary(glossaryFixture)
|
||||
want := []retiredWord{{"control plane", "all"}, {"overlay", "hq"}, {"alert", "tools"}, {"a", "all"}, {"b", "all"}, {"control plane", "all"}}
|
||||
if fmt.Sprint(retired) != fmt.Sprint(want) {
|
||||
t.Errorf("retired = %v, want %v", retired, want)
|
||||
}
|
||||
if fmt.Sprint(identifiers) != "[mesh-host nox-old]" {
|
||||
t.Errorf("identifiers = %v", identifiers)
|
||||
}
|
||||
if fmt.Sprint(heads) != "[control plane controller Controller]" {
|
||||
t.Errorf("heads = %v", heads)
|
||||
}
|
||||
got := checkUnique(retired, heads)
|
||||
expectProblem(t, got, `"Controller" heads two entries`)
|
||||
expectProblem(t, got, `"control plane" is a head word and also retired`)
|
||||
}
|
||||
|
||||
func TestToolsListHoldsTheWordsOfScopeAllAndTools(t *testing.T) {
|
||||
retired, _, _ := parseGlossary(glossaryFixture)
|
||||
if got := fmt.Sprint(toolsList(retired)); got != "[a alert b control plane]" {
|
||||
t.Errorf("toolsList = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// H5b: prose blanking and matching.
|
||||
|
||||
func TestProseBlanksWhatIsNotRunningProse(t *testing.T) {
|
||||
text := "---\nfm: overlay\n---\n" +
|
||||
"bare overlay one\n" +
|
||||
"```\noverlay in a block\n```\n" +
|
||||
"span `overlay` x\n" +
|
||||
"> quoted overlay\n" +
|
||||
"say \"overlay\" and “overlay” and ‘overlay’\n" +
|
||||
"~~overlay~~ struck\n" +
|
||||
"[link](overlay.md)\n" +
|
||||
"<!-- overlay\nover lines -->\n" +
|
||||
"bare overlay two\n"
|
||||
got := prose(text, false)
|
||||
if strings.Count(got, "overlay") != 2 {
|
||||
t.Errorf("only the two bare uses stay: %q", got)
|
||||
}
|
||||
if strings.Count(got, "\n") != strings.Count(text, "\n") || len(got) != len(text) {
|
||||
t.Error("blanking keeps offsets and line numbers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProseOfTheGlossaryBlanksNotAndIdentifierLines(t *testing.T) {
|
||||
got := prose("- **x** — y\n *Not:* control plane, plain\n- *Identifier until renamed:* mesh-host\nthe control plane\n", true)
|
||||
if strings.Count(got, "control plane") != 1 || strings.Contains(got, "mesh-host") {
|
||||
t.Errorf("only the running use stays: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordMatchesWholeWordsPluralsAndLineBreaks(t *testing.T) {
|
||||
rx := wordRegexp("control plane")
|
||||
count := func(s string) int { return len(findBounded(rx, s)) }
|
||||
for s, want := range map[string]int{
|
||||
"the control plane runs": 1,
|
||||
"Control Planes": 1,
|
||||
"control-plane and control\nplane": 2,
|
||||
"uncontrol plane": 0,
|
||||
"control planet": 0,
|
||||
"control plane-x": 0,
|
||||
"x-control plane": 0,
|
||||
"control planexs": 0,
|
||||
} {
|
||||
if got := count(s); got != want {
|
||||
t.Errorf("%q: %d matches, want %d", s, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetiredCatchesAWordInProseAndNamesDocumentAndLine(t *testing.T) {
|
||||
retired := []retiredWord{{"control plane", "all"}, {"overlay", "hq"}, {"alert", "tools"}}
|
||||
broken := mapOf(map[string]string{
|
||||
"AGENTS.md": "fine\nthe control plane here\n",
|
||||
"03-DESIGN/01-to-be/a.md": "an overlay and \"a control plane quoted\" and `overlay` alert\n",
|
||||
"README.md": "mesh-host runs; `mesh-host` too\n",
|
||||
})
|
||||
got := checkRetired(broken, retired, []string{"mesh-host"}, "2026-10-23")
|
||||
expectProblem(t, got, `AGENTS.md:2: "control plane" is retired`)
|
||||
expectProblem(t, got, `03-DESIGN/01-to-be/a.md:1: "overlay" is retired`)
|
||||
expectProblem(t, got, `README.md:1: "mesh-host" is an identifier, allowed only in a code span`)
|
||||
if len(got) != 3 {
|
||||
t.Errorf("a quotation, a code span and a word of scope tools are not problems: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuesAreHeldToTheRuleFromItsDay(t *testing.T) {
|
||||
retired := []retiredWord{{"overlay", "all"}}
|
||||
fsys := mapOf(map[string]string{
|
||||
"04-ISSUES/1-old/00-report.md": "---\nopened: 2026-10-06\n---\noverlay\n",
|
||||
"04-ISSUES/2-new/00-report.md": "---\nopened: 2026-10-07\n---\nfine\n",
|
||||
"04-ISSUES/2-new/01-notes.md": "an overlay\n",
|
||||
"04-ISSUES/3-nodate/00-report.md": "overlay\n",
|
||||
})
|
||||
got := checkRetired(fsys, retired, nil, "2026-10-23")
|
||||
expectProblem(t, got, "04-ISSUES/2-new/01-notes.md:1")
|
||||
if len(got) != 1 {
|
||||
t.Errorf("only the issue opened from 2026-10-07 on is read: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// H5c: allowances, with an injected clock.
|
||||
|
||||
const allowedFixture = `# Words allowed
|
||||
|
||||
| Document | Until | Why |
|
||||
|---|---|---|
|
||||
| ` + "`a.md`" + ` | 2026-12-01 | quotes what was |
|
||||
| ` + "`b.md`" + ` | 2026-10-01 | ran out |
|
||||
| ` + "`c.md`" + ` | soon | no date |
|
||||
| ` + "`d.md`" + ` | 2026-12-01 | |
|
||||
`
|
||||
|
||||
func TestAllowedReadsRowsAndTheInjectedClock(t *testing.T) {
|
||||
allowed, problems := parseAllowed(allowedFixture, "2026-10-23")
|
||||
if _, ok := allowed["a.md"]; !ok || len(allowed) != 2 {
|
||||
t.Errorf("a.md and d.md are in date: %v", allowed)
|
||||
}
|
||||
expectProblem(t, problems, "the allowance for b.md ran out on 2026-10-01")
|
||||
expectProblem(t, problems, "the allowance for c.md has no date")
|
||||
expectProblem(t, problems, "the allowance for d.md gives no reason")
|
||||
// The same text on an earlier day: b.md is still in date.
|
||||
if early, _ := parseAllowed(allowedFixture, "2026-09-30"); len(early) != 3 {
|
||||
t.Errorf("on 2026-09-30 a, b and d are in date: %v", early)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowanceHoldsAWordBackUntilItsDateAndShrinks(t *testing.T) {
|
||||
retired := []retiredWord{{"overlay", "all"}}
|
||||
allowedRow := "| `AGENTS.md` | 2026-12-01 | quotes what was |\n"
|
||||
tree := func(agents, allowed string) map[string]string {
|
||||
return map[string]string{"AGENTS.md": agents, allowedFile: allowed}
|
||||
}
|
||||
expectNone(t, checkRetired(mapOf(tree("an overlay\n", allowedRow)), retired, nil, "2026-10-23"))
|
||||
expectProblem(t, checkRetired(mapOf(tree("an overlay\n", allowedRow)), retired, nil, "2026-12-02"), "ran out on 2026-12-01")
|
||||
expectProblem(t, checkRetired(mapOf(tree("clean\n", allowedRow)), retired, nil, "2026-10-23"), "AGENTS.md no longer uses a retired word")
|
||||
expectProblem(t, checkRetired(mapOf(tree("an overlay\n", "")), retired, nil, "2026-10-23"), `AGENTS.md:1: "overlay" is retired`)
|
||||
}
|
||||
|
||||
// H5d: the daemons' lines and the catalog comparison.
|
||||
|
||||
func TestLinesCatchesADaemonsLineButNotProse(t *testing.T) {
|
||||
fsys := mapOf(map[string]string{
|
||||
"AGENTS.md": "the mesh-controller module is prose\n" +
|
||||
"run `mesh-controller module check x` now\n" +
|
||||
"or:\n```\n$ mesh-controller push\n```\n" +
|
||||
"a quotation \"mesh-controller status\" stays\n" +
|
||||
"> mesh-controller doctor in a block quote\n" +
|
||||
"a mesh-controller pushed is another word\n",
|
||||
"04-ISSUES/1/00-report.md": "---\nopened: 2026-10-08\n---\nmesh-host apply\n",
|
||||
"04-ISSUES/2/00-report.md": "---\nopened: 2026-10-09\n---\nmesh-host apply\n",
|
||||
})
|
||||
got := checkLines(fsys)
|
||||
expectProblem(t, got, "AGENTS.md:2: `mesh-controller module` is a daemon's line")
|
||||
expectProblem(t, got, "AGENTS.md:5: `mesh-controller push` is a daemon's line")
|
||||
expectProblem(t, got, "04-ISSUES/2/00-report.md:4: `mesh-host apply`")
|
||||
if len(got) != 3 {
|
||||
t.Errorf("prose, quotations, block quotes and issues before 2026-10-09 are not problems: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinesListNamesEachLineOnce(t *testing.T) {
|
||||
list := linesList()
|
||||
seen := map[string]bool{}
|
||||
for _, l := range list {
|
||||
if seen[l] {
|
||||
t.Errorf("%q listed twice", l)
|
||||
}
|
||||
seen[l] = true
|
||||
}
|
||||
if !seen["mesh-controller push"] || !seen["mesh-controller module …"] || !seen["nox-mesh-host hand-over"] {
|
||||
t.Errorf("the list lacks a known line: %v", list[:5])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogueCopyDifferenceIsCaughtAndNamesTheList(t *testing.T) {
|
||||
retired := []retiredWord{{"overlay", "all"}, {"alert", "tools"}, {"hq only", "hq"}}
|
||||
dir := t.TempDir()
|
||||
write := func(name, content string) {
|
||||
if err := os.WriteFile(path.Join(dir, name), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write("retired-words", "# copy\nalert\noverlay\n")
|
||||
write("retired-lines", strings.Join(linesList(), "\n")+"\n")
|
||||
expectNone(t, checkCopy(dir, retired))
|
||||
|
||||
write("retired-words", "alert\n")
|
||||
write("retired-lines", "mesh-controller push\n")
|
||||
got := checkCopy(dir, retired)
|
||||
expectProblem(t, got, "retired-words differs from the glossary: it should list alert, overlay")
|
||||
expectProblem(t, got, "retired-lines differs from this check's list")
|
||||
expectProblem(t, checkCopy(t.TempDir(), retired), "cannot be read")
|
||||
}
|
||||
|
||||
func mapOf(files map[string]string) fs.FS {
|
||||
m := fstest.MapFS{}
|
||||
for k, v := range files {
|
||||
m[k] = doc(v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// holders: the glossary defines a word and never says which module holds or claims a seat. Who holds a seat is a fact
|
||||
// of the mesh's assignments, read with one call (the operator, 2026-11-09: "it shouldn't be in the glossary"), and a
|
||||
// copy here drifts. Refused, across line breaks: a seat "held by" or "claimed by" anything but a generic holder or a
|
||||
// seat ("held once for the mesh by `x`", "held today by the forge module"); a named module "holder of" a seat ("the
|
||||
// module `x`, holder of"); a module that "holds the seat" ("it holds the seat", "the `x` module holds it"); and "its
|
||||
// holder, the x". A word for the role ("the module that holds the seat `x`") names no module and passes.
|
||||
|
||||
var (
|
||||
heldByRx = regexp.MustCompile("(?i)\\b(held|claimed)(\\s+once)?(\\s+for\\s+the\\s+mesh)?(\\s+today)?\\s+by\\s+")
|
||||
// What may follow "held by": a holder in general, a seat, or the knowledge base (a seat's word) holding a record.
|
||||
heldByGenericRx = regexp.MustCompile("^(a\\s+module\\b|some\\s+module\\b|its\\s+holder\\b|the\\s+holder\\b|the\\s+identity\\s+seat\\b|the\\s+knowledge\\s+base\\b|one\\s+author\\b|the\\s+`[^`]+`\\s+seat\\b|the\\s+(node\\s+)?seat\\s+`)")
|
||||
holderNamedRx = []*regexp.Regexp{
|
||||
regexp.MustCompile("(?i)(\\bmodule\\s+`?[a-z][a-z0-9-]*`?|`[a-z][a-z0-9-]*`)\\s*,\\s+(the\\s+)?holder\\s+of\\b"),
|
||||
regexp.MustCompile("(?i)\\b(it|which)\\s+holds\\s+(the\\s+)?(node\\s+)?seats?\\b"),
|
||||
regexp.MustCompile("(?i)\\bmodule\\s+holds\\s+(it|the\\s+(node\\s+)?seat)\\b"),
|
||||
regexp.MustCompile("(?i)\\bholder\\s*,\\s+the\\s+[a-z`]"),
|
||||
}
|
||||
)
|
||||
|
||||
func checkHolders(text string) []string {
|
||||
var out []string
|
||||
say := func(i int, m string) {
|
||||
out = append(out, fmt.Sprintf("%s:%d: %q names a seat's holder; the glossary says where it is read instead", glossaryFile, lineOf(text, i), strings.Join(strings.Fields(m), " ")))
|
||||
}
|
||||
for _, loc := range heldByRx.FindAllStringIndex(text, -1) {
|
||||
rest := text[loc[1]:]
|
||||
if heldByGenericRx.MatchString(rest) {
|
||||
continue
|
||||
}
|
||||
end := loc[1] + min(len(rest), 30)
|
||||
say(loc[0], text[loc[0]:end])
|
||||
}
|
||||
for _, rx := range holderNamedRx {
|
||||
for _, loc := range rx.FindAllStringIndex(text, -1) {
|
||||
say(loc[0], text[loc[0]:loc[1]])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestWordsGlossaryNamesNoSeatHolder(t *testing.T) {
|
||||
expectNone(t, checkHolders(readFile(rootFS(), glossaryFile)))
|
||||
}
|
||||
|
||||
func TestHoldersRefusesANamedHolderAndPassesARole(t *testing.T) {
|
||||
for _, bad := range []string{
|
||||
"- **x** — the seat `x`, held once for the mesh by the module `mesh-x`.",
|
||||
"- **x** — the seat `x`, held\n by `mesh-x`.",
|
||||
"- **x** — `go-package-registry` (held today by the forge module).",
|
||||
"- **x** — the seat `x`, claimed by mesh-x.",
|
||||
"- **x** — the module mesh-x, holder of the seat `x`.",
|
||||
"- **x** — the module `mesh-x`, holder of `x`.",
|
||||
"- **mesh-x** — the module that does x: it holds the seat `mesh-x` once.",
|
||||
"- **bus** — The `nats` module holds it.",
|
||||
"- **x** — written by its holder, the assigner.",
|
||||
} {
|
||||
if len(checkHolders(bad)) == 0 {
|
||||
t.Errorf("not refused: %q", bad)
|
||||
}
|
||||
}
|
||||
for _, good := range []string{
|
||||
"- **seat** — a named role, held by a module assignment.",
|
||||
"- **router** — the module that holds the `operator-channel` seat.",
|
||||
"- **bus** — Held by the `mesh-broker` seat.",
|
||||
"- **x** — the seat `x`; its holder is read with `mesh-module-assigner.assignments`.",
|
||||
"- **depends on a seat** — a seat held on its node by some module.",
|
||||
} {
|
||||
if got := checkHolders(good); len(got) != 0 {
|
||||
t.Errorf("refused a role: %v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// domains: a retired domain is no heading of the glossary. ADR 0641 §1 retired Placement into Core (module
|
||||
// assignment) and Delivery (the wants), and §5 has this test refuse it as a domain heading.
|
||||
|
||||
var retiredDomains = []string{"Placement"}
|
||||
|
||||
func checkRetiredDomains(text string) []string {
|
||||
var out []string
|
||||
for _, d := range retiredDomains {
|
||||
rx := regexp.MustCompile(`(?m)^##\s+` + regexp.QuoteMeta(d) + `\b`)
|
||||
for _, loc := range rx.FindAllStringIndex(text, -1) {
|
||||
out = append(out, fmt.Sprintf("%s:%d: %q is a retired domain (ADR 0641 §1) and heads no section", glossaryFile, lineOf(text, loc[0]), d))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestWordsGlossaryHasNoRetiredDomainHeading(t *testing.T) {
|
||||
expectNone(t, checkRetiredDomains(readFile(rootFS(), glossaryFile)))
|
||||
}
|
||||
|
||||
func TestRetiredDomainsRefusesTheHeadingAndPassesProse(t *testing.T) {
|
||||
if len(checkRetiredDomains("## Placement — what runs on which node\n")) == 0 {
|
||||
t.Error("a Placement heading was not refused")
|
||||
}
|
||||
if got := checkRetiredDomains("## Core\n\nwhich was the Placement domain until ADR 0641\n"); len(got) != 0 {
|
||||
t.Errorf("prose refused: %v", got)
|
||||
}
|
||||
}
|
||||
+819
-162
File diff suppressed because it is too large
Load Diff
+18
-2
@@ -173,6 +173,22 @@ quietly stop being true, and nobody will learn that from a document.
|
||||
Not test-driven development as a blanket rule — a test written first against undiscovered
|
||||
behaviour asserts a guess. The obligation is that every decision has a defender.
|
||||
|
||||
### A bus permission is proven under the real permission
|
||||
|
||||
A change that adds or widens what a mesh process reads or writes on the bus (a subscription, a publish, a stream or
|
||||
key-value read, a consumer) carries an integration test that runs it on a bus user with exactly the production
|
||||
permissions of that account (mesh-sdk's composition of them), against a real bus server, and that fails with the
|
||||
bus's permissions violation when the widest new access is not granted. It runs in the forge's check like every
|
||||
integration test. A reviewer reading code does not see a grant, and a fake bus grants everything (ADR 0560).
|
||||
|
||||
How it is checked: a review that approves such a change whose pull request names no such test is the rule skipped.
|
||||
|
||||
### A bus permission under the real permission — how it was earned
|
||||
|
||||
On 2026-10-27 a change widened what every node agent watches; two reviews read its code and its tests ran against a
|
||||
fake bus. Live, every node's agent was refused that read by its grant and stopped composing, and the same day the
|
||||
grants part's user list dropped nine seats' subscriptions (issues 1126 and 1945).
|
||||
|
||||
### Tests run at the lowest level that proves them, within a time budget
|
||||
|
||||
Three levels, by how much real system a test uses (ADR 0391):
|
||||
@@ -180,7 +196,7 @@ Three levels, by how much real system a test uses (ADR 0391):
|
||||
| Level | Uses | Budget | Runs |
|
||||
|---|---|---|---|
|
||||
| **Unit** | one piece of logic alone: no database, network, bus, running service or other process | well under a second per test | locally, the tests of what changed, while working; and in the forge's checks |
|
||||
| **Integration** | the real thing beside it: a database, the bus, a catalogue tree | the repository's whole suite in seconds, never over sixty | only in the forge's checks, once per push |
|
||||
| **Integration** | the real thing beside it: a database, the bus, a catalog tree | the repository's whole suite in seconds, never over sixty | only in the forge's checks, once per push |
|
||||
| **End to end** | the whole mesh, or a lab mesh, from the outside | — | only when the operator asks |
|
||||
|
||||
- **Test each behaviour at the lowest level that can prove it.** A defect found higher up gets a test at the
|
||||
@@ -193,7 +209,7 @@ Three levels, by how much real system a test uses (ADR 0391):
|
||||
|
||||
### Tests within a time budget — how it was earned
|
||||
|
||||
On 2026-10-16 a feature's fixing agents ran the catalogue's whole merge check (about ten minutes), a
|
||||
On 2026-10-16 a feature's fixing agents ran the catalog's whole merge check (about ten minutes), a
|
||||
throwaway database and a locally built gate during local development. Each round cost tens of minutes and
|
||||
caught neither real failure; the forge's checks found both.
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
status: canonical
|
||||
updated: 2026-11-11
|
||||
---
|
||||
|
||||
# The principles
|
||||
|
||||
A short, living list. Every question met while building is tested against these principles and answered by the
|
||||
lead in one line; only a question no principle settles goes to the operator or to a decision record. A detail that
|
||||
a principle settles is not written down as a design line or a record. The list changes when the operator says so:
|
||||
add, sharpen or drop a principle in a pull request to this file.
|
||||
|
||||
1. **NATS-native.** Every interaction between modules is an event, or a call over the bus where an answer is
|
||||
needed. A module keeps its data in the bus's own storage, and reads another module's data only by asking it,
|
||||
through its verbs or its tools.
|
||||
2. **The mesh owns the truth.** State lives in the mesh and is written onto the nodes from there. Nothing is changed
|
||||
by hand on a node.
|
||||
3. **Failure is loud.** Nothing reports success when it did nothing, and nothing fails silently. When in doubt, fail.
|
||||
4. **A module is steered by its tools.** Everything a module holds can be read, everything it does can be started
|
||||
by hand, and everything that goes wrong can be recovered, all through its tools.
|
||||
5. **Everything ships the same way.** The mesh's own parts go through the same build, check and delivery as any app.
|
||||
6. **Nothing is exposed publicly.** No data leaves the mesh unasked: no secret in a record, a log or a public
|
||||
repository, no service reachable from outside unless it is meant to be. Inside the mesh, every agent and module
|
||||
is trusted; who may do what is not a question to ask.
|
||||
7. **The simplest thing that works.** Prefer what already exists. No new mechanism unless it is needed, and fewer
|
||||
parts over more.
|
||||
8. **One word per thing**, from the [glossary](glossary.md).
|
||||
|
||||
How it is checked: the one code review of a milestone checks the code against these principles and the design's
|
||||
shape; an answer to a develop question names the principle that settles it.
|
||||
@@ -46,8 +46,23 @@ to-be document's frontmatter goes to `implemented`, and both stand — one descr
|
||||
the other recording what was intended. Deleting the intention loses the reasoning, which is
|
||||
the expensive half.
|
||||
|
||||
## A design is built as it is written
|
||||
|
||||
Research, decisions and the review by architectural domain are where the mesh's choices are made; building makes
|
||||
none (the operator, 2026-11-10). A design is written to be built without a choice of the builder's own, in a main
|
||||
file and one part per milestone that an agent reads whole (playbook [02](02-graduation.md), *The shape of a
|
||||
design*). The prepare, develop and the code review of playbook [07](07-feature-branches.md) read only that design:
|
||||
the prepare says where each design line lands in code and lists what the design does not answer, the operator
|
||||
answers each such design question and the answer is written into the design, and only then does develop start. Every
|
||||
commit cites the design lines it builds, and the review judges each change against its line. How it is checked:
|
||||
playbook 07's *How it is checked* (the build maps, the commits' `Builds:` lines, the design questions' answering hq
|
||||
commits).
|
||||
|
||||
## The playbooks
|
||||
|
||||
A drawn overview of them, the cycle, a feature's steps, the review loop and an issue's life, is
|
||||
[`playbooks.html`](playbooks.html): a picture of these playbooks, never their authority.
|
||||
|
||||
| # | Playbook | Trigger |
|
||||
|---|---|---|
|
||||
| [01](01-research.md) | Research | An idea from the operator, or one worth investigating before committing to design: setup questions, findings, a direction the operator confirms |
|
||||
@@ -56,18 +71,19 @@ the expensive half.
|
||||
| [04](04-build-handoff.md) | Work breakdown and handoff | A design is ready to be built: milestones usable on their own, issues per repository, each milestone one feature step |
|
||||
| [05](05-constitution-sync.md) | Constitution sync | `how-we-build.md` changed a rule the mesh enforces |
|
||||
| [06](06-writing-a-module.md) | Writing a module | Something that runs today must run on the mesh |
|
||||
| [07](07-feature-branches.md) | Features | Work that changes code across issues or repositories: prepared context, local development, one review, one check, one delivery |
|
||||
| [08](08-small-fixes.md) | Small fixes | One issue in one repository, about an hour of work: one review, one push, one delivery |
|
||||
| [07](07-feature-branches.md) | Features | Work that changes code across issues or repositories: a build map from the milestone's part of its design, the design complete before any code, local development, one review against the design lines, one check, one delivery |
|
||||
| [08](08-small-fixes.md) | Small fixes | One issue in one repository, about an hour of work: one review, one check, one delivery |
|
||||
| [09](09-cleanup.md) | Cleanup | Merged branches, stale pull requests, finished worktrees and folders: read first, delete only what the operator approves, report the rest |
|
||||
| [10](10-visual-design.md) | Visual design | A page the operator will use is about to be designed or rebuilt: mockups with fixed data, the operator picks, the pick is the reference |
|
||||
|
||||
## The cycle is checked
|
||||
|
||||
The flow above is a rule, and a rule states how it is checked:
|
||||
[`00-META/checks/cycle.py`](../checks/cycle.py) refuses a to-be design that names no
|
||||
the cycle test in [`00-META/checks/`](../checks/) refuses a to-be design that names no
|
||||
decision, an `in-progress`/`implemented` design that names no owning code, two issue folders
|
||||
sharing a number or an issue report carrying a status key; a `graduated` research effort that
|
||||
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with `records.py`, `index.py` and
|
||||
`words.py` before any HQ merge (`sh merge-check.sh` runs all four). The issue rules the check
|
||||
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with the records, index and
|
||||
words tests before any HQ merge: `go test ./...` at the root runs all four, and the checker runs the same as `mesh/check` (ADR 0527). The issue rules the check
|
||||
once held — `located` with an owner, `resolved` with a fix, a core issue with its replay — are
|
||||
refused at the write by the issue tracker's `move`
|
||||
(ADR 0275). What the checks cannot see — that code work actually started from a
|
||||
|
||||
@@ -39,20 +39,33 @@ An idea arrives in one of two forms, and the first step differs:
|
||||
- **Explain each question with a concrete picture** in the operator's words: what they would see, do or
|
||||
lose under each option. No internal names (a record number, a verb, a word the glossary lacks) without
|
||||
what it is beside it.
|
||||
- **Write the requirements from the answers, before the options.** Each one line, numbered, saying what
|
||||
the result must hold and how it is checked (for example "R5. A change across repositories is
|
||||
delivered whole or not at all; check: a change over three repositories"). The requirements shape the
|
||||
effort: every option is weighed against them, and the operator corrects them before any option is
|
||||
worked out (the operator, 2026-11-09: "coming up with a list of requirements can also be an important
|
||||
part, it actually shapes the research").
|
||||
3. **Take a number.** `knowledge-base.reserve` with `kind: research` and `for`: the effort's title. The number
|
||||
is spent whether or not the effort is ever proposed; one never proposed is withdrawn after a week. Never
|
||||
pick one by hand.
|
||||
4. **Propose it.** `knowledge-base.propose` with `kind: research`, the `number`, a one-line `title`,
|
||||
`status: active`, `date` (the day it began), and `sections`, each a `name` and its markdown `text`:
|
||||
- `00-overview.md`: the idea in the operator's words, what exists, the operator's answers (dated), and the
|
||||
`status: active`, `date` (the day it began), and `sections`, each a `name` and its markdown `text`. A
|
||||
section is a part of the record, never a file: its `name` is a plain title, a reference to another section
|
||||
names that title ("the Direction's test 8"), and code evidence names the repository, the commit and the
|
||||
function, never a path or a line (the operator, 2026-11-02: "don't use file references or local files
|
||||
anymore"; the knowledge base is to be shown as a web page, where a file name links to nothing). Once
|
||||
proposed, the record is the only copy: it is changed by `knowledge-base.get` and `propose`, never kept
|
||||
in a local draft. The first sections:
|
||||
- **Overview**: the idea in the operator's words, what exists, the operator's answers (dated), and the
|
||||
words the effort uses that the glossary lacks, each defined in one line;
|
||||
- `01-questions.md`: the questions to answer, the first ones first;
|
||||
- `02-method.md`: what will be tried and measured, and on what.
|
||||
- **Requirements**: the numbered requirements of step 2, each with its check;
|
||||
- **Questions**: the questions to answer, the first ones first;
|
||||
- **Method**: what will be tried and measured, and on what.
|
||||
|
||||
Name what it touches as `links` of kind `touches` (`"0276"`, `"research 045"`, `"issue 365"`, or a
|
||||
design's path), and what it names for another reason as `cites`.
|
||||
5. **Do the research.** Try, measure and read, smallest question first. Each finding goes into
|
||||
`04-findings.md`: what was tried, what it showed (with numbers), what it means. Each change of the effort
|
||||
the section **Findings**: what was tried, what it showed (with numbers), what it means. Each change of the effort
|
||||
is `knowledge-base.propose` again, whole, with `why`; every version is kept, and `knowledge-base.history`
|
||||
lists them and gives the difference between two. Until issue 844 is delivered, the knowledge base
|
||||
takes a new version of an accepted, active effort only as a progressive insight: give `insight`, saying
|
||||
@@ -69,20 +82,52 @@ An idea arrives in one of two forms, and the first step differs:
|
||||
server, a protocol) is tried in process, on the machine's loopback, in minutes. Lab runs (about 12 to
|
||||
16 minutes fresh) are for the mesh's own code; before that code exists, a lab run tests nothing an
|
||||
in-process test does not, and its lab scenario is written as the build's acceptance test instead.
|
||||
6. **Place it in the domains.** Before the direction, write `03-domains.md`: the domain or domains the idea
|
||||
6. **Place it in the domains.** Before the direction, write the section **Domains**: the domain or domains the idea
|
||||
belongs to (the glossary's sections, ADR 0244); the modules and seats it lands in, each new, changed or
|
||||
retired; what it does to each domain's words, each word added (with its domain and a one-line meaning),
|
||||
changed or retired, and none on a *Not:* line or already meaning something else (the glossary's homonyms);
|
||||
and whether a domain grows or shrinks, a concept moving between domains included. The direction names its
|
||||
decisions in those words. Asked when the idea is set up, too: an option that lands a concept in the wrong
|
||||
domain is offered with that cost named, like one that breaks a rule.
|
||||
7. **Settle the direction with the operator.** When the questions are answered, write `05-direction.md`:
|
||||
the proposed decisions in a few lines each, the options rejected and why, and the build's acceptance
|
||||
tests: what the build must prove before it is done, carried by its milestones (playbook
|
||||
7. **Settle the direction with the operator.** When the questions are answered, write the section **Direction**:
|
||||
the question the effort answers, the options weighed and the one proposed (a second only for a second
|
||||
question of its own; playbook [02](02-graduation.md), step 2), the design choices that follow from it in a
|
||||
line each, and the build's acceptance tests: what the build must prove before it is done, carried by its milestones (playbook
|
||||
[04](04-build-handoff.md)). Nothing else is left open: each question closes by a test, by a decision the
|
||||
effort records with its reason (a small design limit is the lead's), or by the operator. Ask the
|
||||
operator to confirm or change the direction, one point at a time, then propose it once. Then the effort
|
||||
graduates through playbook [02](02-graduation.md).
|
||||
operator to confirm or change the direction, one point at a time. Then review it by architectural domain (below) and
|
||||
propose it once. Then the effort graduates through playbook [02](02-graduation.md).
|
||||
|
||||
## Review by architectural domain
|
||||
|
||||
A direction the operator settled can still be wrong where nobody looked. Before it is proposed, and again
|
||||
before a design is merged (playbook [02](02-graduation.md)), it is reviewed against each **architectural domain**
|
||||
(the operator, 2026-11-02), by reviewers on the larger model, about two architectural domains each, side by side:
|
||||
the four reviewer agents in `.claude/agents/` (`review-security-and-upgrade`, `review-seams-and-tools`,
|
||||
`review-running-and-testing`, `review-health-and-events`), each given the subject by number or path:
|
||||
|
||||
- **security**: who may call each new verb, how its grants are scoped, which credentials go or remain, what
|
||||
can be replaced or forged, and what enters from outside the mesh;
|
||||
- **seams**: where each piece of state lives on the bus, which seat, verb or event joins two parts, and any
|
||||
link that is not NATS-native. Each piece of state is kept in the bus storage form that fits it (a key-value
|
||||
bucket, a stream, an object store); a form the manifest lacks is added to mesh-sdk, and the review names that change
|
||||
and its cost (a mesh-sdk change rebuilds every module) to the operator, never folds it in silently (the
|
||||
operator, 2026-11-02);
|
||||
- **running qualities**: build capacity, speed, storage growth, and what happens while a part, the bus or the
|
||||
control-node is down;
|
||||
- **health**: what each part's health check says, which conditions are raised and cleared, and whether any
|
||||
failure can pass silently;
|
||||
- **events and logs**: what each step says on the bus, and what can be read back later;
|
||||
- **testing**: an acceptance test for every decision, each one checkable and within the test budget, and the
|
||||
failure paths;
|
||||
- **upgrade**: how what runs today moves to the new shape without a delivery breaking, in which order, and
|
||||
how it is rolled back;
|
||||
- **tools**: which tool reads each new piece of state and which one steers each new behaviour.
|
||||
|
||||
A reviewer judges only whether the settled direction is complete and sound in its architectural domains; it never reopens
|
||||
a decision. Each finding is a sentence for the direction or a new acceptance test; a finding that changes a
|
||||
decision goes to the operator. The direction ends with a section *Reviewed by architectural domain*: per architectural domain, what was
|
||||
found and how it was answered, or that nothing was found.
|
||||
|
||||
## Bounds and models
|
||||
|
||||
@@ -90,9 +135,10 @@ An idea arrives in one of two forms, and the first step differs:
|
||||
|---|---|---|
|
||||
| reading and the setup questions (1, 2) | 30 minutes | the session's own, larger |
|
||||
| proposing (3, 4) | 15 minutes | larger |
|
||||
| each experiment or reading task (5) | 30 minutes | smaller, one agent at a time unless the operator allows more |
|
||||
| each experiment or reading task (5) | 30 minutes | smaller, one agent at a time unless the operator allows more: the agent `research-reader` |
|
||||
| placing it in the domains (6) | 15 minutes | larger |
|
||||
| the direction (7) | 30 minutes | larger |
|
||||
| the review by architectural domain | 20 minutes | larger, about two architectural domains per reviewer |
|
||||
|
||||
A step past its bound is said to the operator at once, with what is being done about it. The lead reports
|
||||
as playbook 07 does: every ten minutes and at once when something is found, stops or needs the operator,
|
||||
@@ -119,13 +165,14 @@ for the operator's Approve on a channel that proves who answered.
|
||||
- Do not put status in prose. It is the effort's `status`, and the prose must not restate it.
|
||||
- Do not give a `became` link while the effort is open — playbook 02 gives it at closure.
|
||||
- Do not write into `03-DESIGN` from an open effort.
|
||||
- Do not write an effort, or a copy of one, into this repository: `records.py` refuses a new path under
|
||||
- Do not write an effort, or a copy of one, into this repository: the records test refuses a new path under
|
||||
`01-RESEARCH/`.
|
||||
|
||||
## Closing
|
||||
|
||||
An effort never just stops. It closes through playbook [02](02-graduation.md) as `graduated` or
|
||||
`abandoned`, always with a `became` link to what it turned into. Nothing is deleted — what was rejected,
|
||||
`abandoned`: a graduated effort with a `became` link to what it turned into, an abandoned one with a `cites` link to
|
||||
what replaced it (playbook 02 step 5). Nothing is deleted — what was rejected,
|
||||
and why, is the more expensive half to rediscover.
|
||||
|
||||
## How it is checked
|
||||
@@ -134,7 +181,8 @@ The rules on an effort are the knowledge base's: `propose` refuses one that brea
|
||||
record (the module's table test holds one case per rule), and the rules that judge the whole record set run
|
||||
after every acceptance and raise a condition when one fails. The habits of steps 2, 5, 6 and 7 (options checked
|
||||
against the rules, one version per settled set, the cheapest level, nothing left open) are the lead's, read by
|
||||
the reviewer; of the pre-check, `propose` refuses a link to nothing and a retired word, and the review returns
|
||||
the reviewer; the review by architectural domain is read in the direction's *Reviewed by architectural domain* section (nothing refuses a direction
|
||||
without it yet: the lead and the reviewer hold it); of the pre-check, `propose` refuses a link to nothing and a retired word, and the review returns
|
||||
a record named but not linked, an untracked deferral and a figure not as measured. That this repository holds
|
||||
no effort is `records.py`'s `no-records` check, on every pull request. An active effort with no new version
|
||||
no effort is the records test's `no-records` check, on every pull request. An active effort with no new version
|
||||
for seven days is the smell of research left behind: its lead closes or continues it.
|
||||
|
||||
@@ -13,7 +13,7 @@ clauses in force for a topic or a record).
|
||||
|
||||
## Graduating research
|
||||
|
||||
The effort's `05-direction.md`, confirmed by the operator (playbook [01](01-research.md), step 7), is what
|
||||
The effort's Direction, confirmed by the operator (playbook [01](01-research.md), step 7), is what
|
||||
graduates. The lead writes the records and the design on the larger model; it is one sitting, bounded at
|
||||
about an hour, and said to the operator if it runs past.
|
||||
|
||||
@@ -21,8 +21,17 @@ about an hour, and said to the operator if it runs past.
|
||||
[`mission.md`](../mission.md), [`context.md`](../context.md) and
|
||||
[`effect.md`](../effect.md). If it conflicts, either the effort is wrong or GENESIS is —
|
||||
say which, in writing, before proceeding.
|
||||
2. **Record the decisions** (below, *Writing a decision record*): one record per choice that could be
|
||||
reversed on its own, not one record for the whole effort.
|
||||
2. **Record the decisions** (below, *Writing a decision record*): one record per question the effort
|
||||
answers, holding that question, the options weighed and the one chosen. Most efforts answer one question
|
||||
and graduate into one record; a second record is written only for a second question of its own, such as
|
||||
how the change can be checked or delivered at all (the operator, 2026-11-06: an effort graduated into
|
||||
eleven records where it answered two questions). The choices made while working out the design that
|
||||
follows (a field, a tool, an event, a bound, the order of steps) are the design's: written in the design
|
||||
with their reasons, under the record they follow from, never as records of their own. A clause of an
|
||||
earlier record the decision changes is a `changes` link from the record, with one line saying in what.
|
||||
How it is checked: the lead names, in the graduation's report to the operator, the question each record
|
||||
answers; an effort graduating into more than two records is said to the operator with why, before the
|
||||
records are proposed.
|
||||
- **Write them together, not one after another.** Reserve every number first, then propose each record
|
||||
as soon as it is written, without waiting for the review of the one before; the reviews run side by
|
||||
side, and the operator's asks for the records that need them arrive together. A record that
|
||||
@@ -49,21 +58,39 @@ about an hour, and said to the operator if it runs past.
|
||||
---
|
||||
```
|
||||
|
||||
The design lists the direction's acceptance tests: what the build proves before it is done.
|
||||
The design lists the direction's acceptance tests: what the build proves before it is done. Before it is
|
||||
merged, the design is reviewed by architectural domain as playbook [01](01-research.md#review-by-architectural-domain) says, and
|
||||
ends with its own *Reviewed by architectural domain* section; a design amended without an effort is reviewed the same way.
|
||||
|
||||
The words of the effort's `03-domains.md` go into the glossary in the same change, each in its domain: a
|
||||
The words of the effort's Domains go into the glossary in the same change, each in its domain: a
|
||||
word added gets its entry, a word whose meaning changed gets its entry rewritten, and a retired word moves to
|
||||
its replacement's *Not:* line. A word that already means something else in another domain is named as a
|
||||
homonym with the qualified form each domain uses.
|
||||
|
||||
4. **Hand it to the work breakdown.** The design goes on through playbook [04](04-build-handoff.md):
|
||||
milestones, issues, and each milestone built as one feature step (playbook [07](07-feature-branches.md)).
|
||||
5. **Close the effort.** `knowledge-base.propose` the effort again, whole, with `status: graduated` and a
|
||||
`became` link to the decision record (`"NNNN"`) and to the design document (its path). An effort that
|
||||
stops without a decision is proposed with `status: abandoned` and a `became` link to what replaced it.
|
||||
5. **Close the effort.** `knowledge-base.close` the effort with `to: graduated`, a `became` link to each
|
||||
decision record (`"NNNN"`) and the design document (its path), and a note of two or three sentences (what it
|
||||
became, what carries it). The verb keeps the served text unchanged and adds a section *Graduated* with the note;
|
||||
the knowledge base's review judges only the closing. Never close by proposing the effort again whole:
|
||||
that is judged as a fresh proposal, so the approved reasoning is re-judged against the decisions that replace it
|
||||
(on 2026-11-10 an effort closed that way met 5, then 11 findings on unchanged text; `close` was approved at once).
|
||||
An effort that stops without a decision is closed `to: abandoned` with a `cites` link to what replaced it: the
|
||||
knowledge base refuses `became` from a research effort to another research effort (its link-fits rule, design
|
||||
55 § 2), so a replacing effort is cited, and `became` stays for the decision and the design an effort turned
|
||||
into.
|
||||
|
||||
## Writing a decision record
|
||||
|
||||
**Before a number: what holds, and the mesh path, reviewed.** Ask the knowledge base what is already decided in
|
||||
the record's area (`knowledge-base.ask`, `holds`), and name in the draft, for every interaction between modules the
|
||||
record decides, the seat and verb, event, provision, state on the bus or manifest field that carries it. Then
|
||||
`review-seams-and-tools` reviews the draft before it is proposed: a clause that has one module name another, read its
|
||||
internals or be special-cased in another part, or puts a module's name into shared code, is a finding (the operator,
|
||||
2026-11-03).
|
||||
How it is checked: the record's References name the seams review's verdict; a record proposed without it is a breach
|
||||
the lead names.
|
||||
|
||||
1. **Take a number.** `knowledge-base.reserve` with `kind: decision` and `for`: the record's title. The
|
||||
number comes from the issue tracker's counter and is spent whether or not the record is accepted; one
|
||||
never proposed is withdrawn after a week. Never pick a number by hand.
|
||||
@@ -94,7 +121,10 @@ about an hour, and said to the operator if it runs past.
|
||||
deferral no issue tracks. The knowledge base then reviews the proposal itself, through a judgement that
|
||||
sees only the record, the records it links and the rules, and answers `approve` or `changes`. On
|
||||
`changes` the record stays proposed with the findings: revise it by proposing it again, whole, with `why`;
|
||||
at most three rounds, then the findings go to the operator with a recommendation.
|
||||
after three rounds the lead does not stop to ask, but fixes each further finding itself and proposes again
|
||||
(the operator, 2026-10-18: ADR 0429 went past three rounds, each finding narrower than the last, and asking
|
||||
first only added a wait). Only a finding that would change what was decided, rather than how the record
|
||||
says it, goes to the operator, with a recommendation.
|
||||
On `approve` it is accepted, or, when it says `needs-operator`, it waits for the operator's Approve on a
|
||||
channel that proves who answered. Nothing proposed is in force. `knowledge-base.get` shows a record's
|
||||
reviews and the operator's ask when it waits for one.
|
||||
@@ -120,13 +150,88 @@ by a progressive insight.
|
||||
record `changes` the older one's clause, so a reader who arrives at the older record by a citation sees
|
||||
what changed it since.
|
||||
|
||||
## The shape of a design
|
||||
|
||||
A design is what its builders build from, and nothing else is (playbook [07](07-feature-branches.md)): decision
|
||||
records and the mesh's principles are read when a design is written, amended or reviewed, and never again by the
|
||||
prepare, the developers or the code review. So a design is written to be built without a choice of the builder's
|
||||
own, and in pieces an agent can read whole. On 2026-11-10 one design of more than 400 KB was built in one
|
||||
milestone: its agents read it by searching for pieces, filled what they did not find themselves, and an audit of
|
||||
that day's 40 or so choices by the leads found seven against the records or the design and 29 questions the design
|
||||
never answered, all found only in develop.
|
||||
|
||||
- **A main file and one milestone part per milestone.** The design's main file
|
||||
(`03-DESIGN/01-to-be/NN-<slug>.md`) holds its frontmatter, its purpose, the whole shape (its parts, their seats,
|
||||
events, provisions and state, drawn and in short prose), its acceptance tests and its *Milestones* section. Each
|
||||
milestone not yet delivered has its **milestone part**, `03-DESIGN/01-to-be/NN-milestones/<milestone>.md`, which
|
||||
holds every line that milestone builds and opens with frontmatter naming `layer: to-be`, the design, the
|
||||
milestone and the `decisions:` its lines follow. A line lives in one place: a statement one milestone builds moves
|
||||
from the main file into that part, never copied.
|
||||
- **Bounds.** The main file at most 80 KB, a milestone part at most 60 KB. An agent reads both whole in under
|
||||
40,000 tokens, under a fifth of what it can hold, and has the rest for the code. A milestone whose part would
|
||||
pass 60 KB is two milestones (playbook [04](04-build-handoff.md)), each usable on its own.
|
||||
- **Numbered design lines.** A milestone part's statements are numbered, each one thing to build with a bold
|
||||
lead, like a decision's clauses, and cited as "design NN M §n"; a subtask, a commit and a review finding cite
|
||||
them so. Each line names what its builder needs: the seat, verb or event, the field and its form, the key and
|
||||
who deletes it, the bound, the caller, the tool that reads or steers it, and the fate of each thing that exists
|
||||
today and changes. A line a builder would have to complete is a design question left open.
|
||||
- **A delivered milestone's part stays.** It records what was intended, as a to-be design does once it ships
|
||||
(the overview, *The two design layers*); no agent reads it again, so it costs no builder anything, and what runs is
|
||||
written in the as-is design. The history of how a milestone was cut or why it waited is the issue tracker's and the
|
||||
records', not the design's.
|
||||
- **Design 47's next amendment is written in this shape**: the main file keeps the purpose, the whole shape, the
|
||||
acceptance tests and the milestones, within 80 KB; every milestone gets its part with every line it builds or
|
||||
built, moved from the main file, the one about to be built first; and the narrative of past milestones and their
|
||||
prepare steps leaves the design for the issues it names. Every other design is put in this shape
|
||||
at its next amendment, and a new design is written in it.
|
||||
|
||||
How it is checked: the reviewer of a design's pull request reads `wc -c` of the main file and of each milestone
|
||||
part against the bounds, and that every statement of a milestone part is numbered; a check among hq's Go tests is to
|
||||
be built (the cycle test reads only a design's main file today).
|
||||
|
||||
## Answering a design question
|
||||
|
||||
A design question (playbook [07](07-feature-branches.md), step 0) is answered by the operator and by nobody else:
|
||||
not by the lead, the preparer, a developer or a reviewer, however technical it is. The one exception is a question
|
||||
of internal form only (playbook 07, step 0, *Who answers a design question*; the operator, 2026-11-11): changing
|
||||
nothing the operator sees, nobody's permissions and no data kept or removed, it is answered by `hq-design-writer` as
|
||||
a design line of the milestone part with its reason, under the record it follows from, read in item 3's round, and
|
||||
told to the operator in one line. The lead asks it as the
|
||||
instruction on asking says, one question per ask, with what the milestone part says, what it leaves open, where it
|
||||
was met (file:line) and a recommendation. Then:
|
||||
|
||||
1. **Within a record's question** (a field, a bound, an owner, an order of steps, inside what a record decides), the
|
||||
answer is the design's own choice: `hq-design-writer` writes it as a design line of the milestone part, citing
|
||||
the operator's answer and its date and the record it follows from.
|
||||
2. **Outside every record's question**, or **against a record** (the design contradicts a decision in force), the
|
||||
answer is a decision: written first as *Writing a decision record* or *Changing a decision* above says, then as
|
||||
design lines.
|
||||
3. **The answers are reviewed together.** Before develop starts, the four reviewers by architectural domain read the
|
||||
milestone part as amended, in one round (playbook [01](01-research.md#review-by-architectural-domain)); answers
|
||||
met during develop are read by them in one round before the code review.
|
||||
4. **They land on the milestone's branch.** The amendment is committed on hq's branch of the milestone
|
||||
(`feat/<slug>`), so its pull request joins the milestone's delivery group, and the builders read the milestone
|
||||
part at that branch's head.
|
||||
|
||||
How it is checked: every design question in the milestone file names the hq commit that answers it; a design line
|
||||
added during a milestone with no operator's answer or record behind it, other than an answer of internal form only
|
||||
that touches nothing the operator sees, no permission and no data, is a finding of the review by architectural
|
||||
domain.
|
||||
|
||||
## Amending an existing design
|
||||
|
||||
A design changes only through a decision.
|
||||
A design changes only through a decision. A choice within what a decision decides (a field, a tool, an event,
|
||||
a bound) is the design's own, written in it with its reason; it needs no record of its own. It is made when the
|
||||
design is written or amended, never while it is built: a choice the design leaves to its builder is a design
|
||||
question (above).
|
||||
|
||||
1. Write the decision record (above). If it reverses an earlier one, it `supersedes` it.
|
||||
2. Edit the to-be design document, add the record to its `decisions:` as `"ADR NNNN"`, and set `updated:`
|
||||
to today.
|
||||
to today, keeping the design in the shape above (a line one milestone builds goes into that milestone's part).
|
||||
A set of amendments across several designs (a milestone's findings, a graduation's) is written by the
|
||||
agent `hq-design-writer` (`.claude/agents/`), started with a handover naming the designs, the records and the
|
||||
branch. Review it by architectural domain before it is merged (playbook [01](01-research.md#review-by-architectural-domain)), and
|
||||
rewrite its *Reviewed by architectural domain* section.
|
||||
3. If the amendment came from an issue, give the issue the document path in the issue tracker:
|
||||
`amended-design` on its `issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind
|
||||
`design` before then — never in the report's frontmatter
|
||||
@@ -150,7 +255,7 @@ Implementation state is a third axis, independent of both design and decision.
|
||||
- Do not edit an as-is document to describe an intention. That is what the to-be layer is for.
|
||||
- Do not change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while
|
||||
the decision stands, is a progressive insight — marked and dated in the record, never silent.
|
||||
- Do not write a decision record, or a copy of one, into this repository: `records.py` refuses a new path
|
||||
- Do not write a decision record, or a copy of one, into this repository: the records test refuses a new path
|
||||
under `02-DECISIONS/` other than its README.
|
||||
|
||||
## How it is checked
|
||||
@@ -159,8 +264,11 @@ The rules on a record are the knowledge base's: `propose` refuses one that break
|
||||
record (its module's table test holds one case per rule), and the four rules that judge the whole record set
|
||||
(an accepted decision cited by nothing, a supersession named by one side only, a changed clause nothing
|
||||
changes, an accepted record extending a superseded one) run after every acceptance and raise a condition
|
||||
when one fails. In this repository, `records.py` checks that a design's `decisions:` entries and every
|
||||
"ADR NNNN" in the text are well formed, and that no record is written here; `index.py` checks the reading
|
||||
order's six topics, which the knowledge base reads. The three habits of graduating step 2 (side by side, clauses
|
||||
when one fails. In this repository, the records test checks that a design's `decisions:` entries and every
|
||||
"ADR NNNN" in the text are well formed, and that no record is written here; the index test checks the reading
|
||||
order's six topics, which the knowledge base reads. A design's *Reviewed by architectural domain* section is read by the
|
||||
reviewer of its pull request; nothing refuses a design without it yet. The three habits of graduating step 2 (side by side, clauses
|
||||
from the direction, the pre-check) are the lead's, read by the reviewer; `propose` refuses a link to nothing,
|
||||
a deferral no issue tracks and a retired word, and the review returns a record named but not linked.
|
||||
The rounds are read in `knowledge-base.get` (its reviews, one per version): a question to the operator about a
|
||||
record's wording after its third round is the failure of step 4 of *Writing a decision record*.
|
||||
|
||||
@@ -66,7 +66,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
|
||||
```
|
||||
|
||||
Then `# NNN. The title`, the title as the tracker holds it, and the symptom as observed, as in the tracker's
|
||||
report. **No status key**: `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
|
||||
report. **No status key**: the cycle test refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
|
||||
`replay:` or `replay-none:` in any report, because the tracker holds them and two places holding one fact
|
||||
drift.
|
||||
|
||||
@@ -91,7 +91,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
|
||||
ADR 0281).
|
||||
4. Resolve. `issue-tracker.move` to `resolved`, which the tracker takes only from `fixing`, with `fixed-by`, and, if the root cause was a design gap,
|
||||
run playbook [02](02-graduation.md) and give `amended-design`. **A core issue** — its `located-in`
|
||||
names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build
|
||||
names the controller, the node-engine, the tool runner, the SDK, or the catalog's bus, forge or build
|
||||
agent — resolves with `replay`, the id of its replay in mesh-lab's replays register, proved to fail on
|
||||
the commit before the fix and pass on it, or with `replay-none` saying why none is possible
|
||||
(ADR 0237);
|
||||
@@ -107,7 +107,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
|
||||
it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it
|
||||
is must not have to already know there was a sequel.
|
||||
- A number never moves. A folder carries the number the tracker gave; a folder whose number collides with
|
||||
another (one numbered before the tracker, or by hand) is a fault, and `cycle.py` refuses two records
|
||||
another (one numbered before the tracker, or by hand) is a fault, and the cycle test refuses two records
|
||||
sharing a number.
|
||||
- An issue whose answer is a general lesson should also have that lesson written where a search by
|
||||
the symptom's literal words finds it, so the next person searching it finds it. Both, not either.
|
||||
@@ -117,6 +117,6 @@ import read, which every imported issue names (`imported.commit` in `issue-track
|
||||
- **A rule states how it is checked.** The cycle's rules on an issue — a status one of the six, `diagnosing` and
|
||||
`fixing` with a worker and no other current one, `located` with an owner, `resolved` only from `fixing` and with a
|
||||
fix, a core issue with its replay, `wontfix` with a reason — are refused at the write by
|
||||
`issue-tracker.move` (mesh-catalog `modules/mesh-issues`, its table test over the rules). `cycle.py` checks what
|
||||
`issue-tracker.move` (mesh-catalog `modules/mesh-issues`, its table test over the rules). the cycle test checks what
|
||||
this repository still holds: no two folders share a number, every report has frontmatter with the day it was
|
||||
`opened:`, and no report carries a status key.
|
||||
|
||||
@@ -9,11 +9,19 @@ an hour, and said to the operator if it runs past.
|
||||
|
||||
A design becomes **milestones**, and each milestone becomes **issues**:
|
||||
|
||||
- **A milestone is usable on its own.** Once its delivery plan is done, the operator or the mesh can do
|
||||
- **A milestone is usable on its own.** Once it is delivered, the operator or the mesh can do
|
||||
something new, said in one sentence ("a Claude Code session is mirrored into a thread on the bus").
|
||||
A milestone that changes nothing anyone can use is not a milestone; fold it into the next.
|
||||
- **A milestone is one feature step** of playbook [07](07-feature-branches.md): prepared, developed in one to
|
||||
two hours, reviewed, checked and delivered as one. A milestone larger than that is split.
|
||||
- **A milestone is one release** of playbook [07](07-feature-branches.md): prepared, developed whole, reviewed,
|
||||
checked and delivered as one, never cut for its size or for time (the operator, 2026-10-17).
|
||||
- **A milestone is one piece of functionality, never cut by an order between its parts** (the operator, 2026-10-21:
|
||||
a milestone may span several modules, repositories and dependencies such as an SDK change; 2026-10-22: "a milestone
|
||||
is a milestone, we shouldn't cut it"). Which of its modules reaches the machines first (a reader of a new manifest
|
||||
word before the modules that use it, a provider before its consumers) is the assigner's rules' (design 47); an SDK
|
||||
change is built into its users. The parts such an order touches are found in playbook [07](07-feature-branches.md)
|
||||
step 0 item 6 and become issues of the same milestone; an order those rules cannot keep is an issue of the build
|
||||
and delivery system, not a cut.
|
||||
Until 2026-10-22 this rule cut milestones by release order (design 47's release 1-2 became six milestones).
|
||||
- **An issue is one repository's part of a milestone,** sized so a fixing agent finishes it inside the
|
||||
milestone's development time. Issues that must land in order are linked `depends-on`.
|
||||
|
||||
@@ -30,8 +38,14 @@ A design becomes **milestones**, and each milestone becomes **issues**:
|
||||
4. **Break it into milestones.** Write them into the design, in a section *Milestones*: for each, its number,
|
||||
its one-sentence outcome, the acceptance tests of the design it proves, the repositories it touches, and
|
||||
what it depends on. Order them so each is usable as soon as it is delivered, the smallest useful one
|
||||
first. A milestone is named by what it lets someone do; how it ships is shaped by the plan its changes would
|
||||
have, asked through `mesh-delivery.what-if`: the modules it moves and their fan-out (ADR 0401 §6).
|
||||
first. A milestone is named by what it lets someone do; how it ships is shaped by the modules its changes move
|
||||
and the nodes that run them (ADR 0401 §6), read from the indexer's fingerprints through the status view's
|
||||
`indexer_modules` and `indexer_candidates`, and from the nodes' haves (`node_haves`).
|
||||
4a. **Give each milestone its part** (playbook [02](02-graduation.md), *The shape of a design*): every line the
|
||||
milestone builds moves from the main file into `NN-milestones/<milestone>.md`, as numbered design lines, each
|
||||
naming what its builder needs. A milestone whose part would pass 60 KB is split here into two milestones, each
|
||||
usable on its own; this is the one place a milestone is ever cut, for what one agent can read, before any work
|
||||
starts. The main file stays within 80 KB. Written by `hq-design-writer` in the same change as the milestones.
|
||||
5. **Ask the operator to confirm the milestones**, in a short table: number, outcome, the acceptance tests it
|
||||
proves, repositories, the parts each repository builds. Change what they change.
|
||||
6. **Open the issues.** For each confirmed milestone, `issue-tracker.open` one issue per repository part, its
|
||||
@@ -40,8 +54,12 @@ A design becomes **milestones**, and each milestone becomes **issues**:
|
||||
entry in the design.
|
||||
7. **Flip the status.** `status: in-progress`, `updated:` today; the design change goes through playbook
|
||||
[08](08-small-fixes.md) (one hq pull request).
|
||||
8. **Hand each milestone to playbook 07, one at a time.** The milestone is the feature step; its issues are the
|
||||
step's issues; the next milestone starts when the previous one's delivery plan is done.
|
||||
8. **Hand each milestone to playbook 07, one at a time.** The milestone is one release of playbook 07; its issues
|
||||
are the release's issues; the next milestone starts when the previous one is delivered. Playbook 07 builds from
|
||||
the milestone part alone and starts its develop step only once that part answers every design question its
|
||||
prepare finds (playbook 07, *Design complete*); a question is never answered by building around it. A design waits
|
||||
while another design's milestones are being built: one design is built at a time (playbook
|
||||
[07](07-feature-branches.md)), so this step starts only when the design before it is delivered.
|
||||
9. **On completion of the last milestone**, run the "when something ships" section of playbook
|
||||
[02](02-graduation.md).
|
||||
|
||||
@@ -52,16 +70,19 @@ A design becomes **milestones**, and each milestone becomes **issues**:
|
||||
owns what is not. The knowledge base's rule `deferral` checks the decision record when it is proposed
|
||||
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)).
|
||||
- HQ is not a code repository and never carries implementation.
|
||||
- How the building is done — worktrees, agents, review, checks, the ready mark and the delivery — is playbook
|
||||
- How the building is done — worktrees, agents, review, checks, the Approve and the delivery — is playbook
|
||||
07's, not this one's.
|
||||
|
||||
## How it is checked
|
||||
|
||||
Read by the lead and the reviewer today; a check in hq's merge-check is tracked by issue 845.
|
||||
Read by the lead and the reviewer today; a check among hq's Go tests is tracked by issue 845.
|
||||
|
||||
- A design in `status: in-progress` has a *Milestones* section, each milestone with its outcome and its
|
||||
issues; an in-progress design without one is the smell of building without a breakdown.
|
||||
- Each milestone of an in-progress design has its part under `NN-milestones/`, within 60 KB, its lines numbered
|
||||
(`ls -l 03-DESIGN/01-to-be/NN-milestones/`); a milestone handed to playbook 07 without one is step 4a skipped.
|
||||
- Every issue a milestone names exists in the tracker (`issue-tracker.get`), and an issue in `fixing` names
|
||||
its milestone in its report.
|
||||
- A milestone whose issues are all `resolved` has had its delivery plan done; its outcome sentence and
|
||||
- A milestone whose issues are all `resolved` is delivered (the status view said `delivered` for its source
|
||||
revision); its outcome sentence and
|
||||
acceptance tests are proven on the mesh before the next milestone starts.
|
||||
|
||||
@@ -83,6 +83,31 @@ Three questions, answered from the machine:
|
||||
thing works: the credential authenticates, a wrong one is refused, the containers reach each
|
||||
other, the data survives a restart.
|
||||
|
||||
## A module that calls or holds a seat, or shares code
|
||||
|
||||
*Added 2026-11-04, widened 2026-11-05* (ADR 0625, ADR 0627, ADR 0634, ADR 0635). Where each piece of shared code goes
|
||||
is [design 20](../../03-DESIGN/01-to-be/20-writing-a-module.md) *Where a seat's code goes*: shared code lives with its
|
||||
owner. In short:
|
||||
|
||||
- **Import the seat's contract and, where it has one, its client package**, from the seat's domain's contracts module
|
||||
(`mesh-<domain>-contracts`, or `mesh-contracts` for the Core domain and a domain with none), never the holder module
|
||||
and never a copy of the seat's types in your own code. The list of seats your module needs is read from the bus,
|
||||
never compiled in.
|
||||
- **Logic you would write over a seat that another caller would write too** belongs in that seat's client package,
|
||||
through a change of the contracts module; it never goes into mesh-sdk, and never into the holder.
|
||||
- **Logic and state layouts only your module owns**, where it holds no seat for them, are your module's own package;
|
||||
another repository's module imports it only by a published version, so your manifest declares it as a package
|
||||
artifact.
|
||||
- **Logic your domain's modules share, owned by no one module and carried by no seat**, is your domain's SDK's
|
||||
(`mesh-<domain>-sdk`); only what modules of more than one domain use goes into mesh-sdk.
|
||||
- **Holding a seat** means implementing its contract in your module. A seat you define yourself gets a contracts
|
||||
module of its own (it may share your repository); a role no other module could ever take over is your module's
|
||||
own tools and events, not a seat.
|
||||
|
||||
The review of the pull request checks the home of every new exported name; the checks design 26 lists refuse a stray
|
||||
file in a contracts module, a seat named in mesh-sdk, an import of a contracts module from mesh-sdk, and a client
|
||||
reaching another seat's client.
|
||||
|
||||
## What the first port actually cost
|
||||
|
||||
Six attempts, one real bug. Recorded because the ratio is the lesson: **the mesh was right every
|
||||
@@ -105,3 +130,23 @@ time and the scaffolding was not.**
|
||||
- **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately.
|
||||
- **A data directory is never removed by the mesh** (ADR 0030),
|
||||
and that protects against the mesh only — not against a disk or a mistaken command.
|
||||
|
||||
## Moving a module's directory
|
||||
|
||||
When a fix moves what a container mounts to a new path (issue 1843: only-office's working directories
|
||||
moved into a root-only box), the old directory does not go away by itself, and it is closed in three steps.
|
||||
|
||||
1. **The move.** The new directory gets a new id at its new path. The old one keeps **its old id at its old
|
||||
path** for as long as it holds data, with the mode it should now have: the node-engine leaves a directory
|
||||
it finds at a path under a new id as it found it, and sets no mode when the owner differs (issue 339), so
|
||||
only the old id closes it. The node-engine removes a directory that is no longer declared only when it is
|
||||
empty; a non-empty one is kept on disk with no further cleanup.
|
||||
2. **The deletion, on the operator's word.** Its contents are data, so they are removed only after the
|
||||
operator's yes, through `<node>/node-files.delete`. That tool moves into the node's trash: the disk space
|
||||
comes back only when the trash is emptied (issue 1870: no tool does that yet).
|
||||
3. **The declaration leaves** in a second small fix, once the directory is empty (issue 1854).
|
||||
|
||||
Plan the three in the first fix's issue, so the second fix is not found later by accident.
|
||||
|
||||
A service that starts on empty working directories may need minutes before it answers (OnlyOffice: about
|
||||
two): the live check after the delivery waits for it before calling the move broken.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,47 +16,115 @@ hour is a feature.
|
||||
|
||||
**Who runs it.** Anyone who writes code, engineers and agents alike.
|
||||
|
||||
**A fix found during a milestone rides in that milestone's group** (the operator, 2026-10-21, after a day with ten
|
||||
mesh-sdk merges, each rebuilding about forty dependent modules, and eight mesh-controller merges, each sending a
|
||||
delivery to every node). Playbook [07](07-feature-branches.md) makes a milestone one piece of functionality
|
||||
released once as one delivery group (ADR 0482). A small fix in a repository the running milestone touches, mesh-sdk
|
||||
and mesh-controller above all, is built as below but on the milestone's branch, so its pull request joins the group
|
||||
and merges with it. The exception is a fault that is broken for the operator now (something they see failing at
|
||||
this moment): it goes alone as a small fix, and the lead's report says why.
|
||||
|
||||
## The aim
|
||||
|
||||
A small fix spends its time on the fix, and runs the long part once: **one review, one push, one check run,
|
||||
one delivery**. Everything before the push is local and fast.
|
||||
one delivery** (ADR 0548; the operator, 2026-10-26: "every release should only run 1 check or 1 try"; "we don't have more
|
||||
processing power"). Everything before the push is local and fast, and no try is run.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Read the issue and the code it names** (the session's own model, or one read-only agent on the smaller
|
||||
model answering exact questions with file:line). If a design question is open, it is settled before any
|
||||
code is written: on the issue, through the knowledge base, or by the operator. A fix that turns out to
|
||||
need a second repository, or more than about an hour, stops here and becomes a feature; one that needs a
|
||||
decision stays a small fix.
|
||||
model answering exact questions with file:line). **Read what exists in the fix's area today, briefly, before any
|
||||
code** (playbook [07](07-feature-branches.md) step 0 item 3a, at a small fix's size): what the code and the live mesh
|
||||
(through the mesh MCP server's tools) do there now, and for each value, label, file or job the fix changes or removes,
|
||||
every reader that relies on it, found by a search across the repositories (written down as run) and not from memory,
|
||||
with what happens to each after the fix. It is a few lines in the issue's note, under `What exists today`, and the
|
||||
review runs the search again; a reader it finds that the note omits makes the fix wrong. **A fix builds what a
|
||||
design says, and decides nothing** (the operator, 2026-11-10; playbook [07](07-feature-branches.md), step 0): its
|
||||
input is the issue and the design that covers the area (the design whose `code:` names the repository, or the
|
||||
issue's design link), read in the parts the fix touches and in the design section that places code, *Where a
|
||||
seat's code goes*. The note names, for everything the fix adds or changes (an exported type, field, constant,
|
||||
package or module, a state, a verb, an event, a bound, a caller), the design line that gives it. A fix that only
|
||||
makes the code do what its issue and its design already say needs nothing more. Anything no design line gives is
|
||||
a **design question**: the session asks the operator, one question with its recommendation, and the answer is
|
||||
written into the design (playbook [02](02-graduation.md), *Answering a design question*) before any code builds
|
||||
it; neither the session nor the lead answers it. A question of internal form only (playbook
|
||||
[07](07-feature-branches.md), step 0, *Who answers a design question*) is answered instead by `hq-design-writer`
|
||||
as a design line, read by the four reviewers by architectural domain in one round, and told to the operator in
|
||||
one line. A handover to the developer quotes the design lines and never
|
||||
decides a placement itself (2026-11-04: a class placed in mesh-sdk by a lead's handover, against that section,
|
||||
passed review and its check). A fix that turns out to need a second repository, or more than about an hour,
|
||||
stops here and becomes a feature; one that needs a design question answered stays a small fix. **The session's own model (the larger) then writes the issue's acceptance
|
||||
criteria** as playbook [07](07-feature-branches.md) step 0 item 5 says: each one sentence, given, when, then,
|
||||
naming the level of the test that proves it and the design line or issue done-when it comes from, as an
|
||||
`issue-tracker.note` headed `Acceptance criteria`. A
|
||||
playbook or design edit has none: its check is `go test ./...` at hq's root, which runs the four checks (ADR 0527).
|
||||
2. **Move the issue to `fixing`** with `by` as who@node.
|
||||
3. **Build on a local branch** `fix/<issue>-<slug>`, in a worktree of its own branched from `main`. Write the
|
||||
test that fails today first; make the change; keep `gofmt`, `go vet` and the unit tests of what changed green
|
||||
(each well under a second; never a merge check or a database locally: how-we-build §5, ADR 0391).
|
||||
Commit, naming the issue. For a playbook or design edit, run the repository's own checks (in hq,
|
||||
`sh merge-check.sh`).
|
||||
4. **One review of the local commit, before anything is pushed** (a reviewer that did not write it, larger
|
||||
model; at most 3 rounds, as playbook 07; from round 2 only what is wrong; a finding outside the issue's
|
||||
done-when is opened as an issue of its own, never asked as a change). The reviewer's findings go back to the same agent that wrote the fix, continued by
|
||||
3. **Build on a local branch** `fix/<issue>-<slug>`, in a worktree of its own branched from `main`, never in
|
||||
the repository's own clone, which stays on `main` (playbook [07](07-feature-branches.md), step 0, item 2). Write the
|
||||
test that fails today first, on an assertion about what the code does: a test that fails only because it
|
||||
does not compile has not failed first (playbook [07](07-feature-branches.md) step 1); make the change; keep `gofmt`, `go vet` and the repository's whole unit suite green
|
||||
before every commit (each test well under a second; never a merge check or a database locally: how-we-build §5, ADR 0391).
|
||||
Check that it compiles with `go vet`, never `go build` in a module's folder: with one main package it writes the
|
||||
program beside `go.mod`, and `git add -A` takes it (issue 1577: two 5-10 MB programs committed on 2026-10-21,
|
||||
caught only by the review); read `git show --stat` before pushing.
|
||||
The tests are written from the acceptance criteria, at least one per criterion at its level, each citing the
|
||||
criterion it proves by its label (`<issue>/AC<n>`, playbook 07 step 0 item 5). Commit, naming the issue. For a playbook or design edit, run the repository's own checks (in hq,
|
||||
`go test ./...` at the root). The integration tests are compiled whole (`go vet -tags integration ./...`), and once
|
||||
per head reported those of the packages the fix touches and of their importers in the module run against an
|
||||
embedded bus, or the tests of them that exercise the fix by name where the importers are many, named in the report;
|
||||
never a whole module's or level's run (playbook 07 step 1). Nothing is pushed, no pull request is opened and no try (`mesh-controller.try`, `mesh-delivery-checker.try`, `mesh-delivery-checker.checker_try`) is called, unless the operator asks for
|
||||
one. A single integration test may be run locally against an embedded bus when it is the fastest way to find a cause;
|
||||
before the push, the integration tests that exercise the change have run that way at the reviewed head
|
||||
(playbook 07 step 3), and the pull request names them and their results. A test fixture writes state in the form its real
|
||||
writer writes it; the review asks it.
|
||||
4. **One review of the local commit** (a reviewer that did not write it, larger
|
||||
model; at most 3 rounds, as playbook 07; from round 2 only what is wrong; it judges each change against the design line its note names, as playbook 07 step 2 item 2a, and code no
|
||||
design line gives is a finding; it reads each acceptance criterion
|
||||
against its test, as playbook 07 step 2 item 6; it runs the `What exists today` search again, as playbook 07 step 2 item 6b; a finding outside the issue's done-when and its acceptance
|
||||
criteria is opened as an issue of its own, never asked as a change; it judges the integration tests by reading them against the code, as playbook 07 step 2 item 4 says; it answers playbook 07 step 2 item 10's two questions, on state and seams and on mesh-agnostic repositories, in its report). The reviewer's findings go back to the same agent that wrote the fix, continued by
|
||||
its id, so the fix keeps its context. A playbook or design edit of a few lines may skip the reviewer: the
|
||||
operator's word on the pull request is its review, and the lead marks it ready on that word.
|
||||
5. **Push once and open the pull request.** The trunk is what runs and nothing holds merged work back
|
||||
(ADR 0401), so a fix waits for no other change's delivery. The forge's merge
|
||||
check runs the integration tests, the only
|
||||
place they run (the operator, 2026-10-16). A failed check
|
||||
goes back to the same agent with the failure, and the reviewer reads only the change it makes.
|
||||
6. **The reviewer marks it ready at once** (for an edit that skipped the reviewer, the lead, on the
|
||||
operator's word) with `mesh-delivery.mark-ready` (the delivery of the head it approved, who reviewed it and
|
||||
why), which writes the label `mesh/ready` and then `ready at <full sha>`, the order the forge counts (issue 913);
|
||||
mesh-delivery merges it when its checks pass. A fix to a module that changes how it runs carries an
|
||||
integration test starting the module whole against the real bus and store, run by the forge's checks
|
||||
operator's Approve of its merge is its review, and the review record says so.
|
||||
5. **Push the reviewed head once and open the pull request, only after the review approves.** The trunk is what runs and nothing holds merged work back
|
||||
(ADR 0401), so a fix waits for no other change's delivery. No pull request, draft or not, exists before the review
|
||||
approves the head, and no fix round is pushed to an open pull request: every push to an open pull request asks a new
|
||||
check. The checker's merge check runs the unit and the integration level once, as the gate (playbook [07](07-feature-branches.md) step 3); the pushed head is read from the forge first, as playbook 07 step 3 item 1 says. A merge whose check has no verdict
|
||||
(the checker drops a check still running at the merge) is reported to the operator as merged unchecked, never as
|
||||
passed (playbook 07 step 3 item 2).
|
||||
A failed check is read from its whole log (never re-asked to see if it passes), then the repository's whole integration level
|
||||
is run locally (`go test -tags integration -count=1 ./...` in each module) so every failure shows at once, where a verdict
|
||||
names only the first (issue 1921); the failures go back to the same agent, fixed in one change, the reviewer reads only the change
|
||||
it makes, and the head is pushed once more.
|
||||
**A temporary mode that merges by hand** (the lead merges because the normal Approve and merge path is not
|
||||
running): the one check still runs, and the lead names its verdict in the pull request's description; a try with tests `.` is asked only when the operator asks for one (research 071; playbook
|
||||
[07](07-feature-branches.md) step 3).
|
||||
6. **The pull request's description carries the review record** (`Reviewed-by: <who@node> at <full sha>`,
|
||||
playbook [07](07-feature-branches.md) step 3) and the session that pushed it (`Pushed-by: <who@node>`, ADR 0505 §5), and mesh-delivery asks the operator's Approve as soon as its
|
||||
checks pass and merges the approved head (ADR 0421); nobody marks, labels, comments or rechecks. A fix to a module that changes how it runs carries an
|
||||
integration test starting the module whole against the real bus and store, run by the checker's one merge check
|
||||
(ADR 0391 §1, §6); unit tests alone are not that proof, and the lab is not used. The operator's word is waited for only when the fix changes what the operator sees, removes data,
|
||||
or needs the operator's terminal.
|
||||
7. **Follow its delivery plan to done** (`mesh-controller.plans` with `wait` and `until: done`), prove the fix on
|
||||
the mesh (the symptom gone, measured), and resolve the issue with `located-in` and `fixed-by` the same hour.
|
||||
**A fix that depends on another pull request outside its delivery group opens only once that one is delivered**, not merely merged (members of one group open together, as playbook 07 step 3 item 1 says) (a note
|
||||
in the description does not hold it back: the Approve merges whatever is asked; 2026-10-25, machines lost modules
|
||||
when a second change merged first). **A reviewed follow-up to a fix still waiting for the Approve is folded into
|
||||
that pull request** (head moved): one Approve, one delivery. Both are checked as playbook
|
||||
[07](07-feature-branches.md) step 3 says.
|
||||
**At the second instance of a fault kind found in a review, the developer searches for every instance** and names
|
||||
each in the report (playbook [07](07-feature-branches.md) step 2, item 9).
|
||||
7. **Follow its delivery to done**: a watcher waits on the status view's events (`mesh-delivery-view.moving`,
|
||||
`.settled`, `.stuck` and `.delivered`) and reads the view's `assigner_wants` and `node_haves` after each wake, as
|
||||
playbook [07](07-feature-branches.md) step 4 says; the delivery is done when the view says `delivered` for its
|
||||
source revision, and the haves per node and the indexer's record of the module name the build (playbook 07 step 4;
|
||||
"on the machines" alone is not "delivered"). Then prove the fix on the mesh (the symptom gone, measured), and resolve the issue with
|
||||
`located-in` and `fixed-by` the same hour.
|
||||
|
||||
A fix that stops before its delivery moves its issue back to `open` with a note saying how far it got
|
||||
(branch, last commit, what is left), as playbook [07](07-feature-branches.md) says.
|
||||
|
||||
**An issue opened along the way is fixed too** (the operator, 2026-10-21). A fix's review, check or delivery that
|
||||
opens an issue (a remark outside the done-when, a fault met live) leaves it to the same session to fix: right after
|
||||
this fix's delivery, as a small fix of its own, or folded into the next one in the same repository. It waits only
|
||||
when another running milestone owns it, or it needs a decision or the operator first; the report then says on what.
|
||||
|
||||
**A second fault found by the delivery.** When a fix's delivery shows a second fault that keeps the issue's
|
||||
done-when from holding, and that fault is in the same files, it is taken into the same issue as a second pull
|
||||
request with a review round of its own. A fault in other code is opened as an issue of its own, and the first
|
||||
@@ -77,10 +145,10 @@ code.
|
||||
|---|---|---|
|
||||
| building the fix | 1 hour | it is a feature: stop and plan it under playbook 07 |
|
||||
| the review | 15 minutes | ask the reviewer where it stands |
|
||||
| the ready mark | 15 minutes after the review approved | the reviewer marks it, or the lead does |
|
||||
| the merge check | 15 minutes | read `mesh-controller.queue` and the check's log |
|
||||
| the merge after ready | 5 minutes after the checks pass | read `mesh-delivery.show` for the delivery |
|
||||
| the delivery plan | its class's budget (`mesh-delivery.times`), at most 30 minutes | read `mesh-controller.plans`, then `mesh-delivery.stalled` |
|
||||
| the merge check | 30 minutes (it waits behind the asks before it for a build node's free slot, ADR 0525) | read `mesh-delivery.show`, the check's log and the check stream's backlog in the delivery view |
|
||||
| the Approve's ask | 5 minutes after the check (`mesh/check`) passes | read `mesh-delivery.show`: an ask not raised is a fault, said to the operator |
|
||||
| the merge after the Approve | 5 minutes | read `mesh-delivery.show` for the delivery |
|
||||
| the delivery | its class's budget (3 minutes for a leaf module, 10 for a core module), at most 30 minutes | read the view's wants page, or `assigner_wants` against `node_haves`: a want held back names its rule, a `stuck` module its words |
|
||||
|
||||
A wait past its bound is said to the operator at once, with what is being done about it.
|
||||
|
||||
@@ -92,10 +160,27 @@ reading itself and uses one subagent only for the review.
|
||||
## How it is checked
|
||||
|
||||
- A small fix has one pull request with one or few commits naming its issue, opened after its review: a pull
|
||||
request with many heads is a fix reviewed after its push.
|
||||
- Its issue moves to `resolved` within the hour its delivery plan ends.
|
||||
- A pull request reviewed and green but unmarked
|
||||
for more than 15 minutes, a fix to a module's running marked ready with no integration test of the module whole, or a review report
|
||||
asking a change outside the issue's done-when, is the failure of steps 4 to 6.
|
||||
request with many heads is a fix reviewed after its pull request was opened.
|
||||
- A pull request merged by hand in a temporary mode names the verdict of its one check in its description
|
||||
on the forge (`gitea.gitea_get_pull_request`); one without is the step skipped.
|
||||
- One fix, one check (ADR 0548): a pull request that existed before its review approved the head, or a head whose check was
|
||||
asked more than once with no new reviewed head, is steps 4 and 5 skipped; so is a `mesh-controller.try` or `mesh-delivery-checker.try` the operator did not ask for.
|
||||
- Its issue moves to `resolved` within the hour the view says its source revision `delivered`.
|
||||
- A pull request with no review record at its head (the Approve's ask says so first), a fix to a module's
|
||||
running pushed with no integration test of the module whole, or a review report
|
||||
asking a change outside the issue's done-when and acceptance criteria, is the failure of steps 4 to 6.
|
||||
- A fixed code issue's note names the design line for each thing the fix adds, and its commit names the same lines
|
||||
(`Builds:`); a fix adding what no design line gives, with no answered design question behind it, is step 1 skipped.
|
||||
- A fixed code issue shows a note with `What exists today` (`issue-tracker.get`) older than its first commit, its
|
||||
search written as run; one without is step 1 skipped, and a reader of a changed value met only after the delivery
|
||||
is that note left short.
|
||||
- A fixed code issue shows a note headed `Acceptance criteria` (`issue-tracker.get`) older than its first
|
||||
commit, and each criterion's label is cited by a test in the pushed tree
|
||||
(`git grep -n '<issue>/AC<n>'`); one without is step 1 skipped.
|
||||
- An issue in `fixing` with nobody on it shows in `issue-tracker.list` with `flag: abandoned`; a stopped fix
|
||||
moves it back to `open` with its note.
|
||||
- A fix found during a milestone is named in the lead's report with the group it rides in, or with the reason it
|
||||
went alone. `mesh-delivery.deliveries` shows the milestone's members in one group, merged as one source revision
|
||||
per milestone; a merge of mesh-sdk or mesh-controller outside the group, with no reported reason, is this rule skipped.
|
||||
- The repository's own clone says `main` (`git -C <clone> branch --show-current`); another branch there is a
|
||||
fix built outside a worktree (issue 1010).
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Playbook 10 — Visual design
|
||||
|
||||
**Trigger.** A page or a screen the operator will use is about to be designed or rebuilt, or the operator judges
|
||||
one unusable. It runs before the decisions about its look are written and before any of its code: a page built
|
||||
first and judged after it ships is rebuilt (the operator, 2026-10-22, on design 59's mail page, judged unusable
|
||||
the day it was delivered).
|
||||
|
||||
**Who runs it.** The lead of the design, on the larger model. Mockups may be drawn by an agent on the smaller
|
||||
model from the lead's brief. The operator picks; nobody else does.
|
||||
|
||||
## What it produces
|
||||
|
||||
- **The reference**: one mockup the operator picked, kept with the design as `NN-mockup/` beside its to-be
|
||||
document. It is a direction, not a pixel specification: the build matches what it shows the operator doing,
|
||||
in the order and the weight it shows, not its exact pixels.
|
||||
- **The decisions it needs**: what the pick changes about what the mesh keeps, shows or does, written as decision
|
||||
records (playbook [02](02-graduation.md)) before the design's milestones (playbook [04](04-build-handoff.md)).
|
||||
- **An acceptance test in the operator's words**: the page is done when the operator uses it, on the devices they
|
||||
named, for the work the mockup shows.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Gather what the page must carry.** A page whose direction needs research first starts at playbook
|
||||
[01](01-research.md); this playbook then draws it. Read the as-is of what it replaces and the real shape of its data from the
|
||||
mesh (how many items, how long a line, what is missing in practice), the operator's complaint in their own
|
||||
words, and a short survey of how others solve it (a handful of products, one line each on what changes for
|
||||
the person). Write it into the work folder; nothing goes on the mesh.
|
||||
2. **Draw three to five mockups with fixed data.** Each is one static HTML file that opens from disk: invented
|
||||
data shaped like the real (names, amounts, lengths, a long subject, an empty sender), no backend, a mock script
|
||||
only where an interaction is the point. Each mockup is a different direction, not a variation of one; each
|
||||
shows a phone's width and a desktop's. One index page links them all.
|
||||
3. **Show them and ask one question.** Put the index on the machine the operator is at, and ask which direction to
|
||||
keep, one question at a time with the lead's recommendation. A round's answer may be a pick, a combination of
|
||||
two, or a change; the next round draws only that. Three rounds without a pick mean something the operator wants
|
||||
is missing: ask what, before drawing again.
|
||||
4. **Make the pick the reference.** Copy it into the design's `NN-mockup/`, with every name, address, employer,
|
||||
mail address, domain, provider and amount of the operator's own replaced by invented ones: hq is public. Say in the design that
|
||||
the reference is a direction, and link it.
|
||||
5. **Write what is new as decisions,** each with the operator's Approve where it changes what they see, then the
|
||||
design and its milestones. The design's acceptance tests end with the operator using the page live.
|
||||
6. **Hold the build to it.** The lead gives playbook [07](07-feature-branches.md)'s reviewer the reference with
|
||||
the built page, at a phone's and a desktop's width, as part of what the review reads against the issue. The
|
||||
operator using the page is the milestone's live proof, made before the next milestone starts (playbook
|
||||
[04](04-build-handoff.md)).
|
||||
|
||||
## Bounds
|
||||
|
||||
| Waiting on | Bound | Past the bound |
|
||||
|---|---|---|
|
||||
| a round of mockups | 1 hour | say to the operator what is taking long |
|
||||
| the operator's pick | the next exchange | ask once more, one question, with the recommendation |
|
||||
| rounds without a pick | 3 | ask what is missing before drawing again |
|
||||
|
||||
## Rules
|
||||
|
||||
- No code reaches the mesh during a visual design: mockups live in the work folder until one is picked.
|
||||
- Mockups use invented data, never a copy of the operator's mail, files or accounts.
|
||||
- The operator's pick stands over the lead's taste; the lead's recommendation is said once, with its reason.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- A to-be design of a page the operator uses links its reference mockup in `NN-mockup/`, and that mockup holds
|
||||
none of the operator's own names, addresses, domains, providers or amounts (read by the reviewer of the design's pull request).
|
||||
- The milestone's review read the built page beside the reference at a phone's and a desktop's width (its
|
||||
report says so).
|
||||
- The design's last acceptance test is the operator using the page, proven before the next milestone starts.
|
||||
@@ -0,0 +1,535 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Novox Playbooks</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#f7f6f2; --card:#ffffff; --ink:#1d1f23; --mute:#5d6470; --line:#d9d6cc; --soft:#efede6;
|
||||
--idea:#2f5fa7; --sym:#b4462b; --dec:#7a4bb0; --build:#3c8a5a; --mesh:#0f6f7a; --op:#c2410c; --side:#8a6d1f;
|
||||
--ok:#15803d; --okbg:#e6f4ea; --warn:#a16207; --warnbg:#fdf3d7; --bad:#b91c1c; --badbg:#fde8e8;
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--bg:#15171b; --card:#1e2127; --ink:#e8e6e1; --mute:#9aa1ad; --line:#353a43; --soft:#262a31;
|
||||
--idea:#7aa7ef; --sym:#f08a6c; --dec:#b993ef; --build:#6fcf97; --mesh:#4fc3cf; --op:#fb923c; --side:#d9b44a;
|
||||
--ok:#4ade80; --okbg:#173222; --warn:#facc15; --warnbg:#3a3016; --bad:#f87171; --badbg:#3a1d1d;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--bg:#15171b; --card:#1e2127; --ink:#e8e6e1; --mute:#9aa1ad; --line:#353a43; --soft:#262a31;
|
||||
--idea:#7aa7ef; --sym:#f08a6c; --dec:#b993ef; --build:#6fcf97; --mesh:#4fc3cf; --op:#fb923c; --side:#d9b44a;
|
||||
--ok:#4ade80; --okbg:#173222; --warn:#facc15; --warnbg:#3a3016; --bad:#f87171; --badbg:#3a1d1d;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
|
||||
main{max-width:1120px;margin:0 auto;padding:28px 16px 72px}
|
||||
h1{font-size:28px;margin:0 0 4px}
|
||||
h2{font-size:20px;margin:44px 0 6px}
|
||||
h3{font-size:16px;margin:0 0 6px}
|
||||
p{margin:6px 0 10px}
|
||||
.sub{color:var(--mute);margin:0 0 18px}
|
||||
.lead{max-width:760px}
|
||||
nav{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 6px}
|
||||
nav a,.chip{display:inline-block;padding:4px 11px;border:1px solid var(--line);border-radius:999px;color:var(--ink);text-decoration:none;font-size:13px;background:var(--card);cursor:pointer}
|
||||
nav a:hover,.chip:hover{border-color:var(--mute)}
|
||||
.chip.on{background:var(--ink);color:var(--bg);border-color:var(--ink)}
|
||||
.box{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px;margin:12px 0}
|
||||
.scroll{overflow-x:auto}
|
||||
svg{display:block;max-width:100%;height:auto;font-family:inherit}
|
||||
svg text{fill:var(--ink);font-size:13px}
|
||||
svg .mute{fill:var(--mute);font-size:11.5px}
|
||||
svg .t{font-weight:600}
|
||||
svg .edge{stroke:var(--mute);stroke-width:1.6;fill:none}
|
||||
svg .dash{stroke-dasharray:5 4}
|
||||
.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:13px;color:var(--mute);margin-top:8px}
|
||||
.legend i{display:inline-block;width:12px;height:12px;border-radius:3px;margin-right:6px;vertical-align:-1px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px}
|
||||
.pb{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px;border-top:4px solid var(--c,var(--line))}
|
||||
.pb .num{font:600 12px/1 ui-monospace,monospace;color:var(--c);letter-spacing:.04em}
|
||||
.pb h3{margin:4px 0 4px;font-size:17px}
|
||||
.pb .trig{color:var(--mute);font-size:13.5px;margin:0 0 8px}
|
||||
.pb details{border-top:1px solid var(--line);padding-top:6px;margin-top:6px}
|
||||
.pb summary{cursor:pointer;font-size:13.5px;font-weight:600;color:var(--ink);list-style:none}
|
||||
.pb summary::before{content:"▸ ";color:var(--mute)}
|
||||
.pb details[open] summary::before{content:"▾ "}
|
||||
.pb ol,.pb ul{margin:6px 0 4px;padding-left:20px;font-size:13.5px}
|
||||
.pb li{margin:2px 0}
|
||||
.kv{display:grid;grid-template-columns:96px 1fr;gap:4px 10px;font-size:13px;margin-top:6px}
|
||||
.kv b{color:var(--mute);font-weight:500}
|
||||
.tag{display:inline-block;font-size:11.5px;padding:1px 7px;border-radius:999px;background:var(--soft);color:var(--mute);margin:0 4px 4px 0}
|
||||
.new{background:var(--okbg);color:var(--ok);font-weight:600}
|
||||
table{border-collapse:collapse;width:100%;font-size:14px}
|
||||
th,td{text-align:left;padding:7px 9px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
th{color:var(--mute);font-weight:600;font-size:12.5px;text-transform:uppercase;letter-spacing:.03em}
|
||||
.note{border-left:3px solid var(--ok);background:var(--okbg);padding:9px 12px;border-radius:6px;font-size:14px}
|
||||
.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||||
@media (max-width:760px){.two{grid-template-columns:1fr}}
|
||||
footer{color:var(--mute);font-size:13px;margin-top:48px}
|
||||
code{font:12.5px ui-monospace,monospace;background:var(--soft);padding:1px 5px;border-radius:4px}
|
||||
.theme{float:right}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<button class="chip theme" id="theme" type="button">Theme</button>
|
||||
<h1>Novox Playbooks</h1>
|
||||
<p class="sub">How work is done on the mesh, from an idea or a symptom to a delivered change. Drawn from hq's <code>00-META/process/</code> at main <code>f269e83</code>, 2026-10-19, amended 2026-10-26 for ADR 0548 (one release, one check: no try).</p>
|
||||
|
||||
<nav>
|
||||
<a href="#cycle">The cycle</a>
|
||||
<a href="#feature">A feature, step by step</a>
|
||||
<a href="#tests">When tests run</a>
|
||||
<a href="#review">The review loop</a>
|
||||
<a href="#issues">An issue's life</a>
|
||||
<a href="#where">Where things live</a>
|
||||
<a href="#all">All playbooks</a>
|
||||
</nav>
|
||||
|
||||
<!-- ============ THE CYCLE ============ -->
|
||||
<h2 id="cycle">The development cycle</h2>
|
||||
<p class="lead">Work enters as an <b>idea</b> or a <b>symptom</b>. An idea becomes a decision, a design and then milestones; a symptom becomes an issue. Both end as code delivered to the machines, and the as-is design is updated. <b>No design without a decision; no development without a design or an issue that sends it.</b></p>
|
||||
<div class="box scroll">
|
||||
<svg viewBox="0 0 1080 470" role="img" aria-label="The development cycle across playbooks 01 to 09">
|
||||
<defs>
|
||||
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker>
|
||||
</defs>
|
||||
<!-- entry points -->
|
||||
<g>
|
||||
<rect x="16" y="40" width="120" height="50" rx="25" fill="none" stroke="var(--idea)" stroke-width="2"/>
|
||||
<text x="76" y="70" text-anchor="middle" class="t">Idea</text>
|
||||
<rect x="16" y="300" width="120" height="50" rx="25" fill="none" stroke="var(--sym)" stroke-width="2"/>
|
||||
<text x="76" y="330" text-anchor="middle" class="t">Symptom</text>
|
||||
</g>
|
||||
<!-- idea path -->
|
||||
<g>
|
||||
<rect x="176" y="30" width="150" height="70" rx="10" fill="var(--card)" stroke="var(--idea)" stroke-width="2"/>
|
||||
<text x="251" y="58" text-anchor="middle" class="t">01 Research</text>
|
||||
<text x="251" y="78" text-anchor="middle" class="mute">questions, findings,</text>
|
||||
<text x="251" y="92" text-anchor="middle" class="mute">direction confirmed</text>
|
||||
|
||||
<rect x="366" y="30" width="160" height="70" rx="10" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
|
||||
<text x="446" y="58" text-anchor="middle" class="t">02 Graduation</text>
|
||||
<text x="446" y="78" text-anchor="middle" class="mute">decisions in the</text>
|
||||
<text x="446" y="92" text-anchor="middle" class="mute">knowledge base · to-be</text>
|
||||
|
||||
<rect x="566" y="30" width="160" height="70" rx="10" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
|
||||
<text x="646" y="58" text-anchor="middle" class="t">04 Breakdown</text>
|
||||
<text x="646" y="78" text-anchor="middle" class="mute">milestones by release,</text>
|
||||
<text x="646" y="92" text-anchor="middle" class="mute">issues per repository</text>
|
||||
|
||||
<rect x="766" y="30" width="150" height="70" rx="10" fill="var(--card)" stroke="var(--build)" stroke-width="2.5"/>
|
||||
<text x="841" y="58" text-anchor="middle" class="t">07 Feature</text>
|
||||
<text x="841" y="78" text-anchor="middle" class="mute">one milestone =</text>
|
||||
<text x="841" y="92" text-anchor="middle" class="mute">one release</text>
|
||||
</g>
|
||||
<!-- symptom path -->
|
||||
<g>
|
||||
<rect x="176" y="290" width="150" height="70" rx="10" fill="var(--card)" stroke="var(--sym)" stroke-width="2"/>
|
||||
<text x="251" y="318" text-anchor="middle" class="t">03 Issues</text>
|
||||
<text x="251" y="338" text-anchor="middle" class="mute">opened in the tracker,</text>
|
||||
<text x="251" y="352" text-anchor="middle" class="mute">diagnosed, located</text>
|
||||
|
||||
<rect x="566" y="290" width="160" height="70" rx="10" fill="var(--card)" stroke="var(--build)" stroke-width="2.5"/>
|
||||
<text x="646" y="318" text-anchor="middle" class="t">08 Small fix</text>
|
||||
<text x="646" y="338" text-anchor="middle" class="mute">one issue, one repository,</text>
|
||||
<text x="646" y="352" text-anchor="middle" class="mute">about an hour</text>
|
||||
</g>
|
||||
<!-- delivered + as-is -->
|
||||
<g>
|
||||
<rect x="950" y="160" width="116" height="74" rx="10" fill="var(--card)" stroke="var(--mesh)" stroke-width="2.5"/>
|
||||
<text x="1008" y="188" text-anchor="middle" class="t">Delivered</text>
|
||||
<text x="1008" y="206" text-anchor="middle" class="mute">Approve, merge,</text>
|
||||
<text x="1008" y="220" text-anchor="middle" class="mute">wants and haves</text>
|
||||
|
||||
<rect x="766" y="160" width="150" height="74" rx="10" fill="var(--card)" stroke="var(--dec)" stroke-width="2" stroke-dasharray="5 4"/>
|
||||
<text x="841" y="188" text-anchor="middle" class="t">As-is updated</text>
|
||||
<text x="841" y="206" text-anchor="middle" class="mute">02 "when it ships":</text>
|
||||
<text x="841" y="220" text-anchor="middle" class="mute">to-be → implemented</text>
|
||||
</g>
|
||||
<!-- side playbooks -->
|
||||
<g>
|
||||
<rect x="176" y="400" width="190" height="56" rx="10" fill="var(--soft)" stroke="var(--side)"/>
|
||||
<text x="271" y="424" text-anchor="middle" class="t">05 Constitution sync</text>
|
||||
<text x="271" y="442" text-anchor="middle" class="mute">how-we-build → derived page</text>
|
||||
<rect x="396" y="400" width="190" height="56" rx="10" fill="var(--soft)" stroke="var(--side)"/>
|
||||
<text x="491" y="424" text-anchor="middle" class="t">06 Writing a module</text>
|
||||
<text x="491" y="442" text-anchor="middle" class="mute">what runs today → a module</text>
|
||||
<rect x="876" y="400" width="190" height="56" rx="10" fill="var(--soft)" stroke="var(--side)"/>
|
||||
<text x="971" y="424" text-anchor="middle" class="t">09 Cleanup</text>
|
||||
<text x="971" y="442" text-anchor="middle" class="mute">branches, worktrees, on the word</text>
|
||||
</g>
|
||||
<!-- edges -->
|
||||
<path class="edge" d="M136,65 H174" marker-end="url(#ar)"/>
|
||||
<path class="edge" d="M326,65 H364" marker-end="url(#ar)"/>
|
||||
<path class="edge" d="M526,65 H564" marker-end="url(#ar)"/>
|
||||
<path class="edge" d="M726,65 H764" marker-end="url(#ar)"/>
|
||||
<path class="edge" d="M916,65 C990,65 1008,100 1008,158" marker-end="url(#ar)"/>
|
||||
<path class="edge" d="M136,325 H174" marker-end="url(#ar)"/>
|
||||
<path class="edge" d="M326,325 H564" marker-end="url(#ar)"/>
|
||||
<text x="445" y="317" text-anchor="middle" class="mute">a fix of about an hour</text>
|
||||
<path class="edge" d="M726,325 C990,325 1008,290 1008,236" marker-end="url(#ar)"/>
|
||||
<path class="edge" d="M948,197 H918" marker-end="url(#ar)"/>
|
||||
<path class="edge dash" d="M251,290 C251,200 420,170 440,102" marker-end="url(#ar)"/>
|
||||
<text x="300" y="196" class="mute">a design gap: decision first</text>
|
||||
<path class="edge dash" d="M326,300 C560,250 720,170 800,102" marker-end="url(#ar)"/>
|
||||
<text x="612" y="236" class="mute">larger than an hour: a feature</text>
|
||||
<path class="edge dash" d="M1008,236 C1008,330 990,360 971,398" marker-end="url(#ar)"/>
|
||||
</svg>
|
||||
<div class="legend">
|
||||
<span><i style="background:var(--idea)"></i>from an idea</span>
|
||||
<span><i style="background:var(--sym)"></i>from a symptom</span>
|
||||
<span><i style="background:var(--dec)"></i>decisions and designs</span>
|
||||
<span><i style="background:var(--build)"></i>code changes</span>
|
||||
<span><i style="background:var(--mesh)"></i>the mesh delivers</span>
|
||||
<span><i style="background:var(--side)"></i>alongside</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ FEATURE SWIMLANE ============ -->
|
||||
<h2 id="feature">A feature, step by step (playbook 07)</h2>
|
||||
<p class="lead">One milestone is one release: one branch name in every repository it touches, one worktree per repository, one fixing agent per repository kept for the whole milestone, one reviewer, and one pull request per repository, opened once. Nothing is pushed or tried before the review (ADR 0548): the branches are pushed <b>once, after the review approves</b>, and the one check runs the integration tests.</p>
|
||||
<div class="box scroll">
|
||||
<svg viewBox="0 0 1080 430" role="img" aria-label="Playbook 07 in lanes: lead, fixing agents, reviewer, the mesh, the operator">
|
||||
<defs>
|
||||
<marker id="ar2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker>
|
||||
</defs>
|
||||
<!-- step columns -->
|
||||
<g class="mute">
|
||||
<text x="190" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">0 · Prepare</text>
|
||||
<text x="190" y="34" text-anchor="middle" class="mute">15 min · then design complete</text>
|
||||
<text x="410" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">1 · Develop</text>
|
||||
<text x="410" y="34" text-anchor="middle" class="mute">30 min per issue · local only</text>
|
||||
<text x="630" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">2 · Review</text>
|
||||
<text x="630" y="34" text-anchor="middle" class="mute">≤ 3 rounds · 15 min</text>
|
||||
<text x="830" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">3 · Check</text>
|
||||
<text x="830" y="34" text-anchor="middle" class="mute">push once · 1 check</text>
|
||||
<text x="1000" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">4 · Deliver</text>
|
||||
<text x="1000" y="34" text-anchor="middle" class="mute">until delivered</text>
|
||||
</g>
|
||||
<!-- lanes -->
|
||||
<g>
|
||||
<rect x="0" y="46" width="1080" height="72" fill="var(--soft)" opacity=".55"/>
|
||||
<rect x="0" y="190" width="1080" height="72" fill="var(--soft)" opacity=".55"/>
|
||||
<rect x="0" y="334" width="1080" height="72" fill="var(--soft)" opacity=".55"/>
|
||||
<text x="10" y="86" class="t">Lead</text><text x="10" y="102" class="mute">larger model</text>
|
||||
<text x="10" y="158" class="t">Fixing agents</text><text x="10" y="174" class="mute">smaller model</text>
|
||||
<text x="10" y="230" class="t">Reviewer</text><text x="10" y="246" class="mute">larger model</text>
|
||||
<text x="10" y="302" class="t">The mesh</text><text x="10" y="318" class="mute">the one check, wants</text>
|
||||
<text x="10" y="374" class="t">Operator</text><text x="10" y="390" class="mute">Telegram</text>
|
||||
</g>
|
||||
<!-- lead -->
|
||||
<rect x="110" y="56" width="160" height="52" rx="8" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
|
||||
<text x="190" y="78" text-anchor="middle" class="t">Build map · questions</text>
|
||||
<text x="190" y="96" text-anchor="middle" class="mute">from the milestone part alone</text>
|
||||
<rect x="340" y="56" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
|
||||
<text x="410" y="78" text-anchor="middle" class="t">Up to trunk</text>
|
||||
<text x="410" y="96" text-anchor="middle" class="mute">each branch, local</text>
|
||||
<rect x="760" y="56" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
|
||||
<text x="830" y="78" text-anchor="middle" class="t">Push reviewed heads</text>
|
||||
<text x="830" y="96" text-anchor="middle" class="mute">once, PRs together</text>
|
||||
<rect x="930" y="56" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
|
||||
<text x="1000" y="78" text-anchor="middle" class="t">Follow to done</text>
|
||||
<text x="1000" y="96" text-anchor="middle" class="mute">resolve the issues</text>
|
||||
<!-- fixing -->
|
||||
<rect x="300" y="128" width="220" height="52" rx="8" fill="var(--card)" stroke="var(--build)" stroke-width="2"/>
|
||||
<text x="410" y="150" text-anchor="middle" class="t">Build each design line, commit</text>
|
||||
<text x="410" y="168" text-anchor="middle" class="mute">Builds: line · unit suite each commit</text>
|
||||
<rect x="560" y="128" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--build)" stroke-width="2"/>
|
||||
<text x="630" y="150" text-anchor="middle" class="t">Fix findings</text>
|
||||
<text x="630" y="168" text-anchor="middle" class="mute">the same agents</text>
|
||||
<!-- reviewer -->
|
||||
<rect x="560" y="200" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--idea)" stroke-width="2"/>
|
||||
<text x="630" y="222" text-anchor="middle" class="t">Read all repos</text>
|
||||
<text x="630" y="240" text-anchor="middle" class="mute">each change against its line</text>
|
||||
<!-- mesh -->
|
||||
<rect x="340" y="272" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--mesh)" stroke-width="2"/>
|
||||
<text x="410" y="294" text-anchor="middle" class="t">no try</text>
|
||||
<text x="410" y="312" text-anchor="middle" class="mute">nothing pushed yet</text>
|
||||
<rect x="760" y="272" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--mesh)" stroke-width="2"/>
|
||||
<text x="830" y="294" text-anchor="middle" class="t">Forge checks</text>
|
||||
<text x="830" y="312" text-anchor="middle" class="mute">one check · mesh/check</text>
|
||||
<rect x="930" y="272" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--mesh)" stroke-width="2"/>
|
||||
<text x="1000" y="294" text-anchor="middle" class="t">Merge · wants</text>
|
||||
<text x="1000" y="312" text-anchor="middle" class="mute">backups first, canary first</text>
|
||||
<!-- operator -->
|
||||
<rect x="110" y="344" width="160" height="52" rx="8" fill="var(--card)" stroke="var(--op)" stroke-width="2.5"/>
|
||||
<text x="190" y="366" text-anchor="middle" class="t">Answers each question</text>
|
||||
<text x="190" y="384" text-anchor="middle" class="mute">written into the design</text>
|
||||
<rect x="860" y="344" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--op)" stroke-width="2.5"/>
|
||||
<text x="930" y="366" text-anchor="middle" class="t">Approve</text>
|
||||
<text x="930" y="384" text-anchor="middle" class="mute">once, for the group</text>
|
||||
<!-- edges -->
|
||||
<path class="edge dash" d="M180,108 V342" marker-end="url(#ar2)"/>
|
||||
<path class="edge" d="M270,370 C290,370 280,170 298,166" marker-end="url(#ar2)"/>
|
||||
<text x="186" y="232" class="mute">questions</text>
|
||||
<text x="276" y="262" class="mute">answered</text>
|
||||
<path class="edge" d="M410,128 V110" marker-end="url(#ar2)"/>
|
||||
<path class="edge" d="M630,200 V182" marker-end="url(#ar2)"/>
|
||||
<path class="edge dash" d="M610,182 V198" marker-end="url(#ar2)"/>
|
||||
<text x="514" y="196" class="mute">round n+1</text>
|
||||
<path class="edge" d="M700,226 C740,226 740,82 758,82" marker-end="url(#ar2)"/>
|
||||
<text x="712" y="140" class="mute">approved</text>
|
||||
<path class="edge" d="M830,108 V270" marker-end="url(#ar2)"/>
|
||||
<path class="edge" d="M900,298 C915,298 920,330 925,342" marker-end="url(#ar2)"/>
|
||||
<path class="edge" d="M1000,344 V326" marker-end="url(#ar2)"/>
|
||||
<path class="edge" d="M1000,272 V110" marker-end="url(#ar2)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="two">
|
||||
<div class="box">
|
||||
<h3>Step 0 · what the lead writes before any agent starts</h3>
|
||||
<ul style="margin:6px 0;padding-left:20px;font-size:14px">
|
||||
<li><span class="tag new">new</span>The milestone part of the design is the only input: the prepare maps each design line onto code (a build map per repository, each row citing its line) and lists every design question, what the design does not answer. It writes no findings or rulings of its own.</li>
|
||||
<li><span class="tag new">new</span>Design complete: the operator answers each question, or the design writer one of internal form only (no change to what the operator sees, no permission, no data kept or removed), read by the reviewers by architectural domain; the answer is written into the milestone part, and only then does develop start. Nobody else answers one.</li>
|
||||
<li><span class="tag new">new</span>What exists today, read from the code and the live mesh, never from memory: every effect of a part removed or moved (bus writes, files, credentials, plans, processes, calls answered), every reader relying on each, what happens to each after the change and its test. It opens each build map; no code before it is complete, and the review refuses work without it.</li>
|
||||
<li><span class="tag new">new</span>Subtasks ordered so a user starts from the commit that adds its contract; one writer per file; one placement of the siblings for every agent.</li>
|
||||
<li>Into each issue: every file and test it touches (file:line), how it is checked and delivered, a group's merge order.</li>
|
||||
<li><span class="tag new">new</span>Every other reader and writer of each value the change touches, each with a criterion.</li>
|
||||
<li>Acceptance criteria: given / when / then, each naming its test level and the design line it comes from.</li>
|
||||
<li>Every part the change reaches is an issue of the milestone; their order is the assigner's rules', never a cut.</li>
|
||||
<li>Fixtures copied from what the other side really writes, never from memory.</li>
|
||||
<li>An issue sized over an hour is split before work starts.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3>Step 1 · the fixing agent's brief</h3>
|
||||
<ul style="margin:6px 0;padding-left:20px;font-size:14px">
|
||||
<li><span class="tag new">new</span>Build each design line as written and nothing it does not say; every commit names its lines (<code>Builds: design NN M §n</code>).</li>
|
||||
<li><span class="tag new">new</span>A question the design does not answer stops that subtask and goes to the lead for the operator; never guessed or built around.</li>
|
||||
<li>One commit per subtask, naming its issue; tests cite their criterion (<code>// 1217/AC3</code>).</li>
|
||||
<li><span class="tag new">new</span>A test fails first on what the code does. A compile error is no failure: take out the change's effect and see an assertion fail.</li>
|
||||
<li>The whole unit suite before every commit; each test well under a second.</li>
|
||||
<li><span class="tag new">new</span>Integration tests compiled whole; once per head reported, those of the touched packages and their importers run against an embedded bus (by name where the importers are many); never a whole module's run.</li>
|
||||
<li>Never push, open or merge; never move an issue; never ssh.</li>
|
||||
<li>Code that differs from the build map: stop that subtask and report, file:line.</li>
|
||||
<li><span class="tag new">new</span>Start a subtask only with room to finish it; stop between subtasks, never inside one.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ TESTS ============ -->
|
||||
<h2 id="tests">When tests run</h2>
|
||||
<p class="lead">Two levels. Unit tests run all the time on the machine; integration tests (a bus, a store or a process started) are compiled there whole, and once per head reported only those of the touched packages and their importers run there, against an embedded bus, never a whole module's or level's: the checker's one merge check, asked by the one push after the review, runs them as the gate. There is no try (ADR 0548).</p>
|
||||
<div class="box scroll">
|
||||
<svg viewBox="0 0 1080 170" role="img" aria-label="Test levels over a feature's timeline">
|
||||
<defs><marker id="ar3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker></defs>
|
||||
<path class="edge" d="M120,140 H1060" marker-end="url(#ar3)"/>
|
||||
<text x="120" y="162" class="mute">develop</text><text x="470" y="162" class="mute">end of step 1</text><text x="640" y="162" class="mute">review</text><text x="800" y="162" class="mute">step 3</text><text x="960" y="162" class="mute">merge</text>
|
||||
<text x="10" y="44" class="t">Unit</text><text x="10" y="60" class="mute">on the machine</text>
|
||||
<g fill="var(--build)">
|
||||
<rect x="120" y="36" width="700" height="16" rx="8" opacity=".25"/>
|
||||
<circle cx="160" cy="44" r="6"/><circle cx="220" cy="44" r="6"/><circle cx="290" cy="44" r="6"/><circle cx="350" cy="44" r="6"/><circle cx="420" cy="44" r="6"/><circle cx="600" cy="44" r="6"/><circle cx="680" cy="44" r="6"/>
|
||||
</g>
|
||||
<text x="830" y="42" class="mute">whole suite before every commit</text><text x="830" y="56" class="mute">each test under a second</text>
|
||||
<text x="10" y="104" class="t">Integration</text><text x="10" y="120" class="mute">touched packages only</text>
|
||||
<g>
|
||||
<rect x="120" y="96" width="340" height="16" rx="8" fill="var(--mute)" opacity=".18"/>
|
||||
<text x="290" y="108" text-anchor="middle" class="mute">compiled whole; touched packages run</text>
|
||||
<rect x="770" y="90" width="120" height="28" rx="6" fill="none" stroke="var(--mesh)" stroke-width="2" stroke-dasharray="5 4"/><text x="830" y="109" text-anchor="middle" class="mute">push once</text>
|
||||
<rect x="920" y="90" width="120" height="28" rx="6" fill="var(--mesh)"/><text x="980" y="109" text-anchor="middle" style="fill:var(--bg)" class="t">merge check</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- ============ REVIEW LOOP ============ -->
|
||||
<h2 id="review">The review loop</h2>
|
||||
<div class="two">
|
||||
<div class="box scroll">
|
||||
<svg viewBox="0 0 520 300" role="img" aria-label="The review loop: at most three rounds">
|
||||
<defs><marker id="ar4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker></defs>
|
||||
<rect x="20" y="20" width="150" height="54" rx="8" fill="var(--card)" stroke="var(--idea)" stroke-width="2"/>
|
||||
<text x="95" y="44" text-anchor="middle" class="t">Round n of ≤ 3</text><text x="95" y="62" text-anchor="middle" class="mute">code + criteria</text>
|
||||
<polygon points="300,20 380,47 300,74 220,47" fill="var(--card)" stroke="var(--mute)"/>
|
||||
<text x="300" y="51" text-anchor="middle" class="t">Wrong?</text>
|
||||
<rect x="400" y="20" width="110" height="54" rx="8" fill="var(--okbg)" stroke="var(--ok)" stroke-width="2"/>
|
||||
<text x="455" y="44" text-anchor="middle" class="t">Approve</text><text x="455" y="62" text-anchor="middle" class="mute">Reviewed-by line</text>
|
||||
<rect x="210" y="130" width="180" height="54" rx="8" fill="var(--card)" stroke="var(--build)" stroke-width="2"/>
|
||||
<text x="300" y="154" text-anchor="middle" class="t">Same fixing agents fix</text><text x="300" y="172" text-anchor="middle" class="mute">answer per finding</text>
|
||||
<rect x="20" y="130" width="160" height="54" rx="8" fill="var(--soft)" stroke="var(--side)"/>
|
||||
<text x="100" y="154" text-anchor="middle" class="t">issue-tracker.open</text><text x="100" y="172" text-anchor="middle" class="mute">anything beyond the issue</text>
|
||||
<rect x="300" y="236" width="210" height="54" rx="8" fill="var(--warnbg)" stroke="var(--warn)" stroke-width="2"/>
|
||||
<text x="405" y="260" text-anchor="middle" class="t">After round 3: the operator</text><text x="405" y="278" text-anchor="middle" class="mute">what is left, with a recommendation</text>
|
||||
<path class="edge" d="M170,47 H218" marker-end="url(#ar4)"/>
|
||||
<path class="edge" d="M380,47 H398" marker-end="url(#ar4)"/><text x="384" y="38" class="mute">no</text>
|
||||
<path class="edge" d="M300,74 V128" marker-end="url(#ar4)"/><text x="306" y="104" class="mute">yes</text>
|
||||
<path class="edge" d="M210,157 C140,157 95,120 95,76" marker-end="url(#ar4)"/><text x="120" y="112" class="mute">n+1</text>
|
||||
<path class="edge dash" d="M250,66 C200,100 170,120 150,128" marker-end="url(#ar4)"/><text x="178" y="96" class="mute">hardening,</text><text x="178" y="108" class="mute">a nicer form</text>
|
||||
<path class="edge dash" d="M360,184 C380,210 400,220 405,234" marker-end="url(#ar4)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3>What a round may ask</h3>
|
||||
<p style="font-size:14px">Only what makes the change <b>wrong</b> against its issues, their acceptance criteria and its decisions. Anything more (hardening, a further case, a nicer form) becomes a new issue, and the round does not wait for it.</p>
|
||||
<h3 style="margin-top:12px">What every round reads</h3>
|
||||
<ul style="margin:6px 0;padding-left:20px;font-size:14px">
|
||||
<li>Every criterion against its test: a test at its level that would fail without the change.</li>
|
||||
<li>Every new integration test traced through the code by reading it: a local run of the touched packages shows only that it passed.</li>
|
||||
<li>That each integration test enters by the path the live trigger takes.</li>
|
||||
<li>Where each piece of state lives and how each seam travels: anything but bus state, a stream or a seat's verb is a finding, even if a record put it there.</li>
|
||||
<li>That no repository holds this mesh's setup (node names, placement, settings): repositories are mesh-agnostic.</li>
|
||||
</ul>
|
||||
<div class="note" style="margin-top:10px">Why reviews took more than one round (R3b, 2026-10-19): a test that "failed" only by not compiling, a reader of the changed record the survey missed, and bus tests judged by reasoning before they ran. ADR 0548 and playbook 07 step 2 item 4 change the last: bus tests are judged by reading them against the code, and the one check runs them after the review.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ ISSUE LIFE ============ -->
|
||||
<h2 id="issues">An issue's life (playbook 03)</h2>
|
||||
<p class="lead">The issue tracker (seat <code>issue-tracker</code>) holds every issue's number, status, owner, fix and links. Its <code>move</code> refuses a wrong move at the write.</p>
|
||||
<div class="box scroll">
|
||||
<svg viewBox="0 0 1080 230" role="img" aria-label="Issue statuses and the moves between them">
|
||||
<defs><marker id="ar5" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker></defs>
|
||||
<g>
|
||||
<rect x="20" y="70" width="120" height="50" rx="25" fill="var(--card)" stroke="var(--sym)" stroke-width="2"/><text x="80" y="100" text-anchor="middle" class="t">open</text>
|
||||
<rect x="220" y="70" width="140" height="50" rx="25" fill="var(--card)" stroke="var(--warn)" stroke-width="2"/><text x="290" y="100" text-anchor="middle" class="t">diagnosing</text>
|
||||
<rect x="440" y="70" width="120" height="50" rx="25" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/><text x="500" y="100" text-anchor="middle" class="t">located</text>
|
||||
<rect x="640" y="70" width="120" height="50" rx="25" fill="var(--card)" stroke="var(--build)" stroke-width="2"/><text x="700" y="100" text-anchor="middle" class="t">fixing</text>
|
||||
<rect x="860" y="70" width="140" height="50" rx="25" fill="var(--okbg)" stroke="var(--ok)" stroke-width="2.5"/><text x="930" y="100" text-anchor="middle" class="t">resolved</text>
|
||||
<rect x="440" y="170" width="120" height="44" rx="22" fill="var(--soft)" stroke="var(--mute)"/><text x="500" y="197" text-anchor="middle" class="t">wontfix</text>
|
||||
</g>
|
||||
<path class="edge" d="M140,95 H218" marker-end="url(#ar5)"/><text x="146" y="86" class="mute">by</text>
|
||||
<path class="edge" d="M360,95 H438" marker-end="url(#ar5)"/><text x="366" y="86" class="mute">located-in</text>
|
||||
<path class="edge" d="M560,95 H638" marker-end="url(#ar5)"/><text x="590" y="86" class="mute">by</text>
|
||||
<path class="edge" d="M760,95 H858" marker-end="url(#ar5)"/><text x="770" y="138" class="mute">fixed-by, delivered,</text><text x="770" y="152" class="mute">proven · core: replay</text>
|
||||
<path class="edge dash" d="M140,105 C300,150 520,150 640,108" marker-end="url(#ar5)"/><text x="300" y="150" class="mute">straight to fixing, with by</text>
|
||||
<path class="edge dash" d="M500,120 V168" marker-end="url(#ar5)"/><text x="508" y="150" class="mute">reason</text>
|
||||
<path class="edge dash" d="M700,70 C700,30 600,30 590,30 C400,30 120,30 90,68" marker-end="url(#ar5)"/><text x="320" y="24" class="mute">stopping: back to open, with a note of branch, last commit and what is left</text>
|
||||
</svg>
|
||||
<div class="legend"><span>A worker idle for a day may be taken over without a reason; otherwise <code>take-over</code> with a reason. An issue about a condition is linked to it, so the alert names the issue.</span></div>
|
||||
</div>
|
||||
|
||||
<!-- ============ WHERE THINGS LIVE ============ -->
|
||||
<h2 id="where">Where things live</h2>
|
||||
<div class="box scroll">
|
||||
<table>
|
||||
<tr><th>What</th><th>Lives in</th><th>Written with</th><th>Never</th></tr>
|
||||
<tr><td>Decisions (ADRs) and research efforts</td><td>the knowledge base (seat <code>knowledge-base</code>)</td><td><code>reserve</code>, then <code>propose</code> the record whole</td><td>a file in hq</td></tr>
|
||||
<tr><td>Issues: status, owner, fix, links</td><td>the issue tracker</td><td><code>open</code>, <code>move</code>, <code>note</code>, <code>link</code></td><td>a status key in a report</td></tr>
|
||||
<tr><td>An issue's narrative (design or governance only)</td><td>hq <code>04-ISSUES/NNN-…/</code></td><td>a pull request</td><td>its status</td></tr>
|
||||
<tr><td>Designs, as-is and to-be</td><td>hq <code>03-DESIGN/</code>, status in frontmatter</td><td>a pull request</td><td>the two layers mixed</td></tr>
|
||||
<tr><td>Playbooks, glossary, how-we-build</td><td>hq <code>00-META/</code></td><td>a pull request</td><td>a copy in a session's memory</td></tr>
|
||||
<tr><td>The rules every session keeps</td><td>instruction sections (module <code>claude-code</code>)</td><td><code>claude_code_instructions_propose</code>, the operator's Approve</td><td>a hand-edited file</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ============ ALL PLAYBOOKS ============ -->
|
||||
<h2 id="all">All playbooks</h2>
|
||||
<p class="sub">Open a card for its steps, what it produces and how it is checked.</p>
|
||||
<div id="filters" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px"></div>
|
||||
<div class="grid" id="cards"></div>
|
||||
|
||||
<footer>Drawn from hq main f269e83 on 2026-10-19 and kept in hq beside the playbooks. The playbooks are the authority; this page is a picture of them, and a change to a playbook's steps updates it in the same pull request (not yet checked: issue 1222).</footer>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const P = [
|
||||
{n:"01",name:"Research",kind:"idea",c:"--idea",
|
||||
trig:"An idea worth investigating before it is committed to design. A symptom is not research.",
|
||||
who:"The session that heard the idea leads.",
|
||||
steps:["Read what is known: ask, get, the tracker's list.","Ask 3 to 5 setup questions, one at a time, each with a recommendation.","Check every option against the mission and how-we-build first.","Write the numbered requirements from the answers, each with its check, before any option.","Reserve a number (kind research), never pick one by hand.","Propose the effort: Overview, Requirements, Questions, Method, sections named by title, no file references.","Research; findings in the section Findings, one version per settled set.","Try at the cheapest level that answers the question.","Write the Domains: modules, seats, glossary words added or retired.","Write the Direction: decisions, rejected options, acceptance tests.","Ask the operator to confirm the direction, one point at a time.","Review the direction by architectural domain: security, seams, running qualities, health, events and logs, testing, upgrade, tools.","If the review asks changes: propose again, at most three times."],
|
||||
prod:"A research effort in the knowledge base, status active, every version kept. Nothing in hq.",
|
||||
next:"02",bounds:"Setup 30 min · proposing 15 min · each experiment 30 min · direction 30 min · review by architectural domain 20 min · report every 10 min",
|
||||
prev:["Research that measured nothing","A recommendation the operator must argue back","Efforts copied into hq"],
|
||||
check:"propose refuses rule breaks; the knowledge base reviews; the records test on every PR."},
|
||||
{n:"02",name:"Graduation & design change",kind:"idea",c:"--dec",
|
||||
trig:"A research effort concludes, or an existing design must change.",
|
||||
who:"The lead, on the larger model; the decision is recorded before the design moves.",
|
||||
steps:["Check the conclusion against mission, context and effect.","Reserve a number per question the effort answers.","Propose one record per question: its options and the one chosen; design choices go in the design.","Pre-check, then propose siblings together.","On changes: revise whole; after three rounds fix findings yourself.","A finding that would change what was decided: ask the operator.","needs-operator: wait for the operator's Approve.","Amend the design, or create a to-be design with status designed: a main file and a part per milestone, leaving the builder no choice. (new)","A design question: the operator answers, written as a design line. (new)","Add the decisions, updated:, and the effort's glossary words.","Review the design by architectural domain before it is merged.","Hand the design to the breakdown (04).","Close the effort: graduated with became links, or abandoned.","When it ships: write the as-is, set the to-be implemented."],
|
||||
prod:"Decision records; a to-be design with frontmatter; glossary entries; later the as-is.",
|
||||
next:"04",bounds:"One sitting of about an hour",
|
||||
prev:["A decision rewritten silently","A deferral nothing tracks","A design amended without a decision"],
|
||||
check:"propose refuses; the records and index tests; set-wide rules raise conditions."},
|
||||
{n:"03",name:"Issues",kind:"symptom",c:"--sym",
|
||||
trig:"Something is wrong: an unenforced rule, a stated behaviour that does not happen, a silent failure.",
|
||||
who:"Anyone opens; a worker claims it by moving it.",
|
||||
steps:["Open it in the tracker: title, report; take the number it gives.","Tracker unreachable: take no number, say so.","A design or governance issue: a folder in hq with the report.","About a condition: link the condition's key.","Move to diagnosing with by, before investigating.","Note the trail; a folder issue also gets a dated diagnosis.","Move to located with located-in, a registered owner.","Move to fixing with by, before fixing.","Name fixed-by at the merge; stay fixing until proven live.","Move to resolved; a core issue gives replay or replay-none.","A design gap: run 02 and give amended-design.","Not going to be fixed: wontfix with a reason."],
|
||||
prod:"A tracker record with notes and links; for a design issue, its narrative folder in hq.",
|
||||
next:"02 · 07 · 08",bounds:"A worker idle for a day is stale",
|
||||
prev:["Numbers that collide","Two agents starting the same work","Status held in two places"],
|
||||
check:"The tracker's move refuses at the write; the cycle test checks the folders."},
|
||||
{n:"04",name:"Work breakdown & handoff",kind:"idea",c:"--dec",
|
||||
trig:"A to-be design is settled and its building is about to start.",
|
||||
who:"The lead who will run the feature, on the larger model.",
|
||||
steps:["Confirm status designed; each decision traces to a record.","An open question remains: back to research.","Set code: to the owning repositories.","Read the as-is; write it first if it does not say what is replaced.","Write the Milestones: outcome, acceptance tests, repositories.","Cut milestones by functionality, never by an order between parts; fold any not usable alone.","Give each milestone its part: numbered design lines, at most 60 KB; the main file at most 80 KB. (new)","Ask the operator to confirm the milestones.","Open one issue per repository part, linked depends-on.","Write the issue numbers into the design; flip to in-progress.","Hand each milestone to 07, one at a time, one design at a time."],
|
||||
prod:"Milestones in the design; issues per repository part; the design in-progress.",
|
||||
next:"07",bounds:"One sitting · one design built at a time · a milestone is one release",
|
||||
prev:["Building without a breakdown","Building against an undocumented as-is","Deferrals with no owning issue"],
|
||||
check:"Read by the lead and the reviewer; a check is tracked by issue 845."},
|
||||
{n:"05",name:"Constitution sync",kind:"side",c:"--side",
|
||||
trig:"how-we-build.md changed a rule the mesh enforces at runtime.",
|
||||
who:"Whoever made the change.",
|
||||
steps:["Edit how-we-build; the reasoning stays there.","Record the change as a decision (02).","Publish the derived page under its slug.","Keep the section numbering stable.","Read the page back; verify the change is there.","Note the sync in the record's consequences."],
|
||||
prod:"A decision record; the derived constitution page injected into design sessions.",
|
||||
next:"02",bounds:"no time bound",
|
||||
prev:["Drift between the reasoned and the enforced copy","A publish that said success and did nothing","Edits to the derived page that vanish"],
|
||||
check:"The read-back in step 5."},
|
||||
{n:"06",name:"Writing a module",kind:"side",c:"--side",
|
||||
trig:"Something that runs today must run on the mesh.",
|
||||
who:"Whoever ports or writes it.",
|
||||
steps:["Read what runs: compose file, environment, where data sits.","Name what it provides and requires.","Declare capabilities for machine facts.","Write resources in the order they must happen.","Every secret in a file, never in env.","Pin every image by digest.","New: generate the credential; adopted: accept it.","Prove it against the real software: auth, refusal, restart."],
|
||||
prod:"A module declaration, optionally a provisioner, and its proof.",
|
||||
next:"07",bounds:"no time bound",
|
||||
prev:["A service running with no password","A new password locking out adopted data","A test that cannot fail"],
|
||||
check:"Example tests refuse the env-file mistake; review."},
|
||||
{n:"07",name:"Features",kind:"build",c:"--build",
|
||||
trig:"Work that changes code across issues or repositories, too large for a small fix.",
|
||||
who:"A lead (larger model), one fixing agent per repository (smaller), one reviewer (larger).",
|
||||
steps:["Prepare: name the branch, make the worktrees, read the milestone part and the design's main file whole. (new)","Every agent is one of hq's definitions (mesh-preparer, mesh-developer, mesh-code-reviewer, mesh-watcher); the lead writes only the handover. (new)","The prepare's only output: a build map per repository, each row a design line and where it lands, and the design questions. No rulings. (new)","Design complete: the operator answers every question (the design writer one of internal form only), each written into the milestone part; then develop. (new)","Read what exists today from the code and the live mesh: every effect of what is removed or moved, every reader of each, what happens to each and its test. No code before it is complete; the review refuses work without it. (new)","The build map opens with what exists today; subtasks ordered contract first, one writer per file.","Write the findings and acceptance criteria into each issue.","Survey every other reader and writer of each changed value. (new)","Answer three questions in the milestone file: who deletes each new key and stream, how the switch-over is tried on one node first, what the one check does not cover. The review checks them. (new)","List every part the change reaches; copy fixtures from the real writer.","Develop: one agent per repository, one commit per subtask, each citing its design lines; a question stops only its subtask. (new)","A test fails first on behaviour, not on compiling. (new)","Bring each branch up to the trunk; nothing is pushed, no try runs. (new)","Review all of it against the design lines, code no line asks for a finding, reading the integration tests against the code; at most 3 rounds; the report answers the state-and-seams and mesh-agnostic questions. (new)","A group pins the group's own mesh-sdk head in every member. (new)","Push the reviewed heads once and read each forge head against the reviewed commit; no draft pull request, no fix round on an open one. (new)","Open one pull request per repository, together, with Reviewed-by and Pushed-by; the one check runs. A failed check is read, fixed, reviewed, pushed once more. (new)","The operator's Approve on Telegram; the group merges in one pass.","Follow the delivery in the status view (moving, settled, stuck, delivered) to done; resolve the issues; clean up. (new)","When the builder cannot deliver its own fix: revert, a build by a session's call, a registration at the terminal, copies kept outside the store; a broken controller is put back by a want the operator writes, a hand build by mesh-hand-compose and a want. (new)"],
|
||||
prod:"One pull request per repository, one source revision delivered, resolved issues, no branch or worktree left.",
|
||||
next:"02 (when it ships)",bounds:"Prepare 15 min · 30 min per issue · review 15 min, ≤ 3 rounds · one check, pushed once",
|
||||
prev:["Pull requests as scratchpads","Context bought again and again","A change built on what was remembered, not on what runs","Review rounds spent on what a run would show","A node agent's bus reads, proven only by review"],
|
||||
check:"The checker's one status, mesh/check; the review record; the tracker's move."},
|
||||
{n:"08",name:"Small fixes",kind:"build",c:"--build",
|
||||
trig:"One issue in one repository, about an hour of work.",
|
||||
who:"The session, with one reviewer on the larger model.",
|
||||
steps:["Read the issue, the design lines it builds and the code; a design question goes to the operator (one of internal form only to the design writer) and into the design before code. (new)","Read what exists in the fix's area today, briefly: every reader of what the fix changes. (new)","Write the acceptance criteria.","Move the issue to fixing.","Build on fix/<issue>-<slug> in its own worktree.","A test that fails today first, on behaviour. (new)","Nothing is pushed and no try runs before the review. (new)","One review of the local commit.","Push the reviewed head once; only then open the pull request. (new)","The operator's Approve; the merge; the delivery, followed in the status view. (new)","Prove the fix live and resolve the same hour.","During a milestone: ride in its delivery group, unless broken for the operator now. (new)"],
|
||||
prod:"One pull request, one delivery, a resolved issue.",
|
||||
next:"—",bounds:"About an hour · one review, one push, one check",
|
||||
prev:["A fix reviewed after its pull request opened","A long part run twice","A fix waiting on someone else's delivery","Ten SDK merges and eight controller merges in a day, one per fix"],
|
||||
check:"The checker's status; the review record; the tracker's move; one delivery group and one source revision per milestone."},
|
||||
{n:"09",name:"Cleanup",kind:"side",c:"--side",
|
||||
trig:"The operator asks, or merged branches, stale pull requests and old worktrees pile up.",
|
||||
who:"A session on the operator's word; the operator decides every deletion.",
|
||||
steps:["Take stock, read-only.","Write one report: item, class, evidence.","Classify: safe, likely safe, kept, unknown.","Report stale pull requests; never close them.","Ask: delete the safe items as listed?","Delete only the approved list; never force.","Report what was cleaned, kept and unknown."],
|
||||
prod:"A stock report, the approved deletions, issues for missing tools.",
|
||||
next:"03",bounds:"A worktree is safe only if untouched for 2 h · a PR is stale after 3 days",
|
||||
prev:["Deleting unfinished work","Deletion by name or age alone","Missing tools left as session notes"],
|
||||
check:"The report names the evidence for each deletion."},
|
||||
{n:"10",name:"Visual design",kind:"idea",c:"--idea",
|
||||
trig:"A page the operator will use is about to be designed or rebuilt, or the operator judges one unusable.",
|
||||
who:"The design's lead; the operator picks.",
|
||||
steps:["Gather what the page must carry: the as-is, the real data's shape, the operator's words, how others do it.","Draw three to five mockups with fixed, invented data, phone and desktop. (new)","Show them; ask one question per round. (new)","Make the pick the reference, with the operator's own details replaced. (new)","Write what is new as decisions, then the design and its milestones.","Hold the build to the reference; done when the operator uses it."],
|
||||
prod:"The reference mockup beside the design, its decisions, an acceptance test of live use.",
|
||||
next:"02, then 04",bounds:"A round 1 h · 3 rounds without a pick: ask what is missing",
|
||||
prev:["A page judged unusable the day it ships","Rebuilding after delivery"],
|
||||
check:"The design links its mockup; the review shows the build beside it; the operator's use is the last test."}
|
||||
];
|
||||
const kinds={all:"All",idea:"From an idea",symptom:"From a symptom",build:"Code changes",side:"Alongside"};
|
||||
let current="all";
|
||||
function esc(s){return s.replace(/&/g,"&").replace(/</g,"<")}
|
||||
function render(){
|
||||
const g=document.getElementById("cards"); g.innerHTML="";
|
||||
P.filter(p=>current==="all"||p.kind===current).forEach(p=>{
|
||||
const d=document.createElement("article"); d.className="pb"; d.style.setProperty("--c",`var(${p.c})`);
|
||||
d.innerHTML=`<div class="num">PLAYBOOK ${p.n}</div><h3>${esc(p.name)}</h3><p class="trig">${esc(p.trig)}</p>
|
||||
<div>${p.bounds.split(" · ").map(b=>`<span class="tag">${esc(b)}</span>`).join("")}</div>
|
||||
<details><summary>Steps (${p.steps.length})</summary><ol>${p.steps.map(s=>`<li>${esc(s).replace("(new)",'<span class="tag new">new</span>')}</li>`).join("")}</ol></details>
|
||||
<details><summary>Produces, prevents, checked by</summary><div class="kv">
|
||||
<b>Who</b><span>${esc(p.who)}</span><b>Produces</b><span>${esc(p.prod)}</span>
|
||||
<b>Then</b><span>${esc(p.next)}</span><b>Prevents</b><span>${p.prev.map(esc).join("; ")}</span>
|
||||
<b>Checked by</b><span>${esc(p.check)}</span></div></details>`;
|
||||
g.appendChild(d);
|
||||
});
|
||||
}
|
||||
const f=document.getElementById("filters");
|
||||
Object.entries(kinds).forEach(([k,v])=>{const b=document.createElement("button");b.type="button";b.className="chip"+(k===current?" on":"");b.textContent=v;
|
||||
b.onclick=()=>{current=k;[...f.children].forEach(x=>x.classList.remove("on"));b.classList.add("on");render()};f.appendChild(b)});
|
||||
render();
|
||||
const root=document.documentElement;
|
||||
try{const t=localStorage.getItem("pb-theme");if(t)root.dataset.theme=t}catch(e){}
|
||||
document.getElementById("theme").onclick=()=>{const dark=root.dataset.theme?root.dataset.theme==="dark":matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
root.dataset.theme=dark?"light":"dark";try{localStorage.setItem("pb-theme",root.dataset.theme)}catch(e){}};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+3
-2
@@ -14,7 +14,7 @@ and a forge address is an operational detail (see [`README`](../README.md)).
|
||||
|
||||
| Repository | Owns |
|
||||
|---|---|
|
||||
| `hal` | The monorepo — the node runtime, the module catalogue, the delivery machinery, and the bootstrap scripts. Every core module lives here. |
|
||||
| `hal` | The monorepo — the node runtime, the module catalog, the delivery machinery, and the bootstrap scripts. Every core module lives here. |
|
||||
| `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped (ADR 0019); the mesh is its first product. The source of truth for *why*. Carries no implementation. |
|
||||
| *(one per application)* | Every standalone application, site or side-project gets its own repository, with `module.yml` at the root. Registered with the mesh as a build source; built and deployed by the same pipeline as anything in the monorepo. |
|
||||
| `migration` | **Private.** The record of one installation replacing the predecessor mesh with this one: the runbook, a dated log of every step and what it cost, the per-service data procedures, the readiness checks, and the scripts. Private because it is the opposite of this repository in every way that matters — it names machines, addresses, ports and paths, because a procedure that cannot be followed is not one. Where hq asks *what did we decide and why*, that repository answers *what happened on the machines, in what order, and what to do next*. Its `HANDOFF.md` is where somebody picking the work up starts. |
|
||||
@@ -36,6 +36,7 @@ one-for-one — `mesh-catalog`, `mesh-sdk` and `mesh-tools` exist where the tabl
|
||||
| `mesh-surfaces` | 3 | tools, web, cli |
|
||||
| `mesh-sdk` | — | the stable spine modules build against — the tool-serving SDK harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile (ADR 0039). |
|
||||
| `mesh-lab` | — | **exists.** The lab — scenario lifecycle, networking, placement. Ships to nobody; runs on a workstation. |
|
||||
| `mesh-delivery` | — | the delivery domain: the parts that build, check and deliver a change by watching and writing keys on the bus, nine modules each whole in it, prefixed `mesh-delivery-` (ADR 0429). Made by the forge module from its config (ADR 0428). |
|
||||
|
||||
Tier 4's shape is open, and deliberately so: see ADR 0019 and
|
||||
research 005.
|
||||
@@ -47,7 +48,7 @@ Named by role, because the layout is itself part of the as-is design — see
|
||||
|
||||
| Area | Holds |
|
||||
|---|---|
|
||||
| Module catalogue | One directory per module, each with a manifest. Core modules sit under the mesh's own namespace; everything else at the top level. |
|
||||
| Module catalog | One directory per module, each with a manifest. Core modules sit under the mesh's own namespace; everything else at the top level. |
|
||||
| Node runtime | The daemon and interactive runtime that every node runs. |
|
||||
| Bootstrap scripts | First-node initialisation, joining an existing mesh, and node rescue. |
|
||||
| Shared library | The SDK every module builds against. |
|
||||
|
||||
@@ -14,7 +14,7 @@ the commit the knowledge base's records name as their source (issue 426).
|
||||
|
||||
This folder keeps one thing: the six topics of the reading order, in the order somebody would learn the
|
||||
system. Each record names its own topic, and the knowledge base reads these lines through the records module
|
||||
and refuses a record whose topic is not one of them. `index.py` checks that they are written.
|
||||
and refuses a record whose topic is not one of them. the index test (`00-META/checks/`) checks that they are written.
|
||||
|
||||
1. **What the mesh is** — `topic: the mesh`. What it is for, and the parts it is made of.
|
||||
2. **Its tiers, from the bottom up** — `topic: the tiers`. The foundation, the node and the controller, and
|
||||
|
||||
@@ -11,7 +11,7 @@ decisions:
|
||||
|
||||
# The mesh as it stands
|
||||
|
||||
A set of machines, each running the same runtime, each loading only the parts of the catalogue
|
||||
A set of machines, each running the same runtime, each loading only the parts of the catalog
|
||||
it has been assigned. They hold no shared filesystem and make no direct connections to one
|
||||
another. What makes them a mesh is a database that knows what should run where, and a message
|
||||
broker that carries everything between them.
|
||||
|
||||
@@ -105,9 +105,9 @@ the resource rather than on the node that built the module.
|
||||
## A documented rule with no enforcement
|
||||
|
||||
Every module exposing capabilities is documented as required to declare the mesh's core runtime
|
||||
as a dependency. **Zero of the catalogue's modules do.**
|
||||
as a dependency. **Zero of the catalog's modules do.**
|
||||
|
||||
This is recorded here rather than quietly corrected, because it is the clearest instance of the
|
||||
rule this repository states about itself: a rule whose enforcement does not exist is
|
||||
indistinguishable from a wrong one, and costs more, because people believe it. Whether the rule
|
||||
or the catalogue is wrong has not been decided.
|
||||
or the catalog is wrong has not been decided.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [mesh-controller, mesh-catalog modules/mesh-delivery, mesh-catalog modules/gitea, mesh-catalog modules/build-agent]
|
||||
updated: 2026-10-17
|
||||
code: [mesh-delivery, mesh-catalog modules/mesh-delivery-gate, mesh-catalog modules/gitea, mesh-catalog modules/mesh-build-go, mesh-catalog modules/messenger, mesh-catalog modules/mesh-issues, mesh-controller, mesh-host, mesh-sdk]
|
||||
updated: 2026-10-26
|
||||
decisions:
|
||||
- "ADR 0239"
|
||||
- "ADR 0267"
|
||||
- "ADR 0276"
|
||||
- "ADR 0282"
|
||||
- "ADR 0289"
|
||||
- "ADR 0304"
|
||||
@@ -17,74 +16,120 @@ decisions:
|
||||
- "ADR 0382"
|
||||
- "ADR 0395"
|
||||
- "ADR 0398"
|
||||
- "ADR 0429"
|
||||
- "ADR 0524"
|
||||
- "ADR 0525"
|
||||
- "ADR 0527"
|
||||
- "ADR 0528"
|
||||
- "ADR 0529"
|
||||
- "ADR 0539"
|
||||
- "ADR 0546"
|
||||
- "ADR 0547"
|
||||
---
|
||||
|
||||
# Delivery — from a merge to the nodes, as it runs
|
||||
|
||||
What runs on the mesh's main branches on 2026-10-16, read from the code and the live record by research 055
|
||||
(findings 1 to 12). It is what [to-be 47](../01-to-be/47-delivery-from-commit-to-delivered.md) replaces, milestone
|
||||
by milestone. Until 2026-10-16 this document described the predecessor's pipeline, which no longer runs.
|
||||
What runs on 2026-10-26, after the delivery of release 6 (milestone R6 of
|
||||
[to-be 47](../01-to-be/47-delivery-from-commit-to-delivered.md), ADR 0539), read from the code on each repository's
|
||||
main (`mesh-delivery`, the gate in `mesh-catalog`, the node agent in `mesh-host`, `mesh-controller`) and from the live
|
||||
mesh: the controller's seat verbs, `mesh-controller.modules`, the gate's `table`, and the view's `assigner_wants` and
|
||||
`node_haves`. Until 2026-10-26 this document described release 5, in which the controller still made a delivery plan
|
||||
for each merge, ordered it in tiers and sent whole declarations to the machines; none of that runs any more.
|
||||
|
||||
## Who does what
|
||||
|
||||
- **mesh-delivery** owns deliveries and delivery groups (ADR 0239): one delivery per commit, a group per branch
|
||||
name shared across repositories. It records each transition in its state on the bus (key-value buckets
|
||||
`deliveries` and `groups`), marks pull requests, asks the controller to deliver, and merges a pull request marked
|
||||
ready at its head once both required checks pass against the trunk's head (ADR 0364) and the operator approved
|
||||
the merge into main on a channel that proves who answered (ADR 0398 §3).
|
||||
- **The controller** plans, orders, sends and judges. It computes a delivery plan for a repository and the files a
|
||||
merge changes, moves only modules whose build source changed with their build dependents (ADR 0267), and
|
||||
reuses a merge check's build of the same reuse key (ADR 0350).
|
||||
- **The builders** (holders of the build seat) take build requests and checks from a work-queue stream on the bus
|
||||
and build inside a toolchain image another module provides, with the network off; on 2026-10-13 no node had Go
|
||||
or npm installed (issue 626).
|
||||
- **The forge module** publishes every forge event on the bus through the forge's system webhook since 2026-10-14
|
||||
(issue 729), and still polls the forge every 30 s, or every 300 s once it hears events. It keeps files of what it
|
||||
announced so it does not announce twice.
|
||||
- **The parts of `mesh-delivery`** are modules of one repository, each a process with its own bus credential that
|
||||
watches keys and writes keys (ADR 0429). Running now: the forge bridge, the indexer, the checker, the teller and the
|
||||
watchman on the control-node; the builder and the status view (`mesh-delivery-view`) on the workstation; and the
|
||||
assigner on the home server. The gate (`mesh-delivery-gate`, holding the seat `mesh-delivery`) is a module of
|
||||
`mesh-catalog` and runs on the control-node.
|
||||
- **The forge bridge** turns the forge's events into stream messages and is the only part that calls the forge; the
|
||||
forge module (`gitea`) still announces them through the forge's system webhook.
|
||||
- **The indexer** writes each module's fingerprint at the merge preview and at a trunk merge; **the builder** claims a
|
||||
build once per fingerprint and orders it as an ask on the stream of the artifact's language.
|
||||
- **The build modules** (`mesh-build-go` on all four machines, `mesh-build-node` on three, `mesh-build-image` on one;
|
||||
ADR 0525) hold the node seats `node-build-go`, `node-build-node` and `node-build-image` and pull asks from the bus's
|
||||
work-queue streams (deliveries first, then checks, then re-checks) while their node has a free slot of build
|
||||
capacity.
|
||||
- **The checker** runs the merge check and writes the one status `mesh/check`, and serves a **try** on a pushed
|
||||
branch. The forge requires no status on any repository (ADR 0534), and no repository holds a `merge-check.sh`
|
||||
(ADR 0529).
|
||||
- **The gate** keeps one key per pull request and the merge's states only: `proposed`, `ready`, `rejected`, `held`,
|
||||
`merged` and `closed`, read from its `table` verb. On the operator's Approve it merges a delivery group's pull
|
||||
requests in one pass and writes their merged heads as one revision of `source.main`. It sends nothing to a
|
||||
machine and asks the controller nothing.
|
||||
- **The controller** (`mesh-controller`, on the control-node) keeps registration, placement in its assignments
|
||||
bucket and every node's inputs, and composes: on events it writes each module's signed composition for each node at
|
||||
each fingerprint into the node's bucket `composed-<node>`, with its digest and its judgement of the move in
|
||||
`mesh-controller_composed`, and it issues a module's credential and grant when its build is done. It plans,
|
||||
orders and sends nothing: of its seat's 65 verbs, 20 are refused by name with their replacement, among them
|
||||
`plans`, `plan`, `push`, `deliver`, `delivery-stop`, `delivery-order` and `delivery-walks` (read 2026-10-26 01:20).
|
||||
- **The assigner** writes each node's want of each module, `want.<node>.<module>`, naming a fingerprint and the
|
||||
digest of the one composition it allows, when its seven rules hold: the build is done, the fingerprint is not
|
||||
held, the node is the module's canary or the canary runs it healthy, at most `step` others ahead, every provider
|
||||
runs healthy, the grant exists and the composition exists. In the code the canary is the first of the module's
|
||||
nodes by name that is online (one ring), and rule 4 always holds, because the mesh keeps no `step`. It also runs
|
||||
the composition check the checker reads. Its differences bucket, from release 5's comparison with what the
|
||||
controller sent, is no longer written.
|
||||
- **Two ways around the rules, both by hand.** A **put** (ADR 0546), written by the assigner's tool `assigner_put`,
|
||||
puts one version of a module on one node: while a put owns a node's want of a module, from its ask until it ends,
|
||||
the rules pass over that want. And the operator's credential may write a want itself, with a composition the
|
||||
operator signed and wrote through mesh-sdk's `mesh-hand-compose` (ADR 0539 §10).
|
||||
- **The node agent**, a process of the node-engine (`mesh-host`) on every node, watches its node's wants and its
|
||||
bucket of compositions, and applies a module only when the want's fingerprint or digest differs from what runs,
|
||||
and only the composition whose digest the want names, after checking its signature. It asks its node's
|
||||
`node-backup` seat to back up a data module first and waits for the backup's end. It writes the have,
|
||||
`have.<node>.<module>` in `node-engine_have`, with the fingerprint, the composition's digest, the phase and the
|
||||
health. A failed apply is retried by the next change of the want or the composition; an apply that failed only
|
||||
because the bus did not answer for a moment is retried with a bounded backoff, and at once when the bus is back.
|
||||
- **The watchman** mirrors every want as a pulse owed by that node's agent until its have names the want's
|
||||
fingerprint, so a node that does not move shows as a stall.
|
||||
- **The status view** derives from `source.main`, the wants and the haves whether the source revision is
|
||||
delivered, and says the event `delivered` once per revision, and `moving`, `settled` and `stuck` once for a
|
||||
module on a node at one composition. The messenger holds the operator's notices on those events, and the issue
|
||||
tracker hears `delivered`.
|
||||
- **The grants part** (`mesh-grants`, on the control-node; ADR 0547) publishes each assignment's membership on the
|
||||
assignments stream when it differs from the one last published. The bus's user list, assertions and grants it
|
||||
composes only in its own state, compared with what the bus holds, and they take no effect; the controller still
|
||||
composes the user list the bus loads.
|
||||
|
||||
## From a merge to a rollout
|
||||
## From a merge to the nodes
|
||||
|
||||
- **The merge window.** The controller collects merges into an open batch; each merge restarts the window, which
|
||||
closes after 90 seconds without a merge or 10 minutes after the batch's first merge (ADR 0276). A lone merge is
|
||||
cut at once (ADR 0282 §2, as ADR 0304 §1 changed it). The batch and its merges are rows in the controller's
|
||||
store, cut by a timer every 5 s, with a catch-up timer every 5 minutes for merges no batch holds.
|
||||
- **The rollout.** A closed batch becomes one rollout, at most one commit per repository, tiered along the module
|
||||
graph. Rollouts run one at a time: a batch cut while a rollout is open is queued behind it, whatever it shares
|
||||
with it. Running unrelated rollouts side by side (ADR 0282 §3, ADR 0304 §2) is decided and not built (issue 525). A rollout's builds go to every machine at once, with no first-machine gate, and the rollout ends on
|
||||
the machines' reports or after fifteen minutes of silence (ADR 0360). A controller timer advances rollouts every
|
||||
30 s.
|
||||
- **A merge during a rollout.** Its place in the next batch is a store row the open rollout's holder writes. On
|
||||
2026-10-16, when another controller worked the plans, a merge was not recorded and waited until a person released
|
||||
it (issue 921).
|
||||
- **"Release branches".** A pull request into a branch other than a trunk is judged by the controller built from that
|
||||
branch (ADR 0358); feature 57 shipped about 37 packages that way over about 2.5 days and held the controller for
|
||||
55.7 h.
|
||||
|
||||
## Checks
|
||||
|
||||
- Every pull request's head gets `mesh/merge-gate` (the composed machines) and `mesh/repo-check` (the repository's
|
||||
`merge-check.sh`), each judged against one trunk commit (ADR 0323). A trunk move asks the check of every active,
|
||||
cleanly merging pull request again (ADR 0395).
|
||||
- A delivery group's members are each checked beside their siblings' heads, and the group also gets a composed
|
||||
check, `mesh/delivery-group`; each member keeps its own gate beside it (ADR 0382).
|
||||
- The queue's order (delivery builds first, then ready checks, then the rest) is kept by pausing the worker and
|
||||
deleting and republishing a request (issue 558); an old head's check is ended after asking the forge for the
|
||||
current head (issue 726).
|
||||
- mesh-delivery ticks every 10 s, reconciles with the controller every 30 s and polls the forge for ready marks
|
||||
every minute, although the forge module already publishes labeled, commented and synchronized events.
|
||||
- A pull request's head is built and checked as it comes in; one status, `mesh/check`, carries the verdict, and a
|
||||
delivery group has one verdict for every member. A move of the trunk asks the checks again, and a running check
|
||||
stops itself when its head or trunk commit moves.
|
||||
- The gate merges on the operator's Approve and moves the source revision in one write. Nothing collects merges or
|
||||
waits for others: the indexer writes the new fingerprints, the builder claims each new one once, the controller
|
||||
composes and grants on the done build, and the assigner writes each want once its rules hold, the canary first.
|
||||
- Each node applies its own wants one module at a time and writes its haves; the source revision is delivered when
|
||||
every want equals its module's fingerprint and every have equals its want, running and healthy. Read on
|
||||
2026-10-26 at 01:20: 303 wants, every one equal to its node's have in fingerprint and composition digest, and 12
|
||||
fingerprints held.
|
||||
- **hq** builds no module: the checker runs hq's Go tests (`go test ./...`) as its check (ADR 0527).
|
||||
|
||||
## Where the record of what runs lives
|
||||
|
||||
Builds and build requests, rollouts with their tiers, batches and merges, and assignments are rows in the
|
||||
controller's store; artifacts are blobs in the artifact store by digest (ADR 0251); packages are versions in the
|
||||
package registry. The controller writes three record mirrors to the bus: assignments, declared config and
|
||||
predefined keys; of the five ADR 0289 §7 decided, the nodes, rollouts, builds and settings mirrors are not in the
|
||||
code. An assignment names its module and node but not the build, artifact or rollout that put it there: which commit
|
||||
runs where, and which delivery brought it, takes three verbs and a join by hand.
|
||||
|
||||
No backup is taken before a delivery moves a module that holds data; backups run nightly and on demand (ADR 0214).
|
||||
Fingerprints, builds, verdicts, wants and haves are keys in the parts' buckets on the bus; compositions are in each
|
||||
node's `composed-<node>` bucket, readable only by that node, with a file over 64 KiB in the object store; builds'
|
||||
files are in the artifact store by digest; packages are versions in the package registry. The controller
|
||||
still records every build it takes in, in its builds bucket (`RecordBuild` in `cmd/mesh-controller/key_takein.go`); its
|
||||
assignments bucket keeps placement alone, and its rollouts bucket keeps the record of releases 1 to 5 read-only, and
|
||||
nothing writes it now.
|
||||
|
||||
## What is not covered
|
||||
|
||||
The integration tests run only in each repository's merge check; there are no end-to-end tests of delivery yet,
|
||||
and the lab is not used (the operator, 2026-10-16).
|
||||
Nothing checks the delivery budget: the over-budget condition and the gate's `times` went with release 6, and no
|
||||
reader of the view's `settled` and `delivered` events measures it yet (issue 1945, note 10). The bus's own move stays
|
||||
a person's step through the controller's `bus` verb (ADR 0539 §15). The checker's left-running keys of build modules
|
||||
that stopped (`checker_status`'s `left_running`) are listed and hold no place. There are no end-to-end tests of
|
||||
delivery, and the lab is not used (the operator, 2026-10-16).
|
||||
|
||||
## How it is checked
|
||||
|
||||
- `mesh-delivery-view.assigner_wants` and `mesh-delivery-view.node_haves`: each want beside its have; `indexer_modules`
|
||||
and `builder_builds` for the fingerprints and builds they name.
|
||||
- `mesh-delivery.table`: the gate's states and transitions; `mesh-delivery.deliveries` and `show` for one delivery.
|
||||
- `mesh-controller.seats` and `mesh_overview`: the controller's verbs, and which are refused by name.
|
||||
- The code: `mesh-delivery` (`modules/mesh-delivery-assigner/internal/assign`, `modules/mesh-delivery-view/internal/view`,
|
||||
`modules/mesh-delivery-watchman/internal/watch`, `modules/mesh-grants`), `mesh-host` (`internal/agent`,
|
||||
`cmd/mesh-host/nodeagent.go`), and the gate in `mesh-catalog` (`modules/mesh-delivery-gate`).
|
||||
|
||||
@@ -24,7 +24,7 @@ is what makes a node a participant — it is reachable whether or not anyone is
|
||||
protocol. Its capability surface is larger, because it includes stand-ins for every capability
|
||||
discovered on peers.
|
||||
|
||||
The two are the same code with the same catalogue. A capability is written once and is
|
||||
The two are the same code with the same catalog. A capability is written once and is
|
||||
available to both.
|
||||
|
||||
## Anatomy naming, and what it obscures
|
||||
@@ -52,7 +52,7 @@ For each assigned module the runtime, at startup:
|
||||
4. For a service module, ensures it is installed and running.
|
||||
|
||||
Installation is idempotent and does the unglamorous work: ensure the runtime directory,
|
||||
reconcile the link from the catalogue's definition into it, generate the environment, run any
|
||||
reconcile the link from the catalog's definition into it, generate the environment, run any
|
||||
outstanding local migrations, create data directories with the right ownership, then start the
|
||||
service under supervision.
|
||||
|
||||
@@ -85,7 +85,7 @@ import is one-time and not persisted.
|
||||
Three bootstrap scripts, and which one runs depends on the situation:
|
||||
|
||||
- **First node.** Nothing exists yet, so the script stands up the database the rest of the
|
||||
mesh reads from, publishes the catalogue, and starts the mesh. This resolves the
|
||||
mesh reads from, publishes the catalog, and starts the mesh. This resolves the
|
||||
circularity of a mesh whose source of truth is itself a provisioned module.
|
||||
- **Joining.** The node registers, takes its assignment from the database, and syncs.
|
||||
- **Rescue.** A node that cannot reach the mesh is brought back far enough to.
|
||||
|
||||
@@ -9,9 +9,9 @@ decisions:
|
||||
- "ADR 0009"
|
||||
---
|
||||
|
||||
# The catalogue, and what its shape says
|
||||
# The catalog, and what its shape says
|
||||
|
||||
The catalogue holds **124 modules**. Thirty-three belong to the mesh's own domain; the other
|
||||
The catalog holds **124 modules**. Thirty-three belong to the mesh's own domain; the other
|
||||
ninety-one run *on* the mesh rather than being *of* it
|
||||
(ADR 0001).
|
||||
|
||||
@@ -30,7 +30,7 @@ By namespace, and the namespace records origin rather than purpose:
|
||||
|
||||
## What the flat level actually contains
|
||||
|
||||
Grouped by what they are *for* — a grouping the catalogue itself does not express:
|
||||
Grouped by what they are *for* — a grouping the catalog itself does not express:
|
||||
|
||||
| Purpose | Roughly |
|
||||
|---|---|
|
||||
@@ -51,7 +51,7 @@ connectivity is made four times.
|
||||
|
||||
## What the shape records
|
||||
|
||||
**The catalogue's shape records what was installed, not what anything is for.** One module is
|
||||
**The catalog's shape records what was installed, not what anything is for.** One module is
|
||||
the unit of one piece of software, because that is the only granularity the module system
|
||||
offers.
|
||||
|
||||
@@ -64,7 +64,7 @@ deliberately does not yet settle the domain list.
|
||||
|
||||
## Where the shape came from
|
||||
|
||||
The catalogue's shape is not arbitrary and it is not a series of mistakes. **This repository
|
||||
The catalog's shape is not arbitrary and it is not a series of mistakes. **This repository
|
||||
began as a dotfiles repository**, and most of what looks inexplicable is inherited from that
|
||||
directly.
|
||||
|
||||
@@ -88,7 +88,7 @@ The generalisation from *place files on my machines* to *manage a mesh of nodes*
|
||||
move and it worked. What it did not do is revisit the assumptions underneath, because they were
|
||||
never stated as assumptions — they were just how the thing already worked.
|
||||
|
||||
**This is the most useful single fact for anyone changing the catalogue**, and it is why the
|
||||
**This is the most useful single fact for anyone changing the catalog**, and it is why the
|
||||
linking principle in particular reads as a deliberate architectural choice when it is an
|
||||
inheritance. See ADR 0012, whose case
|
||||
this strengthens: the argument for links was never made *for a mesh*.
|
||||
@@ -109,10 +109,10 @@ module out of the repository safe.
|
||||
|
||||
**Placement is already decided.** A standalone application belongs in its own repository
|
||||
(ADR 0015), and reviewers reject
|
||||
it in the monorepo. The catalogue's flat level is not a dumping ground by policy; it is one by
|
||||
it in the monorepo. The catalog's flat level is not a dumping ground by policy; it is one by
|
||||
history.
|
||||
|
||||
## Known inconsistencies in the catalogue itself
|
||||
## Known inconsistencies in the catalog itself
|
||||
|
||||
Recorded because a reader will meet them:
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ decisions:
|
||||
|
||||
# The seats, as they run
|
||||
|
||||
Written from the controller's code and the catalogue's manifests on their main branches. This is
|
||||
Written from the controller's code and the catalog's manifests on their main branches. This is
|
||||
the second piece of the new shape to exist, after [the lab](11-the-lab.md), and the first that the
|
||||
predecessor has no equivalent of: there, any well-formed name became a seat by being claimed.
|
||||
|
||||
@@ -24,7 +24,7 @@ have; nothing else can add to it.
|
||||
|
||||
Five seats deliver a provision: the mesh's store delivers the relational database, the mesh's broker
|
||||
the message transport, the artifact-store seat the registry, and two more the package registry for
|
||||
one ecosystem and the git service. The remaining nine — the controller and catalogue seats, and the
|
||||
one ecosystem and the git service. The remaining nine — the controller and catalog seats, and the
|
||||
node-scope ones for the builder, the DNS port, the packet filter, intrusion prevention, the
|
||||
private network, the resolver's configuration and the showcase — mark a role without answering for
|
||||
anything a consumer requires.
|
||||
|
||||
@@ -1,69 +1,153 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go]
|
||||
updated: 2026-10-10
|
||||
code: [mesh-tools, mesh-host, mesh-controller]
|
||||
updated: 2026-10-26
|
||||
decisions:
|
||||
- "ADR 0539"
|
||||
- "ADR 0360"
|
||||
- "ADR 0524"
|
||||
- "ADR 0525"
|
||||
- "ADR 0152"
|
||||
- "ADR 0095"
|
||||
- "ADR 0037"
|
||||
- "ADR 0175"
|
||||
- "ADR 0195"
|
||||
- "ADR 0197"
|
||||
- "ADR 0367"
|
||||
- "ADR 0154"
|
||||
---
|
||||
|
||||
# The mesh MCP server, as it runs
|
||||
|
||||
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
|
||||
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
|
||||
`<node>.mesh-console`, seals its credential to the machine, and the container binds
|
||||
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
|
||||
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
|
||||
endpoint. Nothing on the machine holds a credential a person had to carry.
|
||||
**The `mesh-console` container no longer exists.** Read 2026-10-17 from `mesh-tools` (package
|
||||
`node-tools`), `mesh-host` and `mesh-controller` on their forges' main, and from the live mesh
|
||||
through `mesh_overview`, `mesh_machine`, `mesh_runtimes` and `mesh_describe`. Checked: the
|
||||
catalog (`mesh-catalog`) carries no module named `mesh-console` any more — the name survives only
|
||||
in the glossary's retired-words list and in a test fixture — and none of the four nodes answers a
|
||||
module of that name. How the tool runner reaches a node, its count of modules and tools and the
|
||||
controller's verbs were read again on 2026-10-26, after the delivery of release 6 (ADR 0539).
|
||||
|
||||
## What it is now
|
||||
|
||||
**The tool runner is one of the mesh's core components**, named `node-tools` beside
|
||||
`mesh-controller`, `mesh-host` (the node-engine) and `nats` (the bus) — the mesh's own repositories'
|
||||
check treats the four the same way, as the core whose health the mesh itself defines rather than a
|
||||
module's own. Since release 6 it reaches each node the way every module does: the controller composes
|
||||
and signs its composition for that node, the assigner writes the node's want of it, and the
|
||||
node-engine's node agent applies it and writes its have. What stays particular to it is the account
|
||||
named `<node>.node-tools` on the bus (`BusNodeTools` in `mesh-controller`) and the node-engine's
|
||||
witness and lease code that puts back a tool runner that does not come up (`ComponentNodeTools` in
|
||||
`mesh-host`, ADR 0360 §9).
|
||||
|
||||
**It still carries a manifest, `node-tools/module.json` in `mesh-tools`,** and that manifest is
|
||||
where its grants live: `invokes` names `*`, so its account may call every tool on the mesh;
|
||||
`waits` names the five event families `mesh_wait` may subscribe to (the knowledge base's reviews,
|
||||
mesh-delivery's transitions, the forge's status, the controller's checked event, and the operator
|
||||
channel's decision for the agent module) — the bus refuses a subscription outside this list
|
||||
(ADR 0367 §2); and it
|
||||
`provides` one provision, the MCP endpoint, at node scope, and `listens` on the port it serves from,
|
||||
opened only to the machine itself. Its code lives under `node-tools/` in the `mesh-tools`
|
||||
repository (`internal/console`, `internal/announce`, `internal/bus`).
|
||||
|
||||
**One runtime per node, run by the node-engine as a sibling process, never a container**
|
||||
(ADR 0175). It holds the node's one bus credential and may call every tool on the mesh; its account
|
||||
publishes nothing else. It binds `127.0.0.1:<port>` and refuses to bind anything else, said before
|
||||
binding rather than after (`node-tools/internal/console/http.go`, citing ADR 0152): whoever is on
|
||||
the machine owns the mesh there, and nobody else may reach this port. An agent or a person on the
|
||||
node is pointed at `http://127.0.0.1:<port>/mcp` once; the `serverInfo.name` the handshake answers
|
||||
is `mesh`.
|
||||
|
||||
## What it answers
|
||||
|
||||
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
|
||||
stream. `tools/list` is what the running modules answered: every tool runner built on or after that day
|
||||
serves a `tools` verb for its module, and the mesh MCP server asks the catalogue for the roster and each module
|
||||
for its tools. A module that did not answer is named in the list's `_meta.notAnswering`. On the day it
|
||||
shipped that was 36 of 51 modules — those that serve no tools at all, and those whose rebuilt runtime the
|
||||
mesh records rather than rolls out — and 62 tools from the rest.
|
||||
`initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`, over HTTP, one JSON
|
||||
body per request, no session and no event stream (`node-tools/internal/console/mcp.go`).
|
||||
|
||||
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The mesh MCP server's grant is `*`, so what it
|
||||
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
|
||||
**It announces seven tools, not five** (ADR 0195, extended by ADR 0367): `mesh_overview`,
|
||||
`mesh_machine`, `mesh_search`, `mesh_describe`, `mesh_call`, `mesh_runtimes` and `mesh_wait`. The
|
||||
handshake's `instructions` text names all seven and the four address forms. `mesh_overview` is
|
||||
where the mesh-wide count of modules and tools is read live — a number copied into this document
|
||||
would drift; asked on 2026-10-26 it answered 115 modules and 714 tools across the whole mesh, with
|
||||
two module instances (`route-proxy` on two of the nodes) named as not answering rather than silently
|
||||
absent.
|
||||
|
||||
No call is refused for want of a person's approval: a tool runner serves every call it is issued, whoever made
|
||||
it, and cannot tell an agent from the operator (ADR 0259 §6, issue 340). An act waits for the operator's warrant
|
||||
only where its module asks first, in its own code: the agent module's instruction proposals, and the
|
||||
controller's settings proposals and the actions it offers on a condition.
|
||||
**Addresses, exactly as the handshake states them:** `<seat>.<verb>` for a seat held once for the
|
||||
mesh (`mesh-controller.status`); `<node>/<seat>.<verb>` for a seat every machine holds
|
||||
(`<node>/node-packet-filter.rules`); `<node>/<module>.<tool>` for a module assigned to one machine;
|
||||
and `<module>.<tool>` also for a module whose instances are interchangeable. A tool not listed can
|
||||
still be called by address — listing is discovery, calling is the grant — and a module that is not
|
||||
interchangeable is refused with the machines it runs on rather than guessed at.
|
||||
|
||||
**What exists is what announced itself** (ADR 0197). Every node's tool runner answers the NATS services
|
||||
protocol's `$SRV.PING`, `$SRV.INFO` and `$SRV.STATS`, one service per process, naming in each
|
||||
endpoint's metadata which module, seat, scope and machine serves it. `mesh_overview` and
|
||||
`mesh_machine` gather one request's answers: at least 750ms so a runtime that only just answered
|
||||
`$SRV.PING` is still waited for, up to 5s for one that said it is there but has not yet sent its
|
||||
full announcement — a fixed shorter window lost the largest runtime's listing once a live
|
||||
measurement found how slow its round trip over the bus could be. An announcement unchanged since
|
||||
last asked (by its digest) is not re-fetched, which is why `mesh_runtimes` can show a runtime
|
||||
answering in under 100ms for hundreds of kilobytes of discovery.
|
||||
|
||||
## mesh_wait
|
||||
|
||||
**The seventh tool, added by ADR 0367**, lets a session wait on the bus for an event instead of
|
||||
polling: a call names one or more granted event families and an optional filter, and either answers
|
||||
the first match or says it timed out. The tool runner's own bus account hears only the families its
|
||||
manifest's `waits` list names; an event it hears is never taken as the state — a session reads the
|
||||
state from the verb that owns it once woken. It is meant to be called in a loop from a background
|
||||
command, not inline, because one call blocks until it answers or times out.
|
||||
|
||||
## The mesh's own verbs
|
||||
|
||||
*Since 2026-09-30 evening (ADR 0154).*
|
||||
The mesh MCP server asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
|
||||
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
|
||||
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
|
||||
otherwise; `seat:<seat>.<verb>` says so outright. When the controller does not answer, the list
|
||||
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
|
||||
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
|
||||
**These already answer directly, not as a stated future.** Beside every assigned module's tools,
|
||||
the tool runner asks the `mesh-controller` seat for its own verbs the same way (ADR 0154): measured
|
||||
live on 2026-10-26, the mesh-controller runtime is one instance, mesh-wide, and it answers 65 verbs for
|
||||
that one seat (`status`, `assign`, `modules`, `artifacts` and the rest), of which 20 are refused by
|
||||
name with what replaces them: the delivery verbs release 6 removed (`plans`, `plan`, `push`,
|
||||
`deliver` among them) and the build verbs of release 4 (`build`, `queue` among them). The `mesh-controller`
|
||||
*module* answers none of its own — it serves no module tools, only its seat's, exactly as design 33
|
||||
described while it was still only designed. The 56-to-93 seat-verb counts each node's tool runner
|
||||
answers (below) are a different thing: the tool runner serves the verbs of whichever node-scoped
|
||||
seats are held on that node (`node-backup`, `node-build-go`, `node-service-manager` among them),
|
||||
on behalf of the modules holding them, not the mesh-controller seat's — that one is served by the
|
||||
mesh-controller runtime itself, a separate process, and the tool runner only asks it and relays what
|
||||
it answers.
|
||||
|
||||
## Around it
|
||||
## Where it runs
|
||||
|
||||
- **`invokes`** in a manifest is the grant. It is composed into the bus's user list exactly as a
|
||||
person's account is; the mesh MCP server is the only module that declares it.
|
||||
- **`module check <file|dir>…`** on the controller's binary judges a manifest with no mesh: the strict
|
||||
parse, every per-manifest problem, and the rules between the manifests given. It prints what it cannot
|
||||
judge without a store rather than refusing. The mesh MCP server's own manifest was the first thing checked
|
||||
with it, and the whole catalogue passes.
|
||||
- **The person's client remains.** `operator issue` and `mesh tools|call|mcp` with a credential file
|
||||
still work, for a machine that is not a node and for a mesh not yet able to assign anything.
|
||||
`mesh tools --console <url>` goes through a running mesh MCP server with no credential; it is covered by the
|
||||
runtime repository's tests and was not exercised on the live mesh.
|
||||
**Every node, not a per-operator assignment.** Measured live: all four of the mesh's machines
|
||||
answer a `node-tools` runtime, each with its own module and tool count (between 294 and 312 tools,
|
||||
56 to 93 seat verbs, in this reading). This is a fact that shipped past the design it came from: the
|
||||
to-be document still describes it as assigned to "whichever machines an operator sits at"; what
|
||||
actually runs is the opposite — every node the mesh holds carries one, because it is core
|
||||
infrastructure the mesh itself depends on (discovery, the controller's own verbs, every module's
|
||||
tools), not a convenience assigned to a workstation. There is accordingly no assign or unassign verb
|
||||
for it: removing it from a node is not an operation the controller or the catalog exposes.
|
||||
|
||||
## What shipped bent
|
||||
## What this repository's prior text got wrong
|
||||
|
||||
- A module registered by hand from the catalogue with `--source <url> --path modules/<m>` records a URL,
|
||||
not a place on the git seat: `--self` takes the forge path form (`<owner>/<repository>`), which the
|
||||
operator did not pass. The rebuild-on-merge matched the URL anyway.
|
||||
- Modules whose upgrade policy is *record* — the forge among them — answered `tools` only once
|
||||
something pushed their rebuilt runtime; until then they are listed as not answering while still
|
||||
callable. That is the policy doing what it says, not a fault of the mesh MCP server.
|
||||
The document this replaces described `mesh-console` as a catalog module served by a container and
|
||||
assigned only to workstations. That module and its container are gone; the manifest is not — it
|
||||
moved to `node-tools/module.json`, still declares `invokes: ["*"]`, and gained `waits` and the
|
||||
`mcp-endpoint` provision, described above. What also moved is which language builds it: the
|
||||
manifest's `build.artifacts` names a Go binary built from `cmd/node-tools`, and that is what a node
|
||||
runs. The repository still carries the earlier TypeScript sources (`node-tools/src/mesh.ts`,
|
||||
`http.ts`, `runtime.ts`, `announce.ts`, `broker-nats.ts`, `client.ts`, `launch.ts`, `main.ts`,
|
||||
`mcp.ts`) on main, but the manifest does not build from them. The quirks the prior text recorded
|
||||
from the `mesh-console` era (a module registered by hand recording its source as a URL; a
|
||||
*record*-policy module staying listed as not-answering until its rebuilt runtime is pushed) are not
|
||||
re-checked here: they were about the `mesh-console` container's registration path, which no longer
|
||||
exists.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- `mesh_overview` and `mesh_machine <node>`: the live tool and seat-verb counts, and which modules
|
||||
are named not-answering.
|
||||
- `mesh_runtimes`: which runtimes answered discovery, how long they took, how large their
|
||||
announcement was, and whether it was shortened to fit the bus — read this when an address is said
|
||||
to be missing or on another machine.
|
||||
- `mesh_describe <address>` for one tool's schema; a call through `mesh_call` reaches the module or
|
||||
seat over the bus and the answer is its own, unshaped.
|
||||
- The code: `mesh-tools` (`node-tools/internal/console`, `internal/announce`, `internal/bus`),
|
||||
`mesh-host` and `mesh-controller`'s core-component handling of `node-tools` (witness, lease,
|
||||
bus-user naming). Their unit tests hold the loopback-only refusal, the seven-tool handshake, the
|
||||
discovery gather window and `mesh_wait`'s event-then-read-state rule.
|
||||
- `knowledge-base.get` with `kind: decision` and number 152, 175, 195, 197, 367 or 154.
|
||||
|
||||
@@ -5,6 +5,7 @@ code: [mesh-controller internal/catalogue/state.go, mesh-controller internal/bro
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- "ADR 0201"
|
||||
- "ADR 0439"
|
||||
---
|
||||
|
||||
# A module's state, as it runs
|
||||
@@ -17,7 +18,7 @@ machine and its licence manager; on the day this was written, three buckets exis
|
||||
## What runs
|
||||
|
||||
- **The controller** creates a key-value bucket `<module>_<name>` for every declared state, from the
|
||||
catalogue — on every start and, since the first module that declared state found it missing, on every
|
||||
catalog — on every start and, since the first module that declared state found it missing, on every
|
||||
push before the memberships that name it. A bucket nothing declares any more is reported and kept.
|
||||
Every bucket carries the mesh's caps: 256 KiB a value, 64 MiB a bucket.
|
||||
- **The grants**: the machine's runtime is granted, for each bucket a module it carries owns, writing
|
||||
@@ -45,7 +46,7 @@ machine and its licence manager; on the day this was written, three buckets exis
|
||||
|
||||
## How it is checked
|
||||
|
||||
The controller's catalogue and broker tests (names, grants, memberships, a bucket asserted in place
|
||||
The controller's catalog and broker tests (names, grants, memberships, a bucket asserted in place
|
||||
against a real server); the runtime's tests over a real bus (current values without deletions, refusals,
|
||||
the TypeScript SDK through the runtime); `module check` names a read whose owner on the shelf keeps no
|
||||
such state.
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [mesh-controller, mesh-tools]
|
||||
updated: 2026-10-16
|
||||
updated: 2026-10-19
|
||||
decisions:
|
||||
- "ADR 0106"
|
||||
- "ADR 0436"
|
||||
- "ADR 0438"
|
||||
---
|
||||
|
||||
# Logins on the bus, as they run
|
||||
@@ -25,6 +27,16 @@ Nothing marks what a tool does.** Read from the controller's and the tool runner
|
||||
- **The caller is in the address.** A call's address carries the caller's name, so the runtime takes the caller
|
||||
from the address, never from the message. For a seat every machine holds, the address also names the machine.
|
||||
|
||||
- **A module's process may have a login of its own** (ADR 0436, delivered 2026-10-19 in mesh-controller #390 and
|
||||
mesh-sdk #36). A `process` resource that declares `bus` runs as a user its module makes and gets one login per
|
||||
machine, named by machine, module and process. It may emit its declared events, take work from its module's own
|
||||
work-queue stream through the one consumer the controller makes for it, use its module's states, read the
|
||||
router's records of its module's asks, call the verbs it declares, and listen only on its own inbox. It can
|
||||
answer no call and hold no seat. `module issue` delivers its password as the module's own secret
|
||||
`broker-<process id>`, readable by that user alone. mesh-mail's sender is the first such process: it holds the
|
||||
mail passwords that the tool runner may not read, and sends each mail the operator approved, which the tools
|
||||
record as queued before they put it on its work queue (ADR 0438).
|
||||
|
||||
## What it cannot do
|
||||
|
||||
- **No expiry.** A login lives until its record is deleted and the list rewritten on the bus's node.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [mesh-controller, mesh-catalog modules/claude-code, mesh-catalog modules/gitea, mesh-catalog modules/ssh-client]
|
||||
updated: 2026-10-17
|
||||
decisions:
|
||||
- "ADR 0266"
|
||||
- "ADR 0399"
|
||||
- "ADR 0409"
|
||||
- "ADR 0410"
|
||||
---
|
||||
|
||||
# Accounts on a machine, as they run
|
||||
|
||||
**A node record names at most two logins: the operator account, always, and an agent account, on one node of
|
||||
four. Where a node names no agent account, agents run as the operator account. Nothing ties an identity to a
|
||||
login, and a module that needs one declares it itself.** Read on 2026-10-17 from the controller's and the
|
||||
catalog's code on main (both local clones were a few commits behind the forge, so each claim below was
|
||||
also checked against a live read) and from the live node records. The change planned in ADR 0411 to 0414
|
||||
replaces this document when it is built.
|
||||
|
||||
## The node record
|
||||
|
||||
- **The operator account is stated at the controller's terminal** with `node account <node> <account> [home]`.
|
||||
It is the person's login on that machine. The controller refuses to set it to the node's agent account.
|
||||
- **The agent account is stated at the controller's terminal only** with `node agent-account <node> <account>
|
||||
[home]`, and cleared with `--clear`. It is no verb and no setting, so an agent cannot name itself one
|
||||
(ADR 0266 §1). The controller refuses root, a malformed login, a well-known service account and the
|
||||
operator account.
|
||||
- **Five machine facts come from the record.** `account` and `account-home` are the operator's, absent when
|
||||
the node has no operator account. `agent-account` is the agent account, or the operator account where none is
|
||||
named; `agent-home` is its home (`/home/<account>` when no home is stated); `agent-root` is `never` only where
|
||||
the node names an agent account of its own, otherwise empty. Modules read them as `${machine:...}` words.
|
||||
- **One node of four names an agent account today**, the control node. The workstation, the laptop and the
|
||||
home server name none: `mesh-controller.node` says there "agents run as the operator account; no agent
|
||||
account is named", and the plan for the workstation declares the `claude-code` account resource with the
|
||||
operator's name and no `root` field. The tool runner also receives `MESH_AGENT_ACCOUNT` and `MESH_AGENT_HOME`.
|
||||
- **Not verified:** whether the mesh-config snapshot carries either account. The controller's code reads both
|
||||
from its own node table (columns for account, account home, agent account and agent home); no second place
|
||||
that states them was found.
|
||||
|
||||
## What claude-code does with the agent account
|
||||
|
||||
- **It declares a `user` resource** named `${machine:agent-account}` with home `${machine:agent-home}` and
|
||||
`root: ${machine:agent-root}`, and a `~/.claude` directory under that home owned by it, mode 0700. On the
|
||||
control node the plan holds the resource with `root: never`; elsewhere it holds the operator's account.
|
||||
Where the account is new, the mesh creates it with a home, its own group and no password (ADR 0266 §2).
|
||||
- **`claude-agent`** is a file the module writes at `/usr/local/bin/claude-agent`. Run as the agent account
|
||||
it starts `claude`; run as any other account it runs `sudo -iu <agent account> claude` through that
|
||||
account's own sudo, carrying the operator's guard override only if it is set in that shell. The file
|
||||
`/etc/claude-code/agent-account` holds the account's name.
|
||||
- **The guard of ADR 0266 §28** is the module's hook on every tool call. On a node with an agent account
|
||||
of its own it refuses each call of a session that runs as another account, with no override, and tells the
|
||||
session to start with `claude-agent`. Its record goes to the machine's journal. The same guard also refuses
|
||||
ssh to a mesh machine and writes to `/etc/hosts` and the resolver file (ADR 0245).
|
||||
- **Not verified live:** this rests on the guard's code and README, not on a refusal seen on a running machine.
|
||||
|
||||
## The judgement that the agent account cannot become root
|
||||
|
||||
- **The node-engine judges `root: never` on every look**, by reading only: uid 0, a group that grants root,
|
||||
a sudo, doas or polkit rule, a container runtime's socket or API port, a secret the mesh placed for another
|
||||
account, a setuid program no package owns, and kernel link protection off (ADR 0266 §12 to §16). A way found,
|
||||
or a question unanswered, makes the account resource unhealthy. It does not prevent any way to root.
|
||||
- **The controller's self-check holds** that a node naming an agent account has it judged, in a verdict heard
|
||||
within 15 minutes, as unable to become root (ADR 0266 §21 to §22). Otherwise it raises the urgent condition
|
||||
`agent-can-become-root`. `mesh-controller.node` shows the verdict: for the control node it reads "cannot
|
||||
become root without a person", judged the same day.
|
||||
- **On the three nodes without an agent account** the check does not apply. The controller instead holds an
|
||||
urgent condition `root-not-free` on each machine that runs reads or the mail notifier. Two of the three are
|
||||
silenced until 2026-10-23 on the operator's decision; the home server's is open and not silenced.
|
||||
|
||||
## The accounts agents push as on the forge
|
||||
|
||||
- **Today the forge has a user per machine**, made by the gitea module's `forge-account` provision and
|
||||
holding that machine's key, plus a write token the module still mints for each (ADR 0398, ADR 0400). ADR 0410
|
||||
decides one forge user, `nox-agent`, in the agents' write team, each machine's key titled by machine. No tool
|
||||
reads the forge's user list, so whether `nox-agent` exists yet is **not verified**.
|
||||
- **The forge key is meant for the agent account** (ADR 0409 §2), but a module cannot run as that account:
|
||||
the controller refuses it (issue 989, open). So the `ssh-client` module's key keeper still runs as the
|
||||
operator account.
|
||||
- **An ordering deadlock** between `ssh-client`, which wants `forge-account`, and `gitea`, which provides it,
|
||||
stopped every node's declaration from composing on 2026-10-17 at 00:35 until gitea was registered three
|
||||
minutes later (issue 986). The four urgent conditions it raised were not observed again but stay raised,
|
||||
because the controller's declaration check times out and never clears them (issue 992).
|
||||
Git over ssh was proven from the laptop only; the proof on the other three, and retiring the forge token the
|
||||
module still places, wait on issues 989 and 991 (the latter in `fixing`).
|
||||
- **Main stays closed**: a push to the main branch is refused, and a merge waits for a person (ADR 0398).
|
||||
|
||||
## Logins on the bus
|
||||
|
||||
Not here: see [`16-logins-on-the-bus.md`](16-logins-on-the-bus.md).
|
||||
|
||||
## What it cannot do
|
||||
|
||||
- **No identity carries a login.** An agent, a person and a module are not tied to an account on a machine;
|
||||
the node record names a login per node and nothing else.
|
||||
- **A module declares a login itself.** Each module that needs an account writes a `user` resource of its own,
|
||||
and nothing checks that two modules agree or that a login exists on every machine that needs it.
|
||||
- **Agent and operator share a login on three of four machines.** There the agent runs as the operator account,
|
||||
which may become root; the guard's account rule does not apply, and the exposure is a held condition, not a fix.
|
||||
- **No per-agent bus password.** Every session on a machine calls through that machine's tool-runner login
|
||||
(document 16), so one agent's calls cannot be told from another's.
|
||||
- **Detects, does not prevent.** The judgement above lists the ways to root it looks for; a way it does not list
|
||||
is not seen (ADR 0266 §17).
|
||||
|
||||
## How it is checked
|
||||
|
||||
- `mesh-controller.node` with a node name: "agents run as" line, the verdict and when it was judged, the open
|
||||
conditions (`agent-can-become-root`, `root-not-free`, `uncomposable`).
|
||||
- `mesh-controller.plan` with a node name: the `claude-code.agent-account` resource and its `root` field; with
|
||||
`files` set, the files given, including `claude-agent` and `agent-account`.
|
||||
- `mesh-controller.conditions` for the held conditions; `claude_code_guard` for the guard's record, `claude-code`
|
||||
tools for its status.
|
||||
- `knowledge-base.get` with `kind` decision and number 266, 409 or 410; `issue-tracker.get` with 989 or 991.
|
||||
- The code: the controller's node-record and agent-account files, and the `claude-code` module's manifest and
|
||||
guard in the catalog. Their unit tests hold the node record's refusals and the guard's account rule.
|
||||
@@ -0,0 +1,286 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/mesh-mail, mesh-catalog modules/mesh-mail-web, mesh-catalog modules/mesh-mail-notifier, mesh-catalog modules/mesh-mail-reader]
|
||||
updated: 2026-10-22
|
||||
decisions:
|
||||
- "ADR 0359"
|
||||
- "ADR 0361"
|
||||
- "ADR 0378"
|
||||
- "ADR 0379"
|
||||
- "ADR 0436"
|
||||
- "ADR 0439"
|
||||
- "ADR 0440"
|
||||
- "ADR 0459"
|
||||
- "ADR 0461"
|
||||
- "ADR 0472"
|
||||
---
|
||||
|
||||
# The mail modules, as they run
|
||||
|
||||
**One module, `mesh-mail`, holds every mail account and is the mesh's one `mail-store`. A second module,
|
||||
`mesh-mail-web`, is the mesh's one mail web client. A third, `mesh-mail-notifier`, shows a
|
||||
desktop notice of new mail on the machines it is assigned to. A fourth, `mesh-mail-reader`, holds the seat
|
||||
`mail-reading`: it hears every new mail, reads it in small batches, and keeps a mail entry — its mail group, a
|
||||
one-line summary, its mark and why, never a body — for the mail page to show; the operator's own corrections
|
||||
on the mail page become reading lessons that outrank a mail's own words in a later batch. A caller may read an
|
||||
account's mail, or ask to send from it, only when `mesh-mail`'s trusted setting `grants` names it for that
|
||||
account — one list per account, giving both at once, to the same callers; `mesh-mail-reader` is on that list
|
||||
too, and asks no send, by its own design rather than by a narrower grant. A send is never performed by a tool:
|
||||
it is queued for a process of its own, which sends only once the operator approves it on Telegram.** Read
|
||||
from `mesh-catalog` on `origin/main` at `835bf22b` (2026-10-22) and from the live mesh through the mesh MCP
|
||||
server on 2026-10-22.
|
||||
|
||||
```
|
||||
mesh-mail (mail-store, once for the mesh — the control-node)
|
||||
accounts search read send send-status ── MAIL stream (headers only, 7 days) ──▶ a granted caller
|
||||
own database (index) · own object-store bucket (attachments) · three key-value buckets (sends, account state, newest mails)
|
||||
process keep (IDLE + index) · process watch (mail_health) · process send (mesh-mail-send account; holds the passwords)
|
||||
▲ grants: one list per account, read AND send alike (mesh-mail-reader on it too, asking no send)
|
||||
│
|
||||
┌────┴────────────────┬─────────────────────────┬──────────────────────────────────┐
|
||||
mesh-mail-notifier mesh-mail-reader mesh-mail-web (mail-webclient,
|
||||
(mail-notifier, once (mail-reading, once for once for the mesh — the control-node)
|
||||
per machine — laptop, the mesh — the control- sign-in (Keycloak) ─ pages ─ send ask ─▶ operator's Approve
|
||||
workstation) node) ▲ (Telegram)
|
||||
node-notifier.send on hears MAIL's added events; │ entries groups correct
|
||||
new inbox mail batches of 10 judged ─▶ its own │ lessons undo status
|
||||
database (entries, groups, lessons) ─▶ ─┘
|
||||
mail signal ─▶ operator's channel (Telegram)
|
||||
```
|
||||
|
||||
## 1. The modules and their seats
|
||||
|
||||
- **`mesh-mail`** claims `mail-store`, held once for the mesh (`accounts`, `search`, `read`, `send`,
|
||||
`send-status`). It is assigned today to one machine, the control-node (`module.json`; confirmed live,
|
||||
2026-10-20). ADR 0378 §4 decided to move it to the home server as a step of its own, on the operator's
|
||||
Approve (issue 803); as built, that move has not happened: the live mesh still runs it, and `mesh-mail-web`
|
||||
beside it, on the control-node.
|
||||
- **`mesh-mail-web`** is a module of its own, with its own Go module and source under `modules/mesh-mail-web`;
|
||||
it claims `mail-webclient`, held once for the mesh. It is assigned to the same machine as `mesh-mail` and runs as its
|
||||
own account, `mesh-mail-web`. Its manifest declares no database and no store of its own: nothing beyond its
|
||||
route, its sign-in binding and its own settings on disk.
|
||||
- **`mesh-mail-notifier`** is a module of its own as well (`modules/mesh-mail-notifier`), and keeps a
|
||||
byte-identical copy of `mesh-mail`'s `mailevent.go`, held to it by its `copies_test.go`, because each module
|
||||
builds alone. ADR 0378 §9 and ADR 0440 §1 say both are "built from `mesh-mail`'s source"; as built, they are
|
||||
separate modules that share none of `mesh-mail`'s own code but that copied file. It claims `mail-notifier`, held once per machine; a
|
||||
module claiming it needs `node-notifier` held on the same machine (ADR 0378 §7). It is assigned today to two
|
||||
machines, the laptop and the workstation (live read of each machine's modules, 2026-10-20); the control-node
|
||||
and the home server hold no instance of it.
|
||||
- **`mesh-mail-reader`** is a module of its own (`modules/mesh-mail-reader`), with its own Go module and
|
||||
source (`go.mod`, `cmd/mesh-mail-reader`), importing nothing of `mesh-mail` (`callers.go`). It claims `mail-reading`, a fourth seat `mesh-mail`'s own manifest declares beside its three mail
|
||||
seats, held once for the mesh (ADR 0459 §1, §2). It is assigned to the same machine as `mesh-mail`, under
|
||||
its own account, `mesh-mail-reader` (`runs-as`; confirmed live, 2026-10-22). It is covered whole in §8.
|
||||
- None of the four seats is named `mesh-*`. A caller reaches them by seat (`mail-store.<verb>`,
|
||||
`mail-webclient.<verb>`, `mail-notifier.<verb>`, `mail-reading.<verb>`); `mesh-mail` also answers its own
|
||||
tool names (`mail_accounts` and the rest) through the same handlers.
|
||||
|
||||
## 2. Accounts and the MAIL stream
|
||||
|
||||
- **Three accounts**, all of the kind `imap`, each a password-based sign-in given at the desk as the module's
|
||||
own secret `mail-credential-<account>` — one against the mesh's own mail server, two against outside
|
||||
providers (ADR 0359 §5, ADR 0361 §1; `mesh-controller.settings` for `mesh-mail`, read live on 2026-10-20). No
|
||||
account's name or address is repeated in this document.
|
||||
- **It watches each account and says each mail on the bus.** `mesh-mail` keeps IMAP IDLE on the inbox and polls
|
||||
the other folders; for each new, changed or removed message it publishes an event on its own stream `MAIL` —
|
||||
account, folder, message id, date, from, to, subject, size, flags and attachment references, never a body or
|
||||
an attachment's bytes (ADR 0359 §2). The stream keeps 7 days (`module.json`).
|
||||
- **A send's own record** lives in its state bucket `sends`, which only the `send` process and the tools write
|
||||
(`module.json`; ADR 0436 §8).
|
||||
- **The index and the attachment store are its own.** A row per message and folder lives in its own Postgres
|
||||
database; attachments live in an object-store bucket, each kept once by its SHA-256 digest (ADR 0359 §3, ADR
|
||||
0439 §5-§6).
|
||||
- **Two more buckets answer without the database.** An account bucket holds each account's state and, per
|
||||
folder, its name and its mail and unread counts; a mail bucket holds the newest mails of each account (up to
|
||||
200, fewer once there are more than 81 accounts), for a view that need not ask the database (ADR 0439 §2-§4).
|
||||
`mesh-mail-web`'s accounts page reads the folders from this bucket — a read wider than any one caller's grant
|
||||
— but only for an account its own grant already let it ask about (ADR 0440 §4).
|
||||
|
||||
## 3. `grants` — one list per account, giving both at once
|
||||
|
||||
- **The setting.** `mesh-mail`'s trusted setting `grants` names, per account, the callers that may reach it: a
|
||||
module's name alone (granted wherever it runs), `<machine>.<module>`, or a person. Nobody is granted an
|
||||
account by default (ADR 0359 §4).
|
||||
- **One check, every door.** The code that decides whether a caller may read an account's mail (`mail_search`,
|
||||
`mail_read`, the account's share of `MAIL`) and the code that decides whether it may ask to send from it
|
||||
(`mail_send`) is the same function, reading the one list (`modules/mesh-mail/cmd/mesh-mail/config.go`'s
|
||||
`Grants.Allows`, called from `tools.go` for every tool, `mail_send` included, confirmed on `origin/main` at `a67f0196`). There is no
|
||||
second list and no narrower grant for sending alone: a caller named for an account may do both, and a caller
|
||||
left out of it may do neither.
|
||||
- **Who is granted, today, for every account alike** (`mesh-controller.settings` for `mesh-mail`, read live on
|
||||
2026-10-22): `mesh-mail-notifier` (named alone, so wherever it is assigned it may read); each of the four
|
||||
machines' own `node-tools` — the control-node's, the laptop's, the workstation's and the home server's, which
|
||||
is how an agent session on any of them reaches a mail account at all; the control-node's `mesh-mail-web`; and
|
||||
the control-node's `mesh-mail-reader`. So the notifier, every machine's agent sessions, the web client and
|
||||
the mail reader can each both read an account's mail and ask to send from it, from the one list — ADR 0472
|
||||
§1 and §3 decided the mail reader joins this same list, rather than a narrower one of its own, and that it
|
||||
simply never calls a send verb: its manifest invokes `seat:mail-store.read` and `seat:mail-store.accounts`
|
||||
alone, no send verb of `mail-store` (`modules/mesh-mail-reader/module.json`, confirmed on `origin/main` at
|
||||
`835bf22b`).
|
||||
|
||||
## 4. The send path
|
||||
|
||||
- **A send is an ask, never a write by the tool.** `mail_send` (account, to, subject, body) is open only to a
|
||||
caller the account's `grants` names. It performs nothing itself: it raises an ask on the operator channel
|
||||
naming the account, the recipients, the subject and the first lines, and returns the ask's id at once (ADR
|
||||
0361 §2).
|
||||
- **The operator's Approve queues it once.** On an Approve recorded on a channel that proves who answered,
|
||||
`mesh-mail`'s tools record the send `approved` and publish it, once, on the work queue `mail-sends`, carrying
|
||||
only the send's id, its account and its ask id — never the recipients, subject or body, which stay in the
|
||||
send's own record (ADR 0436 §8).
|
||||
- **A process of its own performs it.** `mesh-mail`'s process `send` runs as its own account, `mesh-mail-send`,
|
||||
never the tool runner's account: it alone reads the accounts' passwords. It takes work from the queue,
|
||||
verifies the operator's warrant again against the router's own record rather than trusting the queued message,
|
||||
claims the send once, hands it to the account's outgoing server, and records the outcome — retrying a send the
|
||||
server provably never took, up to five times, each after a longer wait, before giving up (ADR 0436 §2, §7,
|
||||
§9-§10).
|
||||
- **`mail_send_status` answers one of a fixed set of states**: waiting for the operator, approved and waiting
|
||||
for the sender, being sent, waiting for its next attempt, sent, failed (with its reason), refused, not proven
|
||||
(the handover may or may not have reached the server), declined, expired, or unknown for an id it does not
|
||||
know or that belongs to another caller's send (`send.go`, `sender.go`; ADR 0436 §12).
|
||||
|
||||
## 5. `mesh-mail-web`
|
||||
|
||||
- **Its pages**, all behind sign-in: the accounts page (`/`), a folder (`/folder`), search (`/search`), one
|
||||
mail (`/mail`), an attachment download (`/attachment`), writing a mail (`/write`), its send (`/send`) and its
|
||||
state (`/send-status`), a mail correction (`/mail-correct`) and a reading-lesson undo (`/mail-undo`) — plus
|
||||
the sign-in callback (`/signed-in`). Every changing form (the send, a correction, an undo) carries a token
|
||||
bound to the session (`server.go`, `csrf.go`).
|
||||
- **The accounts page now opens on mail reading, above the classical list** (ADR 0459 §13, issue
|
||||
1430, 1523; `accounts.go`). It asks `mail-reading.status` first; not answering at all makes the whole
|
||||
section say mail reading is unavailable and why, and nothing else here is asked — the classical list below
|
||||
still renders regardless. Otherwise it shows, in order: the mail that matters (`mail-reading.entries`, mark
|
||||
`matters`), each with a correction form (mark, mail group) that posts to `/mail-correct`; the mail groups
|
||||
(`mail-reading.groups`) when mail reading is healthy, or the unhealthy cause and since when in their place
|
||||
when it is not; the scam warnings (the same `entries` answer, mark `scam warning`), each with the same kind
|
||||
of correction form, shown either way; the reading lessons (`mail-reading.lessons`, newest first), each with
|
||||
its own undo form posting to `/mail-undo`; and the weekly reading-lessons-per-100-read-mails measure, from
|
||||
`mail-reading.status`'s own answer. A mail body never reaches this section — only a mail entry's headers,
|
||||
mail group, summary, mark, why and due date.
|
||||
- **A correction or an undo is asked of `mail-reading`, not acted on here.** `/mail-correct` and `/mail-undo`
|
||||
are refused with no sign-in or a form token from another session, before `mail-reading.correct` or
|
||||
`.undo` is ever asked; whatever `mail-reading` answers or refuses, the page is simply shown again, so a
|
||||
refused correction is never surfaced as its own error (`mailwrite.go`, issue 1523).
|
||||
- **Sign-in is Keycloak's.** The module requires `oidc-client` and uses the authorisation-code flow with PKCE;
|
||||
the identity (the ID token's own username claim) must be in the trusted setting `operators` — nobody by
|
||||
default, and today one identity, the operator's own (ADR 0440 §3; `mesh-controller.settings` for
|
||||
`mesh-mail-web`, read live on 2026-10-22). The session cookie is `Secure`, `HttpOnly` and `SameSite=Strict`,
|
||||
ending 12 hours after sign-in (`auth.go`).
|
||||
- **Reachable only from the mesh's own machines.** Its route's reach is set to `internal`, so the route proxy
|
||||
composes only its internal name, never a public one, and the module itself answers any other host 404 (ADR
|
||||
0440 §2; `server.go`).
|
||||
- **A mail is shown as text.** `mail_read`'s own HTML-to-text is rendered through `html/template`'s escaping,
|
||||
never parsed as markup; an attachment is answered only as a download, `Content-Disposition: attachment` and
|
||||
`X-Content-Type-Options: nosniff`; every page carries a content security policy allowing nothing from
|
||||
elsewhere (ADR 0440 §5; `mail.go`).
|
||||
- **It keeps no database of its own.** Nothing on disk beyond its own settings; it reaches mail only through
|
||||
`mail-store`'s verbs, calling as `<machine>.mesh-mail-web`, and reads one thing directly besides —
|
||||
`mesh-mail`'s account bucket, for folders and counts, of an account its own grant already let it ask about
|
||||
(ADR 0440 §4, §6). Its manifest now also invokes `mail-reading`'s six verbs (ADR 0459 §13): it
|
||||
reads `entries`, `groups`, `lessons` and `status`, and writes a correction or an undo with `correct` and
|
||||
`undo` — writes that land in `mail-reading`'s own database, never in anything `mesh-mail-web` keeps itself.
|
||||
|
||||
## 6. `mesh-mail-notifier`
|
||||
|
||||
- **One notice per new inbox mail, at most once.** For each account it is granted, on hearing `mesh-mail`'s
|
||||
`added` event, it shows the sender (name and address), the subject and the account's name through the
|
||||
machine's `node-notifier`, under its own account (`runs-as`), never the tool runner's.
|
||||
- **Quiet rules.** It shows nothing for a mail outside the inbox, for a mail from the account's own address, and
|
||||
for a mail already flagged read when it arrives (`notifier.go`, `mailevent.go`). During an account's first sync
|
||||
or a resync `mesh-mail` publishes no `added` events, so nothing reaches the notifier then (`notifier.go`'s
|
||||
opening note, citing `mesh-mail`'s `imap.go`).
|
||||
- **Folding.** More than three qualifying mails of one account within a trailing 60-second window become one
|
||||
notice, shown once the window has passed since the fold's own first mail, naming how many and the latest
|
||||
sender; a message already shown — tracked by its message id, or by its folder, UIDVALIDITY and UID where it
|
||||
has none — is never shown twice, even redelivered. A notice that fails to send is retried at most once a
|
||||
minute (`notifier.go`, its constants `FoldOver`, `Window`, `RetryBackoff`).
|
||||
- **Assignment.** It is assigned today to the laptop and the workstation (live read, 2026-10-22; `mesh_machine`
|
||||
for each shows its seat `mail-notifier` and tool `mail_notifier_health` there, and neither on the
|
||||
home server or the control-node), where it needs `node-notifier` to be held beneath it. ADR 0461 §7 names
|
||||
these same two machines as the ones it is to be unassigned from, on the operator's Approve, once mail
|
||||
signals (§8) are proven live on the operator's real accounts (issue 1418) — as built, that unassignment has
|
||||
not happened.
|
||||
|
||||
## 7. Health
|
||||
|
||||
- **`mesh-mail`**'s liveness is its `keep` process's own unit health; its watching is judged once a minute by
|
||||
its `watch` process and read through the tool `mail_health`: `waits` (naming the secret) for an account whose
|
||||
password was never given, `unhealthy` (naming the account and why) for a connection down or an IDLE silent
|
||||
for five minutes, healthy otherwise; its `send` process is a plain unit (ADR 0359 §6; `health.go`).
|
||||
- **`mesh-mail-web`**'s own tool `mail_web_health` reads the one state its HTTP endpoint keeps: still setting up
|
||||
sign-in, listening, or ended with its error — unhealthy whenever it is not listening (`main.go`).
|
||||
- **`mesh-mail-notifier`** declares no process resource: its manifest declares only its account and its state
|
||||
directory. Its bundle is the one process the machine's runtime launches for it and keeps alive, under its own
|
||||
account (`runs-as`); it serves its tools and runs the watcher beside them (`main.go`'s opening note; commit
|
||||
12d78520, issue 859). Its tool `mail_notifier_health` is unhealthy once an account's notice
|
||||
has failed to show for more than ten minutes, and answers only when asked; no check of the node-engine judges
|
||||
the module by it. A watching process of its own is tracked by issue 841, not yet decided (its `README.md`).
|
||||
The comment in its `health.go` that still names a process `notify` predates that commit. It keeps one state
|
||||
bucket, `shown`, for 30 days, to show no mail twice (`module.json`).
|
||||
|
||||
## 8. `mesh-mail-reader`
|
||||
|
||||
- **It claims `mail-reading`**, the fourth mail seat, held once for the mesh, serving six verbs:
|
||||
`entries`, `groups`, `correct`, `lessons`, `undo` and `status` (ADR 0459 §1, §2; `module.json`,
|
||||
`tools.go`). Each answers only a caller the trusted setting `callers` names — a bare module name, granted
|
||||
on any machine, `<machine>.<module>`, or `person.<name>`, the same shapes `mesh-mail`'s own `grants`
|
||||
matches, copied in miniature so this module never imports `mesh-mail`'s code (`callers.go`). The setting
|
||||
defaults to `["mesh-mail-web"]` and is unchanged on the live mesh (`mesh-controller.settings` for
|
||||
`mesh-mail-reader`, read live on 2026-10-22), so only `mesh-mail-web` may call a `mail-reading` verb today;
|
||||
any other caller is refused, naming why.
|
||||
- **Its own account and database.** It runs as its own account, `mesh-mail-reader` (`runs-as`), never the
|
||||
tool runner's, because it hears `mesh-mail`'s restricted stream `MAIL` under it. It is granted reading on
|
||||
every account `mesh-mail` holds — named as the control-node's `mesh-mail-reader` in `mesh-mail`'s `grants`
|
||||
for each of the three accounts alike (`mesh-controller.settings` for `mesh-mail`, read live on 2026-10-22) —
|
||||
and never asks to send: its manifest invokes `seat:mail-store.read` and `seat:mail-store.accounts` only, no
|
||||
send verb, and its code calls neither `mail_send` nor anything that moves, flags or deletes a mail (ADR
|
||||
0459 §11, ADR 0472 §3). It keeps its own Postgres database (`postgres-database`, contributed as
|
||||
`mail-reader`), separate from `mesh-mail`'s index.
|
||||
- **Its database holds four tables, no mail body in any of them** (`schema.sql`): `mail_entry` — one row per
|
||||
judged mail, its account, message id, folder, date, sender name and address, subject, its `mail_group`, a
|
||||
`summary` of at most 160 characters, its `mark` (`matters`, `scam warning` or `neither`, or null), `why`,
|
||||
a `due_date`, whether it was `told` and when it was `judged`; `mail_group` — a label and when it was first
|
||||
made; `reading_lesson` — the corrected mail's own headers (account, folder, sender name and address,
|
||||
subject), the mail group, the `change` made, when (`made`), and whether it was `undone`; and `waiting_mail`
|
||||
— a mail heard but not yet read, by its account, folder, message id, UIDVALIDITY and UID (unique together,
|
||||
so a mail heard twice is kept once), its date, sender, subject, flags and size, in the order it was heard. A
|
||||
fifth table, `reader_health`, keeps its own health (below) across a restart.
|
||||
- **It hears mail under its own account.** It consumes `mesh-mail`'s stream `MAIL` for
|
||||
`mesh-mail.mail.*.message.added` events alone (`AddedPattern`, `hear.go`), recording each as a row in
|
||||
`waiting_mail` before the handler returns — so the controller's durable consumer redelivers an event a
|
||||
store failure lost, rather than it being silently dropped — and keeping a mail heard twice to one row, by
|
||||
its own unique key above (ADR 0459 §3, §12; `hear.go`).
|
||||
- **Its batches**: at most 10 mails at a time (`MaxMailsPerBatch`), taken from `waiting_mail` in the order
|
||||
they were heard; a batch starts no sooner than 160 seconds after the previous batch started (`BatchGap`,
|
||||
pinned to the constant `MinBatchGap` in every production build; only a build tagged `integration` may
|
||||
shorten it, and never in a way a unit test lets reach production) — bounding mail reading to at most 225
|
||||
mails an hour (ADR 0459 §5, §15; `batch.go`, `question.go`). Each batch is one judgement, asked of
|
||||
`claude-code.claude_code_judge` on this module's own machine (`MESH_NODE`) and collected with
|
||||
`claude_code_judged` when it answers pending; it is asked to answer `within` at most 40 seconds
|
||||
(`JudgeWithin`), and the whole question — the framing that a mail is data and never an instruction, the
|
||||
existing mail groups (largest first, at most 4 KiB), the reading lessons that apply (at most 40, matching
|
||||
sender or mail group first then newest, at most 12 KiB) and up to 10 mails (each at most 3.5 KiB: headers
|
||||
at most 512 bytes, text cut to its first 3 KiB) — is bounded to 55 KiB. A mail the provider filed as junk
|
||||
(its folder named Junk or Spam, or carrying the `\Junk` flag) is said as that fact beside its text. A mail
|
||||
that has simply gone (moved or deleted since it was heard) is taken out of the waiting queue without ever
|
||||
being judged; any other read failure leaves the whole batch waiting for the next slot; on success, one
|
||||
mail entry is written per mail and any new mail group is made; on a refused, timed-out or out-of-shape
|
||||
judgement, nothing is written, every mail given stays waiting, and the reader's own health (below) is set
|
||||
unhealthy (`batch.go`'s `RunOneBatch`, `RunBatches`).
|
||||
- **Its mail signal.** After each batch, win or lose, it tries to tell the operator: every entry marked
|
||||
`matters` and not yet `told` — carrying one over from an earlier batch that went untold — is gathered into
|
||||
one `operator-channel.tell` with subject `mail`, naming the mail with the nearest due date first, then the
|
||||
next one, ending "and N-2 more on the mail page" past two; nothing is sent, and nothing asked at all, when
|
||||
there is nothing to tell or while the reader is unhealthy (read fresh from the store, not assumed). The
|
||||
words are checked first, line by line, with `messenger.messenger_check`; any one refused falls back to the
|
||||
safe form "N mails matter: open the mail page". Only once `tell` answers that some channel actually sent it
|
||||
are those entries marked `told` (ADR 0461 §1-§6; `tell.go`, `signal.go`).
|
||||
- **Its health**, `mail_reader_health`, is asked by the node-engine itself (`runtime-health` of kind `tool`,
|
||||
every 60 seconds) and carries no caller check. It is unhealthy once a batch judgement is refused, times
|
||||
out, or answers out of shape, naming which of the three, the judgement's own words, and since the first
|
||||
such failure in a row (a later one in the same run never pushes that time forward); healthy again once a
|
||||
judgement succeeds. The state survives a restart, kept in its own database rather than only in memory (ADR
|
||||
0459 §12; `health.go`).
|
||||
- **What it never does.** It changes no mail: it moves, flags, deletes and sends nothing, asking no verb of
|
||||
`mail-store` that would (ADR 0459 §11). It keeps no mail body and no text taken from one — only headers, a
|
||||
summary at most 160 characters, a mark, why, and a due date (ADR 0459 §4).
|
||||
@@ -18,13 +18,15 @@ Where the two disagree, the implementation wins and the disagreement is stated.
|
||||
| [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
|
||||
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
|
||||
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
|
||||
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
|
||||
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalog's shape, and what its shape says |
|
||||
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
|
||||
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
|
||||
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
|
||||
| [`14-a-modules-state.md`](14-a-modules-state.md) | A module's current state on the bus: what the controller creates, the runtime serves, and the first live use showed |
|
||||
| [`15-the-agent-and-its-licences.md`](15-the-agent-and-its-licences.md) | The operator's agent on every machine and the licence manager that keeps its licences |
|
||||
| [`16-logins-on-the-bus.md`](16-logins-on-the-bus.md) | The bus's one list of logins, a login per module assignment, persons limited to their tools, sessions calling through the tool runner's login, and no effect on any tool |
|
||||
| [`17-accounts-on-a-machine.md`](17-accounts-on-a-machine.md) | The operator and agent accounts on the node record, claude-code's own login and guard, the judgement that the agent account cannot become root, and the forge accounts agents push as |
|
||||
| [`18-the-mail-modules.md`](18-the-mail-modules.md) | mesh-mail's accounts and MAIL stream, the one `grants` list that gives both reading and sending at once, the send path through its own process and the operator's Approve, mesh-mail-web's pages and sign-in, and mesh-mail-notifier's pop-up and its quiet rules |
|
||||
|
||||
## What these documents are not
|
||||
|
||||
|
||||
@@ -431,17 +431,17 @@ It is the same rule the node-engine follows about a service that does not exist
|
||||
(ADR 0004) — absence must be distinguishable
|
||||
from a failure to answer — applied to coverage instead of to a machine.
|
||||
|
||||
## A bed reads the catalogue it proves
|
||||
## A bed reads the catalog it proves
|
||||
|
||||
*Written 2026-09-21, from resolving [04-ISSUES/073](../../04-ISSUES/073-beds-carry-copies-of-catalogue-manifests/00-report.md);
|
||||
decided in ADR 0089.*
|
||||
|
||||
The rule above — the run rebuilds what it tests — was held for binaries and images and not for
|
||||
manifests. Beds built the manifests they install inline, as literals copied from the catalogue
|
||||
when each bed was written; the copies did not move when the catalogue did, and a catalogue change
|
||||
manifests. Beds built the manifests they install inline, as literals copied from the catalog
|
||||
when each bed was written; the copies did not move when the catalog did, and a catalog change
|
||||
was proven by no bed at all, while every bed stayed green against its copy.
|
||||
|
||||
**A bed that installs a catalogue module reads that module's manifest from the catalogue the run
|
||||
**A bed that installs a catalog module reads that module's manifest from the catalog the run
|
||||
was pointed at.** It rewrites what the lab must — a build artifact becomes the image the machine
|
||||
holds, an image is pinned, a host port is remapped where one machine carries colliding modules,
|
||||
an address may point at a stand-in the bed raises — and nothing else.
|
||||
@@ -451,15 +451,15 @@ server, a secret in the environment, a requirement edge cut so no second provide
|
||||
that is a mesh test. It does not get a name of its own if it runs the module's runtime — a
|
||||
module's name is its tool namespace and its broker scope
|
||||
(ADR 0093) —
|
||||
so it reads the catalogue and installs what the module requires; a name of its own is for a
|
||||
so it reads the catalog and installs what the module requires; a name of its own is for a
|
||||
fixture that runs no real runtime.
|
||||
|
||||
**The receipt names the catalogue's commit** with the others', so a run taken before a manifest
|
||||
**The receipt names the catalog's commit** with the others', so a run taken before a manifest
|
||||
changed says so — the same rule as for the binaries, for the same reason.
|
||||
|
||||
*How it is checked:* a unit test in the lab refuses an inline manifest literal that names a
|
||||
catalogue module unless the bed is declared, with its reason, in the test's own list, and refuses
|
||||
a declaration for a copy that is gone; the receipt test asserts the catalogue is claimed whenever
|
||||
catalog module unless the bed is declared, with its reason, in the test's own list, and refuses
|
||||
a declaration for a copy that is gone; the receipt test asserts the catalog is claimed whenever
|
||||
the run is pointed at one.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -31,7 +31,7 @@ offered one driver, everything worked, and snapshots took **seventy-six times lo
|
||||
needed to. Nothing failed. Nothing warned. A lab in that state runs correctly and is simply too
|
||||
slow to use — and the inner loop it exists to provide quietly does not exist.
|
||||
|
||||
The second is the more dangerous shape and it is a variant the mesh has not catalogued before.
|
||||
The second is the more dangerous shape and it is a variant the mesh has not cataloged before.
|
||||
Its usual failure is *reported success and did nothing*. This is **reported success and did it
|
||||
seventy-six times slower**, which no error surface catches because nothing is wrong.
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ raises the bootstrap forge under the forge module's container name, with the mod
|
||||
its data directory, so the module holds it by the found rule; the network is the one difference a take
|
||||
has left to say. *How it is checked:* a host test joins a kept network and refuses one it cannot; a
|
||||
host test keeps a left-out module's record and hold and removes an absent module's; a bootstrap test
|
||||
holds the installer's constants to the module's manifest where the catalogue is checked out beside it.
|
||||
holds the installer's constants to the module's manifest where the catalog is checked out beside it.
|
||||
|
||||
**What filters the machine, and the found firewall kept retired** — revision, 2026-10-02
|
||||
(ADR 0168). The node-engine
|
||||
|
||||
@@ -326,7 +326,7 @@ shape moves — it names a thing and the node-engine fetches it — and the cont
|
||||
genesis must raise before the controller can exist. It still raises one, for the store and the broker,
|
||||
which is where somebody else's software belongs.
|
||||
|
||||
The mesh's own components — the node-engine, the controller, the catalogue, the builder, the vault — are
|
||||
The mesh's own components — the node-engine, the controller, the catalog, the builder, the vault — are
|
||||
delivered as binaries into directories named for their versions, by the mechanism
|
||||
ADR 0141 describes. Third-party
|
||||
software stays a container. The split is not about isolation; it is about who built the thing.
|
||||
|
||||
@@ -425,8 +425,8 @@ time.*
|
||||
A network manager rewrites `/etc/resolv.conf` on every connectivity change unless it is told not to
|
||||
(ADR 0117), so the module holding
|
||||
`node-uplink` — the one telling it — writes the file itself, and there is one owner for it, the one
|
||||
whose program would otherwise overwrite it. Each of the catalogue's managers' modules
|
||||
(NetworkManager, systemd-networkd; dhcpcd's left the catalogue with
|
||||
whose program would otherwise overwrite it. Each of the catalog's managers' modules
|
||||
(NetworkManager, systemd-networkd; dhcpcd's left the catalog with
|
||||
ADR 0226, held by no machine) renders the same template from the resolver's holders and
|
||||
requires the mesh's resolver, so a machine is refused when nothing in the mesh resolves rather than
|
||||
given a file listing nothing. The managers' own mechanisms were weighed and not used: NetworkManager's
|
||||
@@ -440,7 +440,7 @@ node declaring one path are refused. The module that wrote the file before, its
|
||||
retire. The file changes owner in one apply on each machine: the node-engine hands a whole file to the
|
||||
resource declaring its path now rather than removing it first, so a machine is never without it.
|
||||
*Checked by the controller's tests that the uplink modules carry one identical template and that
|
||||
nothing else in the catalogue writes the path, a resolution test refusing a second writer, and the
|
||||
nothing else in the catalog writes the path, a resolution test refusing a second writer, and the
|
||||
node-engine's handover test, in which the file is present at every step of the apply.*
|
||||
|
||||
**A machine's names are one seat's** (ADR 0223).
|
||||
@@ -643,7 +643,7 @@ to stop working.*
|
||||
### The name is a label, not a domain
|
||||
|
||||
*2026-09-09, from running the whole mesh in the lab.* A route contribution carried its public name
|
||||
in full — the forge as `git.example.tld`, spelt out in the module. Pointing the same catalogue at a
|
||||
in full — the forge as `git.example.tld`, spelt out in the module. Pointing the same catalog at a
|
||||
different domain — a lab standing in for production, or a second operator's mesh — meant rewriting
|
||||
that name on every routed module. The mesh was holding a **map of names to services**, which is the
|
||||
one thing it must not: a public name is two facts owned by two different places, and neither is the
|
||||
@@ -976,7 +976,7 @@ run-once step before it starts. The step names the binding it reads and the prox
|
||||
step, so when the authority moves the root is fetched again and the proxy is recreated with it
|
||||
(ADR 0099).
|
||||
*How it is checked:* the route-forwarding bed installs the authority, the proxy and a consumer
|
||||
from the catalogue and asserts the routed name is served.
|
||||
from the catalog and asserts the routed name is served.
|
||||
|
||||
**And a machine trusts that authority because a module put its root in its trust store**
|
||||
(ADR 0147). The proxy's fetch
|
||||
|
||||
@@ -57,10 +57,10 @@ produced from there. A module whose path is the repository root is the ordinary
|
||||
not a separate shape.
|
||||
|
||||
*Corrected 2026-09-12. This section previously said the manifest was at the root, full stop, which
|
||||
made a repository hold exactly one module. Nothing that exists is shaped that way: the catalogue
|
||||
made a repository hold exactly one module. Nothing that exists is shaped that way: the catalog
|
||||
holds sixty-seven modules in their own directories, the system being replaced has always built a
|
||||
module from a repository and a path, and the root-only reading left every existing module
|
||||
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
|
||||
unbuildable — pointed at the catalog the builder finds no manifest, pointed at a module's source
|
||||
it finds no manifest either.*
|
||||
|
||||
## A manifest is checked where it is written
|
||||
@@ -84,7 +84,7 @@ read without a store (ADR 0122
|
||||
keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the
|
||||
check says that too).
|
||||
|
||||
*How it is checked:* the controller's test runs the check over the catalogue checkout beside it and
|
||||
*How it is checked:* the controller's test runs the check over the catalog checkout beside it and
|
||||
over a manifest with a known fault, and asserts the first passes and the second names the fault; the
|
||||
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
|
||||
anybody can run.
|
||||
@@ -218,7 +218,7 @@ for, arrived at by an implementation detail.
|
||||
until it is told otherwise; the mesh changing its mind is not a machine acting on it, and
|
||||
collapsing the two is how a mesh comes to report success for something that has not happened.
|
||||
|
||||
*Checked end to end: a commit, a build, a catalogue entry, and a machine that ends up running what
|
||||
*Checked end to end: a commit, a build, a catalog entry, and a machine that ends up running what
|
||||
the source says — with both halves that make the answer trustworthy. It is still running the old
|
||||
one until it is pushed, and it stops being reported as behind once it has caught up, because a
|
||||
status that says "behind" for ever is one nobody reads.*
|
||||
@@ -398,7 +398,7 @@ that distinction once already.*
|
||||
## The four the loop cannot build
|
||||
|
||||
*2026-09-12. Written down because it keeps being rediscovered as if it were new, once per
|
||||
component. It is one rule; the instances are listed below. Revised the same day, when the catalogue
|
||||
component. It is one rule; the instances are listed below. Revised the same day, when the catalog
|
||||
gained an owner and turned out to be a fourth — which is the argument for stating the rule rather
|
||||
than the list.*
|
||||
|
||||
@@ -412,23 +412,23 @@ have a route and one does not:
|
||||
| The controller | It is what installs modules. Nothing can install it before it runs. | **Carried inside the installer** and published once there is a registry (ADR 0067) |
|
||||
| The registry | It *is* where artifacts are delivered from. A store cannot be delivered through itself. | **Pulled from the public internet** — its image is an ordinary public one, never built ([`04-ISSUES/029`](../../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)) |
|
||||
| The builder | It is what builds. Nothing builds it before it runs. | Built at genesis by the init builder (ADR 0070) |
|
||||
| The catalogue | It owns the module graph, and nothing can be resolved or installed without it. | Built at genesis by the init builder (ADR 0070) |
|
||||
| The catalog | It owns the module graph, and nothing can be resolved or installed without it. | Built at genesis by the init builder (ADR 0070) |
|
||||
|
||||
**How they arrive is settled and not yet built.** The installer carries an init builder, which
|
||||
clones the source and builds the controller, the catalogue and the builder before a mesh exists
|
||||
clones the source and builds the controller, the catalog and the builder before a mesh exists
|
||||
to install anything. Two things about that are open and named in
|
||||
ADR 0070: where the init builder
|
||||
clones from, given the forge normally runs on the mesh it would be rebuilding, and what it
|
||||
publishes into, given the registry is installed later in the order today.
|
||||
|
||||
**There is a fourth, and the list was closed too early.** The catalogue owns the module graph
|
||||
**There is a fourth, and the list was closed too early.** The catalog owns the module graph
|
||||
(ADR 0070), so it cannot be
|
||||
installed by something that needs it in order to install anything — the same shape as the other
|
||||
three. This section previously said the list was closed at three, which was written before the
|
||||
catalogue had an owner and is corrected here rather than left to be reasoned from.
|
||||
catalog had an owner and is corrected here rather than left to be reasoned from.
|
||||
|
||||
**And the answer for all four is now one mechanism, not four special cases.** Genesis carries an
|
||||
*init builder* and builds the core modules on the machine — controller, catalogue and builder —
|
||||
*init builder* and builds the core modules on the machine — controller, catalog and builder —
|
||||
rather than carrying a finished image of any of them. So the question is no longer "how does this
|
||||
one get here first?" asked once per component; it is answered once, by the thing that is carried
|
||||
being a builder rather than a result.
|
||||
|
||||
@@ -102,7 +102,7 @@ anything on the machine that can talk to the runtime. So a secret reaches a proc
|
||||
the file the node-engine wrote and points the program at it, and the mesh's own programs take a `_FILE`
|
||||
twin for every variable that carries a credential. Software that reads only its environment is
|
||||
not forbidden; it is **declared**, on the container, with a reason, so the manifest says which
|
||||
secrets are exposed that way. **Checked** at composition: the catalogue engine refuses a
|
||||
secrets are exposed that way. **Checked** at composition: the catalog engine refuses a
|
||||
`${secret:…}` in a container's `env` outright, and a secret-carrying file named in `env-file`
|
||||
unless the container carries the declaration.
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ the one the mesh runs for its own: it offers a place to push, and claims that ro
|
||||
machine.
|
||||
|
||||
**A rule was enforced only at the far end.** A module may not declare an action, and the node-engine
|
||||
refused one correctly — but the controller accepted it into the catalogue, resolved it and
|
||||
refused one correctly — but the controller accepted it into the catalog, resolved it and
|
||||
pushed it, so the refusal arrived on a machine with nothing tying it back to the manifest. The
|
||||
rule held; it was just unusable, which is the same shape as the network shape that cost five
|
||||
failing tests before anyone read the node-engine's log. It is now refused where it is written.
|
||||
|
||||
@@ -132,7 +132,7 @@ adopted as modules, and the machine's service is still reachable.
|
||||
|
||||
Genesis ends with a mesh of one that runs, and that is not the same as a mesh that works. What it
|
||||
has is a controller, a store, a queue and a registry. What it cannot yet do is **produce
|
||||
anything** — and almost every module in the catalogue is waiting to be produced, because a manifest
|
||||
anything** — and almost every module in the catalog is waiting to be produced, because a manifest
|
||||
names what its artifacts are and nothing has made them.
|
||||
|
||||
So installing continues:
|
||||
@@ -172,7 +172,7 @@ need no registry. The gitea module's own server is an upstream image, so buildin
|
||||
|
||||
**The store genesis raises is the `postgres` module's server, so adopting it keeps it** (ADR 0338, the
|
||||
store's twin of the bus's ADR 0333). The bundle raises the store under the name, on the image, in the data
|
||||
directory and with the superuser password file that the catalogue's `postgres` module declares, and every step
|
||||
directory and with the superuser password file that the catalog's `postgres` module declares, and every step
|
||||
of the bundle and of the installer reaches it by that name. At the store step the module declares the
|
||||
container the bundle raised, so the store is adopted where it runs and is not restarted. The module does not
|
||||
change, so no running mesh's store moves. *How it is checked:* a test reads the module's manifest and holds
|
||||
@@ -190,7 +190,7 @@ something it made rather than by a placeholder.
|
||||
mesh running; building it from its own repository and path is what makes it upgradeable. The first
|
||||
time that happens is the moment the mesh stops depending on the installer for anything.
|
||||
|
||||
**And then the catalogue.** Every module with source of its own is built the same way. Until this
|
||||
**And then the catalog.** Every module with source of its own is built the same way. Until this
|
||||
has happened a mesh can install only what is public or carried, which is the foundation and little
|
||||
else.
|
||||
|
||||
@@ -233,7 +233,7 @@ was brought up the supported way, so the rule that it must be is, today, unenfor
|
||||
|
||||
**Nothing asks for the core modules yet.** Installing ends with a builder that works — a raised
|
||||
machine will build the shared base and a module on top of it when asked — and nothing asks. The
|
||||
paragraphs above describing the catalogue being built are a thing somebody now types, rather than a
|
||||
paragraphs above describing the catalog being built are a thing somebody now types, rather than a
|
||||
thing that cannot happen.
|
||||
|
||||
**A module's declaration still has to be copied onto the machine by hand.** The installer reads the
|
||||
@@ -242,7 +242,7 @@ longer among them: it lives in the controller's own repository
|
||||
(ADR 0069), the build the
|
||||
installer runs reports it with the artifact resolved, and genesis registers that
|
||||
([issue 072](../../04-ISSUES/072-the-controllers-manifest-exists-twice/00-report.md)). The
|
||||
builder's manifest belongs in the same repository and is still read from the catalogue; that is
|
||||
builder's manifest belongs in the same repository and is still read from the catalog; that is
|
||||
the last of this.
|
||||
|
||||
**A machine has no account for a registry that asks for one.** The mesh grants a consumer a
|
||||
@@ -275,7 +275,7 @@ issue tracker holds it as issue 602.
|
||||
| An image is named exactly | A machine refuses a bundle naming an image by tag. The refusal is exercised, not assumed. |
|
||||
| The installer is what installed this | **Nothing.** See above. |
|
||||
| The builder can arrive on a fresh mesh | The installer carries it and installs it as its last step, and the genesis bed raises a machine by running the installer. A bed that raises one any other way fails its own acceptance check. |
|
||||
| The builder is assigned before the package registry exists | The build-agent manifest's own test holds the requirement optional and the genesis document declared and issued outside the mesh; the controller's resolution test over the real catalogue assigns the builder with no provider, lists the requirement unbound, and binds it once gitea is offered. The builder's tests refuse a package, an `.npmrc` image and an `@novox/` TypeScript build with neither binding nor genesis document, before anything is built, and build a Go artifact; they read the genesis document only without a binding. The installer's test seals the document at the packages step and gives the builder no `serves` setting and no `npm-password`. **Not yet proved live:** a genesis bed from bare passing the builder, packages, sdk and base steps. |
|
||||
| The builder is assigned before the package registry exists | The build-agent manifest's own test holds the requirement optional and the genesis document declared and issued outside the mesh; the controller's resolution test over the real catalog assigns the builder with no provider, lists the requirement unbound, and binds it once gitea is offered. The builder's tests refuse a package, an `.npmrc` image and an `@novox/` TypeScript build with neither binding nor genesis document, before anything is built, and build a Go artifact; they read the genesis document only without a binding. The installer's test seals the document at the packages step and gives the builder no `serves` setting and no `npm-password`. **Not yet proved live:** a genesis bed from bare passing the builder, packages, sdk and base steps. |
|
||||
| The controller a mesh runs is one it built | The genesis bed asserts the running controller is pinned to a digest this mesh's own registry serves, for an image built from a named repository and commit — not one the installer carried. |
|
||||
| A core module is built rather than only carried | The controller is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. |
|
||||
| Installing produced a mesh that can produce | A module with source of its own is asked for and comes back pinned to a digest this mesh's registry assigned, not to a placeholder. **Done by hand on a raised machine, not yet by a bed** — it built the shared base and then a module naming that base. Nothing automated asserts it, which makes this the weakest check on this page. |
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: proposed
|
||||
status: in-progress
|
||||
code:
|
||||
- mesh-controller cmd/mesh-builder
|
||||
- mesh-controller internal/builder
|
||||
- mesh-catalog modules/build-agent
|
||||
updated: 2026-10-16
|
||||
- mesh-controller modules/build-agent
|
||||
updated: 2026-10-17
|
||||
decisions:
|
||||
- "ADR 0405"
|
||||
- "ADR 0262"
|
||||
@@ -31,6 +31,8 @@ decisions:
|
||||
|
||||
# Building a module
|
||||
|
||||
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's builder (internal/builder, cmd/mesh-builder) implements the recipe kinds described here, and the sections marked built and proven live match the code. Left: not broken down into milestones; issue 1072 found it saying proposed.
|
||||
|
||||
[`12-a-module-repository`](12-a-module-repository.md) says what a module may build — an image, an
|
||||
archive, an upstream mirror — and where the result goes. This says **how a build is modelled**, and
|
||||
why the current model does not fit what a module is.
|
||||
@@ -41,7 +43,7 @@ Turning a module's source into artifacts the mesh can pin, publish and deliver
|
||||
truthfully what was produced and what it was produced against.
|
||||
|
||||
Everything else is somebody else's: *what* to build is the controller's, *what a build means* is
|
||||
the catalogue's (ADR 0072), *where an artifact runs* is the
|
||||
the catalog's (ADR 0072), *where an artifact runs* is the
|
||||
controller's again. The builder's whole responsibility is the middle.
|
||||
|
||||
## The language
|
||||
@@ -81,7 +83,7 @@ output into a second stage; and sets an environment variable naming the compiled
|
||||
module repeats it. Miss any step and the failure arrives somewhere else — as a crash loop, a
|
||||
placeholder digest, a module that builds and does nothing.
|
||||
|
||||
The evidence that this is too hard is in the catalogue: **most modules are not converted**, and the
|
||||
The evidence that this is too hard is in the catalog: **most modules are not converted**, and the
|
||||
two converted during one session were each wrong twice before they were right, against a working
|
||||
example sitting open in the next window.
|
||||
|
||||
@@ -161,8 +163,8 @@ makes the drift worse while hiding it.
|
||||
| A module with its own code needs no Dockerfile | A module declaring only a language builds, and its artifact is pinned to a digest the mesh's registry assigned. |
|
||||
| An archive is produced and delivered | A module declaring an archive is built, and the machine assigned it has the unpacked files — verified on the machine, not in the build's own output. |
|
||||
| A Dockerfile still works | A module that declares one builds exactly as before, because software that needs a particular base has not stopped existing. |
|
||||
| Nothing is announced that was not made | A build made to fail announces nothing, and the catalogue's graph is unchanged afterwards. |
|
||||
| A build keeps what it was told | A catalogue started after a build asks for what it missed and receives the manifest and the artifacts stood on, not a summary. |
|
||||
| Nothing is announced that was not made | A build made to fail announces nothing, and the catalog's graph is unchanged afterwards. |
|
||||
| A build keeps what it was told | A catalog started after a build asks for what it missed and receives the manifest and the artifacts stood on, not a summary. |
|
||||
| The toolchain is the mesh's, not the author's | Changing the toolchain makes every module built against it stale, and each names what moved. |
|
||||
|
||||
## The whole surface, in one place
|
||||
@@ -205,7 +207,7 @@ declared in one of three ways, for the three things a path can be: the module's
|
||||
or file resource, or where a secret, grant or contribution lands), the operator's (an `accesses`
|
||||
entry), or the machine's (a facility a declared capability grants — `container-runtime` grants its
|
||||
socket). *How it is checked:* the parser refuses an undeclared mount naming the path and the three
|
||||
remedies, and a test parses every manifest in the catalogue beside the checkout.
|
||||
remedies, and a test parses every manifest in the catalog beside the checkout.
|
||||
|
||||
### What it builds
|
||||
|
||||
@@ -245,7 +247,7 @@ counts as a copy and what as a base.
|
||||
| `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ |
|
||||
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset (ADR 0155) unless the manifest gives a preference a default (ADR 0262) | ✅ |
|
||||
| `user` | a login | ✅ |
|
||||
| `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalogue converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
|
||||
| `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalog converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
|
||||
| `archive` | files fetched by digest and unpacked | ✅ |
|
||||
| `package` | a package that must be present | ✅ |
|
||||
| `network` | a named container network | ✅ |
|
||||
@@ -290,7 +292,7 @@ This is the bus's shared-work pattern, not a build-specific one: any module decl
|
||||
`accepts` has its work shared by its holders the same way. Building is the first use.
|
||||
|
||||
*Built and proven live 2026-10-03.* `build-agent` holds `node-build-agent` on all four machines; the first
|
||||
build taken by a workstation's agent was a catalogue module at 09:35 UTC; the one-holder `builder` is
|
||||
build taken by a workstation's agent was a catalog module at 09:35 UTC; the one-holder `builder` is
|
||||
retired. The switch found five gaps, each an issue: a worker whose type changed stranded its holder
|
||||
([206](../../04-ISSUES/206-a-seats-worker-changing-type-strands-the-holder-and-the-build-that-would-fix-it/00-report.md)),
|
||||
a re-made worker replayed the stream's history ([207](../../04-ISSUES/207-a-re-made-worker-replayed-every-ask-the-stream-kept/00-report.md)),
|
||||
@@ -314,7 +316,7 @@ controller's seat verb through the mesh MCP server — reads it back afterwards.
|
||||
id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the
|
||||
log. The mesh MCP server's `build` tool asks and answers at once with the id; the outcome is taken in — the
|
||||
build recorded, the module registered with its source — by whoever hears it, the waiting command or
|
||||
the daemon following the role's event, so a build nobody waited for still reaches the catalogue
|
||||
the daemon following the role's event, so a build nobody waited for still reaches the catalog
|
||||
([issue 176](../../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)). A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the
|
||||
builder needs nothing more for it.
|
||||
|
||||
@@ -381,7 +383,7 @@ than retried for ever. Live: the store's size before and after the first nightly
|
||||
*2026-09-29 —
|
||||
ADR 0142.*
|
||||
|
||||
The toolchain list was typescript and python, and only typescript had a base module in the catalogue.
|
||||
The toolchain list was typescript and python, and only typescript had a base module in the catalog.
|
||||
Meanwhile the controller — written in the language this project is mostly written in — was built as
|
||||
an image from a hand-written Dockerfile, which is the per-repository incantation this whole mechanism
|
||||
exists to abolish.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: proposed
|
||||
status: in-progress
|
||||
code:
|
||||
- mesh-sdk src
|
||||
- mesh-tools src/broker-nats.ts (and broker-amqp.ts until the rollout)
|
||||
- mesh-controller internal/link
|
||||
- mesh-controller cmd/mesh-controller (status)
|
||||
- mesh-catalog modules/postgres, modules/keycloak (the Go provisioner loop)
|
||||
updated: 2026-10-06
|
||||
updated: 2026-10-17
|
||||
decisions:
|
||||
- "ADR 0224"
|
||||
- "ADR 0230"
|
||||
@@ -24,6 +24,8 @@ decisions:
|
||||
|
||||
# The module protocol
|
||||
|
||||
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: the wire this design describes is in mesh-sdk and mesh-controller's internal/link, and the design's own checks against the code (2026-09-16) agree. Left: not broken down into milestones; issue 1072 found it saying proposed.
|
||||
|
||||
**What a module's code and the mesh say to each other.** An SDK is an implementation of this in one
|
||||
language and nothing more (ADR 0074).
|
||||
|
||||
|
||||
@@ -5,8 +5,17 @@ code:
|
||||
- mesh-catalog modules/showcase
|
||||
- mesh-controller internal/builder
|
||||
- mesh-sdk src
|
||||
updated: 2026-10-02
|
||||
- mesh-contracts
|
||||
updated: 2026-11-05
|
||||
decisions:
|
||||
- "ADR 0634"
|
||||
- "ADR 0635"
|
||||
- "ADR 0625"
|
||||
- "ADR 0627"
|
||||
- "ADR 0573"
|
||||
- "ADR 0606"
|
||||
- "ADR 0609"
|
||||
- "ADR 0593"
|
||||
- "ADR 0150"
|
||||
- "ADR 0099"
|
||||
- "ADR 0053"
|
||||
@@ -47,10 +56,42 @@ Split the way the protocol is split:
|
||||
mesh set. Reading a file is not a thing an SDK needs to teach.
|
||||
- **No API clients.** A Plex client belongs in the Plex module. It changes when Plex changes,
|
||||
which has nothing to do with any other module — *frequent **and** cascading is the disease.*
|
||||
Logic over a *seat* is not such a client: it is the seat's client package, in the seat's contracts
|
||||
module (below), never in the SDK and never in a holder.
|
||||
- **No storage, no HTTP framework, no logging library.** Use the language's.
|
||||
|
||||
If you find yourself wanting to add something to the SDK, the test is ADR 0039's: **does editing it
|
||||
recompile unrelated modules, and does it change often?** Both, and it does not belong.
|
||||
recompile unrelated modules, and does it change often?** Both, and it does not belong. It goes to
|
||||
its owner instead: a seat's contract or client, the module that owns it, or its domain's SDK (*Where a seat's code
|
||||
goes*, below; ADR 0634 §1).
|
||||
|
||||
## Where a seat's code goes
|
||||
|
||||
*Added 2026-11-04, widened 2026-11-05* (ADR 0625, ADR 0627, ADR 0634, ADR 0635; [design 26](26-the-seats.md) *Where
|
||||
a seat's code goes* is the whole rule). Shared code lives with its owner (ADR 0634 §1): a module that calls or holds
|
||||
a seat, or shares code with other modules, meets these places, and each piece of code has one, the first that fits:
|
||||
|
||||
| the code | lives in | written |
|
||||
|---|---|---|
|
||||
| a seat's types, subjects, the layouts of its states and a typed call per verb or accept | the seat's **contract**: a package of its own per language in its domain's contracts module (`mesh-<domain>-contracts`, or `mesh-contracts` for the Core domain and a domain with none), for a seat defined whole; a seat at the seat list's grain has only its definition, which every reader reads from the bus (ADR 0635 §1, §2) | generated from the seat's definition, never by hand |
|
||||
| logic two or more callers or holders would otherwise each write over that seat (an ask and its awaited answer, publishing a file in parts to a registry) | the seat's **client package**, beside its contract, owned by the contracts module | by hand; it uses only its own seat's contract, mesh-sdk's runtime and its own domain's SDK, never another seat's client, and names no holder, no module's tool and no module's subject |
|
||||
| logic and state layouts one module owns, where it holds no seat for them | that module's **own package**, which other modules import from it: by path in the same repository (ADR 0609 §2), by a published version from another (ADR 0573 §1), so its manifest declares it as a package artifact; its fingerprint counts only that package's files (ADR 0593 §2), so its importers rebuild only when it changes | by hand, in the owner module |
|
||||
| logic and forms no seat carries and no one module owns, used by one domain's modules or seats only (moving a file in parts, the build steps' shared logic) | that domain's **domain SDK**, `mesh-<domain>-sdk`, a package-only module (ADR 0634 §2) | by hand; it imports only mesh-sdk and contract packages, names no module's tool or subject, and holds no seat's type or state layout |
|
||||
| what every module needs whatever the seat (the bus connection, the tools bundle's runtime, state), and a part modules or seats of more than one domain share that names no seat, verb or seat's type | **mesh-sdk** | by hand; it takes no seat-specific type, imports no contracts module, and reads the seat list from the bus |
|
||||
| the seat's implementation | the **holder** module | by hand, against the contract; never the place of the contract or a client |
|
||||
|
||||
**So a module imports the contracts and clients of the seats it uses, the packages of the modules whose state it
|
||||
reads, its domain's SDK where it uses it, and mesh-sdk.** Its users rebuild when something they use changes, not when
|
||||
any seat or any domain does. A seat you define yourself, outside the mesh's own contracts, gets a
|
||||
contracts module of its own, which may share your holder module's repository (ADR 0625 §7): a second holder then
|
||||
imports only the contract, never your holder. A contracts module and a domain SDK are modules like any other, found by
|
||||
their manifests in folders of a repository and delivered to no node (ADR 0634 §8). And a role no other module could
|
||||
ever take over is no seat at all: it is your module's own tools and events (ADR 0625 §8).
|
||||
|
||||
*How it is checked:* by the checks design 26 lists: the generators' tests, mesh-sdk's import and package-list tests,
|
||||
the build modules' regenerate-and-compare step, the catalog check of a client and the manifest validator's refusal of a
|
||||
contracts module that claims a seat; the home of each new exported name, a domain SDK's importers included, and a role
|
||||
made a seat or a seat left as one module's tools, by the review of the pull request that adds it (ADR 0634 §10).
|
||||
|
||||
## A module with four capabilities, in four languages
|
||||
|
||||
@@ -171,7 +212,7 @@ knows. Putting the Plex client anywhere else is the shared-library disease with
|
||||
- **Which private registry.** That there *is* one is settled —
|
||||
ADR 0014 says each module consumes its dependencies
|
||||
from the private registry, the mesh's own shared library included. Which software serves it is
|
||||
not: the catalogue holds `verdaccio`, and a git host usually serves package registries too.
|
||||
not: the catalog holds `verdaccio`, and a git host usually serves package registries too.
|
||||
- **And the bootstrap does not have one.** ADR 0014 assumes a registry exists; on a fresh mesh
|
||||
nothing has installed one when the first SDK is built. That is the same pivot as everything else
|
||||
and it has not been designed.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: proposed
|
||||
status: in-progress
|
||||
code:
|
||||
- mesh-host internal/bootstrap
|
||||
- mesh-host cmd/mesh-bootstrap
|
||||
- mesh-lab test/integration/one-node-mesh.test.ts
|
||||
updated: 2026-10-06
|
||||
updated: 2026-10-17
|
||||
decisions:
|
||||
- "ADR 0226"
|
||||
- "ADR 0067"
|
||||
@@ -16,6 +16,8 @@ decisions:
|
||||
|
||||
# The installation, in full
|
||||
|
||||
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: the node-engine's bootstrap (`mesh-host` internal/bootstrap) carries phases 2 and 3 of the installation, so the steps this text calls manual are partly automated. Left: not broken down into milestones; the text is not yet corrected against the code.
|
||||
|
||||
Every step from a machine with nothing to a mesh that maintains itself. Written out explicitly
|
||||
because it is the procedure everything else depends on, and because the parts that do not exist
|
||||
yet are easier to see beside the parts that do.
|
||||
@@ -63,14 +65,14 @@ and waits — which looks exactly like a builder with no work.
|
||||
|
||||
**Genesis ends with a mesh that runs, which is not the same as a mesh that works.** It has a controller, a store, a broker, a registry and a builder. It holds no module graph, has no private
|
||||
network, no packet filter, and cannot resolve a name. Calling that "installed" is what let the
|
||||
catalogue be missing from a test for weeks without anything complaining.
|
||||
catalog be missing from a test for weeks without anything complaining.
|
||||
|
||||
| # | step | what happens | why it is here |
|
||||
|---|---|---|---|
|
||||
| 13 | the shared base is built | the toolchain and runtime every module with code of its own stands on | nothing else with code can be built until it exists |
|
||||
| 14 | a store module is built and run | a database **provider**, which the foundation's store is not | the foundation's store is the controller's own memory, and offers nothing to anything |
|
||||
| 15 | the catalogue is built and run | the module graph | without it the mesh cannot say what it holds, what a change reaches, or what must be rebuilt |
|
||||
| 16 | the catalogue asks for what it missed | the builds made before it existed are replayed | on a fresh mesh those are always the base, the store and the catalogue itself ([issue 050](../../04-ISSUES/050-the-catalogue-knows-nothing-built-before-it/00-report.md)) |
|
||||
| 15 | the catalog is built and run | the module graph | without it the mesh cannot say what it holds, what a change reaches, or what must be rebuilt |
|
||||
| 16 | the catalog asks for what it missed | the builds made before it existed are replayed | on a fresh mesh those are always the base, the store and the catalog itself ([issue 050](../../04-ISSUES/050-the-catalogue-knows-nothing-built-before-it/00-report.md)) |
|
||||
| 17 | the controller is rebuilt from its own repository | and rolled out through the module path | the moment the mesh stops depending on the installer for anything |
|
||||
| 18 | `mesh-wireguard` is assigned **and the node placed** | a private network | assigning installs the module; placing says where this machine is on it. Both, or the machine has no peers. Assigned by its own name since ADR 0226 retired the `networking` bundle |
|
||||
| 19 | the packet filter is assigned | rules generated from what modules declared | until this, every rule the mesh computes has never been applied to anything |
|
||||
|
||||
@@ -6,7 +6,7 @@ code:
|
||||
- mesh-controller
|
||||
- mesh-catalog
|
||||
- mesh-lab
|
||||
updated: 2026-09-15
|
||||
updated: 2026-10-17
|
||||
decisions:
|
||||
- "ADR 0067"
|
||||
- "ADR 0074"
|
||||
@@ -17,6 +17,8 @@ decisions:
|
||||
|
||||
# The work ahead
|
||||
|
||||
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Phase 2's blocker (issue 053) is resolved and delivered, and the forge's module declares the package registry in full, Phase 2's done-when. Whether every phase holds is checked under issue 1073, which marks it implemented or names what is left.
|
||||
|
||||
Everything decided this cycle and not yet built, in the order its dependencies allow. Each phase
|
||||
ends at something provable on a running mesh, because a phase that ends at a claim is a phase that
|
||||
went missing without anything complaining.
|
||||
@@ -42,7 +44,7 @@ after the phases that change its build path are in — not before.
|
||||
|
||||
**Why here.** The Go controller and the TypeScript SDK disagree about what a grant carries
|
||||
(`consumer` is the module in one, the node in the other). That is exercised by the installer's own
|
||||
provisioning — the catalogue's database — so it belongs before more is built on it.
|
||||
provisioning — the catalog's database — so it belongs before more is built on it.
|
||||
|
||||
- [x] 1.1 the drift was not live — inspection showed the wire agrees (contributions file; envelope
|
||||
required headers). The dead types that disagreed are removed, ADR 0074 and doc 19 corrected
|
||||
@@ -89,7 +91,7 @@ a protocol that agrees, a registry to publish to. Issue 051.
|
||||
the controller's records and the database of each module that asks for one. *(Adopted in place: the module's
|
||||
server names the same container and the same pinned upstream image the foundation runs, so the
|
||||
applier reconciles it rather than raising a second postgres. Proven 22/22 in the one-node lab —
|
||||
the catalogue, a `postgres-database` consumer, gets its database from it. Follow-up: the store
|
||||
the catalog, a `postgres-database` consumer, gets its database from it. Follow-up: the store
|
||||
binds `0.0.0.0` from genesis so a mesh consumer can reach it, but the packet filter is
|
||||
installed later — a brief pre-filter window where `mesh-store` is open before `from: mesh`
|
||||
clamps it; bring the filter up earlier or bind narrower at genesis.)*
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-09-25
|
||||
status: in-progress
|
||||
code: [mesh-controller]
|
||||
updated: 2026-10-17
|
||||
decisions:
|
||||
- "ADR 0084"
|
||||
- "ADR 0027"
|
||||
@@ -11,6 +11,8 @@ decisions:
|
||||
|
||||
# 23 — Choosing a provider
|
||||
|
||||
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's `internal/catalogue/resolve.go` implements the model described here, and designs 26, 27 and 29 rely on it. Not yet checked against this text clause by clause, nor given an as-is: issue 1074.
|
||||
|
||||
A provision is named for what the consumer's code is coupled to
|
||||
(ADR 0027):
|
||||
`postgres-database`, not `database`. That decides *what kind* of provider satisfies a requirement.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: implemented
|
||||
code: [mesh-catalog, mesh-controller, mesh-host, mesh-media-catalog]
|
||||
updated: 2026-10-12
|
||||
status: in-progress
|
||||
code: [mesh-catalog, mesh-controller, mesh-host, mesh-media-catalog, mesh-vault]
|
||||
updated: 2026-10-26
|
||||
decisions:
|
||||
- "ADR 0550"
|
||||
- "ADR 0305"
|
||||
- "ADR 0158"
|
||||
- "ADR 0094"
|
||||
@@ -31,9 +32,10 @@ wrong.
|
||||
- **A provisioned credential** is the login one module uses against another — a database password,
|
||||
a broker account. The mesh mints it per consumer↔provider pair and seals it to the machines that
|
||||
must hold it (ADR 0048),
|
||||
and its provider makes it true. **This is not the vault's**, and the vault does not change it. It
|
||||
already has an owner and a rotation
|
||||
([13](13-credentials-and-their-rotation.md)); disturbing it would buy nothing.
|
||||
and its provider makes it true. This design first left it out of the vault, since it already has an owner
|
||||
and a rotation ([13](13-credentials-and-their-rotation.md)). **It is the vault's now** (ADR 0550 §5, from
|
||||
ADR 0113): the vault keeps the value and hands it sealed; its provider still makes it true, and its
|
||||
rotation is the same machinery.
|
||||
- **A module's own secret** is a value a single module needs for itself — the password of a store
|
||||
it runs privately, an internal signing token. Today the mesh generates this as a value nothing
|
||||
owns, and so it cannot be rotated. **This is the vault's.**
|
||||
@@ -45,6 +47,56 @@ The vault, then, is the provider a module turns to for a secret that is **not**
|
||||
some other provision. It is the answer to *"this module needs a password, and there is no provider
|
||||
whose job it is to give it one."*
|
||||
|
||||
## Every secret lives in the vault
|
||||
|
||||
*Decided 2026-10-26 (ADR 0550), on the operator's answers; built in the milestones of ADR 0547.*
|
||||
|
||||
**The seat `mesh-vault` holds every secret the mesh keeps.** That is every shared secret, the enrolment tokens
|
||||
among them; the mesh's signing key; every licence's grants and tokens; every bus account's password or, where
|
||||
the vault has no value, its hash; and every module's own secrets and credentials. A holder gets a value only
|
||||
sealed to its own key, on request and reply, or asks the vault to act with it, as for a signature. No secret is
|
||||
kept in a bucket, a store database or a module's own rows. The private keys a node or the operator makes for
|
||||
itself stay where they are made (ADR 0550 §1).
|
||||
|
||||
- **The signing key.** The vault makes a new signing key for the mesh, with the mesh's authority beside it; the
|
||||
controller's key is never copied out of its database. The vault signs enrolments, node certificates and the
|
||||
authority's own on request, through its verb `sign`, for the callers its manifest names, and says `signed`
|
||||
naming the caller, the kind and the digest, never the key. The public halves are in a state of the vault that
|
||||
every node reads. The key and the authority leave the vault only as the one copy sealed to the operator key,
|
||||
opened only in the break-glass recovery below, so losing the vault does not lose the key every signature
|
||||
depends on. Each node trusts the vault's key beside the controller's, one node first, and the controller's no
|
||||
more once it loses its lease (ADR 0550 §2).
|
||||
- **Licence grants and tokens.** The refresh tokens, the API key and the access tokens of every licence are vault
|
||||
secrets; the licence manager keeps which licence goes to which holder, and nothing secret
|
||||
([39](39-the-anthropic-licence-manager.md), ADR 0550 §3).
|
||||
- **Bus account passwords.** A bus account's value is the vault's: until ADR 0547's third milestone the controller
|
||||
hands the vault the value of each account ADR 0374 §1 covers and keeps the others' values as today (ADR 0374
|
||||
§4), whose hashes the vault adopts; from then the vault mints every one. The agent of the node holding `mesh-broker` hands the vault,
|
||||
sealed to it, the bcrypt hash of every user of the list the bus loaded; the vault adopts the hash of each user
|
||||
it holds no value for (the controller's own, the node and device accounts, the module and process accounts its
|
||||
values do not cover), writes it under `bus-accounts.<user>` and says `account-hashed` with `adopted: true`. It
|
||||
never touches a key that holds the hash of its own value, and writes an adopted key again when the loaded list
|
||||
carries another hash for that user. Its tool `secret_own` lists the adopted users and each adoption, and
|
||||
`account_adopt` adopts now (ADR 0550 §4).
|
||||
- **Module secrets** stay as the sections below describe them: a module's own secrets, the credentials between a
|
||||
consumer and its provider, and the values an operator or a backend delivers. A node composing its own modules
|
||||
asks the vault for them sealed to its key, and no composition carries one in the clear (ADR 0550 §5).
|
||||
- **Three exceptions until ADR 0547's third milestone, each ending there:** the controller's own signing key in
|
||||
its identity database; the bus account values the controller mints, handed to the vault where the accounts are
|
||||
covered and kept by the controller for the others; and the enrolment tokens and new node accounts the
|
||||
controller makes and keeps in its store. So **no machine is enrolled before the third milestone** (ADR 0550 §1,
|
||||
§6).
|
||||
|
||||
*How it is checked:* before the third milestone's lease write, the controller's licence and identity databases and
|
||||
the licence manager hold no secret the vault does not hold, and no exception is left that the vault has not
|
||||
replaced, read with the vault's `secret_holders` and the store's reader. A unit test that `sign` refuses a caller
|
||||
its manifest does not name and that no answer or event carries the key; `secret_holders` showing the
|
||||
operator-sealed copy of the key and no other holder; and live, a node certificate the vault signed that another
|
||||
node verifies. After the delivery of the hash hand-over, `mesh-grants.grants_user_list` shows no user without a
|
||||
hash and `secret_own` lists the adopted users; unit tests in the vault (adopt; never touch its own value or a
|
||||
covered account; rewrite on a new loaded hash) and in the node agent (every loaded user handed, sealed). The
|
||||
hand-act log shows no enrolment before the third milestone.
|
||||
|
||||
## A secret as a provision
|
||||
|
||||
A module that needs a secret for its own use requires a `secret` provision, exactly as it requires
|
||||
@@ -77,8 +129,10 @@ the foundation: installed at genesis, the way the store and broker are raised fi
|
||||
adopted as modules (ADR 0078). A
|
||||
mesh has root secrets, so a mesh has a vault; it is not something a mesh opts into
|
||||
(ADR 0085, amended). It is provisioned the
|
||||
ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does that,
|
||||
because the controller must mint in order to deliver any provision, the vault's own included.
|
||||
ordinary way. It does not yet mint the mesh's bus accounts: until the third milestone of ADR 0547 the
|
||||
controller mints them, hands the vault each covered account's value (ADR 0374 §1) and keeps the others as
|
||||
today, and from then the vault mints every one
|
||||
(*Every secret lives in the vault*, above).
|
||||
|
||||
There is **one vault per mesh**, on the control-node, reached from any node the way identity is.
|
||||
Node scoping (ADR 0084,
|
||||
@@ -131,7 +185,7 @@ recovery addresses both alike.
|
||||
|
||||
### A provider with one credential
|
||||
|
||||
*Decided 2026-10-01 (ADR 0158); the controller's half built the same day (mesh-controller PR 184): the offer's word, the need carrying the shared secret's name, the vault's one value under one generation stamp, remade for every holder on a later binding or a rotation, the rotate command sending every holder. What remains is each provider's definition saying `credential` and `taken`, with a start that applies the file — the media catalogue's work.*
|
||||
*Decided 2026-10-01 (ADR 0158); the controller's half built the same day (mesh-controller PR 184): the offer's word, the need carrying the shared secret's name, the vault's one value under one generation stamp, remade for every holder on a later binding or a rotation, the rotate command sending every holder. What remains is each provider's definition saying `credential` and `taken`, with a start that applies the file — the media catalog's work.*
|
||||
|
||||
Software that holds one credential — a download client's web password, an indexer's one API key —
|
||||
cannot give each consumer a login, so ADR 0048's form does not fit it and its values were accepted
|
||||
@@ -148,7 +202,7 @@ acceptance. *How it is checked:* the rows of ADR 0158's table; the controller's
|
||||
### A credential the provider's software issued
|
||||
|
||||
*Decided 2026-10-12 (ADR 0305, issue 526), accepted on the operator's approval the same day; built on
|
||||
branches in the controller and the media catalogue.*
|
||||
branches in the controller and the media catalog.*
|
||||
|
||||
Some software is given its one credential rather than told it: a media server signs in to its vendor and
|
||||
keeps the token the vendor issued. The mesh can neither make nor rotate such a value, and a copy typed for
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
layer: to-be
|
||||
design: 25
|
||||
milestone: bindings-first-view
|
||||
updated: 2026-11-11
|
||||
code: [mesh-contracts seats/mesh-broker, mesh-sdk go/seatlist, mesh-catalog modules/nats, mesh-delivery modules/mesh-grants]
|
||||
decisions:
|
||||
- "ADR 0595"
|
||||
- "ADR 0615"
|
||||
- "ADR 0621"
|
||||
- "ADR 0596"
|
||||
---
|
||||
|
||||
# Design 25, milestone bindings-first-view — the grants part asks the bus module for every binding
|
||||
|
||||
**What it builds.** The grants part writes a module's binding (the names on the bus of its bus-state entries) into
|
||||
its membership when it hears the bus module's `created`, rewrites it on `recreated` and removes it on `dropped`
|
||||
(design 25 bus-state §4; ADR 0595 §1, §7). Until now those live events were the binding's only source, so an event
|
||||
the grants part never heard lost the binding for good: on 2026-11-11 a registry holder's membership lacked three of
|
||||
the entries its manifest asks for, although the bus held all three, and about 190 publishes since 2026-11-09 were
|
||||
never said `published` (issue 1966). This milestone gives the grants part a first view of every binding from the
|
||||
owner of that data, the seat `mesh-broker`, at every start and when asked again by hand, so a missed event is
|
||||
recovered with nobody's hand. The live events stay the way a binding changes between starts.
|
||||
|
||||
**Where it comes from.** The operator, 2026-11-11 (about 03:35 CET, in the terminal), chose "Ask the bus module":
|
||||
at every start, and on a re-ask, the grants part takes a first view from the bus module's seat of the entries it
|
||||
holds and composes the bindings from it; the owner answers for its own data. Within that answer, these lines follow
|
||||
ADR 0595 §1 and §7 (the binding is the bus module's to say and the grants part's to write), ADR 0596 §2 (another
|
||||
module's data is asked through its seat, never read from its bus state) and the first-view rules of ADR 0615 §3 to
|
||||
§6, which decide them for the indexer's and the assigner's state and which this milestone applies, as the design's
|
||||
choice, to the bus module's. Each choice below says so. The operator, 2026-11-11 13:30 CET (in the terminal, to lead-47), asked whether
|
||||
the grants part should take a bucket's binding from the bus module's existing `list` answer when the new field
|
||||
`binding` is missing, to break the circle §7 names, answered "Yes, build it": §9.
|
||||
|
||||
**Cited as** "design 25 bindings-first-view §n". It amends design 25 bus-state §4, which says so.
|
||||
|
||||
## The lines
|
||||
|
||||
- §1 **The first view is the seat `mesh-broker`'s `list`, asked with no `module`.** No new verb: `list` already
|
||||
answers every entry the bus module created, from its own state `asked`, by module and local name, and lacks only
|
||||
the binding (§2); a second verb answering the same entries would be a second form of one fact. The grants part
|
||||
declares `seat:mesh-broker.list` in its manifest's `invokes`, and composes its own grant of it as it composes any
|
||||
invoker's. `list` names no class, so from milestone G of design 47 it is a `read` and takes that class's callers,
|
||||
invokers, persons and sessions (design 47 G §391); until then its callers are "any caller whose role grants it",
|
||||
which the grants part's invoke grant meets. The verb stays answered where the bus module's tools answer it today,
|
||||
by its tools bundle in the tool runner of the machine holding the seat, and gains no runtime of its own: ADR 0621
|
||||
§1 gives the indexer's and the assigner's first views a runtime of their own so that the tool runner never asks
|
||||
through itself, and here the asker is the grants part, which composes every account without this answer (§5), so
|
||||
no part waits on itself. Why: the design's choice under the operator's answer and ADR 0596 §2. How it is checked:
|
||||
§8, the whole case.
|
||||
- §2 **Each entry of `list`'s answer carries its binding.** The seat's `Held` gains the field `binding`, of the
|
||||
seat's own form `Binding`, the same value the bus module's `created` and `recreated` carry for that entry in their
|
||||
field `binding`, taken from the entry's plan in the bus module's state `asked`. **`Binding` is reshaped to the form
|
||||
on the wire.** Today the seat's definition says `Binding` is a bus name, a storage type, a template and a stream's
|
||||
subjects, while the bus module sends, and the grants part needs, mesh-sdk's membership binding: `local` (the local
|
||||
name), `storage`, `template`, `stream` and `subject` (a stream's, and a ledger's history's subject), `bucket`,
|
||||
`object-store`, and for a ledger `history`, `state`, `indexes` (each index's bucket by its name), `snapshot` and
|
||||
`types`, which `created` and `recreated` carry and only `dropped` clears (corrected 2026-11-11, develop question D-11 of design 47's milestone G: an earlier wording had the bus module's events leave them empty). The
|
||||
definition's `Binding` takes exactly those fields, with those names and forms, so `created`, `recreated` and
|
||||
`list`'s `binding` carry one form, the generated contract decodes what the bus module sends, and no byte changes
|
||||
on the wire; the fields design 47 G §399 adds come on top of it. Why: the design's choice following this line's
|
||||
own words (one value in the events and the answer), met by the developer on 2026-11-11 and directed by lead-47. It is present on every entry whose objects the bus holds (the
|
||||
entries whose answer carries `settings`) and absent on one it does not hold (waiting to be made, or refused), as a
|
||||
refused create hands no binding (design 25 bus-state §4). `show` and `orphans`, which answer `Held` too, carry it
|
||||
alike. A field G's part adds to `Binding` (design 47 G §399: `objects` and `sent-objects`) reaches `list`'s answer
|
||||
with it, since the form is one. What changes: the seat's definition in mesh-contracts (`seats/mesh-broker`), its
|
||||
generated Go contract, and mesh-sdk's bootstrap copy of the definition in `go/seatlist`, which its test against
|
||||
mesh-contracts holds equal; the bus module's tools fill the field. **The size**: 18 entries in the new form are
|
||||
asked by the manifests on main (counted 2026-11-11), so the answer is a few KiB, an estimate; it stays one reply
|
||||
well under the bus's largest message (1 MiB) while the entries number in the hundreds, and from G a reply over that
|
||||
limit goes as an object by design 47 G §407. Why: the design's choice. How it is checked: unit (mesh-contracts' generated contract), a
|
||||
`created` body as the bus module's keeper writes it, a ledger's among them, decodes into the generated type and
|
||||
encodes back to the same bytes; unit (the bus module's
|
||||
tools), a row where `list`'s `binding` for an entry equals the `binding` its `created` carried, and a row where an
|
||||
entry not held has none; the seat's fixtures.
|
||||
- §3 **It is asked at every start and when asked again by hand, never on a timer.** The grants part's process asks
|
||||
`list` once at each start, as it asks its other first views (design 25 bus-state §5), and again when a person
|
||||
calls `grants_first_view_again` with `state` `bindings`, or with no `state`, which asks every first view (§6).
|
||||
Nothing else asks it: no timer, and no event, since the bus module's events carry the indexer's revision of the
|
||||
verdict they acted on and no revision of the bus module's own, so a gap in them cannot be seen (ADR 0615 §5 asks
|
||||
again on a gap only where one can). **At a start the answer comes first**: the grants part takes no event from its
|
||||
durable consumer of `mesh-broker.created`, `recreated` and `dropped` until the answer is applied, or until the
|
||||
bound of §5 passes. Since the shared stream keeps the events in order and each event sets or removes one entry's
|
||||
whole binding, the events held then, taken after the answer, leave each binding as the last word on it said,
|
||||
whichever came later. Why: the design's choice under ADR 0615 §4 to §6. How it is checked: §8.
|
||||
- §4 **How the answer is applied.** For each entry of the answer that carries a binding, the grants part writes the
|
||||
binding into the module's membership on every machine the module is assigned, kept in its state `memberships` as
|
||||
on `created`, where it holds none, and rewrites it, as on `recreated`, where it holds a different one; where it
|
||||
holds the same, it writes nothing. Each binding written or rewritten is said in its log, naming the module, the
|
||||
local name and the first view as the cause, and recomposes that module's memberships as an event's would. An entry
|
||||
listed without a binding (§2) changes nothing the grants part holds for it, but for an entry the bus holds in an
|
||||
answer of a bus module's build from before this milestone, which carries no `binding` on any entry: §9 composes
|
||||
that binding, and it is then applied by this line as one the answer carried. **A binding the bus no longer
|
||||
lists is kept, never removed by a first view**: the grants part removes a binding only on `dropped` (ADR 0595 §1,
|
||||
§7), so a bus module whose own state `asked` was lost or rebuilt, and answers few entries or none, strips no
|
||||
module of its data. Instead it raises the condition `mesh-grants.binding-unlisted`, one per such binding, naming
|
||||
the module and the local name and the operator's step (read the entry with the seat's `show`; an entry its
|
||||
manifest no longer asks for leaves by the bus module's `drop`, whose `dropped` removes the binding); a later answer
|
||||
that lists the entry, or a `dropped` for it, clears it. **On a re-ask the events keep flowing**: an answer is
|
||||
applied only to entries no `created`, `recreated` or `dropped` named since that ask was sent, so an answer never
|
||||
writes over a newer event. Why: the design's choice under ADR 0595 §7, and ADR 0615 §6's rule that a reader keeps
|
||||
what it holds rather than taking an empty answer as the truth. How it is checked: §8.
|
||||
- §5 **While the bus module does not answer, the grants part composes the rest and says so.** It composes every
|
||||
account and keeps every binding it holds in `memberships`, as it does today; only a binding it never heard waits.
|
||||
Past two minutes without an answer, the bound of its other first views (design 25 bus-state §5), it raises its
|
||||
condition `mesh-grants.first-view-unanswered` naming the seat `mesh-broker` and the verb `list`, as that condition
|
||||
names the owner and the tool of its other first views, and it then takes the events from its durable consumer as
|
||||
they come, so the live path never waits on the first view. It keeps asking, from 10 seconds, doubling, at most
|
||||
every 5 minutes, the backoff the bus module keeps for its own first view (design 25 bus-state §1); the answer
|
||||
clears the condition. This form holds until milestone G of design 47 is delivered; from then the grants part, one
|
||||
of G's observers (design 47 G §387), writes `first-view-unanswered` and `binding-unlisted` (§4) as findings in its
|
||||
state `findings`, said as `finding-written` and `finding-cleared` on its finding stream (design 47 G §383) and
|
||||
stated at its start (§384), naming the same things. Why: the design's choice under ADR 0615 §6. How it is checked:
|
||||
§8; from G, G's own checks of §383 and §387.
|
||||
- §6 **The tools that read and steer it.** `grants_first_views` shows the first view `bindings` beside the others:
|
||||
the seat and verb (`mesh-broker.list`), when it was asked and answered, how many entries the answer listed, how
|
||||
many bindings it wrote and rewrote, how many of those it composed by §9 and the entries §9 left unbound (by module,
|
||||
local name and storage type), the bindings it found unlisted, how many asks and hand acts, and whether its
|
||||
condition is raised, kept in the grants part's own state `user-list` under the key `first-views` as the others are.
|
||||
`grants_first_view_again` takes `bindings` among its `state` values, by a person's act with why, recorded as a hand
|
||||
act; its description names it. From G these two tools are served by mesh-sdk's `go/views` as every part's are
|
||||
(design 47 G §245), with `bindings` among the first views they show. Why: tools-are-how-a-module-is-spoken-to. How
|
||||
it is checked: unit (grants part's tools), `bindings` shown and asked again with its hand act recorded.
|
||||
- §7 **What exists today, and its fate.** The live-event path stays whole (design 25 bus-state §4). The bus module's
|
||||
`list`, `show` and `orphans` keep every field they answer and gain `binding`. The grants part reads no other
|
||||
module's bus state and gains no grant on the shared stream `EVENTS` beyond its durable consumer of the three events:
|
||||
a missed event is recovered by asking its owner, never by reading the stream back by hand. **The order, and what
|
||||
the first wording missed.** This line first said the order between the repositories needs no edge of the module
|
||||
graph, because a grants part meeting an answer without `binding` wrote nothing from it and kept working on the
|
||||
events. It missed that the bus module's own build goes through the registry (corrected 2026-11-11, issue 1966): its
|
||||
image build asks the seat `container-image-registry`'s `mirror` for its base image, which the registry holder
|
||||
serves only with its state `mirrors`, a binding whose `created` the grants part never heard; so the bus module's
|
||||
new build could not be built while the grants part waited for that build's `binding`, a circle. §9 breaks it: the
|
||||
grants part composes the key-value and object-store bindings from the running bus module's answer, so it reaches
|
||||
the machines in a delivery of the grants part (mesh-delivery) alone, with no new build of the bus module (the
|
||||
operator, 2026-11-11 13:30 CET, in the terminal: "Yes, build it"). The order is then the grants part with §9
|
||||
before the bus module's build of this milestone, which mesh-contracts, mesh-sdk and the nats module (mesh-catalog)
|
||||
carry; the edges between the grants part and the bus module otherwise lie on the foundation's cycle, where ADR 0615
|
||||
§2 has the owner's answer and the reader's wait carry the order.
|
||||
- §8 **How it is checked.** **Whole**: a case in the grants part's existing whole-module integration test (mesh-delivery
|
||||
is over its integration budget, so no new test function): a fixture bus module answers `mesh-broker.list` with an
|
||||
entry whose `created` was never said, and after the grants part starts, the module's membership holds that
|
||||
entry's binding; a binding the grants part holds that the answer does not list stays and raises
|
||||
`mesh-grants.binding-unlisted`; with `list` unanswered, the part composes every account, raises
|
||||
`mesh-grants.first-view-unanswered` naming `mesh-broker` and `list` past its bound, and applies a `created` heard
|
||||
meanwhile; an answer that comes after a `dropped` heard since the ask does not write the dropped binding back.
|
||||
**Unit**: the bus module's tools (§2), the grants part's tools (§6), and a table of the grants part's reconcile
|
||||
with one row per case of §4. **Live**: after the delivery, at the grants part's first start, the registry holder's
|
||||
membership names every entry its manifest asks for (`grants_membership`), and `grants_first_views` shows
|
||||
`bindings` answered with no binding unlisted. **§9's cases**: unit rows in the same reconcile table, one per
|
||||
case of §9 (a key-value and an object-store entry composed; a stream and a ledger entry left unbound and logged;
|
||||
an entry without `settings` left unbound; an entry whose `settings` lack the bus object named from its `bus-name`
|
||||
left unbound and logged; an answer carrying `binding` taken as it is, nothing composed). **§9 live**: after the
|
||||
delivery of the grants part alone, before any new build of the bus module, `grants_membership` for the registry
|
||||
holder on the machine it runs on lists `holds`, `windows` and `mirrors` beside the entries it held, and
|
||||
`grants_first_views` shows `bindings` answered with those three composed by §9.
|
||||
- §9 **An answer without `binding` is composed from the bus module's own fields, for the storage types it names
|
||||
whole.** A bus module's build from before this milestone answers `list` with no `binding` on any entry; each entry
|
||||
still carries its `module`, `local-name`, `bus-name`, `storage-type`, `template`, `options`, `fill`, and `settings`
|
||||
keyed by the bus object's name (a key-value bucket's stream as `KV_` and its bus name, an object store's as `OBJ_`
|
||||
and its bus name, a stream by its own name). For an entry of such an answer the grants part composes the binding
|
||||
only when its `settings` hold the bus object its `bus-name` names (so the bus holds it, as §2 has `binding`
|
||||
present only on a held entry), and only for the storage types whose binding the answer names whole: for
|
||||
`key-value`, `local` from `local-name`, `storage` from `storage-type`, `template` from `template` and `bucket` from
|
||||
`bus-name`; for `object-store`, the same with `object-store` from `bus-name`. It composes nothing for a `stream`
|
||||
(the answer does not carry its subjects) or a `ledger` (it does not carry its parts), nor for an entry whose
|
||||
`settings` lack its bus object: those are left as today, unbound until the bus module's `created` or `recreated`,
|
||||
or a later answer that carries `binding`. An answer is taken as one without `binding` only when no entry in it
|
||||
carries the field; an answer where any entry carries it is applied by §4 alone, nothing composed. **It is the bus
|
||||
module's own word**: every field the binding takes is the bus module's answer about what it holds, from its seat,
|
||||
and the grants part adds no name and reads no bus state of its own, so ADR 0595 §1 and §7 (the binding is the bus
|
||||
module's to say and the grants part's to write) and ADR 0596 §2 hold. **How it is said**: once per answer, a log
|
||||
line of the grants part naming the seat `mesh-broker` and the verb `list`, that the answer carries no `binding`,
|
||||
how many bindings it composed and every entry it left unbound by module, local name and storage type; each
|
||||
binding then written is logged by §4 with the cause "first view, composed from `list`'s fields". `grants_first_views`
|
||||
shows the same (§6). **Its fate**: it stays after the bus module's build of this milestone is everywhere, since it
|
||||
acts only on an answer no entry of which carries `binding`, which that build never gives, and covers a bus module
|
||||
held back or set back to an earlier build. Why: the operator's answer of 2026-11-11 13:30 CET (in the terminal,
|
||||
to lead-47, for issue 1966), under ADR 0595 §1 and §7; the fields, the storage types and the log are the design's
|
||||
choice following that answer and the bus module's `list` answer as it stands today (issue 1966, read live
|
||||
2026-11-11). How it is checked: §8, §9's cases and §9 live.
|
||||
@@ -0,0 +1,399 @@
|
||||
---
|
||||
layer: to-be
|
||||
design: 25
|
||||
milestone: bus-state
|
||||
updated: 2026-11-11
|
||||
code: [mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-catalog modules/nats]
|
||||
decisions:
|
||||
- "ADR 0595"
|
||||
- "ADR 0596"
|
||||
- "ADR 0597"
|
||||
- "ADR 0259"
|
||||
- "ADR 0363"
|
||||
- "ADR 0386"
|
||||
- "ADR 0387"
|
||||
- "ADR 0436"
|
||||
- "ADR 0564"
|
||||
- "ADR 0578"
|
||||
- "ADR 0603"
|
||||
- "ADR 0612"
|
||||
- "ADR 0613"
|
||||
- "ADR 0614"
|
||||
- "ADR 0615"
|
||||
- "ADR 0619"
|
||||
- "ADR 0620"
|
||||
- "ADR 0621"
|
||||
- "ADR 0622"
|
||||
- "ADR 0625"
|
||||
- "ADR 0626"
|
||||
- "ADR 0627"
|
||||
- "ADR 0631"
|
||||
---
|
||||
|
||||
# Design 25, milestone bus-state — what a module keeps on the bus, created by the bus module
|
||||
|
||||
**What it builds.** The lines of [design 25](../25-the-bus-on-nats.md) § 12 that are built under issue 1966: who
|
||||
creates a module's bus state and how the module receives its binding (§ 12.2), the seat `mesh-broker`'s verbs,
|
||||
events and fill (§ 12.3), the ledger (§ 12.4), the calls (§ 12.5) and the order of the build and the move (§ 12.6).
|
||||
The three layers and the templates are the design's main file (§ 12.1), and so is how each rule is checked
|
||||
(§ 12.7). The sections keep their numbers, so "design 25 § 12.2" still names them. *Moved here from the
|
||||
main file on 2026-11-11, when the main file was put in the shape of playbook 02, and numbered; their words are
|
||||
unchanged but for the note on bus-state §4 that design 25 bindings-first-view amends it.*
|
||||
|
||||
**How a line is cited.** Each statement is cited as "design 25 bus-state §n". A bare "§n" inside a statement is a
|
||||
clause of the record that statement cites, as it was before the move; "§ 12.n" is a section of this design.
|
||||
|
||||
### 12.2 Who creates it, and how a module receives it
|
||||
|
||||
- §1 **The trigger is the manifest validator's verdict** (ADR 0595 §4), heard as the indexer's event `manifest-accepted`, which
|
||||
carries the module's entries whole, so the bus module never reads the indexer's state and never polls. The indexer
|
||||
says it for every newly accepted manifest, with its entries empty when it asks for none, and the bus module reads an
|
||||
empty one as "this module asks nothing now", which is what its orphans need. A module whose manifest leaves the
|
||||
trunk (its directory deleted) gets an empty `manifest-accepted` too, so its entries reach `orphans` and raise
|
||||
`orphaned`; a manifest the manifest validator set aside to wait gets none, so its entries stay asked. The indexer's tool
|
||||
`indexer_say_accepted` says `manifest-accepted` again for one module by hand, from the verdict it holds, recorded as a
|
||||
hand act, for a verdict the bus module is thought to have missed. It goes, as every event of the
|
||||
indexer, on the module events stream (ADR 0363) under the indexer's own subjects. A publish of it that fails is
|
||||
published again, and one still failing writes the indexer's finding naming the module, cleared once said (today
|
||||
the indexer only logs it). It hears the verdicts on a durable consumer, so one given while it was down is heard
|
||||
when it is back. **Its first view** at start is the indexer's
|
||||
tool `indexer_bus_state` (every accepted manifest's bus-state entries, by module and digest), asked once; the
|
||||
events keep it current after that, and a stream that keeps a verdict only for its age is never its source. A
|
||||
verdict replayed from its consumer is applied only if newer than what the first view answered for that module, by
|
||||
the indexer's revision alone (a digest has no order), so a replay never takes an entry back. An `indexer_bus_state`
|
||||
that does not answer makes the bus module's health unhealthy, naming the verb, and is asked again on a bounded
|
||||
backoff (from 10 seconds, doubling, at most every 5 minutes); nothing is created or orphaned meanwhile. Beside the
|
||||
backoff, the bus module's tool `nats_first_view_again` asks `indexer_bus_state` at once by hand, recorded as a hand
|
||||
act. It keeps
|
||||
what it last heard per module in its own state `asked` (an old `state` section, its own kept data staying there,
|
||||
ADR 0595 §10), from which `list`, `check` and `orphans` answer; a key goes only on an empty `manifest-accepted` for
|
||||
the module, never because a manifest the manifest validator set aside to wait is missing from the first view. Then it runs `check` over what it holds, creating what
|
||||
is missing. The manifest validator checks every entry once, at the merge check and at take-in (ADR 0578; ADR 0595 §8), and also
|
||||
each ledger type's operations: a set-entry or a remove names `into` and `key`, `fields` and an index's `on` name
|
||||
fields of the schema, and `digest-of` names a text field.
|
||||
- §2 **A module that asks bus state waits for the bus module** (ADR 0614 §5). Each bus-state entry of a manifest is a
|
||||
requirement on the provision `bus-state`, which the bus module's manifest provides; the module graph reads it as any
|
||||
requirement, so the assigner holds a consumer's new build until a build of the bus module that offers `bus-state`
|
||||
runs healthy (ADR 0603 §5). That each entry exists before its user starts is the bus module's own work, above, not
|
||||
an edge.
|
||||
- §3 **The bus module has a process of its own** beside its tool bundle, with a declared health check. Its credential
|
||||
creates streams and buckets under modules' names only, and deletes only through `drop` and `recreate`. Its health
|
||||
turns unhealthy, naming the cause, when an accepted verdict has waited on its durable consumer, unanswered, for
|
||||
more than three minutes, or when its last pass over fill, which runs every minute, is more than three minutes old, so the only guard against a store filling never stops without a sound; another module's entry past the raise mark
|
||||
raises that module's condition and leaves the bus module healthy. **Its process has a process account of its own,
|
||||
granted more than any other process's** (ADR 0626 §1; ADR 0436 §1, §2). The process declares `bus` as any process
|
||||
may, with one key more, `keeps`, which is allowed on this process alone; the grants part composes the account from
|
||||
that declaration as it composes every process account, and the vault mints it as the module's own secret. Beyond
|
||||
what a process's `bus` grants, `keeps` grants the bus's create and update subjects for streams, buckets and object
|
||||
stores under modules' names only (`<module>_*`), the snapshot request for an entry before `recreate` or `drop` with
|
||||
its chunks' acknowledgement subjects and its own inbox, and the delete subjects for those two acts. It is granted
|
||||
no subject on which a tool or a seat's verb is answered (ADR 0436 §3), and no subscription to any restricted
|
||||
stream's own subjects: the snapshot is the one way a restricted stream's data reaches it, kept only in the bus's
|
||||
snapshot store, and the process reads no message of it. No other account holds those subjects, the tool runner's
|
||||
included. **The snapshot account stays as it is** (ADR 0626 §2, § 3 above). **The manifest validator refuses any other bus
|
||||
declaration of the bus module** (ADR 0626 §3): anything it would say or hear on the bus beyond its process's `bus`
|
||||
and the bus state the foundation keeps on the old sections (ADR 0595 §10), `keeps` on any other process or module,
|
||||
and a `bus` of its process that names a subject on which a tool or a verb is answered.
|
||||
- §4 **The binding travels on the bus module's events** (ADR 0595 §1, §7, as their progressive insight of 2026-11-03
|
||||
has it, waiting for the operator's Approve). The grants part, which issues every membership, hears them on a durable
|
||||
consumer: it writes the **binding** (the names on the bus) into the module's membership on `created`, on every
|
||||
machine the module is assigned, rewrites it on `recreated` and removes it on `dropped`, kept in its own state
|
||||
`memberships`, so no part writes another's state and a binding outlives the 7 days its event is kept on the shared stream (§ 12.3).
|
||||
*Amended 2026-11-11 by design 25 bindings-first-view §1 to §5*: the events are no longer the binding's only
|
||||
source; at every start, and when asked again by hand, the grants part also takes a first view of every binding
|
||||
from the seat `mesh-broker`'s `list`, so a missed event loses no binding for good. A module whose binding is missing at start fails loudly, naming the entry; it never starts blind. A `refused`
|
||||
create hands no binding. **A refused or overdue creation is loud**: overdue means no `created` or `refused` within
|
||||
five minutes of an accepted verdict that asks for the entry. The condition is raised by the watchman, which today raises the manifest validator's
|
||||
refusals as findings and gains, through its `consumes`, the accepted verdicts and the seat `mesh-broker`'s `created`
|
||||
and `refused`; never by the bus module, which cannot raise it while it is down; it names the module
|
||||
and the entry; a later `created` clears it, and so does a later accepted verdict of that module that no longer asks
|
||||
for the entry.
|
||||
- §5 **A foundation part's first view of another's state is a tool its owner answers in a runtime of its own** (ADR 0615,
|
||||
ADR 0621). The four
|
||||
foundation parts that read each other (the grants part, the tool runner, the indexer and the assigner) stop
|
||||
reading the indexer's and the assigner's bus state in the milestone of ADR 0612, with the other 19 readers of that
|
||||
state (ADR 0615 §1). **Each owner serves them as tools of its own module**, the indexer `manifests`,
|
||||
`modules`, `verdicts` (ADR 0622 §1) and `graph` (ADR 0631 §1), the assigner `placement`, `seat-holders` and `wants`, and declares `runs-as`, so its tools bundle runs in
|
||||
a runtime of its own on the module's own bus account, never in the node's tool runner, and the tool runner never
|
||||
asks through itself (ADR 0621 §1). No new manifest form is added: a process still answers no tool call or seat verb
|
||||
(ADR 0436 §3). **Each verb is granted to exactly its readers** (ADR 0621 §2, as ADR 0615 §3 lists them): `placement`
|
||||
to the grants part, the tool runner and the indexer; `seat-holders` to the tool runner; `wants` to the grants part;
|
||||
`manifests` and `modules` to the grants part, the tool runner and the assigner; `verdicts` to the grants part
|
||||
alone, its `callers` list holding the grants part only (ADR 0622 §1); `graph` to the assigner alone, its `callers`
|
||||
list holding the assigner only, the indexer itself refused, and the assigner's `reads` entry of the indexer's
|
||||
`graph` goes (ADR 0631 §1). These seven are the first-view tools, four of the indexer and three of the assigner.
|
||||
`verdicts` follows the first-view rules whole (ADR 0622 §2, under ADR 0615 §4 to §6): its answer carries the
|
||||
revision, the grants part then follows `manifest-accepted` and `manifest-refused` and asks once more on a gap in
|
||||
their revisions, it raises the condition naming the indexer and `verdicts` past the bound and never reads the
|
||||
indexer's state, and `grants_first_views` and `grants_first_view_again` cover it. `graph` follows them whole too
|
||||
(ADR 0631 §2, under ADR 0615 §4 to §6): its answer carries the revision, the assigner then follows the indexer's
|
||||
`indexer.views` events for the bucket `graph`, which carry the revision, and asks once more on a gap, said in its
|
||||
log; started while the indexer is down, it raises the condition naming the indexer and `graph` past the bound and
|
||||
never reads the indexer's state; and `assigner_first_views` and `assigner_first_view_again` cover it. A reader declares each in `invokes`
|
||||
as `<module>.<tool>` only where it is granted, which is the grant the grants part composes as for any tool, and
|
||||
each tool checks the proved caller against a `callers` list holding exactly its readers and refuses any other, the
|
||||
owner itself included. **The manifest validator of the milestone is its own group's** (ADR 0621 §4): since no field is added,
|
||||
the running manifest validator takes the indexer's and the assigner's manifests in as it does today, and the merge check judges
|
||||
them with the manifest validator built from the group's own head; no release of the manifest validator goes ahead of the milestone. **The
|
||||
owner's runtime answers before any first view of its own** (ADR 0621 §3): at once from its module's own state, as
|
||||
soon as it is connected and before it has asked any other part anything, with the revision of its state it answered at,
|
||||
so the indexer and the assigner, each asking the other at start, never wait on each other (§4). The reader then
|
||||
follows the owner's declared events, each carrying the revision of the state it follows, and never asks the verb
|
||||
again on a timer; a gap in revisions makes it ask once more, said in its log (§5). The owners' events are new
|
||||
families of their own, apart from the indexer's `manifest-refused` and `manifest-accepted` and the assigner's
|
||||
`put`: the indexer's `indexer.views` and the assigner's `assigner.views`, one event per changed key with its
|
||||
bucket, key and revision and the key's value whole, or a delete mark for a key removed, so a reader keeps its
|
||||
picture from the events alone and asks the verb only on a gap (ADR 0615 §5). **Both families are declared in
|
||||
their owners' old `streams` sections**, each an entry with `max-age-days` 7 and `max-mib` 256: the old section keeps
|
||||
a stream by limits, which drops its oldest when full (only a work queue refuses the new instead), so a reader that
|
||||
misses a dropped event sees the gap in revisions and asks once more. The indexer's own kept data stays on the old
|
||||
sections (ADR 0595 §10 names it among the foundation); the assigner's goes beside its existing `streams` entry
|
||||
`placement`, under ADR 0619, which extends ADR 0595 §10's foundation to the parts
|
||||
the bus module's own composition, placement, build or delivery needs before it can answer: the assigner, the tool
|
||||
runner, the builder, the checker, mesh-config and the vault. **The size, an estimate**: read on the bus on
|
||||
2026-11-04, the buckets the readers read hold about 3 MiB of current values at the indexer (its manifests about
|
||||
5 KiB a value) and about 1.5 MiB at the assigner (its wants about 0.25 KiB a value). The milestone's one mesh-sdk
|
||||
rebuild rewrites every module's keys, up to three times in its window (design 51 *How many builds*), so a week of
|
||||
it is some 20 MiB on `indexer.views` and some 10 MiB on `assigner.views` with ordinary merges added, each well
|
||||
under its 256 MiB. Each event carries one value whole, the largest today a manifest of tens of KiB, under the bus's
|
||||
largest message, 1 MiB (the bus server's `max_payload`); an owner whose value with its event would pass it emits
|
||||
the key's revision alone, as a gap, so the reader asks the first view once, and the owner writes its finding
|
||||
naming the key.
|
||||
A waiter on an existing event, `manifest-accepted` or the
|
||||
assigner's `put`, never wakes on them. **When an owner's revisions go back**: should an owner's state be rebuilt
|
||||
by the bus module's `recreate`, its revisions start again; a reader that hears a revision lower than the one it
|
||||
holds takes it as a gap and asks the first view again, and keeps what that answer says, whatever revision it held
|
||||
before. A `recreate` that could not keep the data answers empty at revision 0, so the grants part keeps its list
|
||||
in force (below): a login revoked meanwhile stays until the owner's first change, and the grants part's
|
||||
first-view condition stays raised meanwhile, naming the owner, the empty answer and the operator's step: let the
|
||||
owner write its state again from its sources (the indexer by taking each module in again, `indexer_take_in`; the
|
||||
assigner by its next composition of the wants), then ask the grants part's first view again,
|
||||
`grants_first_view_again`. It is cleared by an answer above revision 0. A reader that starts while its owner is down raises a
|
||||
condition naming the owner and the verb past its bound and keeps asking until it answers, never reading the
|
||||
owner's state instead; once it had its first view, it keeps working on what it heard (§6). The bound is longer
|
||||
than an owner's start (two minutes), so a normal start, readers and owners coming up together, notifies nobody;
|
||||
the owner's answer clears the condition. An answer that is empty at revision 0 (an owner that holds nothing yet)
|
||||
is a first view of nothing, never a reason for the grants part to drop a login it holds: it keeps the list in
|
||||
force until an answer at a revision above 0. Each reader's tools say the owner, verb and revision its first view
|
||||
holds, when it asked and when it was answered (a read tool), and ask the first view again by hand (a re-read tool,
|
||||
recorded as a hand act): the grants part's `grants_first_views` and `grants_first_view_again`, the tool runner's
|
||||
`mesh_first_views` and `mesh_first_view_again`, the indexer's `indexer_first_views` and
|
||||
`indexer_first_view_again`, and the assigner's `assigner_first_views` and `assigner_first_view_again`. The tool
|
||||
runner's two are tools of the mesh MCP server, beside `mesh_runtimes`, since the tool runner serves its own state
|
||||
there: the mesh MCP server gains them, and the operator sees them among its tools. **On a fresh mesh** the
|
||||
assigner starts before any new-form bus state exists and needs no binding of the bus module first: its own state
|
||||
and `assigner.views` are on its old sections, which the grants part creates, so it answers the indexer, the grants
|
||||
part and the tool runner their first views while the bus module still waits on it. The order of owner before reader is kept by the module graph's served-verb edge for the 19 readers (ADR 0614 §2), and,
|
||||
for the four, whose verb edges lie on cycles the graph leaves out (ADR 0614 §3), by the owner's answer at once and
|
||||
the reader's wait (ADR 0615 §2).
|
||||
- §6 **The other 19 readers ask the owners' read tools, one per bucket they read** (ADR 0615 §1, §2). Beside the seven
|
||||
first-view tools above, each owner serves a read tool for every bucket of its state one of the 19 reads today,
|
||||
named `indexer_read_<bucket>` or `assigner_read_<bucket>` and marked as a read: fifteen. The indexer's nine:
|
||||
`indexer_read_manifests`, `indexer_read_modules`, `indexer_read_predefined`, `indexer_read_config_candidates`,
|
||||
`indexer_read_candidates`, `indexer_read_packages`, `indexer_read_graph`, `indexer_read_heard` and
|
||||
`indexer_read_taken`. The assigner's six: `assigner_read_placement`, `assigner_read_seat_holders`,
|
||||
`assigner_read_wants`, `assigner_read_findings`, `assigner_read_asks` and `assigner_read_differences`. These are
|
||||
the nine buckets of the indexer and the six of the assigner the 19 read (§ 12.6), `modules`, `wants` and
|
||||
`placement` among them for the status view; the indexer's tenth, `verdicts`, is read by the grants part alone,
|
||||
through its first-view tool. **They are ordinary module tools**: granted, as any tool is, to every
|
||||
reader that declares it in `invokes`, and callable by a session; the `callers` list of ADR 0621 §2, ADR 0622 §1 and ADR 0631 §1 covers
|
||||
only the seven first-view tools. **The status view keeps all its tools**, their names, arguments and answers
|
||||
unchanged, its `indexer_*` and `assigner_*` tools included: as a reader it answers them by asking the owners' read
|
||||
tools instead of reading their buckets. Checked live after the delivery: a session's call of each status view tool
|
||||
answers, and `mesh_search` finds each owner's read tool by its bucket's words.
|
||||
- §7 **Only the owner reaches it** (ADR 0596 §2). The membership grants the owner's account its own entries and no
|
||||
other module's: the new form has no readers. What another module needs of that data it asks through the owner's
|
||||
tools or seat verbs, and what the owner lets others hear is its declared events, subscribed to through
|
||||
`consumes` (ADR 0363, unchanged). A part watches only its own state, and hears another's events (ADR 0596 §4).
|
||||
|
||||
### 12.3 The seat's verbs, events and fill
|
||||
|
||||
- §8 **The seat gains eight verbs.** The seat `mesh-broker`, until now a seat with no verbs, gains these (ADR 0595 §5, §6). It is one of the ten seats
|
||||
defined whole in the milestone of ADR 0612, its contract a package of its own in `mesh-contracts` (ADR 0613 §1,
|
||||
ADR 0627 §1, ADR 0625 §1; design 26 *A seat's definition*), which settles where its definition lives (ADR 0595 §12). An entry's arguments are the module, its local
|
||||
name, its storage type, its template, its options and its configuration; what is held answers the settings held and
|
||||
the fill (count, bytes, the cap); `drop` and `recreate` carry the warrant and why.
|
||||
|
||||
| Verb | Does | Who may call it |
|
||||
|---|---|---|
|
||||
| `list` | every entry, its template, its settings, its fill against its cap | any caller whose role grants it (ADR 0386) |
|
||||
| `show` | one entry, the same | the same |
|
||||
| `check` | compares what the bus holds with what was asked, entry by entry | the same |
|
||||
| `orphans` | entries the bus holds that no manifest asks for; never one whose manifest the manifest validator refused or outgrew, nor one of the foundation's old sections | the same |
|
||||
| `create` | creates one entry now, for an entry an accepted manifest asks for | an act of the bus module's process alone, answered on no subject |
|
||||
| `apply` | applies a difference that only grows: a larger cap, a longer age | the same |
|
||||
| `recreate` | rebuilds an entry whose settings differ in a way that cannot grow, from its data where the storage type allows | the same, on the operator's warrant, which the process verifies itself |
|
||||
| `drop` | removes one orphan | the same, on the operator's warrant, which the process verifies itself |
|
||||
|
||||
- §9 **Four of them are acts of the process alone.** `create`, `apply`, `recreate` and `drop` stay in the seat's definition as acts its holder's process alone performs,
|
||||
answered on no subject (ADR 0626 §4, ADR 0436 §3): they are not verbs whose subjects a credential is granted, but
|
||||
acts on the bus's own create, update and delete subjects, which only the process's account holds, so no module can
|
||||
ask the bus for them. A person asks for them through the bus module's tools, which hand the request to the process
|
||||
through its own state (below); `recreate` and `drop` act only on the operator's warrant (ADR 0259 §6), asked
|
||||
through the operator channel. Before `recreate` or `drop` acts, the process takes a stream snapshot of the entry
|
||||
(ADR 0595 §5), and it acts only once the snapshot is whole. A drifted entry is said, never quietly fixed: only a
|
||||
difference that grows is applied without a person.
|
||||
|
||||
- §10 **Events**: `created`, `refused` (with why), `drifted` and `drift-cleared`, `full` and `full-cleared`, `orphaned`
|
||||
(cleared by a `drop`, or by a manifest asking for the entry again, which emits `created` for it), `recreated` and
|
||||
`dropped` (ADR 0595 §6, as its progressive insight of 2026-11-03 has it, waiting for the operator's Approve);
|
||||
each carries the module, the local name, the bus name, the template, and the settings asked against those held.
|
||||
**They follow every seat's events**: each is said on `mesh.seat.mesh-broker.event.<event>`, on the shared stream
|
||||
`EVENTS` (§ 3), kept 7 days, and heard through `consumes` `mesh-broker.<event>` like any seat's event. **The bus
|
||||
module keeps their history apart**, because a drift or an orphan is read back days later by a person: a stream of
|
||||
its own in its old `streams` section, on its module subjects, kept 30 days (ADR 0363's bound), created by the grants
|
||||
part as today, with a cap of 256 MiB, to which the process appends every event it says. Its own kept data stays on
|
||||
the old sections (ADR 0595 §10), because it cannot wait for its own process to create it. That stream drops its
|
||||
oldest when full, which loses no live state, because every drift, orphan and fill stands as a condition until it
|
||||
clears; the conditions hold the live state. The tool `history` of one entry reads it back: every event the bus
|
||||
module said of the entry, in order. This settles where they are kept, which ADR 0595 §12 left to the design, and
|
||||
needs no new manifest form: a module's stream never takes a seat's subjects, and every seat keeps one form for its
|
||||
events.
|
||||
|
||||
- §11 **Fill is the bus module's one periodic read** (ADR 0595 §6): every minute, for every entry that refuses when full,
|
||||
because the bus says nothing as a store fills. The condition `full` is raised at a raise mark and cleared below a
|
||||
lower clear mark, so an entry sitting at the mark does not flap; the marks are settings of the bus module, by default
|
||||
a raise at 75 percent of the cap and a clear below 70 (ADR 0595 §6, as its progressive insight of 2026-11-03 has it).
|
||||
The grants part's `mesh-grants.history-full` moves to it: the new condition is raised first, then the old key cleared;
|
||||
no manifest on main asks for a history stream or objects today, so the move loses nothing. A bare stream that refuses
|
||||
when full is read the same way. **The conditions**, each naming the module and the entry: `full` (cleared below the
|
||||
clear mark), `drifted` (cleared by `drift-cleared`), `orphaned` (cleared by a
|
||||
`drop` or by a manifest asking for it again) and a refused create (cleared by a later `created`).
|
||||
|
||||
- §12 **Its tools** (tools-are-how-a-module-is-spoken-to): the four reading verbs, marked reads, answer every entry the
|
||||
bus module created and its fill, and `history` its events for one entry; `create` and `apply`, marked acts, and
|
||||
`recreate` and `drop`, marked dangerous and so granted only by name (ADR 0386 §6, ADR 0387), are its tools for a
|
||||
person. **A person's request reaches the process through the process's own state** (ADR 0626 §4), because a process
|
||||
account answers no tool call (ADR 0436 §3). Such a tool, in the bus module's bundle, holds no grant of the bus's
|
||||
create, update or delete subjects, and neither does the tool runner's account: it writes the request into the bus
|
||||
module's state `requests` (an old `state` section, as `asked` is, ADR 0595 §10), stamped with the caller the tool
|
||||
runner proved and recorded (ADR 0386 §8), and watches that key for the process's answer, never asking again on a
|
||||
timer. The process watches its own state `requests` (ADR 0596 §4) and takes a request up when it is written. The stamp names
|
||||
who asked, for the record; it is no boundary, because every session on a machine reaches the bus as the tool
|
||||
runner's account (ADR 0436 §9). **So the process verifies the operator's warrant itself**: for `recreate` and `drop`
|
||||
it trusts nothing in the request, reads the operator channel's record of the ask under its own account and checks
|
||||
that the warrant binds exactly this act on this entry (ADR 0259 §6), as the mail sender does (ADR 0436 §9); a request
|
||||
without a valid warrant it refuses, deleting nothing and writing why into its answer; with one, it takes the entry's
|
||||
stream snapshot first. **The process wakes on the warrant, never on a timer**: a warrant is said on the operator
|
||||
channel's `decided` for the asker that raised the ask (ADR 0259 §6; ADR 0620 §2), here the bus module itself, which
|
||||
hears its own `decided` on a durable consumer of the process, whose filter the grants part derives from the
|
||||
process's `uses` of the operator channel seat (not from a `consumes`, which a process has none of), and runs its pass over the waiting `recreate` and
|
||||
`drop` requests when one arrives, so a warranted `drop` acts within seconds of the operator's answer, and one given
|
||||
while the process was down is heard when it is back. **A warrant acts once** (ADR 0259 §6): before it acts, the
|
||||
process claims the warrant's ask id by writing the key `acted.<ask id>` in the state `spent` of the process's own bus state, which the tools bundle is not given (a person's request and the
|
||||
record of what it spent are never in one bucket, and no tool can write the record), created by compare-and-set after the snapshot and before any deletion, and a request naming an ask it already
|
||||
acted on is refused, deleting nothing and saying which act that warrant already performed. **An ask lasts 12 hours**,
|
||||
shorter than the 24 hours `requests` keeps a request: a request whose ask ended unanswered is ended and said, and a
|
||||
warrant is not acted on once its ask ended; the tool raises the ask before it hands the request over. A request
|
||||
that cannot act yet says why in its field Waiting, and a hand act the process could not record is written on the
|
||||
request in its field Unrecorded. The tool `nats_requests` reads the waiting and ended requests, and the warrants
|
||||
spent from `spent`, which is declared `read-only: true` (design 32 § 4) so that the tool reads it and holds no publish on it, and drops a waiting request by hand, recorded as a hand act. `create` and `apply` need no warrant, since they only make or grow what an accepted manifest
|
||||
asks for, and the process refuses either for an entry no accepted manifest asks for. A tool acts only for a caller
|
||||
whose role grants it; the process records each act as a hand act (`mesh-hand-acts`) and answers what it did. What
|
||||
stands between the tool runner's account and the process's delete power is that check of the warrant, so its tests
|
||||
are this section's central check (§ 12.7, in the main file). **The mesh deletes kept data only through `drop` and `recreate`** (ADR 0595 §4, as its
|
||||
progressive insight of 2026-11-03 has it): the bus module's older remove tools (its stream, bucket and consumer
|
||||
removals, ADR 0564) refuse an entry of the new form, naming `drop`'s tool, and keep working for the old sections. The bus seat's `apply` (a difference that grows) and the
|
||||
ledger's `apply` (§ 12.4) are two verbs on two subjects, granted apart; this design writes them qualified. Its health
|
||||
names the process's last pass, the entries it could not create, and the entries past the raise mark.
|
||||
|
||||
### 12.4 The ledger
|
||||
|
||||
- §13 **The ledger is a storage type of its own.** A **ledger** is a storage type whose state is derived from its history (ADR 0597). It is built on a stream and
|
||||
key-value buckets, and asked for with the template ledger.
|
||||
|
||||
```
|
||||
module ──apply(instance, operation)──▶ tool runner on its own machine
|
||||
│ 1 checks the operation against the type
|
||||
│ 2 reads the history's last sequence for the instance; replays if the state is behind
|
||||
│ 3 appends to the history, that sequence expected ──▶ history (stream, a subject per instance)
|
||||
│ 4 derives the state by the operation's effect ──▶ state (key-value, updated at the revision read)
|
||||
│ 5 writes the instance's snapshot ──▶ snapshot subject (latest only, never expires)
|
||||
▼
|
||||
answers: taken, with its sequence (and "state behind" when the state bucket is full)
|
||||
module's process: a watcher keeps each declared index (a second key-value bucket) from the history
|
||||
```
|
||||
|
||||
- §14 **A type** is a schema and the operations allowed on it; an **instance** of a type is named by a path
|
||||
(`listing/a17`, `decision/0290`); every change is an **operation**, kept in the instance's history.
|
||||
- §15 **A ledger effect** is what an operation does to the state, one of a fixed set: set fields, set an entry of a
|
||||
map, append to a list, remove (ADR 0597 §2). An effect copies only the fields its type names, and may add the
|
||||
operation's sequence and author. Where a type pairs a text field with its digest, `apply` refuses an operation
|
||||
whose digest does not match. A module's own rules are checked by the module before it calls `apply`, never by the
|
||||
effects. Only the owning module calls `apply` on its ledger, for every effect, remove among them; a person
|
||||
removes through the owner's tools. This settles who may call the remove effect, which ADR 0597 §13 left to the
|
||||
design.
|
||||
- §16 **One writer wins, and the state never goes back** (§5): the expected sequence makes the bus refuse a racing
|
||||
writer; the state is written as an update of the revision read, never a plain put; when that update loses to
|
||||
another writer, `apply` answers success with its operation's sequence and re-reads, never appending again.
|
||||
- §17 **A full state bucket loses no operation** (§6): `apply` answers that the operation is taken, with its sequence,
|
||||
and that the state is behind because the bucket is full; the next `apply` after room is made catches it up.
|
||||
- §18 **The author is proven** (§7): `apply` stamps the owning module's authenticated account and the account that
|
||||
called the owner's tool. The owner passes that author on by naming the tool call it serves: the tool runner, which
|
||||
served that call and proved its caller, stamps the caller it proved, so the module cannot set the author to words of
|
||||
its own; an `apply` naming no call in flight carries the owner's account alone, and one naming another module's
|
||||
call is refused. A copy of old records (design 55 § 8.1) is stamped with the owner's account and keeps the old
|
||||
author as a separate field marked imported, never as the proven author.
|
||||
- §19 **Every instance has a snapshot** (§8): the latest state on a subject of its own, never expiring, written by every
|
||||
`apply`. It makes a kept ledger's rebuild fast and an expiring ledger's possible; a repair that finds it behind
|
||||
the state rewrites it.
|
||||
- §20 **Only the tool runner's ledger credential publishes on a ledger's history** (§9): one per module assignment, that
|
||||
is per node the module runs on, the account `ledger.<node>.<module>` in the tool runner's own secret `ledgers` on
|
||||
that node (ADR 0597 §9, as its progressive insight of 2026-11-03 has it, waiting for the operator's Approve), held by
|
||||
the tool runner and never by the module's own code, whose account may read its ledger and not publish on it. A
|
||||
person's `rotate` of that secret is refused by the vault, as a bus account's is. The grants part issues it with the
|
||||
module's membership on that machine, scoped to the subjects of that module's ledger
|
||||
histories and snapshots, and removes it when the module is unassigned from the machine. Only the
|
||||
owner's account reads the history, the snapshot and the state.
|
||||
- §21 **An index** is a second key-value bucket kept by a watcher in the owning module's process, rebuildable from the
|
||||
history; it raises a condition when it lags behind (§4).
|
||||
|
||||
### 12.5 The calls, and where they live
|
||||
|
||||
- §22 **The new calls live in a package of their own.** The new calls live in a small mesh-sdk package of their own, served by the tool runner and imported only by the
|
||||
modules that use them; the module library, which 112 modules import, does not change (ADR 0595 §9). They are the
|
||||
object store's (put, get, list, delete, info, watch); a stream's read-back and fill; a work queue's size and dead
|
||||
letters; and the ledger's: `apply`, `get`, `history`, `watch`, `query`, and its repairs `rebuild` (the state from
|
||||
the history or the snapshot), `reindex`, `snapshot` and `lag` (ADR 0597 §10). The tool runner serves the repairs as
|
||||
tools of every module that owns a ledger, limited to its own ledgers, so no owner declares them and the next ledger's
|
||||
owner has them on its first day. mesh-sdk's catalog and membership
|
||||
packages change once for the typed `requires` entry and the binding, rebuilding about 30 modules.
|
||||
|
||||
### 12.6 The move, and the order of the build
|
||||
|
||||
- §23 **The build, each step undoable on its own** (ADR 0595 §10): (1) mesh-sdk's typed `requires`, binding and calls
|
||||
package; (2) the manifest validator's rules; (3) the bus module's process, verbs and events, creating only for the new form, with the grants part's writing of
|
||||
the binding on `created`; (4) the tool runner's calls, the ledger's `apply` and its ledger credential among them; (5)
|
||||
modules move, each owner first. Each undo: undoing step 2 refuses the new form again, which no module uses before
|
||||
step 5; undoing step 3 leaves what it created in place, and a module moved after it fails loudly on its missing
|
||||
binding; undoing step 4 leaves a moved module without its calls, which the placement holds off that machine. The
|
||||
placement holds a moved module off a machine whose tool runner does not serve the typed binding and the calls the
|
||||
module asks for (a ledger's `apply` among them), and says so; where the placement reads what a tool runner serves is
|
||||
left to the build of step 4 and tracked on issue 1966.
|
||||
|
||||
- §24 **A module moves when it is next touched** (ADR 0595 §11), keeping its local names and carrying its old settings
|
||||
as options, so its bus objects and data stay and no drift is raised. It asks for each entry once: an old section
|
||||
and the new form are never both used for one local name. During the move the grants part keeps creating the old
|
||||
sections' entries, so each entry has one creator. The indexer counts the modules on each old section, a tool reads
|
||||
the count, and the manifest validator refuses a section once its count is 0. The foundation's own kept data (the bus module's,
|
||||
the grants part's and the indexer's) stays on the old sections for good and is not counted: those modules cannot
|
||||
wait for a process that needs them first. ADR 0619, extending ADR 0595 §10's foundation, adds the parts the bus module's own composition, placement, build or delivery needs before it can answer:
|
||||
the assigner, the tool runner, the builder, the checker, mesh-config and the vault; their new state and streams of
|
||||
this milestone go on their old sections too (§ 12.2; design 51 *The move*; design 32).
|
||||
|
||||
- §25 **Readers move after their owners** (ADR 0596 §3). Today 195 `reads` entries in 37 modules read another module's
|
||||
bus state (counted 2026-11-03 02:00). **A correction of fact**: the indexer's and the assigner's state, counted again
|
||||
from every manifest on main later that day, is read by 23 modules, 10 buckets of the indexer (`verdicts`, read by the grants part, among them) and 6 of the assigner,
|
||||
not by the 19 the first plan counted; and 151 state entries were counted, not 158 (the template table of § 12.1 keeps
|
||||
the first count, which chose the templates). The owners first gain their events, first-view tools and read tools: the
|
||||
indexer a read tool per bucket its readers read, the assigner the same (§ 12.2 lists them). Then all 23 readers move in the milestone of ADR 0612, the
|
||||
four foundation readers included, by the first view of § 12.2 (ADR 0615 §1), the grants part's of the indexer's
|
||||
`verdicts` by its first-view tool `verdicts` (ADR 0622) and then `manifest-accepted` and `manifest-refused`, and the
|
||||
assigner's of the indexer's `graph` by its first-view tool `graph` (ADR 0631) and then `indexer.views`; their order is the module graph's, not
|
||||
a review reading an owner delivered (ADR 0615 §2). The status view's move keeps every tool it serves, each answered by
|
||||
asking the owners' read tools of § 12.2. **The node-engine's reads granted in code stay**: the grants part's code
|
||||
grants each node's node-engine, with no manifest declaring them (`CandidateReads` and the composed reads beside it),
|
||||
the indexer's `candidates` and `verdicts` and its own node's keys of the assigner's `wants`. They are not among the
|
||||
23 readers of ADR 0615 §1 and move in the next milestone, queued on issue 1966. The manifest validator still refuses a moved module's subscription to an event
|
||||
its owner does not declare. Every other reader moves when its module is next touched. These moves are tracked by
|
||||
issue 1966.
|
||||
@@ -7,9 +7,28 @@ code:
|
||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||
- mesh-catalog modules/nats (to be written)
|
||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||
- mesh-tools node-tools/internal/bus (a module's state, ADR 0201)
|
||||
updated: 2026-10-10
|
||||
- mesh-tools node-tools/internal/bus (a module's state, ADR 0201; the new calls and apply, ADR 0595, ADR 0597)
|
||||
- mesh-sdk (the typed bus-state entry, its binding and the new calls' package, ADR 0595)
|
||||
- mesh-delivery (the manifest validator's rules and the grants part's binding, ADR 0595)
|
||||
- mesh-contracts seats/mesh-broker (the seat's definition: `list` answers each entry's binding)
|
||||
updated: 2026-11-11
|
||||
decisions:
|
||||
- "ADR 0595"
|
||||
- "ADR 0626"
|
||||
- "ADR 0631"
|
||||
- "ADR 0620"
|
||||
- "ADR 0259"
|
||||
- "ADR 0627"
|
||||
- "ADR 0436"
|
||||
- "ADR 0619"
|
||||
- "ADR 0615"
|
||||
- "ADR 0621"
|
||||
- "ADR 0622"
|
||||
- "ADR 0614"
|
||||
- "ADR 0613"
|
||||
- "ADR 0612"
|
||||
- "ADR 0596"
|
||||
- "ADR 0597"
|
||||
- "ADR 0289"
|
||||
- "ADR 0264"
|
||||
- "ADR 0235"
|
||||
@@ -38,6 +57,17 @@ under which subject, with which guarantee, under whose account; how a node joins
|
||||
reaches a tool; how the mesh moves from the bus it has to this one; and how each claim is checked.
|
||||
Prose and diagrams only; no configuration is pasted.
|
||||
|
||||
**Amended, 2026-11-03** (ADR 0595, ADR 0596, ADR 0597): what a module keeps on the bus is its **bus state**, a
|
||||
provision of the bus module asked for by template, created by the bus module, read by its owner alone; and the
|
||||
**ledger** is a storage type of its own. § 12 is the design; the `KV_<module>_<name>` row of § 3 and the module's
|
||||
state in § 4 hold for a module still on the old sections.
|
||||
|
||||
**Amended, 2026-11-11** (ADR 0595 §1, §7; ADR 0615 §3 to §6; the operator, 2026-11-11, in the terminal: "Ask the
|
||||
bus module"): the grants part no longer learns a binding only from the bus module's live events; at every start and
|
||||
when asked again by hand it takes a first view of every binding from the seat `mesh-broker`'s `list`, so a missed
|
||||
`created` loses no binding for good (milestone bindings-first-view). The same change puts this design in the shape
|
||||
of playbook 02: § 12.2 to § 12.6 moved, unchanged and numbered, into the part of milestone bus-state.
|
||||
|
||||
## 1. What the bus is for
|
||||
|
||||
**The bus is where the mesh happens.** Not a transport the mesh sends things over — the place a
|
||||
@@ -129,7 +159,7 @@ mesh already has a shape for that — a seat's `accept` subjects, on a work queu
|
||||
holders, which is what a build queue shared by several machines *is*. Keeping a second mechanism for it
|
||||
meant two things to reason about and two places for a permission to be wrong. A build's outcome is the
|
||||
seat's own event — which is why the control branch loses its copy too: one publish reaches whoever
|
||||
asked, the controller that records it and the catalogue that places it — the fan-out a shared exchange gave for free, as a derived subject rather than a
|
||||
asked, the controller that records it and the catalog that places it — the fan-out a shared exchange gave for free, as a derived subject rather than a
|
||||
configured topology.
|
||||
|
||||
**Revised 2026-09-26** ([design 29](32-what-a-module-declares.md)): a module's events and tools
|
||||
@@ -179,7 +209,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|
||||
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>`; and `mesh.again.>` (ADR 0264) | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-08, ADR 0264:* that stream is DEAD_LETTERS, below; each consumer also filters `mesh.again.<consumer>.>`, where a message it gave up on is delivered again to it alone. *2026-10-01:* a build's whole log is here too, as the build seat's `log.<build id>` events (ADR 0157) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
|
||||
| DEAD_LETTER_NOTICES | the server's maximum-deliveries advisory | work queue, a week; one consumer (the controller), no `max-deliver` | the server's word that a consumer gave up on a message, kept whether or not a controller is listening, until the controller has copied the message (ADR 0264) |
|
||||
| DEAD_LETTERS | `mesh.events.dead.<stream>.<consumer>` | limits: no age, 256 MiB, refuses new when full; a day's de-duplication | every message a consumer gave up on, with its consumer, subject, deliveries and when, until a person delivers it again or drops it with why (ADR 0264); a `max-deliveries` condition is open per consumer while it holds any |
|
||||
| `KV_<module>_<name>` | `$KV.<module>_<name>.>` | the newest value per key — as many past values as the owner declared — no age unless the owner declared one; a value at most 256 KiB, a bucket at most 64 MiB | a module's state (ADR 0201): one per name in a manifest's `state`, created from the catalogue on every raise, so it exists before its owner runs anywhere; kept when the module is unassigned, because what it holds is data |
|
||||
| `KV_<module>_<name>` | `$KV.<module>_<name>.>` | the newest value per key — as many past values as the owner declared — no age unless the owner declared one; a value at most 256 KiB, a bucket at most 64 MiB | a module's state (ADR 0201): one per name in a manifest's `state`, created from the catalog on every raise, so it exists before its owner runs anywhere; kept when the module is unassigned, because what it holds is data. *2026-11-03, ADR 0595:* a module's kept data in the new form is created by the bus module from `requires`, by template (§ 12) |
|
||||
|
||||
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
||||
call is a timeout the caller already handles.
|
||||
@@ -193,6 +223,9 @@ holding `mesh-broker` — has an account of its own whose only grant is the snap
|
||||
names, a stream's information, the snapshot request and its acknowledgements, and its own inbox. The
|
||||
night's backup takes every stream through it ([to-be 43](43-backups-against-mistakes.md)). The
|
||||
controller stays the only writer of stream definitions; the writers table holds that at composition.
|
||||
*Amended 2026-11-04, ADR 0626 §2:* that account keeps exactly this grant; beside it, the bus module's process holds
|
||||
a process account of its own for the bus state it creates (§ 12.2), so the module holding `mesh-broker` holds two
|
||||
accounts, each doing only what its record grants.
|
||||
|
||||
### The store window, and what moving it into the server changes
|
||||
|
||||
@@ -266,7 +299,7 @@ expresses this exactly, per subject, and better than a vhost could:
|
||||
permissions for each consumed event's subject, its tool subjects, and that same inbox prefix.
|
||||
Nothing else. A module that tries to publish outside its emits is refused by the server, not by
|
||||
convention.
|
||||
- **A module's state** (ADR 0201), for whichever principal carries the module — today the machine's
|
||||
- **A module's state** (ADR 0201; for the new form, § 12.2: the owner's account alone, no reader, ADR 0596), for whichever principal carries the module — today the machine's
|
||||
runtime, whose grant is the union of its modules': binding to the bucket, reading a key directly,
|
||||
and an ordered consumer for listing and watching, created and deleted on the bucket's own stream
|
||||
and nothing else's; and, for the owner's instances only, publishing under the bucket's own
|
||||
@@ -321,7 +354,7 @@ signing hierarchy for nothing.
|
||||
|
||||
## 5. The broker as a module
|
||||
|
||||
`nats` is a catalogue module claiming the seat `mesh-broker`
|
||||
`nats` is a catalog module claiming the seat `mesh-broker`
|
||||
(ADR 0079: the seat
|
||||
is the server, and the server changes). It declares one container (a single binary; **JetStream on a host
|
||||
directory bind, not a named volume** — revision, second review:
|
||||
@@ -410,7 +443,7 @@ bridged. It is three things:
|
||||
for anyone else — and nothing on control, nodes or builds. It is issued, sealed to the person's
|
||||
own key, and revoked, like a module's.
|
||||
2. **A client that speaks the bus**: a small program on the workstation that connects as that user
|
||||
over TLS, lists tools by asking the catalogue (`mesh.tools.mesh-catalog.catalog_tools`), and
|
||||
over TLS, lists tools by asking the catalog (`mesh.tools.mesh-catalog.catalog_tools`), and
|
||||
turns each tool into a call — as an MCP server for an agent, and as a command line for a person.
|
||||
It uses the sdk's `Broker` contract on the NATS runtime, so it is the same code path a module's
|
||||
tools use, not a second protocol.
|
||||
@@ -606,7 +639,7 @@ find what changed and why.
|
||||
decides it.
|
||||
- The heartbeat interval and the controller's "quiet" threshold on core NATS without persistence —
|
||||
the same numbers as today are proposed.
|
||||
- Whether the person's client is a catalogue module (runs on an enrolled workstation node) or a
|
||||
- Whether the person's client is a catalog module (runs on an enrolled workstation node) or a
|
||||
standalone program (runs anywhere with credentials). Both, in that order, is proposed.
|
||||
- Leaf nodes: a NATS leaf per machine would make every module's connection local and survive the
|
||||
hub's restart. Deliberately out of scope; noted so it is not forgotten.
|
||||
@@ -617,3 +650,162 @@ find what changed and why.
|
||||
that needs it? ADR 0039's test —
|
||||
*does editing it recompile unrelated modules, and does it change often* — probably says no for
|
||||
one instance; worth asking again if a second module needs the same shape.
|
||||
|
||||
## 12. Bus state: what a module keeps on the bus, and the bus module that provides it
|
||||
|
||||
*Added 2026-11-03* (ADR 0595, ADR 0596, ADR 0597, from research 082; the operator, 2026-11-02 20:45: "The names
|
||||
simply represent named configurations or templates"). Until now a manifest declared what a module keeps on the bus
|
||||
in four sections with four vocabularies (`state`, `logs`, `streams`, `objects`), the grants part created each
|
||||
entry once and never changed it, and nothing compared what the bus holds with what was asked. This section is the
|
||||
design of the one form that replaces them; [design 32](32-what-a-module-declares.md) § 4 says how a manifest asks
|
||||
for it, and [design 55](55-the-knowledge-base.md) § 2.1 is its first ledger.
|
||||
|
||||
**Bus state is a provision of the bus module** (ADR 0595 §1). The **bus module** is the module holding the seat
|
||||
`mesh-broker` (today `nats`). A module asks it for each piece of its **bus state** the way it asks postgres for a
|
||||
database: an entry in `requires`, by its local name, its storage type, its template and the template's options.
|
||||
The bus module creates it; the module receives the names on the bus in its membership; nothing else creates it.
|
||||
|
||||
### 12.1 Three layers
|
||||
|
||||
```
|
||||
layer 3 templates state events work queue log objects ledger
|
||||
│ │ │ │ │ │
|
||||
layer 2 storage types key-value │ │ │ object store ledger
|
||||
built on the bucket │ │ │ │ (a stream and
|
||||
stream │ │ │ │ │ key-value buckets)
|
||||
└──────────┴───────────┴──────────┴──────────┴────────────┘
|
||||
layer 1 base type stream
|
||||
```
|
||||
|
||||
A **storage type** is how the bus keeps the data: the **stream** is the base, and the **key-value bucket**, the
|
||||
**object store** and the **ledger** are built on it, each with calls of its own (ADR 0595 §2). A **configuration**
|
||||
is the settings a storage type is given. A **template** is a storage type with a configuration, predefined and
|
||||
named: six exist, from the uses counted on main (158 state entries, 6 event streams, 4 logs, 3 work queues). An
|
||||
**option** is one setting of a template a module may change, within its bound. A module gives a configuration of
|
||||
its own only where no template fits: the bare stream, or a ledger whose history expires.
|
||||
|
||||
**What keeps the data safe is fixed, never an option** (ADR 0595 §3). Each template fixes what happens when it is
|
||||
full, and the manifest validator refuses a manifest that gives it:
|
||||
|
||||
| Template | On | Fixed | Options, their bounds (default) |
|
||||
|---|---|---|---|
|
||||
| state | key-value | refuses when full; a value at most 256 KiB; the bucket at most 64 MiB; one copy, on disk | past values 1 to 64 (1), 0 refused; time to live (none); a lifetime per key; one bucket per machine |
|
||||
| events | stream | drops the oldest when full; the module's own subjects | age 1 to 30 days (7); cap 1 to 8192 MiB (1024); a grant (ADR 0363 §4) |
|
||||
| work queue | stream | removed once taken; refuses when full; one taker; no grant | deliveries 1 to 10; duplicate detection up to its age; cap; age |
|
||||
| log | stream | refuses when full; never rewritten; an entry at most 256 KiB; one subject per key | cap 1 to 8192 MiB (1024); age 1 to 3650 days (none) |
|
||||
| objects | object store | refuses when full; kept for good | cap 1 to 8192 MiB, required; a grant |
|
||||
| ledger | ledger | as log, plus its type and a snapshot per instance; its state and index buckets as state | cap; keeping days only in a ledger's own configuration |
|
||||
| (none: a bare stream) | stream | the module's own subjects; within the mesh's bounds | its own configuration, which must give "when full" (refuse or drop the oldest), a cap of 1 to 8192 MiB and an age of 1 to 3650 days or none, and may give a grant; the manifest validator refuses one missing any of the three |
|
||||
|
||||
The template log is the **module log** of ADR 0297. The mesh, never the module, names the bus objects and their
|
||||
subjects (ADR 0201 §4): `<module>_<local name>`, and a local name holding the separator is refused so no two
|
||||
modules' names meet.
|
||||
|
||||
### 12.2 to 12.6 Who creates it, the seat's verbs and events, the ledger, the calls and the move
|
||||
|
||||
```
|
||||
manifest validator ─verdict: accepted with the module's bus-state entries whole─▶ bus module
|
||||
│ creates each entry
|
||||
▼
|
||||
grants part ◀──────────────── created, recreated, dropped (module, local name, bus name, binding)
|
||||
│ ◀───── at its start, and when asked again: mesh-broker.list (every entry, its binding)
|
||||
│ writes the binding into the module's membership
|
||||
▼
|
||||
the tool runner reads the membership ──▶ the module's calls reach its own entries, and only those
|
||||
```
|
||||
|
||||
These sections are the lines of milestone **bus-state**, in its part
|
||||
[25-milestones/bus-state.md](25-milestones/bus-state.md), where they keep their numbers. The shape, in short: the
|
||||
indexer's `manifest-accepted` carries a module's bus-state entries whole to the bus module, whose process creates
|
||||
each entry under the module's names and says `created`, `refused`, `drifted` and `drift-cleared`, `full` and
|
||||
`full-cleared`, `orphaned`, `recreated` and `dropped` on the seat `mesh-broker`. The seat's reading verbs `list`,
|
||||
`show`, `check` and `orphans` are answered by the bus module's tools; its acts `create`, `apply`, `recreate` and
|
||||
`drop` by its process alone, the last two on the operator's warrant and after a stream snapshot. The grants part
|
||||
writes each entry's binding into the module's membership, kept in its own state `memberships`, from those events
|
||||
and, at every start and when asked again by hand, from its first view of the seat's `list` (milestone
|
||||
**bindings-first-view**, in its part [25-milestones/bindings-first-view.md](25-milestones/bindings-first-view.md)).
|
||||
The ledger is a storage type whose state the tool runner derives from its history; the new calls are a mesh-sdk
|
||||
package of their own; and the foundation parts take their first views of each other's state from the owner's own
|
||||
tools, then follow its events.
|
||||
|
||||
### 12.7 How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A manifest's bus state is valid, once | the manifest validator's unit tests, each refusal naming the field: an unknown template, an option outside its bound, past values 0, "when full" given, a fixed setting given, a name used twice across storage types, two modules' bus names alike, a `reads` in the new form, an effect outside the fixed set (ADR 0595 §8, ADR 0596 §2, ADR 0597 §2) |
|
||||
| The bus module creates, and the binding follows | live, by tool call: a module asking for a state, a log, objects and a ledger in `requires` receives each binding in its membership, written after `created`, never before; `show` gives each entry's template, settings and fill; a module calling `create` is refused |
|
||||
| The bus module runs whole | one integration test in the nats module's repository that starts the bus module whole against the real bus server: a verdict, then `created`, then the binding in the membership; a restart that catches up a verdict given while it was down; a bucket whose settings differ raises `drifted`, the seat's `apply` of a larger cap clears it with `drift-cleared`; a refused create raises its condition and hands no binding; the process connects on its own process account, composed by the grants part with that account's grants and limits, never on an account without them; a `create` handed through `requests` is answered on the same key, which the tool watches; `recreate` and `drop` without the operator's warrant are refused, and a warranted `drop`, its warrant read from the operator channel's own record of the ask under the process's account and never from a stand-in, takes the entry's stream snapshot before it deletes, and acts within seconds of the bus module's `decided`, with no timer between; the same warrant handed in a second request is refused and deletes nothing; each event said is on `mesh.seat.mesh-broker.event.<event>` and appended to the bus module's history stream, and `history` reads it back; an orphan is listed, raises `orphaned`, and goes only by `drop`; each template at a small cap refuses, events drop the oldest, and `full` is raised at the raise mark and cleared only below the clear mark; `recreated` rewrites the binding and `dropped` removes it; at start the first view from `indexer_bus_state` creates what a verdict given while it was down asked; the older remove tools refuse a new-form entry, naming `drop`'s tool |
|
||||
| A module asking bus state waits for the bus module | the module graph's table test: a manifest with a ledger entry has a requirement edge to the module providing `bus-state`, one without has none (ADR 0614 §5) |
|
||||
| A foundation part's first view | unit tests of each owner and reader (ADR 0615's How it is checked), as table cases, and these cases of the whole-module integration tests that exist today (mesh-delivery is over its integration budget, so no new test function): the indexer's (`modules/mesh-delivery-indexer`, its one whole-module test, which the milestone's packages test merges into), the assigner's (`modules/mesh-delivery-assigner`) and the grants part's (`modules/mesh-grants`), and the tool runner's in mesh-tools: the owner answers its verb at once from what it holds, with its revision, before its own first view; the reader asks once at start, then only on a gap in revisions; a reader whose owner is down raises its condition only past the bound and never reads the owner's state, and the owner's answer clears it; an empty answer at revision 0 drops no login; an owner's event carries the changed value whole, and a reader applies a put and a delete from the events alone and never asks the verb; a revision lower than the one held makes the reader ask its first view again; an existing waiter on `manifest-accepted` or on the assigner's `put` does not wake on the new families; for the indexer's `verdicts` (ADR 0622), called by the grants part while the node's tool runner is stopped, it answers, and from any other account, the indexer's own included, it is refused; a missed revision on `manifest-refused` makes the grants part ask `verdicts` once more; the grants part and the indexer started at the same moment, `verdicts` answers within the bound with its revision; with the indexer down past the bound the grants part raises the condition naming the indexer and `verdicts` and never reads the indexer's state, and the answer clears it; for the indexer's `graph` (ADR 0631), called by the assigner it answers with its revision, and from any other account, the indexer's own included, it is refused; a missed revision of the bucket `graph` on `indexer.views` makes the assigner ask `graph` once more; started with the indexer down, the assigner raises past the bound the first-view condition naming the indexer and `graph` and reads nothing of the indexer's state, and the answer clears it; the read tool shows the revision each reader holds, and the re-read tool asks once and records the hand act; in the assigner's whole-module test, the assigner starts before any new-form bus state exists, with no binding of the bus module, and the indexer, the grants part and the tool runner each get their first view of it. Live, after the delivery: the indexer's `indexer_old_sections`, which the milestone extends to count the `reads` entries on each owner's state, reads 0 declared `reads` entries on the indexer's state and 0 on the assigner's, and says the node-engine's reads granted in the grants part's code remain (§ 12.6); and each owner's read tool of § 12.2 answers what its bucket holds, each status view tool answers a session's call as before the move, and `mesh_search` finds each owner's read tool |
|
||||
| The owners' first views are their own tools, in a runtime of their own (ADR 0621) | cases of the indexer's and the assigner's whole-module integration tests: each first-view tool, called from a reader's account while the node's tool runner is stopped, answers (§1); each answers its granted readers and refuses every other account, the owner's own included (§2); the two runtimes started at the same moment, each asking the other's first view at start, both answer within the first-view bound with their revisions (§3); and the milestone's group passes its merge check with no change to the manifest validator's known fields for these verbs (§4) |
|
||||
| The grants part's first view of the bindings (design 25 bindings-first-view) | the cases of design 25 bindings-first-view §8: a case in the grants part's existing whole-module integration test, where a binding whose `created` was never said is written after a start; unit rows of the bus module's tools and the grants part, §9's rows among them (a binding composed from an answer without `binding`); live, the next start after the delivery, and §9's live case after the grants part's delivery alone |
|
||||
| A foundation part's first view comes within its bound | live, after the delivery: each reader's read tool (`grants_first_views`, the mesh MCP server's `mesh_first_views`, `indexer_first_views`, `assigner_first_views`) shows, for the indexer's and the assigner's verbs, the time from its ask to the owner's answer under the two-minute bound, and no first-view condition raised by the delivery |
|
||||
| The bus module's first view | cases in the nats module's one integration test: a replayed verdict older than the first view is not applied; an `indexer_bus_state` that does not answer turns the health unhealthy naming it and is asked again on the backoff; a set-aside manifest missing from the first view keeps its `asked` key, an empty `manifest-accepted` removes it; a deleted module's entry, after its empty `manifest-accepted`, is listed by `orphans` and raises `orphaned`, and a set-aside module's entry is neither; `nats_first_view_again` asks at once and records the hand act |
|
||||
| `manifest-accepted` is never lost silently | unit tests of the indexer: a failed publish is published again, and one still failing writes the finding; `indexer_say_accepted` says it again for one module from the verdict held, and refuses a module with no accepted verdict |
|
||||
| A person's request goes through `requests`, and the process verifies the warrant itself and acts on it once (ADR 0626 §4, ADR 0259 §6) | the process's unit tests: a `recreate` and a `drop` written to `requests` with no warrant, with a warrant for another act or entry, with a warrant the operator channel's record does not hold, and with a warrant whose ask id the process already acted on are each refused, delete nothing and say why; a `create` or the seat's `apply` for an entry no accepted manifest asks for is refused; the stamp is the caller the tool runner proved, whatever the request's body names |
|
||||
| The process's account, and the bus module's only bus declaration (ADR 0626 §1 to §3) | the manifest validator's unit test: the bus module's manifest with its process's `bus` and `keeps` is accepted; `keeps` on any other process or module, any other bus declaration of the bus module, and a subject of its process on which a tool is answered are refused; the composition test: the process is granted the create, update, delete and snapshot subjects under modules' names, nothing on which a tool or a verb is answered, and no subscription to a restricted stream's subjects beyond the snapshot request, its acknowledgements and its inbox; the snapshot account is granted the same subjects as before; no tool runner and no tool of the bus module holds the bus's create, update or delete subjects |
|
||||
| The process's fixes of review round 1 hold | the nats module's whole-module integration test and unit rows: an event or condition the bus did not take is kept, named by the health tool and said again at the next fill pass; a second accepted manifest asking for an entry raises no overdue condition; the warrant's wake on `decided` acts within seconds without a call to the pass by the test; a second request naming a spent ask is refused naming the act it spent; an ask past 12 hours is not acted on |
|
||||
| Drift and orphans, live | live, made only by a manifest change: a manifest that shrinks a setting raises `drifted`; the conditions clear as above. Nothing is filled on the live bus, and no orphan is made there |
|
||||
| Overdue and stopped are loud | live, carried by the change of the first module that moves in step 5, never by a merge made for the test alone: with the bus module's process stopped, an accepted manifest asking for an entry raises the overdue condition within five minutes, from the watchman, and a later `created` clears it; the bus module's health turns unhealthy when its pass is more than three minutes old |
|
||||
| A person's tools reach the process, and only by role | live: a role without the dangerous grant is refused `drop`'s tool; the tool runner's account holds no grant of the bus's create, update or delete subjects |
|
||||
| The author is the proven caller | the ledger's integration test: an `apply` naming another module's tool call is refused; the author stamped is the caller the tool runner proved, whatever the module passed |
|
||||
| Only the owner reaches its data | live: another module's account reading an entry in the new form, a ledger's history, snapshot or state among them, is refused; a raw publish on a ledger's history is refused |
|
||||
| The ledger keeps its promises | one integration test against the real bus (ADR 0597's How it is checked): two writers race and one is refused; two interleaved applies leave the state at the higher sequence; a write cut between history and state is repaired; a full state bucket answers taken with its sequence and catches up; an idle instance whose operations expired rebuilds from its snapshot; `rebuild`, `reindex` and `lag` repair by hand; a forged author is refused; every `apply` leaves the snapshot equal to the state; an index behind raises its condition and `reindex` clears it |
|
||||
| Only the owner applies | the ledger's integration test: another module's ledger `apply` on the knowledge base's ledger is refused |
|
||||
| A bare stream is complete | the manifest validator's unit test: a bare stream missing "when full", a cap or an age is refused, naming the field |
|
||||
| The calls rebuild only their users | a read of the delivery plan: only the importers of the calls package rebuild when it changes |
|
||||
| The move keeps data | live: a module moved from `state` keeps its bucket and data and raises no drift; a new entry in an old section is still created by the grants part; the count falls by one; at 0 a manifest using the section is refused and the foundation's are still accepted; a moved module is held off a machine whose tool runner does not read the typed binding |
|
||||
|
||||
**The test budget** (50 integration and 500 unit tests per repository): the design adds one integration test in the
|
||||
nats module's repository and one in mesh-tools (the ledger), and table cases, not new functions, to the manifest validator's unit
|
||||
tests in mesh-delivery and to mesh-sdk's decoding tests. Each repository's count against the budget is read in the
|
||||
prepare step of issue 1966, and a repository over it merges tests to stay within it.
|
||||
|
||||
**Acceptance.** The operator asks the bus module's `list` and sees every module's kept data with its template, its
|
||||
settings and its fill; a module that moved keeps its data; a drift or an orphan reaches the operator as a condition
|
||||
and is changed only on their word; and the knowledge base keeps its records as a ledger (design 55 § 10).
|
||||
|
||||
## Milestones
|
||||
|
||||
Each milestone not yet delivered has its part in [25-milestones/](25-milestones/), holding every line it builds
|
||||
(playbook 02, *The shape of a design*); § 1 to § 11 and the five steps of § 9 stay in this file.
|
||||
|
||||
- **bus-state** — § 12.2 to § 12.6, built under issue 1966: the bus module creates every module's bus state, the
|
||||
seat `mesh-broker` gains its verbs and events, the grants part writes the bindings, the ledger, the calls and the
|
||||
move. Its part: [25-milestones/bus-state.md](25-milestones/bus-state.md).
|
||||
- **bindings-first-view** — a recovery fix under issue 1966: the grants part takes a first view of every binding
|
||||
from the seat `mesh-broker`'s `list` at every start and when asked again by hand, so a binding whose `created` it
|
||||
never heard is written all the same. Its part:
|
||||
[25-milestones/bindings-first-view.md](25-milestones/bindings-first-view.md). Its acceptance: a module whose
|
||||
bus-state entry the bus holds, and whose `created` the grants part never heard, finds the entry's binding in its
|
||||
membership after the grants part's next start, with nobody's hand.
|
||||
|
||||
## Reviewed by architectural domain
|
||||
|
||||
The amendment of 2026-11-03 to ADR 0595, ADR 0596 and ADR 0597 (designs 25 § 12, 32 § 4 and 55 together) was
|
||||
reviewed by the four reviewers by architectural domain side by side (playbook 01), over three rounds, and the lead
|
||||
ruled on every finding. What was found, and what answers it:
|
||||
|
||||
| Domain | Found | Answered by |
|
||||
|---|---|---|
|
||||
| security | the author could be words the module chose; a person's dangerous tools had no role, mark or path to the process; the caller travelled as a plain field | design 25 § 12.3 (marks, the process's own tool subject, the identity seat's signed token), § 12.4 (the tool runner stamps the caller it proved; the move's authors marked imported) |
|
||||
| seams | the bus module asked for its own events in the new form, which it would have to create itself (ADR 0595 §10); a person's tool had no join to the process; a verdict was written twice with no order | design 25 § 12.3 (its events on its old `streams` section; the process's own tool subject); design 55 § 2.1 (the `review` operation first, `reviews` caught up from it) |
|
||||
| running qualities | overdue creation was undefined and raised by nobody; the database's rollback write lived in memory; a forward delivery after a rollback could miss acceptances and verdicts; no size estimate of the heads | design 25 § 12.2 (five minutes, the watchman, durable consumers, `check` at start); design 55 § 8.1 step 6 (`database.sequence`, the check at every start before the drop), § 2.1 (the heads' estimate) |
|
||||
| health | the bus module's health would have fired on a quiet mesh; conditions had no clearing; `database-behind` had no clearing | design 25 § 12.2 and § 12.3 (each condition with what clears it); design 55 § 8.1 step 6 |
|
||||
| events and logs | an event lost when the module stopped after `apply`; the bus module's events had no reader | design 55 § 1 (each event sent with its operation's sequence as id, the state `sent`); design 25 § 12.3 (`history` of one entry) |
|
||||
| testing | the bus module had no test that starts it whole; live tests would have filled the live bus; the budget was not counted | design 25 § 12.7 (the nats module's integration test, live drift only by a manifest change, the budget paragraph); design 55 § 10 |
|
||||
| upgrade | the build order lacked the grants part's binding and two undo steps | design 25 § 12.6 |
|
||||
| tools | the ledger's repairs had no tool for the next owner; the bus module's events had no read tool | design 25 § 12.5 (served for every ledger owner), § 12.3 |
|
||||
|
||||
**Answered since, 2026-11-04** (ADR 0626, and the registry milestone's ruling on where the seat's events go): the
|
||||
process's own tool subject of the security and seams rows is gone, since a process account answers no tool call; a
|
||||
person's request travels through the process's state `requests` and the process verifies the warrant itself
|
||||
(§ 12.3); the process holds a process account of its own beside the snapshot account (§ 12.2); and the seat's
|
||||
events are on the shared stream like every seat's, with the bus module's 30-day history its own stream (§ 12.3).
|
||||
|
||||
**Answered since**: the margin of `full` and the events of `drop` and `recreate` were the operator's answers of
|
||||
2026-11-03 and are now ADR 0595 §6's progressive insight, with the correction of fact for ADR 0595 §4 (the bus
|
||||
module deletes through `recreate` as well as `drop`); both wait for the operator's Approve and are written in § 12.2
|
||||
and § 12.3. The milestone's amendment (ADR 0613 to ADR 0615: the first view, the bus-state edge, the deletion paths,
|
||||
the caller until the identity seat, the reader count) awaits its own review by architectural domain, which rewrites
|
||||
this section.
|
||||
|
||||
@@ -15,8 +15,26 @@ code:
|
||||
- mesh-catalog modules/gitea/module.json
|
||||
- mesh-controller internal/inventory/migrations/0063-a-machines-names-are-one-seats.sql
|
||||
- mesh-controller internal/inventory/migrations/0064-the-resolver-file-is-the-uplinks.sql
|
||||
updated: 2026-10-10
|
||||
updated: 2026-11-09
|
||||
decisions:
|
||||
- "ADR 0641"
|
||||
- "ADR 0636"
|
||||
- "ADR 0634"
|
||||
- "ADR 0635"
|
||||
- "ADR 0596"
|
||||
- "ADR 0593"
|
||||
- "ADR 0625"
|
||||
- "ADR 0573"
|
||||
- "ADR 0606"
|
||||
- "ADR 0609"
|
||||
- "ADR 0595"
|
||||
- "ADR 0627"
|
||||
- "ADR 0608"
|
||||
- "ADR 0613"
|
||||
- "ADR 0620"
|
||||
- "ADR 0612"
|
||||
- "ADR 0614"
|
||||
- "ADR 0605"
|
||||
- "ADR 0287"
|
||||
- "ADR 0223"
|
||||
- "ADR 0220"
|
||||
@@ -98,7 +116,7 @@ nobody argued for is an entry nobody can explain.
|
||||
|
||||
**What deserves one** (ADR 0161). A provision the
|
||||
mesh's own code dereferences by name is delivered by a mesh seat its provider claims — the store, the
|
||||
bus, the vault, the artifact store, the catalogue. A provision a module merely offers may have several
|
||||
bus, the vault, the artifact store, the catalog. A provision a module merely offers may have several
|
||||
providers, and a consumer with several is a person's choice. A fact of the shape *exactly one machine
|
||||
is X* — the hub — is not a seat, because a seat is held by a module assignment and points at it; it is
|
||||
a placement with a capacity of one, kept by the store, refused by name when a second is placed, and
|
||||
@@ -114,7 +132,7 @@ capability is.
|
||||
ADR 0110): a module
|
||||
declares its own seats with their protocols, so the seats a mesh has are the mesh's own **plus
|
||||
every registered module's**. The set is still closed — a seat named nowhere is refused — but it is
|
||||
computed from the catalogue rather than maintained by hand, which is the property 0110 actually
|
||||
computed from the catalog rather than maintained by hand, which is the property 0110 actually
|
||||
needed and the table could not keep.
|
||||
|
||||
**And the mesh's own half is data, named for its scope.** Revision, 2026-09-27, reconciling two
|
||||
@@ -125,7 +143,7 @@ the set out of compiled code into a table the controller owns, so a rename is on
|
||||
rebuild of everything that names one.
|
||||
|
||||
So the set has two halves and neither is written out here: the mesh's own, which the controller holds
|
||||
as rows, and every registered module's, which is computed from the catalogue. What this document keeps
|
||||
as rows, and every registered module's, which is computed from the catalog. What this document keeps
|
||||
is what a seat *is* — the rest would be a third copy, stale the first time somebody renamed one, which
|
||||
is the fault ADR 0122 exists about.
|
||||
|
||||
@@ -139,32 +157,391 @@ convention, which later seats departed from.
|
||||
|---|---|---|---|
|
||||
| `mesh-controller` | — | mesh | — | the controller |
|
||||
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
|
||||
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
||||
| `mesh-broker` | — | mesh | `mesh-bus`, and `bus-state` (ADR 0595 §1, ADR 0614 §5) | the broker carrying the mesh's own bus; its holder, the bus module, creates the bus state modules ask for (design 25 § 12) |
|
||||
| `mesh-vault` | — | mesh | `secret` | the vault (in the seed since ADR 0161; the earlier *reserved* named an effect no rule produced) |
|
||||
| `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, ADR 0156) | mesh | `artifact-store` | the artifact registry |
|
||||
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
||||
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||
| `mesh-git` | `git` | mesh | `git` | the forge |
|
||||
| `container-image-registry` | `mesh-artifact-store` for images (ADR 0605; the old name readable until every node-engine reads the new one) | mesh | `artifact-store` | the artifact registry (`distribution`) |
|
||||
| `oci-artifact-registry` | `mesh-artifact-store` for bundles and archives (ADR 0605) | mesh | `artifact-store` | the artifact registry (`distribution`) |
|
||||
| `mesh-catalog` | `the-catalogue` | mesh | — | the indexer |
|
||||
| `npm-package-registry` | — (its rename to `mesh-*` stays deferred, ADR 0156 §8) | mesh | `npm-package-registry` | the forge |
|
||||
| `go-package-registry` | — (ADR 0605) | mesh | `go-package-registry` | the forge |
|
||||
| `git` | — | mesh | `git` | the forge |
|
||||
| `mesh-build-machine` | `the-build-machine` | node | — | a builder |
|
||||
| `mesh-resolver` | — | mesh | — | the mesh's resolvers, each holding every node's internal domain (ADR 0194); replicated, held on the anchor and the home server (ADR 0223) |
|
||||
| `mesh-dns-resolver` | — | mesh | — | the mesh's resolvers, each holding every node's internal domain (ADR 0194); replicated, held on the anchor and the home server (ADR 0223) |
|
||||
| ~~`mesh-dns-port`~~ | `the-dns-port` | node | — | retired by ADR 0194: the local resolver became the mesh's one; deleted from the set, and from the store's table, once nothing claimed it (ADR 0220) |
|
||||
| `node-hostname` | `node-hosts-file` (renamed by ADR 0223; the former name an alias) | node | — | owns a machine's names: `/etc/hostname`, written from its holder's `hostname` setting with no default and taking effect at the next boot, and in `/etc/hosts` the machine's own lines and the operator's kept region, changed through its verbs `entries`, `add`, `remove` (ADR 0199). Held by the module `hostname`, formerly `hosts` |
|
||||
| `node-resolver` | — | node | — | a machine's own resolver, held only where something requires `split-dns`: it writes `/etc/resolv.conf` there (the uplink's holder steps back from it) and routes a VPN's domains to the VPN's servers over its link through its verbs `routes`, `route`, `unroute` (ADR 0247). Held by the module `systemd-resolved` |
|
||||
| `mesh-intrusion-prevention` | `the-intrusion-prevention` | node | — | an intrusion-prevention service |
|
||||
| `mesh-packet-filter` | `the-packet-filter` | node | — | the packet filter |
|
||||
| `mesh-private-network` | `the-private-network` | node | — | the private network the mesh runs over |
|
||||
| `the-private-network` | — (it keeps its name, ADR 0636 §6) | node | — | `mesh-wireguard`, the private network the mesh runs over |
|
||||
| ~~`mesh-resolver-configuration`~~ | `the-resolver-configuration` | node | — | retired by ADR 0223: `/etc/resolv.conf` is written by the holder of `node-uplink`, the program that would otherwise rewrite it, and the need of the uplink beside it (ADR 0220) goes with it; deleted from the set, and from the store's table, once nothing claims it |
|
||||
| `mesh-showcase` | `the-showcase` | node | — | the showcase module |
|
||||
|
||||
The controller holds **the mesh's own** entries in code, and a test asserts their size and that
|
||||
every one names the record that made it a seat. A module's seats are not here and never will be —
|
||||
they are read from the catalogue. **This table and
|
||||
they are read from the catalog. **This table and
|
||||
ADR 0126 govern, and code that
|
||||
disagrees is what is wrong.** The implementation in progress predates several
|
||||
things here: seats held by assignments rather than claimed by definitions, the `mesh-vault` seat and
|
||||
its reservation, and the foundation's seats delivering nothing. It is brought to this table before it
|
||||
merges.
|
||||
|
||||
## A seat's definition (from 2026-11-03)
|
||||
|
||||
**A seat is defined once, in one language-neutral file, and every part learns it from the bus** (ADR 0608, from
|
||||
research 081). Every seat has its definition file in its domain's contracts module, written by hand, whether it is
|
||||
defined whole or at the seat list's grain (ADR 0635 §2). In the milestone of ADR 0612, 25 seats are defined whole
|
||||
(ADR 0613 §1, ADR 0620, ADR 0627 §1, ADR 0634 §6, ADR 0635 §5); every other seat is defined at the seat list's grain.
|
||||
|
||||
- **The file** is a schema with conformance fixtures: the seat's verbs and their messages, its events and their
|
||||
bodies, its states and the forms of its provisions, and names its glossary domain (ADR 0636 §5). It lives in the
|
||||
contracts module of its domain (*Where a seat's
|
||||
code goes*, below). Its Go and
|
||||
TypeScript types are generated from it and committed beside it; nobody writes them by hand. The format is JSON, its
|
||||
messages and bodies JSON Schema, as the seat list's verb inputs already are, with the format's own schema checked in;
|
||||
the fixtures are data, never functions.
|
||||
- **A definition names its states** (ADR 0608 §1; ADR 0634 §6 leaves the file's format to this design). A `states`
|
||||
entry is what any holder of the seat keeps and what the seat's verbs answer from: its local name, its template of
|
||||
ADR 0595 (state, events, work queue, log, objects or ledger), who writes it, each form of its keys with the schema
|
||||
of the value under it, how long it keeps its history, and its size cap. The holder writes every state; only a state
|
||||
of the events or log template may name each caller as the writer of its own keys, granted on keys under the
|
||||
caller's own name and no other. No entry names a holder module, so another module can hold the seat by importing
|
||||
the contract alone (ADR 0625 §7). The generated package carries, per state, its local name, a builder and a parser
|
||||
per key form, and the value types, decoded strictly; the fixtures hold one value per key form.
|
||||
`operator-channel`'s `records` becomes one `states` entry in the same change, so the format has one form for a
|
||||
state. The format's own schema refuses an entry with an unknown template or a key form without a value, naming it.
|
||||
**A state is the holder's, asked through the seat's verbs** (ADR 0596 §2): the module that reads another module's
|
||||
bucket directly today keeps doing so only as a reader in transition, through its own manifest's `reads`, until its
|
||||
module moves to the owner's verbs or events (ADR 0596 §3, issue 1966). The bucket's name stays
|
||||
`<holder>_<state>`, the holder's own, so nothing moves under a seat's rename. **How it is checked**: the manifest
|
||||
validator refuses a holder whose `requires` does not declare each of its seat's states with the same local name and
|
||||
template, naming the state; the format's own schema refuses a caller-written state of another template and a state
|
||||
with no size cap.
|
||||
- **What is defined whole.** Ten seats came first (ADR 0613; ADR 0620, on the operator's answer of 2026-11-04 to move
|
||||
them all; ADR 0627 §1): `npm-package-registry`, `go-package-registry`, `container-image-registry` and
|
||||
`oci-artifact-registry` (one common part of their shared verbs, written once, so the four cannot drift),
|
||||
`mesh-broker`, the four seats the milestone adds a verb or an event to, each with every verb its holder's manifest
|
||||
on the trunk declares today, plus the new ones, and `node-engine`. Fifteen more are defined whole in the same
|
||||
milestone because a name leaving mesh-sdk (*Where a seat's code goes*) is their type, state layout or message
|
||||
(ADR 0634 §6, ADR 0635 §5), each with its verbs, events, messages and the layouts of its states:
|
||||
`mesh-module-assigner`, `mesh-wants`, `mesh-node-enroller`, `mesh-catalog`, `node-build-go`, `node-build-image`,
|
||||
`node-build-node`, `mesh-hand-acts`, `mesh-provisioner`, `mesh-conditions`, `mesh-self-check`, `mesh-healers`,
|
||||
`mesh-watchdogs`, `the-private-network` and `mesh-delivery`. Five seats already whole gain the state layouts
|
||||
mesh-sdk held for them: `node-engine`, `mesh-vault`, `mesh-delivery-checker`, `container-image-registry` and
|
||||
`oci-artifact-registry` (ADR 0634 §6). **No event moves subject in this milestone** (ADR 0620), but those of the
|
||||
two renamed seats (*Two seats renamed*, below): a seat's own events are only those published on its seat subject
|
||||
today, and a holder's own events stay under the holder's own subjects, listed beside the seat they belong with,
|
||||
never moved onto it. The four seats the milestone adds to:
|
||||
- `mesh-delivery-checker` (held by the checker): the verbs `try`, `try-read` and `try-end`; no seat event. The
|
||||
holder's own, beside it: its family `checker.asks`, new, with `tag-wanted` (design 51 *Tagged versions*) and
|
||||
`verdict` (*Out of step is learned from events*, below); and its stream `checks`, which already declares
|
||||
`preview.**` and `owed.**`, only its `emits` lacking `preview.written`, which the same change adds.
|
||||
- `mesh-config` (held by mesh-config): the verbs `config` and, new, `renames`; no seat event. The holder's own,
|
||||
beside it: `config-changed`, and its family `config.events`, new, with `renamed`, `retiring` and `retired`
|
||||
(design 32).
|
||||
- `mesh-vault` (held by the vault): the verbs `get`, `secret-ask`, `issued`, `rotate`, `move-in`, `node-key`,
|
||||
`own`, `issue-account`, `sign`, `enrolment-token`, `node-account`, `rotate-node`, `node-account-confirm`,
|
||||
`abandon` and, new, `retire`; no seat event. The holder's own, beside it: `provisioned`, `rotated`,
|
||||
`secret-changed`, `account-hashed`, `own-replaced`, `account-issued`, `signed`, `rotation-begun`,
|
||||
`node-account-ready`, `rotation-done` and, new, `own-retired` (design 32). The seat list mesh-sdk compiled
|
||||
also names `accept` and `move-asks`, which no holder serves and nothing calls: the definition drops them.
|
||||
- `operator-channel` (held by the router, the messenger module): the verbs `open`, `history`, `notify`, `tell`
|
||||
and `asks`; the asks it accepts, `ask`, `cancel` and `condition`; its seat event `decided`; `by-caller` for
|
||||
`ask`, `cancel`, `condition` and `decided`; and its state `asks`, as the holder's seat entry has them. The
|
||||
holder's own, beside it: `refused`, and `offered` and `closed`, new, on its restricted stream (design 55 § 11).
|
||||
Its definition is written from the holder's seat entry.
|
||||
|
||||
And `node-engine` (held by the node-engine on every node) has the verb `manifest-reads` and its seat event
|
||||
`manifest-reads-changed`, which the milestone gives it (ADR 0618 §3, §4; design 51). The 25, by the contracts
|
||||
module that holds each (ADR 0634 §7, ADR 0635 §5):
|
||||
|
||||
| contracts module | domain | seats | held by |
|
||||
|---|---|---|---|
|
||||
| `mesh-delivery-contracts` | Delivery | `mesh-catalog`, `mesh-delivery-checker`, `mesh-delivery`, `mesh-wants`, `git`, `node-build-go`, `node-build-image`, `node-build-node`, `npm-package-registry`, `go-package-registry`, `container-image-registry`, `oci-artifact-registry` | the indexer, the checker, the gate part, the assigner, the three build modules, the forge (three), `distribution` (two) |
|
||||
| `mesh-health-and-repair-contracts` | Health and repair | `mesh-conditions`, `mesh-self-check`, `mesh-healers`, `mesh-watchdogs`, `mesh-hand-acts` | the condition keeper, mesh-self-check, mesh-healers, the watchman, mesh-hand-acts |
|
||||
| `mesh-provisioning-contracts` | Provisioning | `mesh-provisioner` | mesh-provisioner |
|
||||
| `mesh-identity-and-access-contracts` | Identity and access | `mesh-vault`, `mesh-node-enroller` | the vault, the module `mesh-enrolment` |
|
||||
| `mesh-connectivity-contracts` (mesh-networking repository, ADR 0636 §6) | Connectivity | `the-private-network` | `mesh-wireguard` |
|
||||
| `mesh-operator-and-conversation-contracts` | Operator and conversation | `operator-channel` | the router |
|
||||
| `mesh-contracts` | Core, and Config | `node-engine`, `mesh-broker`, `mesh-config`, `mesh-module-assigner` | the node-engine, the bus module, mesh-config, the assigner |
|
||||
|
||||
ADR 0641 retired the Placement domain: module assignment is Core's, so `mesh-module-assigner` is defined in
|
||||
`mesh-contracts`, and the wants are Delivery's, so `mesh-wants` is defined in `mesh-delivery-contracts`; the module
|
||||
`mesh-placement-contracts` is not created (ADR 0641 §1, §2). From research 085's milestone (design 47) the seat
|
||||
`mesh-grants` is defined whole in `mesh-identity-and-access-contracts` (ADR 0636 §5), and the seat `git` gains in
|
||||
`mesh-delivery-contracts` the verbs by which every delivery part reaches the forge (*The git seat*, below). How it is
|
||||
checked: the indexer refuses a definition of these seats from any other contracts module, naming both (ADR 0636 §5),
|
||||
and its domain check refuses a definition naming the domain Placement (ADR 0641 §5).
|
||||
|
||||
**How it is checked**: each of these definitions' conformance fixtures names every event's subject, the seat's and
|
||||
the holder's own, and a case of the holder's whole-module test takes a fixture from the running holder (an event
|
||||
it publishes) and matches it, subject and body, against the definition; the indexer's generator tests cover each
|
||||
of the 25 (ADR 0634 §10, ADR 0635 §6). The builder and the indexer's own events hold no seat:
|
||||
the builder's `numbered` and `old-form-closed`, the indexer's `manifest-accepted` and its family `indexer.views`
|
||||
are those modules' own, declared in their own manifests, which
|
||||
ADR 0613 does not govern, and no seat definition carries them. Every other seat, the foundation's included, has its definition on the bus in the same milestone, all
|
||||
at once (ADR 0608 §2), holding the seat list's grain: its name, whether it is held once or on every node, its verbs
|
||||
with their inputs and its provision. Its file is written by hand in its domain's contracts module, or in
|
||||
`mesh-contracts` where its domain has none; the files the registry branch generated once from mesh-sdk's seat list
|
||||
are their starting text, and nothing generates them again (ADR 0635 §2). Its messages, events and state layouts are
|
||||
added to a seat's definition when its domain's contract next changes, which moves that domain's seats whole into its
|
||||
contracts module in that change (ADR 0613 §3); which domain moves when is tracked by issue 167 (ADR 0613 §4).
|
||||
- **Two seats renamed** (ADR 0634 §9): `mesh-placement` becomes `mesh-module-assigner`, and `mesh-enrolment`
|
||||
becomes `mesh-node-enroller`; the module `mesh-enrolment` keeps its name. Their definitions, the subjects of their
|
||||
verbs and events, every holder's claim, every caller's `invokes` and grant, the claude-code instructions' text, the
|
||||
mesh MCP server's description and the glossary take the new names in the same delivery group, and nothing answers
|
||||
to the old ones: a call on an old subject fails loudly, naming the seat. **No state moves**: a state's bucket is
|
||||
the holder's (`<holder>_<state>`), so placement keeps every revision. The old subjects' events keep only their
|
||||
stream's own retention. **The order is the module graph's**: the grants of the new subjects are an ordering edge
|
||||
that comes before the holders move, and the callers come after the holder that serves the renamed verbs; an older
|
||||
node-engine takes its new build through `mesh-wants`, which is not renamed. **A machine enrolling with an installer
|
||||
built before the rename** asks the old enrolment seat's subject, which no holder answers: its enrolment fails
|
||||
loudly, naming the seat, and nothing joins; it enrols with an installer of the new build. **How it is checked**: the
|
||||
manifest validator refuses a claim, an `invokes`, a `consumes` or a grant naming a seat no definition names, so an
|
||||
old name fails
|
||||
(ADR 0634 §10); the acceptance after the delivery is that `mesh-module-assigner.placement` answers and
|
||||
`mesh-placement.placement` is not found, placement's revisions are those it had before, and no `seat-holders` key,
|
||||
finding or condition stays under an old name.
|
||||
- **Every part learns the seat list from the bus, through the seat `mesh-catalog`** (ADR 0635 §1, as ADR 0636 §4
|
||||
changes it). The node-engine, the tool runner, claude-code, the manifest validator and every other reader of the
|
||||
list of seats call, through mesh-sdk's runtime, the verb `seat-definitions` of the seat `mesh-catalog`: without a
|
||||
seat it answers every trunk's seat with its digest, with a seat that seat's definition. They hear the seat's events
|
||||
`seat-definition-changed` and `seat-definition-removed`, said for the trunks' definitions. A reader names no
|
||||
module: it calls the seat, never its holder, the indexer, by name. A running part acts on what is merged, so a
|
||||
reader takes no definition from a pull request's head; the manifest validator inside a delivery group's check is
|
||||
the one reader that reads the members' heads. No package carries the seat list, and mesh-sdk imports no contracts
|
||||
module.
|
||||
**A reader without the list acts on nothing new, and keeps running what it runs** (ADR 0636 §1, changing ADR 0635
|
||||
§3). A reader that starts without the list applies, composes, grants and judges nothing new, except for the
|
||||
bootstrapped modules below; it keeps running what it already runs, says in its health what it waits for, and a
|
||||
node-engine after a reboot starts again the compositions it last applied. Each node writes one finding when its
|
||||
readers have waited five minutes (ADR 0564 §2); the condition keeper raises it as a condition, which reaches the
|
||||
operator as every condition does, and clears it at the first full read. A reader away longer than the events'
|
||||
retention reads the whole list again. A check that cannot read the definitions it needs fails, naming them, rather
|
||||
than passing without them.
|
||||
**The bootstrapped definitions** (ADR 0636 §2, changing ADR 0608 §2) are the foundation's seats (research 075) and
|
||||
`mesh-catalog`, `mesh-conditions` and `operator-channel`: every reader carries them from its start and acts on their
|
||||
holders without the list, so after a full restart the indexer comes back and serves the list, and the condition
|
||||
keeper and the router, with the channels it sends through (ADR 0259), carry a wait's condition to the operator.
|
||||
**A missing or refused definition blocks new claims only** (ADR 0636 §3): when a seat's definition is missing,
|
||||
removed or refused, readers refuse a new claim, `invokes` or grant of that seat, and never stop or remove a module
|
||||
that already runs holding or calling it.
|
||||
**Each reader's list has two tools** in mesh-sdk's runtime bundle (ADR 0636 §7): one reads which definitions a
|
||||
reader holds, their source and digests and what it waits for; the other, granted to the operator and the healers,
|
||||
makes it read the list again now.
|
||||
**How it is checked** (ADR 0635 §6, ADR 0636 §8; *Acceptance tests*, below): the node-engine's whole-module test,
|
||||
against a stand-in of the seat, acts on nothing but the bootstrapped modules without the list and on all once it
|
||||
answers, and starts its last compositions after a restart; the indexer's whole-module test starts with only the
|
||||
bootstrapped seats and answers `seat-definitions`; with the indexer kept down, the node's finding is written after
|
||||
five minutes and the condition keeper raises it to the operator; a refused definition stops no running module; a
|
||||
reader refuses a pull request head's definition; mesh-sdk's unit test refuses a module's name in the runtime; both
|
||||
tools answer.
|
||||
- **The generator** is each language's build module's: the Go build module's package for Go, the Node and TypeScript
|
||||
build module's for TypeScript (the logic is the module's whose work it is). The generator's version is a build input
|
||||
of every contracts module, so a changed generator rebuilds each contracts module once (ADR 0603 §3). Each build
|
||||
module's test step regenerates and compares, for every module carrying the contracts marker (below): a committed
|
||||
file that differs, a file missing, or a file in the package tree the generator did not write fails the step, naming
|
||||
each path.
|
||||
- **Out of step is learned from events, and the indexer runs no generator.** For a pull request, the check's step
|
||||
fails with the out-of-step paths; the checker puts them in its verdict and emits its declared event `verdict`
|
||||
carrying them (its `preview.written` is declared in the same change). For a trunk, the builder's build-end event
|
||||
carries a trunk build's out-of-step paths, which is how a generator's change is seen, since no check of the
|
||||
contracts module runs then. The indexer consumes both and writes the trunk's finding.
|
||||
- **On the bus**: the indexer reads every seat definition from every trunk and open pull request, from the forge
|
||||
bridge's messages, and keeps them in its own state `seat-definitions`, keyed by seat and source (the trunk, or a
|
||||
pull request's head), as their only writer; a definition is at most 256 KiB. As the holder of `mesh-catalog` it says
|
||||
`seat-definition-changed` (seat, source, digest), kept seven days, only when a digest changed, and
|
||||
`seat-definition-removed` when a trunk's definition goes, both as events of the seat (ADR 0636 §4, changing ADR 0608
|
||||
§3). **Only the trunk's entry is the seat's contract**: the readers of the seat list read the trunks' through
|
||||
`seat-definitions`; the contracts modules' builds and the review ask `indexer_seat_definition`, which answers the
|
||||
trunk's unless a pull request's head is named, and says which it answered. **The manifest validator inside the
|
||||
delivery group's one check reads the seat definitions at every member's head** (ADR 0636 §4), so a group that
|
||||
changes a definition is judged by what it merges. A
|
||||
pull request's entries are removed on the forge bridge's event that it closed or merged. **A seat of the mesh's own
|
||||
contracts is defined only in its domain's contracts module, and a move there is a handover** (ADR 0636 §5, changing
|
||||
ADR 0608 §2): a definition names its glossary domain; the indexer takes a seat of the mesh's
|
||||
own contracts only from that domain's contracts module, `mesh-contracts` for a domain with none, and refuses a
|
||||
definition of it from any other module, with a finding naming both. A definition moving from `mesh-contracts` to its
|
||||
domain's contracts module is a handover: the indexer keeps the last trunk definition until the new one is merged,
|
||||
in either order of the two trunks. A seat defined outside the mesh's own contracts is taken from its own contracts
|
||||
module (ADR 0625 §7). A definition
|
||||
that does not parse fails its pull request's check; the trunk's stays. The merge is the only approval. The
|
||||
foundation's own definitions are bootstrapped before the bus exists, as the bootstrap of a fresh mesh has it
|
||||
(research 075).
|
||||
- **Findings**, written by the indexer, from which the condition keeper raises and clears the conditions (ADR 0564): a
|
||||
trunk's seat definition that does not parse, `seat-definition-unparsed` (a pull request's is its check's verdict, not
|
||||
a condition), cleared by a definition that parses; generated types on a trunk out of step with their definition,
|
||||
`generated-types-out-of-step`, cleared only by a clean build end of the same contracts module, per module and not per repository, since several
|
||||
contracts modules share one; a seat defined outside its domain's contracts module, naming both modules (ADR 0636 §5). **Tools** of the indexer: `indexer_seat_definition`
|
||||
(read one), `indexer_seat_definitions_again` (read a trunk's or a pull request's again now) and `indexer_out_of_step`
|
||||
(the generated types out of step).
|
||||
- **Contracts modules.** A seat's contract is published by a contracts module, which holds the seats' definitions,
|
||||
their contracts and their client packages and no seat (ADR 0625 §4, §5); the seat's holder implements the contract.
|
||||
**The contracts marker**: a contracts
|
||||
module's package artifact names the directory of its definitions in the field `generated-from`, and the manifest validator
|
||||
refuses a module carrying it that claims a seat, naming the seat. **Each domain whose seats the milestone defines
|
||||
whole has a contracts module of its own, created in the milestone** (ADR 0634 §7, ADR 0635 §5; the table above);
|
||||
`mesh-contracts` keeps the Core domain's seats, `mesh-config`, and the definitions of every seat whose domain has
|
||||
none yet. Each declares its packages in Go and TypeScript, cut per seat as *Where a seat's code goes* says, in
|
||||
package series `0.1`, published through the two package seats (design 51) and taken by its users by version, or
|
||||
inside one delivery group by the group's head.
|
||||
- **A contracts module and a domain SDK are modules like any other, found by their manifests** (ADR 0634 §8).
|
||||
`mesh-delivery-contracts` and the Delivery domain's SDK are folders of the mesh-delivery repository, and the other
|
||||
domains' contracts modules folders of the mesh-contracts repository, but the Connectivity domain's:
|
||||
`mesh-connectivity-contracts` is a folder of the mesh-networking repository, beside `mesh-wireguard`, which holds
|
||||
`the-private-network` with its manifest and peer generator its own (ADR 0636 §6, changing ADR 0634 §8). Each is a package-only module, as mesh-sdk long
|
||||
is: published from its trunk, rebuilding the modules that use it like any package (ADR 0573 §2, §3), and delivered
|
||||
to no node. A step of the build or the delivery that treats a module in a folder unlike one at a repository's root
|
||||
is a gap of the build and delivery system, fixed in the milestone, never worked around. **How it is checked**: the
|
||||
delivery group's one check builds and publishes every new module from its folder, and the build modules'
|
||||
regenerate-and-compare step runs on every contracts module the milestone creates (ADR 0634 §10).
|
||||
- **mesh-sdk takes no seat-specific type** (ADR 0613 §5): a unit test of mesh-sdk refuses an exported identifier or
|
||||
string constant naming a seat the definitions define. The two checks every change already runs, the
|
||||
build modules' compare step and that test, are where a stray file and a stray type are refused; mesh-catalog's own
|
||||
checks cannot see another repository.
|
||||
- **A contracts cycle holds nothing.** `mesh-delivery-contracts` is published through the forge module's package seats
|
||||
and built by the Go and Node build modules, and the forge module, `distribution` and the Go build module import it. A
|
||||
package edge and a build module's edge are triggers, never waits, so that cycle starts and is not refused (ADR 0614
|
||||
§6), and a package is numbered per source revision, so the triggers end (design 51 *The versions*).
|
||||
- **Older readers keep working**: a moved contract keeps its subjects, keys and message shapes, and a reader ignores
|
||||
a field it does not know. The two renamed seats are the exception, and every one of their users moves in the same
|
||||
delivery group (ADR 0634 §9, as it changes ADR 0608 §6).
|
||||
- **A vendor-specific** provision or contribution lives in its module's own extension package, published by version
|
||||
like any package; a later holder of the seat may publish its own the same way. A seat's definition stays
|
||||
vendor-neutral, which the seams review checks.
|
||||
|
||||
### Where a seat's code goes
|
||||
|
||||
*Added 2026-11-04, widened 2026-11-05* (ADR 0625, ADR 0627, ADR 0634, ADR 0635; the operator, 2026-11-04: "many small
|
||||
sdk's instead of 1 large sdk", and shared logic lives with its owner). A module is rebuilt only for what it uses, so
|
||||
shared code lives with its owner: a seat's code is cut per seat, in the contracts module of its domain; what one
|
||||
module owns is that module's own package; what one domain's modules and seats share is that domain's SDK; mesh-sdk
|
||||
keeps only what modules or seats of more than one domain use.
|
||||
|
||||
```
|
||||
contracts module of a domain (mesh-<domain>-contracts; mesh-contracts for Core, mesh-config, and a domain with none)
|
||||
├── definitions and fixtures written by hand, every seat's at its grain, the generation's source
|
||||
└── one package per seat defined whole, per language generated: types, subjects, state layouts, a typed call per verb
|
||||
└── its client package (where callers or holders share logic over the seat) written by hand
|
||||
|
||||
domain SDK (mesh-<domain>-sdk) written by hand: logic and forms one domain's modules and seats share, owned by no module
|
||||
a module's own package written by hand: logic and state layouts one module owns, where it holds no seat for them
|
||||
mesh-sdk the runtime every module uses, and what modules or seats of more than one domain share;
|
||||
it imports no contracts module and reads the seat list from the bus
|
||||
```
|
||||
|
||||
- **Shared code lives with its owner** (ADR 0634 §1), and the first home that fits is the one: a seat's types, the
|
||||
layouts of its states and the messages of its verbs and events are its contract's, and the logic its callers or
|
||||
holders share is its client's; logic and state layouts one module owns, where that module holds no seat for them,
|
||||
are that module's own package, which other modules import from it; logic and forms no seat carries and no one
|
||||
module owns, the seat-neutral parts seats' clients or holders share included, are their domain SDK's when only one
|
||||
domain's modules or seats use them, and mesh-sdk's when more than one domain's do or every module does.
|
||||
- **Each seat defined whole has a package of its own, per language** (ADR 0625 §1, ADR 0627 §1): its generated
|
||||
contract, and a module imports only the packages of the seats it uses, so a change of that seat rebuilds only its
|
||||
users. Where the fingerprint counts a language's module rather than its package, each such package is a module of
|
||||
its own in the same folder. In the milestone of ADR 0612 these are the 25 seats above.
|
||||
- **A seat at the seat list's grain has its definition and no package** (ADR 0635 §1, §2): its readers learn it
|
||||
from the bus, and a change of it rebuilds nothing.
|
||||
- **The imports run one way per package** (ADR 0625 §3, as ADR 0634 §2 and ADR 0635 §1 change it): a generated
|
||||
package imports nothing of mesh-sdk; mesh-sdk imports no contracts module; a domain SDK imports only mesh-sdk and
|
||||
contract packages; a client imports its own seat's contract, mesh-sdk's runtime and its own domain's SDK. Where a
|
||||
language's module is the unit of import, the clients are a module of their own in the folder, so the two never
|
||||
form a cycle.
|
||||
- **A client package** (ADR 0625 §4, as ADR 0634 §2 changes it) is the one hand-written package a seat may carry
|
||||
beside its contract, owned by the seat's contracts module, never by a holder. It holds the logic two or more
|
||||
callers or holders would otherwise each write over that seat, and only where such logic exists; it uses only its
|
||||
own seat's contract, mesh-sdk's runtime and its own domain's SDK, never another seat's client, and never names a
|
||||
holder, a module's tool or a module's subject. A seat's types and typed calls are its contract's, never its client's.
|
||||
- **A domain SDK** (ADR 0634 §2) is a package-only module, `mesh-<domain>-sdk`, named after the domain's heading in
|
||||
the glossary, which may sit in a repository with that domain's modules. Its importers are its domain's modules and
|
||||
the holders and callers of its domain's seats; it imports only mesh-sdk and contract packages, names no module's
|
||||
tool or subject, and holds no seat's type or state layout and no state layout one module owns. The first is
|
||||
`mesh-delivery-sdk`, in the milestone of ADR 0612: the moving of a file in parts (below), the build steps' and the
|
||||
composer's shared logic, each made seat-neutral over its callers' types (ADR 0635 §4), and the rules the Delivery
|
||||
domain's modules share.
|
||||
- **A module's own package** (ADR 0634 §1) holds what that module owns and no seat carries: its state layouts that
|
||||
other modules read and the logic over them. Its fingerprint counts only that package's files (ADR 0593 §2), so a
|
||||
change elsewhere in the owner module rebuilds none of the package's importers. Another module of the same repository uses it by path, from the same
|
||||
commit (ADR 0609 §2); a module of another repository uses it only by its name and a published version (ADR 0573
|
||||
§1), so the owner's manifest declares it as a package artifact (ADR 0606 §4).
|
||||
- **The clients of the milestone of ADR 0612** (ADR 0627 §1, ADR 0634 §4, ADR 0635 §4), each moved out of mesh-sdk
|
||||
with every importer in the same delivery group, mesh-sdk keeping no copy and no alias: `operator-channel` (an ask
|
||||
and its awaited answer, mesh-sdk's `asks` and `asker`); `mesh-vault` (the calls its callers share; mesh-sdk's
|
||||
hand-written vault types are replaced by the vault contract's, not moved into the client); `node-engine` (the
|
||||
checks of its compose-candidate ask and answer); `npm-package-registry`, `go-package-registry`,
|
||||
`container-image-registry` and `oci-artifact-registry` (publishing a file in parts, one client per seat, each
|
||||
binding the Delivery domain's SDK's transfer to its own seat's verbs and types); `mesh-hand-acts` (recording a
|
||||
hand act); and `mesh-provisioner` (mesh-sdk's provider runtime and its harness, which only providers use).
|
||||
`the-private-network` has none: the layout of the value it provides, which holds no key material, is the seat's
|
||||
contract in `mesh-connectivity-contracts` (ADR 0634 §1), the private network's manifest and peer generator are
|
||||
`mesh-wireguard`'s own, and the seat has a client only where its callers share logic (ADR 0636 §6, changing ADR 0635
|
||||
§4).
|
||||
- **Moving a file in parts is seat-neutral, in the Delivery domain's SDK, on both sides** (ADR 0627 §2, as ADR 0634
|
||||
§3 changes it): it serves only the four registry seats, all of the Delivery domain. The sending side and the going
|
||||
on from what a holder already holds, and the holder's side (taking the parts into the holder's own objects,
|
||||
checking their hashes, dropping an upload left idle, saying when its objects fill), name no seat, verb or seat's
|
||||
type and take subjects, shapes and checks from their caller. Each registry seat's client binds the sending side,
|
||||
and each holder (the distribution module, the forge's two package seats) binds the receiving side to the seat it
|
||||
holds, so no holder copies another's transfer.
|
||||
- **In a contracts module's published packages every file outside a client is generated** (ADR 0625 §5): the build
|
||||
modules' regenerate-and-compare step fails any other hand-written file there, naming it. The definitions and their
|
||||
fixtures sit outside the published packages and are not judged by that step.
|
||||
- **The contracts modules follow the glossary's domains** (ADR 0625 §6, ADR 0634 §7): a domain's seats move into
|
||||
`mesh-<domain>-contracts`, named after the domain's heading in the glossary, when that domain's seats move; a seat
|
||||
of the Core domain, or of a domain with no contracts module yet, is in `mesh-contracts`. The seven the milestone of
|
||||
ADR 0612 creates are in *A seat's definition*, above; the order of the other domains stays issue 167's (ADR 0613 §4).
|
||||
- **A seat defined outside the mesh's own contracts has a contracts module of its own** (ADR 0625 §7), which may
|
||||
share a repository with its first holder; its definition, contract and client never live in the holder module, so
|
||||
another module can hold the seat by importing only the contract, and the holder can be retired while the contract
|
||||
stays.
|
||||
- **A role only one module could ever play is no seat** (ADR 0625 §8): it is that module's own tools and events,
|
||||
reached by the module's name (ADR 0596 §2). The test is whether another module could take the role over, not how
|
||||
many hold it today, so every seat in force stays a seat, `mesh-broker`, `node-engine` and `route-proxy` included.
|
||||
- **mesh-sdk takes only what modules of more than one domain use** (ADR 0625 §9, as ADR 0627 §2, ADR 0634 §1, §4, §5
|
||||
and ADR 0635 §4 change it): the bus connection, the tools bundle's runtime, state and its helpers, the answer of a
|
||||
module's health tool, the `Membership` type with the names its runtime reads to learn its own membership, the
|
||||
package `views` (which takes its hand-act recorder and its finding writer from its caller), `outward`, the parts of
|
||||
`forgekey` that modules of more than one domain use, and a seat-neutral part seats of more than one domain share,
|
||||
as long as it names no seat, verb or seat's type. In the milestone of ADR 0612 its `go/delivery` is moved whole and
|
||||
deleted, each of its names to the home this section gives it, and every other part naming a seat or a holder's
|
||||
state leaves for its owner, every importer moving in the same delivery group; after it, mesh-sdk holds no package,
|
||||
type or function over a seat defined whole, and no seat list (ADR 0627 §4, ADR 0634 §4, ADR 0635 §1).
|
||||
|
||||
**How it is checked** (ADR 0625 §11, ADR 0627 §3, ADR 0634 §10 and ADR 0635 §6, all built in the milestone of ADR 0612):
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| One package per seat defined whole | a unit test of the generator (one program in the Go build module, emitting both languages and run by the Node build module too) emitting one package per seat defined whole, covering each of the 25; the manifest validator refuses a contracts module whose package holds the generated files of two seats defined whole, naming them |
|
||||
| The imports run one way | a mesh-sdk unit test that no package of mesh-sdk imports a contracts module; the generator tests check no generated package imports mesh-sdk; a domain SDK's importers and imports by the review below |
|
||||
| mesh-sdk holds no logic over seats, and no part that left it | mesh-sdk's package-list test refuses by name `go/delivery`, `asks`, `asker`, the vault's types and its shared calls, the compose-candidate checks, the provider runtime, and every other package or part ADR 0634 §4 and ADR 0635 §4 move; its seat-naming test refuses the defined seats' names in mesh-sdk, so a copy kept without importing a contract fails too |
|
||||
| A client uses only its own seat's contract and its domain's SDK | the catalog check of a client: its own test refuses a client importing another seat's client or another domain's SDK, and one naming a module's tool or subject, and passes every client of the milestone |
|
||||
| The parts transfer is seat-neutral | its unit test, in the Delivery domain's SDK, runs both sides against two different subjects and shapes the caller gives, and the seat-name check holds it seat-neutral; the review refuses a holder that takes parts in without it |
|
||||
| Only a client is written by hand in a published package | each build module's regenerate-and-compare test fails on a hand-written file outside a client package and passes a contracts module holding its definitions and fixtures; the step runs on every contracts module |
|
||||
| A module in a folder is built and published as one at a root | the delivery group's one check builds and publishes every new contracts module and the domain SDK from its folder, and `mesh-wireguard` and `mesh-connectivity-contracts` from the mesh-networking repository |
|
||||
| A seat is defined only in its domain's contracts module | the indexer refuses a second module's definition of a seat, with its finding naming both, and a move from `mesh-contracts` holds in either merge order (ADR 0636 §5) |
|
||||
| A holder keeps its seat's states | the manifest validator refuses a holder whose `requires` does not declare each of its seat's states with the same local name and template |
|
||||
| A contracts module holds no seat; a role only one module could ever play has none | the manifest validator's refusal of a contracts module that claims a seat; review both ways: a new seat definition's pull request is asked whether another module could take the role over, and a module's pull request is asked the same of each new tool or event family other modules call |
|
||||
| Each new exported name sits where this section puts it | the review of every pull request (playbooks 07 and 08, the code reviewer's definition) asks the home of each new exported type, field, constant, package or module against this section, a domain SDK's importers included |
|
||||
|
||||
**Acceptance tests**, each at its level (unit, or a case of the module's one whole-module integration test against the
|
||||
real bus and store) and in its repository. The move raises no repository's test count, since mesh-delivery is already
|
||||
over its budget: each case below replaces or joins an existing one.
|
||||
|
||||
| Test | Level | Repository |
|
||||
|---|---|---|
|
||||
| research 081's 14, 21, 25 and 26 | as research 081 sets them | as research 081 sets them |
|
||||
| mesh-sdk's seat-naming, import and package-list tests; no module's name in the runtime | unit | mesh-sdk |
|
||||
| a reader of the seat list, against a bus the test gives: a reader away longer than the events' retention reads the whole list again; a reader refuses a pull request head's definition; both list tools answer, the re-read one only for the operator and the healers | unit | mesh-sdk |
|
||||
| the node-engine, against a stand-in of the seat `mesh-catalog`: without the list it acts on nothing but the bootstrapped modules, then on all once the stand-in answers; after a restart it starts its last applied compositions again; a refused definition stops no running module | a case of the node-engine's whole-module test | `mesh-host` (the node-engine's repository) |
|
||||
| the indexer starts with only the bootstrapped seats and answers `seat-definitions`, without a seat (names and digests) and with one; with the indexer kept down, the node's finding is written after five minutes and the condition keeper raises it to the operator | cases of the indexer's whole-module test | mesh-delivery |
|
||||
| a trunk definition that does not parse writes its finding and a fixed one clears it; a trunk build end naming out-of-step types writes the finding, a clean build end of another contracts module of the same repository leaves it, and a clean one of the same module clears it | a case of the indexer's whole-module test | mesh-delivery |
|
||||
| `indexer_seat_definition` answers the trunk's entry while a pull request holds another; a pull request's entries go when it closes; a definition over 256 KiB is refused; a seat-list-grain definition is read from a contracts module like a whole one; a removed definition says `seat-definition-removed` and its seat takes no new claim afterwards; a second module defining a seat is refused with its finding, and a definition moving from `mesh-contracts` to its domain's contracts module holds in either merge order; each of the indexer's tools answers | cases of the indexer's whole-module test | mesh-delivery |
|
||||
| the manifest validator refuses a claim, an `invokes`, a `consumes` or a grant naming `mesh-placement` or the seat `mesh-enrolment`, and a holder missing one of its seat's states in `requires` | unit | mesh-delivery |
|
||||
| the checker's `verdict` names out-of-step files; the group's check reads seat definitions at every member's head | a case of the checker's whole-module test | mesh-delivery |
|
||||
| each build module's compare step fails on a differing, a missing and an unwritten file, naming each, per contracts module | unit | mesh-catalog |
|
||||
| a change in one folder module rebuilds and publishes only it and its importers; a module with both a deployed and a package artifact (the grants part or disk-load) is built, delivered and published | a case of the Go build module's whole-module test | mesh-catalog |
|
||||
| after the delivery, `mesh-module-assigner.placement` answers and `mesh-placement.placement` is not found; placement keeps its revisions; no `seat-holders` key, finding or condition stays under an old name | live, read through the mesh's tools | the mesh |
|
||||
| `mesh-wireguard` is built and delivered from the mesh-networking repository | the group's one check, then live | mesh-networking |
|
||||
|
||||
## The foundation's seats
|
||||
|
||||
`mesh-controller`, `mesh-store` and `mesh-broker` name which assignment the mesh *itself* uses: the
|
||||
@@ -312,6 +689,13 @@ difference: it receives a URL either way.
|
||||
|
||||
With the seat unheld, a build from the seat is refused and says why. External builds carry on.
|
||||
|
||||
**The seat's verbs, from research 085's milestone.** The delivery parts reach the forge through the seat `git`'s
|
||||
verbs alone, never by the forge module's own tools: reads for the parts that declare them in their `invokes`, and
|
||||
acts (a status, a comment, the delivery's view, a line on the commit's note, a merge) for the forge bridge alone, as
|
||||
the holder's trusted value `callers` names; the forge's events (a pull request updated, merged or closed, the forge's
|
||||
own webhooks, a repository writable again) are the seat's events. Design 47, *The verbs*, lists them with their
|
||||
callers and how they are checked.
|
||||
|
||||
**Not yet designed:** a credential for cloning a private repository. The mesh's own repositories are
|
||||
public. The natural place for a clone credential is a `secret` from the vault, and that is a decision
|
||||
still to take.
|
||||
@@ -326,7 +710,7 @@ checked as their tables say:
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The set is closed, and every mesh entry names its decision | 0118: a unit test on the mesh's own entries. **The refusal of an unknown seat is at registration, not in the parser** — correction of fact, 2026-09-27: a module may hold a seat *another* module declares, which is the point of naming the seat and not its provider, so whether a claimed name exists is a fact about the whole catalogue and a manifest in isolation cannot be judged on it. Registration tests cover an invented name and a name another module declares; the parser still refuses a claim on the mesh's own `mesh-*`/`node-*` namespace and a scope that disagrees with a declaration in the same manifest. |
|
||||
| The set is closed, and every mesh entry names its decision | 0118: a unit test on the mesh's own entries. **The refusal of an unknown seat is at registration, not in the parser** — correction of fact, 2026-09-27: a module may hold a seat *another* module declares, which is the point of naming the seat and not its provider, so whether a claimed name exists is a fact about the whole catalog and a manifest in isolation cannot be judged on it. Registration tests cover an invented name and a name another module declares; the parser still refuses a claim on the mesh's own `mesh-*`/`node-*` namespace and a scope that disagrees with a declaration in the same manifest. |
|
||||
| The set is derived, and enumerating it is a query | 0118: the overview lists the mesh's own plus every registered module's, asserted against a fixture mesh. |
|
||||
| `mesh-*` is the mesh's, and a module may not declare one | 0118: a registration test refusing a manifest that declares any `mesh-*` seat, naming the prefix. |
|
||||
| Two modules cannot declare the same seat | 0118: a registration test; the second is refused and the first untouched. |
|
||||
@@ -339,7 +723,7 @@ checked as their tables say:
|
||||
| `secret` has one provider, the holder of `mesh-vault` | 0161: a second claimant of the seat is refused by name (`CanHold`); *correction of fact, 2026-10-01: no parser rule ever reserved the word, the seat does the work*. |
|
||||
| A singular fact about machines is a placement of capacity one, refused by name | 0161: the private network command's test for a second hub; the store's unique index. |
|
||||
| A holder of `node-uplink` is the dialect the machine runs | 0161: the node-engine reports `uplink-<manager>` in its profile with every report; a resolution test refuses the other holder naming the capability. |
|
||||
| A seat's holder has the seats it needs beside it: the resolver configuration is refused at `assign` without the uplink held on its node, and the uplink's last holder cannot be taken from beneath it | 0220: seat-dependency tests on the definition, on a synthetic claimant, at assign, at unassign and at composition, and one reading the catalogue for the uplink's possible holders. |
|
||||
| A seat's holder has the seats it needs beside it: the resolver configuration is refused at `assign` without the uplink held on its node, and the uplink's last holder cannot be taken from beneath it | 0220: seat-dependency tests on the definition, on a synthetic claimant, at assign, at unassign and at composition, and one reading the catalog for the uplink's possible holders. |
|
||||
| A replicated seat has every holder on record and composes on each; a seat held once still refuses a second holder, on record or not; `--add` is refused for it | 0223: resolution and composition tests with two holders on record and a third machine, with two claimants and nothing on record, and on `mesh-store`; a unit test that only `mesh-dns-resolver` is replicated, surviving the store's rows; store tests keeping several holders once each, a handover leaving one, and unassigning one taking only its row. |
|
||||
| A retired seat leaves the set once nothing claims it, from the compiled set and the store's table both | 0220: the closed-set test's count, and the store migration that deletes the row. |
|
||||
| A node seat names only literal absolute paths; every path has one seat naming it outright, and stepping back does not cycle | 0287: a unit test over the compiled set; registration tests refusing a module's seat naming a path at mesh scope, with `${`, or already named by another seat. |
|
||||
@@ -347,3 +731,22 @@ checked as their tables say:
|
||||
| The uplink's step-back comes from the seat | 0287: the composition tests for the uplink and the resolver on one machine pass with the case in the collision check removed. |
|
||||
| Holdings are derived, and the overview lists every seat | 0118: the `seats` command test, including an unheld seat. |
|
||||
| A build source on the seat records no address; an unheld seat refuses only self-hosted builds | 0111's tests. |
|
||||
|
||||
## Reviewed by architectural domain
|
||||
|
||||
The amendment of 2026-11-03 for research 081 (ADR 0605 to 0611; *A seat's definition*) was reviewed with designs 51
|
||||
and 32 by the four reviewers by architectural domain; the findings and what answers them are listed in design 51's
|
||||
*Reviewed by architectural domain*. For this design: only the trunk's definition is the contract (security), the
|
||||
indexer learns out-of-step types from the check's verdict event and runs no generator (seams), findings go to the
|
||||
condition keeper (health), a pull request's entries are removed when it closes (running qualities).
|
||||
|
||||
The milestone's amendment (ADR 0612, ADR 0613, ADR 0614, ADR 0620: the seats defined whole, the rest at the seat
|
||||
list's grain, the contracts marker, out of step learned from the checker's `verdict` and the builder's build end, the
|
||||
seat list's one source), with the amendment of 2026-11-04 (ADR 0625, ADR 0627: ten seats defined whole, a package per
|
||||
seat, a package for the seats at the seat list's grain in `mesh-contracts`, the seven clients and the seat-neutral parts transfer, *Where a
|
||||
seat's code goes*), and the amendment of 2026-11-05 (ADR 0634, ADR 0635: shared code with its owner, the domain
|
||||
SDK, a module's own package, a contracts module per domain the milestone moves, 25 seats defined whole with their
|
||||
states, the seat list read from the bus, two seats renamed), with ADR 0636 (the seat list through the seat
|
||||
`mesh-catalog`, a reader without it acting on nothing new, a seat defined only in its domain's contracts module, the
|
||||
Connectivity domain in the mesh-networking repository; accepted at its version 15) and the lead's rulings on the first review by architectural
|
||||
domain written in, await their final review, which rewrites this section.
|
||||
|
||||
@@ -47,7 +47,7 @@ A requirement has three parts:
|
||||
|
||||
**A contract is shared, not per module.** A database's contract is the database's, whoever requires
|
||||
it. **The controller holds every contract**, one per provision name, declared where the provision is
|
||||
defined in the catalogue. Today contracts are implicit in each provider's served fields; the first
|
||||
defined in the catalog. Today contracts are implicit in each provider's served fields; the first
|
||||
phase below makes them explicit, because nothing can be checked against a contract that is not
|
||||
written down. A provider is checked against the contract it claims to answer. A module's own
|
||||
specification may narrow a contract (a password of at least this length, a directory owned by this
|
||||
@@ -213,13 +213,13 @@ a placed directory under the node's root. Each is this design's provider in the
|
||||
placeholders have, not yet the one requirement form below; they are phase 1's first cases.
|
||||
|
||||
*Phase 3, in part (2026-09-30):* every definition's **own** data directory is placed; the conversion
|
||||
moved no data, proven by resolving both catalogues with the controller's rule and comparing
|
||||
moved no data, proven by resolving both catalogs with the controller's rule and comparing
|
||||
([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
|
||||
What the mesh writes *for* a module was still placed by the definition
|
||||
([issue 174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)),
|
||||
the gap this design answered on 2026-09-26; built later the same day: a directory saying
|
||||
`place: "mesh"` is `<root>/mesh/<module>`, a directory beneath a placed one states its path as
|
||||
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogues resolved and compared —
|
||||
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogs resolved and compared —
|
||||
shows forty-eight definitions naming the paths they named before.
|
||||
|
||||
*A preference has a default; the operator's own value has none (2026-10-08,
|
||||
@@ -323,7 +323,7 @@ value in a container's environment is refused when the definition is parsed, wit
|
||||
containers, login, broker account and settings are keyed by it, as today, and a login still fits the
|
||||
backend that keeps it (ADR 0049):
|
||||
the bound is the one the offer of each provision it requires states, a keyless provision states none,
|
||||
and an overflow is refused by the catalogue check before merge and left out of the provider's grants,
|
||||
and an overflow is refused by the catalog check before merge and left out of the provider's grants,
|
||||
reported, at composition — never a refusal of the provider's machine
|
||||
(ADR 0225).
|
||||
|
||||
@@ -371,7 +371,7 @@ not rotated by the vault, which cannot make its replacement: an operator deliver
|
||||
|
||||
**Each requirement says how its recipient takes a new value.** It either *applies* it, through a
|
||||
provisioner (a provider setting a login's password, the broker's provisioner updating an account, a
|
||||
store's provisioner changing its own superuser), or *reads it at start*. Every module in the catalogue
|
||||
store's provisioner changing its own superuser), or *reads it at start*. Every module in the catalog
|
||||
reads its secrets at start, and none watches them. The node-engine already recreates a container when a file
|
||||
it read at creation changes, its env-files and files mounted into it directly
|
||||
([issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)).
|
||||
@@ -433,7 +433,7 @@ as waiting, and nothing is delivered until the answer arrives.
|
||||
| paths in resources, mounts, bindings, secrets and received files | host directory requirements, placed by the assignment |
|
||||
| literals carried in a definition | operator requirements with defaults |
|
||||
|
||||
Each is retired only once nothing uses it. Until then both are accepted, and a catalogue test lists
|
||||
Each is retired only once nothing uses it. Until then both are accepted, and a catalog test lists
|
||||
the definitions still using the old form. That list shrinks to empty, and then the old form is
|
||||
removed from the parser.
|
||||
|
||||
@@ -453,7 +453,7 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
|
||||
is fixed in the node-engine already. *Ends when* nothing outside the vault generates a shared secret after
|
||||
genesis, a lab consumer of analytics receives its site id, and a database credential rotates over
|
||||
its two credentials, the consumer recreated by derivation, never without a working login, and its data intact.
|
||||
3. **Definitions move, and seats move to assignments.** Every catalogue definition is rewritten, adopted
|
||||
3. **Definitions move, and seats move to assignments.** Every catalog definition is rewritten, adopted
|
||||
and running assignments placed where their data already is, and each claim becomes a seat the
|
||||
module can hold, held by the assignment that holds it today. *Ends when* the list of definitions
|
||||
using an old form is empty, the old forms are removed, and the store module runs on two lab
|
||||
@@ -464,7 +464,7 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Every requirement has one of the four provider kinds | The parser refuses any other. |
|
||||
| A definition names no host path, node or mesh | The catalogue tests of ADR 0112. |
|
||||
| A definition names no host path, node or mesh | The catalog tests of ADR 0112. |
|
||||
| A module provider is chosen by named seat, pin, co-location, only one, a person's choice | Resolution tests for each step: a requirement naming a seat served by its holder even with another provider on the consumer's node, and refused when the seat is unheld; several candidates and none local, where assignment lists them with the seat's holder first and records the choice as a pin, and refuses without one. |
|
||||
| A provider answers within its contract | A controller test: an answer carrying a field its contract does not name, or missing one it does, is refused and not delivered. |
|
||||
| A module narrows a contract and never widens it | The parser refuses a module specification that loosens a contract's field. |
|
||||
@@ -484,9 +484,9 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
|
||||
| The controller and a node's host take the same path | 0113's tests: the controller's definition declares requirements and no own secret; a node's bus account is made by the vault and delivered sealed to that node. |
|
||||
| Moving the vault or the broker is break-glass | A resolution test: an ordinary assignment moving `mesh-vault` or `mesh-broker` is refused, naming the procedure. |
|
||||
| A secret that cannot be rotated says so | A vault test: rotating a secret marked not rotatable by the mesh is refused, naming why. |
|
||||
| Only the controller reads the seat placeholder | A catalogue test, from phase 3: no definition uses the seat placeholder. |
|
||||
| Only the controller reads the seat placeholder | A catalog test, from phase 3: no definition uses the seat placeholder. |
|
||||
| Refusal names everything at once | A resolution test with three unresolved requirements of different kinds: one refusal naming all three. |
|
||||
| The old forms retire | The catalogue test listing definitions still using one. It must be empty before a form is removed. |
|
||||
| The old forms retire | The catalog test listing definitions still using one. It must be empty before a form is removed. |
|
||||
|
||||
## Not settled here
|
||||
|
||||
|
||||
@@ -169,11 +169,11 @@ paper is wrong until there is a second mesh to find out.
|
||||
|
||||
The derivation refuses two things where they can still be named: two users with one name — the
|
||||
server reads the file as one of them and which one depends on the order — and a module
|
||||
assigned but absent from the catalogue, which would compose a user with no authority and fail
|
||||
assigned but absent from the catalog, which would compose a user with no authority and fail
|
||||
on its first publish with an authorisation error that says nothing about a missing manifest.
|
||||
A user the mesh has minted no password for is *named* rather than dropped or written as a user
|
||||
anybody is: an ordinary situation with an obvious remedy, and the caller decides whether a
|
||||
partial file is worth writing. A seat's protocol is gathered across the whole catalogue, not
|
||||
partial file is worth writing. A seat's protocol is gathered across the whole catalog, not
|
||||
from one manifest, because a seat is declared by one module and held by another.
|
||||
|
||||
**Out, and what each needs.**
|
||||
@@ -447,7 +447,7 @@ pays for itself furthest away.
|
||||
derived to subjects, the three namespaces, permissions computed from a declaration, and a
|
||||
manifest that contains no subject. Done in the controller's composer (permissions, streams,
|
||||
consumers), in the runtime's client (subjects derived from the credential, never named by a
|
||||
module), and as a catalogue test asserting all 72 manifests hold no subject — because the
|
||||
module), and as a catalog test asserting all 72 manifests hold no subject — because the
|
||||
rule held by construction, and a rule held by construction is one a later field breaks
|
||||
quietly.
|
||||
- [x] 3.9 **seats declared by modules** — the manifest now carries `seats` (name, scope,
|
||||
@@ -470,7 +470,7 @@ pays for itself furthest away.
|
||||
all: a holding is derived at resolution, never stored, so nothing recorded points at an old
|
||||
name (recorded as a progressive insight on ADR 0126). A **kept** rename table tells a
|
||||
manifest written against an old name what it became, because a module lives in its own
|
||||
repository and may be registered long after the catalogue stopped using one.
|
||||
repository and may be registered long after the catalog stopped using one.
|
||||
|
||||
**A seat and the interface it delivers are different names.** The `git` seat became
|
||||
`mesh-git` while the `git` *provision* it delivers did not change, and the same for the
|
||||
@@ -522,7 +522,7 @@ it, and the beds that need a mesh living on NATS can finally run.
|
||||
(ADR 0129). Both sides
|
||||
are behind a seam with an implementation per bus, and on the bus being built one publish does
|
||||
what two did: the outcome is the role's own event, so the asker matches it by the id its
|
||||
request carried, the controller records it and the catalogue places it in the graph. A builder needs a reply queue for nothing and a grant over nobody's inbox.
|
||||
request carried, the controller records it and the catalog places it in the graph. A builder needs a reply queue for nothing and a grant over nobody's inbox.
|
||||
|
||||
Checked against a running server: the round trip; a third party on the role's event hearing
|
||||
the same outcome the asker did, which is what the decision rests on; work leaving the queue
|
||||
@@ -532,7 +532,7 @@ it, and the beds that need a mesh living on NATS can finally run.
|
||||
|
||||
The outcome carries the module name, because only the manifest says what was built and one
|
||||
message now has three readers. A failed build names none: it produced no module version, and
|
||||
the catalogue would otherwise place something that was never made.
|
||||
the catalog would otherwise place something that was never made.
|
||||
- [x] 4.3 an installation completes over the bus, with the same outcome as the path it replaces —
|
||||
**the installer can raise it**: a foundation template that stands up the server, writes the
|
||||
server's own settings and the mesh's first user list beside them, and starts a controller
|
||||
@@ -573,7 +573,7 @@ it, and the beds that need a mesh living on NATS can finally run.
|
||||
correct. It uses the client a module's runtime uses, so what a person may do is answered by the
|
||||
same permission list that answers it for a module and an audit has nothing separate to read.
|
||||
|
||||
Three decisions in it worth keeping. It lists what the **catalogue** has rather than what this
|
||||
Three decisions in it worth keeping. It lists what the **catalog** has rather than what this
|
||||
credential may call: somebody seeing only their own tools cannot tell "not installed" from "not
|
||||
yours", and those need different people to fix them. A failed call says which of three things
|
||||
happened — nobody serves it, this credential may not, or the tool was slow — because the
|
||||
@@ -597,10 +597,10 @@ it, and the beds that need a mesh living on NATS can finally run.
|
||||
|
||||
**The catch-up half needed nothing built, and that was the answer.** It existed because a queue on
|
||||
the bus the mesh runs on today receives only what is published after it is bound, so everything
|
||||
built before the catalogue existed was announced to nobody — and on a fresh mesh that is always
|
||||
the foundation, because those are the things the catalogue needed in order to exist
|
||||
built before the catalog existed was announced to nobody — and on a fresh mesh that is always
|
||||
the foundation, because those are the things the catalog needed in order to exist
|
||||
([issue 050](../../04-ISSUES/050-the-catalogue-knows-nothing-built-before-it/00-report.md)). A
|
||||
whole mechanism followed: the catalogue asks, the controller re-publishes.
|
||||
whole mechanism followed: the catalog asks, the controller re-publishes.
|
||||
|
||||
A stream is a log and a consumer is a position in it. A consumer created later starts at the
|
||||
beginning, so the builds are simply there — asked of a running server rather than assumed, since
|
||||
@@ -636,8 +636,8 @@ left to be re-derived under time pressure:
|
||||
| 1 | this one | prose; nothing deploys |
|
||||
| 2 | the sdk | comments only, and no module rebuilds for it |
|
||||
| 3 | the client library | it is what a module calls to emit, and it is where the subject is derived. Until it lands, a locally-named event is published under the local name itself |
|
||||
| 4 | the catalogue | every manifest and every module's code, renamed together. Safe only once the runtime derives |
|
||||
| 5 | the controller | **it refuses an old-style event name outright**, so landing it before the catalogue makes every unconverted module unregisterable |
|
||||
| 4 | the catalog | every manifest and every module's code, renamed together. Safe only once the runtime derives |
|
||||
| 5 | the controller | **it refuses an old-style event name outright**, so landing it before the catalog makes every unconverted module unregisterable |
|
||||
| 6 | the node-engine | last, because nothing else waits on them |
|
||||
|
||||
Two properties make the sequence safe rather than merely ordered, and both are pinned by tests. A
|
||||
@@ -682,11 +682,11 @@ healthy while reacting to nothing.
|
||||
- [x] 5.4 **the old broker and everything that named AMQP leave the mesh** (ADR 0131,
|
||||
superseding ADR 0127): the two modules that
|
||||
required `amqp` are removed, the broker's module is unassigned and removed (**done 2026-09-28**; the predecessor's own tooling, which rode the same adopted broker, went dark with it, as ADR 0130 accepted), registration refuses
|
||||
a manifest that provides or requires `amqp`, and a whole-catalogue check asserts none does. Not
|
||||
a manifest that provides or requires `amqp`, and a whole-catalog check asserts none does. Not
|
||||
a retirement condition — a decision, taken, with the operator's "I don't care if the predecessor
|
||||
breaks" on record (ADR 0130).
|
||||
Retiring with it: the build outcome's second announcement under the module's own name, which
|
||||
existed only so a catalogue deployed before the rename and one after both heard it.
|
||||
existed only so a catalog deployed before the rename and one after both heard it.
|
||||
|
||||
> **The remote tooling goes with it too.** The predecessor's own mesh talks over that broker, so
|
||||
> shutting it down ends the path that reaches this installation's machines from a workstation.
|
||||
|
||||
@@ -5,7 +5,7 @@ code:
|
||||
- mesh-controller internal/inventory
|
||||
- mesh-controller internal/catalogue
|
||||
- mesh-controller cmd/mesh-controller
|
||||
updated: 2026-10-16
|
||||
updated: 2026-11-06
|
||||
decisions:
|
||||
- "ADR 0181"
|
||||
- "ADR 0182"
|
||||
@@ -16,6 +16,11 @@ decisions:
|
||||
- "ADR 0117"
|
||||
- "ADR 0399"
|
||||
- "ADR 0400"
|
||||
- "ADR 0412"
|
||||
- "ADR 0415"
|
||||
- "ADR 0416"
|
||||
- "ADR 0417"
|
||||
- "ADR 0422"
|
||||
---
|
||||
|
||||
# 29 — A node has operator accounts, and the mesh owns what lives under a home
|
||||
@@ -58,6 +63,15 @@ These are a **family**, not one module: an `ssh-client` module, a shell module,
|
||||
module, each a *universal-tier* consumer of the account fact — assigned wherever a person logs in,
|
||||
which is every node, unlike the graphical stack that a capability gates.
|
||||
|
||||
**A folder a module owns says how it treats a file it did not write** (ADR 0416, ADR 0422). Declaring a
|
||||
directory under a home owns it (ADR 0182 §1), and the owner alone writes there and reacts to every file
|
||||
that enters it, never losing what the file held. The manifest says how, on the directory itself:
|
||||
`unknown` is `move-out` (the node-engine moves every path the manifest does not declare and `knows` does
|
||||
not name into `~/.local/state/mesh-moved/<module>/`, with a list), `module` (the module's own process
|
||||
reacts, as ssh-client does in `~/.ssh`) or `program` (everything in it is the program's, as in a
|
||||
service's data directory). `move-out` is only for a folder under a home, the home of the account the
|
||||
assignment runs for (ADR 0412 §5). A folder many modules write in is not declared at all.
|
||||
|
||||
## 3. The whole of `~/.ssh` is the mesh's — with one boundary drawn inside it
|
||||
|
||||
The predecessor owned a single file (`~/.ssh/config`) and left the rest alone; it drifted, because
|
||||
@@ -84,6 +98,9 @@ ADR 0117 draws for the uplink and the sshd
|
||||
module draws for the firewall: **the mesh must never be able to arrange the one failure that severs
|
||||
its own way back in.** The carve-out is not a convenience; it is that rule, in `~/.ssh`.
|
||||
|
||||
Since ADR 0415 and ADR 0417 the found class no longer holds in `~/.ssh`: ssh-client moves out what it
|
||||
does not know into a kept folder, and never moves a key or `authorized_keys`, which keeps this boundary.
|
||||
|
||||
## 4. Keys are the mesh's to generate — through a CA, and existing keys are adopted, not replaced
|
||||
|
||||
Key *generation* is the mesh's, not each node's improvising its own. The clean form is an **SSH
|
||||
@@ -157,7 +174,7 @@ the client/identity side and the CA are the open pieces.
|
||||
`ssh-client` template can emit a `Host` block per node with the right login name. Composed
|
||||
end-to-end in the controller's tests.
|
||||
|
||||
**Written but not shipped:** the `ssh-client` catalogue module itself exists on a branch of the
|
||||
**Written but not shipped:** the `ssh-client` catalog module itself exists on a branch of the
|
||||
module repository; its pull request was closed with a hold until this design is deployed, and
|
||||
nothing has deployed it since. The predecessor's generator still writes every workstation's ssh
|
||||
client blocks today — which is where [issue 172](../../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md)
|
||||
@@ -192,7 +209,7 @@ alias and its trust, and a fresh machine has no operator dotfiles at all — the
|
||||
service and leave the human unable to work on the box.
|
||||
|
||||
**Why not build it reflexively:** it is a real addition to the node model, the resource model, and
|
||||
the seat set, and must be gotten right. The mechanism half is now settled — ADR 0120 is what lets
|
||||
the seat list, and must be gotten right. The mechanism half is now settled — ADR 0120 is what lets
|
||||
the ssh files be templates with no controller format — so what remains to decide here is the
|
||||
model:
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: proposed
|
||||
code: []
|
||||
updated: 2026-10-06
|
||||
status: in-progress
|
||||
code: [mesh-controller]
|
||||
updated: 2026-11-06
|
||||
decisions:
|
||||
- "ADR 0238"
|
||||
- "ADR 0221"
|
||||
@@ -15,6 +15,8 @@ decisions:
|
||||
|
||||
# 30 — The mesh updates itself on a push
|
||||
|
||||
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's delivery plans, composition check and merge window (plan.go, release_plan.go, merge_gate.go, merge_window_bus.go, changeplan.go, with their migrations), which design 47 builds on. Not yet checked against this text nor given an as-is: issue 1075.
|
||||
|
||||
**Today the mesh does not update itself; a person drives the pipeline by hand, and one class of
|
||||
change freezes it.** A code change lands in `mesh-controller` or `mesh-catalog`, and getting it onto
|
||||
the machines is a sequence somebody types. The predecessor's pipelines rebuilt and redeployed on a
|
||||
@@ -28,7 +30,7 @@ webhook".
|
||||
seat or schema:
|
||||
|
||||
1. `module moved <module> <commit>` — tell the mesh its source advanced (the controller repo has no
|
||||
trigger, so this is manual; the catalogue's webhook does it automatically — see below).
|
||||
trigger, so this is manual; the catalog's webhook does it automatically — see below).
|
||||
2. `build --behind` (or `build <repo> [--ref] [--path <subdir>]`) — the builder rebuilds and
|
||||
records the new image.
|
||||
3. The mesh **reconciles on its own**: the module's declaration now names the new image, the next
|
||||
@@ -39,7 +41,7 @@ seat or schema:
|
||||
**A breaking change** — a manifest schema the controller parses differently (a fact's shape, a
|
||||
seat's name), where the new controller cannot read the manifests the old one stored:
|
||||
|
||||
4. Land the code (controller + catalogue together — they are one change).
|
||||
4. Land the code (controller + catalog together — they are one change).
|
||||
5. Rebuild + deploy the new controller (steps 1–3). **The moment it is live it refuses the
|
||||
still-old-shape stored manifests, and composition freezes for every node that runs an affected
|
||||
module.** Running services are untouched; only new declarations stop.
|
||||
@@ -61,7 +63,7 @@ automating this must solve.
|
||||
|
||||
### 1. The trigger today is HAL's, not the mesh's
|
||||
|
||||
Build-on-push works for the catalogue because its repository has a Gitea webhook pointing at
|
||||
Build-on-push works for the catalog because its repository has a Gitea webhook pointing at
|
||||
`http://host.docker.internal:9877/webhook/gitea` — and **that receiver is `hal-gitea-tools.service`**
|
||||
(`~/.hal/modules/hal/gitea/tools/server.js`), a *predecessor* component. The nox builder consumes
|
||||
build work; it does not receive Git events. So the mesh's own build pipeline currently rides on a
|
||||
@@ -76,12 +78,12 @@ HAL service, and:
|
||||
seat — `mesh-forge-trigger` or folded into the git seat's holder) that receives Git events and turns
|
||||
them into build work over the broker, for **every** repository including `mesh-controller`. Replacing
|
||||
`hal-gitea-tools` is the concrete first build. Its logic already exists to copy: match the pushed
|
||||
repository (and changed paths, for a monorepo like the catalogue) against the build-context
|
||||
repository (and changed paths, for a monorepo like the catalog) against the build-context
|
||||
repository of every registered module, and rebuild the matches.
|
||||
|
||||
### 2. The builder validates too — and a breaking change deadlocks it
|
||||
|
||||
The builder embeds the same catalogue package the controller does, so **it validates a
|
||||
The builder embeds the same catalog package the controller does, so **it validates a
|
||||
manifest against its own compiled-in seat/schema set**. A breaking change therefore couples *four*
|
||||
things, not two: the controller, the **builder**, every affected manifest, and every node's host.
|
||||
This session's seat rename rebuilt the controller but not the builder, and the stale builder then
|
||||
@@ -102,7 +104,7 @@ does not know. The old builder cannot build the new builder. Escapes:
|
||||
|
||||
Either way, self-update for breaking changes needs a **transition discipline** so a push does not
|
||||
auto-freeze: the new controller (and builder) should accept the *old and new* shape together for
|
||||
one release — deprecated aliases in the seat set, a schema that reads both — then a later release
|
||||
one release — deprecated aliases in the seat list, a schema that reads both — then a later release
|
||||
drops the old. With that, a breaking change rolls out on a push like any other: everything reads
|
||||
both, the manifests migrate, the compatibility is removed. Without it, self-update would simply
|
||||
automate the freeze.
|
||||
@@ -124,7 +126,7 @@ automate the freeze.
|
||||
Revision, ADR 0162. The
|
||||
trigger exists: the forge announces a merge on the bus and the controller acts on it (ADR 0157 made
|
||||
the build narrate; this makes the merge a plan). A module's dependencies are one relation in the
|
||||
catalogue — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes
|
||||
catalog — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes
|
||||
what changed and everything reachable from it along those edges, sorts the set into tiers, writes the
|
||||
plan to the store, asks the first tier and returns. Each outcome advances the plan; a tier whose
|
||||
rolled-out modules a later tier is built by waits until the machines report them applied; a
|
||||
@@ -166,7 +168,7 @@ Revision, ADR 0238
|
||||
it — the module's own directory, the whole repository for a module built from its root, a repository a
|
||||
recipe names — and a file no build reads, at the root or in a directory holding no module, rebuilds
|
||||
nothing. One function maps a changed file onto modules and one answers what a merge moves and builds;
|
||||
the merge handler, the what-if, the merge gate and a pull request's check ask them. Only a commit on the
|
||||
the merge handler, the what-if, the composition check and a pull request's check ask them. Only a commit on the
|
||||
module's trunk is registered.
|
||||
|
||||
## What a push does not send (2026-10-05)
|
||||
|
||||
@@ -12,8 +12,16 @@ code:
|
||||
- mesh-tools src/main.ts
|
||||
- mesh-catalog modules/mesh-catalog
|
||||
- mesh-tools node-tools/internal/runtime (a module's state, ADR 0201)
|
||||
updated: 2026-10-12
|
||||
- mesh-sdk (the typed bus-state entry in `requires` and its binding, ADR 0595)
|
||||
updated: 2026-11-04
|
||||
decisions:
|
||||
- "ADR 0609"
|
||||
- "ADR 0612"
|
||||
- "ADR 0613"
|
||||
- "ADR 0618"
|
||||
- "ADR 0606"
|
||||
- "ADR 0595"
|
||||
- "ADR 0596"
|
||||
- "ADR 0307"
|
||||
- "ADR 0240"
|
||||
- "ADR 0233"
|
||||
@@ -62,7 +70,7 @@ onto this in step 3 of ADR 0116.
|
||||
This is the load-bearing rule.
|
||||
ADR 0112 says a manifest names no node, mesh or path. A transport address is the same class of thing: if
|
||||
manifests held literal subjects, reorganising the subject space would mean editing every module in
|
||||
the catalogue, and the mesh would have hundreds of copies of a decision it made once.
|
||||
the catalog, and the mesh would have hundreds of copies of a decision it made once.
|
||||
|
||||
| declared | derived |
|
||||
|---|---|
|
||||
@@ -94,7 +102,7 @@ lives under the emitting module's own name and the consumer carries that couplin
|
||||
|
||||
**How the rule is checked, because it was not.** *Added 2026-09-27, same source.* Two checks, because
|
||||
the mistake happens at two scales. Per manifest, at registration: an event is a local name, and the
|
||||
old bus's form is refused with the name to write instead. Across the whole catalogue, as a test:
|
||||
old bus's form is refused with the name to write instead. Across the whole catalog, as a test:
|
||||
where a consumed event's emitter is present, it must emit that event. The second cannot demand a live
|
||||
emitter for everything — a module lives in its own repository and may be installed long before the
|
||||
one whose events it wants — so it says nothing about an absent emitter and everything about a present
|
||||
@@ -117,7 +125,7 @@ caller already handles). The kind token is what makes `mesh.mod.*.event.>` a saf
|
||||
first draft of this table had no token, which reads better and cannot be implemented.
|
||||
|
||||
**The test this must pass: the manifest survives the wire changing.** Reorganise the subject space
|
||||
and every manifest in the catalogue is still correct. That is the property
|
||||
and every manifest in the catalog is still correct. That is the property
|
||||
ADR 0039 gave the sdk, applied to
|
||||
declarations.
|
||||
|
||||
@@ -172,7 +180,7 @@ can be wrong.
|
||||
|
||||
**And one publish reaches three audiences without anybody's inbox being opened.** A build's outcome is
|
||||
the seat's own event: whoever asked matches it by the id their request carried, the controller records
|
||||
it, the catalogue places it in the graph. That is the fan-out a shared exchange gave for free, written
|
||||
it, the catalog places it in the graph. That is the fan-out a shared exchange gave for free, written
|
||||
as a subject the mesh derived instead of a topology somebody configured — and it is why a holder needs
|
||||
no permission to publish into an asker's inbox, which is the one grant design 25 §4 refuses by name.
|
||||
|
||||
@@ -227,7 +235,7 @@ service" versus "one worker per machine".
|
||||
| credential | sealed, per consumer | none | none | none | none |
|
||||
| reply | — | none | none, or an event later | a report | awaited |
|
||||
| retention | — | age and size | work queue, explicit ack | **last per subject** | none |
|
||||
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own; a module's `state` / `reads` | `serves` |
|
||||
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own; a module's bus state in `requires` (ADR 0595), the old `state` / `reads` until it moves | `serves` |
|
||||
|
||||
**Job** is the one ADR 0041 had no room
|
||||
for. Its table has events at 1:many and provisions at 1:1; a module submitting work to a service
|
||||
@@ -249,7 +257,7 @@ registered for every machine, sent as an event, never reached a machine assigned
|
||||
consumer did not exist yet when the event passed — and a licence binding sent as events replays a
|
||||
week of rotations where only the latest matters. So a module names the state it **owns** with
|
||||
`state`, and another module's it **reads** with `reads: <module>.<name>`. Each is a key-value bucket
|
||||
the controller creates from the catalogue, mesh-wide, existing from registration so a reader can
|
||||
the controller creates from the catalog, mesh-wide, existing from registration so a reader can
|
||||
watch before the owner runs anywhere ([design 25](25-the-bus-on-nats.md) §3). Every instance of the
|
||||
owner writes; a reader reads and watches. A key may name a machine by the module's own convention;
|
||||
the mesh keeps one bucket per name, not one per machine, because "every server, for every machine"
|
||||
@@ -278,6 +286,42 @@ module's assignment** — what a module stored is data
|
||||
(ADR 0030) — and one whose
|
||||
declaration is gone is reported, never removed by the mesh.
|
||||
|
||||
**A module asks for its bus state in `requires`, and reads no other module's.** *Amended 2026-11-03, ADR 0595
|
||||
and ADR 0596, from research 082.* The `state` and `reads` of the two paragraphs above, and the sections `logs`,
|
||||
`streams` and `objects` beside them, are four vocabularies for one need: what a module keeps on the bus. They are
|
||||
replaced by one form, a **provision** of the bus module, as a database is postgres's:
|
||||
|
||||
- **An entry of `requires`, in its typed long form** (ADR 0595 §1): the local name, the storage type, the template
|
||||
(state, events, work queue, log, objects, ledger) and the template's options, within their bounds; a ledger also
|
||||
names its type (its schema, its operations each with its ledger effect, its indexes). The templates, their fixed
|
||||
settings and their options are [design 25](25-the-bus-on-nats.md) § 12.1. mesh-sdk's catalog package carries the
|
||||
entry's type and its strict decoding; every rule that judges it lives in the manifest validator (ADR 0595 §8).
|
||||
- **The binding is what the module receives**: the names on the bus for each local name, written into its
|
||||
membership by the grants part once the bus module says `created` (design 25 § 12.2). The module still names only
|
||||
locally (§ 1). A module whose binding is missing at start fails loudly, naming the entry.
|
||||
- **One name, one form.** A module asks for each entry once: an old section and the new form are never both used for
|
||||
one local name, and one name serves one storage type only.
|
||||
- **No readers.** The new form has no `reads`, and the manifest validator refuses one (ADR 0596 §2). Only the owner's account
|
||||
reaches its bus state. **A module's events are subscribed to; its data is asked for**: what another module needs
|
||||
of a module's kept data it asks through that module's tools or seat verbs (§ 4's tool relationship, `serves`),
|
||||
and what a module lets others hear is its declared events, heard through `consumes` (ADR 0363, unchanged; ADR 0596
|
||||
§1). A bucket's layout is never a contract another module depends on; a tool or a declared event is.
|
||||
- **A state may be read-only to the module's tools.** A state's declaration may carry `read-only: true`: only the
|
||||
processes of the module whose `bus.state` names that state write it, and the module's tools read it, holding no
|
||||
publish on its subjects. The manifest validator refuses a read-only state that no process of the module names. How it is checked:
|
||||
the manifest validator's unit tests for the refusal, and the composition test that a tools bundle's account holds a read on the
|
||||
state and no write. The bus module's state `spent` (design 25 § 12.3) is the first user. (A design limit of the
|
||||
milestone's review; no record decides it yet.)
|
||||
- **The move is a module at a time** (ADR 0595 §11, ADR 0596 §3), each when it is next touched, keeping its local
|
||||
names and its old settings as options so its data stays. A reader moves after its owner: the owner declares the
|
||||
event or gains the tool its readers need first, and the manifest validator refuses a moved module's subscription to an event
|
||||
its owner does not declare. An old section is refused once no module but the foundation's uses it.
|
||||
|
||||
What follows for the paragraphs above: for a module that has moved, ADR 0201 §2 (another module's state read by
|
||||
naming it in `reads`) no longer holds, and the bucket "every instance of the owner writes; a reader reads and
|
||||
watches" becomes "every instance of the owner writes and reads". The runtime's `get`, `put`, `delete`, `keys`,
|
||||
`watch` and `history` stay the calls of the template state.
|
||||
|
||||
**A module declares the data it holds.** *Added 2026-10-06,
|
||||
ADR 0233.*
|
||||
State on the bus is one kind of data; the rest lives on a machine's disks or with a provider, and the
|
||||
@@ -326,7 +370,7 @@ Every shape above appears once, in order, and no step knows where the next one r
|
||||
**A change lands.** The module holding `mesh-git` emits `pushed` — repository, ref, commit. An
|
||||
event, because it is a fact about git and git's identity is the meaning.
|
||||
|
||||
**The change becomes work.** The controller consumes `pushed`, asks the catalogue which modules
|
||||
**The change becomes work.** The controller consumes `pushed`, asks the catalog which modules
|
||||
are built from that repository and path
|
||||
(ADR 0069), and submits one
|
||||
**job** per affected module to the `mesh-build-machine` seat. The builder stays simple: it builds
|
||||
@@ -366,7 +410,7 @@ not to an address it was given at genesis. Held and retried while the store rest
|
||||
chain above went dark at the moment it touched a machine: nothing said which version a machine now runs,
|
||||
or that it refused to. The controller states those as facts under its own seat's namespace, when what
|
||||
a machine runs changes rather than on every convergence pass, and anything that cares subscribes the way
|
||||
the catalogue subscribes to `built` (ADR 0134).
|
||||
the catalog subscribes to `built` (ADR 0134).
|
||||
The facts are second-hand by design — one emitter, one ordering — and a machine that cannot reach the bus
|
||||
produces none, so absence is not health.
|
||||
|
||||
@@ -437,6 +481,136 @@ meaning into shape wherever it can go — a required `channel` field is caught,
|
||||
interpretation of an existing one is not. Saying so is better than implying the fingerprint is
|
||||
complete, because a team that believes it is complete stops reviewing for the case it misses.
|
||||
|
||||
### A package, its series, and the series its users accept (from 2026-11-03)
|
||||
|
||||
(ADR 0606 §4, ADR 0609, from research 081.) This is the manifest's half of design 51's versions.
|
||||
|
||||
- **A package is declared as an artifact**: its name, the kind `package`, its language (`go` or `typescript`), its
|
||||
paths and its **package series** (`0.1`). An image, a bundle or an archive may declare a package series the same
|
||||
way; one that declares none is numbered in `0.1`. Nobody types a version: the builder counts it (design 51).
|
||||
- **A package is named by its name in its own language** (a design limit of the registry milestone, 2026-11-04): a
|
||||
Go package by the module path of its `go.mod`, a TypeScript package by the name in its `package.json`. The
|
||||
indexer reads that name at each merge, the builder numbers the package under it, the package's registry seat
|
||||
publishes it under it, and a user's `packages` names it so. One name per package across the mesh, because the
|
||||
language's own tools fetch by that name and no other. How it is checked: the manifest validator refuses a name in `packages`
|
||||
that no package of the shelf is named, or that a member of the change's delivery group does not name at its head.
|
||||
- **Raising a package series** is a reviewed change of the package's manifest, for a change that breaks its users.
|
||||
The checker's verdict names every change of a package series it sees, so the review of the change sees it.
|
||||
- **A user names the mesh packages it takes in a section of its own, `packages`**, each with the package series it
|
||||
accepts. It is not `uses`, which grants publishing on a seat's `accepts` subjects (§ 3); a package is not a seat. A
|
||||
user builds, and is checked, against the newest version of that series; a publish in another series does not move
|
||||
it, and moving is a reviewed change of the user's manifest. A user that names none accepts the series of the
|
||||
package's first numbered version, so nothing is edited in today's users. The indexer's tool `indexer_package_users`
|
||||
lists, per package, its users and the series each accepts, so a user left on an older series is seen.
|
||||
- **Inside one repository** a module uses its sibling package by path, from the same commit, and waits for no publish.
|
||||
- **A user outside the package's delivery group is not built before the merge**: it is built after it, when the new
|
||||
version is in its series. A user that breaks fails its own build, is not wanted, keeps its previous build, and a
|
||||
condition tells the operator. A group's members are built in the group's check, against the group's tagged version.
|
||||
- **A contracts module's package** names the directory of the seat definitions it is generated from in the field
|
||||
`generated-from`, the contracts marker (design 26).
|
||||
- **The manifest validator reads the new words** (ADR 0578 §1): each package series well formed and only on the four kinds that may
|
||||
carry it, a package naming `go` or `typescript`, and each name in `packages` a package some manifest declares. A
|
||||
running manifest validator that does not know a word sets the manifest aside to wait, never refuses it, while a newer indexer
|
||||
build is merged and not yet delivered; and a node-engine is handed a manifest using these words only once it
|
||||
states the capability to read them (ADR 0614 §4).
|
||||
- **Cost**: the new manifest fields are read through mesh-sdk's manifest types, so their change rebuilds every module
|
||||
once; it is the one rebuild the milestone of ADR 0612 already takes for the contracts move (ADR 0612 §1, ADR 0613 §1).
|
||||
|
||||
### A config value renamed (from 2026-11-03)
|
||||
|
||||
(ADR 0609 §4.) A config value's declaration may name its old name, `renamed-from`. On the merge, mesh-config moves the
|
||||
value it holds under the old name to the new name in its own state, without the operator giving it again, emits
|
||||
`renamed` (module, old name, new name, when; its family `config.events`, kept seven days) and records it. This design decides what ADR 0609 leaves open:
|
||||
|
||||
- **Which values**: a module's own config values only, never a secret, never another module's value. A rename keeps
|
||||
the value's trust: a value set without the operator's Approve stays untrusted under its new name (ADR 0348 §8), and
|
||||
the checker refuses a rename that would change a value's trust.
|
||||
- **How long the old name is read** (the **rename window**): until every node-engine whose node has a want of the
|
||||
module reports applied a build of it that reads the new name. mesh-config hears the status view's `delivered`
|
||||
events and, at its start and on each, asks its `node_haves` for every node, away ones included, since `delivered`
|
||||
counts only the nodes present (design 47); until then mesh-config answers both names. A node away when the last
|
||||
`delivered` was said should close its window when it returns, with no other delivery: mesh-config also wakes on
|
||||
the node-engine seat's event `mesh.seat.node-engine.event.manifest-reads-changed`, said when a node's stated
|
||||
capabilities change and when it returns after being away (ADR 0618 §4), as for the old seat name (design 51), and
|
||||
on it reads that node's stated capabilities through the seat's verb `manifest-reads` (ADR 0618 §3) and its applied
|
||||
builds through `node_haves`, never a watch of the node-engine's have state (ADR 0596). mesh-config declares
|
||||
`seat:node-engine.manifest-reads` in its `invokes`: that is the grant the grants part composes, and with it the
|
||||
grants part grants mesh-config the subscription to the event's subject (its unit row is in design 51). A
|
||||
node-engine that does not yet serve `manifest-reads` counts as stating nothing: the window stays open and
|
||||
the seat's verb `renames` names the node. ADR 0618 does not cover a
|
||||
returned node that applies the module's new build only after its return event, since that build changes what the
|
||||
module reads, not what the node-engine states: such a node closes the window on the next `delivered`, whichever
|
||||
delivery says it. A node holding a window open
|
||||
for seven days is mesh-config's finding naming the node, cleared when the window closes; `config_rename_close`
|
||||
drops a node from a window by hand, recorded as a hand act.
|
||||
- **The checker asks the seat, never the module.** The seat `mesh-config` gains the optional verb `renames` (the
|
||||
open renames, and the nodes holding each window open), in its seat definition (design 26). The checker asks it for
|
||||
every change carrying `renamed-from` or dropping one: a holder that does not serve it is refused by the tool runner
|
||||
naming the verb, and the checker then refuses `renamed-from` naming that; while a window is open, the same answer
|
||||
refuses dropping `renamed-from`, naming the nodes. A check that cannot ask fails, and says so. So no manifest uses
|
||||
`renamed-from` in the milestone that brings it, whose running mesh-config serves no `renames`; the first that does
|
||||
is its live case. The checker declares `seat:mesh-config.renames` in its `invokes`, so by the served-verb edge
|
||||
(ADR 0614 §2) its new build waits until a build of mesh-config that serves `renames` runs healthy; no cycle closes,
|
||||
since mesh-config waits only on the indexer's `indexer_config_readers`. The "not served" answer is then met only by
|
||||
the running checker in the milestone's own check, and after a revert. `config_renames` names, per rename, the nodes
|
||||
still holding the old name open.
|
||||
- **A refused move** (the old name holds nothing, or a secret) is mesh-config's finding, from which the condition keeper
|
||||
raises the condition, cleared once the value moved or `renamed-from` left the trunk's manifest. **Tools of
|
||||
mesh-config**: `config_renames` (what moved, from which name, when), `config_rename_again` (move one value by hand
|
||||
when a move was refused) and `config_rename_close` (above).
|
||||
|
||||
### A value no longer read (from 2026-11-03)
|
||||
|
||||
A config value or secret no manifest on any trunk reads any more is a **retiring value**. mesh-config learns it from
|
||||
the indexer's tool `indexer_config_readers`, which it asks on each of the indexer's `manifest-accepted` events; the
|
||||
indexer says `manifest-accepted` for every newly accepted manifest, its bus-state entries empty when it asks none (the
|
||||
bus module reads an empty one as "this module asks nothing now", design 25 § 12.2). A tool the indexer does not serve
|
||||
yet answers "not known", and nothing retires on it. It is marked retiring only once no node-engine reports applied a
|
||||
build that reads it, as the rename window is (§ above). mesh-config then emits `retiring` (family `config.events`,
|
||||
kept seven days) and asks the operator once, through the operator channel, whether to remove it, naming any node that
|
||||
still read it until lately; it is removed only on the operator's Approve. **mesh-config holds no secret**, so the
|
||||
retiring flow and its one ask stay mesh-config's, and on the Approve it removes a value itself and retires a secret
|
||||
through the seat `mesh-vault`'s new optional verb `retire` (one module's own secret, by node, module and name), carrying
|
||||
the operator's warrant, which the vault checks (ADR 0259 §6) before it removes anything. The vault's caller list for
|
||||
`retire`, a trusted config value in ADR 0363's form, names the holder of the seat `mesh-config` alone, checked against
|
||||
the caller the tool runner proved; the warrant is bound to the node, module and name it was asked for, so it retires
|
||||
nothing else. mesh-config then emits `retired` on `config.events` (module, name, node, the ask), and the vault emits
|
||||
`own-retired` (node, module, name, the ask's id, the caller it proved) beside its `rotated` and `own-replaced`, on the
|
||||
module events stream (ADR 0363) under the vault's own subjects, kept as that stream keeps every module's events. An ask unanswered for seven
|
||||
days is asked once more, and then written as a finding, cleared by an answer, a cancel, or the value read again; a
|
||||
sleeping node holding a retirement open for seven days is a finding naming it. `config_retiring` lists what is
|
||||
retiring, since when, and its ask, and asks it again or cancels it.
|
||||
|
||||
**Where mesh-config keeps it.** Its new state (`renames`, `retiring`, `findings`) and its stream `config.events` are
|
||||
declared in its old `state` and `streams` sections in this milestone, as the builder's and the checker's new state is
|
||||
(design 51 *The move*), so that neither mesh-config nor the deciding parts that ask it wait on the bus module's
|
||||
process, which is delivered in the same milestone. They move to bus state when each module next moves (ADR 0595 §11).
|
||||
`config-changed` stays on the shared events stream, unchanged, so its consumers do not move.
|
||||
|
||||
**How the rename and the retiring are checked**, as cases of mesh-config's first whole-module integration test (C20,
|
||||
C24 below), of the checker's and of the vault's existing whole-module tests: the checker's ask of `renames` meeting a
|
||||
holder that does not serve it refuses `renamed-from` naming the verb, one meeting an open window refuses dropping
|
||||
`renamed-from` naming the nodes, and a check that cannot ask fails saying so; `retire` with the warrant removes the
|
||||
secret and emits `retired` and `own-retired`, without it, with a warrant for another name, or from a caller not on the list it is
|
||||
refused; a waiter on the vault's `rotated` or `own-replaced` does not wake on `own-retired`; a node that was away,
|
||||
returning with a build that reads the new name, closes the rename window on its `manifest-reads-changed` event when
|
||||
it applied that build before the event (ADR 0618 §4), and on the next `delivered` when it applies it after; `config_rename_close` drops a node from a window and records the hand act; a window held seven days and a
|
||||
retirement held seven days each write their finding naming the node, cleared when it closes.
|
||||
|
||||
### The mesh delivery cases this design carries
|
||||
|
||||
Research 081 listed the mesh delivery cases; these are the ones it handed to a design, each with its acceptance test,
|
||||
each a case of its module's one whole-module integration test unless it says live.
|
||||
|
||||
| case | what holds | acceptance test |
|
||||
|---|---|---|
|
||||
| C6, a package series raised | users on `0.1` keep the newest `0.1`; a reviewed change moves a user | builder: a `0.2.0` publish moves no `0.1` user, its check takes the newest `0.1` too, a reviewed change naming `0.2` moves it, a user naming no series builds against the newest `0.1` |
|
||||
| C14, a provision's form gains a field | the provider serves it in a new version; the consumer reads it once rebuilt; an older consumer ignores it | mesh-sdk: a consumer built against the older form reads a provision with the new field |
|
||||
| C18, a new required config value with no default | the check names the nodes that lack it; the want waits; the operator is asked once | live: a module gaining such a value waits on each node lacking it, the operator is asked once naming the nodes, and once given the module is applied |
|
||||
| C20, a config value renamed | the value moves, keeps its trust, both names read until every node runs the new build | mesh-config: a renamed value is present under the new name with no operator input and its trust; the old name answers until the node-engines report the new build; dropping `renamed-from` early, or changing trust, is refused; a refused move writes its finding |
|
||||
| C22, a secret only a person can give | the want waits; the operator is asked once what to give and where | live: a module requiring such a secret waits, one ask names it and where, and once given the module starts |
|
||||
| C24, a config value or secret no longer read | it is retiring, the operator is asked, removed only on the Approve | mesh-config: with one node-engine still reporting a build that reads the value it is not marked retiring and no ask is sent; once that node reports the new build it is marked retiring, `retiring` is emitted and one ask reaches the operator; an ask unanswered seven days is asked once more, then written as a finding; the value stays until an Approve removes it |
|
||||
|
||||
## 9. Provisioning over the bus
|
||||
|
||||
Provisioning rides the bus, and the provider stops having an address.
|
||||
@@ -482,7 +656,7 @@ addresses will believe a class of bug is fixed when it is untouched.
|
||||
controller's address — recorded somewhere, at some moment, and wrong as soon as anything moved.
|
||||
Now it publishes to `mesh.seat.mesh-controller.report` and the bus routes it to whoever holds the
|
||||
seat. Nothing anywhere records where the controller is, so nothing can record it *wrongly*. The
|
||||
same is true of the builder, the catalogue, the telegram sender. This class is not mitigated; it
|
||||
same is true of the builder, the catalog, the telegram sender. This class is not mitigated; it
|
||||
is gone, because the information is no longer stored.
|
||||
|
||||
**Something that is not on the bus: the address stays, exactly as before.** A module that requires
|
||||
@@ -580,11 +754,11 @@ billing existing under that name.
|
||||
|
||||
## 12. How it is checked
|
||||
|
||||
- **A manifest holds no subject.** A catalogue test: no manifest contains a string matching the
|
||||
- **A manifest holds no subject.** A catalog test: no manifest contains a string matching the
|
||||
subject grammar. The rule is worthless if it is followed by convention.
|
||||
- **A preparation is given what the module is given.** A composition test: what the preparation
|
||||
entrypoint receives equals what the module's own code receives, asserted rather than written twice —
|
||||
which is the drift a hand-written step invites, three times over in the catalogue today.
|
||||
which is the drift a hand-written step invites, three times over in the catalog today.
|
||||
- **A convergence that changed nothing says nothing.** Two identical reports, one emitted fact: what is
|
||||
guarded against is a fact per minute per machine, which is a stream nobody reads.
|
||||
- **Permissions are exactly the three namespaces.** A composition test per module: the derived
|
||||
@@ -602,6 +776,45 @@ billing existing under that name.
|
||||
- **A module reaches only the state it declared.** The composer's test: an owner's runtime may write
|
||||
its buckets, a reader's may only read, and nothing else is granted; the runtime's test over a real
|
||||
bus: a name not issued and a reader's write are refused with the reason.
|
||||
- **Bus state is asked for once, in one form, and read by its owner alone** (ADR 0595 §8, ADR 0596). The manifest validator's
|
||||
unit tests: a `reads` in the new form, a local name used twice across storage types or in an old section and the
|
||||
new form, an unknown template and an option outside its bound are each refused, naming the field. Live: a moved
|
||||
module receives its binding after `created`; another module's account reading its entries is refused; a moved
|
||||
reader hears its owner's declared event and asks its owner's tool. The rest is [design 25](25-the-bus-on-nats.md)
|
||||
§ 12.7.
|
||||
- **State is current at once.** The runtime's test over a real bus: a watch hands the current values
|
||||
without deletions, then an end-of-current marker, then changes. Live: a machine assigned after a put
|
||||
reads it at start.
|
||||
|
||||
## Reviewed by architectural domain
|
||||
|
||||
The amendment of 2026-11-03 to ADR 0595, ADR 0596 and ADR 0597 (designs 25 § 12, 32 § 4 and 55 together) was
|
||||
reviewed by the four reviewers by architectural domain side by side (playbook 01), over three rounds, and the lead
|
||||
ruled on every finding. What was found, and what answers it:
|
||||
|
||||
| Domain | Found | Answered by |
|
||||
|---|---|---|
|
||||
| security | the author could be words the module chose; a person's dangerous tools had no role, mark or path to the process; the caller travelled as a plain field | design 25 § 12.3 (marks, the process's own tool subject, the identity seat's signed token), § 12.4 (the tool runner stamps the caller it proved; the move's authors marked imported) |
|
||||
| seams | the bus module asked for its own events in the new form, which it would have to create itself (ADR 0595 §10); a person's tool had no join to the process; a verdict was written twice with no order | design 25 § 12.3 (its events on its old `streams` section; the process's own tool subject); design 55 § 2.1 (the `review` operation first, `reviews` caught up from it) |
|
||||
| running qualities | overdue creation was undefined and raised by nobody; the database's rollback write lived in memory; a forward delivery after a rollback could miss acceptances and verdicts; no size estimate of the heads | design 25 § 12.2 (five minutes, the watchman, durable consumers, `check` at start); design 55 § 8.1 step 6 (`database.sequence`, the check at every start before the drop), § 2.1 (the heads' estimate) |
|
||||
| health | the bus module's health would have fired on a quiet mesh; conditions had no clearing; `database-behind` had no clearing | design 25 § 12.2 and § 12.3 (each condition with what clears it); design 55 § 8.1 step 6 |
|
||||
| events and logs | an event lost when the module stopped after `apply`; the bus module's events had no reader | design 55 § 1 (each event sent with its operation's sequence as id, the state `sent`); design 25 § 12.3 (`history` of one entry) |
|
||||
| testing | the bus module had no test that starts it whole; live tests would have filled the live bus; the budget was not counted | design 25 § 12.7 (the nats module's integration test, live drift only by a manifest change, the budget paragraph); design 55 § 10 |
|
||||
| upgrade | the build order lacked the grants part's binding and two undo steps | design 25 § 12.6 |
|
||||
| tools | the ledger's repairs had no tool for the next owner; the bus module's events had no read tool | design 25 § 12.5 (served for every ledger owner), § 12.3 |
|
||||
|
||||
**Left to the operator**, because each changes an accepted record: a margin between raising and clearing the
|
||||
condition `full` (ADR 0595 §6 raises and clears it at 75 percent, so an entry sitting there flaps), and an event of
|
||||
its own for `drop` and `recreate`. **A correction of fact** for ADR 0595 §4: the bus module deletes through `recreate`
|
||||
as well as `drop`. Each is noted on issue 1966.
|
||||
|
||||
The amendment of 2026-11-03 for research 081 (ADR 0606, ADR 0609; the package, rename, retiring and delivery-case
|
||||
sections of § 8) was reviewed with designs 51 and 26 by the four reviewers by architectural domain; the findings and
|
||||
what answers them are listed in design 51's *Reviewed by architectural domain*. For this design: a rename keeps a
|
||||
value's trust (security), `packages` is a section of its own, not `uses`, and mesh-config hears the status view's
|
||||
`delivered` events (seams), a value retires only once no node-engine runs a build reading it (security), and `renamed`
|
||||
and `retiring` have a family and an ask that is asked again (events).
|
||||
|
||||
The milestone's amendment (ADR 0612, ADR 0613: the seat verb `renames` the checker asks, the vault's `retire`,
|
||||
`manifest-accepted` for every manifest, mesh-config's new state in its old sections) awaits its own review by
|
||||
architectural domain, which rewrites this section.
|
||||
|
||||
@@ -135,7 +135,7 @@ by side until nothing is bound to the old one.
|
||||
*Amended 2026-10-07 (ADR 0246,
|
||||
[issue 298](../../04-ISSUES/298-a-new-seat-verb-deadlocks-across-two-repositories/00-report.md)):*
|
||||
**additive takes two steps when the seat and its holder are in different repositories.** That is the
|
||||
case for every mesh seat whose holder is a catalogue module. §3 makes every promised verb a condition of
|
||||
case for every mesh seat whose holder is a catalog module. §3 makes every promised verb a condition of
|
||||
holding, and a claim may name no verb the seat does not promise. So a verb added to the seat in the
|
||||
controller refuses today's holder, and a holder that serves it is refused by today's controller. Neither
|
||||
change can land first. An added verb therefore lands in three steps, each its own pull request:
|
||||
@@ -163,7 +163,7 @@ still a new version.
|
||||
of the subject table.
|
||||
- **An added verb lands in steps, and each step is checked (§7).** The controller's claim check refuses
|
||||
a claim that serves a verb the seat does not promise, and a holder that misses a required verb. It runs
|
||||
at registration, at handover, and in `module check`, which the catalogue's merge check runs over every
|
||||
at registration, at handover, and in `module check`, which the catalog's merge check runs over every
|
||||
manifest with the controller the mesh runs. So a step out of order fails the pull request that took
|
||||
it. That an optional verb stays optional when read back from the store is held by mesh-controller's
|
||||
`TestAnOptionalVerbStaysOptionalInASetReadFromTheStore` (#114), and that a seat's row gains a verb's
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
---
|
||||
layer: to-be
|
||||
design: 34
|
||||
milestone: every-event
|
||||
updated: 2026-11-11
|
||||
code: [mesh-sdk, mesh-delivery, mesh-tools, mesh-catalog modules/claude-code]
|
||||
decisions:
|
||||
- "ADR 0670"
|
||||
- "ADR 0671"
|
||||
- "ADR 0286"
|
||||
- "ADR 0367"
|
||||
- "ADR 0043"
|
||||
- "ADR 0259"
|
||||
- "ADR 0332"
|
||||
- "ADR 0363"
|
||||
- "ADR 0376"
|
||||
- "ADR 0489"
|
||||
- "ADR 0571"
|
||||
- "ADR 0578"
|
||||
- "ADR 0614"
|
||||
---
|
||||
|
||||
# Design 34, milestone every-event — the tool runner hears every event
|
||||
|
||||
**What it builds.** A session's `mesh_wait` waits on any event of any seat or module, by its name, with no list to
|
||||
keep: the tool runner's bus account is granted the event subjects of the whole mesh, every module's own stream
|
||||
included (ADR 0671 §1), and the list of families in the tool runner's manifest, the manifest validator's rule about it
|
||||
and the grants part's computation of subjects from it go (ADR 0670). The whole shape is in the design's main file, [§3b](../34-the-console.md#3b-waiting-on-the-bus); this
|
||||
part holds every line the milestone builds, cited as "design 34 every-event §n".
|
||||
|
||||
**Where.** Four repositories and hq, one delivery group: mesh-sdk (the shared event subjects; the field, type and
|
||||
membership entry that go), mesh-delivery (the grants part, seat `mesh-grants`; the manifest validator in the indexer),
|
||||
mesh-tools (the tool runner, module `node-tools`) and mesh-catalog (the claude-code module's Stop hook, §20), with hq
|
||||
for the playbook and agent lines (§24). Only these reference the list today: no other manifest carries the key
|
||||
`waits`. The milestone is built on each repository's main as it stands. Design 47's milestone G edits the grants
|
||||
part's account composition and the tool runner's manifest and manifest test too; its lead rebases it onto this
|
||||
milestone's heads when G's develop restarts.
|
||||
|
||||
**The approval mark on events.** *A marked event waits for the operator's warrant* (§32 to §37) writes the operator's
|
||||
answers of 2026-11-11: a wait respects the approval mark, and the gate that asks and checks it (design 46's milestone
|
||||
7, [`7-approval-mark.md`](../46-milestones/7-approval-mark.md)) is built in design 47's milestone G and delivered in one
|
||||
group with G and this milestone ("Build the gate in G"). So §25's grant of a restricted stream's subjects and the gate
|
||||
that holds its mark reach the machines together.
|
||||
|
||||
## The event subjects, in mesh-sdk
|
||||
|
||||
1. **One place names the mesh's event subjects.** mesh-sdk's package `buscompose` gains a file `events.go` with three
|
||||
exports, used by the grants part (§6) and the tool runner (§13), so neither spells the event namespace itself:
|
||||
- `EveryEvent`, the subjects the tool runner's account hears: `mesh.seat.*.event.>` and `mesh.mod.*.event.>`, in
|
||||
that order (ADR 0670 §1);
|
||||
- `EventSubjects(name)`, the subjects one event name lands on. A name is `<emitter>.<event>`, optionally followed
|
||||
by further tokens (`operator-channel.decided.claude-code`); each token matches `[A-Za-z0-9_-]+`. It answers four
|
||||
subjects: `mesh.seat.<emitter>.event.<rest>`, the same followed by `.>`, `mesh.mod.<emitter>.event.<rest>`, and
|
||||
the same followed by `.>`, where `<rest>` is every token after the first; given a namespace (`seat` or `module`,
|
||||
§13) it answers only that namespace's two. It refuses, with an error naming the rule, a name of fewer than two
|
||||
tokens, an empty token, or a token outside that form (so `*` and `>` are refused);
|
||||
- `EventName(subject)`, the inverse for a heard subject: for `mesh.seat.<emitter>.event.<rest>` or
|
||||
`mesh.mod.<emitter>.event.<rest>` it answers the name `<emitter>.<rest>` and its namespace; any other subject is
|
||||
answered as not an event.
|
||||
|
||||
Why four subjects: a name does not say whether its emitter is a seat or a module, and both namespaces are heard
|
||||
(§6); and a seat's event that names its asker or kind after the event (ADR 0259 §3), or a module's that names its
|
||||
module and branch, is heard by the event's name alone, the `filter` narrowing it by body. How it is checked: one
|
||||
table-driven unit test of the three, in `events_test.go`, one of whose rows holds that every subject
|
||||
`EventSubjects` answers for its cases is covered by a subject of `EveryEvent`.
|
||||
2. **The manifest loses `waits`.** `catalogue.Manifest.Waits` and its comment go from `go/catalogue/manifest.go`;
|
||||
the comment on `ToolRunner` loses "and the only module that may declare `waits`". `go/catalogue/waits_test.go` is
|
||||
deleted, and one row is added to the nearest table over raw manifests, `go/catalogue/invokes_test.go`'s
|
||||
`TestAManifestMaySayWhatItInvokes` (Q-T): a manifest
|
||||
`{"module":"node-tools","version":"1","tool-runner":true,"waits":["a.b"]}` decodes with `UnknownField()` naming
|
||||
`waits` (ADR 0670 §2). The decoder itself is unchanged: a stored manifest that still carries `waits` is read
|
||||
without it (ADR 0578 §3).
|
||||
3. **The baseline of manifest reads stays as generated.** `go/catalogue/manifest_reads_baseline.txt` keeps
|
||||
`word:waits`: it is the contract of the engines built before any stated what it reads (ADR 0614 §4), which did read
|
||||
`waits`. In `own_manifest_test.go` the list of baseline entries this contract no longer reads (today the two
|
||||
renamed seats) gains `word:waits`, its comment naming ADR 0670 §2 as the record that retired the word.
|
||||
4. **The wait types go.** `go/buscompose/waits.go` (the type `Wait`) is deleted and `Users.Waits` goes from
|
||||
`go/buscompose/users.go`; `Membership.Waits` and the type `WaitIssued` go from `go/membership/membership.go`
|
||||
(ADR 0670 §2). Nothing replaces them: no membership names what its account hears.
|
||||
|
||||
## The grant, in the grants part (seat `mesh-grants`)
|
||||
|
||||
5. **What the grants part computed from the list goes.** `internal/buscompose/waits.go` is deleted whole (`Wait`,
|
||||
`WaitSubject`, `waitGrants`, `waitsIssuedFor`); `Declared.Waits` goes from `internal/buscompose/users.go`, and with
|
||||
it the term `len(d.Waits) > 0` of `runnerServes`; `m.Waits = waitsIssuedFor(d)` goes from `membership.go`, and the
|
||||
alias `WaitIssued` from `sdk_membership.go`; `waitsOf` and the field `Waits:` of `declaredFor` go from
|
||||
`internal/grants/records.go` (ADR 0670 §2). `safeSubject` in `nats.go` stays: the bus-state grants use it.
|
||||
6. **The tool runner's runtime hears every event subject.** In `internal/buscompose/nats.go`, where a runtime's
|
||||
permissions are composed from the modules it carries, the call of `waitGrants` is replaced by: when one of the
|
||||
carried modules says `tool-runner` (`Declared.ToolRunner`), `buscompose.EveryEvent` (§1) is appended to the
|
||||
runtime account's subscribe list. Nothing else is added for it: no publish, no durable consumer, no queue, no other
|
||||
subject; a runtime carrying no tool runner gets neither subject (ADR 0670 §1, §3; ADR 0043 §2, §3, §5, §8 as ADR
|
||||
0670 §3 now reads them). The grants part writes them into the bus's user list as every grant (ADR 0571 §9); they
|
||||
are read with `mesh-grants.grants_user_list`, and composed at once with `mesh-grants.grants_compose_now`.
|
||||
7. **The tool runner's membership names no events.** The grants part's first composition after its delivery rewrites
|
||||
every tool runner's membership in its state `memberships` (key `<node>.node-tools`, ADR 0571 §10) without the key
|
||||
`waits`, since the field is gone (§4); every other part of it is unchanged. It is read with the grants part's
|
||||
reading of memberships and rewritten at once with `mesh-grants.grants_compose_now`.
|
||||
8. **A composition test holds the grant.** In `internal/buscompose/buscompose_test.go`, one table-driven test composes
|
||||
a runtime carrying the tool runner and one other module, and a runtime carrying the other module alone, and holds:
|
||||
the first's subscribe list contains `mesh.seat.*.event.>` and `mesh.mod.*.event.>`; no subscribe subject of either
|
||||
is wider than those (none of `>`, `mesh.>`, `mesh.*.>`, `mesh.seat.>`, `mesh.mod.>`, nor any `$JS.` subject beyond
|
||||
what the carried modules' own consumes give); the first's publish list holds no subject of another module's or
|
||||
seat's events; the first derives no durable consumer beyond its carried modules' `consumes`; and the second has
|
||||
neither subject (ADR 0670 §1, §3).
|
||||
9. **The real bus holds it too.** The grants part's existing real-bus test (`cmd/mesh-grants/composed_integration_test.go`)
|
||||
gains cases, no new test: the tool runner's account, as composed, subscribes
|
||||
`mesh.mod.mesh-delivery-view.event.delivered` and receives an event published there; it is refused a publish on
|
||||
that subject; it is refused a subscription on `mesh.seat.channel.accept.show.<kind>` and on
|
||||
`mesh.seat.intake.proof.code.<kind>` (an accept and a proof are never events, ADR 0670 §1); and another module's
|
||||
account is refused a subscription on an event it does not consume (ADR 0670 §1, §3).
|
||||
|
||||
## The manifest validator, in the indexer
|
||||
|
||||
10. **The rule about the list goes.** `waitProblems` and its call in `judge/rules.go` are deleted, and with them the
|
||||
regular expression `waitToken` where nothing else uses it; the table rows that judged `waits`
|
||||
(`judge/rules_test.go`, the `waits` cases) are deleted, and one row is added: a manifest that carries `waits` is
|
||||
refused by the manifest validator as the checker runs it (`Shelf`), naming the unknown field `waits`, and set aside
|
||||
to wait as the indexer runs it (`ShelfWaiting`), as any field it does not know (ADR 0578 §1); the tool runner's
|
||||
manifest without it is accepted (ADR 0670 §2). Only revisions taken in after the indexer's delivery are judged so:
|
||||
a manifest the validator accepted before keeps its verdict (ADR 0578 §1).
|
||||
|
||||
## The tool runner (module `node-tools`)
|
||||
|
||||
11. **Its manifest's changes, every one.** `node-tools/module.json` drops the key `waits` and its five entries (ADR
|
||||
0670 §2); gains `operator-channel` in `uses`, for the asks of marked waits (§35; design 46 7-approval-mark §7); gains
|
||||
in its old `state` section the per-machine state `approvals` of design 46 7-approval-mark §13 (the tool runner is a
|
||||
foundation part, ADR 0619 §2, §3); and gains the stream `node-tools-findings` and
|
||||
the `consumes` of `mesh-conditions.findings-asked` that design 47 G §387 writes. Its `version` is `0.1`, as design 47 G §368
|
||||
and §624 have every manifest's. Nothing else in it changes.
|
||||
12. **The publication time is kept.** `bus.Heard` gains `Published`, read from the event's header `x-time`, which
|
||||
mesh-sdk's emit sets (`go/process/emit.go`) and the status view's own emit too; `HearFirst` reads it from the
|
||||
message's headers, and leaves it empty for an event that carries none.
|
||||
13. **`mesh_wait` takes any event name.** `console.Surface.waitAnswer` (`internal/console/wait.go`) no longer reads the
|
||||
membership. Each name of `subjects` becomes its subjects through `buscompose.EventSubjects` (§1); a refused name is
|
||||
answered as a failure quoting that error. An optional argument `from`, `seat` or `module`, narrows every name to
|
||||
that namespace, for an emitter whose name is both a seat's and a module's. A call naming more than 16 names is
|
||||
refused, saying the bound (each name is four subscriptions for the call; 16 names is 64, far above what a watcher
|
||||
names). The same subject named twice is subscribed once. The map from subject to name goes: the heard event is
|
||||
named by `buscompose.EventName` of the subject it arrived on. The timeout, the filter and the timed-out answer are
|
||||
unchanged (ADR 0367 §1). The refusal of names outside a list, and its words naming the manifest, go (ADR 0670 §2).
|
||||
14. **A misspelt name is refused, not waited on for ever.** Before it subscribes, `waitAnswer` refuses a name whose
|
||||
emitter is neither a seat nor a module of the catalog half the tool runner holds from G (its first view by
|
||||
`mesh-catalog.revision`, followed on `catalog-revision`; design 47 G §196), naming that source and its revision.
|
||||
A name whose emitter is known but whose event no seat definition and no module's entry in the catalog half
|
||||
declares (its `emits` and its streams' events, design 46 7-approval-mark §5) is waited on,
|
||||
and its timed-out answer says that no declaration names that event. While the catalog half is not yet read (the
|
||||
first views' state, `mesh_first_views`), names are not checked, and the answer says `unchecked` with why; a
|
||||
marked wait is then held as §34 says.
|
||||
15. **The answer names the heard event.** A heard event is answered as today, `heard` with `subject`, `body` and
|
||||
`at`, where `subject` is now the heard event's whole name (`mesh-delivery-view.delivered`,
|
||||
`operator-channel.decided.claude-code`), with two new fields: `from`, `seat` or `module`, the namespace it
|
||||
arrived on, and `published`, the event's own time (§12), absent when it carries none. The `read` line stays (an
|
||||
event only wakes you, ADR 0367 §4); for a name beginning `operator-channel.decided` it says instead that the
|
||||
operator's answer is read only with `claude_code_answer`, never from the event's body (ADR 0670 §5; ADR 0332 §5,
|
||||
§6).
|
||||
16. **A refused subscription is said, and a wait is never a hot loop.** `HearFirst` subscribes every subject of the
|
||||
call; a subscription the bus refuses is noted, and the others go on waiting. When every subscription of the call is
|
||||
refused, it waits out the call's timeout (or the caller's going) and then answers the refusal, so a watcher that
|
||||
loops on it asks once a timeout, never at once again. Its refusal words become: "the bus refused this account a
|
||||
subscription on <subjects>: the grants part grants this machine's tool runner every event subject (novox/hq ADR
|
||||
0670 §1), so its grant is not composed yet; read the state from its owner meanwhile"; no words name a wait grant or
|
||||
ADR 0367 §2. A heard event's answer and a timed-out answer list under `refused` any subject the bus refused while
|
||||
others stood.
|
||||
17. **The tool's description says what it hears.** `waitTool`'s description of `subjects` says: one or more event
|
||||
names, `<seat or module>.<event>`, such as `mesh-delivery-view.delivered`; an event whose subject goes on after
|
||||
the name (an asker, a kind, a module) is heard by the name too; the filter narrows by the body's fields; `from`
|
||||
narrows to a seat's or a module's. Its own description keeps "an event only wakes you" and adds that an operator's
|
||||
answer is read only with `claude_code_answer`. The words "only those the mesh granted this machine's tool runner"
|
||||
go.
|
||||
18. **The hearing serves `mesh_wait` alone.** `HearFirst` has one caller, `waitAnswer`. The modules the runtime carries
|
||||
share its account, so the bus no longer bounds which events their code could hear; the runtime still gives a
|
||||
carried module's code only what its membership names, and this is accepted under ADR 0670 §1, every session
|
||||
already hearing every event. How it is checked: the code review of every change to the tool runner reads
|
||||
`HearFirst`'s callers.
|
||||
19. **What goes from the tool runner's code, and its tests.** The type `bus.WaitIssued` (`internal/bus/wait.go`) and
|
||||
`Membership.Waits` (`internal/bus/bus.go`) go. In `console/manifest_waits_test.go` the test of the five families
|
||||
becomes a test that the manifest has no key `waits`, its comment citing ADR 0670 §2; its tests of what the tool
|
||||
runner consumes and of the runner's own secrets stay. In `console/wait_test.go`: `waitingRuntime` issues no waits,
|
||||
and the answering test waits on `mesh-delivery-view.delivered` and on `operator-channel.decided` with no membership
|
||||
naming either, its rows holding `subject`, `from`, `published` and `read` (a `decided.<asker>` event heard by
|
||||
`operator-channel.decided`); `TestMeshWaitRefusesAnEventOutsideTheGrantedFamilies` becomes the table of refusals:
|
||||
a name with `*`, with `>`, of one token, 17 names, an emitter no source knows (§14), and a bus-refused subscription
|
||||
answered after the timeout with §16's words; `TestMeshWaitWithNoFamiliesIssuedWaitsOnNothing` is deleted. In
|
||||
`bus/wait_test.go`, `TestTheBusRefusesAWaitOutsideTheGrant` stays as §16's refusal test, with a timeout of a
|
||||
fraction of a second, its comment citing ADR 0670 §1. The manifest test also holds each change of §11: no `waits`,
|
||||
`operator-channel` in `uses`, the state `approvals` in the old `state` section, per machine with its time to live,
|
||||
and none in `requires`, and the finding stream. The
|
||||
tool runner ends milestone G with no more tests than it starts with (design 47 G §419, §626): these tests replace
|
||||
existing ones, and every test deleted counts.
|
||||
|
||||
## The claude-code module's Stop hook
|
||||
|
||||
20. **The hook no longer doubts the grant.** In mesh-catalog `modules/claude-code/cmd/claude-code/stop.go`,
|
||||
`watcherAdvice` says for a wait with an event: "mesh_wait on <event> and reread <read> after every answer, or start
|
||||
a watcher that does", with no condition on the tool runner's grant; its comment cites ADR 0670 §2 beside ADR 0489
|
||||
§3. Its row in `stop_test.go` holds the new words.
|
||||
|
||||
## Events carry no secret
|
||||
|
||||
21. **The prepare lists every event.** The milestone file's prepare (playbook 07, step 0) gains a section *Events and
|
||||
their fields*: every event each module declares (its `emits` and its streams' events) and each seat's definition
|
||||
declares, read from the module catalog: the catalog revision of the change's branch (`mesh-catalog.revision`),
|
||||
which carries the seat definitions too, and nothing else, no manifest and no bus read, so the events G and design
|
||||
46's milestone 7 add are listed (the finding streams of design 47 G §383 among them), each a holder's event with
|
||||
its subject form; and every event mesh-sdk publishes on a module's behalf (the announcement's `states` on
|
||||
`mesh.mod.<module>.event.states`, found by the callers of its subject builders); each with the fields of its body
|
||||
as the emitting code builds it, by file and line (ADR 0670 §4). *Why:* the operator, 2026-11-11: "only the indexer
|
||||
uses the manifest to build the module catalog".
|
||||
22. **The code review confirms none carries a secret, before the grant is delivered.** The milestone's code review
|
||||
(playbook 07, step 2) reads that list again at the group's heads, and confirms that no event's body carries a
|
||||
credential, a token that grants an act, code, or a message marked secret; an intake `choice`'s one-use token is not
|
||||
such a token (ADR 0259 §3, §5). The list is the module catalog's (§21); the review reads no bus. An event found
|
||||
carrying a secret holds the milestone's delivery and goes to the operator as a design question
|
||||
(ADR 0670 §4).
|
||||
23. **The stream events are read too.** §21's list also names every event kept in a module's own stream
|
||||
(`mesh.stream.<module>.event.<event>`, ADR 0363 §2), the seat holders' events published there among them, and
|
||||
whether its stream is restricted (its declaration carries a `grant`, ADR 0363 §4). The mail stream's events are
|
||||
named in it as heard with their content: a mail's own text, a sign-in link or a code someone sent by mail among
|
||||
it, is the operator's mail and not a secret in §22's sense, which is a credential, token or code the mesh itself
|
||||
issues or keeps (ADR 0671 §3, which narrows ADR 0670 §4).
|
||||
24. **From then on every change is asked the same.** In this milestone's hq pull request: playbook 07's review step
|
||||
gains the question, for any change that adds an event or a field to one, "does it carry a secret?"; and the watcher
|
||||
lines that describe the list (`.claude/agents/mesh-watcher.md`, playbook 07's *Following and reporting*: "the
|
||||
families a watcher may wait on are those the mesh granted the tool runner") say instead that `mesh_wait` hears any
|
||||
event by name (ADR 0670 §2, §4).
|
||||
|
||||
## Stream events
|
||||
|
||||
*By ADR 0671: the operator chose every module stream, mail too, on 2026-11-11.*
|
||||
|
||||
25. **The tool runner also hears every module stream, a restricted one included.** `buscompose.EveryEvent` (§1)
|
||||
gains a third subject, `mesh.stream.*.event.>`, after the two of §1, and the grants part's composition (§6) appends
|
||||
it with them. Nothing else is added for it: no durable consumer, no grant to acknowledge, no membership word, no
|
||||
publish, and no subscribe deny of a restricted stream (ADR 0671 §1). A restricted stream's grant value (ADR 0363
|
||||
§4) decides who consumes it, not who hears it, so changing or withdrawing that value changes nothing in the tool
|
||||
runner's grant (ADR 0671 §2); a module that consumes a restricted stream is composed as today (ADR 0376 §2 as ADR
|
||||
0671 §2 reads it). The bus does not hold the approval mark on a restricted stream; the tool runner's own gate does,
|
||||
before it subscribes for a wait (§32 to §37; the operator, 2026-11-11), and that gate is delivered in the same
|
||||
group as this grant (design 46 7-approval-mark §23). *Why:* ADR 0286 §8 has the bus hold a mark, and the bus
|
||||
cannot grant a subscription for one approved wait alone, so the operator chose the tool runner's gate.
|
||||
26. **A name hears its stream form.** `EventSubjects` (§1) answers six subjects, adding
|
||||
`mesh.stream.<emitter>.event.<rest>` and the same followed by `.>`; `from` (§13) takes `stream` too; `EventName`
|
||||
answers a stream subject with namespace `stream`. A seat's holder event kept in its holder's stream is named by its
|
||||
holder module (`mesh-delivery-checker.verdict`). The bound of §13 stays 16 names (96 subscriptions).
|
||||
27. **The tests hold it.** §8's test holds exactly the three subjects for the runtime carrying the tool runner, beside
|
||||
a fixture module that declares a restricted stream, and that no durable consumer and no subject to acknowledge on
|
||||
that stream are derived for the tool runner's runtime (ADR 0671 §1). §9's real-bus test gains cases: the tool
|
||||
runner's account, as composed, receives an event published on a fixture module's stream, and one published on a
|
||||
fixture restricted stream whose grant value has no entry for the tool runner, and still receives one after that
|
||||
value is changed and the accounts are composed again; it is refused a durable consumer and an acknowledgement on
|
||||
that restricted stream; and a module whose `consumes` names the restricted stream with no entry in its grant value
|
||||
gets no durable consumer and no read grant on it (ADR 0671 §1, §2).
|
||||
|
||||
## Delivery
|
||||
|
||||
28. **In G's delivery group, no order named beyond the module graph's.** The members are pushed and checked in design
|
||||
47 milestone G's group, with design 46's milestone 7, whose edges (design 46 7-approval-mark §23) order the gate
|
||||
before the seat definition that marks `execute`; this milestone adds no order of its own between its members, and no
|
||||
transitional field is kept (the operator, 2026-11-03: a change is designed as
|
||||
it should be, its delivery is the delivery system's): a part built with the new mesh-sdk reads a stored manifest of
|
||||
the tool runner that still carries `waits` without it (ADR 0578 §3), and a node-engine is given a want only for a
|
||||
manifest whose words it reads (ADR 0614 §4).
|
||||
29. **What `mesh_wait` answers between the deliveries.** On a node whose tool runner is new and whose grant is not yet
|
||||
composed, the subjects the old grant names are still heard and the rest are listed under `refused` (§16); on a
|
||||
node whose tool runner is older and whose membership the new grants part rewrote, the old runner answers at once
|
||||
that it was issued no event families. Both are loud. A watcher reads its owner after every answer (ADR 0367 §4) and
|
||||
ends at a call failed twice (its definition), so neither becomes a loop; a laptop asleep through the window gets
|
||||
its new tool runner when it wakes, and its watchers read their owners meanwhile in the same way.
|
||||
30. **Going back is going forward** (design 46 7-approval-mark §24 for the gate's part). A fault met after the delivery is fixed by a new change delivered the same way.
|
||||
Reverting the group instead reverts every member, the indexer first, since the manifest validator delivered here
|
||||
sets aside the old tool runner's manifest carrying `waits` (§10); until the revert reaches a node, `mesh_wait`
|
||||
there answers as §29 says.
|
||||
|
||||
## Nothing built ahead of the group
|
||||
|
||||
31. **No tool is built ahead of the group.** The extension of the nats module's `nats_streams` this line held, for
|
||||
§22's bus read, is not built: the secrets review reads the module catalog alone (§21, §22). *Why:* the operator,
|
||||
2026-11-11: "only the indexer uses the manifest to build the module catalog". *How it is checked:* the milestone's
|
||||
prepare names no member outside the group.
|
||||
|
||||
## A marked event waits for the operator's warrant
|
||||
|
||||
*By the operator's answers of 2026-11-11: "can it also respect any 'requires approval' config on the topics?", and
|
||||
"Build the gate in G".* These lines use the gate design 46 7-approval-mark builds for a marked call (its §8 to §19),
|
||||
delivered in the same group. *Why they depart from the records, said once for the section:* ADR 0286 §1 marks tools
|
||||
and verbs, and the operator extended the mark to a module's events and streams; ADR 0671 §1 and §3 have every session
|
||||
hear every module stream, mail included, with no exception, and the operator narrowed that to a wait the operator
|
||||
approves, for a marked event or a restricted stream.
|
||||
|
||||
32. **A module marks an event, and a restricted stream is marked whole.** A module's manifest `needs-approval` (ADR
|
||||
0286 §1) may also name one of its own events, by the name its `emits` or one of its `streams`' `events` gives it,
|
||||
with `*` and `**` as there. Every stream a module declares with a `grant` (ADR 0363 §4; `ModuleStream.Restricted`)
|
||||
is marked whole, every event it carries, with no entry in `needs-approval`; a stream without a `grant` is not, a
|
||||
work-queue one included. A seat's definition marks no event. The indexer's manifest validator refuses, as the
|
||||
checker runs it, a manifest whose `needs-approval` names an event that neither its `emits` nor its streams'
|
||||
`events` carry, naming the event, as ADR 0286 §4 refuses a mark on a tool the module does not serve (design 46
|
||||
7-approval-mark §4's table gains the row). A module that marks only events needs neither `operator-channel` in
|
||||
`uses` nor `approval-mark/1`: the tool runner asks for the wait, not the module.
|
||||
33. **The tool runner reads the marks from the catalog half on the bus.** The tool runner derives each module's marked
|
||||
subjects from the catalog half it holds (§14; the catalog graph entry carries `needs-approval` and each stream's
|
||||
`grant`, design 46 7-approval-mark §5): for a marked event `<event>` of module `<module>`,
|
||||
`mesh.mod.<module>.event.<event>` and `mesh.stream.<module>.event.<event>`, each event pattern as the bus spells it
|
||||
(`WirePattern`); for a stream with a `grant`, every subject of `ModuleStream.Subjects()`; and each of these also
|
||||
followed by `.>`, since an event may name its node or asker after its name (design 47 G §383) and
|
||||
`ModuleStream.Subjects()` answers only the patterns the stream stores. It derives them again on every revision it
|
||||
hears, and no module's name is written in its code; it is design 46 7-approval-mark §5's derivation, extended to
|
||||
events, following the running version as that line says. A mark added while a wait waits applies from the next
|
||||
call, which that call's answer says. *Why:*
|
||||
ADR 0286 §5 had the mark issued in a membership; no controller issues one now, a membership names no event (ADR
|
||||
0670 §2), and the catalog half, which the indexer alone builds from the manifests, keeps the mark out of any bundle
|
||||
as §5 required; from G the catalog half is where the tool runner reads which modules exist (design 47 G §196), and
|
||||
no part but the indexer reads a manifest (the operator, 2026-11-11).
|
||||
34. **Which wait is marked.** A wait is marked when any subject it subscribes (§13, §26, after `from`) overlaps a
|
||||
marked subject of §33, that is, some subject matches both, a wildcard on either side. mesh-sdk's `events.go` (§1)
|
||||
gains `Overlaps(a, b)` for it. Before the catalog half is read (`mesh_first_views`), a wait with any subject
|
||||
outside `mesh.seat.` waits, within its own timeout, for the first view and is then judged; nothing is asked while
|
||||
the marks are unread, and a wait still unjudged when its timeout ends is refused then, saying the marks are not
|
||||
yet read, since a mark that cannot be read runs nothing (ADR 0286 §40; design 46 7-approval-mark §19). A wait
|
||||
`from: seat` is never marked.
|
||||
35. **A marked wait runs only on the operator's warrant for that one wait.** The call is an act as design 46
|
||||
7-approval-mark §8 makes one: the address `mesh_wait`, the node, the module `node-tools` and the tool `mesh_wait`,
|
||||
its build (the tool runner's own fingerprint, read from its `MESH_FINGERPRINT` at its start, design 47 G §205 and
|
||||
§625; while it is unknown, a marked wait is refused, saying so), and every
|
||||
argument given (`subjects`, `from`, `filter`, `timeout`), so the warrant binds the exact names, the filter and the
|
||||
bound. The tool runner refuses, asks, judges and claims it as that part's §9, §10, §11 and §13 to §17 do for a
|
||||
marked tool, asking on the seat `operator-channel` under its own module's name, `node-tools`, whose manifest gains
|
||||
`operator-channel` in `uses` for it (that part's §7). The ask's headline is "Wait on <names> on <node>?"
|
||||
(shortened as that part's §10 says), and its explanation names every event name one per line, the filter, and the
|
||||
bound as the timeout in minutes, then the act's fingerprint. On a warrant it accepts, it subscribes, and the wait's
|
||||
timeout runs from then. One warrant is one wait (that part's §15): a watcher looping on a marked event is approved
|
||||
call by call. Its answer carries the line naming the approval and the warrant is recorded through
|
||||
`mesh-hand-acts.warranted` (that part's §16), and `mesh_approvals` (that part's §18) lists the waits asked beside
|
||||
the calls. *Why:* the operator, 2026-11-11: a marked wait runs only on the operator's warrant for that one wait,
|
||||
shown with the exact names it waits on and its bound.
|
||||
36. **A refused marked wait is said, and never a hot loop.** A marked wait with no warrant the tool runner accepts
|
||||
subscribes to nothing. Its first call publishes the ask at once; every such call answers, as an error, design 46
|
||||
7-approval-mark §12's or §17's words with "this wait" for "the call", when its timeout ends or its caller goes, as
|
||||
§16 answers a refused subscription, or as soon as the router's record of its ask says it was chosen: the wait hears
|
||||
the warrant event `operator-channel.decided.node-tools` for its ask's id and then reads the record, an event only
|
||||
waking it, and answers at once, so the same call made again runs within the warrant's 30 minutes. *Why:* ADR 0286
|
||||
§14 answers within 5 seconds, which a watcher's loop would turn into a call every few seconds for the ask's hour;
|
||||
ending at the choice keeps a long wait from spending the warrant's window. An unmarked wait is heard as §13 to §16
|
||||
say, with no ask. Every machine's marked waits ask under the one asker `node-tools`, so they share its open asks
|
||||
and the router's holding back of the oldest beyond three (ADR 0666 §12, as G builds it), and the wait reads the
|
||||
router's record through `Client.Where` under `node-tools`.
|
||||
37. **The tests hold it.** Unit, as the budget counts them (`meshtest` runs its bus in the test's process): §19's table
|
||||
of refusals in `console/wait_test.go` gains rows, no test function: a wait on a fixture module's marked event, and
|
||||
one on a fixture stream with a `grant`, each with no warrant, subscribes nothing, asks once and answers design 46
|
||||
7-approval-mark §12's words after its timeout; a wait with the most timeout a wait takes (ten minutes, ADR 0367
|
||||
§1), its ask chosen at its second minute, answers at the choice, and the same call made then runs; the same wait
|
||||
with a warrant bound to its exact names, filter and timeout subscribes and answers the event published after it,
|
||||
so the restricted stream's event is heard only after the warrant; with a warrant for other names, another filter
|
||||
or another timeout it is refused; the same names `from: seat` ask nothing; a wait named `<module>.<event>.<node>`
|
||||
under a marked `<module>.<event>` is marked; and before the catalog half is read, a marked wait asks nothing, is
|
||||
judged when the first view comes within its timeout, and is refused at its timeout when it does not. A marked wait while
|
||||
the tool runner's own fingerprint is unknown is refused, saying so. The warrant is
|
||||
written on the test bus as the router's record. `Overlaps` gains rows in §1's table (`mesh.stream.mail.event.mail.>` overlaps
|
||||
`mesh.stream.mail.event.mail.*.message`; `mesh.mod.mail.event.x` does not overlap `mesh.stream.mail.event.x`), and
|
||||
§32's refusal a row in the indexer's `judge/rules_test.go`.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- **A watcher wakes on an event no manifest names** (the operator's acceptance; live, read by tool call): at the first
|
||||
delivery after every node holds the new tool runner, a watcher's `mesh_wait` on `mesh-delivery-view.delivered`
|
||||
answers the status view's `delivered`, its `at` less than a second after its `published`, with no rereading on a
|
||||
timer; the tool runner (`node-tools`) is delivered to every node, and the grants part and the indexer to theirs,
|
||||
read with `mesh-delivery.show` and each node's `node-engine.composition` (§6, §13, §15).
|
||||
- **The grant is exactly what the record names** (unit, and integration on a real bus): the composition test of §8
|
||||
and the real-bus cases of §9 pass.
|
||||
- **The list is gone** (unit): the manifest validator as the checker runs it refuses a manifest carrying `waits`, the
|
||||
indexer sets it aside, and the tool runner's manifest is accepted without it (§2, §10, §11).
|
||||
- **`mesh_wait` refuses loudly and never loops** (unit): §19's table of refusals passes.
|
||||
- **No event carries a secret** (the code review): the milestone file's *Events and their fields* is complete and the
|
||||
code review's confirmation at the group's heads is written in it before the grants part is delivered (§21, §22).
|
||||
- **An answer is never taken from an event** (the code review, and unit): in the members nothing reads an operator's
|
||||
answer from a `decided` event's body, and `mesh_wait`'s answer for one says to read it with `claude_code_answer`
|
||||
(§15).
|
||||
- **A stream event wakes a watcher** (live, read by tool call): a watcher's `mesh_wait` on
|
||||
`mesh-delivery-checker.verdict` wakes on the checker's next verdict (§25, §26; ADR 0671 §1).
|
||||
- **The grant does not follow a restricted stream's grant value** (integration on a real bus): §27's cases pass
|
||||
(ADR 0671 §2).
|
||||
- **A marked wait runs only on the operator's warrant** (unit): §37's rows pass.
|
||||
- **A wait on mail, as the operator sees it** (live, read by tool call and on the phone): a session's `mesh_wait` on
|
||||
`mesh-mail.mail`, the mail module's restricted stream, asks "Wait on mesh-mail.mail on <node>?" on the phone with
|
||||
its bound, and hears nothing before; it ends refused when its timeout ends or as soon as the operator chooses; the
|
||||
same call made after the Approve hears the next mail, and the next call asks again (§32 to §36).
|
||||
@@ -1,11 +1,25 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: [mesh-catalog, mesh-tools, mesh-controller]
|
||||
updated: 2026-10-03
|
||||
status: in-progress
|
||||
code: [mesh-tools, mesh-host, mesh-controller, mesh-sdk, mesh-delivery, mesh-catalog modules/claude-code]
|
||||
updated: 2026-11-11
|
||||
decisions:
|
||||
- "ADR 0670"
|
||||
- "ADR 0671"
|
||||
- "ADR 0286"
|
||||
- "ADR 0043"
|
||||
- "ADR 0259"
|
||||
- "ADR 0332"
|
||||
- "ADR 0571"
|
||||
- "ADR 0363"
|
||||
- "ADR 0376"
|
||||
- "ADR 0489"
|
||||
- "ADR 0578"
|
||||
- "ADR 0614"
|
||||
- "ADR 0197"
|
||||
- "ADR 0195"
|
||||
- "ADR 0175"
|
||||
- "ADR 0367"
|
||||
- "ADR 0160"
|
||||
- "ADR 0159"
|
||||
- "ADR 0152"
|
||||
@@ -26,7 +40,7 @@ it put it there (ADR 0152).
|
||||
|
||||
## 1. What it is
|
||||
|
||||
A module, `mesh-console`, in the catalogue. Its image is the tool runner's own — the client that
|
||||
A module, `mesh-console`, in the catalog. Its image is the tool runner's own — the client that
|
||||
already speaks the bus as a command line and as an MCP server — started in a mode that reads the
|
||||
module's credential and listens on loopback. It has no state, no provision, no seat. What it needs is
|
||||
the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`,
|
||||
@@ -40,7 +54,7 @@ consumer is the operator's agent, [36 — The operator's agent on a machine](36-
|
||||
a machine without the mesh MCP server refuses such a module by name. Nothing else above changes: no seat, no
|
||||
state, no tools of its own.
|
||||
|
||||
Its manifest says three things nothing else in the catalogue says together:
|
||||
Its manifest says three things nothing else in the catalog says together:
|
||||
|
||||
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
|
||||
- `listens` on a port `from: machine` — the filter opens nothing for it, because loopback is not outside;
|
||||
@@ -72,7 +86,7 @@ its own under that module's name, `mesh.mod.<module>.tool.tools`, answering the
|
||||
descriptions and argument schemas — the definitions from the code that answers them, and from nowhere
|
||||
else. A module may not name a tool of its own `tools`; the runtime refuses the collision at load.
|
||||
|
||||
**The mesh MCP server asks the catalogue which modules the mesh holds, then asks each.** `catalog_modules`
|
||||
**The mesh MCP server asks the catalog which modules the mesh holds, then asks each.** `catalog_modules`
|
||||
answers the roster; one `tools` request per module, in parallel, answers the list. The bus refuses at
|
||||
once a request nothing serves, so a module that is not running costs nothing and is named in the answer
|
||||
as not answering, rather than silently absent — *silence and success must never look alike*. The list is
|
||||
@@ -104,7 +118,7 @@ answers is reached through them by its address: `<seat>.<verb>` for a seat held
|
||||
`<node>/<seat>.<verb>` for one held per machine, `<node>/<module>.<tool>` for an assignment, and
|
||||
`<module>.<tool>` as well for a module whose instances are interchangeable. A module that is not
|
||||
interchangeable is called with its machine or refused with the machines it runs on. Each discovery
|
||||
verb asks the mesh when it is called, so nothing is kept for a session's length; the flat catalogue
|
||||
verb asks the mesh when it is called, so nothing is kept for a session's length; the flat catalog
|
||||
stays reachable through the `mesh` client and a setting, unannounced.
|
||||
|
||||
**What exists is what announced itself** (ADR 0197):
|
||||
@@ -117,6 +131,64 @@ listed once per machine does not hold on the live mesh MCP server — postgres a
|
||||
optional, answered by whichever instance replies. The address replaces that statement rather than
|
||||
repairing it.
|
||||
|
||||
## 3b. Waiting on the bus
|
||||
|
||||
*By ADR 0367, as ADR 0670 and ADR 0671 change it.* Beside the five discovery tools of §3a and `mesh_runtimes`, the mesh MCP
|
||||
server offers a seventh, `mesh_wait`: a session names one or more events, an optional `filter` of body fields and a
|
||||
`timeout` (four minutes when absent, ten at most), and is answered the first matching event or told it timed out
|
||||
(ADR 0367 §1). A session calls it in a loop from a background command (ADR 0367 §3), and after every answer or time-out
|
||||
reads the state once from the verb that owns it: an event only wakes it (ADR 0367 §4).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
E[a seat's, a module's or a module stream's event<br/>mesh.seat.<seat>.event.… / mesh.mod.<module>.event.… / mesh.stream.<module>.event.…] -->|heard for the call| TR[the tool runner's account]
|
||||
G[seat mesh-grants] -->|grants the three event subjects| TR
|
||||
M[the catalog half, seat mesh-catalog] -->|the marked events and restricted streams| TR
|
||||
TR -->|a marked wait only on the operator's warrant| OC[seat operator-channel]
|
||||
TR -->|mesh_wait answers it by name| S[a session]
|
||||
S -->|then reads the state| O[the verb that owns it]
|
||||
```
|
||||
|
||||
**The tool runner hears every event of the mesh, and no list says which** (ADR 0670 §1, §2). The grants part (seat
|
||||
`mesh-grants`) grants the account of the runtime carrying the tool runner a plain subscription on the event subjects
|
||||
of every seat and every module, and on nothing else of theirs: no accept, proof, call, work queue or bucket, and no
|
||||
publish. `mesh_wait` subscribes when called and closes the subscription when it answers, so the account keeps no
|
||||
durable consumer and no queue of its own (kept from ADR 0367 §2), and any event name is accepted: a new event is
|
||||
heard from its first publication, with no change to a manifest. The tool runner's manifest names no events, the
|
||||
manifest validator has no rule about them, and no membership lists them. A name whose emitter no seat and no module
|
||||
bears is refused, so a misspelt wait does not run for ever.
|
||||
|
||||
**Who hears.** Whoever may call the mesh MCP server: any process on the machine, through its loopback endpoint (§2),
|
||||
which is the session there. The tool runner's standing grant of every event is accepted under ADR 0670 §1: what a
|
||||
session hears through it, it may hear.
|
||||
|
||||
**Module streams too, mail included** (ADR 0671 §1, §2). The tool runner also hears the events kept in a module's own
|
||||
stream (ADR 0363 §2), the seat holders' events published there among them, a restricted stream such as mail's
|
||||
included. A restricted stream's grant value decides who consumes it, never who hears it, so the tool runner's hearing
|
||||
needs no `consumes` and no entry in that value, and keeps no consumer of it.
|
||||
|
||||
**A marked event is heard only on the operator's warrant** (the operator, 2026-11-11). The approval mark (ADR 0286) covers a module's events and streams as well as its tools and
|
||||
verbs: a module may mark one of its events in `needs-approval`, and a restricted stream is marked whole. The tool
|
||||
runner reads the marks from the catalog half on the bus (seat `mesh-catalog`), and a `mesh_wait` on a marked event, or on any subject
|
||||
of a marked stream, runs only on the operator's warrant for that one wait, asked on the seat `operator-channel` with
|
||||
the exact names it waits on and its bound, and checked before the tool runner subscribes; without one it is refused
|
||||
and says so. The bus grants the subjects, and the tool runner's own gate holds the mark, as the operator chose; the
|
||||
gate is design 46's milestone 7, built in design 47's milestone G and delivered in one group with G and milestone
|
||||
every-event, so the subjects and the gate reach the machines together. This departs from ADR 0286 §1 and §8 (tools and
|
||||
verbs only, held by the bus) and from ADR 0671 §1 and §3 (every module stream heard with no exception) by the
|
||||
operator's answer. An unmarked event is heard with no ask.
|
||||
|
||||
**What that makes audible, and what still bounds it.** Every session now hears the operator channel's `decided` of
|
||||
every asker and the intake bench's events. So no event of a seat or a module carries a secret: no credential, token or
|
||||
code the mesh itself issues or keeps, and no message marked secret, which the code review asks of every change that
|
||||
adds an event or a field (ADR 0670 §4 as ADR 0671 §3 narrows it). A mail's own content is the operator's mail and is
|
||||
heard as it is, a sign-in link or a code someone sent by mail among it (ADR 0671 §3). And a heard event never answers anything: the operator's answer to an ask is read only
|
||||
with `claude_code_answer`, from the router's record and held to the question's digest (ADR 0670 §5, ADR 0332 §5,
|
||||
§6), and an asker acts only on a warrant from its own `decided` subject through its own consumer (ADR 0259 §6).
|
||||
|
||||
What is built for this, line by line, is milestone every-event's part,
|
||||
[`34-milestones/every-event.md`](34-milestones/every-event.md).
|
||||
|
||||
## 4. Where it runs
|
||||
|
||||
On whichever machines an operator sits at, by assignment. It is not on the control node by default and
|
||||
@@ -143,6 +215,14 @@ address gets a refused connection, which is the truthful answer.
|
||||
| a call through the mesh MCP server's endpoint reaches a module over the bus and the answer is the module's own, unshaped | ADR 0035, a surface decides nothing |
|
||||
| on the live mesh: the mesh MCP server assigned to a workstation answers `tools/list` on loopback and a call to the forge returns repositories | the exit of work-order step 3 |
|
||||
| the composed filter for a machine carrying the mesh MCP server opens no port for it | ADR 0144 |
|
||||
| on a real bus, `mesh_wait` answers the first matching event within a second of its publication, and after every answer a session reads the owner's state | ADR 0367 §1, §4 |
|
||||
| live: a watcher's `mesh_wait` on the status view's `delivered`, an event no manifest names, wakes on it with no rereading on a timer | ADR 0670 §2, milestone every-event |
|
||||
| a composition test: the tool runner's runtime account is granted exactly `mesh.seat.*.event.>`, `mesh.mod.*.event.>` and `mesh.stream.*.event.>` beside what its carried modules declare, with no publish on another's events, no durable consumer for them and no wider subject; on a real bus it is refused a publish on an event subject, and another module's account a subscription on an event it does not consume | ADR 0670 §1, §3; ADR 0671 §1 |
|
||||
| on a real bus, the tool runner's account hears a restricted stream's event with no entry in its grant value, still after the value changes, and is refused a durable consumer and an acknowledgement on it | ADR 0671 §1, §2 |
|
||||
| the tool runner's gate refuses a marked wait with no warrant and accepts one with a warrant bound to its exact names, filter and timeout; on the test bus a restricted stream's event is heard only after the warrant | the operator, 2026-11-11; ADR 0286 |
|
||||
| the manifest validator, as the checker runs it, refuses a manifest that carries `waits` as an unknown field, and accepts the tool runner's without it | ADR 0670 §2 |
|
||||
| the milestone's prepare lists every event with its body's fields, and its code review confirms none carries a secret before the grant is delivered; every later change adding an event or a field is asked the same in its review | ADR 0670 §4 |
|
||||
| nothing takes an operator's answer from a `decided` event's body; `claude_code_answer` is its one reader | ADR 0670 §5 |
|
||||
|
||||
## What shipped, 2026-09-30
|
||||
|
||||
@@ -157,17 +237,31 @@ machine with nothing else on it does; the mesh MCP server binds whatever it is g
|
||||
|
||||
Two things shipped bent, both stated in [`00-as-is/13-the-console.md`](../00-as-is/13-the-console.md):
|
||||
the person's client through the mesh MCP server (`--console`) exists and was exercised in the test suite, not
|
||||
on the live mesh; and a module registered from the catalogue by hand recorded its source as a URL rather
|
||||
on the live mesh; and a module registered from the catalog by hand recorded its source as a URL rather
|
||||
than as a path on the git seat, because `--self` takes the forge path form — the rebuild-on-merge still
|
||||
matched it by URL.
|
||||
|
||||
## Milestones
|
||||
|
||||
| Milestone | What it builds | State |
|
||||
|---|---|---|
|
||||
| the mesh MCP server | §1 to §5: the module, its loopback endpoint, its discovery (ADR 0152) | delivered 2026-09-30 (above) |
|
||||
| found by address | §3a: five announced tools and addresses (ADR 0195, ADR 0197) | delivered; [as-is 13](../00-as-is/13-the-console.md) |
|
||||
| waiting on the bus | §3b's `mesh_wait` on a list of five families in the tool runner's manifest (ADR 0367) | delivered (issue 745); [as-is 13](../00-as-is/13-the-console.md) |
|
||||
| every-event | §3b as ADR 0670 and ADR 0671 change it: the tool runner hears every event, module streams included, a marked one on the operator's warrant, and the list goes; one delivery group of mesh-sdk, mesh-delivery, mesh-tools and mesh-catalog, with hq | designed; its part is [`34-milestones/every-event.md`](34-milestones/every-event.md) |
|
||||
|
||||
## What this does not settle
|
||||
|
||||
- Narrowing a mesh MCP server's grant per assignment. ADR 0046 makes it a setting; nothing reads one yet.
|
||||
- The mesh's own verbs on the bus. Design 33's third family; this document only says where they appear
|
||||
once they exist.
|
||||
- A person's identity behind the mesh MCP server. The mesh sees the mesh MCP server's account; design 15 keeps the
|
||||
question open.
|
||||
- **The mesh's own verbs on the bus.** Design 33's third family: these now answer directly, through
|
||||
the `mesh-controller` seat, under ADR 0154 — settled, not open. [As-is 13](../00-as-is/13-the-console.md)
|
||||
records it live.
|
||||
- **Narrowing the tool runner's grant per assignment.** ADR 0046 makes it a setting; nothing reads
|
||||
one yet. Owned by [design 58](58-agent-work-as-the-meshs-own.md) milestone 6 (issue 883): an
|
||||
identity asked for each assignment is one of that milestone's parts.
|
||||
- **A person's identity behind the mesh MCP server.** The mesh still sees only the tool runner's own
|
||||
account, never the caller's; design 15 raised the question and leaves it open. Owned by the same
|
||||
milestone (design 58 milestone 6, issue 883): its outcome is that every person, agent and module
|
||||
becomes an identity with a role, and a call carries which one made it.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-catalog modules/claude-code]
|
||||
updated: 2026-10-13
|
||||
updated: 2026-10-17
|
||||
decisions:
|
||||
- "ADR 0411"
|
||||
- "ADR 0412"
|
||||
- "ADR 0343"
|
||||
- "ADR 0274"
|
||||
- "ADR 0273"
|
||||
@@ -130,7 +132,7 @@ a hypothesis (ADR 0153);
|
||||
the mesh is asked and changed through the controller seat's verbs; the forge through the forge module's
|
||||
tools; a licence through the `anthropic-licence-manager` seat's verbs, never by editing a file. The hard
|
||||
rules in new words: a file the mesh manages is changed through the verb that owns it or through the
|
||||
catalogue, never on disk; a store's database is never written by hand; main is never pushed; the mesh
|
||||
catalog, never on disk; a store's database is never written by hand; main is never pushed; the mesh
|
||||
creates no symlinks and nobody else does; a package is declared, not installed by hand. The glossary's
|
||||
words, none of the predecessor's.
|
||||
|
||||
@@ -155,6 +157,16 @@ does the job, or with the rule that a missing tool is created in its owning modu
|
||||
managed directory, so a session cannot change it, judges with the machines and replaced commands the module
|
||||
last asked of the controller, and records every refusal and every operator's override in the module's state.
|
||||
|
||||
**The wait list and the Stop hook** (ADR 0489, ADR 0490). Beside the guard the plugin carries two more of the
|
||||
module's own commands as hooks. At every session start the session-start hook prints the session's id and every
|
||||
open wait on the machine by the name it is kept under, with the call that takes a name back after a /clear (which
|
||||
gives a session a new id). At every stop the Stop hook reads the session's waits, which `claude_code_wait` keeps
|
||||
in the module's state under the session's name (who@node) and mirrors into the agent's home, and refuses the stop
|
||||
while a wait is within its bound and nothing follows it, or past its bound and not yet told to the operator; it
|
||||
lets the stop through silently when there are no waits, says so on the screen when the list cannot be read, and
|
||||
refuses the same stop at most three times. Like the guard, both hooks ship in the plugin only: the hook
|
||||
registration tools refuse a hook at mesh or node scope (issue 340).
|
||||
|
||||
## 4. The mesh MCP server
|
||||
|
||||
> **Revised 2026-10-03, building it.** The vendor's managed-settings key for tool servers refuses any
|
||||
@@ -219,7 +231,7 @@ All four nodes carry one since 2026-10-03. **Per node:** the role. **Per mesh or
|
||||
extra tool servers, and the operator's managed-settings keys (§2). The controller's verb replaces a
|
||||
setting layer whole, so a layer set for one of these keeps the others it already held. **Prerequisite:** the manager holds its seat and has adopted the licences.
|
||||
|
||||
**Order:** the manager assigned and a refresh observed; the mesh MCP server's provision in the catalogue; this
|
||||
**Order:** the manager assigned and a refresh observed; the mesh MCP server's provision in the catalog; this
|
||||
module on one workstation; the six predecessor files and the hand-made mesh MCP server entry removed there; a
|
||||
new session read to confirm it sees the mesh's instruction file, the mesh MCP server's five tools under `mesh`, and
|
||||
its licence; then the rest.
|
||||
@@ -306,21 +318,27 @@ operator's word — and, for the operator's own home, only at the terminal or on
|
||||
|
||||
## 9. The account the agent runs as
|
||||
|
||||
*Added 2026-10-08 by ADR 0266.*
|
||||
Everywhere above, "the operator's home" and "the operator account" mean **the account the agent runs as**.
|
||||
That is the operator account, unless the node names an **agent account** of its own. The control node does.
|
||||
*Added 2026-10-08 by ADR 0266; changed 2026-10-17 by ADR 0411 and ADR 0412.*
|
||||
Everywhere above, "the operator's home" and "the operator account" mean **the account the agent runs as**:
|
||||
the machine account of the identity the module runs for (ADR 0412). claude-code declares that it can run for
|
||||
an agent identity, in its assignment or when a session is started: started for `nox` it runs with the agent's
|
||||
home; started for the operator, it is the default run, with the operator's configuration. On a
|
||||
machine where no agent is marked as the agent account, its assignment is the default run, as every module's is.
|
||||
|
||||
**The fact.** The operator states the agent account at the controller's terminal. No verb and no setting
|
||||
writes it. The controller resolves the machine facts `agent-account`, `agent-home` and `agent-root`, and gives
|
||||
the tool runner the account and its home. Where the node names none, the facts name the operator account and
|
||||
`agent-root` is empty, so nothing changes there.
|
||||
**The fact.** The machine accounts come from the identity seat (ADR 0411); the operator no longer states the
|
||||
agent account at the controller's terminal. A machine's agent account is the machine account of the agent the
|
||||
identity seat marks as the agent account there, on the operator's Approve: `nox` on the control node. There this module's
|
||||
assignment is run for that agent, and the controller refuses one that is not. The controller resolves the
|
||||
machine facts `agent-account`, `agent-home` and `agent-root` from it, and gives the tool runner the account and
|
||||
its home. Where no agent is marked (every other machine today), the facts name the operator account and `agent-root`
|
||||
is empty, so nothing changes there, though a login `nox` exists for the modules run for it.
|
||||
|
||||
**What the module declares.**
|
||||
|
||||
- A `user` named by `agent-account`, with `root` set from `agent-root`. Where the account is the agent's own,
|
||||
the mesh creates it (home, own group, no password) and the node-engine judges on every look that it cannot
|
||||
become root without a person, by the ways ADR 0266 §3 lists. It detects those ways and no others; it
|
||||
prevents nothing.
|
||||
- No login. The mesh makes the agent's machine account (home, own group, no password) from the identity, and the
|
||||
node-engine judges on every look that it cannot become root without a person, by the ways ADR 0266 §12 to §17 list.
|
||||
It detects those ways and no others; it prevents nothing. The `user` resource named by `agent-account`,
|
||||
which this module declared until ADR 0411, goes.
|
||||
- `~/.claude` under that account's home, owned by it.
|
||||
- `claude-agent`, a command that starts the agent as that account through the operator's own sudo, or
|
||||
directly when already run as it.
|
||||
@@ -397,6 +415,7 @@ the helper of §9 where that is an agent account):
|
||||
| `rules/<name>.md` | one personal rule each |
|
||||
| `skills/<name>/`, `commands/<name>.md`, `agents/<name>.md` | the operator's own skills, commands and subagents at the operator scope, under their own names; and the home-scope items where the home is an agent account's own |
|
||||
| `hooks/` | nothing: a hook is the terminal's, in the managed settings. The directory is kept empty |
|
||||
| `nox-mesh/waits/<session-id>.json`, `nox-mesh/waits/_index.json` | the module's own mirror (ADR 0489, ADR 0490): per Claude Code session id, the open waits of every name bound to that id; and the machine's open names, written whole by `claude_code_wait` and read by the plugin's session-start and Stop hooks; rebuilt from the module's state, never a person's |
|
||||
| `output-styles/<name>.md`, `keybindings.json` | the operator's output styles and keybindings at the operator scope |
|
||||
| `settings.json` | the operator's registered settings, preference keys only, with the keys the program itself writes on a person's choice (the model, the spinner, the drafts, the other preferences the vendor's own commands set) taken over into the registration when the program changes them, so a choice made in one session follows the operator. A key that decides what runs (`hooks`, `permissions`, `mcpServers`, `env`, `apiKeyHelper`, `statusLine`, `autoMode`, any key not on the list) is never taken over: changed on disk, it is written back on the look that finds it. `autoMode` is a permission rule, so it lives in the managed settings at the terminal |
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ decisions:
|
||||
|
||||
# 37 — The operator's machine
|
||||
|
||||
**Every configurable thing on a node is a module, the home included, and the same catalogue serves
|
||||
**Every configurable thing on a node is a module, the home included, and the same catalog serves
|
||||
a server and a laptop.** One default configuration per module, varied per node by a setting or a
|
||||
kept region; roles a machine has once as node-scoped seats with tool contracts; one tool runner
|
||||
per node serving every module's tools outside any container
|
||||
@@ -117,7 +117,7 @@ resolver is asked by the second graphical module; the display server itself is g
|
||||
all four today. Nothing home-scoped composes before it.
|
||||
2. **The tool runner** — `mesh-host` supervises it; mesh-tools serves bundles from memberships
|
||||
and reloads; mesh-controller composes the bundle into the declaration and the memberships to one
|
||||
runtime per node; the catalogue renames the mesh MCP server. Proven when the packet-filter verbs answer
|
||||
runtime per node; the catalog renames the mesh MCP server. Proven when the packet-filter verbs answer
|
||||
from it and its container is gone.
|
||||
3. **`zsh`**, the first environment module: seat, `user` shape, home files, `execute`. Proven on a
|
||||
server first, then every node.
|
||||
|
||||
@@ -52,7 +52,7 @@ as design 28's: nothing here is new ground; every package reshapes something sta
|
||||
| the controller's bus principals | Go: one principal per module per node, grants from what it declares | 132 lines | gains one principal per node for the runtime |
|
||||
| the controller's memberships | Go: one per assignment, the subjects a runtime serves | 143 lines | **unchanged** in shape; the runtime reads several |
|
||||
| the controller's declaration composer | Go, one file | 2 053 lines | gains the runtime's process, the bundles' archives, two env words |
|
||||
| the catalogue | 35 manifests build a per-module tool container on the runtime's base image | — | none do; the runtime is a module of its own |
|
||||
| the catalog | 35 manifests build a per-module tool container on the runtime's base image | — | none do; the runtime is a module of its own |
|
||||
|
||||
**Two measurements decide the shape.** The node-engine needs no change: a `process` and an `archive` are
|
||||
what the runtime and a bundle are, and both are applied today. And the runtime already does
|
||||
@@ -140,11 +140,11 @@ with the shortcut off answers the same.
|
||||
a module whose own code is an image artifact is refused, whatever image it is built on.
|
||||
|
||||
*Amended 2026-10-02, at WP3.* The gate refuses the pattern **spreading**, not standing: a module
|
||||
new to the catalogue in that shape, or one that had already moved to a bundle and returns to it,
|
||||
is refused; a module the catalogue already holds in that shape — judged from the manifest it
|
||||
new to the catalog in that shape, or one that had already moved to a bundle and returns to it,
|
||||
is refused; a module the catalog already holds in that shape — judged from the manifest it
|
||||
holds and what that module's newest build stood on — is rebuilt without complaint. The day the
|
||||
runtime arrives some thirty such modules stand, each moves in its own change from WP4 on, and a
|
||||
gate refusing every rebuild in the meantime would stop the catalogue's pipeline to make a point
|
||||
gate refusing every rebuild in the meantime would stop the catalog's pipeline to make a point
|
||||
this record already makes.
|
||||
|
||||
**Proof.** Composition tests: a node with three assigned modules, one holding a seat, yields one
|
||||
@@ -159,7 +159,7 @@ the runtime is registered.
|
||||
**What changes.** mesh-tools gains a `bundle` artifact of itself beside its images, and its manifest
|
||||
becomes the `node-tools` module: a package for the interpreter, the loopback listener the mesh MCP server
|
||||
declared, `invokes: *`, and nothing else — the process is the controller's to compose (WP2). In the
|
||||
catalogue, `mesh-console` is retired as a module and `node-tools` assigned where it was. The
|
||||
catalog, `mesh-console` is retired as a module and `node-tools` assigned where it was. The
|
||||
runtime's `serve` keeps answering MCP on loopback; the person's end of it keeps the name "console"
|
||||
([glossary](../../00-META/glossary.md)).
|
||||
|
||||
@@ -178,7 +178,7 @@ runtime accepts. *Built 2026-10-02* (mesh-tools `c46f950`, mesh-controller `ca7e
|
||||
`729a5f9`). *Proven live 2026-10-02/03, on all four machines*: the mesh MCP server's container is gone,
|
||||
`node-tools` runs as a unit the node-engine wrote, as the operator's account, `tools/list` on each loopback
|
||||
answers with the same 219 tools as before, and the controller's verbs answer through it; `mesh-console`
|
||||
retired from the catalogue. Three things the step found are issues
|
||||
retired from the catalog. Three things the step found are issues
|
||||
[203](../../04-ISSUES/203-a-fresh-assignment-is-pushed-before-its-credential-exists/00-report.md),
|
||||
[204](../../04-ISSUES/204-a-controller-handover-re-sent-every-node-a-stale-declaration/00-report.md) and
|
||||
[205](../../04-ISSUES/205-a-package-resource-fails-against-a-stale-package-database/00-report.md).
|
||||
@@ -224,7 +224,7 @@ the node-engine re-creates the runtime's process on every reconcile (resolved th
|
||||
credential and state directory gone, the client through `sudo` since the daemon's socket is root's;
|
||||
proven on all four machines — `status`, `banned` and the module's own `fail2ban_settings` answer from
|
||||
the runtime, no `mesh-fail2ban` container, the runtime serving both bundles. Two holders moved; of the
|
||||
thirty-three tool containers the catalogue held, thirty-one remain, and all but these two carried their
|
||||
thirty-three tool containers the catalog held, thirty-one remain, and all but these two carried their
|
||||
module's configuration and secrets in the container's environment, which a bundle does not have — the
|
||||
question research research 020 opened
|
||||
and ADR 0192
|
||||
@@ -235,7 +235,7 @@ container's, the runtime hands each bundle its own. That is WP4b below.
|
||||
|
||||
*mesh-controller, mesh-tools, mesh-catalog. One day. The rest of ADR 0175, under ADR 0192.*
|
||||
|
||||
**What changes**, in order: the manifest's tools artifact gains `env` and the catalogue check
|
||||
**What changes**, in order: the manifest's tools artifact gains `env` and the catalog check
|
||||
refuses a secret's content in it; the composer resolves a bundle's `env` per machine and carries it
|
||||
beside the bundle's archive, `restart-on` included; the runtime hands each bundle its own
|
||||
environment — the contributor's argument for an imported bundle, the child's environment for a
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/claude-licence-manager]
|
||||
updated: 2026-10-04
|
||||
status: in-progress
|
||||
code: [mesh-catalog modules/claude-licence-manager, mesh-vault]
|
||||
updated: 2026-10-26
|
||||
decisions:
|
||||
- "ADR 0550"
|
||||
- "ADR 0209"
|
||||
- "ADR 0206"
|
||||
- "ADR 0183"
|
||||
@@ -18,13 +19,35 @@ decisions:
|
||||
|
||||
# 39 — The Anthropic licence manager
|
||||
|
||||
**One module knows every Anthropic licence the mesh has, keeps each alive, decides which consumer gets
|
||||
which, and hands every node's agent its token over the bus.**
|
||||
**One module knows every Anthropic licence the mesh has, keeps each alive, and decides which consumer gets
|
||||
which; the vault keeps every grant and token and hands every node's agent its token over the bus**
|
||||
(ADR 0550 §3, amending the first shape, in which the manager kept the grants and handed the tokens itself).
|
||||
ADR 0183
|
||||
decides it; this is the shape. It is the successor of the predecessor's manager module, built from what
|
||||
that module learned the hard way, and the counterpart of [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md),
|
||||
which is the consumer on every node.
|
||||
|
||||
## Every grant in the vault
|
||||
|
||||
*Decided 2026-10-26 (ADR 0550 §3), on the operator's answer that every secret lives in the vault; built in
|
||||
ADR 0547's second milestone (issue 1126). Until it is delivered, the sections below hold as first written.*
|
||||
|
||||
- **The vault keeps every licence's secrets**: the refresh tokens, the API key and the access tokens are vault
|
||||
secrets ([24](24-the-secrets-vault.md)). The manager keeps its bindings, its licence records without a secret
|
||||
(account, kind, health, usage, audit), and nothing at rest that is secret.
|
||||
- **The manager stays the one rotation source.** It alone calls the vendor's token endpoint: to refresh or adopt,
|
||||
it asks the vault for the grant sealed to itself and hands what it gets back to the vault. A grant offered at
|
||||
login, sealed to the manager's key (its `public-key` verb is kept), is handed on to the vault the same way.
|
||||
- **A holder's token comes from the vault.** The agent module on a node registers its public half with the vault,
|
||||
not the seat, and asks the vault for its token sealed to it. The vault serves only the licence the manager's
|
||||
binding names for that holder, and says `secret-changed`, never a value. The seat's `current` verb answers which
|
||||
licence a consumer holds and its health, never the token.
|
||||
- **It waits for accounts of their own.** The vault hands a value only to a module running under a bus account of
|
||||
its own (issue 569), so the move waits until the manager and each agent module run under one.
|
||||
|
||||
*How it is checked:* a unit test that the vault refuses a token to a holder no binding names or to one without an
|
||||
account of its own, and a scan after the move that the manager's tables hold no grant.
|
||||
|
||||
## 1. What it is
|
||||
|
||||
A module, `claude-licence-manager`, holding the mesh-scoped seat **`anthropic-licence-manager`**. One
|
||||
@@ -43,7 +66,8 @@ Its store holds four things:
|
||||
|
||||
**The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement.
|
||||
The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one
|
||||
node, the long-lived grants only.
|
||||
node, the long-lived grants only. ADR 0550 §3 ends the carve-out: the grants move into the vault, and the
|
||||
`licences` table keeps no grant (*Every grant in the vault*, above).
|
||||
|
||||
## 2. The licences it manages today
|
||||
|
||||
@@ -77,7 +101,9 @@ Carried from the predecessor, where each rule was earned by an incident:
|
||||
## 4. Handing a token to a node
|
||||
|
||||
*Amended 2026-10-04 by ADR 0206*, replacing the manager's visits: **what each consumer
|
||||
should hold is the manager's state, and the token is fetched when it changes.**
|
||||
should hold is the manager's state, and the token is fetched when it changes.** *Amended 2026-10-26 by
|
||||
ADR 0550 §3:* the token is fetched from the vault, not the seat; the `bindings` state and its generation stay
|
||||
the manager's, and the vault serves the licence a binding names.
|
||||
|
||||
- **The manager keeps a `bindings` state**, one key per consumer: the licence, its kind, and a
|
||||
**generation** that increases with every rotation and every switch. Nothing in it is secret.
|
||||
@@ -162,7 +188,8 @@ gone; a rotation or a switch is a new generation in the `bindings` state.
|
||||
identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one
|
||||
or all), `usage` (current and history), `adopt` (a file on the manager's node, or a key sealed to its
|
||||
`public-key` — ADR 0209), `public-key`, and `current` (a consumer's token, sealed to the key the
|
||||
consumer sends — ADR 0206). The manager asks a node for a candidate grant by the agent module's own tool.
|
||||
consumer sends — ADR 0206; from ADR 0550 §3, which licence a consumer holds and its health, never the
|
||||
token, which the vault hands). The manager asks a node for a candidate grant by the agent module's own tool.
|
||||
|
||||
## 8. Settings
|
||||
|
||||
@@ -179,6 +206,8 @@ one definition serves and one mesh may differ.
|
||||
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0183, attribution |
|
||||
| a failing licence notifies once, and once a day after, not once per tick | §3 |
|
||||
| the mesh MCP server lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build |
|
||||
| the vault refuses a token to a holder no binding names, or to one without a bus account of its own | ADR 0550 §3 |
|
||||
| after the move, the manager's tables hold no grant | ADR 0550 §3 |
|
||||
|
||||
## What this does not settle
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ Measured 2026-10-03 on the four machines and in the repositories.
|
||||
| a bundle's words | paths and constants written with `${dir:…}` and `${port:…}` only; a fact the mesh knows reaches a bundle as a file whose path is a word | the agent module's facts file and settings file |
|
||||
| a bundle calling a tool | `mesh/ask` through the runtime that launched it (ADR 0198); no bundle holds a bus credential | how the manager visits every node |
|
||||
| a module's own long-running code | a bundle the runtime launches and restarts (ADR 0198); the runtime's subscription and grants built, the modules moving in design 38 WP4c's waves | the manager's daemon (WP3, WP4) |
|
||||
| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue, built on the controller placement ADR 0183 moved away from; assigned to nothing | its client ported into the manager; the module retired (WP6) |
|
||||
| the credentials write, the strip, the identity read | `anthropic-consumer` in the catalogue; tested; assigned to nothing | ported into the agent module with its tests; the module retired (WP6) |
|
||||
| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalog, built on the controller placement ADR 0183 moved away from; assigned to nothing | its client ported into the manager; the module retired (WP6) |
|
||||
| the credentials write, the strip, the identity read | `anthropic-consumer` in the catalog; tested; assigned to nothing | ported into the agent module with its tests; the module retired (WP6) |
|
||||
| the predecessor's manager and consumer | the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints, cooldowns | ported as logic with its tests |
|
||||
|
||||
## The order the work allows
|
||||
@@ -99,7 +99,7 @@ resolves it. The runtime's port stays what its code fixes; assigning it is
|
||||
second question and not this package's.
|
||||
|
||||
**Proof.** The plan for a workstation carrying a consumer of `mcp-endpoint` shows it bound to the
|
||||
runtime's port; the controller's tests and the catalogue's checks pass.
|
||||
runtime's port; the controller's tests and the catalog's checks pass.
|
||||
|
||||
## WP2 — The agent module
|
||||
|
||||
@@ -130,7 +130,7 @@ runtime's port; the controller's tests and the catalogue's checks pass.
|
||||
**Proof, before anything runs live.** Unit tests: the renderer writes the mesh's keys and nothing else;
|
||||
it writes nothing when nothing changed; the credentials write strips a refresh token and is atomic; the
|
||||
lineage cases from the predecessor; a sealed hand-over opens only with the module's key; no tool's answer
|
||||
contains a token. The catalogue's checks pass.
|
||||
contains a token. The catalog's checks pass.
|
||||
|
||||
**Proof, live, on one workstation, configuration only.** Assign the module; set the node's role; push.
|
||||
The agent's managed directory holds the two files; everything under the person's agent directory is
|
||||
@@ -188,11 +188,11 @@ credentials file is left as it was and the agent authenticates through the key-h
|
||||
|
||||
*The live mesh and mesh-catalog. One day.* The module on every node, the predecessor's files removed on
|
||||
the second workstation; a login under a licence's account collected and adopted, and one under the wrong
|
||||
account refused and notified; `anthropic-manager` and `anthropic-consumer` retired from the catalogue;
|
||||
account refused and notified; `anthropic-manager` and `anthropic-consumer` retired from the catalog;
|
||||
designs 36 and 39 set to `implemented` with the as-is written
|
||||
([playbook 02](../../00-META/process/02-graduation.md)).
|
||||
|
||||
**Proof.** `claude_code_status` answers on every node; the refusal's notification arrived; the catalogue
|
||||
**Proof.** `claude_code_status` answers on every node; the refusal's notification arrived; the catalog
|
||||
has no module built on the old placement.
|
||||
|
||||
## The home, personal rules and lessons — WP7 to WP10
|
||||
@@ -259,7 +259,7 @@ WP10 the rest of the nodes, and the as-is (the live mesh)
|
||||
neither list, the watched entries and the pending requests.
|
||||
|
||||
**Proof, before anything runs live.** The module's tests of design 36 "How it is checked" for ADR 0273 §1,
|
||||
§2, §3 and §4, each failing on the commit before. The catalogue's checks pass.
|
||||
§2, §3 and §4, each failing on the commit before. The catalog's checks pass.
|
||||
|
||||
### WP8 — Personal rules and the operator's items, live
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-host, mesh-controller, mesh-catalog]
|
||||
updated: 2026-10-16
|
||||
updated: 2026-11-04
|
||||
decisions:
|
||||
- "ADR 0394"
|
||||
- "ADR 0310"
|
||||
@@ -146,7 +146,7 @@ against WP2's controller before anything is published.
|
||||
the node. The rendering is ADR 0203's and ADR 0204's: module order, a naming line per contribution,
|
||||
`PATH` entries added only when missing. `${machine:…}` in a contributed value is resolved first.
|
||||
- **Refusals.** A variable set by two modules on one node is refused, naming both. A placeholder in a
|
||||
module that does not claim the matching seat is refused, both at the catalogue check and at
|
||||
module that does not claim the matching seat is refused, both at the catalog check and at
|
||||
composition.
|
||||
|
||||
**Proof.** The controller's tests:
|
||||
@@ -157,7 +157,7 @@ against WP2's controller before anything is published.
|
||||
- each refusal, by name;
|
||||
- the seat table carries both seats and refuses a module declaring either.
|
||||
|
||||
The catalogue check over the whole catalogue passes.
|
||||
The catalog check over the whole catalog passes.
|
||||
|
||||
## WP3 — The modules
|
||||
|
||||
@@ -188,7 +188,7 @@ The catalogue check over the whole catalogue passes.
|
||||
- **`zsh-autosuggestions` and `zsh-syntax-highlighting`, new.** Each declares its package and
|
||||
contributes its loader from the distribution's path, in the `normal` and `last` slots.
|
||||
|
||||
**Proof.** The controller's catalogue check over the whole catalogue passes. The modules' tests pass.
|
||||
**Proof.** The controller's catalog check over the whole catalog passes. The modules' tests pass.
|
||||
A rehearsal composition for a node holding all five shows:
|
||||
|
||||
- the `.zshrc` block with the prompt in `normal` and highlighting in `last`;
|
||||
@@ -227,7 +227,7 @@ The user-scoped units of `mesh-host` #72 stay to-be 38's WP6.
|
||||
|
||||
1. Merge WP1 and roll the node-engine.
|
||||
2. Merge WP2, and push the controller.
|
||||
3. Merge WP3 and WP4, and build the new modules by hand: a new catalogue module's first build is asked
|
||||
3. Merge WP3 and WP4, and build the new modules by hand: a new catalog module's first build is asked
|
||||
for, not automatic.
|
||||
4. On one server, assign `node-env`, `zsh`, `zsh-autosuggestions` and `zsh-syntax-highlighting`, and
|
||||
push. Then check:
|
||||
@@ -262,7 +262,7 @@ group**: the lighting daemon's, the container runtime's, a serial port's. It dec
|
||||
│
|
||||
node-engine usermod --append ── recorded on openrazer.account: "the mesh added openrazer"
|
||||
│
|
||||
account judge database lists it? ── running manager holds it? ── no: relogin needed
|
||||
account probe database lists it? ── running manager holds it? ── no: relogin needed
|
||||
│ (module openrazer, kind account)
|
||||
controller module.openrazer.<machine>.unhealthy, cleared on the first look after a new login
|
||||
```
|
||||
@@ -280,7 +280,7 @@ group**: the lighting daemon's, the container runtime's, a serial port's. It dec
|
||||
when the resource is undeclared, is given back. It is not given back while another declared resource
|
||||
asks for it. The account is taken out of that one group (`gpasswd --delete`), read back, and a failure
|
||||
is said and is not fatal, as giving back a shell is (WP1).
|
||||
- **The node-engine, looking.** An account judge sits beside liveness and the failed-unit reading
|
||||
- **The node-engine, looking.** An account probe sits beside liveness and the failed-unit reading
|
||||
(issue 315). On every look it reads, for each account a module puts in groups, the database's groups
|
||||
and the groups of the account's running manager, from that process's status file. It states each as the
|
||||
module's resource of kind `account`: healthy, or unhealthy with *not in the group* or *relogin needed*.
|
||||
@@ -328,7 +328,7 @@ control-node withholds it until a call to it needs a person's approval. The othe
|
||||
writes it into, and a word naming that file for its tools bundle. The bundle registers `execute` only
|
||||
when the file says `serve`. `zsh_config` also answers whether `execute` is served on the node, and why.
|
||||
|
||||
**Merge order.** The controller first, then the catalogue. Either order composes. With the catalogue
|
||||
**Merge order.** The controller first, then the catalog. Either order composes. With the catalog
|
||||
alone and the control-node's value set, the self-check would say the holder there is silent.
|
||||
|
||||
**Proof.** The tests ADR 0268 lists under *how it is checked*. Live, after both are merged and rolled
|
||||
@@ -382,7 +382,7 @@ move, write or delete a file, and a cleanup needed a shell on the machine. Most
|
||||
project folders, downloads, logs, scratch.
|
||||
|
||||
- **A node seat, `node-files`, on every node**, held by a module of its own, `mesh-files`, beside `mesh-reads`
|
||||
in the catalogue; the controller holds the seat's definition beside the other node seats. It has a verb for each file action: list,
|
||||
in the catalog; the controller holds the seat's definition beside the other node seats. It has a verb for each file action: list,
|
||||
stat, readlink, read, search, write, copy, move, delete, make a folder, change mode, change owner, account and group
|
||||
lookup, and the trash's list and restore. Each says what it replaces (ADR 0245): `ls`, `stat`, `readlink`, `cat`,
|
||||
`grep`, `cp`, `mv`, `rm`, `mkdir`, `chmod`, `chown`, `getent passwd`, `getent group`, `id`, and `ssh <node> cp` for a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-catalog, mesh-controller, mesh-host]
|
||||
updated: 2026-10-14
|
||||
updated: 2026-10-22
|
||||
decisions:
|
||||
- "ADR 0369"
|
||||
- "ADR 0303"
|
||||
@@ -26,6 +26,9 @@ decisions:
|
||||
- "ADR 0164"
|
||||
- "ADR 0165"
|
||||
- "ADR 0166"
|
||||
- "ADR 0507"
|
||||
- "ADR 0508"
|
||||
- "ADR 0510"
|
||||
---
|
||||
|
||||
# 42. The machines' modules, in order
|
||||
@@ -47,7 +50,7 @@ not before. Modules that need none go ahead now.
|
||||
|
||||
## How every module moves
|
||||
|
||||
1. Written in the catalogue, with its tools and their tests, and checked by the controller's catalogue
|
||||
1. Written in the catalog, with its tools and their tests, and checked by the controller's catalog
|
||||
check.
|
||||
2. Merged, which builds it.
|
||||
3. Assigned to **the first workstation, the proving machine**, and pushed. Its tools and files are
|
||||
@@ -69,14 +72,14 @@ In order:
|
||||
| 1 | `sudo` | the operator account's escalation, as a drop-in it owns | declares what three modules' tools assume and nothing stated |
|
||||
| 2 | `localization` | locale, time zone, mesh MCP server keymap | one machine on another zone and keymap |
|
||||
| 3 | `time-sync` | timesyncd and its servers | two different daemons across four machines |
|
||||
| 4 | `pacman` | the package manager's configuration, mirrors and their refresh, cache cleaning | mirrors generated once and never again; caches never cleaned |
|
||||
| 4 | `pacman` | the package manager's configuration, mirrors and their refresh, cache cleaning; every node's packages on the bus; upgrades one node at a time, each with a package record, a held kernel, an after-upgrade check and a rollback (ADR 0507, ADR 0508) | mirrors generated once and never again; caches never cleaned; no view of which version runs where; every node upgraded at the same moment with no way back |
|
||||
| 5 | `logrotate` | the timer and base configuration | rotation running on one machine of four |
|
||||
| 6 | `avahi` | the daemon | on all four, owned by none |
|
||||
| 7 | `systemd` | the service manager's tools (to-be 41 WP4) | built, assigned nowhere |
|
||||
| 8 | `docker` | the runtime's packages, base configuration, group | four configurations, one owner on one machine |
|
||||
| 9 | `ssh-client` | everything under `~/.ssh` (research 027/03) | a predecessor's entries winning over the mesh's; stale keys |
|
||||
| 10 | scripts | the operator's own scripts, shared and per role (research 027/03) | under no version control, copied by hand |
|
||||
| 11 | `kernel` | kernel, microcode, boot entries, and the kernel's run-time settings per machine (transparent huge pages first) | two machines without microcode; huge pages left at the distribution's `always` on a machine whose slow disks they flood (issue 746) |
|
||||
| 11 | `kernel` | kernel, microcode, boot entries, the fallback kernel and its boot entry, and the kernel's run-time settings per machine (transparent huge pages first) | two machines without microcode; huge pages left at the distribution's `always` on a machine whose slow disks they flood (issue 746); only one machine with a fallback kernel |
|
||||
|
||||
`systemd`, `pacman` and `docker` hold the three seats that apply resources
|
||||
(ADR 0207):
|
||||
@@ -101,6 +104,33 @@ mode in force and the file pages each unit holds in huge pages. `docker` holds t
|
||||
ADR 0164 decides and the capability detected as
|
||||
ADR 0165 decides; what of the three is not built yet is tracked by issue 398.
|
||||
|
||||
**Changed 2026-10-22** (ADR 0507, ADR 0508, ADR 0510; milestone issues 1840 and 1841). `pacman` gains a
|
||||
process of its own on every node, the keeper, under an account of its own and a bus account of its own. It keeps
|
||||
the node's installed packages and what an upgrade would change in the module's states, written only on change
|
||||
(at its start, when the package manager's lock is released, when the mirror list is rewritten, and once a day),
|
||||
and says each change as an event; the controller keeps the packages each node's modules declare, so the view
|
||||
answers every node, one that is off included, and names the packages no module declares. Upgrades are runs on
|
||||
the bus. The weekly timer of ADR 0477 §5, which upgraded every node at the same moment, is dropped. Instead the
|
||||
nodes upgrade one at a time, in the order of the mesh-level key `upgrade-order`, each after the node before it
|
||||
passed its after-upgrade check. With no order set no round starts, and every node's health says why. Before each
|
||||
run a package record is taken and read back. A kernel that an installed DKMS module's own bound excludes is held
|
||||
with its headers, and said. After each run the after-upgrade check names left configuration files, the
|
||||
distribution's news, newly failed units, a restart needed, processes still mapping a replaced file and DKMS
|
||||
modules not built for an installed kernel. A rollback returns a node to a record and says first what it would
|
||||
change. Only the operator passes a node that stopped a round. The keeper never runs as root: the few acts that
|
||||
need root go through a root-owned helper process with a closed list of verbs, on a socket only the keeper's
|
||||
account opens (ADR 0510). The module's health is read from the bus, not from a unit's age.
|
||||
|
||||
`kernel` gains the fallback kernel: `linux-lts` and its headers on every machine, with its boot entry on
|
||||
whichever loader the machine boots with, read from the machine (systemd-boot or GRUB), the default entry left
|
||||
untouched (ADR 0508 §9-§10, changing ADR 0381 §4). A hook found on a machine that copies every kernel image to
|
||||
one fixed boot name is taken over: moved into the module's state and named, since it would put the fallback
|
||||
kernel in the default entry's place. *How it is checked:* the modules' unit tests (the root helper refuses every
|
||||
verb and argument off its list and every account but the keeper's; the round starts a node only after the one
|
||||
before it passed; the boot-entry script in stand-in trees of both loaders), an integration test that runs the
|
||||
keepers of two made-up nodes on a real bus, and, after the delivery, the view answering every node and every
|
||||
boot menu listing the fallback kernel.
|
||||
|
||||
**Added 2026-10-04.** Two more modules for every machine:
|
||||
- `power` holds `node-power` (ADR 0211). Other modules contribute code for
|
||||
its moments (after boot, before sleep, after waking, before shutdown, on mains, on battery), and it
|
||||
@@ -169,11 +199,11 @@ both places of both bars once. The bar writes no block that depends on the hardw
|
||||
the power module's state `power.draw`, one value per machine, which its bundle puts from its own root
|
||||
sampler. The block `shows: state`; the bar's bundle watches it under a read the controller grants the
|
||||
bar for what the modules on its machine offer, and the bar names no module. The battery block is the
|
||||
bar's own, reading the kernel. The catalogue's `checks/paths` refuses a module that names another
|
||||
bar's own, reading the kernel. The catalog's `checks/paths` refuses a module that names another
|
||||
module's path under `/run` or `/var/lib`.
|
||||
|
||||
The node-engine is delivered first, so the machines report the two capabilities. The controller comes
|
||||
next, because it parses `data`, `if-capability` and `renders`. The catalogue comes last. Left open: the
|
||||
next, because it parses `data`, `if-capability` and `renders`. The catalog comes last. Left open: the
|
||||
draw at the wall (a metered plug or a UPS), a GPU-load block from a generic module, and the headsets.
|
||||
|
||||
## Phase 3 — one machine model
|
||||
@@ -183,6 +213,6 @@ and `memory-pressure` (research 027/03).
|
||||
|
||||
## How it is checked
|
||||
|
||||
Each module's own tests and the catalogue check, at merge. On the proving machine, each tool answered
|
||||
Each module's own tests and the catalog check, at merge. On the proving machine, each tool answered
|
||||
through the mesh and each owned file checked in place, before any other machine is assigned. This
|
||||
document's tables are updated as each module lands.
|
||||
|
||||
@@ -146,8 +146,8 @@ a manifest. Not off-site: a lost machine loses its backups with its data, by the
|
||||
|
||||
## How it is checked
|
||||
|
||||
The catalogue check (`module check`, and the catalogue-wide test beside it) refuses a provider that
|
||||
The catalog check (`module check`, and the catalog-wide test beside it) refuses a provider that
|
||||
grants without saying what it keeps for its consumers, an irreplaceable item with neither a backup nor a
|
||||
redundancy, and any backup line written by hand (ADR 0233). The store rule this design stated before
|
||||
was held only by a controller test over the catalogue checked out beside it, never by `module check`. The weekly
|
||||
was held only by a controller test over the catalog checked out beside it, never by `module check`. The weekly
|
||||
restore test above, and the 48-hour status line, are the running checks.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
|
||||
updated: 2026-10-14
|
||||
updated: 2026-11-04
|
||||
decisions:
|
||||
- "ADR 0357"
|
||||
- "ADR 0325"
|
||||
@@ -68,8 +68,8 @@ Owning repositories: **mesh-controller** (the condition store, watchdogs, `docto
|
||||
calls, the hand-act log, the facts snapshot), **`mesh-host`** (the node-engine: the apply queue, report
|
||||
order, epoch refusal, the `report` verb, rollback witnessing), **mesh-tools** (the tool runner and the
|
||||
mesh MCP server: their heartbeat, passing every argument, health answers), **mesh-catalog** (the
|
||||
`operator-channel` seat's holder and channels, the watcher, the providers' retirement, the catalogue's
|
||||
merge gate), **mesh-sdk** (the TypeScript providers' loop and its retirement), **mesh-lab** (the
|
||||
`operator-channel` seat's holder and channels, the watcher, the providers' retirement, the catalog's
|
||||
composition check), **mesh-sdk** (the TypeScript providers' loop and its retirement), **mesh-lab** (the
|
||||
replays and the induced-failure scenarios).
|
||||
|
||||
---
|
||||
@@ -320,7 +320,7 @@ producer that wrote it.
|
||||
|
||||
| Probe | Asserts | From |
|
||||
|---|---|---|
|
||||
| D1 | every machine's declaration composes, and passes the node-engine's validation (the validator is a package of `mesh-host` the controller and the merge gate import — one validator) | 236, 263 |
|
||||
| D1 | every machine's declaration composes, and passes the node-engine's validation (the validator is a package of `mesh-host` the controller and the composition check import — one validator) | 236, 263 |
|
||||
| D2 | every holder of the mesh's resolver answers a machine name for IPv4, and NODATA for IPv6 | 262 |
|
||||
| D3 | every seat on record has a live holder that answers (`holder-silent`, H3) | 208, 218 |
|
||||
| D4 | every kept archive is held by a manifest | 253 |
|
||||
@@ -680,12 +680,12 @@ runs, and a restore that builds a new store beside the live one for a person to
|
||||
**The facts snapshot.** The controller exports daily, and after any change of machines, assignments or
|
||||
seats: every machine (a stable pseudonym of the same length as its name, its role, operating system,
|
||||
C library, architecture, node-engine and tool runner builds), assignments, settings keys and their
|
||||
non-secret values, seats and their holders, the catalogue commit, and the versions the mesh runs of
|
||||
non-secret values, seats and their holders, the catalog commit, and the versions the mesh runs of
|
||||
the bus server, the store and the node-engine. No secret and no address: an address is replaced by one
|
||||
from a documentation range. It is kept in the artifact store as `facts/latest`, where the build seat
|
||||
reads it.
|
||||
|
||||
**The merge gate.** In mesh-controller, `mesh-host` and mesh-catalog, a check composes every machine of
|
||||
**The composition check.** In mesh-controller, `mesh-host` and mesh-catalog, a check composes every machine of
|
||||
the snapshot with the change applied and runs the node-engine's validator over each. A change that
|
||||
makes any machine fail to compose or validate fails its check, naming the machine's role and the
|
||||
module. The resolver module's tests run under both C libraries the snapshot lists. A test in each core
|
||||
@@ -768,7 +768,7 @@ replay covers what must not be done to it on purpose, and every rule keeps a liv
|
||||
- **It runs on the build seat**, asked by the controller when the forge's announcer says a pull request's
|
||||
head moved: the repository's own `merge-check.sh` in the mesh's Go toolchain with no container runtime
|
||||
socket, beside a throwaway store and bus of the versions the snapshot says run, then mesh-lab's replays.
|
||||
The verdict is the head commit's status `mesh/merge-gate`, an error never a pass. A catalogue change is
|
||||
The verdict is the head commit's status `mesh/merge-gate`, an error never a pass. A catalog change is
|
||||
judged by the controller the mesh runs; a node-engine change by it built with the change's validator.
|
||||
- **Replays live where their incident is**: a component's logic in its repository as `TestReplay<issue>`,
|
||||
what the mesh runs (the bus's release, the resolver under musl and glibc) in mesh-lab's `replays/`, all
|
||||
@@ -868,7 +868,7 @@ the `data` verb; `cleanup` extended to a module's own retired data, deleted by t
|
||||
holder after a last restore point; the controller's grant names `node-backup.backed-up`. Built on
|
||||
branches in mesh-controller (migration 0072), `mesh-host` (the installer's user list; the kept-directory
|
||||
report), mesh-catalog (the holder's measuring and deletion, every module's data, the providers' held
|
||||
sizes), the media catalogue and the photo application, not yet merged. **Not yet:** the TypeScript
|
||||
sizes), the media catalog and the photo application, not yet merged. **Not yet:** the TypeScript
|
||||
providers saying their consumers' sizes (until then a consumer at two of them is `data-held-twice`, a
|
||||
warning, not compared), and SMART read under an array.
|
||||
|
||||
@@ -907,7 +907,7 @@ a build whose migration cannot be undone; the verdicts of the next three core bu
|
||||
| Repository | Delivers |
|
||||
|---|---|
|
||||
| mesh-controller | the facts snapshot and S14 |
|
||||
| mesh-controller, `mesh-host`, mesh-catalog | the compose-and-validate merge gate; versions tested as run; the resolver's tests under both C libraries |
|
||||
| mesh-controller, `mesh-host`, mesh-catalog | the compose-and-validate check; versions tested as run; the resolver's tests under both C libraries |
|
||||
| mesh-lab | R4, R5 and the rest of the window's incidents; running the replays on every core merge |
|
||||
|
||||
**Done when:** the replays of 236, 262, 263 and 266 fail on the commit before their fix and pass after;
|
||||
@@ -921,7 +921,7 @@ the verdict as their status, and `merge-check.sh`; in `mesh-host` `merge-check.s
|
||||
installer's user list; in mesh-lab the replays of 262 and 266, the register and the prover; in hq the
|
||||
replay rule in `cycle.py`. The prover ran the replays of **236, 262, 263, 266 and 273: each fails on the
|
||||
commit before its fix** (236's check did not exist there) **and passes on it**. **Not yet:** R4, R5 and the
|
||||
rest of the window's incidents as replays; the merge gate's first runs against the live mesh's snapshot;
|
||||
rest of the window's incidents as replays; the composition check's first runs against the live mesh's snapshot;
|
||||
the status required to merge, which is the operator's setting on the forge; mesh-tools' and mesh-sdk's
|
||||
`merge-check.sh`.
|
||||
|
||||
@@ -931,8 +931,8 @@ and "the commit is the build at hand"), on branches, not yet merged:
|
||||
|
||||
| Repository | Delivers |
|
||||
|---|---|
|
||||
| mesh-controller | the planner's one mapping of a changed file onto modules and its one answer of a merge's reach, asked by the merge handler, the what-if, the gate and the check; a file no build reads touches nothing (issue 280's *left open*); every pull request answered — the gate when it reaches a module, the repository's own check for the mesh's repositories, a pass that says so otherwise; the gate run by the build seat with its judge chosen from the graph, composing the plan's definitions only, failing only a manifest problem the change brings; the delivery plan computed and carried with the verdict; a build of a commit off its module's trunk recorded and never registered; its own manifest naming every verb of its seat |
|
||||
| mesh-catalog | the forge's announcer saying, with every pull request, the directories holding a module at its head, the files it deletes and whether it has a `merge-check.sh`; both statuses and the delivery plan on the pull request; `gitea_branch_protection_get` and `gitea_branch_protection_set`; the catalogue's own check |
|
||||
| mesh-controller | the planner's one mapping of a changed file onto modules and its one answer of a merge's reach, asked by the merge handler, the what-if, the gate and the check; a file no build reads touches nothing (issue 280's *left open*); every pull request answered — the gate when it reaches a module, the repository's own check for the mesh's repositories, a pass that says so otherwise; the gate run by the build seat with the program it composes with chosen from the graph, composing the plan's definitions only, failing only a manifest problem the change brings; the delivery plan computed and carried with the verdict; a build of a commit off its module's trunk recorded and never registered; its own manifest naming every verb of its seat |
|
||||
| mesh-catalog | the forge's announcer saying, with every pull request, the directories holding a module at its head, the files it deletes and whether it has a `merge-check.sh`; both statuses and the delivery plan on the pull request; `gitea_branch_protection_get` and `gitea_branch_protection_set`; the catalog's own check |
|
||||
| `mesh-host`, mesh-tools, mesh-sdk, mesh-lab, mesh-media-catalog, and the applications built from their own repositories | each its own `merge-check.sh`; mesh-tools' tests each on a bus of their own |
|
||||
| mesh-tools-go, mesh-tools | a C compiler and Python in the Go toolchain, git in the TypeScript one |
|
||||
| hq | its own `merge-check.sh`, running `records.py`, `index.py` and `cycle.py` |
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
---
|
||||
layer: to-be
|
||||
design: 46
|
||||
milestone: 7-approval-mark
|
||||
updated: 2026-11-11
|
||||
code: [mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-host, mesh-catalog modules/zsh, mesh-catalog modules/gitea, mesh-catalog modules/mesh-files]
|
||||
decisions:
|
||||
- "ADR 0286"
|
||||
- "ADR 0259"
|
||||
- "ADR 0627"
|
||||
- "ADR 0656"
|
||||
- "ADR 0666"
|
||||
- "ADR 0668"
|
||||
- "ADR 0595"
|
||||
- "ADR 0597"
|
||||
- "ADR 0310"
|
||||
- "ADR 0394"
|
||||
- "ADR 0619"
|
||||
- "ADR 0268"
|
||||
- "ADR 0564"
|
||||
---
|
||||
|
||||
# Design 46, milestone 7 — a call that needs the operator's approval
|
||||
|
||||
**What it builds.** A tool or a seat verb marked in its manifest or its seat's definition runs only on the operator's
|
||||
approval of that one call, with its exact arguments (ADR 0286). The first call asks the operator on the seat
|
||||
`operator-channel` and runs nothing; the same call made again within 30 minutes of the operator's Approve runs once.
|
||||
The tool runner (module `node-tools`) asks, judges and claims; it reads the marks from the catalog half on the bus; and
|
||||
on a machine whose tool runner does not honour the mark the grants part withholds every marked subject. The shape is in
|
||||
the design's main file,
|
||||
[§10, *A call that needs the operator's approval*](../46-the-conversation-with-the-operator.md#a-call-that-needs-the-operators-approval);
|
||||
this part holds every line the milestone builds, cited as "design 46 7-approval-mark §n".
|
||||
|
||||
**When.** It is built and delivered in design 47's milestone G, in G's delivery group, with design 34's milestone
|
||||
every-event, whose marked waits (design 34 every-event §32 to §37) use this gate (the operator, 2026-11-11: "Build the
|
||||
gate in G").
|
||||
|
||||
**Where.** mesh-sdk (the manifest field, the seat definition's field, the per-machine state's keys), mesh-contracts
|
||||
(the login shell's seat definition), mesh-delivery (the indexer's manifest validator and catalog graph entry, the grants
|
||||
part's deny), mesh-tools (the tool runner's gate, its state, its tools, and the mesh MCP server's `mesh_describe` in
|
||||
it) and mesh-catalog (the zsh module, which holds the login shell, and the forge module, §21). Nothing is built in a controller: no controller issues memberships today, bus permissions are the grants part's
|
||||
(seat `mesh-grants`), the hand-act log is the seat `mesh-hand-acts`, and the asker's logic is operator-channel's client
|
||||
in mesh-contracts (ADR 0627 §1).
|
||||
|
||||
**What changes from ADR 0286 as written.** Each line says in its *Why* where something built since ADR 0286
|
||||
(2026-10-10) changes how a clause is met: the mark is read from the catalog half and not from a membership (§5), the
|
||||
digest is Go alone (§8), the claim is kept in the runner's bus state and not a file (§13), `warranted` is a kept call
|
||||
and not a retry on a timer (§16), and a fourth open ask is offered as G makes it (§17). The controller's own verbs (ADR
|
||||
0286 §43) are not built: no controller serves verbs.
|
||||
|
||||
**The bound of the gate, a known limit.** The gate holds for every call that reaches the tool runner's handler. It does
|
||||
not hold for a process that reads the tool runner's bus credential (the file `broker` in its state directory,
|
||||
`own-secrets`) and speaks to the bus itself: that account may subscribe a marked subject, a restricted stream's among
|
||||
them, on a machine whose runner honours the mark. Read on the workstation on 2026-11-11 with `node-reads.stat`, the
|
||||
file is mode 0600, owned by the operator's account, in a directory of mode 0755, and the prepare of 2026-11-11 read
|
||||
the same mode and owner on all four nodes: every process running as the
|
||||
operator's account, every module on the machine's shared account and every agent session among them, can read it and
|
||||
hear a restricted stream without a warrant. This is the same known limit as design 47 G §385 (modules running as the
|
||||
machine's account, issue 1966 note 193, whose fix carries its check), and this milestone does not change it: that a
|
||||
restricted stream is heard only on a warrant holds against every caller once that fix runs modules and sessions on
|
||||
accounts that cannot read the file, and until then against callers that go through the tool runner. The
|
||||
prepare reads the file's mode and owner on every node with `<node>/node-files.stat` and writes them in the milestone
|
||||
file; a node where the file is readable beyond the operator's account is a finding of the prepare. The forge module's
|
||||
own credential (§21) lies in a directory of mode 0700, owned by the forge's account, which the reading account cannot
|
||||
enter; `node-files.stat` answers such a file's mode from this milestone (§21a), and the prepare reads it then.
|
||||
|
||||
## The mark
|
||||
|
||||
1. **A module marks its tools and verbs in `needs-approval`.** mesh-sdk's `catalogue.Manifest` gains `NeedsApproval`,
|
||||
the manifest key `needs-approval`: a list of strings, each a tool the module serves (`<tool>`) or a seat verb it
|
||||
serves (`<seat>.<verb>`), and, under design 34 every-event §32, one of its events. Absent is no mark. *Why:* ADR
|
||||
0286 §1, §49 (each marking is a manifest change, reviewed like any other). Only the contract's manifest reads
|
||||
(`ManifestReads`) gain `word:needs-approval`; the baseline of the engines before any stated what it reads
|
||||
(`manifest_reads_baseline.txt`) is unchanged, since none of them reads the word (Q-BL). *How it is checked:* unit, a
|
||||
row in `go/catalogue/invokes_test.go`'s `TestAManifestMaySayWhatItInvokes`, the nearest table over raw manifests
|
||||
(Q-T): a manifest naming a tool, a seat verb and an event in `needs-approval` decodes all three with `UnknownField()`
|
||||
empty, and one without it has no mark.
|
||||
2. **A seat's definition marks a verb for every holder.** mesh-sdk's `seatdef` reads a verb's `needs-approval`, a
|
||||
boolean, in a seat definition (`seats/<seat>/seat.json` in mesh-contracts); the seat definitions the catalog half
|
||||
carries (design 47 G §194) carry it on the verb. The definition format's own schema in mesh-contracts
|
||||
(`format/seat-definition.schema.json`) gains a verb's `needs-approval`, a boolean (Q-SC). Every strict reader of a
|
||||
seat definition reads the word from G's reader release (design 47 G §340), so no reader refuses a definition that
|
||||
carries it (Q-RR). *Why:* ADR 0286 §2. *How it is checked:* unit, a fixture row in `go/seatdef/seatdef_test.go`'s
|
||||
`TestADefinitionIsReadStrictlyNamingWhatIsWrong` (Q-T), and the schema validates the login shell's definition of §21.
|
||||
3. **Marks add up.** A verb is marked when its seat's definition or its holder's `needs-approval` marks it; a holder
|
||||
cannot unmark what its seat marks. *Why:* ADR 0286 §3. *How it is checked:* unit, a row of §5's table.
|
||||
4. **The manifest validator refuses a mark it cannot honour.** The indexer's manifest validator (`judge/rules.go`)
|
||||
refuses, as the checker runs it, and sets aside to wait, as the indexer runs it, a manifest that: marks a tool its
|
||||
`tools` does not list, or a verb its `claims` do not serve (ADR 0286 §4); marks a tool or a verb, or holds a seat
|
||||
verb its seat marks, without `operator-channel` in `uses` (§7); marks a tool or a verb while every artifact of kind
|
||||
`bundle` it builds is run by one of its `process` resources (`artifact`), so its own program answers its tools and
|
||||
no tool runner loads or gates them (a module such as the forge's, whose tools bundle no process runs, is not
|
||||
refused); or names in `needs-approval` one name that is both one of its tools and one of its events. A tool mark
|
||||
is checked against `tools` where the manifest lists its tools; where only its bundle names them, the tool runner
|
||||
checks each marked tool against the tools the bundle registers at load: a bundle whose marks name a tool it does not
|
||||
register is not loaded, so no tool of that module is served on that node at all, failing closed rather than serving
|
||||
unmarked what the manifest meant to mark, and that is its
|
||||
finding `<node>.<module>.mark-unserved` (said as §16's), cleared when a bundle that serves it, or a manifest without
|
||||
the mark, is loaded. Each refusal names the entry and the rule. *Why:* a mark
|
||||
nothing enforces would be a call that runs unasked while saying it is guarded; ADR 0286 §43 had the controller
|
||||
enforce its own verbs, and no other own-process server has a gate, so such a mark is refused rather than ignored;
|
||||
and an entry that names two things is read one way by one reader and another way by the next (the design's choices
|
||||
under ADR 0286 §4, §40, the refusal kept by lead-47 on 2026-11-11). *How it is checked:* unit, one row per refusal
|
||||
in `judge/rules_test.go`'s `TestRulesManifests` (Q-T), rows that accept the zsh and forge manifests of §21, and a row of §22's mark table for
|
||||
`mark-unserved`.
|
||||
|
||||
## Where the marks are read, and where they are withheld
|
||||
|
||||
5. **From the catalog half on the bus, never from a bundle.** The indexer's catalog graph entry of a module (design 47
|
||||
G §174) carries the validated manifest whole (design 47 G §443), its `needs-approval`, its `emits` and its streams
|
||||
(their events as the manifest spells them, and whether each carries a `grant`) among it; mesh-sdk's `buscompose`
|
||||
exports `WirePattern`, which a reader uses to turn a stream's events into the bus's subjects, the grants part's own
|
||||
copy going (Q-WP). No part but the indexer reads a manifest: every other part reads the entry of the build concerned
|
||||
from the catalog half (design 47 G §442; the operator, 2026-11-11: "only the indexer uses the manifest to build the
|
||||
module catalog"; Q-MR), the grants part's `declaredFor` among them for every grant, not only the marks; the seat
|
||||
definitions of the catalog half carry the verb marks (§2). The tool runner, which from G takes its first view of the
|
||||
catalog half by `mesh-catalog.revision` and follows `catalog-revision` (design 47 G §196), derives from it, for each
|
||||
module it serves, its marked tools and verbs, in a package of its own, `node-tools/internal/approval`, again on
|
||||
every revision it hears. No module's name is written in its code. The marks follow the running version: a bundle's
|
||||
served version is the fingerprint its composed environment carries (design 47 G §424), whose entry the runner reads
|
||||
by `mesh-catalog.entry` (design 47 G §443) when it loads the bundle; it applies that entry's marks and, while the
|
||||
newest revision's entry for that module differs, the union of both, so a mark is never lost before or after the
|
||||
module moves, and a delivery that drops a mark while the old bundle still runs is still asked. When the served
|
||||
entry cannot be read, every marked call on that runtime is refused (§19). A mark added while a call waits applies
|
||||
from the next call, which the answer of §12 says. *Why:* ADR 0286 §5 had the controller issue the mark in a
|
||||
membership so that a bundle could not drop it; no controller issues memberships now, and the judged catalog half is
|
||||
the same guarantee: the bundle never carries the mark (ADR 0656 §6, one source for which modules exist). The union
|
||||
keeps ADR 0286 §3's "marks add up" across a version change. The served version is read from what the bundle runs,
|
||||
not from a placement, which can change before the bundle does (step 0b). *How it is checked:* unit, a table test in
|
||||
`approval` derives the marks from fixture catalog halves and served entries, §3's row, a version change and the
|
||||
union among them, and a served entry that cannot be read.
|
||||
6. **A machine whose tool runner does not honour the mark serves no marked tool or verb.** The tool runner states
|
||||
`approval-mark/1` among its call capabilities (`runtime.CallCapabilities`), which its announcement carries with
|
||||
the tool runner's own fingerprint, and its node's node-engine takes both into the node's record; the assignment
|
||||
half carries, for each node, its capability readings, each with the fingerprint of the runner that announced it
|
||||
(design 47 G §194; Q-CR). The grants part, composing a runtime's account, reads that runtime's node's readings and
|
||||
the fingerprint the node-engine's `running.<node>.node-tools` last said running (design 47 G §309): where the tool
|
||||
runner does not state `approval-mark/1`, where the node has no readings, or where the readings' fingerprint differs
|
||||
from the running one or is absent (an older reading, failing closed as §19; no time is compared), it composes a
|
||||
subscribe deny of every marked subject of every module the runtime carries, the marks read from the catalog half
|
||||
as §5 has them: `mesh.mod.<module>.tool.<tool>` and the same followed by `.*`; the caller-named subject mesh-sdk's
|
||||
`process.Call` sends on first, `mesh.mod.<module>.call.<tool>.>`; for a marked verb of a seat held per node
|
||||
`mesh.seat.<seat>.tool.<verb>`, the same followed by `.<node>`, and `mesh.seat.<seat>.call.<verb>.<node>.>`; and for
|
||||
a marked verb of a seat held once for the mesh `mesh.seat.<seat>.tool.<verb>` and `mesh.seat.<seat>.call.<verb>.>`,
|
||||
since its subjects name no node (Q-DL). The bus's deny outranks its allow of `mesh.mod.<module>.tool.>` for each.
|
||||
Going back to an older tool runner on a node leaves the window until its readings update, in which the old deny or
|
||||
none stands; that window is accepted. `Permissions` gains `SubscribeDeny`, written into the user list as the
|
||||
server's subscribe deny, as `PublishDeny` is for publishing; the user list's `User` gains `subscribe_deny` beside
|
||||
`publish_deny`, in its comparison and in the differences the grants part says (Q-SD). An added `subscribe_deny` is a
|
||||
narrowing: the user-list guard lets it pass at once, on the first composition after the release, and counts it as
|
||||
no right lost. The withheld tools and verbs are named in the grants part's read of that account
|
||||
(`mesh-grants.grants_user_list`, a field `withheld` with each subject and "its tool runner does not state
|
||||
approval-mark/1"), and in the node-engine's `composition` of the module, which reads them through a verb `withheld`
|
||||
that the seat `mesh-grants`'s definition gains: class `read`, callers the node-engine, persons and the operator,
|
||||
input `node`, answer each module's withheld subjects and why, with a fixture in the definition; the node-engine's
|
||||
manifest gains `mesh-grants` in `uses`; when the verb does not answer, the composition says "withheld unknown: the
|
||||
grants part does not answer"; never read from the grants part's state (Q-NE). *Why:* ADR 0286 §7, §8, §9: the bus
|
||||
enforces the mark; the runner's status of ADR 0286 §7 is read from the grants part's account read, since a runner
|
||||
that does not honour the mark is a version that cannot say so. A reading is tied to the runner that made it, not to
|
||||
a time, so a clock or a late reading cannot lift the deny (step 0b). A call that reaches no responder because its
|
||||
subject is withheld is answered by the mesh MCP server naming `withheld` and the grants part's read. *How it is
|
||||
checked:* unit, the grants part's composition table: readings lacking the capability, no readings, readings with
|
||||
another runner's fingerprint and readings with none each compose the deny of every subject above, the caller-named
|
||||
ones, a marked seat verb's `.<node>` form and a mesh-wide seat's marked verb among them, each named in `withheld`;
|
||||
readings with the capability and the running fingerprint compose no deny; the release's first composition passes
|
||||
the user-list guard with the deny added; a node gaining `approval-mark/1`, where only its readings change, has the
|
||||
deny removed; unit, the assigner's table: a reading keeps the announcing runner's fingerprint; whole (node-engine),
|
||||
`composition` shows the withheld subjects, and with the grants part down says "withheld unknown"; integration, the
|
||||
grants part's real-bus test: an account composed with the deny is refused delivery of a call on a marked subject
|
||||
while it holds `mesh.mod.<module>.tool.>`, and so is a call sent through `process.Call` on the caller-named
|
||||
subject.
|
||||
7. **The asker's grants are a user's of `operator-channel`.** Because the manifest says `operator-channel` in `uses`
|
||||
(§4), the grants part composes, on the account of the runtime carrying the module, the publish of
|
||||
`mesh.seat.operator-channel.accept.ask.<module>` and `.cancel.<module>`, the hearing of its warrants on
|
||||
`mesh.seat.operator-channel.event.decided.<module>` and the read of the router's record under `<module>.` in the
|
||||
seat's state `asks`, as for every user (ADR 0259 §3). The runner passes the client its own publish and read on that
|
||||
runtime's account (`Client.Publish`, `Client.Record`), never the bundle's. *Why:* ADR 0286 §6. *How it is checked:*
|
||||
unit, a row in the grants part's composition table for a fixture module with a mark.
|
||||
|
||||
## The act, and the first call
|
||||
|
||||
8. **The act is digested with operator-channel's client.** For a call of a marked tool or verb, the tool runner
|
||||
builds an `operatorchannelclient.Act` with `address` (as called), `node` (the node that serves it), `module` and
|
||||
`tool`, or `seat` and `verb`, `build` (the fingerprint the bundle's environment carries, the version it serves, §5; Q-BD), and `arg.<name>` for each
|
||||
argument given, a string as itself and any other value as its JSON in RFC 8785's canonical form; an argument left
|
||||
out is not in the act. Its digest is `operatorchannelclient.ActDigest`. *Why:* ADR 0286 §10, §11; the digest is in
|
||||
mesh-contracts since ADR 0627 §1. The TypeScript twin ADR 0286 named is not built: the tool runner is Go, and no
|
||||
TypeScript asker computes an act. *How it is checked:* unit, rows in `approval` for the act's fields, an omitted
|
||||
argument, a number, an object and a changed build each giving another digest.
|
||||
9. **A call refused without asking.** On a call with no approval for its act, the runner refuses at once, asking
|
||||
nothing, a call whose arguments hold anything `outward.CheckSecret` refuses (mesh-sdk), or whose arguments are more
|
||||
than 16 KiB. *Why:* ADR 0286 §12; mesh-sdk's `outward` is the content rule ADR 0310 §4 moved there. *How it is
|
||||
checked:* unit, two rows of §22's table.
|
||||
10. **One ask, under the module's name.** Otherwise the runner asks through an `operatorchannelclient.Client` whose
|
||||
`Module` is the marked module, whose `State` is the runner's state of §13, and whose new field `KeyPrefix`, which
|
||||
the client in mesh-contracts gains and puts before every key of its own `State` it writes and reads (empty for
|
||||
every asker today), is `<node>.`; never before the router's record it reads through `Record` and `Where`, whose key
|
||||
stays `<module>.<id>` in the seat state `asks`; unless an ask for the same act is
|
||||
open there (§13's `act.` key), in which case it asks nothing new. The ask: `about` the act's digest; `who`
|
||||
`operator`; `expires` 60 minutes after; `on-expiry` "nothing runs; call again to ask anew", since the client's
|
||||
`Check` refuses an authorising ask without it, and no option is taken on expiry (ADR 0286 §22; Q-OC2); two options, `approve` (label Approve) and `decline` (label
|
||||
Decline), both at level `approve`, each binding the act's digest; the headline "Run <tool> on <node>?",
|
||||
"Run <tool>?" when that is longer than the router allows (`HeadlineLength`, 60), and, when that too is longer,
|
||||
the names cut with "…" to fit (Q-HL), ending "Part shown" whenever an argument is not shown
|
||||
whole; the explanation: the first line of the tool's own description, the node, that it was called through the
|
||||
mesh's tools, which cannot tell who called, every argument one per line as "name: value", and the act's
|
||||
fingerprint, its digest's first 24 hexadecimal digits in groups of four. *Why:* ADR 0286 §13, §15, §16, §20 to
|
||||
§23; the client checks the ask (`Check`) before it publishes. *How it is checked:* unit, a row in `approval` holds
|
||||
the published ask's fields against a fixture call, and a second identical call publishes nothing.
|
||||
11. **An argument is shown whole only where it may leave the mesh.** Each argument is shown whole where
|
||||
`outward.Check` passes it, else in its shape with ‹path›, ‹address› or ‹withheld› in place of what it refuses; the
|
||||
arguments are shown whole when they are at most 2000 bytes and 40 lines, otherwise their first 20 lines and 1200
|
||||
bytes, marked "NOT SHOWN IN FULL", and the explanation then says that the whole is read with `mesh_approvals`
|
||||
(§18) on that node, whose fingerprint must be the one on the phone. *Why:* ADR 0286 §17 to §19. *How it is
|
||||
checked:* unit, rows of §10's test with a path, an address and a long argument.
|
||||
12. **The first call answers "Not run", or that the router has not taken it.** The runner waits up to 5 seconds for
|
||||
the router's record of the ask (§14) and answers the call as an error, never as a result. With the record: "Not
|
||||
run: this needs the operator's approval. Asked on <channel> at <time> (ask <id>, fingerprint …); make the same call
|
||||
again within 30 minutes of the approval; the ask expires at <time>." With no record within the 5 seconds: "Not
|
||||
run, asked; not yet taken by the router (ask <id>, fingerprint …); make the same call again later." The ask stays
|
||||
in the seat's stream of accepts, a work queue the router takes it from when it runs, and the `act.` key stays, so
|
||||
the next call reads the router's record of the same ask and asks nothing new. A mark added since the caller last
|
||||
called is named in the answer ("marked since <revision>"). *Why:* ADR 0286 §14, §37; the caller calls again, as
|
||||
the operator chose on 2026-11-11 ("Call again (as designed)") over a held call that runs on the Approve and returns
|
||||
its outcome (issue 417, 2026-10-16). *How it is checked:* unit,
|
||||
rows of §22's table hold both answers; a row with no router holds that a second call asks nothing new; a row holds
|
||||
a headline whose names are cut with "…" to 60 characters (Q-HL).
|
||||
|
||||
## The runner's record, and the same call again
|
||||
|
||||
13. **The runner keeps its asks and claims in a state of its own, per machine.** The tool runner, a foundation part,
|
||||
declares a state `approvals` in its manifest's old `state` section, per machine, with a time to live of 172,800
|
||||
seconds (48 hours), which the grants part creates as it creates a foundation part's states (ADR 0619 §2, §3; the bus
|
||||
module refuses a per-machine state in the new form today). Its size is the old form's fixed one, 64 MiB
|
||||
(`StateMaxBytes`), which §18 reads from the bucket, and a value is at most 256 KiB: an ask whose kept call would pass
|
||||
it fails closed, naming the cause (ADR 0595 §3; Q-CAP). A runner whose membership binds no `approvals` serves every
|
||||
unmarked call as before and refuses only marked ones (§19).
|
||||
Its keys: `<node>.ask.<id>`, the asked call as the client keeps it (the ask, the act, the whole arguments, when
|
||||
asked, whether acted on, and `recorded` once `warranted` answered, §16; the client's `Kept` gains `act`, `arguments`
|
||||
and `recorded` for it, Q-KP), and `<node>.act.<digest>`, the id of the
|
||||
newest ask for that act. The runner on each machine is the one writer of its machine's keys; a key is deleted by
|
||||
its time to live, never by hand. A per-machine state reaches today only the key named for its machine; mesh-sdk's
|
||||
per-machine form and the grants part's `stateAccess` gain the keys below it, `<node>.>`, for every per-machine
|
||||
state, a bundle's membership (`stateIssuedFor`, ADR 0260 §18) too, in the same form: one more subject per
|
||||
per-machine state on each account, and every module's per-machine keys keep their form; mesh-sdk's rebuild of it
|
||||
rides in G's group. That size is the bucket's, shared by every machine's keys. A write the bus refuses (the size
|
||||
reached, or any other) fails closed under §19 and is said in the answer; `mesh_approvals` reports the bucket's fill
|
||||
against its size read from the bucket, saying that both are every machine's together. A run still unrecorded (§16) an hour before its key's 48 hours end is written in the runner's log
|
||||
as an error naming the ask; this is written only while the runner runs, so a machine asleep through that hour keeps
|
||||
only the `call-unanswered` history of §16, which is accepted.
|
||||
*Why:* ADR 0286 §24, §30 kept the waiting call in the runner's record and claimed a warrant in the node's own file,
|
||||
kept 48 hours, by one writer per node; the client built since keeps asks and marks a warrant acted on by
|
||||
compare-and-set in a module's bus state (ADR 0259 §10, ADR 0627 §1), and a per-machine state (ADR 0595) is one
|
||||
writer per machine, so the file is not built. The keys below the machine's own depart from ADR 0260 §16 (a
|
||||
per-machine state reaches only its machine's key), the design's choice following the operator's answers of
|
||||
2026-11-11 (the gate built in G); the key form is the design's choice too. *How it is
|
||||
checked:* unit, the manifest test of `node-tools` holds the old-form entry (per machine, its time to live) and no
|
||||
`approvals` in `requires`; a row of §22's table: an ask over 256 KiB refused naming the cause; another: with no
|
||||
binding, an unmarked call runs and a marked one is refused; the grants part's composition table holds the
|
||||
`<node>.>` reach and no other node's; integration, §22's whole test reads the keys.
|
||||
14. **The same call again is judged on the router's record.** When a call of a marked tool comes and `<node>.act.`
|
||||
names an ask, the runner reads the router's own record of that ask under the module's name (`Client.Where`, the
|
||||
seat state `asks`, key `<module>.<id>`, whose value in operator-channel's definition gains the `warrant` it already
|
||||
carries, so the definition names what `Where` reads; Q-OC1), never a record of its own or the caller's; it composes the ask again from
|
||||
the act and holds the warrant to it with `operatorchannelclient.For` (the same asker and ask, the ask's digest, a
|
||||
person's choice, an option offered, at its level, before it expired); it requires the option to be `approve` and
|
||||
the act to be the one it bound (`operatorchannelclient.Performs`: the same node, build and every argument); and it
|
||||
requires the call within 30 minutes of the choice. It judges the newest ask it keeps for the act. *Why:* ADR 0286
|
||||
§26 to §29, §32. *How it is checked:* unit, rows of §22's table: approved, approved for other arguments, approved
|
||||
31 minutes ago, declined, expired, another asker's warrant.
|
||||
15. **One approval runs one call.** Before it runs, the runner marks `<node>.ask.<id>` acted on by compare-and-set
|
||||
(`Client.Take`); a warrant marked before is refused, so one approval runs one call however often the call is made,
|
||||
and a warrant replayed onto the bus runs nothing. After "already run", "expired" or "approved too long ago" the next
|
||||
identical call asks anew, and the used mark stays. *Why:* ADR 0286 §30, §31, §33, §36. *How it is checked:* unit, a
|
||||
row of §22's table makes the approved call twice and runs it once; two calls at the same moment run it once.
|
||||
16. **The call runs, and its warrant is recorded, across a restart too.** Once the checks pass, the runner runs the
|
||||
call and answers with the tool's own answer and a line naming the approval ("run on the operator's approval via
|
||||
<channel> at <time>, ask <id>"). It then calls `mesh-hand-acts.warranted` with `asker` (the module) and `ask` (the
|
||||
id) as a kept call in mesh-sdk's kept-call form (ADR 0668 §1, §2, §7; design 47 G §351, §425: the verb takes it once
|
||||
per ask), the key that keeps it open being `<node>.ask.<id>` without `recorded`, and no event replacing it; it is not
|
||||
`busretry`, which stops after 30 seconds (Q-KC). On its answer the runner writes `recorded` on `<node>.ask.<id>`. At each start the runner calls `warranted` again for every acted, unrecorded ask its keys hold,
|
||||
within their 48 hours, each named in its `findings-stated`'s `pending` until it is made (design 47 G §384). One
|
||||
unanswered after five minutes is the runner's finding `<node>.<ask id>.call-unanswered` in its state `findings`,
|
||||
said on its stream `node-tools-findings` (design 47 G §383, §387), which the condition keeper hears; it clears when
|
||||
`warranted` answers. *Why:* ADR 0286 §34, §35; the hand-act log is the seat `mesh-hand-acts`, whose `warranted`
|
||||
reads the router's own record, and its "again every ten minutes for a week" is ADR 0668's kept call now, kept
|
||||
across a restart by the `recorded` mark (ADR 0668 §3, ADR 0564). *How it is checked:* integration, §22's whole test
|
||||
reads the hand-act recorded once, also when the runner is restarted after the run and before `warranted` answers;
|
||||
unit, §18's row of `record`.
|
||||
17. **What a later call is told.** For the same call made again before it can run, the caller is told, as an error:
|
||||
still waiting; declined (the same act is not asked again for 10 minutes, and the answer says so); expired,
|
||||
approved too long ago or already run (each asks again, §15); not yet taken by the router (§12); or refused, with the
|
||||
router's words. An ask the router holds back as the asker's fourth open one is still waiting, and
|
||||
`operator-channel.asks` lists it (ADR 0666 §12, as G builds it). *Why:* ADR 0286 §38, §39; ADR 0286 §40's refusal
|
||||
of a fourth open ask was the router's before G, which now offers a fourth and holds the oldest back. *How it is
|
||||
checked:* unit, rows of §22's table.
|
||||
18. **The tools read and steer what was asked.** The mesh MCP server on each machine gains the tool `mesh_approvals`:
|
||||
with no argument, the calls asked on that machine in the last day, newest first, each with its state and
|
||||
fingerprint, read from §13's state, and the marks it applies, each module's marked tools and verbs with their
|
||||
source (its module's entry in the catalog half or its seat's definition) and the catalog revision read; given an ask's id, its whole
|
||||
arguments and the act's digest; given `record` and an ask's id, it asks `warranted` again for that one ask by hand,
|
||||
in the kept-call form of §16, recorded in the hand-act log first; given `cancel` and an ask's id, it takes that open ask back (`Client.Cancel`),
|
||||
recorded first in the hand-act log through `mesh-hand-acts.hand-act` with why, the verb taking who from the caller
|
||||
it proves (Q-HA), and refused when the record
|
||||
cannot be written. `mesh_describe` answers, for a marked tool or verb, `needs-approval` with its source. *Why:*
|
||||
ADR 0286 §25 (the runner's `approvals`), named as the mesh MCP server's own tools are; a tool reads everything a
|
||||
module holds and does by hand what it does by itself (the operator's instruction of 2026-10-25). *How it is
|
||||
checked:* unit, rows in the mesh MCP server's tool table test: the list, the marks with their sources, an ask read
|
||||
whole, a `record` that asks `warranted` again for one ask, a cancel recorded and one refused without a record, and
|
||||
`mesh_describe` of a marked tool.
|
||||
|
||||
## Failing closed
|
||||
|
||||
19. **Nothing runs when a part of the check is missing, and a stuck gate is said.** A call of a marked tool is
|
||||
refused, runs nothing and says why when: no module holds `operator-channel` (read from the assigner's first view
|
||||
`mesh-delivery-assigner.views.seat-holders`, which the tool runner consumes; the seat list names no holders); the
|
||||
served entry of the module cannot be read (§5), which refuses every marked call on that runtime; the
|
||||
router refuses the ask; the ask cannot be published; the router's record cannot be read; §13's state cannot be read
|
||||
or written, in which case no marked call runs on that machine until it can, the failed read or write being made
|
||||
again in the kept-call form (ADR 0668 §7; design 47 G §351), kept open by the failing key, until it succeeds; or the marks are not yet read. Before
|
||||
the first view of the catalog half is read, a call waits for it within its 5 seconds and is then judged, asking
|
||||
nothing while the marks are unread; still unread, it is refused, naming `mesh_first_views`. When §13's state has
|
||||
been unreadable or unwritable for five minutes, the runner raises its finding `<node>.approvals.approvals-unreadable`
|
||||
(said on `node-tools-findings` as §16's), and clears it when the kept call succeeds; its `findings-stated` at a start
|
||||
judges it anew from a read of the state (design 47 G §384). Each refusal is
|
||||
written in the runner's log with the ask's id and fingerprint, never the arguments. *Why:* ADR 0286 §40, §42; a
|
||||
gate that refuses every call for a reason nobody hears is a silent fault, and the finding and its five minutes are
|
||||
the design's choice. *How it is checked:* unit, one row of §22's table per case, the holder read from a fixture
|
||||
`views.seat-holders` with none and with one; a row where the served entry cannot be read and every marked call on
|
||||
that runtime is refused; and a row where the state fails, the finding is raised after five
|
||||
minutes, the state recovers with no further call, and the finding clears.
|
||||
20. **The terminal is not exempt.** A marked tool called through `mesh-cli` or any caller asks as every call does: the
|
||||
runner cannot tell who called. *Why:* ADR 0286 §41. The gate wraps every handler the runner serves, a bundle's
|
||||
tools and seat verbs and the runner's own tools beside them (the ledger repairs) alike (Q-LR); the act, the ask,
|
||||
the check, the claim, the recording and the refusals of §8 to §19 are all in `node-tools/internal/approval` (Q-PK).
|
||||
*How it is checked:* the code review reads that the gate sits on every handler, before the bundle, and that no path
|
||||
to a bundle or a runner's own tool passes around it; unit, a row of §22's table: a runner's own tool marked in a
|
||||
fixture is refused unasked, never run.
|
||||
|
||||
## Marked first
|
||||
|
||||
21. **`node-login-shell.execute` is marked in its seat's definition.** mesh-contracts' `seats/node-login-shell/seat.json`
|
||||
marks the verb `execute` with `needs-approval`; the zsh module's manifest (mesh-catalog `modules/zsh`), which holds
|
||||
the seat, gains `operator-channel` in `uses`. The control-node keeps withholding `execute`, and whether it serves
|
||||
it again under the mark stays the operator's setting. *Why:* ADR 0286 §44, §45: the reads it carried have their
|
||||
tools on every node (the seat `node-reads` of issue 418, resolved 2026-10-12, which `node-files` replaces whole, ADR
|
||||
0394 §9). *How it is checked:* live, as the acceptance says.
|
||||
- **The forge's two acts on a trunk's protection are marked.** The forge module (mesh-catalog `modules/gitea`)
|
||||
gains a tool `gitea_branch_protection_loosen`, marked in its `needs-approval`, taking `owner`, `repo`, `branch`,
|
||||
`act` (`remove-protection` or `allow-force-push`) and `why` (Q-FG), which alone performs the two acts
|
||||
`gitea_branch_protection_set_full` refuses today: removing a trunk's protection, and allowing a force-push onto
|
||||
a trunk; `gitea_branch_protection_set_full` keeps refusing both and its refusal names the new tool. The manifest
|
||||
gains `operator-channel` in `uses`. No other tool or verb is marked in this milestone. *Why:* the forge's own
|
||||
refusal says these two acts are the operator's and waited for this mark (issue 417, 2026-10-28); a mark is on a
|
||||
whole tool (ADR 0286 §1), so the acts move to a tool of their own, as ADR 0286 §48 splits a tool into an unmarked
|
||||
and a marked form, and tightening a protection stays unasked; the operator, 2026-11-11: "Mark them in G". The
|
||||
principle: loosening a trunk's protection runs only on the operator's own warrant for that one act, never on a
|
||||
session's word. Its bound: a process that reads the forge module's own credential speaks to the forge directly
|
||||
and goes around it, the same known limit as issue 1966 note 193; the prepare reads that credential's mode and
|
||||
owner on the forge's node with `<node>/node-files.stat` (§21a) and writes them in the milestone file. *How it
|
||||
is checked:* the forge module's tool test: `set_full` refuses both acts naming the new tool; the new tool
|
||||
performs each; and live, as the acceptance says.
|
||||
21a. **`node-files.stat` answers a mode under a directory the account cannot enter.** The seat `node-files`' holder
|
||||
(mesh-catalog `modules/mesh-files`) answers `stat` of a path its account cannot reach through `sudo -n` with its
|
||||
fixed `/usr/bin/stat`, as its writes already go (ADR 0394 §2), answering the path's mode, owner, group and kind and
|
||||
nothing of its content; the secret places ADR 0394 §7 names are stated, never read. Its README's "not yet through
|
||||
sudo" loses `stat`. *Why:* the prepare could not read the forge credential's mode (step 0b); ADR 0394 §2 has the
|
||||
holder use sudo where it must. The lead's handover named the retired `node-reads` (ADR 0394 §9), whose successor this
|
||||
is. *How it is checked:* unit, a row in mesh-files' `Stat` test: a file under a directory of mode 0700 owned by
|
||||
another account answers its mode and owner through sudo, with no content.
|
||||
|
||||
## Tests, delivery and going back
|
||||
|
||||
22. **The tests.** The tool runner's tests in `node-tools/internal/approval` are one table-driven unit test of the
|
||||
refusals and answers (§9, §12, §14, §15, §17, §19) and one of the marks (§5), on the tool runner's test bus
|
||||
(`meshtest`), the router's record and the warrant written there as the router writes them; and one integration
|
||||
test that starts the runner against the real bus, with no store, with a fixture marked module: a call, the
|
||||
operator's Approve written as the router's record, the call again, the run, the hand-act, `mesh_approvals`, and a
|
||||
restart between the run and `warranted`'s answer. The client's `KeyPrefix` (§10) has a row in mesh-contracts'
|
||||
client test: every key of its own `State` written and read carries it, an empty prefix writes today's keys, and
|
||||
with a prefix set `Where` still reads `<module>.<id>`. The mesh MCP server's `TestMeshWaitIsAnnouncedAmongSeven` counts eight
|
||||
tools with `mesh_approvals` (Q-T). In milestone G a module ends with no more tests than it starts with (design 47 G §419, §626): these tests replace
|
||||
existing ones, and every test deleted counts. *Why:* the operator's test
|
||||
budget (one whole test over many small ones). *How it is checked:* the check counts each module's tests against
|
||||
its budget.
|
||||
23. **In G's delivery group, ordered by the module graph.** The members are pushed and checked in design 47 milestone
|
||||
G's group, with design 34's milestone every-event. The orders are the module graph's: the indexer's manifest
|
||||
validator and catalog graph entry that read `needs-approval` before any manifest or seat definition carrying it (a
|
||||
reader edge, design 47 G §89, whose list covers `needs-approval`; Q-RE); and the grants part that composes the deny of §6 before the seat definition marking
|
||||
`execute` (an edge of the seat definition's readers), so no machine serves `execute` unasked between the two; and
|
||||
the grants part serving `withheld` before a node-engine calls it (a served-verb edge, §6).
|
||||
*Why:* the operator, 2026-11-11, "Build the gate in G"; the operator, 2026-11-03, a change is designed as it should
|
||||
be and its delivery is the delivery system's. *How it is checked:* the delivery plan names the three edges.
|
||||
24. **Going back is going forward.** A fault met after the delivery is fixed by a new change delivered the same way.
|
||||
Reverting instead reverts G's whole group in the reverse of §23's order: the seat definition's mark first, then the
|
||||
grants part, then the validator. The `approvals` state is then declared by no running module: it is an orphan
|
||||
`orphans` lists, its keys aging out within 48 hours, dropped through ADR 0595's `drop` on the operator's word.
|
||||
*Why:* design 47 G's revert rule; ADR 0595 §4, §5. *How it is checked:* the delivery plan of a revert names the
|
||||
order.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- **A marked call is asked on the phone, does nothing, and runs once on the Approve** (live, read by tool call):
|
||||
`node-login-shell.execute` on a node other than the control-node answers "Not run", the ask is on
|
||||
`operator-channel.asks`, and after the operator's Approve the same call runs once, a third identical call is refused
|
||||
as already run, and `mesh-hand-acts.hand-acts` names the warranted act (§10, §12, §14 to §16, §21).
|
||||
- **The forge's loosening is asked, and the protection is put back** (live, read by tool call): on a scratch repository
|
||||
`acceptance-approval-mark`, created by the acceptance through the forge's `gitea_create_repo` and kept afterwards,
|
||||
never deleted, with full protection set on its trunk by `gitea_branch_protection_set_full`,
|
||||
`gitea_branch_protection_loosen` removing the trunk's protection answers "Not run" and asks on the phone; after the
|
||||
Approve the same call runs once; then `gitea_branch_protection_set_full` sets the full protection again and
|
||||
`gitea_branch_protection_get` reads it back as it was (§21).
|
||||
- **A dropped mark still asks while the old bundle runs** (live, read by tool call): a delivery that drops a module's
|
||||
mark asks a call of that tool until the node runs the new bundle, read with `mesh_approvals` (§5).
|
||||
- **No machine serves `execute` unasked** (live, read by tool call): before this acceptance passes, on every node
|
||||
`execute` is either asked as above or absent, read with each node's `node-engine.composition` and
|
||||
`mesh-grants.grants_user_list`'s `withheld` (§6).
|
||||
- **A marked wait as the operator sees it** (live): design 34 every-event's acceptance of a wait on a mail event.
|
||||
- **The mark cannot be lost on the way** (unit): §5's and §8's tables pass; a bundle carries no mark.
|
||||
- **It fails closed** (unit): §19's rows pass.
|
||||
- **The validator refuses a mark nothing enforces** (unit): §4's rows pass.
|
||||
@@ -1,9 +1,12 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog]
|
||||
updated: 2026-10-13
|
||||
code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog, mesh-contracts, mesh-delivery]
|
||||
updated: 2026-11-11
|
||||
decisions:
|
||||
- "ADR 0668"
|
||||
- "ADR 0667"
|
||||
- "ADR 0666"
|
||||
- "ADR 0347"
|
||||
- "ADR 0345"
|
||||
- "ADR 0344"
|
||||
@@ -15,6 +18,11 @@ decisions:
|
||||
- "ADR 0253"
|
||||
- "ADR 0234"
|
||||
- "ADR 0227"
|
||||
- "ADR 0627"
|
||||
- "ADR 0597"
|
||||
- "ADR 0595"
|
||||
- "ADR 0310"
|
||||
- "ADR 0268"
|
||||
---
|
||||
|
||||
# 46 — The conversation with the operator
|
||||
@@ -221,7 +229,20 @@ when the title and the note alone do not fit, and says that too.
|
||||
|
||||
### Limits, batching, history
|
||||
|
||||
- An asker holds **at most three open asks**; a fourth is refused in words.
|
||||
- An asker holds **at most three open asks**; a fourth is refused in words. *Amended 2026-11-10, ADR 0666 §12, from the
|
||||
delivery of design 47's milestone G:* **no ask is hidden, and the newest is never held back.** An asker still has at
|
||||
most three asks offered at once, but a fourth is offered, not refused: the oldest of its offered asks is held back
|
||||
instead, kept open with its answer tickets withdrawn, its message edited to say it waits behind newer asks of the
|
||||
same asker, and offered again, with new tickets, as soon as one of the three ends. An ask with the same `about` as an
|
||||
open one of the same asker replaces it (§10), so a held-back ask is never one a newer ask already stands for. `asks`
|
||||
lists each asker's held-back asks with since when, and the router's tool `ask-again` offers one now, holding back the
|
||||
oldest offered one in its place. A Decline ends an ask like any answer, and its asker may ask again at once: the
|
||||
pipeline runner's verb `ask-again` asks a declined `approval` step so (design 47, *Approval is a step*). Why the
|
||||
oldest: the newest is what the asker needs now, and the oldest has had the operator's eye longest; holding it back
|
||||
edits its message rather than removing it, so the operator still sees it waits. *How it is checked:* the router's
|
||||
whole-module test offers a fourth ask and holds back the oldest, offers the held-back one again when one ends,
|
||||
replaces an open ask by a newer one with the same `about`, and offers an ask after a Decline; live, a fourth ask and
|
||||
a declined approval each reach the operator (design 47, test 71).
|
||||
- Asks count against the router's hourly cap, as messages do; answers do not.
|
||||
- Asks to one channel within the burst window go out together under a heading ("3 questions waiting"),
|
||||
each as its own message, answerable and editable alone.
|
||||
@@ -424,7 +445,8 @@ what it does in plain words, its assurance level (`acknowledge`, `approve`, `des
|
||||
answer (`operator`); an expiry; what happens on expiry, in words; and optionally what it is about (a
|
||||
condition's key), so a newer ask about it replaces the older. It is refused at once, on its asker's
|
||||
`decided` subject, when its words break a rule, when no ready channel can carry one of its options' levels,
|
||||
or when the asker already has three open.
|
||||
or, until the delivery of design 47's milestone G, when the asker already has three open; from that delivery a fourth
|
||||
is offered and the asker's oldest held back (§6, *Limits, batching, history*; ADR 0666 §12).
|
||||
|
||||
**A warrant** carries the ask's id and asker, the option, `by` ("the operator, via telegram, user id
|
||||
verified"), the channel module, the proofs, the level met and when. No secret. An ask that ends without one
|
||||
@@ -545,43 +567,27 @@ said on the away channel, with its why.
|
||||
|
||||
### A call that needs the operator's approval
|
||||
|
||||
*Added 2026-10-10, ADR 0286.*
|
||||
A module's manifest marks a tool, or a seat verb it serves, as **`needs-approval`**; a seat's definition may mark
|
||||
a verb for every holder. The controller issues the mark in the runner's membership, never from the bundle, and
|
||||
withholds a marked tool from a runner that does not declare `approval-mark/1`, by leaving its subject out of that
|
||||
runner's bus permissions.
|
||||
*Added 2026-10-10, ADR 0286; amended 2026-11-11 to the mesh as built and as design 47's milestone G leaves it.*
|
||||
A module's manifest marks a tool, or a seat verb it serves, in **`needs-approval`**; a seat's definition may mark
|
||||
a verb for every holder. The tool runner reads the marks from the catalog half on the bus (seat `mesh-catalog`), never from
|
||||
the bundle, and on a machine whose tool runner does not state that it honours the mark the grants part withholds every
|
||||
marked subject from its account.
|
||||
|
||||
```
|
||||
agent ──call──► tool runner (serving node) ──ask, under the module's name──► router ──► Telegram
|
||||
agent ──call──► tool runner (serving node) ──ask, under the module's name──► seat operator-channel ──► Telegram
|
||||
▲ │ "Not run: asked, ask <id>" │ Approve
|
||||
│ ▼ ▼
|
||||
└──same call──► runner reads the router's record ◄────────────── warrant (decided.<module>)
|
||||
│ Warrant.For + Option.Performs on the act; within 30 min; claimed once
|
||||
│ For + Performs on the act; within 30 min; claimed once in its own state
|
||||
▼
|
||||
runs the call once ──► mesh-controller.warranted (hand-act log)
|
||||
runs the call once ──► seat mesh-hand-acts, warranted
|
||||
```
|
||||
|
||||
- **The act** is the address, the node, the module and tool, the bundle's build and every argument given, digested
|
||||
with the shared library's `ActDigest` (in Go and TypeScript, one set of vectors).
|
||||
- **The first call asks and does nothing.** One ask per act (its `about` is the act's digest): "Run <tool> on
|
||||
<node>?", the tool's first line, every argument whole where it may leave the mesh or in its shape with
|
||||
‹path›, ‹address› or ‹withheld› and "Part shown" (whole when at most 2000 bytes and 40 lines, else the
|
||||
first 20 lines and 1200 bytes), the fingerprint, and where the whole is read (the runner's
|
||||
`approvals` on that node). Approve and Decline, both at approve; expiry 60 minutes; never a default. The call
|
||||
answers as an error: "Not run", with the ask's id, that the call runs within 30 minutes of the approval, and
|
||||
when the ask expires.
|
||||
- **The same call again** runs once on Approve, read from the router's record, within 30 minutes of the choice,
|
||||
claimed in the node's own file (kept 48 hours, one writer per node); the runner judges the newest ask it keeps
|
||||
for the act, and after "already run", "expired" or "approved too long ago" the next identical call asks anew;
|
||||
then `warranted` records the hand-act. Anything else is
|
||||
refused and said: waiting, declined (not asked again for 10 minutes), expired, already run, or the router's
|
||||
refusal.
|
||||
- **Fail closed:** a secret's shape in an argument or more than 16 KiB of arguments, no operator channel, a
|
||||
fourth open ask, an unpublished ask, an unreadable router record, node file or mark: nothing runs.
|
||||
- **The controller** enforces the same mark on its own verbs, asking as `mesh-controller`.
|
||||
- **Marked first:** `node-login-shell.execute` on every node once the reads it carried have tools (issue 418).
|
||||
`mesh-delivery.release` waits until a warrant the controller holds passes the mark of a verb it calls (issue
|
||||
417), so a Release on a condition is not asked twice.
|
||||
The first call asks and does nothing; the same call made again within 30 minutes of the operator's Approve runs once;
|
||||
everything else is refused and said, and anything the check cannot read runs nothing. `node-login-shell.execute` is
|
||||
marked first. Every line of it is in milestone 7's part,
|
||||
[`46-milestones/7-approval-mark.md`](46-milestones/7-approval-mark.md), built and delivered with design 47's milestone
|
||||
G (the operator, 2026-11-11).
|
||||
|
||||
### The hand-act
|
||||
|
||||
@@ -696,12 +702,13 @@ Each phase ends at its own *done when*. Owning repositories from [`repos.md`](..
|
||||
|---|---|---|---|
|
||||
| **1 — Fix the first holder in place** | D1–D4 in the output seat's holder and the watcher, then D5–D10; no change of shape. The operator then makes the two bots and the mesh is given their values through the controller | mesh-catalog | the holder's tests for D1–D4 pass; a live test message reaches the phone from both the holder and the watcher |
|
||||
| **2 — The desk's actions** | `node-notifier.send` gains actions; the dunst holder emits the chosen one; the launcher's prompt returns typed text; the screen-lock holder emits lock and idle changes | mesh-catalog | a notification with two actions returns the chosen token as an event; locking emits an event |
|
||||
| **3 — The seats, and the router** | the kinded bench in the seat set; `kind` and `capabilities` on a claim; `channel-capabilities/1` and its contract tests; publishing on a seat's event subjects and its grant; `channel` and `intake` seats; the output seat's holder becomes the router and holds `channel/desktop` and `intake/desktop`; presence as current state; the operator's identity list and identity check in the controller, the router's `trusted` stamping and untrusted input (§4); the content rule allowing machine names, references and `detail`, refusals naming the offending part (§9) | mesh-controller (seat set, claim fields, registration refusals, grants, the identity list and check); mesh-sdk and mesh-tools (seat-event publishing in the shared library and the tool runner); mesh-catalog (the seats' definitions, the router) | the catalogue refuses an unknown capability and a second holder of one kind; a message is routed to the desk by capability and context; an envelope from an identity not on the list is re-emitted untrusted; a path is refused to its sender by name, and travels as a reference the desk opens |
|
||||
| **3 — The seats, and the router** | the kinded bench in the seat list; `kind` and `capabilities` on a claim; `channel-capabilities/1` and its contract tests; publishing on a seat's event subjects and its grant; `channel` and `intake` seats; the output seat's holder becomes the router and holds `channel/desktop` and `intake/desktop`; presence as current state; the operator's identity list and identity check in the controller, the router's `trusted` stamping and untrusted input (§4); the content rule allowing machine names, references and `detail`, refusals naming the offending part (§9) | mesh-controller (seat list, claim fields, registration refusals, grants, the identity list and check); mesh-sdk and mesh-tools (seat-event publishing in the shared library and the tool runner); mesh-catalog (the seats' definitions, the router) | the catalog refuses an unknown capability and a second holder of one kind; a message is routed to the desk by capability and context; an envelope from an identity not on the list is re-emitted untrusted; a path is refused to its sender by name, and travels as a reference the desk opens |
|
||||
| **4 — Asks, and operator messages** | `ask`, `ask cancel`, `asks`, `answer`; the events; kinds, life, limits, batching, history; escalation; addressing, the register of agents with kept messages, the responder; the untrusted-input rule in every agent module's instructions (§5) | mesh-catalog | the router tests of ADR 0234 pass; live: an agent's question answered at the desk, and with the desk locked, on the phone; `@mesh status` answered; a message to a stopped agent kept and said |
|
||||
| **5a — Asks on the bus** (ADR 0259) | the kinded bench, caller-named verbs, `proof` subjects and the records users read (controller); a bundle's seat traffic, compare-and-set state and its own account (tool runner); the asker's and channel's types (shared library); the router's asks, tokens, identities, judging, warrants, expiry and linking with its hour (messenger); `telegram` and `desk-channel`; the controller asking for its conditions' actions and acting on warrants | mesh-controller, mesh-tools, mesh-sdk, mesh-catalog | a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified"; a desk Silence goes through an ask |
|
||||
| **5 — Authorise, with TOTP** | `authorises` in the verb table; the router's checks for the code; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the TOTP seed as the router's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced |
|
||||
| **6 — Telegram live** | the `telegram` module (built in 5a) with replies and forced-reply codes, under its own account on the control node; the operator's two bots configured; the watcher assigned off the control node; the dead-man ping | mesh-catalog | the self-check says the away channel carries every tier; rehearsals: an approve and a destroy answered on the phone, with the hand-acts read |
|
||||
| **7 — A call that needs approval** (ADR 0286) | `needs-approval` in the manifest and the seat set, issued in the membership, withheld from a runner without `approval-mark/1` in its bus permissions (controller); the act's digest in TypeScript beside Go, with shared vectors (shared library); the check, the ask, the runner's `approvals`, the node's used file and `warranted` (tool runner); the controller's own verbs; `execute` marked (the controller's seat set) once issue 418's reads exist | mesh-controller, mesh-sdk, mesh-tools, mesh-catalog | a marked call on a node is asked on the phone, does nothing, runs once when made again after Approve, is refused the third time and asked again, and `hand-acts` shows the warrant |
|
||||
| **7 — A call that needs approval** (ADR 0286), delivered with design 47's milestone G | the mark in the manifest and the seat definition, read by the tool runner from the bus; the manifest validator's refusals; the capability `approval-mark/1`; the act, the ask, the check, the claim in the runner's own state, `warranted` and `mesh_approvals`; `execute` marked. Its part is [`46-milestones/7-approval-mark.md`](46-milestones/7-approval-mark.md) | mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-catalog | its part's acceptance |
|
||||
| **8 — No ask hidden** (ADR 0666 §12), delivered with design 47's milestone G | a fourth ask offered and the asker's oldest held back, offered again when one ends; `asks` lists the held-back asks; the router's tool `ask-again`; the pipeline runner's `approval` asks and their `about` | mesh-catalog (the router) | a fourth ask and a declined approval each reach the operator; `asks` shows the held-back one |
|
||||
|
||||
**Switching phase 5a on: the operator's steps, in order** (amended 2026-10-09, ADR 0259). Each line is typed with
|
||||
`nox` (`mesh-cli`) on the control-node, from the operator's own account, which is the controller's terminal
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,419 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
design: 47
|
||||
milestone: G
|
||||
updated: 2026-11-11
|
||||
decisions:
|
||||
- "ADR 0668"
|
||||
- "ADR 0667"
|
||||
- "ADR 0666"
|
||||
- "ADR 0664"
|
||||
- "ADR 0661"
|
||||
- "ADR 0656"
|
||||
- "ADR 0633"
|
||||
- "ADR 0619"
|
||||
- "ADR 0614"
|
||||
- "ADR 0596"
|
||||
- "ADR 0595"
|
||||
- "ADR 0593"
|
||||
- "ADR 0584"
|
||||
- "ADR 0573"
|
||||
- "ADR 0567"
|
||||
- "ADR 0564"
|
||||
- "ADR 0548"
|
||||
- "ADR 0448"
|
||||
- "ADR 0429"
|
||||
- "ADR 0398"
|
||||
- "ADR 0363"
|
||||
- "ADR 0283"
|
||||
---
|
||||
|
||||
# 47, milestone G: pipelines per module: registration, watched branches, steps and policy, what holds a delivery, approval, groups, dependencies, a publish
|
||||
|
||||
One file of milestone G's part of [design 47](../47-delivery-from-commit-to-delivered.md); the part opens at
|
||||
[G.md](G.md). Its design lines are numbered across every file of the part and cited as "design 47 G §n".
|
||||
|
||||
## Pipelines per module
|
||||
|
||||
*ADR 0666, ADR 0667; ADR 0595, ADR 0596, ADR 0448 §2, §4, ADR 0548 §5, ADR 0564 §2, §4, ADR 0283, ADR 0656 §4, §6, ADR 0633 §5,
|
||||
ADR 0664 §4, ADR 0398 §2, ADR 0593 §9; research 086, Direction.*
|
||||
|
||||
§1 From milestone G's delivery. A change of any module, the delivery system's own parts included, goes from a commit to
|
||||
the machines through that module's **pipeline**: the pipeline steps its **policy** names, run for each commit or group
|
||||
on each of the module's **watched branches** by the seat `mesh-pipeline-runner`, ordered by the catalog graph's edges
|
||||
between artifacts, with no hand act (ADR 0666 §1). Milestone G is the one milestone that builds it, beside the two
|
||||
graphs, the revision view and the delivery record (ADR 0667 §1). Until G's delivery the sections this one replaces
|
||||
(*Checks*, *A try*, *The Approve and the merge*, the state table and the trunk-only want) hold as written, and they
|
||||
check and deliver milestone G itself (ADR 0667 §2); each of them says so in a line of its own.
|
||||
|
||||
```
|
||||
a session ──merge-pull (seat git)──► forge ──commit on a watched branch──► forge bridge ──► indexer (mesh-catalog)
|
||||
│ registrations; a catalog graph
|
||||
│ per watched branch; the revision
|
||||
│ validated before take-in
|
||||
▼ take-in.taken
|
||||
pipeline runner (mesh-pipeline-runner) ── asks each pipeline step on the serving seat's work queue, hears step-ended:
|
||||
│ lint, unit tests, integration tests ──► the build seats (node-build-go, node-build-node, node-build-image)
|
||||
│ composition ──────────────────────────► the assigner (mesh-wants)
|
||||
│ code review ──────────────────────────► the runner's own verb `review`, from the reviewer's bus account
|
||||
│ approval ─────────────────────────────► operator-channel: accepted kind ask, then decided
|
||||
▼ pipeline-ended (every step's result), keyed by source revision
|
||||
assigner ──► wants from a revision whose required steps passed, on the watched branch a placement chose
|
||||
gate ──► the delivery record keeps the step and pipeline events
|
||||
```
|
||||
|
||||
### A module's registration
|
||||
|
||||
- §2 **What it holds.** The seat `mesh-catalog` keeps each module's **registration**: its git origin, the path of its
|
||||
manifest, its watched branches and its policy, on the bus, every version with who changed it and the warrant of the
|
||||
operator's Approve (ADR 0666 §2). The indexer is a foundation part, so the registrations are kept in its old sections
|
||||
(ADR 0619 §1 to §3, ADR 0666 §4): one key per module in its old `state` section `registrations`, and every version on
|
||||
its old `streams` section's stream `registration-history`, kept with no age limit.
|
||||
The catalog graph is derived from the registrations and the manifests they point to; the mesh knows modules, and a
|
||||
repository only as a module's git origin.
|
||||
- §3 **Changing one is a trusted act.** The seat's verbs `register` (a module whole), `policy` (one module's step
|
||||
statuses, per artifact), `watch` and `unwatch` (add or remove a watched branch) each raise an authorising ask of tier
|
||||
approve through the seat `operator-channel` (design 46 §10), naming every value the change moves, one line each
|
||||
("`mesh-grants:program` unit tests: `required` → `bypass`"), and write the version only on its warrant; a Decline or
|
||||
an expiry writes nothing (ADR 0666 §2). Until a record binds each forge account to its bus identity, `register` and
|
||||
`policy` refuse code review as a `required` step, naming why, so no review gates while its author cannot be told
|
||||
(ADR 0666 §22; issue 1966, note 157). A manifest moved to another path is the one manifest change that changes the
|
||||
registration; any other manifest change is a commit, judged at its revision.
|
||||
- §4 **Said and read.** Each written change is said as the seat's event `registration-changed` (the module, the version,
|
||||
what moved); the verb `registrations` answers every registration, or one with its history. The indexer derives the
|
||||
modules it keeps from them, and the forge bridge its watched branches, each by a first view of `registrations` and
|
||||
then that event, never by reading the ledger (ADR 0666 §3, ADR 0596).
|
||||
- §5 **An unprotected branch is refused.** `register` and `watch` read a branch's protection through the seat `git`'s read
|
||||
verb `branch` (its head, and whether a rule refuses a direct push and admits only merges), and refuse a watched branch
|
||||
the forge does not protect, naming it (ADR 0666 §3).
|
||||
- §6 **The foundation parts are the manifest validator's list.** The manifest validator's list (ADR 0619 §3) holds
|
||||
exactly the foundation parts, ADR 0619 §1's, the runner, which joins them (ADR 0666 §4), and the forge and image
|
||||
registry modules (§584); the checker leaves the
|
||||
list when it is removed (*How milestone G itself is checked and delivered*). A registration holds no foundation
|
||||
value. How it is checked: a unit test of the manifest validator refuses a template in the manifest of every part on
|
||||
its list, the runner's included, and judges every other part's manifest as any part's.
|
||||
- §7 **No part fixes a name: a guideline the code review reads.** (§507) Every branch, repository and module name a part
|
||||
needs is read from the registrations and the catalog graph, never fixed in its code. This is a guideline, not a
|
||||
step: the code review reads it, and no lint step scans for names (the operator, 2026-11-11 about 00:05 CET: the scan was a flaky check; where
|
||||
guidelines live is research 087's). It departs from ADR 0666 §5's refusal by the lint step, by the operator's choice,
|
||||
and a later record brings it in line. The names G's own work removes (about 39 in 9 parts, found on 2026-11-09,
|
||||
research 086, Findings) are removed by its subtasks, each naming the name it removes.
|
||||
- §8 **The first registrations are one ask.** At its first start on G's build the indexer, holding no registration,
|
||||
proposes one registration for every module its trunk revision holds, from what it took in (the origin and path of
|
||||
each), its watched branch the repository's default branch read through the seat `git`, and the statuses the check
|
||||
judged until then: lint, unit tests, integration tests and composition `required`, code review `optional`, approval
|
||||
`bypass`. Each step the artifact's build seat answers `steps` with none is `bypass` instead, so no module is held by
|
||||
a `required` step it has nothing for; the indexer asks `steps` of each build seat as a kept call (ADR 0668 §1, §2),
|
||||
and the seed waits until every build seat has answered, never reading an unanswered seat as "none": a seat still
|
||||
unanswered after five minutes is the indexer's `call-unanswered`, naming the seat (ADR 0668 §3), and the seat's new
|
||||
build serving `steps` is an edge of kind 6 before the seed. A module whose default branch the forge does not protect
|
||||
is left out of the seed and named in the ask, as `register` would refuse it (ADR 0666 §3). It raises the rest as one
|
||||
ask naming every registration, as the seat `mesh-module-assigner`'s `adopt` proposes placement whole (ADR 0567).
|
||||
While the ask is open the indexer's finding `seed-waits`, naming the ask, is raised by the condition keeper as
|
||||
waiting on the operator (ADR 0283) and cleared by the warrant; a Decline or an expiry leaves the seed unwritten, says
|
||||
so and keeps `seed-waits`, naming `indexer_seed_again`, which raises the ask again, recorded in the hand-act log.
|
||||
Until that Approve the runner starts no delivering pipeline and says why, so the operator is told of this one ask
|
||||
before G's delivery starts. Why: the design's choices under ADR 0666 §3, §7 and ADR 0668 (round 1 of the review by
|
||||
architectural domain, 2026-11-10). How it is checked: whole (indexer), a build seat that never answers `steps`
|
||||
holds the seed and raises `call-unanswered`, one that answers none gives `bypass`; an unprotected default branch is
|
||||
left out and named; `seed-waits` is raised while the ask is open and cleared by the warrant.
|
||||
- §9 **Kept for the life of the mesh**, never derived again (research 086, choice 25): the history stream's cap is 64 MiB.
|
||||
About 120 registrations of about 1 KiB, and an estimated change a week each (a guess, not a measure), grow about 6 MiB
|
||||
a year, so the cap holds about ten years; past 80 % the indexer says `filling`, as every store of *Two graphs, one
|
||||
source of truth* does.
|
||||
- §10 **How it is checked.** Whole (indexer): an unprotected watched branch is refused, naming it; a policy change without
|
||||
the warrant writes nothing, and a declined one writes nothing; a change from `required` to `bypass` asks, showing
|
||||
that line; `registration-changed` is said and `registrations` answers the history; the first start proposes the seed
|
||||
as one ask, and `indexer_seed_again` asks it again after a Decline and after an expiry; a policy making code review
|
||||
`required` is refused. Tests 75, 68 and 83.
|
||||
|
||||
### Watched branches and the catalog graph per branch
|
||||
|
||||
- §11 **Only a watched branch starts a pipeline.** (§509) The forge bridge hears every commit of every watched branch the
|
||||
registrations name, through the seat `git`'s event `branch-moved` (a branch's new head, from the forge's push
|
||||
webhook, which a merge and an admitted push alike say), and takes the commit from the forge's own record by the seat's
|
||||
verb `branch`, never from the event's body (ADR 0666 §6). One event for every way a branch moves, because a merge is
|
||||
not the only one. `branch-moved` is kept on the seat's stream `SEAT_git_branch-moved`, which the bridge hears through
|
||||
a durable consumer, so a move said while the bridge is down is heard when it starts; at its start it also reads each
|
||||
watched branch's head the same way.
|
||||
- §12 **A head with no pipeline is a finding.** (§497) A watched branch's head that starts no pipeline within five minutes is the
|
||||
watchman's finding `pipeline-stands`, naming the branch and its head, raised by the condition keeper (ADR 0564 §2),
|
||||
unless it waits as progress not owed: a member of a gathered group is governed by ADR 0633 §5's 30-minute bound, and
|
||||
a merge behind a pending revision of the same branch by its 60-minute bound (ADR 0666 §24). The finding clears on the
|
||||
runner's `pipeline-started` for that head.
|
||||
- §13 **The catalog graph per watched branch.** The indexer keeps the catalog graph once per watched branch, from that
|
||||
branch's manifests at each source revision (ADR 0666 §6): each revision names its branch, and the stores, caps and
|
||||
pins of *Two graphs, one source of truth* hold per branch.
|
||||
- §14 **A placement names the branch.** Each module of a placement key names the watched branch its node runs, by default the
|
||||
registration's first, set by a placement proposal like every other placement change (ADR 0567, ADR 0666 §6). The
|
||||
assigner computes each node's entries from the catalog graph of the branch its placement names, so a module watched on
|
||||
`main` and `beta` runs `beta` only where a placement chose it.
|
||||
- §15 **A source revision per watched branch.** The gate keeps one source revision per watched branch, `source.<branch>`,
|
||||
in place of `source.main` (ADR 0429 §11 to §13 as ADR 0666 §6 changes them), in the old `state` section `source`
|
||||
already holds, its key's layout being the only change. The gate is no foundation part: the bus module's delivery
|
||||
waits on no act of the gate's, since the indexer takes a revision in from the forge bridge's stream and the assigner
|
||||
writes wants from the runner's events, so the gate's kept data are as §613 has them, state by state (ADR 0666 §4), and serves it by the seat
|
||||
`mesh-delivery`'s verb `source` with the branch, saying `source-moved` on each new revision; the indexer and the
|
||||
node-engine's catch-up read it there, never the gate's state (ADR 0596). Each want names a build-input fingerprint
|
||||
computed at one such revision.
|
||||
- §16 **Validated before any take-in, on every branch.** Before it takes a revision in, on every watched branch, the running
|
||||
indexer asks the manifest validator built from the revision's heads to judge it in the build seat's sandbox (the
|
||||
ask `run`, *What ADR 0656 leaves to this design*). A revision it refuses stays pending under ADR 0633 §5's
|
||||
60-minute bound, its hand acts and its condition `take-in-waits`; the way back is a revert, an ordinary merge
|
||||
(ADR 0666 §6, §26). A revision on the branch the indexer's own placement chose is taken in only by the indexer it
|
||||
names (ADR 0633 §1). This validation is no pipeline step and waits on no pipeline, so every delivering pipeline runs
|
||||
on a revision already taken in.
|
||||
- §17 **A branch whose protection was removed starts nothing.** (§497, §509) On every `branch-moved` the forge bridge reads the
|
||||
branch's protection again; a move of a watched branch whose protection was removed starts no delivering pipeline, and
|
||||
the forge bridge writes the finding `branch-unprotected`, naming the branch, which the condition keeper raises and
|
||||
clears once the protection is back (ADR 0666 §6).
|
||||
- §18 **A pending revision holds its own branch only.** A merge waits behind a pending revision only on the same watched
|
||||
branch; other branches move on (ADR 0666 §6, which changes ADR 0633 §3).
|
||||
- §19 **A refused new indexer holds its branch until a revert lands.** A merge of a new indexer whose own validator refuses
|
||||
its revision has already landed when it is refused, since the validation runs before take-in and after the merge
|
||||
(ADR 0666 §6). It stays pending, and every later merge on that watched branch waits behind it, said by
|
||||
`take-in-waits` at the 60-minute bound, until a revert, an ordinary merge through its pipeline (ADR 0666 §26), lands
|
||||
and is taken in; nothing takes a later merge in past it. This is the cost of validating every revision with the
|
||||
validator built from its own heads, the design's reading of ADR 0666 §6, and the operator is told it with the
|
||||
condition, which names the pending revision and the revert as the way on.
|
||||
- §20 **How it is checked.** Whole (forge bridge): a commit of a branch no registration names starts nothing; a commit on a
|
||||
watched branch is taken from the seat's `branch` read, not the event; a head missed while the bridge was stopped is
|
||||
read at its start. Whole (indexer, assigner): a commit on `beta` gives a revision of the `beta` graph only and moves
|
||||
only the want of the node whose placement chose `beta`; a revision whose own validator refuses it stays pending and
|
||||
holds `beta` while `main` moves on. Whole (watchman): a head with no pipeline for five minutes raises
|
||||
`pipeline-stands`, cleared by `pipeline-started` for that head, and a gathered member does not; a move said while the
|
||||
bridge was stopped is heard from the seat's stream. Tests 64, 68, 76, 78 and 82.
|
||||
|
||||
### Pipeline steps and the policy
|
||||
|
||||
- §21 **The runner.** The seat `mesh-pipeline-runner`, held once for the mesh by the module `mesh-delivery-pipeline`,
|
||||
runs each pipeline (ADR 0666 §7). It is a foundation part, since the bus module's own delivery waits on its pipeline
|
||||
(ADR 0666 §4): its kept data are in old sections (ADR 0619 §2), it waits on no bus module process to start, and it
|
||||
joins the manifest validator's list of foundation parts, which refuses its manifest asking a template (ADR 0666 §4,
|
||||
which changes ADR 0619 §1 and §3). It reads the policy from the registrations, asks each step that is not `bypass` of
|
||||
the seat that serves it, and says each step's start and end and the pipeline's end (*Health and going back*, below).
|
||||
- §22 **The steps and who serves them.** Lint, unit tests and integration tests are the build seats' (`node-build-go`,
|
||||
`node-build-node`, `node-build-image`): each gains the verb `steps`, which answers which pipeline steps an artifact
|
||||
can run by its language's convention (as *The check from release 4* says the tests are found), and the accepted kind
|
||||
`step` on its stream `check`, which runs one. Composition is the assigner's, an accepted kind `step` of the seat
|
||||
`mesh-wants`. Code review comes in through the runner's own verb `review`. Approval is asked as the seat
|
||||
`operator-channel`'s accepted kind `ask`, on its work queue by the runner's name (design 46 §10, *Asks, warrants and answers on the bus*;
|
||||
the seat has no verb `ask`), and heard on its event `decided` (ADR 0666 §7, §10). A step with nothing to do ends at once as
|
||||
`nothing to run`, never as passed.
|
||||
- §23 **The policy.** (§553) Each step's status, per module and per artifact, is `required` (runs; holds the delivery until it
|
||||
passes), `optional` (runs; a failure is shown and may be accepted) or `bypass` (nothing runs; said as bypassed),
|
||||
kept in the registration and never in a manifest: the manifest validator refuses a manifest that names a status
|
||||
(ADR 0666 §7; research 086, R3). A `required` step that ends `nothing to run` holds the delivery and names the step.
|
||||
- §24 **Asked on a work queue, ended by an event.** The runner asks each step on the serving seat's work queue and hears
|
||||
its end as that seat's event `step-ended` (the record's `step ended`), on
|
||||
`mesh.seat.<seat>.event.step-ended.<node>`, the node that ran it in the subject and each holder's account granted only
|
||||
its own node's subject, through a durable consumer of its own, so a step outlives a restart of either side (ADR 0666
|
||||
§7, ADR 0363 §2). A build seat's holder restarted mid-step gets the step again from its work queue and finishes it
|
||||
(ADR 0664 §1). A unit or integration test step says its catalog module's test counts in `step-ended`; `pipeline-ended` carries each
|
||||
catalog module's counts against the test budget, which is counted per catalog module (at most 50 integration and
|
||||
500 unit tests each), and `pipeline-ended` names any catalog module over it (ADR 0666 §8; the test budget, per
|
||||
module, the operator 2026-11-11). A step's outcome is one of `passed`, `failed`, `nothing to run`,
|
||||
`bypassed`, `carried` (with the pipeline it was carried from), `could not run` (naming the part not heard),
|
||||
`waiting on <person>`, `accepted` (a failed `optional` step a person accepted, with who and why), `not asked` (an
|
||||
`approval` step after a failed `required` step), `superseded` and `cancelled`.
|
||||
- §25 **Carried, not run.** A step whose build-input fingerprint and policy did not change is carried by its package
|
||||
digests, as ADR 0548 §5 holds, the runner making the carry instead of the checker: a test step runs only the packages
|
||||
with no kept result for their digest, and a run on request's results are reused when the merged commit's inputs match
|
||||
(ADR 0666 §8; research 086, choice 24). An `approval` step is never carried.
|
||||
- §26 **Approval comes last.** The runner asks the `approval` step only once every other `required` step of the pipeline
|
||||
passed, so the operator approves a commit the mesh already judged, the one place ADR 0661 §3 had the check gate the
|
||||
ask. A failure after an approval is said, as a failed `optional` step is, and the approval stays.
|
||||
- §27 **Kept on the bus, in old sections.** The runner keeps each pipeline's state in its old `state` section `pipelines`,
|
||||
one key per pipeline keyed by its watched branch, source revision and module, written by compare-and-set so a holder
|
||||
replaced mid-pipeline and its successor never both write one; its step log on its old `streams` section's stream
|
||||
`step-log`; each step's output in its old `objects` section `step-outputs`, named in the log; and each package's
|
||||
passing result per level in its old `state` section `kept`, under the digest of what its tests read (ADR 0666 §4, §8;
|
||||
*The runner's kept data*, below). `kept` starts empty: the runner takes no verdict of the checker's, since a step's
|
||||
result is taken only from the bus account of that step's seat holder (ADR 0666 §21), so the first pipelines after G
|
||||
run each package's tests once and carry from then on (the design's choice, 2026-11-10).
|
||||
- §28 **How it is checked.** Whole (runner): a step with unchanged fingerprint and policy is carried and says from where,
|
||||
and an `approval` step is not; a module with no integration tests shows `nothing to run`, and a `required` unit-test
|
||||
step on a module with no tests holds its delivery; a bypassed step is said; a runner restarted mid-step hears the
|
||||
step's end and finishes the pipeline; two runners writing one pipeline key, one loses by compare-and-set; a test step's
|
||||
counts are in `step-ended` and a catalog module over budget is named in `pipeline-ended` (§419). Whole (Go build module): `steps`
|
||||
answers the steps of a fixture artifact, a `step` ask ends with `step-ended` on its own node's subject, one on another
|
||||
node's subject is refused by the bus, and a build module restarted mid-step finishes it. Unit (manifest validator): a manifest naming a status is refused. Tests 65, 66, 73
|
||||
and 88.
|
||||
|
||||
### What holds a delivery
|
||||
|
||||
- §29 **The assigner writes a want only from a passed pipeline.** The runner says each pipeline's end as `pipeline-ended`
|
||||
(the record's `pipeline ended`), keyed by source revision, with every step's result. The assigner consumes it, and
|
||||
writes a want only from a source revision whose every `required` step passed for the modules it moves, on the watched
|
||||
branch a placement chose, by compare-and-set on a newer revision, so an older revision ending after a newer one
|
||||
writes nothing (ADR 0666 §9). The compare-and-set is keyed per module and watched branch: it compares a revision only
|
||||
with the last one written for the same module on the same watched branch, so a revision of `beta` ending after a
|
||||
newer one of `main` writes its own want and refuses neither; a later `pipeline-ended` of the same revision (after a
|
||||
passing `rerun`, below) is taken, since its end count is higher (the design's choice under ADR 0666 §9: source
|
||||
revisions are kept per watched branch, `source.<branch>`, and two branches' revisions have no order between them). This is one more rule beside the assigner's others (ADR 0429 §6), checked per module.
|
||||
From G's delivery it replaces the trunk-only want (ADR 0656 §2 as ADR 0666 §9 changes it). For a group's source
|
||||
revision the runner says one `group-ended` once every member's modules have ended, and the assigner writes the
|
||||
group's wants only on it: a member's own `pipeline-ended` writes nothing (ADR 0666 §9).
|
||||
- §30 **A failed `optional` step holds nothing.** It is shown in `pipeline-ended`, in the delivery record and in the runner's
|
||||
`show`; the operator or a session may accept it by the runner's verb `accept` (§394, §610), recorded in the hand-act log with who and why.
|
||||
- §31 **A newer commit supersedes an older pipeline of a module whose fingerprint it changed.** On a newer commit of the
|
||||
same watched branch the runner ends a module's older running pipeline as `superseded` only when the newer commit
|
||||
changed that module's build-input fingerprint (ADR 0666 §9); a module the commit leaves as it was finishes its
|
||||
pipeline. A newer publish of a package likewise supersedes the rebuild pipelines an older publish of it started. A
|
||||
superseded pipeline's waiting asks are dropped by each build seat's `drop` (ADR 0664 §4), naming
|
||||
the newer commit as the cause, so a cancelled pipeline leaves no ask on a stream (a gap G's prepare found: a cancelled
|
||||
build withdrew only the builder's wait).
|
||||
- §32 **A step nothing starts ends its pipeline.** A step whose prerequisite nothing starts (a seat with no holder, a
|
||||
prerequisite step `bypass`) ends the pipeline refused within five minutes, naming the step and what it waits for,
|
||||
never as a silent wait (research 086, R5); a step whose seat has no holder ends its pipeline failed, naming the seat
|
||||
(ADR 0666 §17).
|
||||
- §33 **A failed head is said to its author, then to the operator.** (§540) A `required` step that failed, or ended `could not
|
||||
run`, on a watched branch's head is the runner's finding `pipeline-failed`, naming the module, the branch, the commit
|
||||
and the step; the condition keeper raises it as the commit's author's for two hours and then the operator's, as ADR
|
||||
0505 §5 and §6 have a delivery whose cure is its author's new head, and clears it when a newer revision of that branch
|
||||
passes that step (ADR 0666 §24). The author is the forge's record of the commit, read through the seat `git`.
|
||||
- §34 **The delivery record keeps every step.** The gate's delivery record consumes the step and pipeline events and keeps
|
||||
them as long as the record (ADR 0666 §9; *The delivery record*).
|
||||
- §35 **How it is checked.** Whole (assigner): a failed `required` step writes no want; a failed `optional` step holds
|
||||
nothing; an older revision's `pipeline-ended` after a newer one's writes nothing, and a revision of one watched branch
|
||||
ending after a newer one of another writes its own want; a group with one member passed and
|
||||
another still running writes no want until `group-ended`. Whole (runner): a newer commit that changed a module's
|
||||
fingerprint supersedes its older pipeline and drops its asks, and one that left it unchanged lets it finish; a step
|
||||
with no holder ends the pipeline naming the seat within five minutes; a failed `required` step on a head raises
|
||||
`pipeline-failed`, the author's for two hours and then the operator's, cleared by a newer passing revision. Tests 66,
|
||||
67, 87, 89 and 98.
|
||||
|
||||
### Approval is a step, and a session merges a pull request
|
||||
|
||||
- §36 **No Approve per merge.** The operator's approval is the pipeline step `approval`, its status in each module's policy;
|
||||
no Approve is asked per merge, and the delivery gate's merge turns and its Approve per merge go (ADR 0666 §10, which
|
||||
changes ADR 0661 §3 to §5). The operator is asked only where a module's policy says `approval`, and for registrations
|
||||
and policy changes.
|
||||
- §37 **The approval is an ask of the runner's.** The runner raises an authorising ask of tier approve as the seat
|
||||
`operator-channel`'s accepted kind `ask`, under its own name, its `about` the module and the watched branch only, so a newer commit's ask
|
||||
replaces the older one's (design 46 §10). The warrant ends the step passed; an ask that is declined, expires or is
|
||||
refused ends it `declined`, and one that is replaced ends it `superseded`. Only a declined `approval` step is asked
|
||||
again, by the runner's verb `ask-again` (ADR 0666 §10). While it waits the runner
|
||||
says `wait-held` waiting on the operator, never a stall (ADR 0283).
|
||||
- §38 **A session merges a pull request.** Any session the tool runner proves, and the operator, merges by calling the seat
|
||||
`git`'s verb `merge-pull` over the mesh, which merges with the forge tools' account (ADR 0398 §2) at the head the
|
||||
caller names. It judges that this is still the pull request's head and that the branch's protection refuses a direct
|
||||
push, the protection read again on every `branch-moved` of the branch, and it judges the head's runs by its modules'
|
||||
policies: it refuses a head when any run at its build-input fingerprint failed a step a policy marks `required`, and
|
||||
merges a head whose `required` steps passed or that has no run yet, a failed `optional` or a `bypass` step refusing
|
||||
nothing; it never waits for a run and never asks an Approve (ADR 0666 §11). The forge keeps only its branch protection.
|
||||
- §39 **Judged from its own state, never by a call.** (§487) The holder of the seat `git` consumes the runner's `step-ended`
|
||||
through a durable consumer of its own and keeps, in its own state `failed-steps`, each failed `required` step by
|
||||
module and build-input fingerprint, with the commits it was seen at and the run that failed it, until none of those
|
||||
commits is a watched branch's head or the head an open pull request names (*The runner's kept data*); so a head
|
||||
merges while the runner is down, judged on what it already heard (ADR 0666 §11, ADR 0596). A head's fingerprints are
|
||||
those its runs named for its commit (kept as §475 says); a step that failed at that fingerprint in any run, a group's or another commit's
|
||||
of the same content included, refuses it, unless a later run of that same step at that fingerprint, asked again by
|
||||
the operator or the healers (the runner's `rerun`), passed: that pass clears the failure from `failed-steps` (ADR 0666
|
||||
§11). The runner's `step-ended` says that the step was a rerun and the caller the runtime proved, and the forge module
|
||||
clears a failure only for a rerun whose caller is the operator or a healer; a plain pass, or a rerun asked by anyone
|
||||
else, clears nothing. A run still running is judged by the steps it has ended, since the merge never waits. A refusal names the run, the module and the step, so the session knows what to fix. The gate merges nothing, and its forge merge token is
|
||||
removed with the checker's buckets and bus account, after the first pipeline after G's delivery has delivered a change
|
||||
end to end and on the operator's word (ADR 0667 §12; *How milestone G itself is checked and delivered*); the forge bridge hears the landed commit as it hears every commit of a watched
|
||||
branch.
|
||||
- §40 **The operator-channel hides no ask.** It keeps design 46's three open asks per asker, never holds the newest back,
|
||||
and replaces an open ask by the same asker's ask with the same `about` (ADR 0666 §12; design 46 §6).
|
||||
- §41 **How it is checked.** Whole (forge module, forge bridge): a session's `merge-pull` at its head merges with no run yet,
|
||||
and with a passed one, and asks no Approve; with the runner stopped a head merges; a head at whose fingerprint any
|
||||
run failed a `required` step, a head that moved and a branch whose protection was removed since its last move are
|
||||
refused, each naming why; a failed `optional` step refuses nothing. Whole (runner): an `approval` step is asked last,
|
||||
a Decline and an expiry each end it `declined` and `ask-again` asks it once more, and a newer commit's ask replaces the
|
||||
older and ends its step `superseded`.
|
||||
Live: a declined approval and a fourth ask each reach the operator. Tests 71 and 91.
|
||||
|
||||
### Groups
|
||||
|
||||
- §42 **Formed from the branch name.** The gate forms a group from a shared branch name carrying its issue number, as it
|
||||
does today without the fixed list of trunk names (ADR 0666 §13).
|
||||
- §43 **A member reaches its group by landing.** A member has reached its group when its commit lands on its modules'
|
||||
watched branch. The forge bridge sets the group on each landed member's commit, reading the members from the seat
|
||||
`mesh-delivery`'s verb `groups`, and publishes `forge.group.merged` when that verb says every member has (ADR 0666 §13,
|
||||
which changes ADR 0633 §4).
|
||||
- §44 **One source revision, one pipeline per branch.** The indexer keeps landed members pending until the group's end and
|
||||
takes them in together as one source revision of that watched branch, saying `take-in.taken`; the runner starts the
|
||||
group's one pipeline per watched branch on that event, and a member of a gathered group starts no pipeline of its own
|
||||
(ADR 0666 §13).
|
||||
When one member's `required` step fails, none of the group's modules is delivered (research 086, R5).
|
||||
- §45 **A group that never completes is said.** By ADR 0633 §5's 30-minute bound and its condition; `bridge_group_end_again`
|
||||
says a group's end again from the gate's group verb, never the gate's `merges` state (ADR 0666 §13, which changes ADR
|
||||
0633 §5).
|
||||
- §46 **How it is checked.** Whole (indexer, gate, forge bridge): a group of three repositories, its own packages included,
|
||||
is one source revision and one pipeline; one member's failed `required` step delivers none; a group with a member that
|
||||
never lands raises `take-in-waits` at 30 minutes. Tests 67 and 81.
|
||||
|
||||
### Dependencies between artifacts
|
||||
|
||||
- §47 **Six kinds of edge between artifact references.** The catalog graph's edges run between **artifact references**,
|
||||
`<module>:<artifact>`, such as `mesh-grants:program`, of six kinds (ADR 0666 §14):
|
||||
|
||||
| Kind | B before A | Derived from |
|
||||
|---|---|---|
|
||||
| 1 | B published before A builds | a package A's build source imports (ADR 0573 §3) |
|
||||
| 2 | B published before A delivers | an artifact A's manifest fetches by reference at its start |
|
||||
| 3 | B delivered before A builds | a build module A is built by, and the toolchain its agreed version names (ADR 0584 §1) |
|
||||
| 4 | B delivered before A delivers | a provision A requires and B provides, bus state among them (ADR 0614 §5), and a format's reader (below) |
|
||||
| 5 | B has acted for A's new version | a seat's acting rule (*What ADR 0656 leaves to this design*): a grant, a credential, a verdict, bus state |
|
||||
| 6 | B runs a version that serves A | a verb or a manifest word A uses and B serves or reads (ADR 0614 §2, §4), the build seats' `kept-artifact` among them |
|
||||
|
||||
Each kind is derived from what manifests and seat definitions already say, and declared per artifact in the manifest's
|
||||
artifact entry, its field `needs`, only where nothing else says it. Today's edges run between modules, and only a
|
||||
package edge names an artifact; at module level the tool runner and the grants part looked like a cycle, per artifact
|
||||
they are a chain (`mesh-grants:program`, then `node-tools:runtime`, then `mesh-grants:tools`).
|
||||
- §48 **Removal runs the other way.** When something goes away the order is reversed: a seat moved to a new holder has its
|
||||
old holder released before the new one claims (ADR 0666 §14).
|
||||
- §49 **The edges alone order.** (§569) The build and the delivery order by these edges alone; a cycle of delivery edges
|
||||
(kinds 2, 4, 5, 6) alone is delivered as one step, and a cycle with any build edge (kind 1 or 3), whatever its other
|
||||
edges, is refused, naming every artifact on it (ADR 0666 §14; research 086, R17). The cycle rule is the indexer's, in the catalog graph it keeps, so the checking
|
||||
program's cycle rule goes with the checker.
|
||||
- §50 **A format's reader first.** A format a running reader cannot read is preceded by its reader, an edge of kind 4 on
|
||||
every watched branch (ADR 0666 §14): the indexer's manifest validator for a new field of a seat definition or a
|
||||
manifest, and on each node the node-engine that reads a catalog graph entry's new fields before any manifest that writes them
|
||||
(ADR 0667 §4).
|
||||
- §51 **How it is checked.** Unit (indexer's graph): a chain per artifact where modules would cycle; two programs needing
|
||||
each other's new verb are one step; two artifacts each needing the other's new package to build are refused, naming
|
||||
both; a seat moved to a new holder releases the old first. Live: the tool runner and the grants part delivered in
|
||||
the order `mesh-grants:program`, `node-tools:runtime`, `mesh-grants:tools`. Test 79.
|
||||
|
||||
### A publish rebuilds
|
||||
|
||||
- §52 **A version is reserved before the build and released when every artifact is published.** A module's version
|
||||
is its manifest's `version`, `MAJOR.MINOR`, and a patch the build system gives: the next after the highest version
|
||||
the catalog holds released for that `MAJOR.MINOR`, 0 for one it holds none of. The builder reserves it when the
|
||||
module's build starts and passes it to the build of every artifact of the module (§370), so code and packed files
|
||||
carry it; each artifact published is said as the event `package-published` of the builder's seat `mesh-builds`
|
||||
(ADR 0666 §15), and the version is released only once every artifact of the module at it is published (§349); only the first watched branch releases (§371). A
|
||||
failed publish leaves the version reserved, and `builder_publish_again` publishes the missing artifacts under the
|
||||
same version. The seat `mesh-builds` is new, held once for the mesh, defined in `mesh-delivery-contracts`; the
|
||||
builder held none until now, and its waits stay on its own stream in this milestone. Why: the operator, 2026-11-10
|
||||
about 21:40 CET, a design choice of G's part (gaps items 22, 23 and 43). This reserves the number when the build
|
||||
starts, where ADR 0666 §15 numbers a package at its publication and design 51 publishes a held digest under a new
|
||||
number: a packed file carries its version, so it must be known before the build; the operator's choice, which a
|
||||
later research brings the records in line with. How it is checked: test 102.
|
||||
- §53 **Every dependent behind is rebuilt against it, on `package-published`.** The builder says `package-published`
|
||||
for every package build of a watched branch: with its released version on the registration's first watched branch,
|
||||
and on another watched branch by its digest alone, the build stored by digest with no version (§371). The runner,
|
||||
which consumes it, asks the catalog's verb `outdated` for that package on that branch, a kept call (ADR 0668 §1,
|
||||
§2), and starts a pipeline for each dependent by kind 1 whose recorded part digest differs from the published one:
|
||||
the catalog compares (§375), the runner starts, and no other part rebuilds anything. A `package-published` whose
|
||||
version is lower than the highest the catalog holds released for that module starts nothing (§382). Nothing else is
|
||||
rebuilt, and a change to tests alone publishes nothing (ADR 0666 §15; research 086, R18). Why: ADR 0666 §15 has the
|
||||
runner consume `package-published`; the digest comparison is the operator's (2026-11-10), and one event for both
|
||||
branches is the design's choice, round 2 of the review by architectural domain, 2026-11-10.
|
||||
- §54 **A second start writes nothing.** The runner starts a pipeline on two events, the indexer's `take-in.taken` (ADR
|
||||
0666 §13) and `package-published` (ADR 0666 §15). When both reach it for the same source revision and module, the
|
||||
second start writes nothing: the runner writes a pipeline's key (watched branch, source revision and module, *The
|
||||
runner's kept data*) by compare-and-set on its absence, so one revision and module has one pipeline. The design's
|
||||
choice under ADR 0666 §13 and §15: the key the runner already keeps is the one place a start is judged, so neither
|
||||
event's producer needs to know of the other.
|
||||
- §55 **Unread packages refuse.** A build that cannot read the published packages refuses and names the unread state
|
||||
(ADR 0666 §15).
|
||||
- §56 **How it is checked.** Whole (builder, indexer): a mesh-sdk merge changing one package publishes one version, and only
|
||||
the artifacts importing that package rebuild; a merge changing only tests rebuilds nothing; a build whose packages
|
||||
cannot be read refuses naming the state. Whole (runner): a `take-in.taken` and a `package-published` for the same
|
||||
source revision and module start one pipeline, the second writing nothing. Tests 70, 80 and 101.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user