cursedbelt-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/LICENSE +15 -0
  2. package/dist/client/wsWire.d.ts +49 -0
  3. package/dist/client/wsWire.js +57 -0
  4. package/dist/config/appConfig.d.ts +361 -0
  5. package/dist/config/appConfig.js +308 -0
  6. package/dist/core/activity/index.d.ts +8 -0
  7. package/dist/core/activity/index.js +7 -0
  8. package/dist/core/activity/model.d.ts +115 -0
  9. package/dist/core/activity/model.js +63 -0
  10. package/dist/core/analytics/index.d.ts +18 -0
  11. package/dist/core/analytics/index.js +18 -0
  12. package/dist/core/analytics/range.d.ts +186 -0
  13. package/dist/core/analytics/range.js +271 -0
  14. package/dist/core/analytics/schedule.d.ts +301 -0
  15. package/dist/core/analytics/schedule.js +372 -0
  16. package/dist/core/category/index.d.ts +3 -0
  17. package/dist/core/category/index.js +6 -0
  18. package/dist/core/category/memoryStore.d.ts +21 -0
  19. package/dist/core/category/memoryStore.js +97 -0
  20. package/dist/core/category/tree.d.ts +26 -0
  21. package/dist/core/category/tree.js +113 -0
  22. package/dist/core/category/types.d.ts +48 -0
  23. package/dist/core/category/types.js +8 -0
  24. package/dist/core/color.d.ts +14 -0
  25. package/dist/core/color.js +32 -0
  26. package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
  27. package/dist/core/colorScheme/colorSchemeStore.js +123 -0
  28. package/dist/core/colorScheme/index.d.ts +7 -0
  29. package/dist/core/colorScheme/index.js +7 -0
  30. package/dist/core/domainOwners.d.ts +19 -0
  31. package/dist/core/domainOwners.js +15 -0
  32. package/dist/core/download.d.ts +14 -0
  33. package/dist/core/download.js +37 -0
  34. package/dist/core/events/ccEventBus.d.ts +44 -0
  35. package/dist/core/events/ccEventBus.js +70 -0
  36. package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
  37. package/dist/core/file-tree/fileTreeModel.js +312 -0
  38. package/dist/core/folder-tree/actions.d.ts +84 -0
  39. package/dist/core/folder-tree/actions.js +119 -0
  40. package/dist/core/folder-tree/index.d.ts +3 -0
  41. package/dist/core/folder-tree/index.js +20 -0
  42. package/dist/core/folder-tree/path.d.ts +76 -0
  43. package/dist/core/folder-tree/path.js +146 -0
  44. package/dist/core/folder-tree/tree.d.ts +86 -0
  45. package/dist/core/folder-tree/tree.js +184 -0
  46. package/dist/core/layout/index.d.ts +1 -0
  47. package/dist/core/layout/index.js +7 -0
  48. package/dist/core/master-lock/index.d.ts +14 -0
  49. package/dist/core/master-lock/index.js +14 -0
  50. package/dist/core/master-lock/kdf.d.ts +63 -0
  51. package/dist/core/master-lock/kdf.js +117 -0
  52. package/dist/core/master-lock/policy.d.ts +32 -0
  53. package/dist/core/master-lock/policy.js +52 -0
  54. package/dist/core/master-lock/presence.d.ts +52 -0
  55. package/dist/core/master-lock/presence.js +61 -0
  56. package/dist/core/master-lock/wire.d.ts +139 -0
  57. package/dist/core/master-lock/wire.js +81 -0
  58. package/dist/core/media/bgRemoval.d.ts +27 -0
  59. package/dist/core/media/bgRemoval.js +101 -0
  60. package/dist/core/media/ffmpeg.d.ts +57 -0
  61. package/dist/core/media/ffmpeg.js +126 -0
  62. package/dist/core/media/format.d.ts +20 -0
  63. package/dist/core/media/format.js +54 -0
  64. package/dist/core/media/index.d.ts +12 -0
  65. package/dist/core/media/index.js +18 -0
  66. package/dist/core/media/mediaItem.d.ts +15 -0
  67. package/dist/core/media/mediaItem.js +34 -0
  68. package/dist/core/media/renderPlan.d.ts +63 -0
  69. package/dist/core/media/renderPlan.js +232 -0
  70. package/dist/core/media/segment.d.ts +79 -0
  71. package/dist/core/media/segment.js +247 -0
  72. package/dist/core/media/size.d.ts +1 -0
  73. package/dist/core/media/size.js +3 -0
  74. package/dist/core/media/trim.d.ts +21 -0
  75. package/dist/core/media/trim.js +40 -0
  76. package/dist/core/media/types.d.ts +148 -0
  77. package/dist/core/media/types.js +11 -0
  78. package/dist/core/media/uploadLimits.d.ts +7 -0
  79. package/dist/core/media/uploadLimits.js +16 -0
  80. package/dist/core/media/validate.d.ts +9 -0
  81. package/dist/core/media/validate.js +121 -0
  82. package/dist/core/media/videoCuts.d.ts +152 -0
  83. package/dist/core/media/videoCuts.js +284 -0
  84. package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
  85. package/dist/core/navigation/breadcrumbCompat.js +21 -0
  86. package/dist/core/navigation/coverageTest.d.ts +24 -0
  87. package/dist/core/navigation/coverageTest.js +48 -0
  88. package/dist/core/navigation/index.d.ts +6 -0
  89. package/dist/core/navigation/index.js +10 -0
  90. package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
  91. package/dist/core/navigation/migrateNavPreferences.js +63 -0
  92. package/dist/core/navigation/navigationModel.d.ts +165 -0
  93. package/dist/core/navigation/navigationModel.js +14 -0
  94. package/dist/core/navigation/navigationSelectors.d.ts +63 -0
  95. package/dist/core/navigation/navigationSelectors.js +167 -0
  96. package/dist/core/navigation/searchCatalogue.d.ts +23 -0
  97. package/dist/core/navigation/searchCatalogue.js +76 -0
  98. package/dist/core/net/reconnectSchedule.d.ts +46 -0
  99. package/dist/core/net/reconnectSchedule.js +38 -0
  100. package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
  101. package/dist/core/palette/ShortcutRegistry.js +52 -0
  102. package/dist/core/palette/fuzzyRank.d.ts +18 -0
  103. package/dist/core/palette/fuzzyRank.js +73 -0
  104. package/dist/core/palette/hotkeys.d.ts +28 -0
  105. package/dist/core/palette/hotkeys.js +118 -0
  106. package/dist/core/palette/index.d.ts +3 -0
  107. package/dist/core/palette/index.js +8 -0
  108. package/dist/core/schema/projection.d.ts +84 -0
  109. package/dist/core/schema/projection.js +184 -0
  110. package/dist/core/session/claims.d.ts +145 -0
  111. package/dist/core/session/claims.js +157 -0
  112. package/dist/core/sharing/index.d.ts +5 -0
  113. package/dist/core/sharing/index.js +4 -0
  114. package/dist/core/sharing/model.d.ts +195 -0
  115. package/dist/core/sharing/model.js +150 -0
  116. package/dist/core/site/siteCopy.d.ts +92 -0
  117. package/dist/core/site/siteCopy.js +91 -0
  118. package/dist/core/slots.d.ts +22 -0
  119. package/dist/core/slots.js +31 -0
  120. package/dist/core/styles/constructableSheet.d.ts +51 -0
  121. package/dist/core/styles/constructableSheet.js +80 -0
  122. package/dist/core/tokens/registry.d.ts +119 -0
  123. package/dist/core/tokens/registry.js +447 -0
  124. package/dist/core/uiScale/index.d.ts +8 -0
  125. package/dist/core/uiScale/index.js +8 -0
  126. package/dist/core/uiScale/scale.d.ts +36 -0
  127. package/dist/core/uiScale/scale.js +47 -0
  128. package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
  129. package/dist/core/uiScale/uiScaleStore.js +63 -0
  130. package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
  131. package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
  132. package/dist/core/wire/index.d.ts +17 -0
  133. package/dist/core/wire/index.js +17 -0
  134. package/dist/core/wire/ingress.d.ts +44 -0
  135. package/dist/core/wire/ingress.js +134 -0
  136. package/dist/core/wire/marshalWorker.d.ts +70 -0
  137. package/dist/core/wire/marshalWorker.js +273 -0
  138. package/dist/core/wire/marshalling.d.ts +122 -0
  139. package/dist/core/wire/marshalling.js +126 -0
  140. package/dist/core/wire/wireHub.d.ts +80 -0
  141. package/dist/core/wire/wireHub.js +116 -0
  142. package/dist/ctgr/base64url.d.ts +8 -0
  143. package/dist/ctgr/base64url.js +39 -0
  144. package/dist/ctgr/chunkAccumulator.d.ts +214 -0
  145. package/dist/ctgr/chunkAccumulator.js +307 -0
  146. package/dist/ctgr/chunkStore.d.ts +24 -0
  147. package/dist/ctgr/chunkStore.js +125 -0
  148. package/dist/ctgr/codec.d.ts +44 -0
  149. package/dist/ctgr/codec.js +153 -0
  150. package/dist/ctgr/compress.d.ts +2 -0
  151. package/dist/ctgr/compress.js +20 -0
  152. package/dist/ctgr/crc32.d.ts +2 -0
  153. package/dist/ctgr/crc32.js +23 -0
  154. package/dist/ctgr/digest.d.ts +1 -0
  155. package/dist/ctgr/digest.js +15 -0
  156. package/dist/ctgr/index.d.ts +9 -0
  157. package/dist/ctgr/index.js +17 -0
  158. package/dist/ctgr/types.d.ts +64 -0
  159. package/dist/ctgr/types.js +21 -0
  160. package/dist/ctgr/v0compat.d.ts +27 -0
  161. package/dist/ctgr/v0compat.js +128 -0
  162. package/dist/index.d.ts +15 -0
  163. package/dist/index.js +17 -0
  164. package/dist/shared/errors.d.ts +101 -0
  165. package/dist/shared/errors.js +99 -0
  166. package/dist/testing/bytes.d.ts +32 -0
  167. package/dist/testing/bytes.js +33 -0
  168. package/dist/testing/domInspect.d.ts +59 -0
  169. package/dist/testing/domInspect.js +115 -0
  170. package/dist/testing/fakeFetch.d.ts +23 -0
  171. package/dist/testing/fakeFetch.js +21 -0
  172. package/dist/testing/index.d.ts +3 -0
  173. package/dist/testing/index.js +11 -0
  174. package/package.json +373 -0
  175. package/src/client/wsWire.ts +83 -0
  176. package/src/config/appConfig.spec.ts +158 -0
  177. package/src/config/appConfig.ts +372 -0
  178. package/src/core/activity/index.ts +21 -0
  179. package/src/core/activity/model.ts +160 -0
  180. package/src/core/analytics/index.ts +18 -0
  181. package/src/core/analytics/range.spec.ts +280 -0
  182. package/src/core/analytics/range.ts +383 -0
  183. package/src/core/analytics/schedule.spec.ts +311 -0
  184. package/src/core/analytics/schedule.ts +624 -0
  185. package/src/core/category/category.spec.ts +144 -0
  186. package/src/core/category/index.ts +19 -0
  187. package/src/core/category/memoryStore.ts +104 -0
  188. package/src/core/category/tree.ts +118 -0
  189. package/src/core/category/types.ts +59 -0
  190. package/src/core/color.spec.ts +35 -0
  191. package/src/core/color.ts +35 -0
  192. package/src/core/colorScheme/colorScheme.spec.ts +127 -0
  193. package/src/core/colorScheme/colorSchemeStore.ts +184 -0
  194. package/src/core/colorScheme/index.ts +23 -0
  195. package/src/core/domainOwners.ts +32 -0
  196. package/src/core/download.spec.ts +38 -0
  197. package/src/core/download.ts +35 -0
  198. package/src/core/events/ccEventBus.spec.ts +90 -0
  199. package/src/core/events/ccEventBus.ts +120 -0
  200. package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
  201. package/src/core/file-tree/fileTreeModel.ts +511 -0
  202. package/src/core/folder-tree/actions.spec.ts +103 -0
  203. package/src/core/folder-tree/actions.ts +153 -0
  204. package/src/core/folder-tree/index.ts +46 -0
  205. package/src/core/folder-tree/path.spec.ts +113 -0
  206. package/src/core/folder-tree/path.ts +148 -0
  207. package/src/core/folder-tree/tree.spec.ts +157 -0
  208. package/src/core/folder-tree/tree.ts +244 -0
  209. package/src/core/layout/index.ts +7 -0
  210. package/src/core/master-lock/index.ts +50 -0
  211. package/src/core/master-lock/kdf.spec.ts +95 -0
  212. package/src/core/master-lock/kdf.ts +145 -0
  213. package/src/core/master-lock/policy.ts +55 -0
  214. package/src/core/master-lock/presence.spec.ts +73 -0
  215. package/src/core/master-lock/presence.ts +71 -0
  216. package/src/core/master-lock/wire.ts +155 -0
  217. package/src/core/media/bgRemoval.spec.ts +120 -0
  218. package/src/core/media/bgRemoval.ts +123 -0
  219. package/src/core/media/ffmpeg.spec.ts +135 -0
  220. package/src/core/media/ffmpeg.ts +162 -0
  221. package/src/core/media/format.spec.ts +41 -0
  222. package/src/core/media/format.ts +58 -0
  223. package/src/core/media/index.ts +105 -0
  224. package/src/core/media/mediaItem.spec.ts +46 -0
  225. package/src/core/media/mediaItem.ts +40 -0
  226. package/src/core/media/renderPlan.spec.ts +140 -0
  227. package/src/core/media/renderPlan.ts +318 -0
  228. package/src/core/media/segment.spec.ts +159 -0
  229. package/src/core/media/segment.ts +352 -0
  230. package/src/core/media/size.spec.ts +19 -0
  231. package/src/core/media/size.ts +3 -0
  232. package/src/core/media/trim.spec.ts +75 -0
  233. package/src/core/media/trim.ts +60 -0
  234. package/src/core/media/types.ts +147 -0
  235. package/src/core/media/uploadLimits.ts +18 -0
  236. package/src/core/media/validate.spec.ts +106 -0
  237. package/src/core/media/validate.ts +131 -0
  238. package/src/core/media/videoCuts.spec.ts +327 -0
  239. package/src/core/media/videoCuts.ts +355 -0
  240. package/src/core/navigation/breadcrumbCompat.ts +44 -0
  241. package/src/core/navigation/coverageTest.ts +54 -0
  242. package/src/core/navigation/index.ts +50 -0
  243. package/src/core/navigation/migrateNavPreferences.ts +119 -0
  244. package/src/core/navigation/navigation.spec.ts +388 -0
  245. package/src/core/navigation/navigationModel.ts +185 -0
  246. package/src/core/navigation/navigationSelectors.ts +206 -0
  247. package/src/core/navigation/searchCatalogue.ts +89 -0
  248. package/src/core/net/reconnectSchedule.spec.ts +49 -0
  249. package/src/core/net/reconnectSchedule.ts +78 -0
  250. package/src/core/palette/ShortcutRegistry.ts +69 -0
  251. package/src/core/palette/fuzzyRank.ts +78 -0
  252. package/src/core/palette/hotkeys.ts +132 -0
  253. package/src/core/palette/index.ts +22 -0
  254. package/src/core/palette/palette.spec.ts +160 -0
  255. package/src/core/schema/projection.spec.ts +122 -0
  256. package/src/core/schema/projection.ts +225 -0
  257. package/src/core/session/claims.ts +181 -0
  258. package/src/core/sharing/index.ts +33 -0
  259. package/src/core/sharing/model.spec.ts +104 -0
  260. package/src/core/sharing/model.ts +296 -0
  261. package/src/core/site/siteCopy.ts +115 -0
  262. package/src/core/slots.ts +38 -0
  263. package/src/core/styles/constructableSheet.ts +86 -0
  264. package/src/core/tokens/registry.spec.ts +160 -0
  265. package/src/core/tokens/registry.ts +536 -0
  266. package/src/core/uiScale/index.ts +32 -0
  267. package/src/core/uiScale/scale.ts +63 -0
  268. package/src/core/uiScale/uiScale.spec.ts +77 -0
  269. package/src/core/uiScale/uiScaleStore.ts +100 -0
  270. package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
  271. package/src/core/wire/index.ts +56 -0
  272. package/src/core/wire/ingress.spec.ts +276 -0
  273. package/src/core/wire/ingress.ts +182 -0
  274. package/src/core/wire/marshalWorker.spec.ts +196 -0
  275. package/src/core/wire/marshalWorker.ts +361 -0
  276. package/src/core/wire/marshalling.spec.ts +139 -0
  277. package/src/core/wire/marshalling.ts +238 -0
  278. package/src/core/wire/wireHub.spec.ts +137 -0
  279. package/src/core/wire/wireHub.ts +213 -0
  280. package/src/ctgr/base64url.ts +44 -0
  281. package/src/ctgr/chunkAccumulator.ts +403 -0
  282. package/src/ctgr/chunkStore.spec.ts +101 -0
  283. package/src/ctgr/chunkStore.ts +168 -0
  284. package/src/ctgr/codec.spec.ts +208 -0
  285. package/src/ctgr/codec.ts +202 -0
  286. package/src/ctgr/compress.ts +21 -0
  287. package/src/ctgr/crc32.ts +23 -0
  288. package/src/ctgr/digest.ts +14 -0
  289. package/src/ctgr/index.ts +39 -0
  290. package/src/ctgr/types.ts +80 -0
  291. package/src/ctgr/v0compat.spec.ts +73 -0
  292. package/src/ctgr/v0compat.ts +156 -0
  293. package/src/declaredDepsAreImported.spec.ts +87 -0
  294. package/src/index.ts +25 -0
  295. package/src/leafSubpathsImportNothing.spec.ts +287 -0
  296. package/src/lockfileIsTracked.spec.ts +50 -0
  297. package/src/namedSubpathsResolve.spec.ts +122 -0
  298. package/src/noPathDeps.spec.ts +24 -0
  299. package/src/publishShape.spec.ts +89 -0
  300. package/src/shared/errors.ts +153 -0
  301. package/src/skippedTestsAreAnswered.spec.ts +221 -0
  302. package/src/testTmpRoot.spec.ts +102 -0
  303. package/src/testing/bytes.ts +38 -0
  304. package/src/testing/domInspect.spec.ts +79 -0
  305. package/src/testing/domInspect.ts +127 -0
  306. package/src/testing/fakeFetch.ts +26 -0
  307. package/src/testing/index.ts +11 -0
@@ -0,0 +1,536 @@
1
+ /**
2
+ * The closed token set — the compile-time vocabulary every wire is drawn from
3
+ * (04 §2 V3). A `PortSpec` may only reference a `TokenDef` obtained from this
4
+ * registry; anything hand-declared fails V3 `token-not-registered`, which is
5
+ * what makes "compatible" a decidable property instead of a naming convention.
6
+ *
7
+ * Compatibility is **lattice membership**, not string equality: a wire connects
8
+ * iff `produced ⊑ consumed` under the reflexive-transitive closure of
9
+ * `subtypeOf`. String equality is retired — it forced every producer of a
10
+ * narrower thing (a photo feed) to lie about its type to reach a consumer of
11
+ * the wider thing (a media grid).
12
+ *
13
+ * Staging plugins extend the set at design time through `x-<pluginId>:*` ids
14
+ * (01 §5.2). Three laws keep the extension mechanism from corrupting the core
15
+ * vocabulary, all enforced here and reported by V3:
16
+ * - namespace: an extension id must be `x-<pluginId>:<name>`;
17
+ * - collision: an extension may never redeclare a base id;
18
+ * - inversion: a core token may never declare an extension as its supertype
19
+ * (the core would stop compiling the moment the plugin is deleted).
20
+ */
21
+ import { z } from 'zod';
22
+
23
+ export type TokenMode = 'value' | 'signal';
24
+
25
+ /** JSON-Schema description of a token payload — the unit of structural comparison (J4-D). */
26
+ export type SchemaShape = Record<string, unknown>;
27
+
28
+ export interface TokenDeprecation {
29
+ replacedBy: string;
30
+ /** Free-form marker of when the deprecation started; surfaced in the V3 warning. */
31
+ since: string;
32
+ }
33
+
34
+ export interface TokenDef<S extends z.ZodType = z.ZodType> {
35
+ /** `<namespace>:<name>` — namespace is an engine id or `x-<pluginId>` for staging. */
36
+ id: string;
37
+ mode: TokenMode;
38
+ schema: S;
39
+ /** Parent token id; the single lattice edge. Absent ⇒ a lattice root. */
40
+ subtypeOf?: string;
41
+ /** Canonical payload injected by the V7.4 wire smoke probe. Must satisfy `schema`. */
42
+ sample: z.infer<S>;
43
+ /** True when this token receives third-party traffic — triggers the V3 marshaller gates. */
44
+ external?: boolean;
45
+ deprecated?: TokenDeprecation;
46
+ description: string;
47
+ }
48
+
49
+ /**
50
+ * 02 §7.1 — the only entry point for external payloads.
51
+ *
52
+ * Both symbols are DEFINED in `core/wire/marshalling.ts` beside the runtime that
53
+ * enforces them, and re-exported here because every existing importer (V3,
54
+ * `verify/types.ts`, the fixtures) reaches for them through the token registry.
55
+ * They used to be a second, divergent declaration of the same names — see the
56
+ * unification note on `CCWireMarshaller` (05 §B1).
57
+ */
58
+ import type { CCWireMarshaller } from '../wire/marshalling';
59
+
60
+ export {
61
+ type CCWireMarshaller,
62
+ MARSHALLER_WORKER_OFFLOAD_BYTES,
63
+ requiresWorkerOffload,
64
+ } from '../wire/marshalling';
65
+
66
+ export type TokenRegistryIssueCode =
67
+ | 'token-collision'
68
+ | 'token-namespace-violation'
69
+ | 'subtype-inversion'
70
+ | 'token-supertype-missing'
71
+ | 'token-subtype-cycle'
72
+ | 'token-sample-invalid';
73
+
74
+ export interface TokenRegistryIssue {
75
+ code: TokenRegistryIssueCode;
76
+ tokenId: string;
77
+ message: string;
78
+ /** Set for harvested extensions so V3 can point the diagnostic at the source file. */
79
+ path?: string;
80
+ }
81
+
82
+ const TOKEN_ID_RE = /^[a-z][a-z0-9-]*:[a-z0-9-]+$/;
83
+ const EXTENSION_ID_RE = /^x-[a-z0-9-]+:[a-z0-9-]+$/;
84
+
85
+ export const isExtensionTokenId = (id: string): boolean => id.startsWith('x-');
86
+
87
+ /** `x-habits:mark` ⇒ `habits`; `media:videos` ⇒ `media`. */
88
+ export const namespaceOf = (id: string): string => id.slice(0, id.indexOf(':'));
89
+
90
+ /** `x-habits:mark` ⇒ `plugin-habits`. */
91
+ export const stagingPluginOf = (id: string): string => `plugin-${namespaceOf(id).slice('x-'.length)}`;
92
+
93
+ /**
94
+ * Normalized structural description of a token payload. Two schemas that were
95
+ * authored independently in two repos still compare, which is the whole point of
96
+ * the J4-D fallback: identity is the fast path, structure is the truth.
97
+ */
98
+ export function describeSchema(schema: z.ZodType): SchemaShape {
99
+ const described = z.toJSONSchema(schema, { io: 'output', unrepresentable: 'any' }) as SchemaShape;
100
+ const { $schema: _ignored, ...rest } = described;
101
+ return rest;
102
+ }
103
+
104
+ export type SchemaComparison = 'identical' | 'backward-compatible-extension' | 'divergent';
105
+
106
+ const stableStringify = (value: unknown): string => {
107
+ if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
108
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
109
+ const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
110
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(',')}}`;
111
+ };
112
+
113
+ /**
114
+ * Compare a producer's payload shape against the consumer's canonical shape.
115
+ *
116
+ * `backward-compatible-extension` means the producer satisfies every requirement
117
+ * the consumer states AND everything it adds is optional — a consumer written
118
+ * against the older shape still validates every payload the newer producer
119
+ * emits. Anything else is drift: same token id, different contract, silent
120
+ * runtime breakage (the `media:video-ref` `{id}` vs `{id,name}` class).
121
+ */
122
+ export function compareSchemaShapes(producer: SchemaShape, consumer: SchemaShape): SchemaComparison {
123
+ return compareNodes(producer, consumer);
124
+ }
125
+
126
+ /**
127
+ * The comparison recurses, because the interesting skew is never at the top
128
+ * level: a producer that added an optional `caption` to each item of
129
+ * `{ items: [...] }` differs only three levels down, and a shallow compare would
130
+ * call that drift and block a build for no reason.
131
+ */
132
+ function compareNodes(producer: unknown, consumer: unknown): SchemaComparison {
133
+ if (stableStringify(producer) === stableStringify(consumer)) return 'identical';
134
+ if (typeof producer !== 'object' || producer === null || typeof consumer !== 'object' || consumer === null)
135
+ return 'divergent';
136
+
137
+ const producerNode = producer as Record<string, unknown>;
138
+ const consumerNode = consumer as Record<string, unknown>;
139
+
140
+ if (producerNode.type === 'array' && consumerNode.type === 'array') return compareNodes(producerNode.items, consumerNode.items);
141
+
142
+ const producerProps = (producerNode.properties ?? null) as Record<string, unknown> | null;
143
+ const consumerProps = (consumerNode.properties ?? null) as Record<string, unknown> | null;
144
+ if (producerNode.type !== 'object' || consumerNode.type !== 'object' || !producerProps || !consumerProps) return 'divergent';
145
+
146
+ const producerRequired = new Set((producerNode.required as string[] | undefined) ?? []);
147
+ const consumerRequired = new Set((consumerNode.required as string[] | undefined) ?? []);
148
+
149
+ const widen = (current: SchemaComparison, next: SchemaComparison): SchemaComparison => {
150
+ if (current === 'divergent' || next === 'divergent') return 'divergent';
151
+ if (current === 'backward-compatible-extension' || next === 'backward-compatible-extension')
152
+ return 'backward-compatible-extension';
153
+ return 'identical';
154
+ };
155
+
156
+ let verdict: SchemaComparison = 'identical';
157
+
158
+ for (const [key, consumerProp] of Object.entries(consumerProps)) {
159
+ const producerProp = producerProps[key];
160
+ if (producerProp === undefined) return 'divergent'; // a field the consumer needs and the producer never emits
161
+ if (consumerRequired.has(key) && !producerRequired.has(key)) return 'divergent'; // may arrive absent
162
+ verdict = widen(verdict, compareNodes(producerProp, consumerProp));
163
+ if (verdict === 'divergent') return 'divergent';
164
+ }
165
+
166
+ for (const key of Object.keys(producerProps)) {
167
+ if (key in consumerProps) continue;
168
+ // A required extra field is NOT backward compatible: a consumer that echoes
169
+ // the payload back through the same token would fail the producer's schema.
170
+ if (producerRequired.has(key)) return 'divergent';
171
+ verdict = widen(verdict, 'backward-compatible-extension');
172
+ }
173
+
174
+ return verdict;
175
+ }
176
+
177
+ export interface ConnectResult {
178
+ ok: boolean;
179
+ reason?: 'unknown-produced' | 'unknown-consumed' | 'mode-mismatch' | 'not-a-subtype';
180
+ message?: string;
181
+ }
182
+
183
+ export class TokenRegistry {
184
+ private readonly tokens = new Map<string, TokenDef>();
185
+ private readonly marshallers = new Map<string, CCWireMarshaller>();
186
+ private readonly shapes = new Map<string, SchemaShape>();
187
+
188
+ constructor(base: readonly TokenDef[] = []) {
189
+ const issues = this.registerAll(base, { origin: 'base' });
190
+ if (issues.length > 0) {
191
+ // Base tokens are authored code, not user data — a broken base registry is a
192
+ // programming error that must never reach a diagnostic list.
193
+ throw new Error(`Invalid base token registry: ${issues.map((i) => `${i.code} (${i.tokenId})`).join(', ')}`);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Register harvested staging extensions. Returns issues rather than throwing:
199
+ * these ids come from files on disk, so they are data and belong in the
200
+ * diagnostic list (V3), not in a crash.
201
+ */
202
+ registerAll(defs: readonly TokenDef[], opts: { origin: 'base' | 'staging'; path?: string } = { origin: 'staging' }): TokenRegistryIssue[] {
203
+ const issues: TokenRegistryIssue[] = [];
204
+ const accepted: TokenDef[] = [];
205
+
206
+ for (const def of defs) {
207
+ const path = opts.path;
208
+ // Collision is checked BEFORE the namespace laws: an extension that
209
+ // redeclares `media:videos` is a collision, and saying "bad namespace"
210
+ // would send the author to fix the wrong thing.
211
+ if (this.tokens.has(def.id)) {
212
+ issues.push({
213
+ code: 'token-collision',
214
+ tokenId: def.id,
215
+ path,
216
+ message: `Token '${def.id}' is already declared by the base registry — a staging extension may extend the vocabulary, never redefine it.`,
217
+ });
218
+ continue;
219
+ }
220
+ if (!TOKEN_ID_RE.test(def.id)) {
221
+ issues.push({
222
+ code: 'token-namespace-violation',
223
+ tokenId: def.id,
224
+ path,
225
+ message: `Token id '${def.id}' is not '<namespace>:<name>' in kebab-case.`,
226
+ });
227
+ continue;
228
+ }
229
+ if (opts.origin === 'staging' && !EXTENSION_ID_RE.test(def.id)) {
230
+ issues.push({
231
+ code: 'token-namespace-violation',
232
+ tokenId: def.id,
233
+ path,
234
+ message: `Staging token '${def.id}' must be namespaced 'x-<pluginId>:<name>'.`,
235
+ });
236
+ continue;
237
+ }
238
+ if (opts.origin === 'base' && isExtensionTokenId(def.id)) {
239
+ issues.push({
240
+ code: 'token-namespace-violation',
241
+ tokenId: def.id,
242
+ path,
243
+ message: `Base token '${def.id}' may not use the reserved staging 'x-' namespace.`,
244
+ });
245
+ continue;
246
+ }
247
+ const sampleCheck = def.schema.safeParse(def.sample);
248
+ if (!sampleCheck.success) {
249
+ issues.push({
250
+ code: 'token-sample-invalid',
251
+ tokenId: def.id,
252
+ path,
253
+ message: `Token '${def.id}' sample payload does not satisfy its own schema: ${sampleCheck.error.issues[0]?.message ?? 'invalid'}.`,
254
+ });
255
+ continue;
256
+ }
257
+ this.tokens.set(def.id, def);
258
+ this.shapes.set(def.id, describeSchema(def.schema));
259
+ accepted.push(def);
260
+ }
261
+
262
+ // Lattice edges are validated after the batch lands: a staging catalog may
263
+ // declare a child before its parent within the same file.
264
+ for (const def of accepted) {
265
+ const parentId = def.subtypeOf;
266
+ if (parentId === undefined) continue;
267
+ const parent = this.tokens.get(parentId);
268
+ if (!parent) {
269
+ issues.push({
270
+ code: 'token-supertype-missing',
271
+ tokenId: def.id,
272
+ path: opts.path,
273
+ message: `Token '${def.id}' declares supertype '${parentId}', which is not registered.`,
274
+ });
275
+ this.tokens.delete(def.id);
276
+ this.shapes.delete(def.id);
277
+ continue;
278
+ }
279
+ if (!isExtensionTokenId(def.id) && isExtensionTokenId(parentId)) {
280
+ issues.push({
281
+ code: 'subtype-inversion',
282
+ tokenId: def.id,
283
+ path: opts.path,
284
+ message: `Core token '${def.id}' declares staging extension '${parentId}' as its supertype — the core would stop compiling when the plugin is removed.`,
285
+ });
286
+ this.tokens.delete(def.id);
287
+ this.shapes.delete(def.id);
288
+ continue;
289
+ }
290
+ if (this.hasSubtypeCycle(def.id)) {
291
+ issues.push({
292
+ code: 'token-subtype-cycle',
293
+ tokenId: def.id,
294
+ path: opts.path,
295
+ message: `Token '${def.id}' participates in a subtypeOf cycle.`,
296
+ });
297
+ this.tokens.delete(def.id);
298
+ this.shapes.delete(def.id);
299
+ }
300
+ }
301
+
302
+ return issues;
303
+ }
304
+
305
+ registerMarshaller(marshaller: CCWireMarshaller): void {
306
+ this.marshallers.set(marshaller.tokenId, marshaller);
307
+ }
308
+
309
+ get(id: string): TokenDef | undefined {
310
+ return this.tokens.get(id);
311
+ }
312
+
313
+ has(id: string): boolean {
314
+ return this.tokens.has(id);
315
+ }
316
+
317
+ ids(): string[] {
318
+ return [...this.tokens.keys()].sort();
319
+ }
320
+
321
+ marshallerFor(tokenId: string): CCWireMarshaller | undefined {
322
+ return this.marshallers.get(tokenId);
323
+ }
324
+
325
+ shapeOf(tokenId: string): SchemaShape | undefined {
326
+ return this.shapes.get(tokenId);
327
+ }
328
+
329
+ /** The reflexive-transitive closure of `subtypeOf`: is `childId ⊑ parentId`? */
330
+ isSubtype(childId: string, parentId: string): boolean {
331
+ let cursor: string | undefined = childId;
332
+ const seen = new Set<string>();
333
+ while (cursor !== undefined) {
334
+ if (cursor === parentId) return true;
335
+ if (seen.has(cursor)) return false;
336
+ seen.add(cursor);
337
+ cursor = this.tokens.get(cursor)?.subtypeOf;
338
+ }
339
+ return false;
340
+ }
341
+
342
+ /** Every ancestor of a token, nearest first — used by V3 diagnostics. */
343
+ ancestorsOf(tokenId: string): string[] {
344
+ const chain: string[] = [];
345
+ const seen = new Set<string>();
346
+ let cursor = this.tokens.get(tokenId)?.subtypeOf;
347
+ while (cursor !== undefined && !seen.has(cursor)) {
348
+ chain.push(cursor);
349
+ seen.add(cursor);
350
+ cursor = this.tokens.get(cursor)?.subtypeOf;
351
+ }
352
+ return chain;
353
+ }
354
+
355
+ canConnect(producedId: string, consumedId: string): ConnectResult {
356
+ const produced = this.tokens.get(producedId);
357
+ const consumed = this.tokens.get(consumedId);
358
+ if (!produced) return { ok: false, reason: 'unknown-produced', message: `Token '${producedId}' is not registered.` };
359
+ if (!consumed) return { ok: false, reason: 'unknown-consumed', message: `Token '${consumedId}' is not registered.` };
360
+ if (produced.mode !== consumed.mode)
361
+ return {
362
+ ok: false,
363
+ reason: 'mode-mismatch',
364
+ message: `'${producedId}' is a ${produced.mode} port and '${consumedId}' is a ${consumed.mode} port.`,
365
+ };
366
+ if (!this.isSubtype(producedId, consumedId))
367
+ return {
368
+ ok: false,
369
+ reason: 'not-a-subtype',
370
+ message: `'${producedId}' is not '${consumedId}' or a subtype of it (lattice: ${
371
+ [producedId, ...this.ancestorsOf(producedId)].join(' ⊑ ')
372
+ }).`,
373
+ };
374
+ return { ok: true };
375
+ }
376
+
377
+ private hasSubtypeCycle(startId: string): boolean {
378
+ const seen = new Set<string>();
379
+ let cursor: string | undefined = startId;
380
+ while (cursor !== undefined) {
381
+ if (seen.has(cursor)) return true;
382
+ seen.add(cursor);
383
+ cursor = this.tokens.get(cursor)?.subtypeOf;
384
+ }
385
+ return false;
386
+ }
387
+ }
388
+
389
+ /* ---------------------------------------------------------------------------
390
+ * The base registry. Every id here is part of the app's public wire vocabulary;
391
+ * removing one is a breaking change that goes through the deprecation flow
392
+ * (02 §3.3 step 3), never a delete.
393
+ * ------------------------------------------------------------------------ */
394
+
395
+ const MediaItemSchema = z.object({
396
+ id: z.string(),
397
+ kind: z.enum(['video', 'photo', 'file', 'audio']),
398
+ });
399
+
400
+ const VideoItemSchema = z.object({
401
+ id: z.string(),
402
+ kind: z.enum(['video', 'photo', 'file', 'audio']),
403
+ streamUrl: z.string().optional(),
404
+ hlsUrl: z.string().optional(),
405
+ hasHls: z.boolean().optional(),
406
+ });
407
+
408
+ export const BASE_TOKENS: readonly TokenDef[] = [
409
+ {
410
+ id: 'media:items',
411
+ mode: 'value',
412
+ description: 'Any addressable media collection — the lattice root every media feed narrows.',
413
+ schema: z.object({ items: z.array(MediaItemSchema) }),
414
+ sample: { items: [{ id: 'itm_1', kind: 'file' as const }] },
415
+ },
416
+ {
417
+ id: 'media:videos',
418
+ mode: 'value',
419
+ subtypeOf: 'media:items',
420
+ description: 'Video collection with per-item playback URLs; degrades to progressive when hls is off.',
421
+ schema: z.object({ items: z.array(VideoItemSchema) }),
422
+ sample: { items: [{ id: 'vid_1', kind: 'video' as const, streamUrl: '/api/asset/stream/vid_1' }] },
423
+ },
424
+ {
425
+ id: 'media:photos',
426
+ mode: 'value',
427
+ subtypeOf: 'media:items',
428
+ description: 'Photo collection.',
429
+ schema: z.object({ items: z.array(MediaItemSchema) }),
430
+ sample: { items: [{ id: 'pho_1', kind: 'photo' as const }] },
431
+ },
432
+ {
433
+ id: 'media:video-ref',
434
+ mode: 'value',
435
+ description: 'A single video selection handed between a gallery and an editor.',
436
+ schema: z.object({ id: z.string(), name: z.string().optional() }),
437
+ sample: { id: 'vid_1' },
438
+ },
439
+ {
440
+ id: 'asset:file-ref',
441
+ mode: 'value',
442
+ description: 'A single stored file reference.',
443
+ schema: z.object({ id: z.string(), mime: z.string().optional() }),
444
+ sample: { id: 'file_1' },
445
+ },
446
+ {
447
+ id: 'text:content',
448
+ mode: 'value',
449
+ description: 'Plain text body — diff panes, note editors, transcript views.',
450
+ schema: z.object({ text: z.string() }),
451
+ sample: { text: 'hello' },
452
+ },
453
+ {
454
+ id: 'signal:refresh',
455
+ mode: 'signal',
456
+ description: 'Scoped invalidation pulse. Fan-in target: every emission must carry a scope (03 §6.1).',
457
+ schema: z.object({ scope: z.string().optional() }),
458
+ sample: { scope: 'videos' },
459
+ },
460
+ {
461
+ id: 'session:private-mode',
462
+ mode: 'value',
463
+ description: 'GuardEngine session assertion. Advisory on the client; every route re-verifies (02 §4.2).',
464
+ schema: z.object({
465
+ unlocked: z.boolean(),
466
+ token: z.string().nullable(),
467
+ claims: z.unknown().nullable(),
468
+ }),
469
+ sample: { unlocked: false, token: null, claims: null },
470
+ },
471
+ {
472
+ id: 'nav:path',
473
+ mode: 'value',
474
+ description: 'Current client route path — replaces the deleted Breadcrumbs plugin.',
475
+ schema: z.object({ path: z.string() }),
476
+ sample: { path: '/' },
477
+ },
478
+ {
479
+ id: 'knowledge:conversation-ref',
480
+ mode: 'value',
481
+ description: 'A RAG conversation reference; deep-link entity for /chat/:conversationId.',
482
+ schema: z.object({ id: z.string() }),
483
+ sample: { id: 'conv_1' },
484
+ },
485
+ {
486
+ id: 'knowledge:note-ref',
487
+ mode: 'value',
488
+ description: 'A note reference; deep-link entity for note routes.',
489
+ schema: z.object({ id: z.string() }),
490
+ sample: { id: 'note_1' },
491
+ },
492
+ {
493
+ id: 'knowledge:records',
494
+ mode: 'value',
495
+ external: true,
496
+ description: 'Imported record rows (CSV/sheet/webhook) — external traffic, marshaller-gated (02 §7.1).',
497
+ schema: z.object({ rows: z.array(z.record(z.string(), z.unknown())) }),
498
+ sample: { rows: [{ id: '1' }] },
499
+ },
500
+ {
501
+ id: 'calendar:reminder',
502
+ mode: 'signal',
503
+ description: 'A due reminder pulse consumed by the notifications inbox.',
504
+ schema: z.object({ scope: z.string().optional(), at: z.number() }),
505
+ sample: { at: 0 },
506
+ },
507
+ {
508
+ id: 'stopwatch:toggled',
509
+ mode: 'signal',
510
+ description: 'Stopwatch widget start/stop pulse.',
511
+ schema: z.object({ running: z.boolean() }),
512
+ sample: { running: false },
513
+ },
514
+ {
515
+ id: 'dev:job-status',
516
+ mode: 'value',
517
+ description: 'JobBroker progress for a deploy/pipeline/promotion job.',
518
+ schema: z.object({
519
+ jobId: z.string(),
520
+ state: z.enum(['queued', 'running', 'done', 'failed']),
521
+ rev: z.number().optional(),
522
+ }),
523
+ sample: { jobId: 'job_1', state: 'queued' as const },
524
+ },
525
+ {
526
+ id: 'nes:rom-url',
527
+ mode: 'value',
528
+ deprecated: { replacedBy: 'asset:file-ref', since: 'core-refactor' },
529
+ description: 'DEPRECATED — the NES toy was pruned from the core catalog (02 §1.6).',
530
+ schema: z.object({ url: z.string() }),
531
+ sample: { url: 'about:blank' },
532
+ },
533
+ ];
534
+
535
+ /** Fresh registry per compilation — the compiler never shares mutable state between runs. */
536
+ export const createTokenRegistry = (): TokenRegistry => new TokenRegistry(BASE_TOKENS);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * UI-scale core (framework-free) — the pure model + the document/localStorage store
3
+ * behind the app-wide "text/UI size" accessibility control. `initUiScale()` applies
4
+ * the saved size on app boot (before React mounts → no flash); the React control +
5
+ * hook live in `cursedbelt/react`.
6
+ */
7
+ export {
8
+ clampUiScale,
9
+ DEFAULT_UI_SCALE,
10
+ MAX_UI_SCALE,
11
+ MIN_UI_SCALE,
12
+ nearestUiScaleLevel,
13
+ stepUiScale,
14
+ UI_SCALE_LEVELS,
15
+ UI_SCALE_STEP,
16
+ type UiScaleLevel,
17
+ type UiScaleLevelId,
18
+ uiScaleToFontSize,
19
+ } from './scale';
20
+ export {
21
+ applyUiScaleToDocument,
22
+ createUiScaleStore,
23
+ getUiScale,
24
+ initUiScale,
25
+ setUiScale,
26
+ subscribeUiScale,
27
+ UI_SCALE_STORAGE_KEY,
28
+ type UiScaleStorage,
29
+ type UiScaleStore,
30
+ type UiScaleStoreOptions,
31
+ uiScaleStore,
32
+ } from './uiScaleStore';
@@ -0,0 +1,63 @@
1
+ /**
2
+ * UI scale — the pure model behind an app-wide "text/UI size" accessibility
3
+ * control. A single number multiplies the document's root font-size, so every
4
+ * **rem-based** Tailwind utility (text, spacing, gaps, rem-sized icons) grows or
5
+ * shrinks together. Because the whole UI scales proportionally — like browser
6
+ * zoom — relative spacing, alignment, and source order are preserved by
7
+ * construction: there's no per-element override that could reflow things into an
8
+ * overlap or reorder a label below its section. (Anything sized in raw px won't
9
+ * scale; cwip's components are rem-first so they do.)
10
+ *
11
+ * This file is framework- and DOM-free so it's trivially testable; the React hook
12
+ * + the store that actually touches `document`/`localStorage` build on it.
13
+ */
14
+
15
+ export type UiScaleLevelId = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
16
+
17
+ export interface UiScaleLevel {
18
+ readonly id: UiScaleLevelId;
19
+ readonly label: string;
20
+ /** Root-font-size multiplier (1 = the browser/app default). */
21
+ readonly scale: number;
22
+ }
23
+
24
+ /** Named presets for a simple picker. Continuous values in between are valid too. */
25
+ export const UI_SCALE_LEVELS: readonly UiScaleLevel[] = [
26
+ { id: 'sm', label: 'Small', scale: 0.9 },
27
+ { id: 'md', label: 'Default', scale: 1 },
28
+ { id: 'lg', label: 'Large', scale: 1.15 },
29
+ { id: 'xl', label: 'Larger', scale: 1.3 },
30
+ { id: 'xxl', label: 'Largest', scale: 1.5 },
31
+ ];
32
+
33
+ export const MIN_UI_SCALE = 0.8;
34
+ export const MAX_UI_SCALE = 2;
35
+ export const DEFAULT_UI_SCALE = 1;
36
+ /** One press of the +/- stepper. */
37
+ export const UI_SCALE_STEP = 0.1;
38
+
39
+ /** Clamp to the supported range, rounded to 3 decimals; non-finite → default. */
40
+ export function clampUiScale(n: number): number {
41
+ if (!Number.isFinite(n)) return DEFAULT_UI_SCALE;
42
+ return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(n * 1000) / 1000));
43
+ }
44
+
45
+ /** Nudge a scale up (+1) or down (-1) by one step, clamped. */
46
+ export function stepUiScale(scale: number, dir: 1 | -1): number {
47
+ return clampUiScale(scale + dir * UI_SCALE_STEP);
48
+ }
49
+
50
+ /** The preset closest to a (continuous) scale — drives a picker's active state. */
51
+ export function nearestUiScaleLevel(scale: number): UiScaleLevel {
52
+ const s = clampUiScale(scale);
53
+ return UI_SCALE_LEVELS.reduce((best, level) =>
54
+ Math.abs(level.scale - s) < Math.abs(best.scale - s) ? level : best,
55
+ );
56
+ }
57
+
58
+ /** The CSS `font-size` value to set on the root, e.g. 1.15 → "115%". Percent (not
59
+ * px) so it composes with the user's own browser base-font preference. */
60
+ export function uiScaleToFontSize(scale: number): string {
61
+ const pct = Math.round(clampUiScale(scale) * 1000) / 10; // 1.15 → 115, 0.9 → 90
62
+ return `${pct}%`;
63
+ }