cbrowser 18.78.0 → 18.80.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 (235) hide show
  1. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  2. package/dist/analysis/accessibility-empathy.js +395 -66
  3. package/dist/analysis/accessibility-empathy.js.map +1 -1
  4. package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
  5. package/dist/analysis/agent-ready-audit.js +78 -0
  6. package/dist/analysis/agent-ready-audit.js.map +1 -1
  7. package/dist/analysis/page-understanding.d.ts +139 -1
  8. package/dist/analysis/page-understanding.d.ts.map +1 -1
  9. package/dist/analysis/page-understanding.js +179 -12
  10. package/dist/analysis/page-understanding.js.map +1 -1
  11. package/dist/artifact-store.d.ts +19 -5
  12. package/dist/artifact-store.d.ts.map +1 -1
  13. package/dist/artifact-store.js +41 -11
  14. package/dist/artifact-store.js.map +1 -1
  15. package/dist/browser.d.ts +9 -0
  16. package/dist/browser.d.ts.map +1 -1
  17. package/dist/browser.js +81 -5
  18. package/dist/browser.js.map +1 -1
  19. package/dist/cli.js +55 -10
  20. package/dist/cli.js.map +1 -1
  21. package/dist/cognitive/index.d.ts.map +1 -1
  22. package/dist/cognitive/index.js +3 -2
  23. package/dist/cognitive/index.js.map +1 -1
  24. package/dist/config.d.ts +57 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +234 -7
  27. package/dist/config.js.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/mcp-server-remote.d.ts.map +1 -1
  31. package/dist/mcp-server-remote.js +108 -4
  32. package/dist/mcp-server-remote.js.map +1 -1
  33. package/dist/mcp-server.d.ts.map +1 -1
  34. package/dist/mcp-server.js +62 -938
  35. package/dist/mcp-server.js.map +1 -1
  36. package/dist/mcp-tools/account-personas.d.ts +41 -0
  37. package/dist/mcp-tools/account-personas.d.ts.map +1 -0
  38. package/dist/mcp-tools/account-personas.js +98 -0
  39. package/dist/mcp-tools/account-personas.js.map +1 -0
  40. package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
  41. package/dist/mcp-tools/base/analysis-tools.js +12 -2
  42. package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
  43. package/dist/mcp-tools/base/artifact-tools.d.ts +3 -0
  44. package/dist/mcp-tools/base/artifact-tools.d.ts.map +1 -0
  45. package/dist/mcp-tools/base/artifact-tools.js +78 -0
  46. package/dist/mcp-tools/base/artifact-tools.js.map +1 -0
  47. package/dist/mcp-tools/base/audit-tools.d.ts +11 -0
  48. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  49. package/dist/mcp-tools/base/audit-tools.js +595 -340
  50. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  51. package/dist/mcp-tools/base/browser-management-tools.d.ts.map +1 -1
  52. package/dist/mcp-tools/base/browser-management-tools.js +18 -0
  53. package/dist/mcp-tools/base/browser-management-tools.js.map +1 -1
  54. package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
  55. package/dist/mcp-tools/base/bug-analysis-tools.js +59 -3
  56. package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
  57. package/dist/mcp-tools/base/capture-tools.d.ts +37 -0
  58. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  59. package/dist/mcp-tools/base/capture-tools.js +267 -6
  60. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  61. package/dist/mcp-tools/base/cognitive-tools.d.ts +12 -0
  62. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  63. package/dist/mcp-tools/base/cognitive-tools.js +63 -15
  64. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  65. package/dist/mcp-tools/base/extraction-tools.d.ts.map +1 -1
  66. package/dist/mcp-tools/base/extraction-tools.js +33 -2
  67. package/dist/mcp-tools/base/extraction-tools.js.map +1 -1
  68. package/dist/mcp-tools/base/index.d.ts +2 -0
  69. package/dist/mcp-tools/base/index.d.ts.map +1 -1
  70. package/dist/mcp-tools/base/index.js +9 -0
  71. package/dist/mcp-tools/base/index.js.map +1 -1
  72. package/dist/mcp-tools/base/marketing-tools.js +3 -3
  73. package/dist/mcp-tools/base/marketing-tools.js.map +1 -1
  74. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  75. package/dist/mcp-tools/base/persona-comparison-tools.js +529 -40
  76. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  77. package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
  78. package/dist/mcp-tools/base/site-knowledge-tools.js +36 -3
  79. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  80. package/dist/mcp-tools/base/values-tools.d.ts +113 -0
  81. package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
  82. package/dist/mcp-tools/base/values-tools.js +868 -78
  83. package/dist/mcp-tools/base/values-tools.js.map +1 -1
  84. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  85. package/dist/mcp-tools/base/visual-testing-tools.js +130 -10
  86. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  87. package/dist/mcp-tools/index.d.ts +3 -1
  88. package/dist/mcp-tools/index.d.ts.map +1 -1
  89. package/dist/mcp-tools/index.js +3 -28
  90. package/dist/mcp-tools/index.js.map +1 -1
  91. package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
  92. package/dist/mcp-tools/persona-creation-tools.js +363 -20
  93. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  94. package/dist/mcp-tools/persona-lifecycle-tools.d.ts +3 -0
  95. package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -0
  96. package/dist/mcp-tools/persona-lifecycle-tools.js +406 -0
  97. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -0
  98. package/dist/mcp-tools/screenshot-utils.d.ts.map +1 -1
  99. package/dist/mcp-tools/screenshot-utils.js +56 -10
  100. package/dist/mcp-tools/screenshot-utils.js.map +1 -1
  101. package/dist/mcp-tools/tier-gate.js +2 -2
  102. package/dist/mcp-tools/tier-gate.js.map +1 -1
  103. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  104. package/dist/mcp-tools/tool-categories.js +4 -1
  105. package/dist/mcp-tools/tool-categories.js.map +1 -1
  106. package/dist/mcp-tools/ui-resources.d.ts +167 -0
  107. package/dist/mcp-tools/ui-resources.d.ts.map +1 -0
  108. package/dist/mcp-tools/ui-resources.js +707 -0
  109. package/dist/mcp-tools/ui-resources.js.map +1 -0
  110. package/dist/mcp-tools/widget-kit.d.ts +299 -0
  111. package/dist/mcp-tools/widget-kit.d.ts.map +1 -0
  112. package/dist/mcp-tools/widget-kit.js +2034 -0
  113. package/dist/mcp-tools/widget-kit.js.map +1 -0
  114. package/dist/mcp-ui-resources.d.ts.map +1 -1
  115. package/dist/mcp-ui-resources.js +14 -4
  116. package/dist/mcp-ui-resources.js.map +1 -1
  117. package/dist/persona-questionnaire.d.ts +69 -1
  118. package/dist/persona-questionnaire.d.ts.map +1 -1
  119. package/dist/persona-questionnaire.js +177 -76
  120. package/dist/persona-questionnaire.js.map +1 -1
  121. package/dist/persona-scope.d.ts +20 -0
  122. package/dist/persona-scope.d.ts.map +1 -0
  123. package/dist/persona-scope.js +59 -0
  124. package/dist/persona-scope.js.map +1 -0
  125. package/dist/personas.d.ts +51 -17
  126. package/dist/personas.d.ts.map +1 -1
  127. package/dist/personas.js +947 -286
  128. package/dist/personas.js.map +1 -1
  129. package/dist/recording/capture-player.d.ts +68 -0
  130. package/dist/recording/capture-player.d.ts.map +1 -0
  131. package/dist/recording/capture-player.js +298 -0
  132. package/dist/recording/capture-player.js.map +1 -0
  133. package/dist/recording/engine.d.ts +52 -0
  134. package/dist/recording/engine.d.ts.map +1 -1
  135. package/dist/recording/engine.js +241 -3
  136. package/dist/recording/engine.js.map +1 -1
  137. package/dist/recording/inline-player.d.ts +39 -0
  138. package/dist/recording/inline-player.d.ts.map +1 -0
  139. package/dist/recording/inline-player.js +272 -0
  140. package/dist/recording/inline-player.js.map +1 -0
  141. package/dist/recording/saliency-overlay.d.ts +133 -0
  142. package/dist/recording/saliency-overlay.d.ts.map +1 -0
  143. package/dist/recording/saliency-overlay.js +360 -0
  144. package/dist/recording/saliency-overlay.js.map +1 -0
  145. package/dist/recording/types.d.ts +32 -0
  146. package/dist/recording/types.d.ts.map +1 -1
  147. package/dist/recording/types.js +46 -0
  148. package/dist/recording/types.js.map +1 -1
  149. package/dist/site-model/manager.d.ts +20 -0
  150. package/dist/site-model/manager.d.ts.map +1 -1
  151. package/dist/site-model/manager.js +52 -0
  152. package/dist/site-model/manager.js.map +1 -1
  153. package/dist/trait-reference.d.ts +26 -2
  154. package/dist/trait-reference.d.ts.map +1 -1
  155. package/dist/trait-reference.js +129 -19
  156. package/dist/trait-reference.js.map +1 -1
  157. package/dist/types.d.ts +105 -18
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js.map +1 -1
  160. package/dist/url-resolution.d.ts +58 -0
  161. package/dist/url-resolution.d.ts.map +1 -0
  162. package/dist/url-resolution.js +143 -0
  163. package/dist/url-resolution.js.map +1 -0
  164. package/dist/values/big-five-inference.d.ts +121 -0
  165. package/dist/values/big-five-inference.d.ts.map +1 -0
  166. package/dist/values/big-five-inference.js +241 -0
  167. package/dist/values/big-five-inference.js.map +1 -0
  168. package/dist/values/big-five-values.d.ts +76 -0
  169. package/dist/values/big-five-values.d.ts.map +1 -0
  170. package/dist/values/big-five-values.js +152 -0
  171. package/dist/values/big-five-values.js.map +1 -0
  172. package/dist/values/index.d.ts +1 -1
  173. package/dist/values/index.d.ts.map +1 -1
  174. package/dist/values/index.js +1 -1
  175. package/dist/values/index.js.map +1 -1
  176. package/dist/values/persona-completeness.d.ts +91 -0
  177. package/dist/values/persona-completeness.d.ts.map +1 -0
  178. package/dist/values/persona-completeness.js +114 -0
  179. package/dist/values/persona-completeness.js.map +1 -0
  180. package/dist/values/persona-diff.d.ts +50 -0
  181. package/dist/values/persona-diff.d.ts.map +1 -0
  182. package/dist/values/persona-diff.js +126 -0
  183. package/dist/values/persona-diff.js.map +1 -0
  184. package/dist/values/persona-values.d.ts +16 -0
  185. package/dist/values/persona-values.d.ts.map +1 -1
  186. package/dist/values/persona-values.js +33 -0
  187. package/dist/values/persona-values.js.map +1 -1
  188. package/dist/values/schwartz-values.d.ts +9 -1
  189. package/dist/values/schwartz-values.d.ts.map +1 -1
  190. package/dist/values/schwartz-values.js +10 -2
  191. package/dist/values/schwartz-values.js.map +1 -1
  192. package/dist/values/value-mappings.d.ts +41 -2
  193. package/dist/values/value-mappings.d.ts.map +1 -1
  194. package/dist/values/value-mappings.js +65 -8
  195. package/dist/values/value-mappings.js.map +1 -1
  196. package/dist/visual/attention-quality.d.ts +27 -0
  197. package/dist/visual/attention-quality.d.ts.map +1 -1
  198. package/dist/visual/attention-quality.js +164 -2
  199. package/dist/visual/attention-quality.js.map +1 -1
  200. package/dist/visual/attention-transport.d.ts +41 -2
  201. package/dist/visual/attention-transport.d.ts.map +1 -1
  202. package/dist/visual/attention-transport.js +98 -26
  203. package/dist/visual/attention-transport.js.map +1 -1
  204. package/dist/visual/barrier-overlay-html.d.ts +10 -0
  205. package/dist/visual/barrier-overlay-html.d.ts.map +1 -1
  206. package/dist/visual/barrier-overlay-html.js.map +1 -1
  207. package/dist/visual/cognitive-models.d.ts +109 -13
  208. package/dist/visual/cognitive-models.d.ts.map +1 -1
  209. package/dist/visual/cognitive-models.js +166 -3
  210. package/dist/visual/cognitive-models.js.map +1 -1
  211. package/dist/visual/cognitive-transport-chain.d.ts +149 -5
  212. package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
  213. package/dist/visual/cognitive-transport-chain.js +558 -30
  214. package/dist/visual/cognitive-transport-chain.js.map +1 -1
  215. package/dist/visual/cognitive-transport.d.ts +45 -3
  216. package/dist/visual/cognitive-transport.d.ts.map +1 -1
  217. package/dist/visual/cognitive-transport.js +126 -12
  218. package/dist/visual/cognitive-transport.js.map +1 -1
  219. package/dist/visual/fixation-corpus.d.ts +52 -0
  220. package/dist/visual/fixation-corpus.d.ts.map +1 -1
  221. package/dist/visual/fixation-corpus.js +95 -0
  222. package/dist/visual/fixation-corpus.js.map +1 -1
  223. package/dist/visual/llm-relevance.d.ts +193 -0
  224. package/dist/visual/llm-relevance.d.ts.map +1 -0
  225. package/dist/visual/llm-relevance.js +480 -0
  226. package/dist/visual/llm-relevance.js.map +1 -0
  227. package/dist/visual/perceptual-transport.d.ts +60 -0
  228. package/dist/visual/perceptual-transport.d.ts.map +1 -1
  229. package/dist/visual/perceptual-transport.js +228 -19
  230. package/dist/visual/perceptual-transport.js.map +1 -1
  231. package/dist/visual/persona-context.d.ts +23 -0
  232. package/dist/visual/persona-context.d.ts.map +1 -0
  233. package/dist/visual/persona-context.js +44 -0
  234. package/dist/visual/persona-context.js.map +1 -0
  235. package/package.json +2 -1
@@ -14,7 +14,12 @@
14
14
  * @license MIT
15
15
  */
16
16
  import { z } from "zod";
17
- import { generatePersonaQuestionnaire, buildTraitsFromAnswers, deriveValuesFromTraits, TRAIT_REFERENCE_MATRIX, } from "../persona-questionnaire.js";
17
+ import { generatePersonaQuestionnaire, buildTraitsFromAnswers, buildBigFiveFromAnswers, deriveValuesFromTraits, TRAIT_REFERENCE_MATRIX, } from "../persona-questionnaire.js";
18
+ import { bigFiveInferenceReference, validateInferredBigFive } from "../values/big-five-inference.js";
19
+ import { deriveValuesFromBigFive } from "../values/big-five-values.js";
20
+ import { TRAIT_INFERENCE_CRITERIA, ALL_TRAITS, assessTraitCompleteness } from "../values/persona-completeness.js";
21
+ import { summariseTraits } from "../values/persona-diff.js";
22
+ import { widgetUri } from "./widget-kit.js";
18
23
  const VALUES_QUESTIONS = [
19
24
  {
20
25
  id: "security",
@@ -168,8 +173,7 @@ function getTraitHeader(trait) {
168
173
  emotionalContagion: "Emotional",
169
174
  fearOfMissingOut: "FOMO",
170
175
  socialProofSensitivity: "Social Proof",
171
- mentalModelRigidity: "Flexibility",
172
- siteFamiliarity: "Site Memory",
176
+ mentalModelFlexibility: "Flexibility",
173
177
  };
174
178
  return headers[trait] || trait;
175
179
  }
@@ -203,7 +207,7 @@ export function registerPersonaCreationTools(server) {
203
207
  description: "Start creating a custom persona. Best practice: present the choice to the user via AskUserQuestion (questionnaire vs description mode) rather than showing raw JSON.",
204
208
  inputSchema: {
205
209
  persona_name: z.string().describe("Name for the new persona (e.g., 'tech-savvy-millennial')"),
206
- comprehensive: z.boolean().optional().describe("Include all 25 traits (true) or just core 8 traits (false, default)"),
210
+ comprehensive: z.boolean().optional().describe("Include all 26 traits (true) or just core 8 traits (false, default)"),
207
211
  },
208
212
  annotations: {
209
213
  title: "Start Persona Creation",
@@ -259,7 +263,7 @@ IMPORTANT: Do NOT show this text to the user. USE AskUserQuestion to present the
259
263
  description: "Start the questionnaire mode for persona creation. Each question is best presented via AskUserQuestion for interactive selection.",
260
264
  inputSchema: {
261
265
  persona_name: z.string().describe("Name for the new persona"),
262
- comprehensive: z.boolean().optional().describe("Include all 25 traits (default: false, core 8 traits only)"),
266
+ comprehensive: z.boolean().optional().describe("Include all 26 traits (default: false, core 8 traits only)"),
263
267
  },
264
268
  annotations: {
265
269
  title: "Start Persona Questionnaire",
@@ -296,7 +300,7 @@ IMPORTANT: Do NOT show this text to the user. USE AskUserQuestion to present the
296
300
  Question 1 of ${totalQuestions} (Trait: ${firstQuestion.trait})
297
301
  "${thirdPersonQuestion}"
298
302
 
299
- Header: "${getTraitHeader(firstQuestion.trait)}"
303
+ Header: "${firstQuestion.trait ? getTraitHeader(firstQuestion.trait) : firstQuestion.id}"
300
304
 
301
305
  Options:
302
306
  ${firstQuestion.options.map((o, i) => `${i + 1}. "${o.label}" - ${o.description}`).join("\n")}
@@ -338,8 +342,10 @@ IMPORTANT: Use AskUserQuestion - do NOT just display this text.`,
338
342
  const now = Date.now();
339
343
  if (session.phase === "traits") {
340
344
  const currentQuestion = session.questions[session.currentIndex];
341
- session.answers[currentQuestion.trait] = answer_value;
342
- session.answerMetadata[currentQuestion.trait] = {
345
+ // Keyed by id so Big Five items, which carry no trait, land somewhere
346
+ // real. For a trait question id and trait are the same string.
347
+ session.answers[currentQuestion.id] = answer_value;
348
+ session.answerMetadata[currentQuestion.id] = {
343
349
  value: answer_value,
344
350
  answeredAt: now,
345
351
  questionIndex: session.currentIndex,
@@ -366,6 +372,13 @@ IMPORTANT: Use AskUserQuestion - do NOT just display this text.`,
366
372
  // Check if completely done
367
373
  if (session.phase === "values" && session.currentIndex >= session.valueQuestions.length) {
368
374
  const traits = buildTraitsFromAnswers(session.answers);
375
+ // Big Five answers, if the questionnaire included them. Kept on the
376
+ // persona so anything deriving values later has the layer the research
377
+ // actually relates to values, rather than re-deriving from cognitive
378
+ // traits and crossing a gap the literature does not cover. Without
379
+ // this the five items would have been asked and thrown away.
380
+ // (2026-08-01)
381
+ const bigFive = buildBigFiveFromAnswers(session.answers);
369
382
  const derivedResult = deriveValuesFromTraits(traits);
370
383
  const derivedValues = derivedResult.values;
371
384
  const values = {
@@ -389,6 +402,7 @@ IMPORTANT: Use AskUserQuestion - do NOT just display this text.`,
389
402
  text: JSON.stringify({
390
403
  questionnaire_complete: true,
391
404
  persona_name: personaName,
405
+ ...(Object.keys(bigFive).length ? { bigFive } : {}),
392
406
  duration_seconds: Math.round(duration / 1000),
393
407
  traits,
394
408
  values,
@@ -413,7 +427,7 @@ IMPORTANT: Use AskUserQuestion - do NOT just display this text.`,
413
427
  Question ${answeredCount + 1} of ${totalQuestions} (Trait: ${nextQuestion.trait})
414
428
  "${thirdPersonQuestion}"
415
429
 
416
- Header: "${getTraitHeader(nextQuestion.trait)}"
430
+ Header: "${nextQuestion.trait ? getTraitHeader(nextQuestion.trait) : nextQuestion.id}"
417
431
 
418
432
  Options:
419
433
  ${nextQuestion.options.map((o, i) => `${i + 1}. "${o.label}" - ${o.description}`).join("\n")}
@@ -486,8 +500,19 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
486
500
  mode: "manual_inference",
487
501
  persona_name,
488
502
  user_description: description,
489
- instruction: `Based on the user's description, infer appropriate trait values (0.0 to 1.0) for each cognitive trait. Use the trait reference matrix below to guide your inference. If the description mentions ANY physical or sensory disability (vision loss, motor impairment, color blindness, hearing loss, tremor, etc.), you MUST also include accessibilityTraits. Without accessibilityTraits, the persona gets cognitive-only modeling and the perceptual transport layer will report near-perfect perception — which is wrong for someone with a disability. Return the traits via persona_create_submit_traits.`,
503
+ instruction: `Based on the user's description, infer appropriate trait values (0.0 to 1.0) for each cognitive trait. ALSO estimate the persona's Big Five profile from the same description and return it as bigFive with bigFiveEvidence — see big_five_inference below. This is not optional decoration: without it the persona's values are derived from cognitive traits, and that route cannot reach hedonism, power, universalism or the relatedness need, so those four stay at the 0.5 baseline. Use the trait reference matrix below to guide your inference. If the description mentions ANY disability or impairment — sensory (vision loss, colour blindness, hearing loss), physical (motor impairment, tremor), OR cognitive and attentional (ADHD, dyslexia, autism, brain fog, fatigue, low processing speed) — you MUST also include accessibilityTraits. Cognitive conditions map onto attentionSpan and processingSpeed in that same block; listing only physical and sensory examples led to ADHD personas getting no accessibility modeling at all. Without accessibilityTraits, the persona gets cognitive-only modeling and the perceptual transport layer will report near-perfect perception — which is wrong for someone with a disability. Return the traits via persona_create_submit_traits.`,
490
504
  trait_reference: traitInfo,
505
+ big_five_inference: bigFiveInferenceReference(),
506
+ // What to look for, per trait, so the caller is not guessing at
507
+ // what counts as evidence. The submit tool REJECTS a persona with
508
+ // unaccounted traits, so this is the contract rather than advice.
509
+ trait_criteria: TRAIT_INFERENCE_CRITERIA,
510
+ completeness_contract: {
511
+ rule: `All ${ALL_TRAITS.length} traits must be accounted for. Give each one a value inferred from the description, OR list it in unsupportedTraits.`,
512
+ why: "An unsupplied trait is filled from a population baseline and the result is indistinguishable from a considered value — the persona looks complete either way. Naming what the description cannot support keeps that visible.",
513
+ unsupportedTraits: "string[] — traits this description genuinely does not speak to. A legitimate answer, not a failure.",
514
+ enforcement: "persona_create_submit_traits returns an error listing the unaccounted traits and writes nothing.",
515
+ },
491
516
  ...(includeTraitReference ? {} : { trait_reference_note: "Compact form: trait names and meanings. For per-level behaviours call persona_traits_list, or pass includeTraitReference:true." }),
492
517
  accessibility_traits_reference: {
493
518
  visionLevel: "0=blind, 0.3=legally blind, 0.5=low vision (needs magnification), 0.7=mild vision loss, 1.0=sighted",
@@ -540,7 +565,23 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
540
565
  hearingLevel: z.number().min(0).max(1).optional().describe("Hearing level: 0=deaf, 1=full hearing"),
541
566
  }).optional().describe("Sensory/motor/physical traits for disability modeling. Without these, the persona gets cognitive-only modeling (no vision/motor simulation in empathy_audit)."),
542
567
  ageRange: z.string().optional().describe("Age range: '18-24', '25-45', '45-65', '65+'"),
568
+ discardBigFive: z.boolean().optional().describe("Deliberately drop an existing Big Five profile on this persona and derive values from cognitive traits instead. Without this, an existing profile is carried forward rather than silently downgraded — omitting bigFive on a rewrite used to demote the persona from thirteen value axes to nine and say nothing."),
569
+ traitProvenance: z.record(z.string(), z.enum(["supported", "asserted"])).optional().describe("Per trait: 'supported' if the description says it, 'asserted' if you decided it anyway. Both are legitimate. Supplying this is what makes the trait count mean something — '26 of 26 supplied' reads as quality whether the values were read out of the description or invented, and without this field nothing can tell those apart."),
570
+ unsupportedTraits: z.array(z.string()).optional().describe("Traits the description genuinely does not speak to. Required for any trait you are not supplying a value for — see completeness_contract. Naming them is a valid answer; omitting them is not."),
571
+ bigFive: z.object({
572
+ openness: z.number().min(0).max(1),
573
+ conscientiousness: z.number().min(0).max(1),
574
+ extraversion: z.number().min(0).max(1),
575
+ agreeableness: z.number().min(0).max(1),
576
+ neuroticism: z.number().min(0).max(1),
577
+ }).optional().describe("Big Five profile INFERRED from the description. Unlocks all 13 motivational values through published correlations instead of the 9 the cognitive-trait route reaches. Requires bigFiveEvidence."),
578
+ bigFiveEvidence: z.array(z.object({
579
+ factor: z.string(),
580
+ score: z.number().min(0).max(1),
581
+ quote: z.string().describe("The words in the description justifying this score"),
582
+ })).optional().describe("One entry per factor, quoting the description. Required whenever bigFive is supplied — an estimate nobody can check against the source is not evidence."),
543
583
  },
584
+ _meta: { ui: { resourceUri: widgetUri("persona-created") } },
544
585
  annotations: {
545
586
  title: "Submit Persona Traits",
546
587
  readOnlyHint: false,
@@ -548,9 +589,86 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
548
589
  idempotentHint: false,
549
590
  openWorldHint: false,
550
591
  },
551
- }, async ({ persona_name, traits, description, accessibilityTraits, ageRange }) => {
592
+ }, async ({ persona_name, traits, description, accessibilityTraits, ageRange, bigFive, bigFiveEvidence, unsupportedTraits, traitProvenance, discardBigFive }) => {
593
+ // Refused, not warned.
594
+ //
595
+ // A warning is read once by a model already moving to its next call, and
596
+ // the persona is written regardless -- the gap surfaces months later when
597
+ // someone audits. Five personas on this install are missing the same four
598
+ // traits because one session skipped them and nothing objected. Declining
599
+ // the write is the only version of this that changes what gets stored.
600
+ const completeness = assessTraitCompleteness(traits, unsupportedTraits, traitProvenance);
601
+ if (!completeness.ok) {
602
+ return {
603
+ content: [{ type: "text", text: JSON.stringify({
604
+ error: "incomplete_persona",
605
+ message: completeness.message,
606
+ unaccountedTraits: completeness.unaccounted,
607
+ traitCriteria: Object.fromEntries(completeness.unaccounted.map((t) => [t, TRAIT_INFERENCE_CRITERIA[t]])),
608
+ howToProceed: "Re-submit with a value for each trait the description supports, and the rest listed in unsupportedTraits. Nothing was written.",
609
+ written: false,
610
+ }, null, 2) }],
611
+ };
612
+ }
552
613
  const builtTraits = buildTraitsFromAnswers(traits);
553
- const derivedResult = deriveValuesFromTraits(builtTraits);
614
+ // Big Five, when the caller inferred one from the description.
615
+ //
616
+ // Preferred over the cognitive-trait derivation because it reaches all
617
+ // thirteen axes through published correlations instead of nine through
618
+ // our own bridge — but recorded as its OWN route, not merged into stated
619
+ // Big Five. A model reading prose and five numbers a human supplied are
620
+ // different evidence, and collapsing them would erase the distinction the
621
+ // route ordering exists to express. Rejected rather than degraded when it
622
+ // fails validation: a bad profile silently replacing a working
623
+ // trait-derivation is worse than no profile. (2026-08-01)
624
+ let bigFiveAccepted;
625
+ let bigFiveRejected;
626
+ let bigFiveUnrelated;
627
+ if (bigFive) {
628
+ const verdict = validateInferredBigFive(bigFive, bigFiveEvidence, description);
629
+ if (verdict.ok) {
630
+ bigFiveAccepted = bigFive;
631
+ bigFiveUnrelated = verdict.unrelatedQuotes;
632
+ }
633
+ else
634
+ bigFiveRejected = verdict.reason;
635
+ }
636
+ // A rewrite must not silently demote a persona's values route.
637
+ //
638
+ // Submitting without bigFive is the honest answer when the description
639
+ // does not support one -- and on an EXISTING persona that already had a
640
+ // Big Five profile, it quietly rewrote the record from `big_five` (or
641
+ // `bigfive_inferred`, thirteen axes) down to `cognitive_traits` (nine
642
+ // axes, four frozen at baseline). Following the instruction correctly
643
+ // made the persona worse, and nothing said so. Nobody would notice until
644
+ // they wondered why four values had gone flat.
645
+ //
646
+ // The existing profile is CARRIED FORWARD rather than dropped, because a
647
+ // downgrade nobody asked for is the worse error -- and it is reported
648
+ // loudly, with the traits it was inferred from now changed underneath it.
649
+ // `discardBigFive: true` is the way to actually mean the downgrade.
650
+ // (2026-08-01)
651
+ let carriedBigFive;
652
+ if (!bigFiveAccepted && !discardBigFive) {
653
+ try {
654
+ const { loadCustomPersonas } = await import("../personas.js");
655
+ const prior = loadCustomPersonas()[persona_name];
656
+ const priorDeriv = prior?.valuesDerivation;
657
+ const priorMethod = priorDeriv?.method;
658
+ const priorBigFive = prior?.bigFive;
659
+ if (priorBigFive && (priorMethod === "big_five" || priorMethod === "bigfive_inferred")) {
660
+ bigFiveAccepted = priorBigFive;
661
+ carriedBigFive = {
662
+ from: priorMethod,
663
+ note: `This persona already had a ${priorMethod} profile and this submission supplied none, which would have downgraded it to cognitive_traits — nine of thirteen value axes, with hedonism, power, universalism and the relatedness need frozen at 0.5. The existing profile was kept. It was inferred against the PREVIOUS description and traits, so if the persona has genuinely changed, re-supply bigFive with evidence from the new description. To downgrade on purpose, re-submit with discardBigFive:true.`,
664
+ };
665
+ }
666
+ }
667
+ catch { /* no prior persona; nothing to carry */ }
668
+ }
669
+ const derivedResult = bigFiveAccepted
670
+ ? deriveValuesFromBigFive(bigFiveAccepted)
671
+ : deriveValuesFromTraits(builtTraits);
554
672
  // buildTraitsFromAnswers fills every unsupplied trait from research
555
673
  // baselines, so a caller who names 3 traits gets back a complete 26-trait
556
674
  // persona with no way to tell which 23 were invented. Report both lists.
@@ -564,24 +682,57 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
564
682
  const personaObj = createCognitivePersona(persona_name, description || persona_name, builtTraits, { ageRange: ageRange || undefined });
565
683
  // Attach accessibility traits — either from explicit parameter or inferred from name/description
566
684
  let resolvedAccessibilityTraits = accessibilityTraits;
685
+ // Which keywords fired, so an inferred number can be argued with rather
686
+ // than just noticed.
687
+ let inferredFrom = [];
567
688
  if (!resolvedAccessibilityTraits || Object.keys(resolvedAccessibilityTraits).length === 0) {
568
689
  // Infer disability traits from persona name and description
569
690
  const text = `${persona_name} ${description || ''}`.toLowerCase();
570
691
  const hasVision = /vision|blind|magnif|low.?vis|sight|visual.?impair/.test(text);
571
692
  const hasMotor = /motor|tremor|parkinson|arthrit|dexterity|mobility/.test(text);
572
- const hasCognitive = /adhd|dyslexic|dyslexia|cognitive|autism|intellectual/.test(text);
573
693
  const hasHearing = /deaf|hearing|hard.?of.?hearing/.test(text);
574
694
  const hasElderly = /elderly|senior|65|70|75|80|retired|aging|aged/.test(text);
695
+ // Cognitive conditions are NOT one bucket, and treating them as one
696
+ // produced a wrong number with a confident label. ADHD, dyslexia,
697
+ // autism and intellectual disability all matched a single regex and all
698
+ // came out as processingSpeed 0.4 / attentionSpan 0.3 -- so an ADHD
699
+ // persona whose previous record said processingSpeed 0.9 was rewritten
700
+ // to 0.4 by a keyword. ADHD is an attention-regulation condition; it is
701
+ // not a claim about how fast someone processes information, and plenty
702
+ // of people with ADHD process quickly. Autism is not a speed claim
703
+ // either. Dyslexia slows READING specifically. Only the conditions that
704
+ // genuinely bear on speed set speed. (2026-08-01)
705
+ const hasAdhd = /adhd|add\b|attention deficit|executive function/.test(text);
706
+ const hasDyslexia = /dyslexi/.test(text);
707
+ const hasAutism = /autis|asperger|neurodiver/.test(text);
708
+ const hasSlowProcessing = /intellectual disab|brain fog|processing (speed )?(disorder|difficult|impair)|slow (to )?process|cognitive impair|dementia|traumatic brain/.test(text);
709
+ const hasCognitive = hasAdhd || hasDyslexia || hasAutism || hasSlowProcessing;
575
710
  if (hasVision || hasMotor || hasCognitive || hasHearing || hasElderly) {
711
+ // Left undefined where the description supports no claim, so the
712
+ // block below falls back to the persona's own traits rather than to a
713
+ // number this inference invented.
714
+ const speed = hasSlowProcessing ? 0.35
715
+ : hasDyslexia ? 0.6
716
+ : hasElderly ? 0.5
717
+ : undefined;
718
+ const attention = hasAdhd ? 0.3
719
+ : hasAutism ? undefined
720
+ : undefined;
576
721
  resolvedAccessibilityTraits = {
577
722
  visionLevel: hasVision ? 0.4 : hasElderly ? 0.6 : 1.0,
578
723
  contrastSensitivity: hasVision ? 0.3 : hasElderly ? 0.5 : 1.0,
579
724
  motorControl: hasMotor ? 0.3 : hasElderly ? 0.6 : 1.0,
580
725
  tremor: hasMotor,
581
- processingSpeed: hasCognitive ? 0.4 : hasElderly ? 0.5 : builtTraits.comprehension ?? 0.8,
582
- attentionSpan: hasCognitive ? 0.3 : builtTraits.patience ?? 0.7,
726
+ ...(speed !== undefined ? { processingSpeed: speed } : {}),
727
+ ...(attention !== undefined ? { attentionSpan: attention } : {}),
583
728
  hearingLevel: hasHearing ? 0.2 : 1.0,
584
729
  };
730
+ inferredFrom = [
731
+ hasVision && "vision", hasMotor && "motor", hasHearing && "hearing", hasElderly && "age",
732
+ hasAdhd && "ADHD (attention only — not a processing-speed claim)",
733
+ hasDyslexia && "dyslexia (reading speed)", hasAutism && "autism (no speed or attention claim inferred)",
734
+ hasSlowProcessing && "processing-speed condition",
735
+ ].filter(Boolean);
585
736
  }
586
737
  }
587
738
  if (resolvedAccessibilityTraits && Object.keys(resolvedAccessibilityTraits).length > 0) {
@@ -597,9 +748,53 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
597
748
  fatigueSusceptibility: (resolvedAccessibilityTraits.visionLevel ?? 1.0) < 0.7 || (resolvedAccessibilityTraits.motorControl ?? 1.0) < 0.7 ? 0.7 : 0.3,
598
749
  };
599
750
  }
751
+ // The route that produced the values, recorded ON the persona so every
752
+ // reader gets the same answer. bigFive is stored too, so the derivation
753
+ // can be re-run and checked rather than taken on trust.
754
+ personaObj.valuesDerivation = bigFiveAccepted
755
+ ? { method: "bigfive_inferred", squash: "tanh", precision: 3,
756
+ inferredFrom: "description", evidence: bigFiveEvidence ?? [] }
757
+ : { method: "cognitive_traits", squash: "tanh", precision: 3 };
758
+ if (bigFiveAccepted) {
759
+ personaObj.bigFive = bigFiveAccepted;
760
+ }
761
+ personaObj.schwartzValues = derivedResult.values;
762
+ // Kept ON the persona: which traits are baseline-by-declaration rather
763
+ // than by omission. Without it the distinction is lost the moment the
764
+ // response is discarded.
765
+ if (unsupportedTraits?.length) {
766
+ personaObj.unsupportedTraits = unsupportedTraits;
767
+ }
768
+ // The attention pattern createCognitivePersona stamps from its template is
769
+ // NOT a declaration.
770
+ //
771
+ // A recreate flipped a stored persona from "thorough" to "skim" while still
772
+ // reporting attentionPatternSource: "declared" -- nobody declared either
773
+ // value. The template's default arrived through humanBehavior, which is
774
+ // exactly where a real declaration lives, so the profile could not tell
775
+ // them apart. Dropping it lets the derivation run and report itself as
776
+ // derived; a caller who wants a declaration sets it through
777
+ // persona_update, which is the surface that takes one. (2026-08-01)
778
+ const hb = personaObj.humanBehavior;
779
+ if (hb?.attention && "pattern" in hb.attention)
780
+ delete hb.attention.pattern;
600
781
  registerPersonas([personaObj]);
782
+ // Persist to disk. registerPersonas writes to an in-memory map that dies
783
+ // with the process, so a creation whose CMS write also failed left the
784
+ // persona existing NOWHERE -- while the response said registered: true.
785
+ // "Registered" described RAM and read as "saved". (2026-08-01)
786
+ let savedToFile;
787
+ let fileSaveError;
788
+ try {
789
+ const { saveCustomPersona } = await import("../personas.js");
790
+ savedToFile = saveCustomPersona(personaObj);
791
+ }
792
+ catch (e) {
793
+ fileSaveError = e?.message ?? String(e);
794
+ }
601
795
  // Also save to CMS if an API key is configured (makes it persistent across sessions)
602
796
  let savedToCms = false;
797
+ let cmsError;
603
798
  try {
604
799
  const { getSessionApiKey } = await import("./base/cognitive-tools.js");
605
800
  const apiKey = getSessionApiKey();
@@ -618,6 +813,13 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
618
813
  accessibility_traits: resolvedAccessibilityTraits || null,
619
814
  age_range: ageRange || personaObj.demographics?.age_range || null,
620
815
  source: "mcp",
816
+ // Sent so the row is not re-labelled by guesswork later. A stored
817
+ // block with no provenance reads as `stated`, which would claim a
818
+ // human wrote numbers this route inferred.
819
+ values_derivation: bigFiveAccepted
820
+ ? { method: "bigfive_inferred", squash: "tanh", precision: 3,
821
+ inferredFrom: "description", evidence: bigFiveEvidence ?? [] }
822
+ : { method: "cognitive_traits", squash: "tanh", precision: 3 },
621
823
  }),
622
824
  });
623
825
  if (res.ok) {
@@ -626,10 +828,20 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
626
828
  }
627
829
  else {
628
830
  const errBody = await res.text().catch(() => "");
831
+ // The REASON travels to the caller. "savedToCms: false" alone does
832
+ // not distinguish an unreachable CMS from a credential the CMS
833
+ // will never accept -- and the second is the actual situation on
834
+ // an OAuth connector, where the bearer forwarded here is an OAuth
835
+ // token and /api/personas only accepts cbk_ account keys. Those
836
+ // need different responses from whoever reads this.
837
+ cmsError = res.status === 401 || res.status === 403
838
+ ? `CMS rejected the credential (${res.status}). The session bearer is forwarded as-is, so an OAuth connector token reaches an endpoint that only accepts cbk_ account keys. Set an account key with set_api_key to make CMS writes succeed.`
839
+ : `CMS returned ${res.status}: ${errBody.slice(0, 120)}`;
629
840
  console.warn(`[persona] CMS save failed (${res.status}): ${errBody.slice(0, 100)}`);
630
841
  }
631
842
  }
632
843
  else {
844
+ cmsError = "no API key in this session — nothing was sent to the CMS";
633
845
  console.warn(`[persona] No API key in session — cannot save to CMS`);
634
846
  }
635
847
  }
@@ -640,13 +852,42 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
640
852
  content: [{
641
853
  type: "text",
642
854
  text: JSON.stringify({
643
- success: true,
855
+ // success is computed below from what actually persisted.
644
856
  persona_name,
645
857
  description,
646
858
  traits: builtTraits,
647
859
  providedTraits,
648
860
  defaultedTraits,
649
- traitCompleteness: `${providedTraits.length} of ${allTraitNames.length} supplied; ${defaultedTraits.length} filled from research baselines`,
861
+ // The old line was `${n} of ${n} supplied; 0 filled from research
862
+ // baselines`, and it read as a quality signal no matter how the
863
+ // numbers were arrived at -- a caller who invented six of them got
864
+ // the same clean sentence as one who read all 26 out of the text.
865
+ // The count measures ACCOUNTING. Whether the description supports a
866
+ // value is a different question, and the sentence has to stop
867
+ // answering it by implication.
868
+ traitCompleteness: (() => {
869
+ const base = `${providedTraits.length} of ${allTraitNames.length} supplied; ${defaultedTraits.length} filled from research baselines`;
870
+ const a = completeness.asserted?.length ?? 0;
871
+ const u = completeness.unlabelled?.length ?? 0;
872
+ if (a || u) {
873
+ const parts = [
874
+ a ? `${a} asserted by the caller rather than read from the description` : "",
875
+ u ? `${u} with no provenance stated` : "",
876
+ ].filter(Boolean);
877
+ return `${base}. ${parts.join("; ")}. This counts accounting, not support — a supplied value can still be an invention.`;
878
+ }
879
+ return `${base}; all supplied values marked as supported by the description`;
880
+ })(),
881
+ ...(completeness.asserted?.length ? { assertedTraits: completeness.asserted } : {}),
882
+ ...(completeness.unlabelled?.length ? {
883
+ unlabelledTraits: completeness.unlabelled,
884
+ provenanceNote: "These were supplied with no traitProvenance entry, so whether the description supports them is unknown — not assumed. Pass traitProvenance to say which values you read out of the text and which you decided.",
885
+ } : {}),
886
+ ...(carriedBigFive ? { bigFiveCarriedForward: carriedBigFive } : {}),
887
+ ...(bigFiveUnrelated?.length ? {
888
+ bigFiveQuotesUnrelated: bigFiveUnrelated,
889
+ bigFiveQuoteNote: "These quotes are in the description but contain no word bearing on the factor they were cited for. The profile was accepted — the check is a heuristic — but a quote that is not about the factor is not evidence for it.",
890
+ } : {}),
650
891
  // A misspelled trait name was previously accepted in silence and had
651
892
  // no effect, since buildTraitsFromAnswers only applies keys already
652
893
  // present in the trait table.
@@ -655,15 +896,117 @@ Phase: VALUES (${session.currentIndex + 1} of ${session.valueQuestions.length})`
655
896
  warning: `Ignored ${unknownTraits.length} unrecognized trait name(s): ${unknownTraits.join(", ")}. These had no effect.`,
656
897
  } : {}),
657
898
  values: derivedResult.values,
658
- derivations: derivedResult.derivations,
899
+ // The two routes report their working differently -- the trait
900
+ // derivation lists `derivations`, the Big Five one `contributions`.
901
+ // Both are surfaced under their own name so a caller can tell which
902
+ // route ran from the response alone, without inferring it from
903
+ // which key happens to be present.
904
+ ...("derivations" in derivedResult
905
+ ? { derivations: derivedResult.derivations }
906
+ : {}),
907
+ ...("contributions" in derivedResult
908
+ ? { bigFiveContributions: derivedResult.contributions,
909
+ hypothesisAxes: derivedResult.hypothesisAxes }
910
+ : {}),
911
+ valuesRoute: bigFiveAccepted ? "bigfive_inferred" : "cognitive_traits",
912
+ traitSummary: summariseTraits(builtTraits),
913
+ // Named at creation, which is the only moment the distinction is
914
+ // free: a declared-unsupported trait sits at a population baseline
915
+ // and is indistinguishable from a considered value in every later
916
+ // view of this persona.
917
+ ...(unsupportedTraits?.length ? {
918
+ unsupportedTraits,
919
+ unsupportedDetail: unsupportedTraits.map((t) => ({
920
+ trait: t, value: builtTraits[t],
921
+ source: "population baseline — the description did not speak to this",
922
+ })),
923
+ } : {}),
924
+ ...(bigFiveAccepted ? { bigFive: bigFiveAccepted, bigFiveEvidence: bigFiveEvidence ?? [] } : {}),
925
+ // A rejected profile is REPORTED, never swallowed. Silently falling
926
+ // back would leave the caller believing the Big Five route ran.
927
+ ...(bigFiveRejected ? {
928
+ bigFiveRejected,
929
+ bigFiveRejectedNote: "The inferred profile was not stored and values were derived from cognitive traits instead, which cannot reach hedonism, power, universalism or the relatedness need. Fix the profile and resubmit to reach all thirteen.",
930
+ } : {}),
659
931
  accessibilityTraits: resolvedAccessibilityTraits || null,
660
- hasDisabilityModeling: !!(resolvedAccessibilityTraits && Object.keys(resolvedAccessibilityTraits).length > 0),
932
+ // Presence of the block is not impairment. A persona with vision,
933
+ // motor and hearing all at 1.0 has accessibility traits and no
934
+ // disability, and flagging it flipped an instruction claiming the
935
+ // perceptual layer "will simulate sensory impairment" -- a false
936
+ // statement about what the run does. ADHD is cognitive; supplying
937
+ // ceiling sensory values must not read as a sensory disability.
938
+ hasDisabilityModeling: (() => {
939
+ const a = resolvedAccessibilityTraits;
940
+ if (!a)
941
+ return false;
942
+ const below = (k, t = 0.95) => typeof a[k] === "number" && a[k] < t;
943
+ return below("visionLevel") || below("motorControl") || below("hearingLevel")
944
+ || below("contrastSensitivity") || below("processingSpeed", 0.6)
945
+ || below("attentionSpan", 0.6) || a.tremor === true || !!a.colorBlindness;
946
+ })(),
661
947
  disabilityTraitsSource: accessibilityTraits ? 'explicit' : resolvedAccessibilityTraits ? 'inferred_from_description' : 'none',
948
+ ...(inferredFrom.length ? {
949
+ disabilityInferredFrom: inferredFrom,
950
+ disabilityInferenceNote: "Keyword inference, not a diagnosis. It sets a trait only where the condition bears on it — a condition named in the description does not license a number for every accessibility trait. Pass accessibilityTraits explicitly to override any of it.",
951
+ } : {}),
662
952
  ageRange: ageRange || personaObj.demographics?.age_range || null,
663
953
  registered: true,
954
+ // Both stores named, and whether the persona SURVIVES stated
955
+ // outright. savedToCms alone read as a minor caveat when combined
956
+ // with registered:true it meant the persona was in RAM and nowhere
957
+ // else.
664
958
  savedToCms,
959
+ savedToFile: savedToFile ? true : false,
960
+ ...(savedToFile ? { filePath: savedToFile } : {}),
961
+ ...(fileSaveError ? { fileSaveError } : {}),
962
+ // Agentic callers branch on success. A partial write that reports
963
+ // success is a write nobody retries, which is how the two stores
964
+ // drift apart in the first place -- so a half-completed write is
965
+ // reported as a failure with the half that landed named, rather
966
+ // than as a success with a caveat.
967
+ success: !!savedToFile && savedToCms,
968
+ ...(savedToFile && !savedToCms ? { partialWrite: true } : {}),
969
+ writeStatus: { fileStore: savedToFile ? "ok" : "failed", cms: savedToCms ? "ok" : "failed" },
970
+ ...(cmsError ? { cmsError } : {}),
971
+ persisted: !!savedToFile || savedToCms,
972
+ persistenceNote: savedToFile && savedToCms
973
+ ? "Written to the file store and the CMS."
974
+ : savedToFile
975
+ ? "Written to the file store. The CMS copy was NOT written, so the two stores disagree — the persona survives restarts but will not appear for other clients of the CMS."
976
+ : savedToCms
977
+ ? "Written to the CMS only. The file store did NOT take it, so tools reading the file store will not find this persona."
978
+ : "NOT PERSISTED. The persona exists only in this server process's memory and will be gone when it restarts. `registered: true` above means RAM, not storage.",
665
979
  instruction: resolvedAccessibilityTraits
666
- ? `Persona "${persona_name}" created with disability modeling (vision=${resolvedAccessibilityTraits.visionLevel ?? 'default'}, motor=${resolvedAccessibilityTraits.motorControl ?? 'default'}). The perceptual transport layer will simulate sensory impairment. Use with empathy_audit for accurate disability scoring.`
980
+ // Names the modalities actually impaired instead of claiming
981
+ // "sensory impairment" wholesale. An ADHD persona with vision,
982
+ // motor and hearing at 1.0 and a short attention span has
983
+ // something real to model -- but nothing SENSORY, and the old
984
+ // text promised a simulation that would not happen. A claim about
985
+ // what a run will do has to match what the numbers support.
986
+ ? (() => {
987
+ const a = resolvedAccessibilityTraits;
988
+ const impaired = [];
989
+ const lo = (k, label, t = 0.95) => {
990
+ if (typeof a[k] === "number" && a[k] < t)
991
+ impaired.push(`${label} ${a[k]}`);
992
+ };
993
+ lo("visionLevel", "vision");
994
+ lo("contrastSensitivity", "contrast");
995
+ lo("motorControl", "motor");
996
+ lo("hearingLevel", "hearing");
997
+ lo("processingSpeed", "processing speed", 0.6);
998
+ lo("attentionSpan", "attention span", 0.6);
999
+ if (a.tremor === true)
1000
+ impaired.push("tremor");
1001
+ if (a.colorBlindness)
1002
+ impaired.push(`colour vision (${String(a.colorBlindness)})`);
1003
+ const sensory = impaired.some((x) => /^(vision|contrast|hearing|colour)/.test(x));
1004
+ return `Persona "${persona_name}" created with accessibility modeling on: ${impaired.join(", ")}. `
1005
+ + (sensory
1006
+ ? "The perceptual transport layer will simulate the sensory limits above. "
1007
+ : "No SENSORY channel is impaired, so perception is simulated as intact; the limits above are cognitive and motor. ")
1008
+ + "Use with empathy_audit for disability scoring.`".slice(0, -1);
1009
+ })()
667
1010
  : `Persona "${persona_name}" created with cognitive-only modeling. For disability-specific scoring (vision impairment, motor difficulty, etc.), recreate with accessibilityTraits parameter.`,
668
1011
  usage_example: {
669
1012
  tool: "empathy_audit",