website-xp-phone 1.6.5-e → 1.6.6-a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/package.json +2 -2
- package/public/blog/music-links.json +4 -4
- package/public/blog/posts.json +1 -1
- package/public/checksums.txt +20 -20
- package/public/soundcloud.json +25 -25
- package/scripts/mutable-assets.mjs +38 -0
- package/scripts/refresh-content.mjs +13 -1
- package/scripts/verify-policy.mjs +66 -0
- package/scripts/write-dist-checksums.mjs +48 -1
- package/src/components/MusicContent.tsx +4 -5
package/.node-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.14.1
|
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.14.1
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "website-xp-phone",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6a",
|
|
4
4
|
"license": "GPL v2",
|
|
5
5
|
"author": "Kirill <kine> Nevzorov <akinevz@outlook.com>",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"packageManager": "npm@10.8.2",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=
|
|
9
|
+
"node": ">=24.14.1",
|
|
10
10
|
"npm": ">=9.2.0",
|
|
11
11
|
"clippy": "^1"
|
|
12
12
|
},
|
|
@@ -45,15 +45,15 @@
|
|
|
45
45
|
"url": "https://open.spotify.com/track/6gbmylJ7sB7NFfMfTQHosf"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
|
-
"heading": "Playlists!",
|
|
48
|
+
"heading": "Playlists from KINE!",
|
|
49
49
|
"content": [
|
|
50
50
|
{
|
|
51
51
|
"title": "KINE - Amega 3 Atlantic Romanji",
|
|
52
|
-
"
|
|
52
|
+
"url": "https://www.youtube.com/watch?v=sgJsVfPpjV0&list=PLAOwLA0fz3HQCtJ5seeEkv5x--o9DM-JE"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"title": "KINE - Bass Overdose",
|
|
56
|
-
"
|
|
56
|
+
"url": "https://www.youtube.com/watch?v=hgISJPPpzxA&list=PLAOwLA0fz3HTUFtCJGmK-TfUBNsqo_42G"
|
|
57
57
|
}
|
|
58
58
|
]
|
|
59
59
|
},
|
|
@@ -130,4 +130,4 @@
|
|
|
130
130
|
}
|
|
131
131
|
]
|
|
132
132
|
}
|
|
133
|
-
]
|
|
133
|
+
]
|
package/public/blog/posts.json
CHANGED
package/public/checksums.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
b26e02734750158c1de093f7e28c4fdab71eaac6b822e948a103bdbf63238f87 404.html
|
|
2
|
+
b26e02734750158c1de093f7e28c4fdab71eaac6b822e948a103bdbf63238f87 404/index.html
|
|
3
3
|
7da83c3d559cadd8e46256230136893e2ce6fe70f03a41ef2d81181d744eadda 5.html
|
|
4
4
|
3b3034521c33c8fd32ae15b18e1758f4155160cf65241c2c282b407881d1f554 BAHNSCHRIFT.TTF
|
|
5
5
|
d70465bbbb7a22764551a2c9fde0cc7c617618fbb41da07661b71a3598e0a25a Beep.ogg
|
|
@@ -10,9 +10,9 @@ c079b24147f64f850f3c10a52f971d77a3b7a248104005d4a8a3fb781f130849 RuegConfig.7z
|
|
|
10
10
|
f2090319a17b8426af1826047aa9fe316923998c3d8db162b70fdfbb7d73ed09 Smoll.png
|
|
11
11
|
da6292bf7acd20d8c4dd54bfd0f54605f9f2fda10f551bba15969dcbe9e14df2 TODO.pdf
|
|
12
12
|
fdbbad0b3d609ea589e355e0a53f25c12d1dbcfd32ccf22f0332ff67bec22664 Toiny.png
|
|
13
|
-
|
|
13
|
+
d6b33cde491f304cb3cef14255bc7c8e656b9bd96e37fa333ce2d4449786e379 addons.html
|
|
14
14
|
417de6bb69b0e1cd69ce72a30cf85fca751fd267074822c7582110ba66c63487 addons/energy-swing.txt
|
|
15
|
-
|
|
15
|
+
d6b33cde491f304cb3cef14255bc7c8e656b9bd96e37fa333ce2d4449786e379 addons/index.html
|
|
16
16
|
176aaac676a7e3fee2ea16bf0335d450e3ff370b33e2946e1ddbba19598601c9 addons/lego-yoda-death-readme.txt
|
|
17
17
|
8ba04990cf098dffbb0af10c36345455889b8bbbe40071a63e778203f0956c2f addons/lego-yoda-death.mp3
|
|
18
18
|
0e9faacb88a2db38b9762d108a31c4ad603abb9bf83d738b74984506a26549fa addons/mana-blast.txt
|
|
@@ -33,8 +33,8 @@ a49b6fb08f5c5e4d4828cf49e98b72692dc36888502f262041f1822f864fc20b addons/rueg-wa
|
|
|
33
33
|
93f4825028c263e69ca4560f45ff54415fccad38c10e37ea1eefef3ab0ff3f29 assets/PerfectDOSVGA437Win-BZfDqYRn.woff2
|
|
34
34
|
c71295b97f51c2f199b8f1a0ef3cc55fbe5dec6f78daee9098b4dbfe942931a8 assets/PerfectDOSVGA437Win-C4lD4zTj.woff
|
|
35
35
|
9e8db3fde46bba35d28f839d0c507a7407b7e78639a7d52a27e2bc148d29a5b2 assets/firebase-CpVmm8V0.js
|
|
36
|
+
aaf5224bec769a9e7d36022ce942078bce4fba7206c2102f10242a19b4056d1f assets/index-B7RqB93e.js
|
|
36
37
|
0159bd2174114cf0ed5addb5aa3249eaa7a5e73997c20e22ee6b0f25b53520b9 assets/index-BsQF6izn.css
|
|
37
|
-
59ec3c10f1ab9d066c5fb2e9e2627be6396690979d6478c28a827c1a699c7f5c assets/index-ByWLgtih.js
|
|
38
38
|
e122aa26fc9b7191c3af1b0fca8e780474c16240733b69286f9cd50efb447736 assets/markdown-Cj-Co3zP.js
|
|
39
39
|
0c69b2d91191bce88ce4ec0ad81b26f3dd106ea62eff080b30ce550f75438a6c assets/ms_sans_serif-C3pax6mQ.woff
|
|
40
40
|
917afdc85e0876f9086d34392ece168b5590c97e458e6e827dee47ce56684154 assets/ms_sans_serif-Du8rjN1q.woff2
|
|
@@ -47,7 +47,7 @@ e6253b060c25fa6449cb37bcac0bc40db932aa4f83502d53af54277f48d2718e assets/react-c
|
|
|
47
47
|
2ca1269006a1f8e93b6068060a2ad39852589d9151d0e422e93ed6569e61a8ac assets/xpcss-DnhuGufg.css
|
|
48
48
|
37eeb63788be9805d5ff1494d7e737c810d0ca98094ce2f3899b04534767618f avatar.jpg
|
|
49
49
|
465b5adb81e7c1ee040e1a9842d947aea2a9df4f5d153908438745d373e8f22c avatar.png
|
|
50
|
-
|
|
50
|
+
ef560cdabac707e6952c02eb302bcbf6b4360b15ed959f77a31056aafce6174a blog.html
|
|
51
51
|
57bc1a174b8e4b77b417c9f0f8841a17f40e8de9398e5c6901f997be0c1829dc blog/1st.md
|
|
52
52
|
59996c7883d75c6d9d2a3f8bebdd894df7f13d234ad8fbd488152c7354b05d3c blog/2nd.md
|
|
53
53
|
169fab54f1fb8596d3648dcc2c07fdc87012dca4048018d724dece997fa3033c blog/3rd.md
|
|
@@ -56,17 +56,17 @@ c798889828d3da966f7946d9c8a5814c4e3409d1d14dd5eaa4652b387cde0345 blog/5th.md
|
|
|
56
56
|
f9d3862eb88c58f2f788758cb0c9e7a5f1bd1989c4b4b9c9c5fa8a9430e19140 blog/7.md
|
|
57
57
|
237dc169931f8712f58fe1ad3a2ee626636b8e653e40f00a9d53ddd76cd3e1a6 blog/P5kKjYA.png
|
|
58
58
|
380fc0b2043ea9fdc006d99578a6e2e3bc85b0bdfaec53e0c2378fb431e068d8 blog/exhausted.txt
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
ef560cdabac707e6952c02eb302bcbf6b4360b15ed959f77a31056aafce6174a blog/index.html
|
|
60
|
+
9bde35b21fab1d72290d40f5d35233cec047e2abd3c9c8fc8c5b3261ea3d7229 blog/music-links.json
|
|
61
61
|
9b356b02e46d302dfcd1b54f10b7e584743e2852fe4f3cd2e76189d4ce60f519 blog/pizza.gif
|
|
62
|
-
|
|
62
|
+
8c331687624fd21bbe0e6d83c77b2f7d86296ff9e7ae42983d61467203c00a1a blog/posts.json
|
|
63
63
|
d327f052d31ca0ed0154ccba9670eb139338e955f0eb6912b036cb3d4795edf6 blog/references/NA2000_03.pdf
|
|
64
64
|
006bc1b90067fcb72301755e5444664018ccfef3211a2a08c784bce23532842b blog/relevant.jpg
|
|
65
65
|
252795f5503587b6338a6c91e2b2f02640738532797bd98f60e29cdfc0677348 blog/soundcloud.json
|
|
66
66
|
1864ee7f96e2e53bc99bac537fc469004de22665f07a72abfc1535ce6d7bbc04 blog/wsl.1.md
|
|
67
67
|
7450b1ef49558a9476245635ba282913a3af31c1b1e5d72053dab225d7f04b6a blog/рунисч-глоссарий.md
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
e6929c4468016c1f26b3ac93ec4d318a24258603e1581b3c3263c8c7d0050c34 contact.html
|
|
69
|
+
e6929c4468016c1f26b3ac93ec4d318a24258603e1581b3c3263c8c7d0050c34 contact/index.html
|
|
70
70
|
812708d9776e7907534bda54e61169a56ee928923b53445691889d71ef213583 crunchy_kick.ogg
|
|
71
71
|
bb79532b74f54410660a90d90e8db3dfa447e083d2411133ad9fddc9ded7c949 dist.7z
|
|
72
72
|
9d78649ba6229bf35d63f277a95d3fc5e18ac11b948003244b4f6b8007b3b19f documents/Creating Good Code.mp4
|
|
@@ -93,21 +93,21 @@ bf87b6222b106d2ff310312bbe120033011233124d686ca01ca695565715e6ff images/kogg1.p
|
|
|
93
93
|
b3e19635a8088227b05bdb37e4ebf6d3e85c871b32249087bcf85e0108a43492 images/rueg1.png
|
|
94
94
|
ed9b21eea7cdec55954f9af6d00bc61608d027cf4e1bc6182090ad051913e224 images/rueg2.png
|
|
95
95
|
f56503d3d9ad906b5d5b4e1e2c9a9ef3206947959cbc1835b2f6681e5cb115f2 incorrect_responses.txt
|
|
96
|
-
|
|
96
|
+
9e317856ae9a49ae20cd627c3dfe1237b7dbff904b6e83b66d5dbd68344865aa index.html
|
|
97
97
|
6cab264bc21a1f2abc344f27af3cde46d55f7e8aa4340f3cb3ed49e1e4ae63fa loading.css
|
|
98
98
|
825c84d88cba6ff7fdaa324946c134ab3161007d0a63a3c5906ff381c49b2f7e lol.jpg
|
|
99
99
|
2a1670273af4da1a966e557b6fbdce11c4e50e4488ddcfda465613871b52906b medium.png
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
3dbb6df7c5d86b7cdfd3813ca50cd23140acdddb99c1dde89c1cea7ce25035fa music.html
|
|
101
|
+
3dbb6df7c5d86b7cdfd3813ca50cd23140acdddb99c1dde89c1cea7ce25035fa music/index.html
|
|
102
102
|
65b55ce337610c955fa25b7a5982024fa2a5f336b996ec4b19bd96d863ba0803 resume.html
|
|
103
|
-
|
|
103
|
+
f01d9871a6ed377295f4f5e517cabbcfdb43f40b80921476d4aad857b34148e4 resume/index.html
|
|
104
104
|
9e335bff121283bade7005bff78316104846ea5e99e7e2f594fac9fddf39c455 robots.txt
|
|
105
|
-
|
|
105
|
+
95764ca3b40995a4565673e1c23373ec0a99d826f7be3f498cf4929a40d4e7e0 sitemap.html
|
|
106
106
|
510cddda7904e7ee40f744917e3879e4e0d8887d3444ca4c92b4ece5f4b613cd sitemap.xml
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
95764ca3b40995a4565673e1c23373ec0a99d826f7be3f498cf4929a40d4e7e0 sitemap/index.html
|
|
108
|
+
3419929b03f393b24c0c53995e358a425ed6d35d124bb26c06f682a810eb377b soundcloud.json
|
|
109
109
|
fc6ff550d5c7fbd55537560ef0d5470bc68f74b2f41b6386d78b149e897b033c spinner.svg
|
|
110
110
|
e58ca0ef8466ec1d0b0a0e71c4c1f1b620a8d30285df4a8d9e1b243654f0cd6e tada.wav
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
58e7c3d9f3ad95c273ab48571fc99e096a54163c2de1ea6d492d332c33b3ce2a wow.html
|
|
112
|
+
58e7c3d9f3ad95c273ab48571fc99e096a54163c2de1ea6d492d332c33b3ce2a wow/index.html
|
|
113
113
|
da5cb674a690cb58ae78f63c2162ed18ee5daf7576d05c57930f000e9df48194 yooh.mp3
|
package/public/soundcloud.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "https://soundcloud.com/akinevz",
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-14T22:09:50.672Z",
|
|
4
4
|
"asOfUploadingTrackCount": 84,
|
|
5
5
|
"trackCount": 84,
|
|
6
6
|
"tracks": [
|
|
@@ -16,18 +16,6 @@
|
|
|
16
16
|
"title": "Zone12",
|
|
17
17
|
"url": "https://soundcloud.com/akinevz/zone12"
|
|
18
18
|
},
|
|
19
|
-
{
|
|
20
|
-
"owner": "akinevz",
|
|
21
|
-
"path": "/akinevz/thing",
|
|
22
|
-
"title": "Thing",
|
|
23
|
-
"url": "https://soundcloud.com/akinevz/thing"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"owner": "akinevz",
|
|
27
|
-
"path": "/akinevz/hoppa",
|
|
28
|
-
"title": "Hoppa",
|
|
29
|
-
"url": "https://soundcloud.com/akinevz/hoppa"
|
|
30
|
-
},
|
|
31
19
|
{
|
|
32
20
|
"owner": "akinevz",
|
|
33
21
|
"path": "/akinevz/bloody-bits",
|
|
@@ -124,6 +112,18 @@
|
|
|
124
112
|
"title": "Noodle Shop Courier EQ",
|
|
125
113
|
"url": "https://soundcloud.com/kirill_nevzorov/noodle-shop-courier"
|
|
126
114
|
},
|
|
115
|
+
{
|
|
116
|
+
"owner": "akinevz",
|
|
117
|
+
"path": "/akinevz/thing",
|
|
118
|
+
"title": "Thing",
|
|
119
|
+
"url": "https://soundcloud.com/akinevz/thing"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"owner": "akinevz",
|
|
123
|
+
"path": "/akinevz/hoppa",
|
|
124
|
+
"title": "Hoppa",
|
|
125
|
+
"url": "https://soundcloud.com/akinevz/hoppa"
|
|
126
|
+
},
|
|
127
127
|
{
|
|
128
128
|
"owner": "akinevz",
|
|
129
129
|
"path": "/akinevz/out-of-breath",
|
|
@@ -528,18 +528,6 @@
|
|
|
528
528
|
"title": "Zone12",
|
|
529
529
|
"url": "https://soundcloud.com/akinevz/zone12"
|
|
530
530
|
},
|
|
531
|
-
{
|
|
532
|
-
"owner": "akinevz",
|
|
533
|
-
"path": "/akinevz/thing",
|
|
534
|
-
"title": "Thing",
|
|
535
|
-
"url": "https://soundcloud.com/akinevz/thing"
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
"owner": "akinevz",
|
|
539
|
-
"path": "/akinevz/hoppa",
|
|
540
|
-
"title": "Hoppa",
|
|
541
|
-
"url": "https://soundcloud.com/akinevz/hoppa"
|
|
542
|
-
},
|
|
543
531
|
{
|
|
544
532
|
"owner": "akinevz",
|
|
545
533
|
"path": "/akinevz/bloody-bits",
|
|
@@ -576,6 +564,18 @@
|
|
|
576
564
|
"title": "Gabber Housecat HQ",
|
|
577
565
|
"url": "https://soundcloud.com/akinevz/asomniac-shed"
|
|
578
566
|
},
|
|
567
|
+
{
|
|
568
|
+
"owner": "akinevz",
|
|
569
|
+
"path": "/akinevz/thing",
|
|
570
|
+
"title": "Thing",
|
|
571
|
+
"url": "https://soundcloud.com/akinevz/thing"
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"owner": "akinevz",
|
|
575
|
+
"path": "/akinevz/hoppa",
|
|
576
|
+
"title": "Hoppa",
|
|
577
|
+
"url": "https://soundcloud.com/akinevz/hoppa"
|
|
578
|
+
},
|
|
579
579
|
{
|
|
580
580
|
"owner": "akinevz",
|
|
581
581
|
"path": "/akinevz/out-of-breath",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source of truth for which built `dist/` artifacts are expected to vary
|
|
3
|
+
* between otherwise-identical builds (a "content-refresh" run).
|
|
4
|
+
*
|
|
5
|
+
* Everything NOT listed here is treated as STABLE and must not drift
|
|
6
|
+
* across a content-only refresh. This is a denylist (mutable set)
|
|
7
|
+
* rather than an allowlist (stable set) because:
|
|
8
|
+
*
|
|
9
|
+
* 1. The mutable set is small and has stable filenames, while the
|
|
10
|
+
* stable set is large and uses Vite content-hashed names that
|
|
11
|
+
* change on every legitimate code edit.
|
|
12
|
+
* 2. A forgotten mutable entry fails loudly (you add it); a forgotten
|
|
13
|
+
* stable entry is a silent drift regression — the unsafe failure
|
|
14
|
+
* mode for a drift detector.
|
|
15
|
+
*
|
|
16
|
+
* `checksums.txt` itself is excluded from the manifest (see
|
|
17
|
+
* write-dist-checksums.mjs), so it does not need to appear here. It is
|
|
18
|
+
* listed for documentation completeness and to guard against it ever
|
|
19
|
+
* being included in the walk.
|
|
20
|
+
*
|
|
21
|
+
* Paths are relative to `dist/` and use forward slashes.
|
|
22
|
+
*/
|
|
23
|
+
export const MUTABLE_PATHS = Object.freeze([
|
|
24
|
+
"index.html",
|
|
25
|
+
"soundcloud.json",
|
|
26
|
+
"blog/music-links.json",
|
|
27
|
+
"checksums.txt",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export const MUTABLE_PATH_SET = Object.freeze(new Set(MUTABLE_PATHS));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns true for artifacts that are allowed to change between
|
|
34
|
+
* content-only refresh builds.
|
|
35
|
+
*/
|
|
36
|
+
export function isMutablePath(relPath) {
|
|
37
|
+
return MUTABLE_PATH_SET.has(relPath);
|
|
38
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run as refreshSoundcloud } from "./refresh-soundcloud.mjs";
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
@@ -8,6 +8,13 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
8
8
|
const __dirname = dirname(__filename);
|
|
9
9
|
const projectRoot = resolve(__dirname, "..");
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Stale documents/drift-report.json is removed at the start of every refresh cycle so
|
|
13
|
+
* that `verify:policy` (which may run before `checksums`) does not act on a
|
|
14
|
+
* report produced by a previous build. The checksums step rewrites it.
|
|
15
|
+
*/
|
|
16
|
+
const driftReportPath = resolve(projectRoot, "public", "documents", "drift-report.json");
|
|
17
|
+
|
|
11
18
|
const LOCAL_SOURCES = [
|
|
12
19
|
{
|
|
13
20
|
inputPath: resolve(projectRoot, "public/blog/posts.json"),
|
|
@@ -29,6 +36,11 @@ async function freezeBlogContent() {
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export async function run() {
|
|
39
|
+
await unlink(driftReportPath).catch((error) => {
|
|
40
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
32
44
|
await freezeBlogContent();
|
|
33
45
|
await refreshSoundcloud();
|
|
34
46
|
}
|
|
@@ -3,11 +3,13 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { MUTABLE_PATH_SET } from "./mutable-assets.mjs";
|
|
6
7
|
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
9
10
|
const projectRoot = resolve(__dirname, "..");
|
|
10
11
|
const srcRoot = resolve(projectRoot, "src");
|
|
12
|
+
const driftReportPath = resolve(projectRoot, "public", "documents", "drift-report.json");
|
|
11
13
|
const SECTION_PROPS_KEYS = readAllowedSectionKeys();
|
|
12
14
|
const runtimeRoots = [resolve(srcRoot, "components"), resolve(srcRoot, "windowing")];
|
|
13
15
|
const runtimeTopLevelFiles = [resolve(srcRoot, "App.tsx")];
|
|
@@ -256,9 +258,73 @@ async function checkContentSchema() {
|
|
|
256
258
|
validateMusicLinks(musicLinks);
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
async function checkDriftGate() {
|
|
262
|
+
let raw;
|
|
263
|
+
try {
|
|
264
|
+
raw = await readFile(driftReportPath, "utf8");
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
267
|
+
// No drift report present — the checksums step has not been run
|
|
268
|
+
// since the last clean. Skip the gate rather than fail, so
|
|
269
|
+
// `verify:policy` remains usable standalone.
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const report = JSON.parse(raw);
|
|
276
|
+
assert(
|
|
277
|
+
report && typeof report === "object" && !Array.isArray(report),
|
|
278
|
+
"public/documents/drift-report.json must be a JSON object",
|
|
279
|
+
);
|
|
280
|
+
assert(typeof report.baselineUrl === "string", "documents/drift-report.json.baselineUrl must be string");
|
|
281
|
+
assert(typeof report.generatedAt === "string", "documents/drift-report.json.generatedAt must be string");
|
|
282
|
+
assert(Array.isArray(report.violations), "documents/drift-report.json.violations must be array");
|
|
283
|
+
assert(typeof report.violationCount === "number", "documents/drift-report.json.violationCount must be number");
|
|
284
|
+
|
|
285
|
+
// Cross-check that every violation is for a path the denylist actually
|
|
286
|
+
// permits to mutate. The checksums script should never emit a violation
|
|
287
|
+
// for a mutable path, but if it does we want to surface it explicitly.
|
|
288
|
+
for (const violation of report.violations) {
|
|
289
|
+
assert(
|
|
290
|
+
violation && typeof violation === "object",
|
|
291
|
+
"documents/drift-report.json.violations[] must be objects",
|
|
292
|
+
);
|
|
293
|
+
assert(typeof violation.relPath === "string", "documents/drift-report.json.violations[].relPath must be string");
|
|
294
|
+
// Non-mutable paths are the only ones that can violate the gate.
|
|
295
|
+
// A mutable path here would mean the denylist in mutable-assets.mjs
|
|
296
|
+
// and the gate logic in write-dist-checksums.mjs disagree.
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Sanity: the mutablePaths recorded in the report must match the current
|
|
300
|
+
// denylist exactly — guards against accidental denylist drift between
|
|
301
|
+
// the two scripts.
|
|
302
|
+
const reportMutable = new Set(
|
|
303
|
+
Array.isArray(report.mutablePaths) ? report.mutablePaths : [],
|
|
304
|
+
);
|
|
305
|
+
assert(
|
|
306
|
+
reportMutable.size === MUTABLE_PATH_SET.size &&
|
|
307
|
+
[...MUTABLE_PATH_SET].every((p) => reportMutable.has(p)),
|
|
308
|
+
"documents/drift-report.json.mutablePaths does not match scripts/mutable-assets.mjs",
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (report.violationCount > 0) {
|
|
312
|
+
const lines = report.violations.map(
|
|
313
|
+
(v) => ` ${v.relPath}: ${v.status} (prev=${v.prev ?? "(none)"})`,
|
|
314
|
+
);
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Drift gate failed — ${report.violationCount} non-mutable artifact(s) changed ` +
|
|
317
|
+
`vs ${report.baselineUrl}:\n${lines.join("\n")}\n` +
|
|
318
|
+
`If this is a legitimate code change, rebuild from scratch and commit the new checksums.\n` +
|
|
319
|
+
`If this is a content-only refresh, investigate why a stable artifact changed.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
259
324
|
async function run() {
|
|
260
325
|
await checkRuntimePolicy();
|
|
261
326
|
await checkContentSchema();
|
|
327
|
+
await checkDriftGate();
|
|
262
328
|
process.stdout.write("Policy verification passed.\n");
|
|
263
329
|
}
|
|
264
330
|
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "node:fs/promises";
|
|
27
27
|
import { dirname, relative, resolve } from "node:path";
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { isMutablePath, MUTABLE_PATH_SET } from "./mutable-assets.mjs";
|
|
29
30
|
|
|
30
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
32
|
const __dirname = dirname(__filename);
|
|
@@ -35,6 +36,14 @@ const publicRoot = resolve(projectRoot, "public");
|
|
|
35
36
|
const distChecksumsPath = resolve(distRoot, "checksums.txt");
|
|
36
37
|
const publicChecksumsPath = resolve(publicRoot, "checksums.txt");
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Sidecar written next to `checksums.txt` capturing any STABLE-invariant
|
|
41
|
+
* violations observed during this run. `verify-policy.mjs` reads it so the
|
|
42
|
+
* drift gate runs in CI (where the remote baseline is reachable) rather
|
|
43
|
+
* than only locally. An empty violations array means "no drift".
|
|
44
|
+
*/
|
|
45
|
+
const driftReportPath = resolve(publicRoot, "documents", "drift-report.json");
|
|
46
|
+
|
|
38
47
|
/** Where the previous deployment's manifest lives. */
|
|
39
48
|
const REMOTE_CHECKSUMS_URL =
|
|
40
49
|
process.env.CHECKSUMS_URL || "https://akinevz.com/checksums.txt";
|
|
@@ -113,6 +122,7 @@ async function run() {
|
|
|
113
122
|
const existing = await readRemoteChecksums();
|
|
114
123
|
const rows = [];
|
|
115
124
|
let index = 0;
|
|
125
|
+
const driftViolations = [];
|
|
116
126
|
|
|
117
127
|
for (const filePath of files.sort()) {
|
|
118
128
|
// Skip the manifest file itself so it doesn't appear in its own diff.
|
|
@@ -128,17 +138,54 @@ async function run() {
|
|
|
128
138
|
: previous === digest
|
|
129
139
|
? "STABLE"
|
|
130
140
|
: "CHANGED";
|
|
141
|
+
const mutable = isMutablePath(relPath);
|
|
131
142
|
index += 1;
|
|
132
143
|
|
|
133
144
|
process.stdout.write(
|
|
134
|
-
`${String(index).padStart(3, "0")}. [${status}] ${relPath}\n` +
|
|
145
|
+
`${String(index).padStart(3, "0")}. [${status}] ${relPath}${mutable ? " (mutable)" : ""}\n` +
|
|
135
146
|
` prev: ${previous ?? "(none)"}\n` +
|
|
136
147
|
` next: ${digest}\n`,
|
|
137
148
|
);
|
|
138
149
|
|
|
150
|
+
// Drift gate: a non-mutable artifact MUST be STABLE relative to the
|
|
151
|
+
// remote baseline. A mutable artifact may be STABLE, CHANGED, or NEW.
|
|
152
|
+
if (!mutable && status !== "STABLE") {
|
|
153
|
+
driftViolations.push({
|
|
154
|
+
relPath,
|
|
155
|
+
status,
|
|
156
|
+
prev: previous ?? null,
|
|
157
|
+
next: digest,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
rows.push(`${digest} ${relPath}`);
|
|
140
162
|
}
|
|
141
163
|
|
|
164
|
+
// Always emit a drift report so verify-policy can consume a definite
|
|
165
|
+
// signal rather than relying on file presence.
|
|
166
|
+
const driftReport = JSON.stringify(
|
|
167
|
+
{
|
|
168
|
+
baselineUrl: REMOTE_CHECKSUMS_URL,
|
|
169
|
+
generatedAt: new Date().toISOString(),
|
|
170
|
+
mutablePaths: [...MUTABLE_PATH_SET],
|
|
171
|
+
violationCount: driftViolations.length,
|
|
172
|
+
violations: driftViolations,
|
|
173
|
+
},
|
|
174
|
+
null,
|
|
175
|
+
2,
|
|
176
|
+
);
|
|
177
|
+
await writeFile(driftReportPath, `${driftReport}\n`, "utf8");
|
|
178
|
+
|
|
179
|
+
if (driftViolations.length > 0) {
|
|
180
|
+
const lines = driftViolations.map(
|
|
181
|
+
(v) => ` ${v.relPath}: ${v.status} (prev=${v.prev ?? "(none)"})`,
|
|
182
|
+
);
|
|
183
|
+
process.stderr.write(
|
|
184
|
+
`Drift gate failed — ${driftViolations.length} non-mutable artifact(s) changed ` +
|
|
185
|
+
`vs ${REMOTE_CHECKSUMS_URL}:\n${lines.join("\n")}\n`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
142
189
|
const manifest = `${rows.join("\n")}\n`;
|
|
143
190
|
|
|
144
191
|
// Write the deployed copy (`dist/checksums.txt`) and the source copy
|
|
@@ -97,8 +97,7 @@ const BackgroundImage = ({
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
return (
|
|
100
|
-
<div
|
|
101
|
-
>
|
|
100
|
+
<div>
|
|
102
101
|
{content ? <Section theme="nowindow" content={content} /> : null}
|
|
103
102
|
<a
|
|
104
103
|
href={url}
|
|
@@ -109,7 +108,7 @@ const BackgroundImage = ({
|
|
|
109
108
|
...containerStyle,
|
|
110
109
|
display: "block",
|
|
111
110
|
padding: "1em",
|
|
112
|
-
boxSizing: "border-box"
|
|
111
|
+
boxSizing: "border-box",
|
|
113
112
|
}}
|
|
114
113
|
>
|
|
115
114
|
{children}
|
|
@@ -522,7 +521,6 @@ const toMusicSections = (
|
|
|
522
521
|
});
|
|
523
522
|
|
|
524
523
|
return [
|
|
525
|
-
|
|
526
524
|
{
|
|
527
525
|
className: "music-as-of-uploading",
|
|
528
526
|
heading: "Discography Total",
|
|
@@ -547,7 +545,8 @@ const toMusicSections = (
|
|
|
547
545
|
favouriteLinkList.length > 0
|
|
548
546
|
? favouriteLinkList
|
|
549
547
|
: ["No favourite links configured yet."],
|
|
550
|
-
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
551
550
|
className: "music-source",
|
|
552
551
|
heading: "Surviving Archive",
|
|
553
552
|
content: [
|