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 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.5e",
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": ">=20.19.2",
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
- "link": "https://www.youtube.com/watch?v=sgJsVfPpjV0&list=PLAOwLA0fz3HQCtJ5seeEkv5x--o9DM-JE"
52
+ "url": "https://www.youtube.com/watch?v=sgJsVfPpjV0&list=PLAOwLA0fz3HQCtJ5seeEkv5x--o9DM-JE"
53
53
  },
54
54
  {
55
55
  "title": "KINE - Bass Overdose",
56
- "link": "https://www.youtube.com/watch?v=hgISJPPpzxA&list=PLAOwLA0fz3HTUFtCJGmK-TfUBNsqo_42G"
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
+ ]
@@ -426,4 +426,4 @@
426
426
  ]
427
427
  }
428
428
  ]
429
- }
429
+ }
@@ -1,5 +1,5 @@
1
- 290afbd84618a31bac4d0c3dc9f8a9763fafdb4ed6cbdc2d43a2e1f8da32d0fa 404.html
2
- 290afbd84618a31bac4d0c3dc9f8a9763fafdb4ed6cbdc2d43a2e1f8da32d0fa 404/index.html
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
- 0598a6f71a18d62e5ff0f6ad19608912d042b1228a174ed7bff618324aad90aa addons.html
13
+ d6b33cde491f304cb3cef14255bc7c8e656b9bd96e37fa333ce2d4449786e379 addons.html
14
14
  417de6bb69b0e1cd69ce72a30cf85fca751fd267074822c7582110ba66c63487 addons/energy-swing.txt
15
- 0598a6f71a18d62e5ff0f6ad19608912d042b1228a174ed7bff618324aad90aa addons/index.html
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
- cc385faf0b3ee43304cb9ece70eacd3910eb38dd8abdabebed8b193a682eed5d blog.html
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
- cc385faf0b3ee43304cb9ece70eacd3910eb38dd8abdabebed8b193a682eed5d blog/index.html
60
- a444a7a380aac04f8a7a39c6d765c405baaff824a065f4dcd963c5a4a5e93f5e blog/music-links.json
59
+ ef560cdabac707e6952c02eb302bcbf6b4360b15ed959f77a31056aafce6174a blog/index.html
60
+ 9bde35b21fab1d72290d40f5d35233cec047e2abd3c9c8fc8c5b3261ea3d7229 blog/music-links.json
61
61
  9b356b02e46d302dfcd1b54f10b7e584743e2852fe4f3cd2e76189d4ce60f519 blog/pizza.gif
62
- 13872adf5e58294874d64c700acc1894e9167ed4649c3d2c157fa8f3bbcb9abf blog/posts.json
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
- 1beed156a95789676ab77f97c5c3765c055e7a4ddb0ec6833a66aef25067883f contact.html
69
- 1beed156a95789676ab77f97c5c3765c055e7a4ddb0ec6833a66aef25067883f contact/index.html
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
- 1d053db953b9c1cc5278963ea131afca8c2739d8ff0d16a1787fe9e323176100 index.html
96
+ 9e317856ae9a49ae20cd627c3dfe1237b7dbff904b6e83b66d5dbd68344865aa index.html
97
97
  6cab264bc21a1f2abc344f27af3cde46d55f7e8aa4340f3cb3ed49e1e4ae63fa loading.css
98
98
  825c84d88cba6ff7fdaa324946c134ab3161007d0a63a3c5906ff381c49b2f7e lol.jpg
99
99
  2a1670273af4da1a966e557b6fbdce11c4e50e4488ddcfda465613871b52906b medium.png
100
- adc146605ba8877470c390b8105c065c5b644ab62feeb45c545466fb2df8d677 music.html
101
- adc146605ba8877470c390b8105c065c5b644ab62feeb45c545466fb2df8d677 music/index.html
100
+ 3dbb6df7c5d86b7cdfd3813ca50cd23140acdddb99c1dde89c1cea7ce25035fa music.html
101
+ 3dbb6df7c5d86b7cdfd3813ca50cd23140acdddb99c1dde89c1cea7ce25035fa music/index.html
102
102
  65b55ce337610c955fa25b7a5982024fa2a5f336b996ec4b19bd96d863ba0803 resume.html
103
- b285a00a19fb7e1aaf298b5f37720a144cb803bc42868772e9f9d52a0af1685c resume/index.html
103
+ f01d9871a6ed377295f4f5e517cabbcfdb43f40b80921476d4aad857b34148e4 resume/index.html
104
104
  9e335bff121283bade7005bff78316104846ea5e99e7e2f594fac9fddf39c455 robots.txt
105
- 69ff96e7f4e8823bbee3fd6109bd9c333aa53d080526a0dd3a1a8e2552adce0e sitemap.html
105
+ 95764ca3b40995a4565673e1c23373ec0a99d826f7be3f498cf4929a40d4e7e0 sitemap.html
106
106
  510cddda7904e7ee40f744917e3879e4e0d8887d3444ca4c92b4ece5f4b613cd sitemap.xml
107
- 69ff96e7f4e8823bbee3fd6109bd9c333aa53d080526a0dd3a1a8e2552adce0e sitemap/index.html
108
- ec8328c6dfd2f193e5f2f3d52732f1667c7a20e9f5adeecdac3196f99af27f45 soundcloud.json
107
+ 95764ca3b40995a4565673e1c23373ec0a99d826f7be3f498cf4929a40d4e7e0 sitemap/index.html
108
+ 3419929b03f393b24c0c53995e358a425ed6d35d124bb26c06f682a810eb377b soundcloud.json
109
109
  fc6ff550d5c7fbd55537560ef0d5470bc68f74b2f41b6386d78b149e897b033c spinner.svg
110
110
  e58ca0ef8466ec1d0b0a0e71c4c1f1b620a8d30285df4a8d9e1b243654f0cd6e tada.wav
111
- 96008c26f9ca8e88c84ff734b08ed30251e94ea508680e5055c630262fcfff9e wow.html
112
- 96008c26f9ca8e88c84ff734b08ed30251e94ea508680e5055c630262fcfff9e wow/index.html
111
+ 58e7c3d9f3ad95c273ab48571fc99e096a54163c2de1ea6d492d332c33b3ce2a wow.html
112
+ 58e7c3d9f3ad95c273ab48571fc99e096a54163c2de1ea6d492d332c33b3ce2a wow/index.html
113
113
  da5cb674a690cb58ae78f63c2162ed18ee5daf7576d05c57930f000e9df48194 yooh.mp3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "https://soundcloud.com/akinevz",
3
- "generatedAt": "2026-08-14T17:31:42.385Z",
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: [