quoxcli 0.3.1 → 0.5.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 (306) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/actions.js +15 -0
  6. package/lib/api/endpoints/admin.js +27 -0
  7. package/lib/api/endpoints/agent-attach.js +7 -0
  8. package/lib/api/endpoints/blog.js +16 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/compliance.js +25 -0
  11. package/lib/api/endpoints/contactsq.js +4 -0
  12. package/lib/api/endpoints/devServerRegistry.js +25 -0
  13. package/lib/api/endpoints/engagement.js +26 -0
  14. package/lib/api/endpoints/evidence.js +20 -0
  15. package/lib/api/endpoints/hat.js +14 -0
  16. package/lib/api/endpoints/inbox.js +6 -0
  17. package/lib/api/endpoints/inboxq.js +19 -0
  18. package/lib/api/endpoints/index.js +14 -0
  19. package/lib/api/endpoints/loops.js +26 -0
  20. package/lib/api/endpoints/mon.js +295 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/quoxseo.js +6 -0
  23. package/lib/api/endpoints/server.js +21 -0
  24. package/lib/api/endpoints/setup.js +6 -0
  25. package/lib/api/endpoints/tools.js +13 -0
  26. package/lib/api/endpoints/v1.js +47 -0
  27. package/lib/commands/actions.js +81 -11
  28. package/lib/commands/admin.js +35 -0
  29. package/lib/commands/agent.js +11 -4
  30. package/lib/commands/agentAttach.js +61 -7
  31. package/lib/commands/approvals.js +198 -3
  32. package/lib/commands/ask.js +4 -2
  33. package/lib/commands/auth.js +64 -11
  34. package/lib/commands/backup.js +11 -4
  35. package/lib/commands/blog.js +110 -0
  36. package/lib/commands/capabilities.js +3 -2
  37. package/lib/commands/chat.js +142 -28
  38. package/lib/commands/chatCompanion.js +27 -0
  39. package/lib/commands/chatState.js +45 -0
  40. package/lib/commands/claude.js +7 -33
  41. package/lib/commands/compliance.js +109 -0
  42. package/lib/commands/connect.js +69 -0
  43. package/lib/commands/contacts.js +85 -4
  44. package/lib/commands/contactsSendAt.js +21 -0
  45. package/lib/commands/dev.js +6 -8
  46. package/lib/commands/doctor.js +22 -3
  47. package/lib/commands/engagement.js +90 -0
  48. package/lib/commands/evidence.js +155 -0
  49. package/lib/commands/ext.js +23 -0
  50. package/lib/commands/fleet.js +25 -0
  51. package/lib/commands/gripe.js +24 -13
  52. package/lib/commands/hat.js +76 -0
  53. package/lib/commands/image.js +68 -0
  54. package/lib/commands/inbox.js +37 -3
  55. package/lib/commands/inboxq.js +147 -0
  56. package/lib/commands/kuma.js +1123 -0
  57. package/lib/commands/kumaSla.js +174 -0
  58. package/lib/commands/licence.js +39 -1
  59. package/lib/commands/licenceLinkView.js +311 -0
  60. package/lib/commands/linkedin.js +19 -0
  61. package/lib/commands/loop.js +318 -0
  62. package/lib/commands/lucid.js +1 -0
  63. package/lib/commands/mail.js +161 -20
  64. package/lib/commands/memory.js +68 -4
  65. package/lib/commands/memorySync.js +158 -0
  66. package/lib/commands/mon.js +55 -144
  67. package/lib/commands/notes.js +29 -7
  68. package/lib/commands/notesSeal.js +259 -0
  69. package/lib/commands/objective.js +53 -3
  70. package/lib/commands/offload.js +2 -3
  71. package/lib/commands/org.js +21 -4
  72. package/lib/commands/plan.js +17 -0
  73. package/lib/commands/provider.js +87 -8
  74. package/lib/commands/qlarity.js +32 -1
  75. package/lib/commands/quoxseo.js +31 -0
  76. package/lib/commands/research.js +79 -0
  77. package/lib/commands/rows.js +9 -0
  78. package/lib/commands/run.js +20 -2
  79. package/lib/commands/scope.js +2 -2
  80. package/lib/commands/server.js +150 -0
  81. package/lib/commands/setup.js +127 -75
  82. package/lib/commands/shortcut.js +41 -0
  83. package/lib/commands/skills.js +24 -5
  84. package/lib/commands/slot.js +49 -9
  85. package/lib/commands/status.js +92 -44
  86. package/lib/commands/tool.js +30 -0
  87. package/lib/commands/vault.js +6 -3
  88. package/lib/commands/vm.js +2 -2
  89. package/lib/commands/walkthrough.js +14 -2
  90. package/lib/commands/watch.js +11 -2
  91. package/lib/commands/wazuh.js +824 -0
  92. package/lib/commands/wazuhIntake.js +317 -0
  93. package/lib/commands/wazuhReport.js +192 -0
  94. package/lib/commands/workflow.js +47 -4
  95. package/lib/commands/x.js +50 -0
  96. package/lib/commands/youtube.js +172 -0
  97. package/lib/core/actionItems.js +115 -0
  98. package/lib/core/agentAlias.js +2 -0
  99. package/lib/core/agentFamilies.js +7 -0
  100. package/lib/core/announce.js +86 -0
  101. package/lib/core/auth.js +20 -2
  102. package/lib/core/callMe.js +118 -0
  103. package/lib/core/client.js +37 -3
  104. package/lib/core/commandRegistrars.js +17 -0
  105. package/lib/core/config.js +29 -3
  106. package/lib/core/confirm.js +68 -7
  107. package/lib/core/crossOrgApprovals.js +69 -0
  108. package/lib/core/errors.js +52 -1
  109. package/lib/core/findingReconcile.js +90 -0
  110. package/lib/core/global-flags.js +1 -1
  111. package/lib/core/governedSession.js +136 -8
  112. package/lib/core/inboxResolution.js +3 -2
  113. package/lib/core/liveSessionsReport.js +1 -1
  114. package/lib/core/loginGuard.js +73 -0
  115. package/lib/core/loopGlyph.js +97 -0
  116. package/lib/core/offload.js +73 -7
  117. package/lib/core/org-override.js +5 -1
  118. package/lib/core/output.js +8 -8
  119. package/lib/core/profileTrust.js +182 -0
  120. package/lib/core/quoxglyph.js +22 -1
  121. package/lib/core/router.js +14 -0
  122. package/lib/core/sessionHeartbeat.js +12 -8
  123. package/lib/core/shortcut.js +83 -0
  124. package/lib/core/stepUpPrompt.js +99 -0
  125. package/lib/core/version.js +1 -1
  126. package/lib/core/vm.js +5 -0
  127. package/lib/core/walkthrough.js +9 -5
  128. package/lib/dragon/companionView.js +34 -0
  129. package/lib/dragon/frames.js +118 -0
  130. package/lib/dragon/jobs.js +123 -0
  131. package/lib/dragon/renderWorker.js +15 -0
  132. package/lib/dragon/select.js +72 -0
  133. package/lib/evidence/bundleExport.js +47 -0
  134. package/lib/evidence/bundleVerify.js +77 -0
  135. package/lib/quoxon/EXTENSIONS.md +146 -0
  136. package/lib/quoxon/api.js +364 -27
  137. package/lib/quoxon/app.js +1629 -183
  138. package/lib/quoxon/approvalBatch.js +117 -0
  139. package/lib/quoxon/approvalFacts.js +64 -8
  140. package/lib/quoxon/approvalSentence.js +48 -0
  141. package/lib/quoxon/approvalsView.js +124 -7
  142. package/lib/quoxon/autoOpen.js +94 -0
  143. package/lib/quoxon/backgroundWork.js +59 -0
  144. package/lib/quoxon/bang.js +1 -1
  145. package/lib/quoxon/capabilities.js +7 -2
  146. package/lib/quoxon/commandShort.js +22 -3
  147. package/lib/quoxon/commands.js +110 -15
  148. package/lib/quoxon/connect.js +5 -3
  149. package/lib/quoxon/countdownLine.js +10 -1
  150. package/lib/quoxon/deck.js +30 -0
  151. package/lib/quoxon/deckLazy.js +58 -0
  152. package/lib/quoxon/demo.autoplay.js +1 -1
  153. package/lib/quoxon/demo.js +3 -1
  154. package/lib/quoxon/destructive.js +35 -0
  155. package/lib/quoxon/dividerAocl.js +4 -3
  156. package/lib/quoxon/dividerRule.js +3 -3
  157. package/lib/quoxon/dnd.js +71 -0
  158. package/lib/quoxon/dragonCompanion.js +285 -0
  159. package/lib/quoxon/dragonModes.js +1 -0
  160. package/lib/quoxon/dragonMoments.js +160 -0
  161. package/lib/quoxon/dragonReads.js +68 -0
  162. package/lib/quoxon/dragonView.js +2 -2
  163. package/lib/quoxon/echo.js +1 -0
  164. package/lib/quoxon/events.js +67 -10
  165. package/lib/quoxon/extensions.js +636 -0
  166. package/lib/quoxon/gripeQueue.js +135 -0
  167. package/lib/quoxon/inboxView.js +182 -0
  168. package/lib/quoxon/installCli.js +79 -0
  169. package/lib/quoxon/keys.js +4 -4
  170. package/lib/quoxon/loopCard.js +460 -0
  171. package/lib/quoxon/loopCards.js +189 -0
  172. package/lib/quoxon/loopStream.js +98 -0
  173. package/lib/quoxon/mailView.js +71 -0
  174. package/lib/quoxon/main.js +29 -5
  175. package/lib/quoxon/markdown.js +1 -1
  176. package/lib/quoxon/model.js +15 -0
  177. package/lib/quoxon/mouse.js +12 -1
  178. package/lib/quoxon/neverAnswered.js +20 -0
  179. package/lib/quoxon/objectiveOffer.js +82 -0
  180. package/lib/quoxon/objects/away.js +43 -5
  181. package/lib/quoxon/objects/index.js +22 -4
  182. package/lib/quoxon/objects/model.js +33 -0
  183. package/lib/quoxon/objects/proof.js +12 -4
  184. package/lib/quoxon/onboarding/connectServer.js +162 -0
  185. package/lib/quoxon/onboarding/copy.js +6 -0
  186. package/lib/quoxon/onboarding/index.js +5 -43
  187. package/lib/quoxon/onboarding/probe.js +20 -1
  188. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  189. package/lib/quoxon/onboarding/view.js +16 -3
  190. package/lib/quoxon/orgSwitch.js +49 -1
  191. package/lib/quoxon/planSteps.js +51 -0
  192. package/lib/quoxon/print.js +8 -3
  193. package/lib/quoxon/profileSwitch.js +7 -0
  194. package/lib/quoxon/promptHistory.js +3 -1
  195. package/lib/quoxon/qdragonFly.js +7 -6
  196. package/lib/quoxon/qmotion.js +17 -0
  197. package/lib/quoxon/render.js +187 -41
  198. package/lib/quoxon/roster.js +36 -17
  199. package/lib/quoxon/rosterSessions.js +44 -0
  200. package/lib/quoxon/rows.js +244 -1
  201. package/lib/quoxon/screenContext.js +1 -1
  202. package/lib/quoxon/secretShape.js +86 -0
  203. package/lib/quoxon/serverChip.js +42 -0
  204. package/lib/quoxon/session.js +10 -0
  205. package/lib/quoxon/slash/agents.js +2 -1
  206. package/lib/quoxon/slash/argSources.js +9 -2
  207. package/lib/quoxon/slash/args.js +4 -2
  208. package/lib/quoxon/slash/broadcast.js +18 -5
  209. package/lib/quoxon/slash/broadcastRun.js +13 -2
  210. package/lib/quoxon/slash/cliCommand.js +42 -1
  211. package/lib/quoxon/slash/cliLeaves.json +139 -2
  212. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  213. package/lib/quoxon/slash/hatWalk.js +1 -1
  214. package/lib/quoxon/slash/palette.js +23 -2
  215. package/lib/quoxon/slash/paletteController.js +10 -4
  216. package/lib/quoxon/slash/qpeCmd.js +35 -0
  217. package/lib/quoxon/slash/restart.js +31 -4
  218. package/lib/quoxon/slash/resume.js +18 -1
  219. package/lib/quoxon/slash/stepUp.js +5 -5
  220. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  221. package/lib/quoxon/slash/todoCmd.js +122 -0
  222. package/lib/quoxon/slashSpinner.js +35 -0
  223. package/lib/quoxon/slotAttach.js +8 -0
  224. package/lib/quoxon/slotChat.js +17 -0
  225. package/lib/quoxon/slotFormat.js +1 -1
  226. package/lib/quoxon/slotOutbox.js +113 -0
  227. package/lib/quoxon/suggestCommand.js +34 -3
  228. package/lib/quoxon/term.js +25 -1
  229. package/lib/quoxon/toolCards.js +30 -0
  230. package/lib/quoxon/transcript.js +2 -1
  231. package/lib/quoxon/welcome.js +1 -1
  232. package/lib/quoxon/whatsnew.js +1 -1
  233. package/lib/quoxon/whatsnew.jsonl +81 -0
  234. package/lib/quoxon/windowOrg.js +11 -1
  235. package/lib/server/devServers.js +137 -0
  236. package/lib/server/installFlow.js +168 -0
  237. package/lib/server/status.js +15 -1
  238. package/lib/toolOutputShape.js +55 -0
  239. package/package.json +4 -2
  240. package/vendor/dragonlab/SOURCE +4 -0
  241. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  242. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  243. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  244. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  245. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  246. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  247. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  248. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  249. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  250. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  251. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  252. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  253. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  254. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  255. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  256. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  257. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  258. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  259. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  260. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  261. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  262. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  263. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  264. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  265. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  266. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  267. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  268. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  269. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  270. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  271. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  272. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  273. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  274. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  275. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  276. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  277. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  278. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  279. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  280. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  281. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  282. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  283. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  284. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  285. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  286. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  287. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  288. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  289. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  290. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  291. package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
  292. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  293. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  294. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  295. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  296. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  297. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  298. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  299. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  300. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  301. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
  302. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  303. package/vendor/dragonlab/lib/power-effects.js +294 -0
  304. package/vendor/dragonlab/lib/power-registry.js +74 -0
  305. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  306. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // One-shot Sentinel review for the companions launcher: node dragon.mjs sentinel [--scene ID] [--message TEXT] ...
3
+ import {renderSentinelCharacter,SENTINEL_CLIPS} from './sentinel-character.js';
4
+ import {renderSentinelIcon} from './sentinel-icon.js';
5
+ import {artSlot,composeMessage} from '../lucid/recovery/layout.mjs';
6
+ import {cellsToAnsi} from '../../vendor/quox-dragon/qart.js';
7
+ const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
8
+ for(let i=0;i<args.length;i++){
9
+ if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
10
+ else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
11
+ }
12
+ const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
13
+ if(args.includes('--help')){console.log('Sentinel companion review (one frame). --scene ID --at MS --list --large --mono --still --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N. Below 48x16 cells the approved shield icon is shown.');process.exit(0);}
14
+ if(args.includes('--list')){console.log(SENTINEL_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
15
+ const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
16
+ if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
17
+ const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro');
18
+ const cols=Number(value('--cols',large?Math.min(88,w-4):slot.cols)),rows=Number(value('--rows',large?Math.min(25,h-4):slot.rows));
19
+ const message=value('--message','SENTINEL\nOn watch.');
20
+ if(!slot.showArt&&!large&&!args.includes('--cols')){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
21
+ else{
22
+ const scene=value('--scene',args.includes('--attach')||args.includes('--intro')?'greeting':'idle');
23
+ const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still')};
24
+ let r=renderSentinelCharacter(scene,Number(value('--at',0)),opts);
25
+ if(!r.ok&&r.code==='ICON_FALLBACK'){const ih=Math.max(6,Math.min(rows,10));r=renderSentinelIcon({cols:Math.min(cols,ih*2),rows:ih,mono:opts.mono});}
26
+ if(!r.ok)throw Error(r.code);
27
+ console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,message,{terminalCols:w})]:composeMessage(r.frame,message,{terminalCols:w})).join('\n'));
28
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Design-stage prototype geometry. Body, horns, wings, tail, cheeks and
3
+ * mouth are transcribed from Stage 2 Aegis Bean. A2 eyes are the user's
4
+ * provisional "stronger white + separation" study, not final approval.
5
+ * This file is NOT a production mascot.
6
+ *
7
+ * Source masters:
8
+ * design/stage2/source/sentinel-geometry.json (candidate A)
9
+ * design/stage2/refinement/sentinel-a-solid-bold-whites.svg (eye A2)
10
+ */
11
+ export const SENTINEL_ART=Object.freeze({
12
+ source:'design/stage2/refinement/sentinel-a-solid-bold-whites.svg',
13
+ viewBox:[0,0,320,240],
14
+ paths:Object.freeze({
15
+ body:'M141 53 C115 49 103 69 99 102 C91 121 94 146 103 168 C108 192 130 212 154 209 C184 211 202 188 209 155 C217 126 212 98 198 76 C186 57 163 52 141 53 Z',
16
+ wingL:'M104 111 C83 85 57 57 38 49 L46 83 Q31 78 24 66 C25 100 44 118 73 126 Q45 126 34 114 C46 142 76 152 103 143 Z',
17
+ wingR:'M202 107 C226 77 248 49 266 47 L259 81 Q280 69 284 61 C291 98 270 117 247 127 Q276 125 289 116 C279 144 246 151 204 145 Z',
18
+ hornL:'M116 65 Q108 49 110 18 C96 32 93 51 100 72 Z',
19
+ hornR:'M172 61 Q195 37 192 13 C210 28 211 50 194 78 Z',
20
+ tail:'M206 178 C232 164 239 189 255 185 C271 181 272 165 268 152 L286 157 L289 169 L300 160 C300 180 288 196 272 200 C253 206 230 185 209 190 Z',
21
+ footL:'M116 199 Q115 210 127 215 Q135 214 137 207 Z',
22
+ footR:'M172 199 Q175 213 185 214 Q197 210 197 192 Z',
23
+ belly:'M123 161 C132 193 152 200 173 184 C155 211 136 210 120 188 Q115 174 123 161 Z',
24
+ mouth:'M143 165 Q158 179 174 163',
25
+ browL:'M101 72 Q125 64 142 75',
26
+ browR:'M167 73 Q187 64 204 77',
27
+ }),
28
+ eyes:Object.freeze({
29
+ islands:{centres:[[126,112],[181,111]],whiteRadius:[21,27],pupil:.45},
30
+ bold:{centres:[[125,113],[182,112]],whiteRadius:[23,29],pupil:.44},
31
+ }),
32
+ cheeks:Object.freeze([[99,159,8],[211,150,6.5]]),
33
+ nose:Object.freeze([99,152,3.3]),
34
+ colors:Object.freeze({
35
+ body:'#5799EE',wing:'#356CB9',horn:'#86D2FF',tail:'#5799EE',
36
+ foot:'#366CB9',belly:'#8ECFFF',cheek:'#FA93B7',brow:'#B3E7FF',
37
+ eyeWhite:'#FFFFFF',
38
+ }),
39
+ });
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Sentinel Aegis Bean / A2 solid-eye braille PROTOTYPE.
3
+ *
4
+ * Uses the ACTUAL original Q modules for mask-field->Braille conversion,
5
+ * deterministic blink/gaze and exclusive terminal-cell eye ownership.
6
+ * Only source SVG geometry is newly rasterised. No terminal API / Node I/O.
7
+ *
8
+ * opt-in prototype on an isolated branch; not called by the Lair or Quox CLI.
9
+ */
10
+ import {makeField,fieldToCells} from '../../vendor/quox-dragon/qart.js';
11
+ import {eyeCells,eyeState,overlay,GOOGLY,EYE_WHITE} from '../../vendor/quox-dragon/qeyes.js';
12
+ import {SENTINEL_ART as ART} from './sentinel-art.js';
13
+ import {flattenPath,sampleShape} from './svg-coverage.js';
14
+ import {renderWardenEyes} from './sentinel-warden-eyes.js';
15
+
16
+ const BASE=Object.freeze({cols:88,rows:25,mono:false,theme:'dark',eyes:'bold',still:false});
17
+ const AXIS=.41;
18
+ const CACHE=new Map();
19
+ const BASE_CACHE=new Map();
20
+ function cachedBase(key,make){
21
+ if(BASE_CACHE.has(key))return BASE_CACHE.get(key);
22
+ const item=make();
23
+ if(BASE_CACHE.size>=16)BASE_CACHE.delete(BASE_CACHE.keys().next().value);
24
+ BASE_CACHE.set(key,item);
25
+ return item;
26
+ }
27
+ const clamp=(x,a,b)=>Math.min(b,Math.max(a,x));
28
+ function hex(s){return [1,3,5].map(i=>parseInt(s.slice(i,i+2),16));}
29
+ const TONES=Object.fromEntries(Object.entries(ART.colors).map(([k,v])=>[k,hex(v)]));
30
+ function polygon(path){
31
+ if(!CACHE.has(path))CACHE.set(path,flattenPath(path));
32
+ return CACHE.get(path);
33
+ }
34
+ function circle(x,y,r){
35
+ const a=[];for(let k=0;k<36;k++){const th=k*Math.PI*2/36;a.push([x+Math.cos(th)*r,y+Math.sin(th)*r*.63]);}
36
+ return [a];
37
+ }
38
+ function bounds(frame){
39
+ let minX=frame[0].length,maxX=-1,minY=frame.length,maxY=-1,n=0;
40
+ for(let y=0;y<frame.length;y++)for(let x=0;x<frame[y].length;x++)
41
+ if(frame[y][x].ch!==' '&&frame[y][x].ch!=='\u2800'){
42
+ minX=Math.min(x,minX);maxX=Math.max(x,maxX);
43
+ minY=Math.min(y,minY);maxY=Math.max(y,maxY);n++;
44
+ }
45
+ return {minX,maxX,minY,maxY,cells:n,
46
+ width:n?maxX-minX+1:0,height:n?maxY-minY+1:0};
47
+ }
48
+ function pack(field,inks,mono){
49
+ // This is the production qart.js cell packer: same BR dot-order,
50
+ // same visual threshold, same per-cell bitmask and no invented font.
51
+ const rows=fieldToCells(field,{color:!mono,quant:false,tint:TONES.body});
52
+ if(mono)return rows.map(row=>row.map(c=>({...c,rgb:null,bg:null})));
53
+ const W=field.W;
54
+ return rows.map((row,y)=>row.map((c,x)=>{
55
+ if(c.ch===' ')return {...c,bg:null};
56
+ let winner=-1,m=0;
57
+ for(let dy=0;dy<4;dy++)for(let dx=0;dx<2;dx++){
58
+ const i=(y*4+dy)*W+x*2+dx;
59
+ if(field.v[i]>m){m=field.v[i];winner=i;}
60
+ }
61
+ const f=.28+.72*Math.min(1,m),flash=clamp(m-1,0,1);
62
+ const k=winner*3;
63
+ const rgb=[inks[k],inks[k+1],inks[k+2]]
64
+ .map(v=>Math.round(v*f+(255-v*f)*flash*.9));
65
+ return {...c,rgb,bg:null};
66
+ }));
67
+ }
68
+ function eyeGeometry(eyeVariant,scale,ox,oy,cols,rows){
69
+ const style=ART.eyes[eyeVariant];
70
+ const [[lx,ly],[rx,ry]]=style.centres;
71
+ const designHeight=200,Hq=scale*designHeight;
72
+ const axis=(lx+rx)*.5;
73
+ // Source-space coordinates mapped to Q's actual symmetric, even-dot eye rig.
74
+ // Artistic eye asymmetry is knowingly deferred for this first parity proof.
75
+ const geo={cols,rows,Hq,ox:ox+axis*scale-AXIS*Hq,oy};
76
+ const variant={...GOOGLY,
77
+ r:style.whiteRadius[0]/designHeight,
78
+ sep:(rx-lx)/(2*designHeight),
79
+ y:((ly+ry)/2)/designHeight,
80
+ pupil:style.pupil,
81
+ ox:1.01,oy:1.04,rim:Math.max(1,1.8*scale),
82
+ rest:[.45,.65],
83
+ };
84
+ return {geo,variant};
85
+ }
86
+
87
+ // Static silhouette poses are kept to a few identifiable acting beats.
88
+ // Resting geometry is byte-for-byte the already loved A2 prototype.
89
+ const POSES=Object.freeze(['rest','alert','brace']);
90
+ const MOUTHS=Object.freeze(['smile','flat','roar']);
91
+ function moved(polygons,centre,angle){
92
+ if(!angle)return polygons;
93
+ const sin=Math.sin(angle),cos=Math.cos(angle),[cx,cy]=centre;
94
+ return polygons.map(poly=>poly.map(([x,y])=>[
95
+ cx+(x-cx)*cos-(y-cy)*sin,
96
+ cy+(x-cx)*sin+(y-cy)*cos
97
+ ]));
98
+ }
99
+ function renderBase(cols,rows,mono,pose='rest',mouth='smile',stern=false,poseAmount=0){
100
+ const W=cols*2,H=rows*4;
101
+ const scale=Math.min((W-10)/320,(H-10)/240,.55);
102
+ const ox=(W-320*scale)/2,oy=(H-240*scale)/2;
103
+ const F=makeField(W,H);
104
+ const priority=new Float32Array(W*H),inks=new Uint8Array(W*H*3);
105
+ const apply=(polygons,colour,z,intensity=1,erase=false)=>{
106
+ const tone=TONES[colour]||TONES.body;
107
+ sampleShape(polygons,{W,H,scale,ox,oy},(x,y)=>{
108
+ const i=y*W+x;if(z<priority[i])return;
109
+ priority[i]=z;F.v[i]=erase?0:intensity;
110
+ inks.set(tone,i*3);
111
+ });
112
+ };
113
+ const stroked=(source,width,colour,z,intensity=1,erase=false)=>{
114
+ const tone=TONES[colour]||TONES.body;
115
+ sampleShape(polygon(source),{W,H,scale,ox,oy,stroke:width},(x,y)=>{
116
+ const i=y*W+x;if(z<priority[i])return;
117
+ priority[i]=z;F.v[i]=erase?0:intensity;
118
+ inks.set(tone,i*3);
119
+ });
120
+ };
121
+ // Same painter's order as Q: wings/tail first, body above, eyes last.
122
+ // Pose intensity is quantised into seven deterministic wing/tail poses.
123
+ // The eye compositor still owns its cells, and the original rest pose is
124
+ // exactly preserved when the incoming amount is zero.
125
+ const bend=Math.max(0,Math.min(1,poseAmount));
126
+ const wingAngle=pose==='brace'?.27*bend:pose==='alert'?.12*bend:0;
127
+ const tailAngle=pose==='brace'?-.15*bend:pose==='alert'?.07*bend:0;
128
+ apply(moved(polygon(ART.paths.wingL),[104,111],wingAngle),'wing',1,.60);
129
+ apply(moved(polygon(ART.paths.wingR),[202,107],-wingAngle*.83),'wing',1,.60);
130
+ apply(moved(polygon(ART.paths.tail),[207,180],tailAngle),'tail',2,.90);
131
+ apply(polygon(ART.paths.footL),'foot',2.5,.83);
132
+ apply(polygon(ART.paths.footR),'foot',2.5,.83);
133
+ apply(polygon(ART.paths.hornL),'horn',3,1);
134
+ apply(polygon(ART.paths.hornR),'horn',3,1);
135
+ apply(polygon(ART.paths.body),'body',4,1);
136
+ apply(polygon(ART.paths.belly),'belly',4.1,.92);
137
+ for(const [x,y,r] of ART.cheeks)
138
+ apply(circle(x,y,r),'cheek',5,1.03);
139
+ apply(circle(...ART.nose),'brow',6,1.1);
140
+ // No floating "oh lovely!" raised brows in Sentinel's serious mode.
141
+ // His menace comes from the SHAPE of the eyelids and eye whites themselves.
142
+ if(!stern){
143
+ stroked(ART.paths.browL,3,'brow',6,1);
144
+ stroked(ART.paths.browR,3,'brow',6,1);
145
+ }
146
+ const mouthPath=mouth==='flat'
147
+ ?'M143 165 Q158 164 174 163'
148
+ :mouth==='roar'?'M137 164 Q153 148 174 156 Q160 171 137 164 Z'
149
+ :ART.paths.mouth;
150
+ if(mouth==='roar')apply(polygon(mouthPath),'body',7,0,true);
151
+ else stroked(mouthPath,mouth==='flat'?4:5,'body',7,0,true);
152
+
153
+ const base=pack(F,inks,mono);
154
+ return {base,scale,ox,oy};
155
+ }
156
+
157
+ /**
158
+ * Deterministic pure render of the stage-2 approved-direction A solid body.
159
+ *
160
+ * We intentionally refuse the tiny width, instead of claiming that shrinking
161
+ * this character works at 32 columns. A separately approved icon is needed.
162
+ */
163
+ export function renderSentinelPrototype(ms=1200,options={}){
164
+ const opts={...BASE,...options},{cols,rows}=opts;
165
+ if(!Number.isSafeInteger(cols)||!Number.isSafeInteger(rows)||cols<=0||rows<=0)
166
+ return {ok:false,code:'INVALID_VIEWPORT',reason:'Positive integer art cell dimensions required.'};
167
+ if(cols>200||rows>72)return {ok:false,code:'TOO_LARGE',reason:'Art grid exceeds 200×72 cells.'};
168
+ if(cols<48||rows<16)
169
+ return {ok:false,code:'ICON_FALLBACK',reason:'Sentinel A requires at least 48×16 art cells; use icon or text.'};
170
+ if(!['dark','light'].includes(opts.theme))
171
+ return {ok:false,code:'UNKNOWN_THEME',reason:'Theme must be dark or light.'};
172
+ if(!Object.hasOwn(ART.eyes,opts.eyes))
173
+ return {ok:false,code:'UNKNOWN_EYES',reason:'Use bold or islands eye study.'};
174
+ const pose=opts.pose??'rest',mouth=opts.mouth??'smile',stern=Boolean(opts.stern);
175
+ if(opts.poseAmount!==undefined&&(!Number.isFinite(opts.poseAmount)||opts.poseAmount<0||opts.poseAmount>1))
176
+ return {ok:false,code:'INVALID_POSE_AMOUNT',reason:'Pose amount must be between 0 and 1.'};
177
+ const poseAmount=opts.poseAmount===undefined?1:Math.round(opts.poseAmount*6)/6;
178
+ if(!POSES.includes(pose))return {ok:false,code:'UNKNOWN_POSE',reason:'Unknown Sentinel wing/tail pose.'};
179
+ if(!MOUTHS.includes(mouth))return {ok:false,code:'UNKNOWN_MOUTH',reason:'Unknown Sentinel mouth shape.'};
180
+ const cacheKey=[cols,rows,Boolean(opts.mono),pose,mouth,stern,poseAmount].join(':');
181
+ const {base,scale,ox,oy}=cachedBase(
182
+ cacheKey,()=>renderBase(cols,rows,Boolean(opts.mono),pose,mouth,stern,poseAmount)
183
+ );
184
+ const {geo,variant}=eyeGeometry(opts.eyes,scale,ox,oy,cols,rows);
185
+ const time=opts.still?0:Number.isFinite(ms)?Math.max(0,ms):0;
186
+ // EXACT original Q eyeCells and overlay, including whole-cell footprint
187
+ // ownership. The underlying solid body NEVER bleeds into an eye cell.
188
+ const warden=opts.warden;
189
+ const eyeLayer=warden
190
+ ?renderWardenEyes(geo,variant,{...warden,mono:Boolean(opts.mono)})
191
+ :eyeCells(geo,opts.eyeOverride??eyeState(opts.still?'open':'idle',time),variant);
192
+ const eyes=opts.mono?eyeLayer.map(row=>row.map(c=>c?{...c,rgb:null,bg:null}:null)):eyeLayer;
193
+ const frame=overlay(base,eyes).map(row=>row.map(c=>({
194
+ ch:c.ch,rgb:c.rgb||null,bg:null,
195
+ })));
196
+ const b=bounds(frame);
197
+ if(b.minX<1||b.maxX>=cols-1||b.minY<1||b.maxY>=rows-1)
198
+ return {ok:false,code:'ART_CLIPPED',reason:'Art touches its reserved safe margin.',bounds:b};
199
+ return {ok:true,frame,dragon:'Sentinel',prototype:true,
200
+ design:'Aegis Bean A2 · solid body',eyeVariant:opts.eyes,
201
+ pose,poseAmount,mouth,expression:warden?'warden':'rest',
202
+ cols,rows,artSize:b,scale,
203
+ // Snout points LEFT in the A silhouette; mouth for defensive fire must
204
+ // follow this geometry, not the legacy Q body's assumed mouth position.
205
+ anchors:{mouth:{
206
+ x:(ox+ART.nose[0]*scale)/2,
207
+ y:(oy+ART.nose[1]*scale)/4,
208
+ }},
209
+ source:ART.source,method:'qart.fieldToCells + qeyes.eyeCells/overlay',
210
+ diagnostics:{
211
+ eyeOwnedCells:eyeLayer.flat().filter(c=>c!==null).length,
212
+ eyeWhiteCells:eyeLayer.flat().filter(c=>c?.ch&&c.ch!==' ').length,
213
+ eyeCutoutCells:eyeLayer.flat().filter(c=>c?.ch===' ').length,
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * SENTINEL, full-character acting rig. Stage 3, isolated prototype.
3
+ *
4
+ * All expression timing is pure / deterministic. Under threat, Sentinel's
5
+ * EYE WHITES become rounded triangular wedges angled inward into a V.
6
+ * It is NOT a surprised character with brows raised above round eyes.
7
+ *
8
+ * No security operations are performed here. "Aegis Inferno" is an
9
+ * explicitly-labelled, fictional DEMO unless a host supplies evidence.
10
+ */
11
+ import {renderSentinelPrototype} from './sentinel-braille.js';
12
+ import {renderPower} from '../../lib/power-effects.js';
13
+
14
+ const clamp=(x,a=0,b=1)=>Math.max(a,Math.min(b,x));
15
+ const ease=u=>{u=clamp(u);return u*u*(3-2*u);};
16
+ const ramp=(t,a,b)=>ease((t-a)/Math.max(1,b-a));
17
+ const approach=(a,b,u)=>a+(b-a)*clamp(u);
18
+
19
+ export const SENTINEL_CLIPS=Object.freeze([
20
+ {id:'idle',title:'Clocktower',duration:4000,loop:true,message:'On watch'},
21
+ {id:'greeting',title:'Reporting In',duration:2000,message:'Sentinel attached. Ready to inspect.'},
22
+ {id:'scan',title:'Sweep',duration:2500,message:'Scanning'},
23
+ {id:'suspicious',title:'Something Moved',duration:1600,message:'Anomaly detected. Assessing.'},
24
+ {id:'warning',title:'Gate Warning',duration:1600,message:'Attention required'},
25
+ {id:'warden',title:'Warden V',duration:1500,message:'Threat confirmed. Holding position.'},
26
+ {id:'brace',title:'Hold the Gate',duration:2000,message:'Protective action in progress'},
27
+ {id:'scorch',title:'Aegis Inferno',duration:3800,message:'Aegis Inferno · demonstration only',demo:true},
28
+ {id:'holding',title:'Holding',duration:2400,message:'Control active; verification pending'},
29
+ {id:'all-clear',title:'All Clear',duration:1700,message:'Scoped result verified'},
30
+ {id:'failure',title:'Not Yet',duration:1500,message:'Unable to verify protection'},
31
+ {id:'approval',title:'Your Call',duration:2000,message:'Authorisation required'},
32
+ {id:'standby',title:'Low Watch',duration:3500,loop:true,message:'On standby'},
33
+ ]);
34
+ export const SENTINEL_CLIP_IDS=Object.freeze(SENTINEL_CLIPS.map(c=>c.id));
35
+ const CLIPS=new Map(SENTINEL_CLIPS.map(c=>[c.id,c]));
36
+
37
+ // Scene pose is the character acting; effect is added only after face/wings
38
+ // have moved. Holding/back-on-watch is a distinct posture from victory.
39
+ function performance(scene,t){
40
+ const s=(a,b)=>ramp(t,a,b);
41
+ switch(scene){
42
+ case 'idle':return {pose:'rest',mouth:'smile',stern:false,
43
+ gaze:[Math.sin(t/900)*.28,.45],severity:0};
44
+ case 'greeting':return {pose:t>=420&&t<1000?'alert':'rest',
45
+ mouth:'smile',stern:false,gaze:[.15,-.08],severity:0,
46
+ softEyes:{open:1,gx:t<400?0:.22,gy:t<700?-.45:.35}};
47
+ case 'scan':return {pose:'rest',mouth:'flat',stern:true,
48
+ gaze:[Math.sin(t/460)*.85,.18],severity:.13+.08*s(350,900)};
49
+ case 'suspicious':{
50
+ const k=s(100,650);
51
+ return {pose:t>=500?'alert':'rest',mouth:'flat',stern:true,
52
+ gaze:[.42,.12],severity:.12+.54*k,
53
+ leftSeverity:.10+.49*k,rightSeverity:.18+.63*k};
54
+ }
55
+ case 'warning':return {pose:'alert',mouth:'flat',stern:true,
56
+ gaze:[.18,.05],severity:.45+.27*s(50,650)};
57
+ case 'warden':{
58
+ const k=s(120,480);
59
+ return {pose:t>=400?'brace':t>=170?'alert':'rest',
60
+ mouth:t>=420?'flat':'smile',stern:t>100,gaze:[0,.02],
61
+ severity:.97*k};
62
+ }
63
+ case 'brace':return {pose:t>=350?'brace':'alert',mouth:'flat',
64
+ stern:true,gaze:[-.5,.08],severity:.72+.26*s(90,550)};
65
+ case 'holding':return {pose:'brace',mouth:'flat',stern:true,
66
+ gaze:[-.30,.09],severity:.70};
67
+ case 'approval':return {pose:'rest',mouth:'flat',stern:true,
68
+ gaze:[-.30,.30],severity:.33,
69
+ leftSeverity:.25,rightSeverity:.49};
70
+ case 'failure':{
71
+ const k=s(220,750);
72
+ return {pose:t<850?'alert':'rest',mouth:'flat',stern:true,
73
+ gaze:[-.25,.60],severity:.62-.42*k,
74
+ leftSeverity:.55-.43*k,rightSeverity:.70-.34*k};
75
+ }
76
+ case 'standby':return {pose:'rest',mouth:'smile',stern:false,
77
+ gaze:[.2,.70],severity:0,
78
+ softEyes:{open:.50+.10*Math.sin(t/550),gx:.10,gy:.82}};
79
+ case 'all-clear':{
80
+ const release=s(230,980);
81
+ return {pose:t<1100?'brace':'rest',
82
+ mouth:t<500?'flat':'smile',stern:t<950,
83
+ gaze:[-.25+release*.42,.38],
84
+ severity:.95*(1-release)};
85
+ }
86
+ case 'scorch':{
87
+ const intent=s(340,760);
88
+ const recover=s(2920,3620);
89
+ const fire=t>=1850&&t<2790;
90
+ return {
91
+ pose:t>=800&&t<3650?'brace':t>=350&&t<800?'alert':'rest',
92
+ mouth:fire?'roar':t>=350&&t<3400?'flat':'smile',
93
+ stern:t>=320&&t<3600,
94
+ gaze:[-.64*intent+.25*recover,.13],
95
+ severity:Math.max(0,Math.min(.97,intent*.97)*(1-.8*recover)),
96
+ // A brief narrowed glare at the peak; eyes are NEVER blank for
97
+ // the entire flame. The V look comes before the fire begins.
98
+ openness:fire&&t>=2210&&t<2310?.64:1,
99
+ };
100
+ }
101
+ default:throw new RangeError('Unrecognised Sentinel clip: '+scene);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Smoothly animate two small wing rotations without introducing continuous
107
+ * non-deterministic geometry churn. The rest A2 art remains the canonical
108
+ * untouched pose. Transition from alert's 0.12 radians to brace's 0.27
109
+ * radians uses a matching 0.12 / 0.27 bridge, so pose changes don't snap.
110
+ */
111
+ function posture(scene,t,pose){
112
+ if(pose==='rest')return 0;
113
+ if(scene==='greeting')return ramp(t,420,630)*(1-ramp(t,780,990));
114
+ if(scene==='scan')return .2;
115
+ if(scene==='suspicious')return ramp(t,500,1000);
116
+ if(scene==='warning')return ramp(t,0,400);
117
+ if(scene==='approval')return .2;
118
+ if(scene==='failure')return 1-ramp(t,350,840);
119
+ if(scene==='warden')return pose==='alert'
120
+ ?ramp(t,170,390)
121
+ :.45+.55*ramp(t,400,760);
122
+ if(scene==='brace')return pose==='alert'
123
+ ?ramp(t,0,345)
124
+ :.45+.55*ramp(t,350,800);
125
+ if(scene==='holding')return 1;
126
+ if(scene==='all-clear')return 1-ramp(t,200,1040);
127
+ if(scene==='scorch'){
128
+ if(pose==='alert')return ramp(t,350,795);
129
+ return (.45+.55*ramp(t,800,1350))*(1-ramp(t,3180,3610));
130
+ }
131
+ return 1;
132
+ }
133
+
134
+ const FRAME_BYTES=200*72*4;
135
+ function checkFrame(frame,cols,rows){
136
+ if(!Array.isArray(frame)||frame.length!==rows||
137
+ frame.some(row=>!Array.isArray(row)||row.length!==cols))
138
+ return 'NON_RECTANGULAR_FRAME';
139
+ if(frame.flat().some(c=>!c||typeof c.ch!=='string'||c.bg!=null))
140
+ return 'INVALID_CELL_OR_BACKGROUND';
141
+ return null;
142
+ }
143
+
144
+ /**
145
+ * renderSentinelCharacter(scene, t, options)
146
+ *
147
+ * - Usable from a normal Quox CLI conversation art slot.
148
+ * - No global clocks, networking, user data, DOM, filesystem or timers.
149
+ * - Returns structured error and icon fallback below 48x16.
150
+ * - Always returns visualOnly:true, even for serious expressions.
151
+ * - No victory / all-clear implied by this renderer without host evidence.
152
+ */
153
+ export function renderSentinelCharacter(scene='idle',ms=0,options={}){
154
+ const clip=CLIPS.get(scene);
155
+ if(!clip)return {ok:false,code:'UNKNOWN_CLIP',
156
+ reason:'Unknown Sentinel clip: '+String(scene),available:SENTINEL_CLIP_IDS};
157
+ const {cols=88,rows=25,mono=false,still=false,theme='dark',eyes='bold'}=options;
158
+ // LAB by default. A host may opt into real-status rendering ONLY with
159
+ // scoped evidence from its own verified execution engine.
160
+ const demo=options.demo!==false;
161
+ if(!demo&&scene==='all-clear'&&!(options.evidence?.verified===true&&
162
+ typeof options.evidence?.scope==='string'&&options.evidence.scope.trim()))
163
+ return {ok:false,code:'EVIDENCE_REQUIRED',
164
+ reason:'All Clear requires scoped, independently verified evidence.'};
165
+ if(!demo&&scene==='scorch'&&!(options.evidence?.approved===true&&
166
+ options.evidence?.verified===true))
167
+ return {ok:false,code:'EVIDENCE_REQUIRED',
168
+ reason:'Scorch in real-status mode requires approved, verified protective action.'};
169
+ if(!Number.isSafeInteger(cols)||!Number.isSafeInteger(rows)||cols<=0||rows<=0)
170
+ return {ok:false,code:'INVALID_VIEWPORT',reason:'Positive integer art dimensions required.'};
171
+ if(cols>200||rows>72)return {ok:false,code:'TOO_LARGE',
172
+ reason:'Maximum art budget is 200 by 72 cells.'};
173
+ if(cols<48||rows<16)return {ok:false,code:'ICON_FALLBACK',
174
+ reason:'At this size show a Sentinel face icon or a labelled text status.'};
175
+ let time=Number.isFinite(ms)?Math.max(0,ms):0;
176
+ if(still)time=scene==='scorch'?1580:
177
+ ['warden','brace','holding'].includes(scene)?930:Math.min(clip.duration*.55,2000);
178
+ else time=clip.loop?time%clip.duration:Math.min(time,clip.duration);
179
+ const p=performance(scene,time);
180
+ const mode=p.severity<=0?null:{
181
+ intensity:clamp(p.severity),gx:p.gaze[0],gy:p.gaze[1],
182
+ open:p.openness??1,
183
+ ...(p.leftSeverity===undefined?{}:{leftIntensity:p.leftSeverity}),
184
+ ...(p.rightSeverity===undefined?{}:{rightIntensity:p.rightSeverity}),
185
+ };
186
+ const base=renderSentinelPrototype(time,{
187
+ cols,rows,mono,still,theme,eyes,
188
+ pose:p.pose,poseAmount:posture(scene,time,p.pose),
189
+ mouth:p.mouth,stern:p.stern,
190
+ eyeOverride:p.softEyes??null,warden:mode
191
+ });
192
+ if(!base.ok)return base;
193
+ let frame=base.frame;
194
+ let effectPhase=null;
195
+ if(scene==='scorch'){
196
+ // Mapping aligns existing proven blue-shield/breath overlays with
197
+ // character beats: NOTICE -> JUDGE -> BRACE -> IMPACT -> SCORCH ->
198
+ // RECHECK. No action, exploit or mitigation command is executed.
199
+ const effectTime=time<350?time*.85:
200
+ time<750?300+(time-350)*.7:
201
+ time<1350?580+(time-750)*.7:
202
+ time<1850?1000+(time-1350)*.9:
203
+ time<2750?1500+(time-1850)*1.38:
204
+ time<3250?2750+(time-2750):
205
+ 3250+(time-3250);
206
+ const mouth=base.anchors?.mouth??null;
207
+ frame=renderPower('power-sentinel',effectTime,frame,
208
+ {mono,theme,anchors:mouth?{mouth}:null,still:false});
209
+ effectPhase=time<350?'notice':time<750?'judge':
210
+ time<1350?'brace':time<1850?'impact':
211
+ time<2750?'scorch':time<3250?'recheck':'resolve';
212
+ }
213
+ const err=checkFrame(frame,cols,rows);
214
+ if(err)return {ok:false,code:err,reason:'Prototype emitted an invalid art rectangle.'};
215
+ const descriptor=demo?'DEMO · '+clip.message:clip.message;
216
+ return {
217
+ ...base,frame,
218
+ scene,clip:clip.title,time,duration:clip.duration,
219
+ pose:p.pose,poseAmount:base.poseAmount,
220
+ eyesShape:mode?'rounded-triangular-V':'Q-original-round',
221
+ expressionIntensity:mode?.intensity??0,
222
+ effectPhase,visualOnly:true,
223
+ demo,
224
+ verified:!demo&&options.evidence?.verified===true,
225
+ scope:!demo?(options.evidence?.scope??null):null,
226
+ accessibleLabel:'Sentinel · '+descriptor,
227
+ caption:descriptor,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Optional host adapter, NOT an instruction to run an operation.
233
+ * The visual state follows independently provided evidence; raw detection
234
+ * never authorizes a victorious smile or a completed fire effect.
235
+ */
236
+ export function sentinelSceneForEvent(event={}){
237
+ const status=event.status??'idle';
238
+ switch(status){
239
+ case 'attached':return {scene:'greeting',label:'Ready to inspect'};
240
+ case 'scanning':return {scene:'scan',label:'Scan running'};
241
+ case 'anomaly':return {scene:'suspicious',label:'Anomaly under investigation'};
242
+ case 'warning':return {scene:'warning',label:'Attention required'};
243
+ case 'threat-confirmed':
244
+ return {scene:event.confirmed===true?'warden':'suspicious',
245
+ label:event.confirmed===true?'Threat confirmed':'Suspicious event'};
246
+ case 'approval-required':return {scene:'approval',label:'Authorisation required'};
247
+ case 'mitigation-running':return {scene:'brace',label:'Protective action in progress'};
248
+ case 'containment-unverified':return {scene:'holding',label:'Verification pending'};
249
+ case 'mitigation-verified':
250
+ if(event.verified===true&&typeof event.scope==='string'&&event.scope.trim())
251
+ return {scene:'all-clear',label:'Verified for '+event.scope.trim()};
252
+ return {scene:'holding',label:'Outcome not verified'};
253
+ case 'failed':case 'unknown':return {scene:'failure',label:'Protection not verified'};
254
+ case 'standby':return {scene:'standby',label:'Standby'};
255
+ default:return {scene:'idle',label:'Sentinel on watch (no security conclusion)'};
256
+ }
257
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Original Sentinel shield-face icon at small terminal art sizes.
3
+ * Source: design/stage2/refinement/sentinel-a-tiny-shield-icon.svg.
4
+ *
5
+ * This is deliberately separate icon geometry, not a crushed full dragon.
6
+ * SVG paths are resampled to 2×4 Braille dots using the actual qart packer.
7
+ * No backgrounds, no terminal control codes, deterministic in colour/mono.
8
+ */
9
+ import {makeField,fieldToCells} from '../../vendor/quox-dragon/qart.js';
10
+ import {flattenPath,sampleShape} from './svg-coverage.js';
11
+
12
+ const SHAPE=Object.freeze({
13
+ leftHorn:'M12 15 Q9 12 10 4 Q15 7 17 13 Z',
14
+ rightHorn:'M31 13 Q36 8 37 4 Q40 12 37 17 Z',
15
+ shield:'M9 14 Q23 7 39 14 L37 28 Q34 40 24 44 Q13 39 10 28 Z',
16
+ smile:'M20 35 Q24 38 28 34',
17
+ });
18
+ const rgba={
19
+ horn:[141,218,255],body:[87,153,238],rim:[20,39,68],
20
+ eye:[255,253,246],cheek:[250,147,183],
21
+ };
22
+ function ellipse(cx,cy,rx,ry){
23
+ const pts=[];
24
+ for(let k=0;k<72;k++){const th=k*Math.PI*2/72;pts.push([cx+rx*Math.cos(th),cy+ry*Math.sin(th)]);}
25
+ return [pts];
26
+ }
27
+ function mask(renderer,source,paint,stroke=0){
28
+ renderer(source,paint,stroke);
29
+ }
30
+ function showBounds(frame){
31
+ let x0=Infinity,x1=-1,y0=Infinity,y1=-1,n=0;
32
+ for(let y=0;y<frame.length;y++)for(let x=0;x<frame[y].length;x++)
33
+ if(frame[y][x].ch!==' '&&frame[y][x].ch!=='\u2800'){
34
+ n++;x0=Math.min(x0,x);x1=Math.max(x1,x);y0=Math.min(y0,y);y1=Math.max(y1,y);
35
+ }
36
+ return n?{x0,x1,y0,y1,cells:n}:null;
37
+ }
38
+ export function renderSentinelIcon({cols=16,rows=8,mono=false,mood='cute'}={}){
39
+ if(!Number.isSafeInteger(cols)||!Number.isSafeInteger(rows)||cols<=0||rows<=0)
40
+ return {ok:false,code:'INVALID_VIEWPORT',reason:'Icon viewport needs positive cell dimensions.'};
41
+ if(cols>200||rows>72)return {ok:false,code:'TOO_LARGE',reason:'Icon viewport exceeds supported limit.'};
42
+ if(cols<12||rows<6)return {ok:false,code:'TEXT_FALLBACK',reason:'Use a shield symbol plus Sentinel label.'};
43
+ if(!['cute','alert','warden'].includes(mood))
44
+ return {ok:false,code:'UNKNOWN_MOOD',reason:'Icon mood must be cute, alert or warden.'};
45
+ const W=cols*2,H=rows*4;
46
+ const scale=Math.min((W-4)/48,(H-4)/48,1.5);
47
+ const ox=(W-48*scale)/2,oy=(H-48*scale)/2;
48
+ const field=makeField(W,H),layers=new Float32Array(W*H),rgb=new Uint8Array(W*H*3);
49
+ const stamp=(polys,color,z,erase=false,stroke=0)=>{
50
+ sampleShape(polys,{W,H,scale,ox,oy,stroke},(x,y)=>{
51
+ const p=y*W+x;if(z<layers[p])return;
52
+ layers[p]=z;field.v[p]=erase?0:1;
53
+ if(!erase)rgb.set(color,p*3);
54
+ });
55
+ };
56
+ const P=(p)=>flattenPath(p);
57
+ stamp(P(SHAPE.leftHorn),rgba.horn,1);
58
+ stamp(P(SHAPE.rightHorn),rgba.horn,1);
59
+ stamp(P(SHAPE.shield),rgba.body,2);
60
+ const angry=mood==='warden',watch=mood!=='cute';
61
+ for(const [cx,cy,rx,ry,px,py] of [
62
+ [18,23,5.7,6.8,20,23],[31,23,5.2,6.2,33,23],
63
+ ]){
64
+ // Physical dark margin must cut out the solid body first. Whole Braille
65
+ // cells containing white eyes are given eye-only colour ownership below.
66
+ stamp(ellipse(cx,cy,rx+1.0,ry+1.0),rgba.rim,3,true);
67
+ stamp(ellipse(cx,cy,rx,ry),rgba.eye,4);
68
+ stamp(ellipse(px,py+(watch?.5:0),angry?2.2:2.4,angry?3.0:3.3),rgba.rim,5,true);
69
+ if(angry){
70
+ // The tiny expression follows the same inward-V white cut,
71
+ // only after the stronger eye white has been drawn.
72
+ const side=cx<25?-1:1;
73
+ const triangle=[];
74
+ for(let x=0;x<8;x++){
75
+ const outside=cx+side*(-rx+x*.92);
76
+ triangle.push([outside,cy-ry-.2+(7-x)*.42]);
77
+ }
78
+ stamp([triangle],rgba.rim,6,true);
79
+ }
80
+ }
81
+ if(mood==='cute')stamp(P(SHAPE.smile),rgba.rim,6,true,1.5);
82
+ else stamp(P('M20 35 L28 35'),rgba.rim,6,true,1.4);
83
+
84
+ const frame=fieldToCells(field,{color:false,quant:false,tint:rgba.body});
85
+ // Recolour complete Braille cells based on the majority surviving pixels.
86
+ // Unlike direct stacking, there can be no blue body ink in an eye-white
87
+ // terminal cell. Mono reproduces the exact same dot mask.
88
+ for(let cy=0;cy<rows;cy++)for(let cx=0;cx<cols;cx++){
89
+ const cell=frame[cy][cx];cell.bg=null;
90
+ if(mono||cell.ch===' '){cell.rgb=null;continue;}
91
+ const counts=new Map();
92
+ for(let dy=0;dy<4;dy++)for(let dx=0;dx<2;dx++){
93
+ const p=(cy*4+dy)*W+cx*2+dx;
94
+ if(field.v[p]<=0)continue;
95
+ const c=[rgb[p*3],rgb[p*3+1],rgb[p*3+2]].join(',');
96
+ counts.set(c,(counts.get(c)||0)+1);
97
+ }
98
+ const color=[...counts].sort((a,b)=>b[1]-a[1])[0]?.[0];
99
+ cell.rgb=color?color.split(',').map(Number):rgba.body;
100
+ }
101
+ return {ok:true,frame,cols,rows,icon:true,mood,source:'stage2/refinement/sentinel-a-tiny-shield-icon.svg',
102
+ bounds:showBounds(frame),visualOnly:true};
103
+ }