@kitn.ai/ui 0.28.0 → 0.29.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 (376) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-tooling/construct/public.d.ts +15 -0
  3. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  4. package/dist/construct-cli.es.js +3 -3
  5. package/dist/construct.d.ts +5 -0
  6. package/dist/construct.js +327 -0
  7. package/dist/core-C8fzo39E.js +1 -12
  8. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  9. package/dist/mcp.es.js +1 -1
  10. package/package.json +16 -16
  11. package/bin/route.test.js +0 -58
  12. package/src/agent-tooling/README.md +0 -192
  13. package/src/agent-tooling/archetypes.ts +0 -205
  14. package/src/agent-tooling/catalog/README.md +0 -676
  15. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  16. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  17. package/src/agent-tooling/catalog/invariants.ts +0 -284
  18. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  19. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  20. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  21. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  22. package/src/agent-tooling/construct/cli.ts +0 -147
  23. package/src/agent-tooling/construct/codegen.ts +0 -1635
  24. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  25. package/src/agent-tooling/construct/dev.ts +0 -148
  26. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  27. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  28. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  29. package/src/agent-tooling/construct/schema.ts +0 -452
  30. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  31. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  32. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  33. package/src/agent-tooling/integrations/mastra.ts +0 -182
  34. package/src/agent-tooling/integrations/mock.ts +0 -148
  35. package/src/agent-tooling/integrations/ollama.ts +0 -97
  36. package/src/agent-tooling/integrations/openai.ts +0 -95
  37. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  38. package/src/agent-tooling/integrations/pi.ts +0 -176
  39. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  40. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  41. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  42. package/src/agent-tooling/mcp/manifest.ts +0 -422
  43. package/src/agent-tooling/mcp/server.ts +0 -116
  44. package/src/agent-tooling/mcp/stdio.ts +0 -18
  45. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  46. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  47. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  48. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  49. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  50. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  51. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  52. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  53. package/src/agent-tooling/recipes/index.ts +0 -22
  54. package/src/agent-tooling/recipes/types.ts +0 -32
  55. package/src/agent-tooling/registry.ts +0 -132
  56. package/src/agent-tooling/route-emit.ts +0 -305
  57. package/src/agent-tooling/types.ts +0 -390
  58. package/src/components/artifact-card.tsx +0 -120
  59. package/src/components/artifact.tsx +0 -914
  60. package/src/components/attachment-types.ts +0 -66
  61. package/src/components/attachments.tsx +0 -536
  62. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  63. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  64. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  65. package/src/components/audio-visualizer/index.tsx +0 -528
  66. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  67. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  68. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  69. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  70. package/src/components/audio-visualizer/sizes.ts +0 -54
  71. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  72. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  73. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  74. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  75. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  76. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  77. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  78. package/src/components/card-fallback.tsx +0 -60
  79. package/src/components/card-renderer.tsx +0 -220
  80. package/src/components/card.tsx +0 -212
  81. package/src/components/chain-of-thought.tsx +0 -348
  82. package/src/components/chat-container.tsx +0 -89
  83. package/src/components/chat-scope-picker.tsx +0 -57
  84. package/src/components/chat-thread.tsx +0 -1014
  85. package/src/components/checkpoint.tsx +0 -88
  86. package/src/components/choice-card.tsx +0 -743
  87. package/src/components/coachmark.tsx +0 -249
  88. package/src/components/code-block.tsx +0 -181
  89. package/src/components/composer-dom.ts +0 -159
  90. package/src/components/composer-highlight.ts +0 -242
  91. package/src/components/composer-history.ts +0 -82
  92. package/src/components/composer.tsx +0 -1075
  93. package/src/components/confirm-card.tsx +0 -402
  94. package/src/components/context.tsx +0 -377
  95. package/src/components/conversation-item.tsx +0 -241
  96. package/src/components/conversation-list.tsx +0 -429
  97. package/src/components/conversation-panel.tsx +0 -118
  98. package/src/components/dismissed-stub.tsx +0 -80
  99. package/src/components/embed.tsx +0 -200
  100. package/src/components/empty.tsx +0 -177
  101. package/src/components/feedback-bar.tsx +0 -166
  102. package/src/components/file-tree.tsx +0 -530
  103. package/src/components/file-upload.tsx +0 -167
  104. package/src/components/form-widgets.tsx +0 -525
  105. package/src/components/form.tsx +0 -1296
  106. package/src/components/home-panel.tsx +0 -159
  107. package/src/components/image.tsx +0 -65
  108. package/src/components/link-preview.tsx +0 -195
  109. package/src/components/loader.tsx +0 -367
  110. package/src/components/markdown.tsx +0 -162
  111. package/src/components/message-skills.tsx +0 -40
  112. package/src/components/message.tsx +0 -765
  113. package/src/components/model-switcher.tsx +0 -132
  114. package/src/components/prompt-input.tsx +0 -188
  115. package/src/components/prompt-suggestion.tsx +0 -150
  116. package/src/components/reasoning.tsx +0 -293
  117. package/src/components/response-compare-types.ts +0 -152
  118. package/src/components/response-compare.tsx +0 -455
  119. package/src/components/response-stream.tsx +0 -109
  120. package/src/components/screen.tsx +0 -181
  121. package/src/components/scroll-button.tsx +0 -93
  122. package/src/components/source.tsx +0 -192
  123. package/src/components/tasks-card.tsx +0 -801
  124. package/src/components/text-shimmer.tsx +0 -37
  125. package/src/components/thinking-bar.tsx +0 -50
  126. package/src/components/thread.tsx +0 -228
  127. package/src/components/toast.tsx +0 -503
  128. package/src/components/tool-classify.ts +0 -26
  129. package/src/components/tool-types.ts +0 -49
  130. package/src/components/tool.tsx +0 -190
  131. package/src/components/use-card-resolution.ts +0 -44
  132. package/src/components/voice-input.tsx +0 -222
  133. package/src/components/voice-output.tsx +0 -242
  134. package/src/components/widget-tab-bar.tsx +0 -73
  135. package/src/components/workspace-shell.tsx +0 -285
  136. package/src/diagnostics/hook.ts +0 -352
  137. package/src/diagnostics/index.ts +0 -82
  138. package/src/diagnostics/report-request.ts +0 -296
  139. package/src/elements/agent-card.tsx +0 -67
  140. package/src/elements/artifact.tsx +0 -194
  141. package/src/elements/attachments.tsx +0 -140
  142. package/src/elements/audio-visualizer.tsx +0 -227
  143. package/src/elements/autoloader.ts +0 -89
  144. package/src/elements/avatar.tsx +0 -33
  145. package/src/elements/badge.tsx +0 -32
  146. package/src/elements/button.tsx +0 -149
  147. package/src/elements/card.tsx +0 -133
  148. package/src/elements/cards.tsx +0 -266
  149. package/src/elements/chain-of-thought.tsx +0 -141
  150. package/src/elements/chat-scope-picker.tsx +0 -65
  151. package/src/elements/chat-types.ts +0 -102
  152. package/src/elements/chat-workspace.tsx +0 -188
  153. package/src/elements/chat.tsx +0 -304
  154. package/src/elements/checkbox-group.tsx +0 -164
  155. package/src/elements/checkbox.tsx +0 -134
  156. package/src/elements/checkpoint.tsx +0 -47
  157. package/src/elements/choice.tsx +0 -100
  158. package/src/elements/coachmark.tsx +0 -90
  159. package/src/elements/code-block.tsx +0 -60
  160. package/src/elements/command.tsx +0 -197
  161. package/src/elements/compare.tsx +0 -111
  162. package/src/elements/compiled.css +0 -2
  163. package/src/elements/composer.tsx +0 -162
  164. package/src/elements/confirm-card.tsx +0 -76
  165. package/src/elements/context-meter.tsx +0 -107
  166. package/src/elements/conversation-item.tsx +0 -120
  167. package/src/elements/conversation-list.tsx +0 -203
  168. package/src/elements/css.ts +0 -5
  169. package/src/elements/default-input.tsx +0 -392
  170. package/src/elements/define-entry.ts +0 -12
  171. package/src/elements/define.tsx +0 -563
  172. package/src/elements/diagnostic-events.ts +0 -114
  173. package/src/elements/dialog.tsx +0 -145
  174. package/src/elements/disclosure.ts +0 -95
  175. package/src/elements/dock.tsx +0 -215
  176. package/src/elements/dropdown.tsx +0 -123
  177. package/src/elements/editable-label.tsx +0 -140
  178. package/src/elements/element-data-types.ts +0 -152
  179. package/src/elements/element-diagnostics.ts +0 -392
  180. package/src/elements/element-manifest.json +0 -354
  181. package/src/elements/element-nonscalar.json +0 -187
  182. package/src/elements/element-types.d.ts +0 -4353
  183. package/src/elements/embed.tsx +0 -35
  184. package/src/elements/empty.tsx +0 -29
  185. package/src/elements/feedback-bar.tsx +0 -66
  186. package/src/elements/file-tree.tsx +0 -62
  187. package/src/elements/file-upload.tsx +0 -44
  188. package/src/elements/form.tsx +0 -94
  189. package/src/elements/hover-card.tsx +0 -80
  190. package/src/elements/icon.tsx +0 -41
  191. package/src/elements/image.tsx +0 -32
  192. package/src/elements/input.tsx +0 -492
  193. package/src/elements/kbd.tsx +0 -45
  194. package/src/elements/link-preview.tsx +0 -34
  195. package/src/elements/loader.tsx +0 -25
  196. package/src/elements/markdown.tsx +0 -38
  197. package/src/elements/menu.tsx +0 -241
  198. package/src/elements/message-skills.tsx +0 -83
  199. package/src/elements/message.tsx +0 -361
  200. package/src/elements/model-switcher.tsx +0 -121
  201. package/src/elements/nav.tsx +0 -87
  202. package/src/elements/notice.tsx +0 -53
  203. package/src/elements/pane-grid.tsx +0 -111
  204. package/src/elements/pane-group.tsx +0 -119
  205. package/src/elements/pane.tsx +0 -129
  206. package/src/elements/popover.tsx +0 -81
  207. package/src/elements/progress-bar.tsx +0 -39
  208. package/src/elements/prompt-dock.tsx +0 -82
  209. package/src/elements/prompt-input.tsx +0 -240
  210. package/src/elements/prompt-suggestions.tsx +0 -113
  211. package/src/elements/radio-group.tsx +0 -129
  212. package/src/elements/reasoning.tsx +0 -71
  213. package/src/elements/register-impl.ts +0 -148
  214. package/src/elements/register.ts +0 -76
  215. package/src/elements/remote.tsx +0 -225
  216. package/src/elements/resizable.globals.d.ts +0 -36
  217. package/src/elements/resizable.tsx +0 -704
  218. package/src/elements/response-stream.tsx +0 -40
  219. package/src/elements/screen.tsx +0 -90
  220. package/src/elements/scroll-area.tsx +0 -33
  221. package/src/elements/scroll-button.tsx +0 -179
  222. package/src/elements/search.tsx +0 -185
  223. package/src/elements/segmented.tsx +0 -129
  224. package/src/elements/select.tsx +0 -165
  225. package/src/elements/separator.tsx +0 -36
  226. package/src/elements/setting-item.tsx +0 -50
  227. package/src/elements/settings-group.tsx +0 -37
  228. package/src/elements/skeleton.tsx +0 -45
  229. package/src/elements/slider.tsx +0 -171
  230. package/src/elements/slot-text.ts +0 -73
  231. package/src/elements/slots.ts +0 -787
  232. package/src/elements/source.tsx +0 -126
  233. package/src/elements/status.tsx +0 -42
  234. package/src/elements/styles.css +0 -328
  235. package/src/elements/switch.tsx +0 -118
  236. package/src/elements/tabs.tsx +0 -95
  237. package/src/elements/tasks.tsx +0 -103
  238. package/src/elements/text-shimmer.tsx +0 -28
  239. package/src/elements/thinking-bar.tsx +0 -43
  240. package/src/elements/thread.tsx +0 -139
  241. package/src/elements/toast.tsx +0 -106
  242. package/src/elements/tool.tsx +0 -78
  243. package/src/elements/tooltip.tsx +0 -85
  244. package/src/elements/validate-messages.ts +0 -76
  245. package/src/elements/voice-input.tsx +0 -100
  246. package/src/elements/voice-output.tsx +0 -82
  247. package/src/index.ts +0 -430
  248. package/src/primitives/audio-bands.ts +0 -174
  249. package/src/primitives/card-component-types.ts +0 -63
  250. package/src/primitives/card-contract.ts +0 -80
  251. package/src/primitives/card-data-types.ts +0 -361
  252. package/src/primitives/card-host.tsx +0 -37
  253. package/src/primitives/card-recovery.ts +0 -160
  254. package/src/primitives/card-registry.tsx +0 -78
  255. package/src/primitives/card-resolution.ts +0 -36
  256. package/src/primitives/card-routing.ts +0 -85
  257. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  258. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  259. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  260. package/src/primitives/card-schemas/choice.schema.json +0 -75
  261. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  262. package/src/primitives/card-schemas/embed.schema.json +0 -63
  263. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  264. package/src/primitives/card-schemas/form.schema.json +0 -53
  265. package/src/primitives/card-schemas/link.schema.json +0 -54
  266. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  267. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  268. package/src/primitives/card-tags.ts +0 -61
  269. package/src/primitives/card-validate-cards.ts +0 -255
  270. package/src/primitives/card-validate-schemas.ts +0 -58
  271. package/src/primitives/card-validate.ts +0 -131
  272. package/src/primitives/chat-config.tsx +0 -103
  273. package/src/primitives/composer-model.ts +0 -49
  274. package/src/primitives/composer-triggers.ts +0 -35
  275. package/src/primitives/controllable.ts +0 -21
  276. package/src/primitives/conversation-store.ts +0 -248
  277. package/src/primitives/create-kai-chat.ts +0 -66
  278. package/src/primitives/create-tween.ts +0 -266
  279. package/src/primitives/embed-providers.ts +0 -254
  280. package/src/primitives/field-mask.ts +0 -256
  281. package/src/primitives/field-semantics.ts +0 -115
  282. package/src/primitives/highlighter.ts +0 -157
  283. package/src/primitives/input-mask.ts +0 -853
  284. package/src/primitives/link-preview.ts +0 -87
  285. package/src/primitives/message-feedback.ts +0 -124
  286. package/src/primitives/pdf-preview.ts +0 -121
  287. package/src/primitives/toast-store.ts +0 -338
  288. package/src/primitives/url-scheme-policy.ts +0 -70
  289. package/src/primitives/use-audio-analysis.ts +0 -340
  290. package/src/primitives/use-auto-resize.ts +0 -31
  291. package/src/primitives/use-resize-observer.ts +0 -45
  292. package/src/primitives/use-sequencer.ts +0 -72
  293. package/src/primitives/use-speech-recognition.ts +0 -146
  294. package/src/primitives/use-stick-to-bottom.ts +0 -75
  295. package/src/primitives/use-text-stream.ts +0 -143
  296. package/src/primitives/use-voice-recorder.ts +0 -62
  297. package/src/primitives/visualizer-sequences.ts +0 -350
  298. package/src/remote/host-embed.ts +0 -345
  299. package/src/remote/index.ts +0 -2
  300. package/src/remote/origin.ts +0 -30
  301. package/src/remote/provider-runtime.ts +0 -262
  302. package/src/remote/provider.ts +0 -2
  303. package/src/remote/validate.ts +0 -22
  304. package/src/remote/version.ts +0 -12
  305. package/src/remote/wire.ts +0 -48
  306. package/src/schemas/from-tool-call.ts +0 -151
  307. package/src/schemas/index.ts +0 -341
  308. package/src/schemas/provider-subsets.ts +0 -538
  309. package/src/schemas/registry.ts +0 -355
  310. package/src/schemas/tool-defs.ts +0 -793
  311. package/src/solid.ts +0 -173
  312. package/src/state/index.ts +0 -44
  313. package/src/state/messages.ts +0 -53
  314. package/src/state/mock.ts +0 -297
  315. package/src/state/parts.ts +0 -295
  316. package/src/state/persistence.ts +0 -180
  317. package/src/state/stream.ts +0 -246
  318. package/src/state/suggestions.ts +0 -9
  319. package/src/state/threads.ts +0 -142
  320. package/src/test-utils/fake-clock.ts +0 -88
  321. package/src/types.ts +0 -95
  322. package/src/ui/action-icons.ts +0 -53
  323. package/src/ui/agent-card.tsx +0 -218
  324. package/src/ui/avatar.tsx +0 -23
  325. package/src/ui/badge.tsx +0 -21
  326. package/src/ui/button.tsx +0 -49
  327. package/src/ui/card.tsx +0 -245
  328. package/src/ui/checkbox-group.tsx +0 -153
  329. package/src/ui/checkbox.tsx +0 -63
  330. package/src/ui/collapsible.tsx +0 -163
  331. package/src/ui/command.tsx +0 -103
  332. package/src/ui/dialog.tsx +0 -223
  333. package/src/ui/dock.tsx +0 -630
  334. package/src/ui/dropdown.tsx +0 -636
  335. package/src/ui/editable-label.tsx +0 -112
  336. package/src/ui/hover-card.tsx +0 -345
  337. package/src/ui/icon.tsx +0 -167
  338. package/src/ui/input.tsx +0 -391
  339. package/src/ui/kbd.tsx +0 -118
  340. package/src/ui/nav.tsx +0 -282
  341. package/src/ui/notice.tsx +0 -84
  342. package/src/ui/overlay.tsx +0 -277
  343. package/src/ui/pane-grid.tsx +0 -117
  344. package/src/ui/pane-group.tsx +0 -291
  345. package/src/ui/pane.tsx +0 -193
  346. package/src/ui/popover.tsx +0 -106
  347. package/src/ui/progress-bar.tsx +0 -83
  348. package/src/ui/prompt-dock.tsx +0 -145
  349. package/src/ui/radio.tsx +0 -150
  350. package/src/ui/resizable.tsx +0 -693
  351. package/src/ui/scroll-area.tsx +0 -26
  352. package/src/ui/segmented.tsx +0 -109
  353. package/src/ui/select.tsx +0 -168
  354. package/src/ui/separator.tsx +0 -10
  355. package/src/ui/settings-group.tsx +0 -67
  356. package/src/ui/skeleton.tsx +0 -73
  357. package/src/ui/slider.tsx +0 -178
  358. package/src/ui/status.tsx +0 -48
  359. package/src/ui/switch.tsx +0 -119
  360. package/src/ui/tabs.tsx +0 -168
  361. package/src/ui/textarea.tsx +0 -21
  362. package/src/ui/tooltip.tsx +0 -118
  363. package/src/utils/cn.ts +0 -30
  364. package/src/wire/chunk.ts +0 -248
  365. package/src/wire/consume.ts +0 -599
  366. package/src/wire/diagnostics.ts +0 -727
  367. package/src/wire/encode-probe.ts +0 -214
  368. package/src/wire/encode.ts +0 -947
  369. package/src/wire/files.ts +0 -342
  370. package/src/wire/formats/anthropic.ts +0 -274
  371. package/src/wire/formats/openai.ts +0 -228
  372. package/src/wire/index.ts +0 -95
  373. package/src/wire/media-types.ts +0 -344
  374. package/src/wire/read.ts +0 -396
  375. package/src/wire/sink-helpers.ts +0 -50
  376. package/src/wire/sse.ts +0 -153
@@ -1,231 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- /** Surface changes appearance; target changes delivery. Two axes, never one. */
4
- export const SurfaceArchetype = z.enum(['full-screen', 'widget', 'docked', 'inline', 'platform-embed']);
5
- export const DeliveryTarget = z.enum(['bundler', 'script-tag']);
6
-
7
- /** The three readers `src/wire/read.ts` exports. The drift lint (Task 7) resolves them. */
8
- export const WireReader = z.enum(['readModelStream', 'readOpenAIStream', 'readAnthropicStream']);
9
-
10
- /** BYO key: the endpoint is always the consumer's own. One swappable field by design. */
11
- export const Backend = z.object({
12
- endpoint: z.literal('consumer-owned'),
13
- reader: WireReader,
14
- });
15
-
16
- /**
17
- * Tagged, because a bare path cannot honestly describe every invariant:
18
- * `none` is a REPORTED coverage gap, never a failure and never a fake path.
19
- */
20
- export const EnforcedBy = z.discriminatedUnion('kind', [
21
- z.object({ kind: z.literal('test'), paths: z.array(z.string()).min(1) }),
22
- z.object({ kind: z.literal('lint'), script: z.string() }),
23
- z.object({ kind: z.literal('structural'), path: z.string() }),
24
- z.object({ kind: z.literal('none'), until: z.string().optional() }),
25
- ]);
26
-
27
- export const Diagnosis = z.object({ symptom: z.string(), cause: z.string() });
28
-
29
- /**
30
- * One wrong/right pair, as CODE rather than prose. A weak model applies a
31
- * fragment it can pattern-match far more reliably than a sentence it has to
32
- * reason from, and the pairs are also mechanically searchable: the self-audit
33
- * checklist greps emitted code for the `wrong` form and expects zero hits. So
34
- * keep `wrong` a compact, literal, greppable fragment — not a paraphrase.
35
- */
36
- export const InvariantExample = z.object({
37
- wrong: z.string().min(1),
38
- right: z.string().min(1),
39
- note: z.string().optional(),
40
- });
41
-
42
- export const Invariant = z.object({
43
- id: z.string().regex(/^[a-z0-9-]+$/),
44
- statement: z.string().min(1),
45
- appliesTo: z.object({
46
- tags: z.array(z.string()).optional(),
47
- parts: z.array(z.string()).optional(),
48
- targets: z.array(DeliveryTarget).optional(),
49
- }),
50
- enforcedBy: EnforcedBy,
51
- /**
52
- * `enforced` — a guard covers what the statement prescribes.
53
- * `partial` — a guard covers PART of it and the rest is uncovered; the
54
- * statement must say which half is which. Added because two
55
- * records were `enforced` while their headline consumer-facing
56
- * prescription had no check at all, and `status` is what a
57
- * downstream tool reads when it does not read the prose.
58
- * `open` — nothing covers it. Travels with `enforcedBy.kind === 'none'`,
59
- * in both directions, asserted in invariants.test.ts.
60
- */
61
- status: z.enum(['enforced', 'partial', 'open']),
62
- diagnosis: z.array(Diagnosis).default([]),
63
- // `.default([])` mirrors `diagnosis` above for consistency, and nothing more:
64
- // it is NOT a compatibility affordance. `z.infer` makes `examples` required on
65
- // `TInvariant`, and the only caller is `listInvariants()` over an in-repo
66
- // literal — no external or serialized input reaches `Invariant.parse`, so
67
- // there is no unmigrated record for the default to rescue. What actually makes
68
- // the field mandatory is invariants.test.ts, which requires at least one pair
69
- // on EVERY record and constrains the shape of each.
70
- examples: z.array(InvariantExample).default([]),
71
- });
72
-
73
- /**
74
- * WHERE one element goes: `child` is slotted into `parent`'s named `slot`.
75
- *
76
- * Structured rather than prose, for the reason the wiring edges are: an emitter
77
- * has to be able to READ the composition. The scaffolder emitted the rail as a
78
- * sibling below the chat while `<kai-chat>` documented a `sidebar` slot whose
79
- * description is "left column (your nav / conversation list)", and an agent
80
- * building from the MCP could not answer which was intended from anything the
81
- * MCP served — because nothing served it. A sentence in `intent` would not have
82
- * fixed that: `intent` already said "chat with a conversations sidebar" while
83
- * the emitter did the other thing.
84
- *
85
- * NOT resolvable by `lint:catalog-drift`. That lint's ground truth is
86
- * derived.json, which carries props, events, methods, parts, composedFrom and
87
- * tokens — no slots — so `child`/`parent` resolve as elements and the SLOT NAME
88
- * resolves against nothing. element-meta.json has the slots; wiring them into
89
- * the derived layer is the way to close it.
90
- */
91
- export const CompositionPlacement = z.object({
92
- /** the element that goes inside */
93
- child: z.string(),
94
- /** the element it goes inside of */
95
- parent: z.string(),
96
- /** the `slot` attribute the child carries */
97
- slot: z.string(),
98
- note: z.string().optional(),
99
- });
100
-
101
- /** One host-coordinates edge: this event on A sets this property on B. */
102
- export const WiringEdge = z.object({
103
- from: z.string(),
104
- event: z.string(),
105
- to: z.string(),
106
- property: z.string(),
107
- note: z.string().optional(),
108
- });
109
-
110
- export const SurfaceRecipe = z.object({
111
- id: z.string().regex(/^[a-z0-9-]+$/),
112
- intent: z.string().min(1),
113
- archetypes: z.array(SurfaceArchetype).min(1),
114
- targets: z.array(DeliveryTarget).min(1),
115
- ingredients: z.array(z.string()).min(1),
116
- backend: Backend,
117
- // `.min(1)` like its siblings, and for a sharper reason than symmetry: an
118
- // empty `wiring` is what a recipe looks like when it makes NO host-coordinates
119
- // claim at all. Every wiring check in the drift lint is a loop over this
120
- // array, so emptying it deleted the topology from both recipes while the lint
121
- // went on printing "2 recipes ... resolved clean". The lint carries a readable
122
- // duplicate of this check so the failure is a message rather than a ZodError.
123
- wiring: z.array(WiringEdge).min(1),
124
- /**
125
- * Optional, and `.min(1)` when present: a recipe whose ingredients nest states
126
- * where they nest, and a recipe of one element has nothing to say here. An
127
- * empty array would be a third thing — a composition claim that claims
128
- * nothing — so the schema refuses it.
129
- */
130
- composition: z.array(CompositionPlacement).min(1).optional(),
131
- invariants: z.array(z.string()).min(1),
132
- corpus: z.array(z.string()).min(1),
133
- });
134
-
135
- export const InventorySort = z.enum(['surface', 'ingredient', 'corpus']);
136
- export const InventoryEntry = z.object({
137
- title: z.string().min(1),
138
- sort: InventorySort,
139
- note: z.string().min(1),
140
- });
141
-
142
- export const ScenarioId = z.enum(['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7']);
143
- export const Scenario = z.object({
144
- id: ScenarioId,
145
- prompt: z.string().min(1),
146
- needs: z.array(z.string()).min(1),
147
- depth: z.string().min(1),
148
- scoring: z.array(z.string()).min(1),
149
- });
150
-
151
- /** The derived layer's committed artifact. Task 3's generator writes it; Task 3's test parses it. */
152
- export const DerivedElement = z.object({
153
- tag: z.string(),
154
- props: z.array(
155
- z.object({
156
- name: z.string(),
157
- scalar: z.boolean(),
158
- optional: z.boolean(),
159
- /**
160
- * The function-valued-property contract, which `scalar` does NOT encode:
161
- * `scalar: false` says "not an attribute", never "this is a callback you
162
- * must supply". Derived by the generator from element-meta.json's prop
163
- * type: strip a leading `undefined | `, then true iff the remainder
164
- * starts with `(` and contains `=>`. Deliberately not a bare
165
- * `includes('=>')`, which over-matches objects and arrays that merely
166
- * CONTAIN callbacks (kai-cards.policy, kai-toast-region.toasts).
167
- */
168
- fn: z.boolean(),
169
- }),
170
- ),
171
- events: z.array(z.string()),
172
- methods: z.array(z.string()),
173
- parts: z.array(z.string()),
174
- /** Spec §3 names both; element-meta.json already carries them. */
175
- composedFrom: z.array(z.string()),
176
- tokens: z.array(z.string()),
177
- });
178
-
179
- /**
180
- * Which MessagePart variants an element consumes. NOT derivable from any type
181
- * today, so spec §3's registered-copy rule applies: this is an explicit copy,
182
- * and Task 7's drift lint fails when the union gains a variant no record
183
- * accounts for. Registered in "Copies this plan creates" at the end of the plan.
184
- */
185
- export const PartConsumption = z.object({
186
- tag: z.string(),
187
- consumes: z.array(z.string()).min(1),
188
- });
189
-
190
- export const EventException = z.object({
191
- file: z.string(),
192
- event: z.string(),
193
- bubbles: z.boolean(),
194
- composed: z.boolean(),
195
- });
196
-
197
- export const DerivedCatalog = z.object({
198
- elements: z.array(DerivedElement).min(1),
199
- // REGISTERED COPY: this floor restates MIN_VARIANTS, which lives in
200
- // scripts/lib/message-part-variants.mjs (Task 2) and cannot be imported into a
201
- // .ts module that also runs in the browser bundle. The generator asserts the
202
- // real MIN_VARIANTS; this is the schema-side backstop. If MIN_VARIANTS moves,
203
- // move this too — see "Copies this plan creates".
204
- partVariants: z.array(z.string()).min(4),
205
- integrations: z
206
- .array(z.object({ id: z.string(), category: z.string(), streamFormat: z.string(), keyExposure: z.string() }))
207
- .min(1),
208
- capabilityGroups: z.array(z.object({ id: z.string(), components: z.array(z.string()) })).min(1),
209
- themeTokens: z.array(z.string()).min(1),
210
- // .min(1) because the tree HAS deliberate protocol exceptions — events that
211
- // set bubbles or composed on purpose — so an empty array cannot be a true
212
- // reading of it: it means the extractor broke, and a broken extractor that
213
- // parses clean would silently gut spec §5's exception list.
214
- //
215
- // Deliberately no count here. The number is the extractor's to report, not
216
- // this comment's to restate: `npm run build:api` (its gen-catalog.mjs step)
217
- // prints the count as it writes, and the current set is the `eventExceptions`
218
- // array in src/agent-tooling/catalog/derived.json. A hand-typed figure here
219
- // would be stale the first time an element opts in or out, on the schema for
220
- // the very field whose exception list was wrong before.
221
- eventExceptions: z.array(EventException).min(1),
222
- });
223
-
224
- export type TCompositionPlacement = z.infer<typeof CompositionPlacement>;
225
- export type TInvariant = z.infer<typeof Invariant>;
226
- export type TInvariantExample = z.infer<typeof InvariantExample>;
227
- export type TSurfaceRecipe = z.infer<typeof SurfaceRecipe>;
228
- export type TScenario = z.infer<typeof Scenario>;
229
- export type TInventoryEntry = z.infer<typeof InventoryEntry>;
230
- export type TPartConsumption = z.infer<typeof PartConsumption>;
231
- export type TDerivedCatalog = z.infer<typeof DerivedCatalog>;
@@ -1,96 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- /**
4
- * Tags agents invented while working from an acceptance pack, and what they
5
- * should have used instead. The pack's `FABRICATED.md` is rendered from this.
6
- *
7
- * WHY THIS IS AN AUTHORED RECORD AND NOT A LOG THE EVALUATOR APPENDS TO.
8
- * A row here is a claim about the tree — "the kit does not ship this tag" — and
9
- * the claim rots in one direction that matters: the day the kit ships
10
- * `kai-datagrid`, a row saying it does not exist becomes a lie told to every
11
- * future agent, on the one page whose whole job is telling agents what is real.
12
- * Living beside `invariants.ts` puts it where `fabrications.test.ts` can resolve
13
- * it against `derived.json` in both directions on every CI run. A JSON log
14
- * written by a judged run would have neither the review nor the check.
15
- *
16
- * The schema is declared here rather than in `catalog-types.ts` because this
17
- * record arrived a task later than that file and nothing else consumes the
18
- * shape; fold it in there if a second consumer appears.
19
- *
20
- * EVERY ROW COMES FROM AN OBSERVED RUN. Do not seed it with tags a model might
21
- * plausibly invent — a fabricated fabrication is exactly the failure the page
22
- * exists to prevent, and it would be indistinguishable from a real one.
23
- */
24
- export const Fabrication = z
25
- .object({
26
- /** The tag that was written and does not exist. Checked ABSENT from derived.json. */
27
- invented: z.string().regex(/^kai-[a-z0-9-]+$/),
28
- /** What the agent was trying to accomplish, in its own terms. */
29
- wanted: z.string().min(1),
30
- /** The real element to reach for. Checked PRESENT in derived.json when non-null. */
31
- useInstead: z.string().regex(/^kai-[a-z0-9-]+$/).nullable(),
32
- /**
33
- * Required when `useInstead` is null. "There is nothing for this" is a real
34
- * and useful answer — S6 exists to elicit exactly that — but it has to be
35
- * said, because a blank cell reads as an unfinished row.
36
- */
37
- noReplacementReason: z.string().min(1).optional(),
38
- /** ISO date of the run that first produced it. */
39
- firstSeen: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
40
- /** Provenance, so a row can be traced back to the run that observed it. */
41
- scenario: z.string().min(1),
42
- model: z.string().min(1),
43
- })
44
- .refine((f) => f.useInstead !== null || Boolean(f.noReplacementReason), {
45
- message: 'a row with no `useInstead` must carry `noReplacementReason`',
46
- path: ['noReplacementReason'],
47
- });
48
-
49
- export type TFabrication = z.infer<typeof Fabrication>;
50
-
51
- /**
52
- * EMPTY, and that is the honest state: the apparatus that would fill it (Task
53
- * 8b's runner and evaluator) has just been built and no acceptance run has been
54
- * performed. `FABRICATED.md` says the same thing in the same words to the agent.
55
- */
56
- export const fabrications: TFabrication[] = [];
57
-
58
- export function listFabrications(): TFabrication[] {
59
- return z.array(Fabrication).parse(fabrications);
60
- }
61
-
62
- /**
63
- * Resolve rows against the tags the kit actually ships, in BOTH directions.
64
- *
65
- * This is a named function rather than a loop inside the test for one reason:
66
- * the record is empty, so a loop over it iterates zero times and passes whatever
67
- * the rule is. Deleting both assertions left the suite green. A rule that cannot
68
- * be exercised is not a guard, and the direction it protects — `invented`, the
69
- * one that rots when the kit later ships the tag — is the direction this file's
70
- * own comment says matters most.
71
- *
72
- * Extracted so it can be run over FIXTURES that make it fire, and over the live
73
- * record, from the same code path.
74
- *
75
- * @returns problems; empty means every row resolves
76
- */
77
- export function resolveFabrications(rows: TFabrication[], knownTags: Iterable<string>): string[] {
78
- const known = new Set(knownTags);
79
- if (known.size === 0) {
80
- // Otherwise every row "resolves" in the direction that matters and none in
81
- // the other, which is a broken input reported as a clean record.
82
- return ['no known tags were supplied, so nothing could be resolved'];
83
- }
84
- const problems: string[] = [];
85
- for (const row of rows) {
86
- if (known.has(row.invented)) {
87
- problems.push(
88
- `\`${row.invented}\` is recorded as invented, but the kit SHIPS it now. Delete the row: it tells every future agent that a real element does not exist, on the one page whose job is saying what is real.`,
89
- );
90
- }
91
- if (row.useInstead && !known.has(row.useInstead)) {
92
- problems.push(`\`${row.invented}\` points at \`${row.useInstead}\` as the replacement, and the kit does not ship that either.`);
93
- }
94
- }
95
- return problems;
96
- }
@@ -1,284 +0,0 @@
1
- import { z } from 'zod';
2
- import { Invariant, type TInvariant } from './catalog-types';
3
-
4
- /**
5
- * Spec §5. Every one already known to break real consumers.
6
- *
7
- * The `statement` is the prose an agent applies; the `examples` are the part a
8
- * weak model can pattern-match, so each `wrong` is a single-line literal the
9
- * self-audit can grep for (enforced by invariants.test.ts, not by this comment).
10
- *
11
- * `enforcedBy` paths are REPO-relative; `lint` names a script in
12
- * packages/ui/package.json. `kind: 'none'` is an honest coverage gap. Where a
13
- * guard covers only part of a statement the record is `status: 'partial'` and
14
- * the statement says which half is which — an invariant that overstates its own
15
- * enforcement is worse than one that admits a gap, because a reader stops
16
- * looking.
17
- *
18
- * EXAMPLES ARE CONSUMER CODE. Every `right` form must be runnable by someone who
19
- * has only installed the package: no import that is not in the `exports` map.
20
- * The kit's own guards (isSafeUrl, isRenderableLink) live in `src/primitives`,
21
- * which ships compiled and unexported, so they are named as repo-internal
22
- * guidance and never written as a consumer import.
23
- */
24
- export const invariants: TInvariant[] = [
25
- {
26
- id: 'reactivity-two-halves',
27
- statement:
28
- 'A new array reference NOTIFIES; a new object for each changed item makes the change VISIBLE. Editing an existing item needs both. Adds and removes need only the fresh array. Setting the same array back is a no-op even if an item inside it was swapped. The test pins how the KIT behaves — it will render stale unless both arrive — but nothing checks CONSUMER code, so this is a rule you apply, not a guarantee you will be warned about. Reorders follow the same rule as adds and removes; the test names reorders in its title but exercises only an add and a remove, so treat that half as reasoned rather than pinned.',
29
- appliesTo: { tags: ['kai-chat', 'kai-conversations'] },
30
- enforcedBy: { kind: 'test', paths: ['packages/ui/src/components/reactivity-contract.test.tsx'] },
31
- status: 'enforced',
32
- diagnosis: [
33
- {
34
- symptom: 'messages render once but never update while streaming',
35
- cause: 'the same array reference is being set back; the element is never notified',
36
- },
37
- {
38
- symptom: 'the list re-renders but an edited item shows stale content',
39
- cause: 'the array is new but the item object identity is unchanged; the reference-keyed <For> keeps the old row',
40
- },
41
- ],
42
- examples: [
43
- {
44
- wrong: "chat.messages.push({ id, role: 'user', parts: [{ type: 'text', text }] });",
45
- right: "chat.messages = [...chat.messages, { id, role: 'user', parts: [{ type: 'text', text }] }];",
46
- note: 'Mutating in place never notifies. Neither does assigning the same array reference back — the setter compares references.',
47
- },
48
- {
49
- wrong: 'messages[last].parts.push(part);',
50
- right: 'chat.messages = messages.map((m, i) => (i === last ? { ...m, parts: [...m.parts, part] } : m));',
51
- note: 'This is the half that gets missed. A fresh array alone notifies, but the reference-keyed <For> keeps the old row until the EDITED ITEM is a new object too. createAssistantStream from @kitn.ai/ui/state already does both.',
52
- },
53
- ],
54
- },
55
- {
56
- id: 'props-not-attributes',
57
- statement:
58
- "Set arrays, objects and functions as JS PROPERTIES on the element instance. Only scalars (strings, numbers, booleans) belong in attributes, and the derived layer's scalar flag records which prop is which. What actually goes wrong, because the mechanism is not the obvious one: a framework template binding or String() stringifies an array to '[object Object]', which is not JSON, and the attribute path falls back to handing the element that raw STRING — so the prop is silently a string and the list renders nothing. A function cannot survive JSON at all: JSON.stringify({ onSubmit }) is '{}', so every callback is dropped. And an attribute re-set is not how updates are delivered — see reactivity-two-halves. A hand-written, valid-JSON attribute does happen to parse today, because a transitive dependency JSON.parses attributes whose declared default is not a string, but that is that dependency's behaviour and not this kit's contract; do not build on it. NOTHING IN THIS REPO ENFORCES THIS — it is a consumer contract, and the scalar flag in the derived layer is how the catalog SERVES the fact, which is not the same as checking it.",
59
- appliesTo: {},
60
- // kind:'none' after measurement, replacing a `structural` pointer at
61
- // define.tsx that did not contain the claimed mechanism. What define.tsx
62
- // actually does is install non-reflecting accessors for the handful of props
63
- // colliding with reflected global IDL attributes (role/hidden/autofocus), in
64
- // the OPPOSITE direction: it stops property writes reflecting TO attributes.
65
- // The attribute->property path lives in component-register, a transitive
66
- // dependency, and measured in the real jsdom project against the real
67
- // element it PARSES a JSON attribute onto the property rather than rejecting
68
- // it. Nothing here checks consumer code for the contract; S1's scoring
69
- // exercises a property update on kai-chat.messages, but no scenario names
70
- // this invariant, so it is not claimed as a measurement either.
71
- enforcedBy: { kind: 'none' },
72
- status: 'open',
73
- diagnosis: [
74
- {
75
- symptom: 'an element renders empty and the data looks right in devtools',
76
- cause: "the property holds the STRING '[object Object]', not an array; it arrived through an attribute that was stringified rather than assigned",
77
- },
78
- {
79
- symptom: 'everything renders but no callback ever fires',
80
- cause: 'the object went through JSON into an attribute, and JSON drops functions silently',
81
- },
82
- ],
83
- examples: [
84
- {
85
- wrong: "el.setAttribute('messages', String(messages));",
86
- right: 'el.messages = messages;',
87
- note: "Measured against the real element: the attribute becomes '[object Object]', JSON.parse fails, and the fallback leaves a STRING on the prop. messages is scalar:false in the derived layer; placeholder, loading and theme are scalar:true and do belong in attributes.",
88
- },
89
- {
90
- wrong: "cards.setAttribute('policy', JSON.stringify({ onSubmit }));",
91
- right: 'cards.policy = { onSubmit };',
92
- note: "Measured: JSON.stringify({ onSubmit }) is '{}', so the handler is gone before the attribute is even set. No attribute can carry a function.",
93
- },
94
- ],
95
- },
96
- {
97
- id: 'events-non-bubbling',
98
- statement:
99
- 'Non-bubbling is the default: public kai-* events are dispatched through the one helper that hard-codes bubbles:false and composed:false, so listen on the element itself, never on a parent or document. The protocol exceptions (kai-maximize-intent, kai-maximize-state and kai-card) bubble or compose deliberately and are listed in the derived layer under eventExceptions — do not generalise from them to the rest.',
100
- appliesTo: {},
101
- enforcedBy: { kind: 'structural', path: 'packages/ui/src/elements/define.tsx' },
102
- status: 'enforced',
103
- diagnosis: [
104
- {
105
- symptom: 'a delegated listener on document or a parent never fires',
106
- cause: 'kai-* events do not bubble; attach the listener to the element that dispatches it',
107
- },
108
- {
109
- symptom: 'a listener on the element works, but the same one on a wrapper div does not',
110
- cause: 'same cause; only the three eventExceptions cross the element boundary',
111
- },
112
- ],
113
- examples: [
114
- {
115
- wrong: "document.addEventListener('kai-submit', (e) => send(e.detail.value));",
116
- right: "chat.addEventListener('kai-submit', (e) => send(e.detail.value));",
117
- note: 'The dispatch helper in src/elements/define.tsx passes { bubbles: false, composed: false }, so nothing above the host ever sees the event.',
118
- },
119
- {
120
- wrong: "wrapper.addEventListener('kai-message-action', handleAction);",
121
- right: "chat.addEventListener('kai-message-action', handleAction);",
122
- note: 'Delegating from a wrapper is the most common shape of this bug, because it is the habit every DOM framework teaches.',
123
- },
124
- ],
125
- },
126
- {
127
- id: 'host-coordinates',
128
- statement:
129
- 'There is no store. Data flows in via properties, out via events, and the host wires element A to element B. Solid context does not cross element boundaries, so nothing coordinates elements except the host application, and no element owns another. Placing two elements in the same subtree wires nothing.',
130
- appliesTo: {},
131
- enforcedBy: { kind: 'none' },
132
- status: 'open',
133
- diagnosis: [
134
- {
135
- symptom: 'two elements are expected to sync but do not',
136
- cause: 'nothing auto-coordinates; the host must listen on one element and set properties on the other',
137
- },
138
- {
139
- symptom: 'a property assignment is silently ignored and the prop is not in the reference',
140
- cause: 'the data was put on the element that displays the conversation rather than the one that owns the list',
141
- },
142
- ],
143
- examples: [
144
- {
145
- wrong: 'chat.conversationRows = rows;',
146
- right: 'conversations.conversations = rows;',
147
- note: "kai-chat's own `conversations` prop is a boolean flag (turns the built-in list panel on); it has no `conversationRows`-shaped data prop, and the sidebar is its own element. One element never holds the whole app state.",
148
- },
149
- {
150
- wrong: "chat.addEventListener('kai-conversation-select', (e) => load(e.detail.id));",
151
- right:
152
- "conversations.addEventListener('kai-conversation-select', (e) => { chat.messages = threadsById[e.detail.id]; });",
153
- note: 'Event out of A, property into B, wired by the host. The event is dispatched by the element that owns the list, so that is where the listener goes.',
154
- },
155
- ],
156
- },
157
- {
158
- id: 'untrusted-model-output',
159
- statement:
160
- "Everything the model produced is untrusted input: a MessagePart, card envelope or tool argument reaching innerHTML, an href or src, window.open or an iframe is a vulnerability. THE DEFECT IS NEVER A MISSING GUARD, IT IS WHICH PATH GOT IT — every one found so far sat on a path the CONSUMER controls while the model-controlled path beside it had none. So put a policy on the sink, and MATCH THE LIST TO THE SINK rather than reaching for one universal list: http:, https: and mailto: for anything navigable the user may click, resolved against the page so ordinary relative links still work; http: and https: ONLY for a model-supplied citation, which is a reference to a page on the public web and has no business being relative. Those are two lists because there are two sinks — it is the same split the kit makes internally between SAFE_SCHEMES and RENDERABLE_SCHEMES, not a variant invented here. Whichever you use, parse inside a try/catch and RETURN FALSE on an unparseable URL: new URL() throws, and a throw at a sink crashes the render. And render model text as TEXT. Escaping is the correct rendering: the source text must stay VISIBLE as well as inert, because a filter that deleted it would pass the security check and be a worse UI. COVERAGE, and read this before trusting CI here: the three XSS suites are tests and ONLY tests. They run in the required test job, so the vectors they pin cannot come back — but NOTHING structural stops a NEW sink landing unguarded. No lint script in the package is about sinks, and the coupling map's unenforced list has no entry for the class. A new sink is caught in review or not at all.",
161
- appliesTo: {},
162
- // WHAT THE THREE SUITES DO NOT CATCH: they pin the vectors that were FOUND
163
- // (#246 markdown innerHTML, #247 the artifact's three URL sinks, and the
164
- // hostile-stream path), so they are regression guards, not a guard over the
165
- // CLASS. A newly written component that puts model text on a fresh
166
- // unguarded sink adds no failing test anywhere. Verified against
167
- // HANDOFF-2026-08-13 §13.2 ("They are tests and only tests... nothing
168
- // structural stops a NEW sink landing unguarded"), and re-checked against
169
- // the tree: none of packages/ui/scripts/lint-*.mjs concerns sinks, and
170
- // docs/coupling-map.md has no row for the class. Kept as kind:'test'
171
- // because the regression coverage is real; status is `partial` because the
172
- // class is not covered, and the statement carries the gap.
173
- enforcedBy: {
174
- kind: 'test',
175
- paths: [
176
- 'packages/ui/tests/components/markdown-xss.test.tsx',
177
- 'packages/ui/tests/components/artifact-url-xss.test.tsx',
178
- 'packages/ui/tests/components/hostile-model-output.test.tsx',
179
- ],
180
- },
181
- status: 'partial',
182
- diagnosis: [
183
- {
184
- symptom: 'a custom renderer for a tool result or a card body executes markup the model emitted',
185
- cause: 'model text reached innerHTML on a hand-written path; the guarded path is the one every real message flows through, and this one bypassed it',
186
- },
187
- {
188
- symptom: 'a citation or card link navigates to javascript: or data:',
189
- cause: 'a model-supplied URL reached an href, a src or window.open with no scheme check',
190
- },
191
- ],
192
- examples: [
193
- {
194
- wrong: 'el.innerHTML = part.text;',
195
- right: 'el.textContent = part.text;',
196
- note: 'For rich text render the part through <kai-markdown>, which escapes rather than sanitizes (src/components/markdown.tsx) and filters link and image URLs. Never hand-roll a second markdown-to-innerHTML path.',
197
- },
198
- {
199
- wrong: "window.open(card.url, '_blank');",
200
- right:
201
- "const isNavigable = (u) => { try { return ['http:', 'https:', 'mailto:'].includes(new URL(u, location.href).protocol); } catch { return false; } };\nif (isNavigable(card.url)) window.open(card.url, '_blank', 'noopener,noreferrer');",
202
- note: "THE try/catch IS NOT OPTIONAL: new URL() THROWS on an unparseable input like 'http://[', and an uncaught throw here crashes the render instead of blocking the link — worse than the bug you are fixing. Returning false is the whole contract. Resolving against location.href is deliberate: a relative or fragment href inherits http: and is allowed, which is what makes ordinary links keep working. REPO-INTERNAL ONLY: contributors inside this package use isSafeUrl/SAFE_SCHEMES from src/primitives/url-scheme-policy.ts (re-exported from src/primitives/card-routing.ts for the existing call sites), which has this exact shape — neither is reachable from the published package, so never emit an import for them.",
203
- },
204
- {
205
- wrong: '<a href={source.url}>{source.title}</a>',
206
- right:
207
- 'const isCitationUrl = (u) => { try { return [\'http:\', \'https:\'].includes(new URL(u).protocol); } catch { return false; } };\n{isCitationUrl(source.url) ? <a href={source.url} rel="noopener noreferrer">{source.title}</a> : <span>{source.title}</span>}',
208
- note: "No base here, unlike the navigable case: a model-supplied citation is a reference to a page on the public web, so a relative path is not a citation and returns false. It must RETURN false, not throw — a throw escapes the ternary and the fallback never renders, which is exactly the deleted-text failure this invariant's own statement forbids. The <span> keeps the title VISIBLE. REPO-INTERNAL equivalent: isRenderableLink in src/primitives/link-preview.ts, also not exported.",
209
- },
210
- ],
211
- },
212
- {
213
- id: 'kit-parses-consumer-fetches',
214
- statement:
215
- "The kit parses; the consumer fetches. Two halves, covered differently. KIT SIDE: every MessagePart variant the wire encodes must be accounted for, or a variant is gone once the request leaves — that half is enforced by lint:silent-drops in CI. CONSUMER SIDE: never hand-roll an SSE reader; import readOpenAIStream, readAnthropicStream or readModelStream from @kitn.ai/ui/wire, and fetch from your own endpoint, because there is no client, no key handling and no provider SDK below wire/. NO CI CHECK COVERS THAT SECOND HALF — no guard reads consumer or scaffolded code for a hand-rolled reader; it is measured by the acceptance deck instead, at scenario S2, whose scoring line is 'imports readOpenAIStream from @kitn.ai/ui/wire; no hand-rolled SSE reader anywhere in the output'.",
216
- appliesTo: {},
217
- // WHAT lint:silent-drops DOES NOT CATCH: it analyzes src/wire, so it covers
218
- // the kit-side half only. A consumer or a scaffold that hand-rolls its own
219
- // SSE loop never enters its scan, and the lint stays green. That gap is
220
- // stated in the statement above so no reader concludes CI catches it, and
221
- // scenario S2 is what actually measures it. Recorded rather than closed:
222
- // flipping this to kind:'none' would discard a real guard over a real half,
223
- // which is what `status: 'partial'` exists to express.
224
- enforcedBy: { kind: 'lint', script: 'lint:silent-drops' },
225
- status: 'partial',
226
- diagnosis: [
227
- {
228
- symptom: 'streaming works for one provider and silently drops parts for another',
229
- cause: 'a hand-rolled reader misses part variants the wire layer already handles; replace it with the wire import',
230
- },
231
- {
232
- symptom: 'tokens arrive glued together, or a multibyte character renders as garbage mid-stream',
233
- cause: 'a hand-rolled reader split on a data: prefix and assumed one frame per chunk; keep-alive comments, multi-line frames and codepoints split across a socket boundary are all real',
234
- },
235
- ],
236
- examples: [
237
- {
238
- wrong: "text += JSON.parse(line.replace('data: ', '')).choices[0].delta.content;",
239
- right: 'const turn = await readOpenAIStream(res, stream);',
240
- note: "import { readOpenAIStream } from '@kitn.ai/ui/wire'; createAssistantStream from '@kitn.ai/ui/state' owns the message, the reader fills it. readAnthropicStream and readModelStream are the other two entry points.",
241
- },
242
- {
243
- wrong: "await fetch('https://api.openai.com/v1/chat/completions', { headers: { Authorization: 'Bearer ' + apiKey } });",
244
- right:
245
- "await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: toOpenAIMessages(history) }) });",
246
- note: 'The consumer fetches, from their own endpoint. A provider key in the browser is a leaked key; toOpenAIMessages/toAnthropicMessages encode the thread for the wire.',
247
- },
248
- ],
249
- },
250
- {
251
- id: 'upgrade-race',
252
- statement:
253
- 'A property set before the element upgrades is lost. On script-tag targets, load order is not ours. Until issue #99 option B (upgrade-property preservation in defineWebComponent) lands, every script-tag recipe must state this race loudly and set properties only after registration — await customElements.whenDefined(tag), which is the guarantee. A timer, or DOMContentLoaded, is a guess about load order rather than a guarantee: registration can land later, from an async chunk or a dynamically inserted script.',
254
- appliesTo: { targets: ['script-tag'] },
255
- enforcedBy: { kind: 'none', until: 'issue #99 option B lands in defineWebComponent' },
256
- status: 'open',
257
- diagnosis: [
258
- {
259
- symptom: 'properties set in inline script are ignored on a CDN page',
260
- cause: 'the element had not upgraded yet; the set landed on a plain HTMLElement and was lost',
261
- },
262
- {
263
- symptom: 'the same code works under a bundler and not from a script tag',
264
- cause: 'the bundler happened to order the registration first; a script tag gives no such guarantee',
265
- },
266
- ],
267
- examples: [
268
- {
269
- wrong: 'setTimeout(() => { chat.messages = messages; }, 0);',
270
- right: "customElements.whenDefined('kai-chat').then(() => { chat.messages = messages; });",
271
- note: 'A timer bets on load order. whenDefined resolves when the registry actually has the tag, which is the thing you need to be true.',
272
- },
273
- {
274
- wrong: "document.addEventListener('DOMContentLoaded', () => { chat.messages = messages; });",
275
- right: "customElements.whenDefined('kai-chat').then(() => { chat.messages = messages; });",
276
- note: 'DOMContentLoaded is about the parser, not the registry. It says the markup is there, never that the element behind the tag has been defined.',
277
- },
278
- ],
279
- },
280
- ];
281
-
282
- export function listInvariants(): TInvariant[] {
283
- return z.array(Invariant).parse(invariants);
284
- }