@nurix/etna 0.18.19 → 0.18.21

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 (283) hide show
  1. package/README.md +1 -1
  2. package/data/harness-index.json +1 -1
  3. package/data/harnesses/code/CLAUDE.md +1 -1
  4. package/data/harnesses/code/agents/brainstorm.md +1 -1
  5. package/data/harnesses/code/agents/maintenance.md +2 -2
  6. package/data/harnesses/code/harness.json +2 -2
  7. package/data/harnesses/code/rules/database.md +38 -0
  8. package/data/harnesses/code/rules/express.md +28 -0
  9. package/data/harnesses/code/rules/frontend.md +14 -0
  10. package/data/harnesses/code/rules/general.md +2 -40
  11. package/data/harnesses/code/rules/nomenclature.md +1 -8
  12. package/data/harnesses/code/rules/nustack.md +150 -0
  13. package/data/harnesses/code/skills/brainstorm/README.md +1 -1
  14. package/data/harnesses/code/skills/brainstorm/SKILL.md +1 -1
  15. package/data/harnesses/code/skills/brainstorm/exit-artifacts.md +4 -4
  16. package/data/harnesses/code/skills/brainstorm/templates/source-of-truth.md +3 -3
  17. package/data/harnesses/code/skills/design/SKILL.md +68 -0
  18. package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
  19. package/data/harnesses/code/skills/design/data/combinations/Readme.md +29 -0
  20. package/data/harnesses/code/skills/design/data/combinations/charts.csv +33 -0
  21. package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +437 -0
  22. package/data/harnesses/code/skills/design/data/combinations/icons.csv +157 -0
  23. package/data/harnesses/code/skills/design/data/combinations/landing.csv +38 -0
  24. package/data/harnesses/code/skills/design/data/combinations/lucide-names.json +2001 -0
  25. package/data/harnesses/code/skills/design/data/combinations/manifest.json +283 -0
  26. package/data/harnesses/code/skills/design/data/combinations/palettes.csv +49 -0
  27. package/data/harnesses/code/skills/design/data/combinations/product-router.csv +80 -0
  28. package/data/harnesses/code/skills/design/data/combinations/styles.csv +54 -0
  29. package/data/harnesses/code/skills/design/data/combinations/typography.csv +61 -0
  30. package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +106 -0
  31. package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +154 -0
  32. package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +151 -0
  33. package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +139 -0
  34. package/data/harnesses/code/skills/design/data/design-md/library/apple.md +139 -0
  35. package/data/harnesses/code/skills/design/data/design-md/library/binance.md +140 -0
  36. package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +140 -0
  37. package/data/harnesses/code/skills/design/data/design-md/library/cal.md +140 -0
  38. package/data/harnesses/code/skills/design/data/design-md/library/clay.md +139 -0
  39. package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +139 -0
  40. package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +147 -0
  41. package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +156 -0
  42. package/data/harnesses/code/skills/design/data/design-md/library/composio.md +138 -0
  43. package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +138 -0
  44. package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +137 -0
  45. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +148 -0
  46. package/data/harnesses/code/skills/design/data/design-md/library/figma.md +141 -0
  47. package/data/harnesses/code/skills/design/data/design-md/library/framer.md +156 -0
  48. package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +140 -0
  49. package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +153 -0
  50. package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +158 -0
  51. package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +156 -0
  52. package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +122 -0
  53. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +140 -0
  54. package/data/harnesses/code/skills/design/data/design-md/library/meta.md +139 -0
  55. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +139 -0
  56. package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +139 -0
  57. package/data/harnesses/code/skills/design/data/design-md/library/miro.md +153 -0
  58. package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +140 -0
  59. package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +155 -0
  60. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +140 -0
  61. package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +141 -0
  62. package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +145 -0
  63. package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +142 -0
  64. package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +152 -0
  65. package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +137 -0
  66. package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +140 -0
  67. package/data/harnesses/code/skills/design/data/design-md/library/resend.md +170 -0
  68. package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +145 -0
  69. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +137 -0
  70. package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +168 -0
  71. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +163 -0
  72. package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +147 -0
  73. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +158 -0
  74. package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +171 -0
  75. package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +166 -0
  76. package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +160 -0
  77. package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +173 -0
  78. package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +145 -0
  79. package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +153 -0
  80. package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +167 -0
  81. package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +140 -0
  82. package/data/harnesses/code/skills/design/data/design-md/library/uber.md +140 -0
  83. package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +140 -0
  84. package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +130 -0
  85. package/data/harnesses/code/skills/design/data/design-md/library/warp.md +140 -0
  86. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +140 -0
  87. package/data/harnesses/code/skills/design/data/design-md/library/wired.md +139 -0
  88. package/data/harnesses/code/skills/design/data/design-md/library/wise.md +137 -0
  89. package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +138 -0
  90. package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +158 -0
  91. package/data/harnesses/code/skills/design/data/design-md/nurix.md +134 -0
  92. package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +580 -0
  93. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.resolver.json +20 -0
  94. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +964 -0
  95. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.resolver.json +20 -0
  96. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +960 -0
  97. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.resolver.json +20 -0
  98. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +965 -0
  99. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.resolver.json +20 -0
  100. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +954 -0
  101. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.resolver.json +20 -0
  102. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +949 -0
  103. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.resolver.json +20 -0
  104. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +962 -0
  105. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.resolver.json +20 -0
  106. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +958 -0
  107. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.resolver.json +20 -0
  108. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +953 -0
  109. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.resolver.json +20 -0
  110. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +960 -0
  111. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.resolver.json +20 -0
  112. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +973 -0
  113. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.resolver.json +20 -0
  114. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +954 -0
  115. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.resolver.json +20 -0
  116. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +963 -0
  117. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.resolver.json +20 -0
  118. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +968 -0
  119. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.resolver.json +20 -0
  120. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +981 -0
  121. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.resolver.json +20 -0
  122. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +962 -0
  123. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.resolver.json +20 -0
  124. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +964 -0
  125. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.resolver.json +20 -0
  126. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +959 -0
  127. package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +251 -0
  128. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.resolver.json +20 -0
  129. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +965 -0
  130. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.resolver.json +20 -0
  131. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +978 -0
  132. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.resolver.json +20 -0
  133. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +982 -0
  134. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.resolver.json +20 -0
  135. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +958 -0
  136. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +23 -0
  137. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +589 -0
  138. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +964 -0
  139. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.resolver.json +20 -0
  140. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +955 -0
  141. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.resolver.json +20 -0
  142. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +977 -0
  143. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.resolver.json +20 -0
  144. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +950 -0
  145. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.resolver.json +20 -0
  146. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +976 -0
  147. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.resolver.json +20 -0
  148. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +979 -0
  149. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.resolver.json +20 -0
  150. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +955 -0
  151. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.resolver.json +20 -0
  152. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +958 -0
  153. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.resolver.json +20 -0
  154. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +953 -0
  155. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.resolver.json +20 -0
  156. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +904 -0
  157. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.resolver.json +20 -0
  158. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +957 -0
  159. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.resolver.json +20 -0
  160. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +945 -0
  161. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.resolver.json +20 -0
  162. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +960 -0
  163. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.resolver.json +20 -0
  164. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +953 -0
  165. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.resolver.json +20 -0
  166. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +955 -0
  167. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.resolver.json +20 -0
  168. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +958 -0
  169. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +23 -0
  170. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +613 -0
  171. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +968 -0
  172. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.resolver.json +20 -0
  173. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +956 -0
  174. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.resolver.json +20 -0
  175. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +960 -0
  176. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.resolver.json +20 -0
  177. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +958 -0
  178. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.resolver.json +20 -0
  179. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +980 -0
  180. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.resolver.json +20 -0
  181. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +964 -0
  182. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.resolver.json +20 -0
  183. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +957 -0
  184. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.resolver.json +20 -0
  185. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +979 -0
  186. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.resolver.json +20 -0
  187. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +974 -0
  188. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.resolver.json +20 -0
  189. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +973 -0
  190. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.resolver.json +20 -0
  191. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +979 -0
  192. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.resolver.json +20 -0
  193. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +966 -0
  194. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.resolver.json +20 -0
  195. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +961 -0
  196. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.resolver.json +20 -0
  197. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +977 -0
  198. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.resolver.json +20 -0
  199. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +991 -0
  200. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.resolver.json +20 -0
  201. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +966 -0
  202. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.resolver.json +20 -0
  203. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +988 -0
  204. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +23 -0
  205. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +602 -0
  206. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +974 -0
  207. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.resolver.json +20 -0
  208. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +979 -0
  209. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.resolver.json +20 -0
  210. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +960 -0
  211. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.resolver.json +20 -0
  212. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +968 -0
  213. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.resolver.json +20 -0
  214. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +965 -0
  215. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.resolver.json +20 -0
  216. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +988 -0
  217. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.resolver.json +20 -0
  218. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +978 -0
  219. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
  220. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
  221. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
  222. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
  223. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
  224. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
  225. package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
  226. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
  227. package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
  228. package/data/harnesses/code/skills/design/references/create.md +131 -0
  229. package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
  230. package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
  231. package/data/harnesses/code/skills/design/references/improve.md +87 -0
  232. package/data/harnesses/code/skills/design/references/interaction.md +77 -0
  233. package/data/harnesses/code/skills/design/references/introspect.md +100 -0
  234. package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
  235. package/data/harnesses/code/skills/design/references/personality.md +428 -0
  236. package/data/harnesses/code/skills/design/references/structure.md +138 -0
  237. package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +1032 -0
  238. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +1006 -0
  239. package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
  240. package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +903 -0
  241. package/data/harnesses/code/skills/design/scripts/lib/golden.mjs +26 -0
  242. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +454 -0
  243. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +267 -0
  244. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
  245. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
  246. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +600 -0
  247. package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +68 -0
  248. package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +314 -0
  249. package/data/harnesses/code/skills/documentation/SKILL.md +61 -17
  250. package/data/harnesses/code/skills/documentation/references/examples.md +41 -0
  251. package/data/harnesses/code/skills/documentation/references/style-guide.md +14 -9
  252. package/data/harnesses/code/skills/documentation/references/templates.md +305 -51
  253. package/data/harnesses/etna/CLAUDE.md +1 -1
  254. package/data/harnesses/etna/agents/brainstorm.md +1 -1
  255. package/data/harnesses/etna/agents/maintenance.md +2 -2
  256. package/data/harnesses/etna/harness.json +2 -2
  257. package/data/harnesses/etna/rules/general.md +2 -40
  258. package/data/harnesses/etna/rules/nomenclature.md +1 -8
  259. package/data/harnesses/etna/skills/brainstorm/README.md +1 -1
  260. package/data/harnesses/etna/skills/brainstorm/SKILL.md +1 -1
  261. package/data/harnesses/etna/skills/brainstorm/exit-artifacts.md +4 -4
  262. package/data/harnesses/etna/skills/brainstorm/templates/source-of-truth.md +3 -3
  263. package/data/harnesses/etna/skills/documentation/SKILL.md +61 -17
  264. package/data/harnesses/etna/skills/documentation/references/examples.md +41 -0
  265. package/data/harnesses/etna/skills/documentation/references/style-guide.md +14 -9
  266. package/data/harnesses/etna/skills/documentation/references/templates.md +305 -51
  267. package/data/harnesses/mobile/CLAUDE.md +1 -1
  268. package/data/harnesses/mobile/agents/brainstorm.md +1 -1
  269. package/data/harnesses/mobile/agents/maintenance.md +2 -2
  270. package/data/harnesses/mobile/harness.json +2 -2
  271. package/data/harnesses/mobile/rules/database.md +38 -0
  272. package/data/harnesses/mobile/rules/general.md +2 -40
  273. package/data/harnesses/mobile/rules/nomenclature.md +1 -8
  274. package/data/harnesses/mobile/rules/nustack.md +150 -0
  275. package/data/harnesses/mobile/skills/brainstorm/README.md +1 -1
  276. package/data/harnesses/mobile/skills/brainstorm/SKILL.md +1 -1
  277. package/data/harnesses/mobile/skills/brainstorm/exit-artifacts.md +4 -4
  278. package/data/harnesses/mobile/skills/brainstorm/templates/source-of-truth.md +3 -3
  279. package/data/harnesses/mobile/skills/documentation/SKILL.md +61 -17
  280. package/data/harnesses/mobile/skills/documentation/references/examples.md +41 -0
  281. package/data/harnesses/mobile/skills/documentation/references/style-guide.md +14 -9
  282. package/data/harnesses/mobile/skills/documentation/references/templates.md +305 -51
  283. package/package.json +1 -1
@@ -0,0 +1,249 @@
1
+ // skills/design/data/operator-pack/exemplars/command-center.tsx — personality-invariant: composition anchor, not a look source.
2
+ import { useMemo, useState } from "react";
3
+ import { Activity, AlertTriangle, ArrowDown, ArrowUp, Hand, Radio, RefreshCw, ShieldAlert, Signal, Undo2, WifiOff, type LucideIcon } from "lucide-react";
4
+ import { Button } from "@nurix/components/button";
5
+ import { Badge } from "@nurix/components/badge";
6
+ import { Card } from "@nurix/components/card";
7
+ import { Skeleton } from "@nurix/components/skeleton";
8
+ import { EmptyState } from "@nurix/components/empty-state";
9
+ import { Alert, AlertTitle, AlertDescription } from "@nurix/components/alert";
10
+ import { ScrollArea } from "@nurix/components/scroll-area";
11
+ import { Sparkline } from "@nurix/components/sparkline";
12
+ import { Kbd } from "@nurix/components/kbd";
13
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose } from "@nurix/components/dialog";
14
+ import { Statistics, StatisticsHeader, StatisticsHeading, StatisticsTitle, StatisticsMetric, StatisticsValue, StatisticsUnit, StatisticsTrend } from "@nurix/components/statistics";
15
+ type SurfaceState = "loading" | "ready" | "error";
16
+ type PanelStatus = "ok" | "degraded" | "down" | "loading";
17
+ type Sentiment = "negative" | "neutral" | "positive";
18
+ interface HealthPanel { key: string; label: string; status: PanelStatus; p95: number; trend: number[]; since: string; }
19
+ interface StreamEvent { id: string; at: string; session: string; kind: "started" | "escalated" | "resolved" | "dropped"; detail: string; }
20
+ interface Escalation { id: string; session: string; reason: string; sentiment: Sentiment; waitingSec: number; }
21
+ export interface CommandCenterProps {
22
+ state?: SurfaceState;
23
+ panels?: HealthPanel[];
24
+ events?: StreamEvent[];
25
+ escalations?: Escalation[];
26
+ updatedAt?: string;
27
+ onRetry?: () => void;
28
+ }
29
+
30
+ const STATUS_TONE: Record<PanelStatus, { text: string; word: string; Icon: LucideIcon }> = {
31
+ ok: { text: "text-success", word: "healthy", Icon: Signal },
32
+ degraded: { text: "text-warning", word: "degraded", Icon: AlertTriangle },
33
+ down: { text: "text-destructive", word: "down", Icon: WifiOff },
34
+ loading: { text: "text-muted-foreground", word: "probing", Icon: RefreshCw },
35
+ };
36
+ const SENTIMENT_TONE: Record<Sentiment, string> = { negative: "text-destructive", neutral: "text-muted-foreground", positive: "text-success" };
37
+ const KIND_LABEL: Record<StreamEvent["kind"], string> = { started: "Session started", escalated: "Escalated", resolved: "Resolved", dropped: "Call dropped" };
38
+
39
+ // Format a waiting duration in seconds as a compact mm:ss string.
40
+ function fmtWait(sec: number): string {
41
+ return `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, "0")}`;
42
+ }
43
+ // One KPI tile in the health header; the numeric value carries tabular figures.
44
+ function KpiTile({ label, value, unit, trend }: { label: string; value: string; unit?: string; trend?: number }) {
45
+ return (
46
+ <Statistics variant="card" size="md">
47
+ <StatisticsHeader>
48
+ <StatisticsHeading><StatisticsTitle size="sm">{label}</StatisticsTitle></StatisticsHeading>
49
+ </StatisticsHeader>
50
+ <StatisticsMetric>
51
+ <StatisticsValue className="tabular-nums">{value}</StatisticsValue>
52
+ {unit ? <StatisticsUnit>{unit}</StatisticsUnit> : null}
53
+ {trend !== undefined ? <StatisticsTrend value={trend} pill /> : null}
54
+ </StatisticsMetric>
55
+ </Statistics>
56
+ );
57
+ }
58
+ // One subsystem health panel; loads, degrades, and fails independently and names which.
59
+ function HealthTile({ panel }: { panel: HealthPanel }) {
60
+ if (panel.status === "loading") {
61
+ return (
62
+ <Card className="flex flex-col gap-3 p-4" role="status" aria-busy="true">
63
+ <Skeleton className="h-4 w-24" /><Skeleton className="h-7 w-16" /><Skeleton className="h-7 w-full" />
64
+ </Card>
65
+ );
66
+ }
67
+ const tone = STATUS_TONE[panel.status];
68
+ return (
69
+ <Card className="flex flex-col gap-2 p-4">
70
+ <div className="flex items-center justify-between">
71
+ <span className="text-sm font-medium text-foreground">{panel.label}</span>
72
+ <span className={`flex items-center gap-1 text-xs ${tone.text}`}><tone.Icon className="size-4" aria-hidden />{tone.word}</span>
73
+ </div>
74
+ <div className="flex items-baseline gap-1">
75
+ <span className="text-xl font-semibold tabular-nums text-foreground">{panel.p95}</span>
76
+ <span className="text-xs text-muted-foreground">ms p95</span>
77
+ </div>
78
+ <Sparkline points={panel.trend} type="line" height={28} className="w-full text-primary" />
79
+ {panel.status === "degraded" ? <p className="text-xs text-warning">Stale since {panel.since}; showing last known values.</p> : null}
80
+ {panel.status === "down" ? <p className="text-xs text-destructive">Probe timed out {panel.since} ago; retrying automatically.</p> : null}
81
+ </Card>
82
+ );
83
+ }
84
+ // The live session-event stream; additions announce politely without stealing focus.
85
+ function StreamPane({ events }: { events: StreamEvent[] }) {
86
+ return (
87
+ <Card className="flex min-h-0 flex-col">
88
+ <div className="flex items-center gap-2 border-b border-border px-4 py-3">
89
+ <Activity className="size-4 text-primary" aria-hidden />
90
+ <h2 className="text-sm font-semibold text-foreground">Live session stream</h2>
91
+ <Badge variant="outline" className="ml-auto tabular-nums">{events.length}</Badge>
92
+ </div>
93
+ <ScrollArea className="min-h-0 flex-1">
94
+ <ul aria-live="polite" aria-relevant="additions" className="divide-y divide-border">
95
+ {events.map((e) => (
96
+ <li key={e.id} className="flex items-baseline gap-3 px-4 py-2 text-sm">
97
+ <time className="shrink-0 font-mono tabular-nums text-muted-foreground">{e.at}</time>
98
+ <span className="shrink-0 font-mono text-muted-foreground">{e.session}</span>
99
+ <span className="shrink-0 text-foreground">{KIND_LABEL[e.kind]}</span>
100
+ <span className="truncate text-muted-foreground">{e.detail}</span>
101
+ </li>
102
+ ))}
103
+ </ul>
104
+ </ScrollArea>
105
+ </Card>
106
+ );
107
+ }
108
+ // The escalation queue: a sortable, keyboard-actionable table of sessions flagged for human takeover.
109
+ function EscalationTable({ rows, sort, onToggleSort, onTakeOver, onDismiss }: { rows: Escalation[]; sort: "asc" | "desc"; onToggleSort: () => void; onTakeOver: (e: Escalation) => void; onDismiss: (e: Escalation) => void }) {
110
+ return (
111
+ <table className="w-full border-collapse text-sm">
112
+ <thead className="sticky top-0 bg-card">
113
+ <tr className="border-b border-border text-left text-muted-foreground">
114
+ <th scope="col" className="px-3 py-2 font-medium">Session</th>
115
+ <th scope="col" className="px-3 py-2 font-medium">Reason</th>
116
+ <th scope="col" className="px-3 py-2 font-medium">Sentiment</th>
117
+ <th scope="col" aria-sort={sort === "asc" ? "ascending" : "descending"} className="px-3 py-2 text-right font-medium">
118
+ <button type="button" onClick={onToggleSort} className="ml-auto inline-flex items-center gap-1 rounded focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">Waiting{sort === "asc" ? <ArrowUp className="size-4" aria-hidden /> : <ArrowDown className="size-4" aria-hidden />}</button>
119
+ </th>
120
+ <th scope="col" className="px-3 py-2 text-right font-medium">Actions</th>
121
+ </tr>
122
+ </thead>
123
+ <tbody>
124
+ {rows.map((e) => (
125
+ <tr key={e.id} className="border-b border-border">
126
+ <td className="px-3 py-2"><span className="font-mono tabular-nums text-foreground">{e.session}</span></td>
127
+ <td className="px-3 py-2 text-muted-foreground"><span className="line-clamp-1">{e.reason}</span></td>
128
+ <td className="px-3 py-2"><span className={SENTIMENT_TONE[e.sentiment]}>{e.sentiment}</span></td>
129
+ <td className="px-3 py-2 text-right"><span className="font-mono tabular-nums text-foreground">{fmtWait(e.waitingSec)}</span></td>
130
+ <td className="px-3 py-2">
131
+ <div className="flex items-center justify-end gap-2">
132
+ <Button size="sm" onClick={() => onTakeOver(e)}><Hand className="size-4" aria-hidden />Take over</Button>
133
+ <Button size="sm" variant="ghost" onClick={() => onDismiss(e)}>Dismiss</Button>
134
+ </div>
135
+ </td>
136
+ </tr>
137
+ ))}
138
+ </tbody>
139
+ </table>
140
+ );
141
+ }
142
+ // The whole-surface loading state: skeleton tiles mirror the KPI, health, and pane grid.
143
+ function CommandSkeleton() {
144
+ return (
145
+ <div className="flex flex-1 flex-col gap-4" role="status" aria-busy="true">
146
+ <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">{[0, 1, 2, 3].map((i) => <Skeleton key={i} className="h-20 w-full" />)}</div>
147
+ <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">{[0, 1, 2, 3].map((i) => <Skeleton key={i} className="h-28 w-full" />)}</div>
148
+ <div className="grid min-h-64 flex-1 grid-cols-1 gap-4 lg:grid-cols-2"><Skeleton className="h-full w-full" /><Skeleton className="h-full w-full" /></div>
149
+ </div>
150
+ );
151
+ }
152
+ // Voice-AI live-ops bridge: monitor concurrent sessions, detect escalations, intervene under time pressure.
153
+ export function CommandCenter({ state = "ready", panels = SEED_PANELS, events = SEED_EVENTS, escalations = SEED_ESCALATIONS, updatedAt = "14:32:07", onRetry }: CommandCenterProps) {
154
+ const [sort, setSort] = useState<"asc" | "desc">("desc");
155
+ const [active, setActive] = useState<Escalation | null>(null);
156
+ const [handled, setHandled] = useState<{ id: string; session: string; verb: string } | null>(null);
157
+ const rows = useMemo(() => {
158
+ const visible = handled ? escalations.filter((e) => e.id !== handled.id) : escalations;
159
+ return [...visible].sort((a, b) => (sort === "asc" ? a.waitingSec - b.waitingSec : b.waitingSec - a.waitingSec));
160
+ }, [escalations, handled, sort]);
161
+ return (
162
+ <main data-density-step="tighter" className="flex min-h-[100dvh] flex-col gap-4 bg-background p-4 text-foreground">
163
+ <header className="flex flex-wrap items-center gap-3">
164
+ <Radio className="size-5 text-primary" aria-hidden />
165
+ <h1 className="text-lg font-semibold">Voice operations bridge</h1>
166
+ <span className="flex items-center gap-1 text-sm text-success"><Signal className="size-4" aria-hidden />Live</span>
167
+ <span className="ml-auto flex items-center gap-2 text-muted-foreground">Updated <time className="font-mono tabular-nums">{updatedAt}</time><Kbd>/</Kbd>to search</span>
168
+ </header>
169
+ {state === "loading" ? <CommandSkeleton /> : state === "error" ? (
170
+ <div className="flex flex-1 items-center justify-center">
171
+ <Alert variant="destructive" className="max-w-md">
172
+ <AlertTitle>Telemetry feed unavailable</AlertTitle>
173
+ <AlertDescription>The bridge lost its link to the session gateway, so live signal is not arriving. Reconnect to resume monitoring.</AlertDescription>
174
+ <Button size="sm" variant="outline" onClick={onRetry} className="mt-3"><RefreshCw className="size-4" aria-hidden />Reconnect</Button>
175
+ </Alert>
176
+ </div>
177
+ ) : (
178
+ <>
179
+ <section aria-label="Key metrics" className="grid grid-cols-2 gap-4 lg:grid-cols-4">{/* mock: illustrative KPI figures */}
180
+ <KpiTile label="Active sessions" value="128" trend={6} />
181
+ <KpiTile label="Escalations waiting" value={String(rows.length)} trend={-3} />
182
+ <KpiTile label="Median handle time" value="2:41" unit="min" />
183
+ <KpiTile label="Containment rate" value="72" unit="%" trend={2} />
184
+ </section>
185
+ <section aria-label="Subsystem health" className="grid grid-cols-2 gap-4 lg:grid-cols-4">
186
+ {panels.map((p) => <HealthTile key={p.key} panel={p} />)}
187
+ </section>
188
+ <section aria-label="Live operations" className="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-2">
189
+ <StreamPane events={events} />
190
+ <Card className="flex min-h-0 flex-col">
191
+ <div className="flex items-center gap-2 border-b border-border px-4 py-3">
192
+ <ShieldAlert className="size-4 text-primary" aria-hidden />
193
+ <h2 className="text-sm font-semibold text-foreground">Escalation queue</h2>
194
+ <Badge variant="outline" className="ml-auto tabular-nums">{rows.length}</Badge>
195
+ </div>
196
+ <ScrollArea className="min-h-0 flex-1">
197
+ {rows.length === 0 ? (
198
+ <EmptyState icon={<ShieldAlert className="size-8 text-muted-foreground" aria-hidden />} title="No escalations waiting" description="Every live session is being handled by the AI agent. Flagged sessions surface here for human takeover." />
199
+ ) : (
200
+ <EscalationTable rows={rows} sort={sort} onToggleSort={() => setSort((s) => (s === "asc" ? "desc" : "asc"))} onTakeOver={setActive} onDismiss={(e) => setHandled({ id: e.id, session: e.session, verb: "Dismissed" })} />
201
+ )}
202
+ </ScrollArea>
203
+ </Card>
204
+ </section>
205
+ {handled ? (
206
+ <div role="status" className="flex items-center gap-3 rounded-md border border-border bg-muted px-4 py-2 text-sm text-foreground">
207
+ <span>{handled.verb} session <span className="font-mono tabular-nums">{handled.session}</span>.</span>
208
+ <Button size="sm" variant="ghost" className="ml-auto" onClick={() => setHandled(null)}><Undo2 className="size-4" aria-hidden />Undo</Button>
209
+ </div>
210
+ ) : null}
211
+ </>
212
+ )}
213
+ <Dialog open={!!active} onOpenChange={(open) => { if (!open) setActive(null); }}>
214
+ <DialogContent variant="danger" size="sm">
215
+ <DialogHeader>
216
+ <DialogTitle>Take over session {active?.session}?</DialogTitle>
217
+ <DialogDescription>The AI agent will be muted and the caller connected to a human. The current turn is interrupted immediately.</DialogDescription>
218
+ </DialogHeader>
219
+ <DialogBody className="text-sm text-muted-foreground">Flagged for: {active?.reason} Waiting {active ? fmtWait(active.waitingSec) : ""}.</DialogBody>
220
+ <DialogFooter>
221
+ <DialogClose asChild><Button variant="ghost" size="sm">Cancel</Button></DialogClose>
222
+ <Button size="sm" onClick={() => { if (active) setHandled({ id: active.id, session: active.session, verb: "Took over" }); setActive(null); }}><Hand className="size-4" aria-hidden />Take over session</Button>
223
+ </DialogFooter>
224
+ </DialogContent>
225
+ </Dialog>
226
+ </main>
227
+ );
228
+ }
229
+ // exemplar fixture data — seeded, domain-shaped
230
+ const SEED_PANELS: HealthPanel[] = [
231
+ { key: "asr", label: "Speech recognition", status: "ok", p95: 214, trend: [190, 205, 198, 212, 208, 214], since: "" },
232
+ { key: "llm", label: "Language model", status: "degraded", p95: 1180, trend: [420, 610, 880, 1020, 1140, 1180], since: "14:29" },
233
+ { key: "tts", label: "Speech synthesis", status: "ok", p95: 96, trend: [88, 92, 90, 94, 91, 96], since: "" },
234
+ { key: "tel", label: "Telephony gateway", status: "down", p95: 0, trend: [140, 150, 0, 0, 0, 0], since: "2m" },
235
+ ];
236
+ const SEED_EVENTS: StreamEvent[] = [
237
+ { id: "v1", at: "14:32:07", session: "cs-4821", kind: "escalated", detail: "Three low-confidence recognition turns on a billing dispute; caller growing frustrated." },
238
+ { id: "v2", at: "14:32:01", session: "cs-4790", kind: "resolved", detail: "Refund confirmed, transcript archived." },
239
+ { id: "v3", at: "14:31:44", session: "cs-4818", kind: "started", detail: "Inbound, plan upgrade intent." },
240
+ { id: "v4", at: "14:31:22", session: "cs-4802", kind: "dropped", detail: "Carrier RTP timeout on the telephony gateway before the greeting completed." },
241
+ { id: "v5", at: "14:31:05", session: "cs-4795", kind: "escalated", detail: "Explicit request for a human agent." },
242
+ ];
243
+ const SEED_ESCALATIONS: Escalation[] = [
244
+ { id: "e-01", session: "cs-4821", reason: "Three low-confidence recognition turns on a billing dispute; caller growing frustrated.", sentiment: "negative", waitingSec: 187 },
245
+ { id: "e-02", session: "cs-4795", reason: "Explicit request for a human agent.", sentiment: "neutral", waitingSec: 94 },
246
+ { id: "e-03", session: "cs-4766", reason: "Sentiment dropped after a failed address verification.", sentiment: "negative", waitingSec: 63 },
247
+ { id: "e-04", session: "cs-4740", reason: "Refund amount exceeds the agent's autonomous limit.", sentiment: "neutral", waitingSec: 41 },
248
+ { id: "e-05", session: "cs-4712", reason: "Caller confirmed resolution; awaiting supervisor sign-off.", sentiment: "positive", waitingSec: 12 },
249
+ ];
@@ -0,0 +1,249 @@
1
+ // skills/design/data/operator-pack/exemplars/create-edit.tsx
2
+ // personality-invariant: composition anchor, not a look source.
3
+ import { useState, type FormEvent, type ReactNode } from "react";
4
+ import { TriangleAlert, CircleCheck, Plus, RotateCcw } from "lucide-react";
5
+ import { Sheet, SheetContent, SheetHeader, SheetBody, SheetFooter, SheetTitle, SheetDescription } from "@nurix/components/sheet";
6
+ import { Button } from "@nurix/components/button";
7
+ import { Input } from "@nurix/components/input";
8
+ import { NumberInput } from "@nurix/components/number-input";
9
+ import { Switch } from "@nurix/components/switch";
10
+ import { Checkbox } from "@nurix/components/checkbox";
11
+ import { Label } from "@nurix/components/label";
12
+ import { Badge } from "@nurix/components/badge";
13
+ import { Kbd } from "@nurix/components/kbd";
14
+ import { Skeleton } from "@nurix/components/skeleton";
15
+ import { EmptyState } from "@nurix/components/empty-state";
16
+ import { Alert, AlertTitle, AlertDescription } from "@nurix/components/alert";
17
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@nurix/components/native-select";
18
+ import { RadioGroupSegmented, RadioGroupSegmentedItem } from "@nurix/components/radio-group";
19
+
20
+ export type RuleSurfaceStatus = "loading" | "empty" | "error" | "degraded" | "ready";
21
+ export type ThresholdDirection = "above" | "below";
22
+ type FormPhase = "form" | "preview" | "success";
23
+ type SubmitStatus = "idle" | "submitting" | "failed";
24
+ type FieldErrors = Partial<Record<keyof AlertRuleDraft, string>>;
25
+
26
+ export interface NotifyChannel { id: string; label: string; kind: "email" | "webhook" | "pager"; }
27
+ export interface MetricOption { slug: string; label: string; unit: string; }
28
+ export interface SubmitOutcome { ok: boolean; message?: string; }
29
+
30
+ export interface AlertRuleDraft {
31
+ name: string; metricSlug: string; direction: ThresholdDirection;
32
+ threshold: number | null; channelIds: string[]; enabled: boolean;
33
+ }
34
+
35
+ export interface CreateEditRuleSurfaceProps {
36
+ mode: "create" | "edit"; status: RuleSurfaceStatus; open: boolean;
37
+ metrics: MetricOption[]; channels: NotifyChannel[]; metricsCachedAt?: string | null;
38
+ initial?: Partial<AlertRuleDraft>; onSubmit: (draft: AlertRuleDraft) => Promise<SubmitOutcome>;
39
+ onCancel: () => void; onAddChannel: () => void; onRetryLoad: () => void;
40
+ }
41
+
42
+ const BLANK_DRAFT: AlertRuleDraft = { name: "", metricSlug: "", direction: "above", threshold: null, channelIds: [], enabled: true };
43
+
44
+ // Collect blocking errors for the whole draft; re-run on every render so blur and submit share one source.
45
+ function validate(draft: AlertRuleDraft): FieldErrors {
46
+ const errors: FieldErrors = {};
47
+ if (draft.name.trim().length < 3) errors.name = "Name the rule (3 characters or more) so it's findable in the alert list.";
48
+ if (!draft.metricSlug) errors.metricSlug = "Choose the metric this rule watches.";
49
+ if (draft.threshold == null) errors.threshold = "Set the threshold value to compare against.";
50
+ if (draft.channelIds.length === 0) errors.channelIds = "Add at least one channel so alerts have somewhere to go.";
51
+ return errors;
52
+ }
53
+
54
+ // Label-over-input wrapper: the visible label, the control, then an inline error (or hint) below.
55
+ function Field({ id, label, hint, error, htmlFor, children }: { id: string; label: string; hint?: string; error?: string; htmlFor?: string; children: ReactNode }) {
56
+ return (
57
+ <div className="grid gap-1.5">
58
+ <Label id={`${id}-label`} htmlFor={htmlFor} className="text-sm font-medium">{label}</Label>
59
+ {children}
60
+ {error ? <p id={`${id}-error`} className="text-sm text-destructive">{error}</p> : hint ? <p className="text-sm text-muted-foreground">{hint}</p> : null}
61
+ </div>
62
+ );
63
+ }
64
+ // One label/value row inside the preview summary.
65
+ function PreviewRow({ label, children }: { label: string; children: ReactNode }) {
66
+ return (
67
+ <div className="grid grid-cols-[8rem_1fr] gap-4">
68
+ <dt className="text-muted-foreground">{label}</dt>
69
+ <dd className="text-foreground">{children}</dd>
70
+ </div>
71
+ );
72
+ }
73
+ // The create/edit alert-rule surface: a side sheet that opens beside the alert list and returns to it on close.
74
+ export function CreateEditRuleSurface(props: CreateEditRuleSurfaceProps) {
75
+ const { mode, status, open, metrics, channels, metricsCachedAt, onSubmit, onCancel, onAddChannel, onRetryLoad } = props;
76
+ const [draft, setDraft] = useState<AlertRuleDraft>(() => ({ ...BLANK_DRAFT, ...props.initial }));
77
+ const [touched, setTouched] = useState<Record<string, boolean>>({});
78
+ const [attempted, setAttempted] = useState(false);
79
+ const [phase, setPhase] = useState<FormPhase>("form");
80
+ const [submitStatus, setSubmitStatus] = useState<SubmitStatus>("idle");
81
+ const [submitError, setSubmitError] = useState<string | null>(null);
82
+ const errors = validate(draft);
83
+ const metric = metrics.find((m) => m.slug === draft.metricSlug);
84
+ const selectedChannels = channels.filter((c) => draft.channelIds.includes(c.id));
85
+ const isForm = status === "ready" || status === "degraded";
86
+ const busy = submitStatus === "submitting";
87
+ function patch<K extends keyof AlertRuleDraft>(key: K, value: AlertRuleDraft[K]) { setDraft((d) => ({ ...d, [key]: value })); }
88
+ function blur(key: keyof AlertRuleDraft) { setTouched((t) => ({ ...t, [key]: true })); }
89
+ const show = (key: keyof AlertRuleDraft): string | undefined => ((attempted || touched[key]) && errors[key]) || undefined;
90
+ // Validate the whole draft; a clean draft advances to the preview-then-commit step.
91
+ function handlePreview(event: FormEvent) {
92
+ event.preventDefault();
93
+ setAttempted(true);
94
+ setSubmitError(null);
95
+ if (Object.keys(errors).length === 0) setPhase("preview");
96
+ }
97
+ // Commit the reviewed draft; failure returns to the form with entries preserved and the reason inline.
98
+ async function handleConfirm() {
99
+ setSubmitStatus("submitting");
100
+ const outcome = await onSubmit(draft);
101
+ if (outcome.ok) { setSubmitStatus("idle"); setPhase("success"); }
102
+ else { setSubmitStatus("failed"); setSubmitError(outcome.message ?? "The alerting service rejected the rule."); setPhase("form"); }
103
+ }
104
+ function reset() { setDraft({ ...BLANK_DRAFT }); setTouched({}); setAttempted(false); setSubmitStatus("idle"); setSubmitError(null); setPhase("form"); }
105
+ return (
106
+ <Sheet open={open} onOpenChange={(next) => { if (!next) onCancel(); }}>
107
+ <SheetContent side="right" size="lg" data-density-step="looser">
108
+ <SheetHeader showDivider>
109
+ <SheetTitle>{mode === "create" ? "New alert rule" : "Edit alert rule"}</SheetTitle>
110
+ <SheetDescription>Watches one metric and notifies your channels when it crosses a threshold.</SheetDescription>
111
+ </SheetHeader>
112
+ <SheetBody>
113
+ {status === "loading" && (
114
+ <div className="grid gap-6" aria-hidden="true">
115
+ {[0, 1, 2, 3].map((i) => <div key={i} className="grid gap-2"><Skeleton className="h-4 w-28" /><Skeleton className="h-9 w-full" /></div>)}
116
+ </div>
117
+ )}
118
+ {status === "error" && (
119
+ <div className="grid justify-items-center gap-4 py-8 text-center" role="alert">
120
+ <TriangleAlert className="size-8 text-destructive" />
121
+ <p className="text-base font-semibold text-foreground">Couldn't load this rule</p>
122
+ <p className="max-w-sm text-sm text-muted-foreground">The alerting service didn't respond, so its current settings aren't available. Retry, or close and reopen it from the alert list.</p>
123
+ <Button variant="outline" onClick={onRetryLoad}><RotateCcw className="size-4" /> Retry</Button>
124
+ </div>
125
+ )}
126
+ {status === "empty" && (
127
+ <EmptyState
128
+ icon={<Plus className="size-6 text-muted-foreground" />}
129
+ title="No notification channels yet"
130
+ description="An alert rule needs somewhere to send alerts. Add a channel, then come back to finish this rule."
131
+ action={<Button onClick={onAddChannel}><Plus className="size-4" /> Add a channel</Button>}
132
+ />
133
+ )}
134
+ {isForm && phase === "form" && (
135
+ <form id="rule-form" onSubmit={handlePreview} className="grid gap-6">
136
+ {status === "degraded" && (
137
+ <Alert role="status" aria-live="polite">
138
+ <TriangleAlert className="size-4" /><AlertTitle>Metric list may be stale</AlertTitle>
139
+ <AlertDescription>The metrics service is unreachable, so this is a cached catalog from {metricsCachedAt}. A metric added since then won't appear here yet.</AlertDescription>
140
+ </Alert>
141
+ )}
142
+ {submitStatus === "failed" && submitError && (
143
+ <Alert variant="destructive" role="alert">
144
+ <TriangleAlert className="size-4" /><AlertTitle>Rule not created</AlertTitle>
145
+ <AlertDescription>{submitError} Your entries are preserved, adjust and try again.</AlertDescription>
146
+ </Alert>
147
+ )}
148
+ <Field id="name" label="Rule name" htmlFor="name" error={show("name")}>
149
+ <Input id="name" value={draft.name} placeholder="API error rate over 2 percent" onChange={(e) => patch("name", e.target.value)} onBlur={() => blur("name")} aria-invalid={show("name") ? true : undefined} aria-describedby={show("name") ? "name-error" : undefined} />
150
+ </Field>
151
+ <Field id="metric" label="Metric" htmlFor="metric" error={show("metricSlug")}>
152
+ <Select value={draft.metricSlug} onValueChange={(v) => { patch("metricSlug", v); blur("metricSlug"); }}>
153
+ <SelectTrigger id="metric" aria-invalid={show("metricSlug") ? true : undefined}><SelectValue placeholder="Choose a metric to watch" /></SelectTrigger>
154
+ <SelectContent>{metrics.map((m) => <SelectItem key={m.slug} value={m.slug}>{m.label}</SelectItem>)}</SelectContent>
155
+ </Select>
156
+ </Field>
157
+ <div className="grid grid-cols-2 gap-4">
158
+ <Field id="direction" label="Condition">
159
+ <RadioGroupSegmented value={draft.direction} aria-labelledby="direction-label" onValueChange={(v) => patch("direction", v as ThresholdDirection)}>
160
+ <RadioGroupSegmentedItem value="above">Above</RadioGroupSegmentedItem>
161
+ <RadioGroupSegmentedItem value="below">Below</RadioGroupSegmentedItem>
162
+ </RadioGroupSegmented>
163
+ </Field>
164
+ <Field id="threshold" label={`Threshold${metric ? ` (${metric.unit})` : ""}`} htmlFor="threshold" error={show("threshold")}>
165
+ <NumberInput id="threshold" className="tabular-nums" value={draft.threshold} step={0.5} allowDecimals onSave={(v) => patch("threshold", v)} onBlur={() => blur("threshold")} aria-invalid={show("threshold") ? true : undefined} aria-describedby={show("threshold") ? "threshold-error" : undefined} />
166
+ </Field>
167
+ </div>
168
+ <Field id="channels" label="Notify channels" error={show("channelIds")}>
169
+ <div className="grid gap-2" role="group" aria-labelledby="channels-label">
170
+ {channels.map((c) => (
171
+ <Label key={c.id} className="flex items-center gap-3 text-sm font-normal">
172
+ <Checkbox checked={draft.channelIds.includes(c.id)} onCheckedChange={(v) => { patch("channelIds", v ? [...draft.channelIds, c.id] : draft.channelIds.filter((id) => id !== c.id)); blur("channelIds"); }} />
173
+ <span className="text-foreground">{c.label}</span>
174
+ <Badge variant="outline" className="ml-auto">{c.kind}</Badge>
175
+ </Label>
176
+ ))}
177
+ </div>
178
+ </Field>
179
+ <Field id="enabled" label="Enable on save">
180
+ <div className="flex items-center gap-3">
181
+ <Switch checked={draft.enabled} aria-labelledby="enabled-label" onCheckedChange={(v) => patch("enabled", v)} />
182
+ <span className="text-sm text-muted-foreground">{draft.enabled ? "Starts evaluating as soon as it's created." : "Saved paused, enable it later from the alert list."}</span>
183
+ </div>
184
+ </Field>
185
+ </form>
186
+ )}
187
+ {isForm && phase === "preview" && (
188
+ <div className="grid gap-4">
189
+ <p className="text-base font-semibold text-foreground">Review before creating</p>
190
+ <dl className="grid gap-3 text-sm">
191
+ <PreviewRow label="Rule"><span className="font-medium text-foreground">{draft.name}</span></PreviewRow>
192
+ <PreviewRow label="Condition">{metric?.label} goes {draft.direction} <span className="font-mono tabular-nums">{draft.threshold}</span> {metric?.unit}</PreviewRow>
193
+ <PreviewRow label="Channels">{selectedChannels.map((c) => c.label).join(", ")}</PreviewRow>
194
+ <PreviewRow label="State">{draft.enabled ? "Enabled on create" : "Created paused"}</PreviewRow>
195
+ </dl>
196
+ <p className="text-sm text-muted-foreground">A rule that pages is hard to unsend, so confirm the scope before creating.</p>
197
+ </div>
198
+ )}
199
+ {isForm && phase === "success" && (
200
+ <div className="grid justify-items-center gap-4 py-8 text-center" role="status" aria-live="polite">
201
+ <CircleCheck className="size-8 text-success" />
202
+ <p className="text-base font-semibold text-foreground">Alert rule created</p>
203
+ <p className="max-w-sm text-sm text-muted-foreground">{draft.name} is {draft.enabled ? "live and evaluating now" : "saved and paused"}. It notifies <span className="tabular-nums">{selectedChannels.length}</span> channel{selectedChannels.length === 1 ? "" : "s"} when it fires.</p>
204
+ <div className="flex items-center gap-2">
205
+ <Button variant="outline" onClick={onCancel}>View rule</Button>
206
+ <Button onClick={reset}>Create another rule</Button>
207
+ </div>
208
+ </div>
209
+ )}
210
+ </SheetBody>
211
+ {isForm && phase !== "success" && (
212
+ <SheetFooter showDivider>
213
+ <div className="flex w-full items-center justify-between gap-4">
214
+ <p className="text-sm text-muted-foreground" role="status" aria-live="polite">
215
+ {busy ? "Creating rule" : phase === "form" ? <>Press <Kbd size="sm">Enter</Kbd> to review before creating.</> : "Nothing is created until you confirm."}
216
+ </p>
217
+ <div className="flex items-center gap-2">
218
+ {phase === "form" ? (
219
+ <>
220
+ <Button type="button" variant="ghost" onClick={onCancel}>Cancel</Button>
221
+ <Button type="submit" form="rule-form" size="lg">{mode === "create" ? "Create rule" : "Save changes"}</Button>
222
+ </>
223
+ ) : (
224
+ <>
225
+ <Button type="button" variant="ghost" onClick={() => setPhase("form")} disabled={busy}>Back to edit</Button>
226
+ <Button type="button" size="lg" onClick={handleConfirm} disabled={busy}>{busy ? "Creating rule" : "Confirm and create"}</Button>
227
+ </>
228
+ )}
229
+ </div>
230
+ </div>
231
+ </SheetFooter>
232
+ )}
233
+ </SheetContent>
234
+ </Sheet>
235
+ );
236
+ }
237
+
238
+ // exemplar fixture data — seeded, domain-shaped
239
+ export const metricCatalog: MetricOption[] = [
240
+ { slug: "api_error_rate", label: "API error rate", unit: "%" },
241
+ { slug: "p95_latency", label: "p95 response time", unit: "ms" },
242
+ { slug: "queue_depth", label: "Ingest queue depth", unit: "jobs" },
243
+ ];
244
+
245
+ export const notifyChannels: NotifyChannel[] = [
246
+ { id: "ch_oncall_primary", label: "On-call primary", kind: "pager" },
247
+ { id: "ch_platform_alerts", label: "#platform-alerts", kind: "webhook" },
248
+ { id: "ch_sre_mailing", label: "sre-team mailing list", kind: "email" },
249
+ ];