quoxcli 0.3.1 → 0.4.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 (188) hide show
  1. package/lib/api/endpoints/actions.js +15 -0
  2. package/lib/api/endpoints/admin.js +20 -0
  3. package/lib/api/endpoints/blog.js +16 -0
  4. package/lib/api/endpoints/chat.js +7 -0
  5. package/lib/api/endpoints/contactsq.js +4 -0
  6. package/lib/api/endpoints/devServerRegistry.js +25 -0
  7. package/lib/api/endpoints/engagement.js +26 -0
  8. package/lib/api/endpoints/evidence.js +20 -0
  9. package/lib/api/endpoints/hat.js +14 -0
  10. package/lib/api/endpoints/index.js +12 -0
  11. package/lib/api/endpoints/loops.js +26 -0
  12. package/lib/api/endpoints/mon.js +173 -0
  13. package/lib/api/endpoints/objectives.js +6 -0
  14. package/lib/api/endpoints/quoxseo.js +6 -0
  15. package/lib/api/endpoints/server.js +21 -0
  16. package/lib/api/endpoints/setup.js +6 -0
  17. package/lib/api/endpoints/v1.js +34 -0
  18. package/lib/commands/actions.js +81 -11
  19. package/lib/commands/agent.js +11 -4
  20. package/lib/commands/agentAttach.js +46 -7
  21. package/lib/commands/approvals.js +5 -0
  22. package/lib/commands/ask.js +3 -1
  23. package/lib/commands/blog.js +110 -0
  24. package/lib/commands/chat.js +127 -28
  25. package/lib/commands/chatCompanion.js +27 -0
  26. package/lib/commands/chatState.js +45 -0
  27. package/lib/commands/claude.js +3 -32
  28. package/lib/commands/contacts.js +85 -4
  29. package/lib/commands/contactsSendAt.js +21 -0
  30. package/lib/commands/engagement.js +90 -0
  31. package/lib/commands/evidence.js +154 -0
  32. package/lib/commands/ext.js +23 -0
  33. package/lib/commands/fleet.js +25 -0
  34. package/lib/commands/hat.js +76 -0
  35. package/lib/commands/image.js +68 -0
  36. package/lib/commands/kuma.js +1050 -0
  37. package/lib/commands/kumaSla.js +174 -0
  38. package/lib/commands/licence.js +39 -1
  39. package/lib/commands/loop.js +291 -0
  40. package/lib/commands/objective.js +48 -1
  41. package/lib/commands/org.js +21 -4
  42. package/lib/commands/provider.js +58 -0
  43. package/lib/commands/quoxseo.js +31 -0
  44. package/lib/commands/research.js +79 -0
  45. package/lib/commands/rows.js +9 -0
  46. package/lib/commands/run.js +18 -1
  47. package/lib/commands/scope.js +2 -2
  48. package/lib/commands/server.js +113 -0
  49. package/lib/commands/shortcut.js +41 -0
  50. package/lib/commands/skills.js +20 -4
  51. package/lib/commands/vm.js +2 -2
  52. package/lib/commands/youtube.js +172 -0
  53. package/lib/core/actionItems.js +115 -0
  54. package/lib/core/agentAlias.js +2 -0
  55. package/lib/core/agentFamilies.js +7 -0
  56. package/lib/core/client.js +10 -2
  57. package/lib/core/commandRegistrars.js +9 -0
  58. package/lib/core/config.js +1 -1
  59. package/lib/core/findingReconcile.js +90 -0
  60. package/lib/core/global-flags.js +1 -1
  61. package/lib/core/governedSession.js +1 -3
  62. package/lib/core/liveSessionsReport.js +1 -1
  63. package/lib/core/loopGlyph.js +97 -0
  64. package/lib/core/offload.js +36 -7
  65. package/lib/core/output.js +7 -7
  66. package/lib/core/sessionHeartbeat.js +12 -8
  67. package/lib/core/shortcut.js +83 -0
  68. package/lib/core/version.js +1 -1
  69. package/lib/core/vm.js +5 -0
  70. package/lib/dragon/companionView.js +34 -0
  71. package/lib/dragon/frames.js +118 -0
  72. package/lib/dragon/jobs.js +123 -0
  73. package/lib/dragon/renderWorker.js +15 -0
  74. package/lib/dragon/select.js +72 -0
  75. package/lib/evidence/bundleExport.js +47 -0
  76. package/lib/evidence/bundleVerify.js +77 -0
  77. package/lib/quoxon/EXTENSIONS.md +23 -0
  78. package/lib/quoxon/api.js +192 -6
  79. package/lib/quoxon/app.js +655 -89
  80. package/lib/quoxon/approvalFacts.js +8 -6
  81. package/lib/quoxon/approvalSentence.js +44 -0
  82. package/lib/quoxon/approvalsView.js +63 -4
  83. package/lib/quoxon/commandShort.js +15 -3
  84. package/lib/quoxon/commands.js +75 -10
  85. package/lib/quoxon/countdownLine.js +10 -1
  86. package/lib/quoxon/deck.js +30 -0
  87. package/lib/quoxon/demo.autoplay.js +1 -1
  88. package/lib/quoxon/demo.js +3 -1
  89. package/lib/quoxon/dividerAocl.js +4 -3
  90. package/lib/quoxon/dividerRule.js +3 -3
  91. package/lib/quoxon/dragonCompanion.js +255 -0
  92. package/lib/quoxon/dragonModes.js +1 -0
  93. package/lib/quoxon/dragonMoments.js +82 -0
  94. package/lib/quoxon/dragonReads.js +68 -0
  95. package/lib/quoxon/dragonView.js +2 -2
  96. package/lib/quoxon/events.js +24 -3
  97. package/lib/quoxon/extensions.js +276 -0
  98. package/lib/quoxon/inboxView.js +79 -0
  99. package/lib/quoxon/installCli.js +79 -0
  100. package/lib/quoxon/keys.js +2 -2
  101. package/lib/quoxon/loopCard.js +456 -0
  102. package/lib/quoxon/loopCards.js +180 -0
  103. package/lib/quoxon/loopStream.js +87 -0
  104. package/lib/quoxon/main.js +12 -3
  105. package/lib/quoxon/mouse.js +12 -1
  106. package/lib/quoxon/objectiveOffer.js +82 -0
  107. package/lib/quoxon/objects/index.js +22 -4
  108. package/lib/quoxon/objects/model.js +33 -0
  109. package/lib/quoxon/objects/proof.js +12 -4
  110. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  111. package/lib/quoxon/onboarding/view.js +1 -1
  112. package/lib/quoxon/orgSwitch.js +27 -1
  113. package/lib/quoxon/planSteps.js +51 -0
  114. package/lib/quoxon/print.js +6 -3
  115. package/lib/quoxon/qdragonFly.js +7 -6
  116. package/lib/quoxon/qmotion.js +17 -0
  117. package/lib/quoxon/render.js +130 -33
  118. package/lib/quoxon/roster.js +26 -12
  119. package/lib/quoxon/rows.js +244 -1
  120. package/lib/quoxon/serverChip.js +42 -0
  121. package/lib/quoxon/session.js +10 -0
  122. package/lib/quoxon/slash/cliCommand.js +42 -1
  123. package/lib/quoxon/slash/cliLeaves.json +76 -1
  124. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  125. package/lib/quoxon/slash/palette.js +1 -1
  126. package/lib/quoxon/slash/qpeCmd.js +35 -0
  127. package/lib/quoxon/slash/restart.js +25 -1
  128. package/lib/quoxon/slash/resume.js +18 -1
  129. package/lib/quoxon/slash/todoCmd.js +122 -0
  130. package/lib/quoxon/suggestCommand.js +33 -2
  131. package/lib/quoxon/toolCards.js +30 -0
  132. package/lib/quoxon/whatsnew.jsonl +45 -0
  133. package/lib/quoxon/windowOrg.js +11 -1
  134. package/lib/server/devServers.js +137 -0
  135. package/package.json +2 -1
  136. package/vendor/dragonlab/SOURCE +4 -0
  137. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  138. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  139. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  140. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  141. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  142. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  143. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  144. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  145. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  146. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  147. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  148. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  149. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  150. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  151. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  152. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  153. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  154. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  155. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  156. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  157. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  158. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  159. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  160. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  161. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  162. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  163. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  164. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  165. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  166. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  167. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  168. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  169. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  170. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  171. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  172. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  173. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  174. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  175. package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
  176. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  177. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  178. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  179. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  180. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  181. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  182. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  183. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  184. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  185. package/vendor/dragonlab/lib/power-effects.js +294 -0
  186. package/vendor/dragonlab/lib/power-registry.js +74 -0
  187. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  188. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
@@ -0,0 +1,56 @@
1
+ import {performance} from 'node:perf_hooks';
2
+ import {SEER_CLIPS,renderSeer,renderStory,STORY_DURATION} from './character.mjs';
3
+ import {composeMessage} from '../../lucid/recovery/layout.mjs';
4
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
5
+
6
+ /** One bounded performance. No alternate screen or full-screen clearing. */
7
+ export async function watch(paths,options,config){
8
+ if(!process.stdout.isTTY||!process.stdin.isTTY)throw new Error('--watch/--intro needs an interactive terminal. Use --scene and --at for snapshots.');
9
+ const {terminalCols,terminalRows,scene,large,message}=config;
10
+ const clip=scene?SEER_CLIPS.find(c=>c.id===scene):null;
11
+ const duration=(config.arrival&&options.deck?4400:clip?.duration??STORY_DURATION)+(config.arrival?6000:0);
12
+ const realCols=process.stdout.columns,realRows=process.stdout.rows;
13
+ const start=performance.now();
14
+ const render=t=>{
15
+ const r=scene?renderSeer(paths,scene,t,options):renderStory(paths,t,options);
16
+ if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
17
+ const caption=message??'SEER · '+r.clip+'\n'+r.caption;
18
+ return large?[...cellsToAnsi(r.frame),...caption.split('\n')]:composeMessage(r.frame,caption,{terminalCols});
19
+ };
20
+ // Reserve enough lines for captions too; keep within the real terminal.
21
+ const height=options.rows+3;
22
+ if(height>=terminalRows)throw new Error('Animation needs room for its caption; reduce --rows.');
23
+ let ended=false,drawn=false,timer,resolveDone,rejectDone;
24
+ const wasRaw=Boolean(process.stdin.isRaw);
25
+ const done=new Promise((resolve,reject)=>{resolveDone=resolve;rejectDone=reject;});
26
+ const cleanup=(error)=>{
27
+ if(ended)return;ended=true;clearInterval(timer);
28
+ process.stdin.off('data',key);process.stdout.off('resize',resized);
29
+ process.off('SIGINT',sigint);process.off('SIGTERM',sigterm);
30
+ process.stdin.setRawMode(wasRaw);process.stdin.pause();
31
+ if(drawn)process.stdout.write(`\x1b[${height}B\r`);
32
+ process.stdout.write('\x1b[0m\x1b[?25h');
33
+ error?rejectDone(error):resolveDone();
34
+ };
35
+ const key=data=>{if(data.includes(3)||data.toString().includes('q'))cleanup();};
36
+ const sigint=()=>cleanup(),sigterm=()=>cleanup();
37
+ const resized=()=>cleanup(new Error('Terminal resized; rerun for the new art slot.'));
38
+ try{
39
+ process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',key);
40
+ process.stdout.on('resize',resized);process.on('SIGINT',sigint);process.on('SIGTERM',sigterm);
41
+ process.stdout.write('\x1b[?25l');
42
+ // Allocate the block once; each frame replaces only these lines.
43
+ process.stdout.write('\n'.repeat(height)+`\x1b[${height}A`);drawn=true;
44
+ const tick=()=>{
45
+ try{
46
+ if(process.stdout.columns!==realCols||process.stdout.rows!==realRows)return resized();
47
+ const t=Math.min(performance.now()-start,duration-1),lines=render(t);
48
+ if(lines.length>height)throw new Error('Message exceeds the animation block; use a shorter message or a static preview.');
49
+ process.stdout.write(Array.from({length:height},(_,i)=>'\r\x1b[2K'+(lines[i]??'')+'\n').join('')+`\x1b[${height}A`);
50
+ if(performance.now()-start>=duration)cleanup();
51
+ }catch(e){cleanup(e);}
52
+ };
53
+ tick();if(!ended)timer=setInterval(tick,1000/12);
54
+ await done;
55
+ }catch(e){cleanup(e);await done;}
56
+ }
@@ -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
+ }